diff --git a/.dockerignore b/.dockerignore index 28c6753f5..e05914176 100644 --- a/.dockerignore +++ b/.dockerignore @@ -19,7 +19,11 @@ Dockerfile .git/ORIG_HEAD .git/packed-refs .git/refs/remotes/ +.git/rr-cache/ .gitignore settings.json src/node_modules +admin/node_modules +ui/node_modules +node_modules diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..f521471dc --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +end_of_line = lf +# editorconfig-tools is unable to ignore longs strings or urls +max_line_length = off + +[CHANGELOG.md] +indent_size = 4 + +[*.bat] +end_of_line = crlf diff --git a/.env.default b/.env.default new file mode 100644 index 000000000..e9b560b72 --- /dev/null +++ b/.env.default @@ -0,0 +1,18 @@ +# Please copy and rename this file. +# +# !Attention! +# Always ensure to load the env variables in every terminal session. +# Otherwise the env variables will not be available + +DOCKER_COMPOSE_APP_PORT_PUBLISHED=9001 +DOCKER_COMPOSE_APP_PORT_TARGET=9001 + +# IMPORTANT: When the env var DEFAULT_PAD_TEXT is unset or empty, then the pad is not established (not the landing page). +# The env var DEFAULT_PAD_TEXT seems to be mandatory in the latest version of etherpad. +DOCKER_COMPOSE_APP_DEV_ENV_DEFAULT_PAD_TEXT="Welcome to etherpad" + +DOCKER_COMPOSE_APP_ADMIN_PASSWORD= + +DOCKER_COMPOSE_POSTGRES_DATABASE=db +DOCKER_COMPOSE_POSTGRES_PASSWORD=etherpad-lite-password +DOCKER_COMPOSE_POSTGRES_USER=etherpad-lite-user diff --git a/.env.dev.default b/.env.dev.default new file mode 100644 index 000000000..b78b5599a --- /dev/null +++ b/.env.dev.default @@ -0,0 +1,18 @@ +# Please copy and rename this file. +# +# !Attention! +# Always ensure to load the env variables in every terminal session. +# Otherwise the env variables will not be available + +DOCKER_COMPOSE_APP_DEV_PORT_PUBLISHED=9001 +DOCKER_COMPOSE_APP_DEV_PORT_TARGET=9001 + +# IMPORTANT: When the env var DEFAULT_PAD_TEXT is unset or empty, then the pad is not established (not the landing page). +# The env var DEFAULT_PAD_TEXT seems to be mandatory in the latest version of etherpad. +DOCKER_COMPOSE_APP_DEV_ENV_DEFAULT_PAD_TEXT="Welcome to etherpad" + +DOCKER_COMPOSE_APP_DEV_ADMIN_PASSWORD= + +DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_DATABASE=db +DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PASSWORD=etherpad-lite-password +DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_USER=etherpad-lite-user \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 4adcdb102..041f7c1a9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,5 +1,3 @@ -IMPORTANT: Please disable plugins prior to posting a bug report. If you have a problem with a plugin please post on the plugin repository. Thanks! - --- name: Bug report about: Create a report to help us improve @@ -9,6 +7,8 @@ assignees: '' --- + + **Describe the bug** A clear and concise description of what the bug is. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..1e7ac79ed --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + versioning-strategy: "increase" + open-pull-requests-limit: 30 + groups: + dev-dependencies: + dependency-type: "development" \ No newline at end of file diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index ec8b0859e..000000000 --- a/.github/stale.yml +++ /dev/null @@ -1,24 +0,0 @@ -# Number of days of inactivity before an issue becomes stale -daysUntilStale: 60 -# Number of days of inactivity before a stale issue is closed -daysUntilClose: 7 -# Issues with these labels will never be considered stale -exemptLabels: - - pinned - - security - - Bug - - Serious Bug - - Minor bug - - Black hole bug - - Special case Bug - - Upstream bug - - Feature Request -# Label to use when marking an issue as stale -staleLabel: wontfix -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. Thank you - for your contributions. -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: false diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml index 9e33cbe10..3c6e63bb4 100644 --- a/.github/workflows/backend-tests.yml +++ b/.github/workflows/backend-tests.yml @@ -1,181 +1,290 @@ name: "Backend tests" # any branch is useful for testing before a PR is submitted -on: [push, pull_request] +on: + push: + paths-ignore: + - "doc/**" + pull_request: + paths-ignore: + - "doc/**" + +permissions: + contents: read jobs: withoutpluginsLinux: # run on pushes to any branch # run on PRs from external forks if: | - (github.event_name != 'pull_request') - || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) + (github.event_name != 'pull_request') + || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) name: Linux without plugins runs-on: ubuntu-latest - strategy: fail-fast: false matrix: - node: [12, 14, 16] - + node: [20, 22, 23] steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node }} - - - name: Install libreoffice - run: | - sudo add-apt-repository -y ppa:libreoffice/ppa - sudo apt update - sudo apt install -y --no-install-recommends libreoffice libreoffice-pdfimport - - - name: Install all dependencies and symlink for ep_etherpad-lite - run: src/bin/installDeps.sh - - - name: Run the backend tests - run: cd src && npm test + - + name: Checkout repository + uses: actions/checkout@v4 + - + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 9.0.4 + run_install: false + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Only install direct dependencies + run: pnpm config set auto-install-peers false + - + name: Install libreoffice + uses: awalsh128/cache-apt-pkgs-action@v1.5.0 + with: + packages: libreoffice libreoffice-pdfimport + version: 1.0 + - + name: Install all dependencies and symlink for ep_etherpad-lite + run: bin/installDeps.sh + - name: Install admin ui + working-directory: admin + run: pnpm install + - name: Build admin ui + working-directory: admin + run: pnpm build + - + name: Run the backend tests + run: pnpm test + - name: Run the new vitest tests + working-directory: src + run: pnpm run test:vitest withpluginsLinux: # run on pushes to any branch # run on PRs from external forks if: | - (github.event_name != 'pull_request') - || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) + (github.event_name != 'pull_request') + || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) name: Linux with Plugins runs-on: ubuntu-latest - strategy: fail-fast: false matrix: - node: [12, 14, 16] - + node: [20, 22, 23] steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node }} - - - name: Install libreoffice - run: | - sudo add-apt-repository -y ppa:libreoffice/ppa - sudo apt update - sudo apt install -y --no-install-recommends libreoffice libreoffice-pdfimport - - - name: Install Etherpad plugins - # The --legacy-peer-deps flag is required to work around a bug in npm v7: - # https://github.com/npm/cli/issues/2199 - run: > - npm install --no-save --legacy-peer-deps - ep_align - ep_author_hover - ep_cursortrace - ep_font_size - ep_hash_auth - ep_headings2 - ep_image_upload - ep_markdown - ep_readonly_guest - ep_set_title_on_pad - ep_spellcheck - ep_subscript_and_superscript - ep_table_of_contents - - # This must be run after installing the plugins, otherwise npm will try to - # hoist common dependencies by removing them from src/node_modules and - # installing them in the top-level node_modules. As of v6.14.10, npm's hoist - # logic appears to be buggy, because it sometimes removes dependencies from - # src/node_modules but fails to add them to the top-level node_modules. Even - # if npm correctly hoists the dependencies, the hoisting seems to confuse - # tools such as `npm outdated`, `npm update`, and some ESLint rules. - - name: Install all dependencies and symlink for ep_etherpad-lite - run: src/bin/installDeps.sh - - - name: Run the backend tests - run: cd src && npm test + - + name: Checkout repository + uses: actions/checkout@v4 + - + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 9.0.4 + run_install: false + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Only install direct dependencies + run: pnpm config set auto-install-peers false + - + name: Install libreoffice + uses: awalsh128/cache-apt-pkgs-action@v1.5.0 + with: + packages: libreoffice libreoffice-pdfimport + version: 1.0 + - + name: Install all dependencies and symlink for ep_etherpad-lite + run: bin/installDeps.sh + - name: Install admin ui + working-directory: admin + run: pnpm install + - name: Build admin ui + working-directory: admin + run: pnpm build + - + name: Install Etherpad plugins + run: > + pnpm install --workspace-root + ep_align + ep_author_hover + ep_cursortrace + ep_font_size + ep_hash_auth + ep_headings2 + ep_markdown + ep_readonly_guest + ep_set_title_on_pad + ep_spellcheck + ep_subscript_and_superscript + ep_table_of_contents + - + name: Run the backend tests + run: pnpm test + - name: Run the new vitest tests + working-directory: src + run: pnpm run test:vitest withoutpluginsWindows: # run on pushes to any branch # run on PRs from external forks if: | - (github.event_name != 'pull_request') - || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) + (github.event_name != 'pull_request') + || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) name: Windows without plugins runs-on: windows-latest - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - uses: actions/setup-node@v2 - with: - node-version: 12 - - - name: Install all dependencies and symlink for ep_etherpad-lite - run: src/bin/installOnWindows.bat - - - name: Fix up the settings.json - run: | - powershell -Command "(gc settings.json.template) -replace '\"max\": 10', '\"max\": 10000' | Out-File -encoding ASCII settings.json.holder" - powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json" - - - name: Run the backend tests - run: cd src && npm test + - + name: Checkout repository + uses: actions/checkout@v4 + - + uses: actions/setup-node@v4 + with: + node-version: 20 + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 9.0.4 + run_install: false + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Only install direct dependencies + run: pnpm config set auto-install-peers false + - + name: Install all dependencies and symlink for ep_etherpad-lite + run: bin/installOnWindows.bat + - name: Install admin ui + working-directory: admin + run: pnpm install + - name: Build admin ui + working-directory: admin + run: pnpm build + - + name: Fix up the settings.json + run: | + powershell -Command "(gc settings.json.template) -replace '\"max\": 10', '\"max\": 10000' | Out-File -encoding ASCII settings.json.holder" + powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json" + - + name: Run the backend tests + working-directory: src + run: pnpm test + - name: Run the new vitest tests + working-directory: src + run: pnpm run test:vitest withpluginsWindows: # run on pushes to any branch # run on PRs from external forks if: | - (github.event_name != 'pull_request') - || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) + (github.event_name != 'pull_request') + || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) name: Windows with Plugins runs-on: windows-latest steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - uses: actions/setup-node@v2 - with: - node-version: 12 - - - name: Install Etherpad plugins - # The --legacy-peer-deps flag is required to work around a bug in npm v7: - # https://github.com/npm/cli/issues/2199 - run: > - npm install --no-save --legacy-peer-deps - ep_align - ep_author_hover - ep_cursortrace - ep_font_size - ep_hash_auth - ep_headings2 - ep_image_upload - ep_markdown - ep_readonly_guest - ep_set_title_on_pad - ep_spellcheck - ep_subscript_and_superscript - ep_table_of_contents - - # This must be run after installing the plugins, otherwise npm will try to - # hoist common dependencies by removing them from src/node_modules and - # installing them in the top-level node_modules. As of v6.14.10, npm's hoist - # logic appears to be buggy, because it sometimes removes dependencies from - # src/node_modules but fails to add them to the top-level node_modules. Even - # if npm correctly hoists the dependencies, the hoisting seems to confuse - # tools such as `npm outdated`, `npm update`, and some ESLint rules. - - name: Install all dependencies and symlink for ep_etherpad-lite - run: src/bin/installOnWindows.bat - - - name: Fix up the settings.json - run: | - powershell -Command "(gc settings.json.template) -replace '\"max\": 10', '\"max\": 10000' | Out-File -encoding ASCII settings.json.holder" - powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json" - - - name: Run the backend tests - run: cd src && npm test + - + name: Checkout repository + uses: actions/checkout@v4 + - + uses: actions/setup-node@v4 + with: + node-version: 22 + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 9.0.4 + run_install: false + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Only install direct dependencies + run: pnpm config set auto-install-peers false + - name: Install admin ui + working-directory: admin + run: pnpm install + - name: Build admin ui + working-directory: admin + run: pnpm build + - + name: Install Etherpad plugins + # The --legacy-peer-deps flag is required to work around a bug in npm + # v7: https://github.com/npm/cli/issues/2199 + run: > + pnpm install --workspace-root + ep_align + ep_author_hover + ep_cursortrace + ep_font_size + ep_hash_auth + ep_headings2 + ep_markdown + ep_readonly_guest + ep_set_title_on_pad + ep_spellcheck + ep_subscript_and_superscript + ep_table_of_contents + # Etherpad core dependencies must be installed after installing the + # plugin's dependencies, otherwise npm will try to hoist common + # dependencies by removing them from src/node_modules and installing them + # in the top-level node_modules. As of v6.14.10, npm's hoist logic appears + # to be buggy, because it sometimes removes dependencies from + # src/node_modules but fails to add them to the top-level node_modules. + # Even if npm correctly hoists the dependencies, the hoisting seems to + # confuse tools such as `npm outdated`, `npm update`, and some ESLint + # rules. + - + name: Install all dependencies and symlink for ep_etherpad-lite + run: bin/installOnWindows.bat + - + name: Fix up the settings.json + run: | + powershell -Command "(gc settings.json.template) -replace '\"max\": 10', '\"max\": 10000' | Out-File -encoding ASCII settings.json.holder" + powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json" + - + name: Run the backend tests + working-directory: src + run: pnpm test + - name: Run the new vitest tests + working-directory: src + run: pnpm run test:vitest diff --git a/.github/workflows/build-and-deploy-docs.yml b/.github/workflows/build-and-deploy-docs.yml new file mode 100644 index 000000000..3134de22a --- /dev/null +++ b/.github/workflows/build-and-deploy-docs.yml @@ -0,0 +1,70 @@ +# Workflow for deploying static content to GitHub Pages +name: Deploy Docs to GitHub Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["develop"] + paths: + - doc/** # Only run workflow when changes are made to the doc directory + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + packages: read + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Pages + uses: actions/configure-pages@v5 + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 9.0.4 + run_install: false + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Only install direct dependencies + run: pnpm config set auto-install-peers false + - name: Install dependencies + run: pnpm install + - name: Build app + working-directory: doc + run: pnpm run docs:build + env: + COMMIT_REF: ${{ github.sha }} + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload entire repository + path: './doc/.vitepress/dist' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index f3b1cf2c2..94dd1ac75 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -6,49 +6,41 @@ on: pull_request: # The branches below must be a subset of the branches above branches: [develop] + paths-ignore: + - 'doc/**' schedule: - cron: '0 13 * * 1' +permissions: + contents: read + jobs: analyze: + permissions: + actions: read # for github/codeql-action/init to get workflow details + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/autobuild to send a status report name: Analyze runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v2 - with: - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. - fetch-depth: 2 - - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - # Override language selection by uncommenting this and choosing your languages - # with: - # languages: go, javascript, csharp, python, cpp, java - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + - + name: Checkout repository + uses: actions/checkout@v4 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - + run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + - + name: Initialize CodeQL + uses: github/codeql-action/init@v3 + - + name: Autobuild + uses: github/codeql-action/autobuild@v3 + - + name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 000000000..9a9dcfebb --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,20 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Reqest, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: 'Checkout Repository' + uses: actions/checkout@v4 + - name: 'Dependency Review' + uses: actions/dependency-review-action@v4 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 000000000..313d48fc6 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,144 @@ +name: Docker +on: + pull_request: + paths-ignore: + - 'doc/**' + push: + branches: + - 'develop' + paths-ignore: + - 'doc/**' + tags: + - 'v?[0-9]+.[0-9]+.[0-9]+' +env: + TEST_TAG: etherpad/etherpad:test +permissions: + contents: read + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - + name: Check out + uses: actions/checkout@v4 + with: + path: etherpad + + - + name: Set up QEMU + if: github.event_name == 'push' + uses: docker/setup-qemu-action@v3 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - + name: Build and export to Docker + uses: docker/build-push-action@v6 + with: + context: ./etherpad + target: production + load: true + tags: ${{ env.TEST_TAG }} + cache-from: type=gha + cache-to: type=gha,mode=max + - + name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 9.0.4 + run_install: false + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - + name: Test + working-directory: etherpad + run: | + docker run --rm -d -p 9001:9001 --name test ${{ env.TEST_TAG }} + ./bin/installDeps.sh + docker logs -f test & + while true; do + echo "Waiting for Docker container to start..." + status=$(docker container inspect -f '{{.State.Health.Status}}' test) || exit 1 + case ${status} in + healthy) break;; + starting) sleep 2;; + *) printf %s\\n "unexpected status: ${status}" >&2; exit 1;; + esac + done + (cd src && pnpm run test-container) + git clean -dxf . + - + name: Docker meta + if: github.event_name == 'push' + id: meta + uses: docker/metadata-action@v5 + with: + images: etherpad/etherpad + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + - + name: Log in to Docker Hub + if: github.event_name == 'push' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Build and push + id: build-docker + if: github.event_name == 'push' + uses: docker/build-push-action@v6 + with: + context: ./etherpad + target: production + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + - name: Update repo description + uses: peter-evans/dockerhub-description@v4 + if: github.ref == 'refs/heads/master' + with: + readme-filepath: ./etherpad/README.md + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + repository: etherpad/etherpad + enable-url-completion: true + - name: Check out + if: github.event_name == 'push' && github.ref == 'refs/heads/develop' + uses: actions/checkout@v4 + with: + path: ether-charts + repository: ether/ether-charts + token: ${{ secrets.ETHER_CHART_TOKEN }} + - name: Update tag in values-dev.yaml + if: success() && github.ref == 'refs/heads/develop' + working-directory: ether-charts + run: | + sed -i 's/tag: ".*"/tag: "${{ steps.build-docker.outputs.digest }}"/' values-dev.yaml + - name: Commit and push changes + working-directory: ether-charts + if: success() && github.ref == 'refs/heads/develop' + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + git add values-dev.yaml + git commit -m 'Update develop image tag' + git push diff --git a/.github/workflows/dockerfile.yml b/.github/workflows/dockerfile.yml deleted file mode 100644 index 5f8384705..000000000 --- a/.github/workflows/dockerfile.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: "Dockerfile" - -# any branch is useful for testing before a PR is submitted -on: [push, pull_request] - -jobs: - dockerfile: - # run on pushes to any branch - # run on PRs from external forks - if: | - (github.event_name != 'pull_request') - || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) - name: build image and run connectivity test - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: docker build - run: | - docker build -t etherpad:test . - docker run -d -p 9001:9001 etherpad:test - ./src/bin/installDeps.sh - sleep 3 # delay for startup? - cd src && npm run test-container diff --git a/.github/workflows/frontend-admin-tests.yml b/.github/workflows/frontend-admin-tests.yml index 5fb0f39c2..4963fac24 100644 --- a/.github/workflows/frontend-admin-tests.yml +++ b/.github/workflows/frontend-admin-tests.yml @@ -1,7 +1,13 @@ # Leave the powered by Sauce Labs bit in as this means we get additional concurrency name: "Frontend admin tests powered by Sauce Labs" -on: [push] +on: + push: + paths-ignore: + - 'doc/**' + +permissions: + contents: read # to fetch code (actions/checkout) jobs: withplugins: @@ -11,71 +17,137 @@ jobs: strategy: fail-fast: false matrix: - node: [12, 14, 16] + node: [20, 22, 23] steps: - - name: Generate Sauce Labs strings - id: sauce_strings - run: | - printf %s\\n '::set-output name=name::${{ github.workflow }} - ${{ github.job }} - Node ${{ matrix.node }}' - printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}-node${{ matrix.node }}' - - - name: Checkout repository - uses: actions/checkout@v2 - - - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node }} - - - name: Install etherpad plugins - # We intentionally install an old ep_align version to test upgrades to the minor version number. - # The --legacy-peer-deps flag is required to work around a bug in npm v7: - # https://github.com/npm/cli/issues/2199 - run: npm install --no-save --legacy-peer-deps ep_align@0.2.27 - - # This must be run after installing the plugins, otherwise npm will try to - # hoist common dependencies by removing them from src/node_modules and - # installing them in the top-level node_modules. As of v6.14.10, npm's hoist - # logic appears to be buggy, because it sometimes removes dependencies from - # src/node_modules but fails to add them to the top-level node_modules. Even - # if npm correctly hoists the dependencies, the hoisting seems to confuse - # tools such as `npm outdated`, `npm update`, and some ESLint rules. - - name: Install all dependencies and symlink for ep_etherpad-lite - run: src/bin/installDeps.sh - - # Nuke plugin tests - - name: Install etherpad plugins - run: rm -Rf node_modules/ep_align/static/tests/* - - - name: export GIT_HASH to env - id: environment - run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})" - - - name: Create settings.json - run: cp settings.json.template settings.json - - - name: Write custom settings.json that enables the Admin UI tests - run: "sed -i 's/\"enableAdminUITests\": false/\"enableAdminUITests\": true,\\n\"users\":{\"admin\":{\"password\":\"changeme\",\"is_admin\":true}}/' settings.json" - - - name: increase maxHttpBufferSize - run: "sed -i 's/\"maxHttpBufferSize\": 10000/\"maxHttpBufferSize\": 100000/' settings.json" - - - name: Remove standard frontend test files, so only admin tests are run - run: mv src/tests/frontend/specs/* /tmp && mv /tmp/admin*.js src/tests/frontend/specs - - - uses: saucelabs/sauce-connect-action@v1.1.2 - with: - username: ${{ secrets.SAUCE_USERNAME }} - accessKey: ${{ secrets.SAUCE_ACCESS_KEY }} - tunnelIdentifier: ${{ steps.sauce_strings.outputs.tunnel_id }} - - - name: Run the frontend admin tests - shell: bash - env: - SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} - SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} - SAUCE_NAME: ${{ steps.sauce_strings.outputs.name }} - TRAVIS_JOB_NUMBER: ${{ steps.sauce_strings.outputs.tunnel_id }} - GIT_HASH: ${{ steps.environment.outputs.sha_short }} - run: | - src/tests/frontend/travis/adminrunner.sh + - + name: Generate Sauce Labs strings + id: sauce_strings + run: | + printf %s\\n '::set-output name=name::${{ github.workflow }} - ${{ github.job }} - Node ${{ matrix.node }}' + printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}-node${{ matrix.node }}' + - + name: Checkout repository + uses: actions/checkout@v4 + - + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 9.0.4 + run_install: false + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Cache playwright binaries + uses: actions/cache@v4 + id: playwright-cache + with: + path: | + ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }} + - name: Only install direct dependencies + run: pnpm config set auto-install-peers false + #- + # name: Install etherpad plugins + # # We intentionally install an old ep_align version to test upgrades to + # # the minor version number. The --legacy-peer-deps flag is required to + # # work around a bug in npm v7: https://github.com/npm/cli/issues/2199 + # run: pnpm install --workspace-root ep_align@0.2.27 + # Etherpad core dependencies must be installed after installing the + # plugin's dependencies, otherwise npm will try to hoist common + # dependencies by removing them from src/node_modules and installing them + # in the top-level node_modules. As of v6.14.10, npm's hoist logic appears + # to be buggy, because it sometimes removes dependencies from + # src/node_modules but fails to add them to the top-level node_modules. + # Even if npm correctly hoists the dependencies, the hoisting seems to + # confuse tools such as `npm outdated`, `npm update`, and some ESLint + # rules. + - + name: Install all dependencies and symlink for ep_etherpad-lite + run: pnpm i + #- + # name: Install etherpad plugins + # run: rm -Rf node_modules/ep_align/static/tests/* + - + name: export GIT_HASH to env + id: environment + run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})" + - + name: Create settings.json + run: cp settings.json.template settings.json + - + name: Write custom settings.json that enables the Admin UI tests + run: "sed -i 's/\"enableAdminUITests\": false/\"enableAdminUITests\": true,\\n\"users\":{\"admin\":{\"password\":\"changeme1\",\"is_admin\":true}}/' settings.json" + - + name: increase maxHttpBufferSize + run: "sed -i 's/\"maxHttpBufferSize\": 50000/\"maxHttpBufferSize\": 10000000/' settings.json" + - + name: Disable import/export rate limiting + run: | + sed -e '/^ *"importExportRateLimiting":/,/^ *\}/ s/"max":.*/"max": 100000000/' -i settings.json + - name: Build admin frontend + working-directory: admin + run: | + pnpm run build + # name: Run the frontend admin tests + # shell: bash + # env: + # SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + # SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + # SAUCE_NAME: ${{ steps.sauce_strings.outputs.name }} + # TRAVIS_JOB_NUMBER: ${{ steps.sauce_strings.outputs.tunnel_id }} + # GIT_HASH: ${{ steps.environment.outputs.sha_short }} + # run: | + # src/tests/frontend/travis/adminrunner.sh + #- + # uses: saucelabs/sauce-connect-action@v2.3.6 + # with: + # username: ${{ secrets.SAUCE_USERNAME }} + # accessKey: ${{ secrets.SAUCE_ACCESS_KEY }} + # tunnelIdentifier: ${{ steps.sauce_strings.outputs.tunnel_id }} + #- + # name: Run the frontend admin tests + # shell: bash + # env: + # SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + # SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + # SAUCE_NAME: ${{ steps.sauce_strings.outputs.name }} + # TRAVIS_JOB_NUMBER: ${{ steps.sauce_strings.outputs.tunnel_id }} + # GIT_HASH: ${{ steps.environment.outputs.sha_short }} + # run: | + # src/tests/frontend/travis/adminrunner.sh + - name: Run the frontend admin tests + shell: bash + run: | + pnpm run prod & + connected=false + can_connect() { + curl -sSfo /dev/null http://localhost:9001/ || return 1 + connected=true + } + now() { date +%s; } + start=$(now) + while [ $(($(now) - $start)) -le 15 ] && ! can_connect; do + sleep 1 + done + cd src + pnpm exec playwright install + pnpm exec playwright install-deps + pnpm run test-admin + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report-${{ matrix.node }} + path: src/playwright-report/ + retention-days: 30 diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index bc138416c..c11058b24 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -1,134 +1,238 @@ # Leave the powered by Sauce Labs bit in as this means we get additional concurrency name: "Frontend tests powered by Sauce Labs" -on: [push] +on: + push: + paths-ignore: + - 'doc/**' + +permissions: + contents: read # to fetch code (actions/checkout) jobs: - withoutplugins: - name: without plugins + playwright-chrome: + name: Playwright Chrome + runs-on: ubuntu-latest + steps: + - + name: Generate Sauce Labs strings + id: sauce_strings + run: | + printf %s\\n '::set-output name=name::${{ github.workflow }} - ${{ github.job }}' + printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}' + - + name: Checkout repository + uses: actions/checkout@v4 + - + uses: actions/setup-node@v4 + with: + node-version: 22 + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 9.0.4 + run_install: false + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + - uses: actions/cache@v4 + name: Setup pnpm cache + if: always() + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Only install direct dependencies + run: pnpm config set auto-install-peers false + - + name: Install all dependencies and symlink for ep_etherpad-lite + run: bin/installDeps.sh + - + name: export GIT_HASH to env + id: environment + run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})" + - + name: Create settings.json + run: cp ./src/tests/settings.json settings.json + - name: Cache playwright binaries + uses: actions/cache@v4 + id: playwright-cache + with: + path: | + ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }} + - name: Run the frontend tests + shell: bash + run: | + pnpm run prod & + connected=false + can_connect() { + curl -sSfo /dev/null http://localhost:9001/ || return 1 + connected=true + } + now() { date +%s; } + start=$(now) + while [ $(($(now) - $start)) -le 15 ] && ! can_connect; do + sleep 1 + done + cd src + pnpm exec playwright install chromium --with-deps + pnpm run test-ui --project=chromium + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report-${{ matrix.node }}-chrome + path: src/playwright-report/ + retention-days: 30 + playwright-firefox: + name: Playwright Firefox + runs-on: ubuntu-latest + steps: + - name: Generate Sauce Labs strings + id: sauce_strings + run: | + printf %s\\n '::set-output name=name::${{ github.workflow }} - ${{ github.job }}' + printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}' + - name: Checkout repository + uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 9.0.4 + run_install: false + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + - uses: actions/cache@v4 + name: Setup pnpm cache + if: always() + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Only install direct dependencies + run: pnpm config set auto-install-peers false + - name: Install all dependencies and symlink for ep_etherpad-lite + run: bin/installDeps.sh + - name: export GIT_HASH to env + id: environment + run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})" + - name: Create settings.json + run: cp ./src/tests/settings.json settings.json + - name: Cache playwright binaries + uses: actions/cache@v4 + id: playwright-cache + with: + path: | + ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }} + - name: Run the frontend tests + shell: bash + run: | + pnpm run prod & + connected=false + can_connect() { + curl -sSfo /dev/null http://localhost:9001/ || return 1 + connected=true + } + now() { date +%s; } + start=$(now) + while [ $(($(now) - $start)) -le 15 ] && ! can_connect; do + sleep 1 + done + cd src + pnpm exec playwright install firefox --with-deps + pnpm run test-ui --project=firefox + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report-${{ matrix.node }}-firefox + path: src/playwright-report/ + retention-days: 30 + playwright-webkit: + name: Playwright Webkit runs-on: ubuntu-latest steps: - - name: Generate Sauce Labs strings - id: sauce_strings - run: | - printf %s\\n '::set-output name=name::${{ github.workflow }} - ${{ github.job }}' - printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}' + - + name: Generate Sauce Labs strings + id: sauce_strings + run: | + printf %s\\n '::set-output name=name::${{ github.workflow }} - ${{ github.job }}' + printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}' + - + name: Checkout repository + uses: actions/checkout@v4 + - + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Cache playwright binaries + uses: actions/cache@v4 + id: playwright-cache + with: + path: | + ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }} + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 9.0.4 + run_install: false + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + - uses: actions/cache@v4 + name: Setup pnpm cache + if: always() + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Only install direct dependencies + run: pnpm config set auto-install-peers false + - + name: Install all dependencies and symlink for ep_etherpad-lite + run: bin/installDeps.sh + - + name: export GIT_HASH to env + id: environment + run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})" + - + name: Create settings.json + run: cp ./src/tests/settings.json settings.json + - name: Run the frontend tests + shell: bash + run: | + pnpm run prod & + connected=false + can_connect() { + curl -sSfo /dev/null http://localhost:9001/ || return 1 + connected=true + } + now() { date +%s; } + start=$(now) + while [ $(($(now) - $start)) -le 15 ] && ! can_connect; do + sleep 1 + done + cd src + pnpm exec playwright install webkit --with-deps + pnpm run test-ui --project=webkit || true + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report-${{ matrix.node }}-webkit + path: src/playwright-report/ + retention-days: 30 - - name: Checkout repository - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: 12 - - name: Install all dependencies and symlink for ep_etherpad-lite - run: src/bin/installDeps.sh - - - name: export GIT_HASH to env - id: environment - run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})" - - - name: Create settings.json - run: cp settings.json.template settings.json - - - name: Disable import/export rate limiting - run: | - sed -e '/^ *"importExportRateLimiting":/,/^ *\}/ s/"max":.*/"max": 0/' -i settings.json - - - uses: saucelabs/sauce-connect-action@v1 - with: - username: ${{ secrets.SAUCE_USERNAME }} - accessKey: ${{ secrets.SAUCE_ACCESS_KEY }} - tunnelIdentifier: ${{ steps.sauce_strings.outputs.tunnel_id }} - - - name: Run the frontend tests - shell: bash - env: - SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} - SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} - SAUCE_NAME: ${{ steps.sauce_strings.outputs.name }} - TRAVIS_JOB_NUMBER: ${{ steps.sauce_strings.outputs.tunnel_id }} - GIT_HASH: ${{ steps.environment.outputs.sha_short }} - run: | - src/tests/frontend/travis/runner.sh - - withplugins: - name: with plugins - runs-on: ubuntu-latest - - steps: - - name: Generate Sauce Labs strings - id: sauce_strings - run: | - printf %s\\n '::set-output name=name::${{ github.workflow }} - ${{ github.job }}' - printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}' - - - name: Checkout repository - uses: actions/checkout@v2 - - - uses: actions/setup-node@v2 - with: - node-version: 12 - - - name: Install Etherpad plugins - # The --legacy-peer-deps flag is required to work around a bug in npm v7: - # https://github.com/npm/cli/issues/2199 - run: > - npm install --no-save --legacy-peer-deps - ep_align - ep_author_hover - ep_cursortrace - ep_embedmedia - ep_font_size - ep_hash_auth - ep_headings2 - ep_image_upload - ep_markdown - ep_readonly_guest - ep_set_title_on_pad - ep_spellcheck - ep_subscript_and_superscript - ep_table_of_contents - - # This must be run after installing the plugins, otherwise npm will try to - # hoist common dependencies by removing them from src/node_modules and - # installing them in the top-level node_modules. As of v6.14.10, npm's hoist - # logic appears to be buggy, because it sometimes removes dependencies from - # src/node_modules but fails to add them to the top-level node_modules. Even - # if npm correctly hoists the dependencies, the hoisting seems to confuse - # tools such as `npm outdated`, `npm update`, and some ESLint rules. - - name: Install all dependencies and symlink for ep_etherpad-lite - run: src/bin/installDeps.sh - - - name: export GIT_HASH to env - id: environment - run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})" - - - name: Create settings.json - run: cp settings.json.template settings.json - - - name: Disable import/export rate limiting - run: | - sed -e '/^ *"importExportRateLimiting":/,/^ *\}/ s/"max":.*/"max": 0/' -i settings.json - - # XXX we should probably run all tests, because plugins could effect their results - - name: Remove standard frontend test files, so only plugin tests are run - run: rm src/tests/frontend/specs/* - - - uses: saucelabs/sauce-connect-action@v1 - with: - username: ${{ secrets.SAUCE_USERNAME }} - accessKey: ${{ secrets.SAUCE_ACCESS_KEY }} - tunnelIdentifier: ${{ steps.sauce_strings.outputs.tunnel_id }} - - - name: Run the frontend tests - shell: bash - env: - SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} - SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} - SAUCE_NAME: ${{ steps.sauce_strings.outputs.name }} - TRAVIS_JOB_NUMBER: ${{ steps.sauce_strings.outputs.tunnel_id }} - GIT_HASH: ${{ steps.environment.outputs.sha_short }} - run: | - src/tests/frontend/travis/runner.sh diff --git a/.github/workflows/lint-package-lock.yml b/.github/workflows/lint-package-lock.yml deleted file mode 100644 index a9596aa3c..000000000 --- a/.github/workflows/lint-package-lock.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: "Lint" - -# any branch is useful for testing before a PR is submitted -on: [push, pull_request] - -jobs: - lint-package-lock: - # run on pushes to any branch - # run on PRs from external forks - if: | - (github.event_name != 'pull_request') - || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) - name: package-lock.json - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - uses: actions/setup-node@v2 - with: - node-version: 12 - - - name: Install lockfile-lint - run: npm install lockfile-lint - - - name: Run lockfile-lint on package-lock.json - run: npx lockfile-lint --path src/package-lock.json --validate-https --allowed-hosts npm diff --git a/.github/workflows/load-test.yml b/.github/workflows/load-test.yml index f4cbbb58d..50847cd9a 100644 --- a/.github/workflows/load-test.yml +++ b/.github/workflows/load-test.yml @@ -1,111 +1,173 @@ name: "Loadtest" # any branch is useful for testing before a PR is submitted -on: [push, pull_request] +on: + push: + paths-ignore: + - "doc/**" + pull_request: + paths-ignore: + - "doc/**" + +permissions: + contents: read jobs: withoutplugins: # run on pushes to any branch # run on PRs from external forks if: | - (github.event_name != 'pull_request') - || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) + (github.event_name != 'pull_request') + || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) name: without plugins runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - uses: actions/setup-node@v2 - with: - node-version: 12 - - - name: Install all dependencies and symlink for ep_etherpad-lite - run: src/bin/installDeps.sh - - - name: Install etherpad-load-test - run: sudo npm install -g etherpad-load-test - - - name: Run load test - run: src/tests/frontend/travis/runnerLoadTest.sh 25 50 + - + name: Checkout repository + uses: actions/checkout@v4 + - + uses: actions/setup-node@v4 + with: + node-version: 20 + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 9.0.4 + run_install: false + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Only install direct dependencies + run: pnpm config set auto-install-peers false + - + name: Install all dependencies and symlink for ep_etherpad-lite + run: bin/installDeps.sh + - + name: Install etherpad-load-test + run: sudo npm install -g etherpad-load-test-socket-io + - + name: Run load test + run: src/tests/frontend/travis/runnerLoadTest.sh 25 50 withplugins: # run on pushes to any branch # run on PRs from external forks if: | - (github.event_name != 'pull_request') - || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) + (github.event_name != 'pull_request') + || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) name: with Plugins runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - uses: actions/setup-node@v2 - with: - node-version: 12 - - - name: Install etherpad-load-test - run: sudo npm install -g etherpad-load-test - - - name: Install etherpad plugins - # The --legacy-peer-deps flag is required to work around a bug in npm v7: - # https://github.com/npm/cli/issues/2199 - run: > - npm install --no-save --legacy-peer-deps - ep_align - ep_author_hover - ep_cursortrace - ep_font_size - ep_hash_auth - ep_headings2 - ep_markdown - ep_readonly_guest - ep_set_title_on_pad - ep_spellcheck - ep_subscript_and_superscript - ep_table_of_contents - - # This must be run after installing the plugins, otherwise npm will try to - # hoist common dependencies by removing them from src/node_modules and - # installing them in the top-level node_modules. As of v6.14.10, npm's hoist - # logic appears to be buggy, because it sometimes removes dependencies from - # src/node_modules but fails to add them to the top-level node_modules. Even - # if npm correctly hoists the dependencies, the hoisting seems to confuse - # tools such as `npm outdated`, `npm update`, and some ESLint rules. - - name: Install all dependencies and symlink for ep_etherpad-lite - run: src/bin/installDeps.sh - - # configures some settings and runs npm run test - - name: Run load test - run: src/tests/frontend/travis/runnerLoadTest.sh 25 50 + - + name: Checkout repository + uses: actions/checkout@v4 + - + uses: actions/setup-node@v4 + with: + node-version: 20 + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 9.0.4 + run_install: false + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Only install direct dependencies + run: pnpm config set auto-install-peers false + - + name: Install etherpad-load-test + run: pnpm install -g etherpad-load-test-socket-io + - + name: Install etherpad plugins + # The --legacy-peer-deps flag is required to work around a bug in npm v7: + # https://github.com/npm/cli/issues/2199 + run: > + pnpm install --workspace-root + ep_align + ep_author_hover + ep_cursortrace + ep_font_size + ep_hash_auth + ep_headings2 + ep_markdown + ep_readonly_guest + ep_set_title_on_pad + ep_spellcheck + ep_subscript_and_superscript + ep_table_of_contents + # Etherpad core dependencies must be installed after installing the + # plugin's dependencies, otherwise npm will try to hoist common + # dependencies by removing them from src/node_modules and installing them + # in the top-level node_modules. As of v6.14.10, npm's hoist logic appears + # to be buggy, because it sometimes removes dependencies from + # src/node_modules but fails to add them to the top-level node_modules. + # Even if npm correctly hoists the dependencies, the hoisting seems to + # confuse tools such as `npm outdated`, `npm update`, and some ESLint + # rules. + - + name: Install all dependencies and symlink for ep_etherpad-lite + run: bin/installDeps.sh + - + name: Run load test + run: src/tests/frontend/travis/runnerLoadTest.sh 25 50 long: # run on pushes to any branch # run on PRs from external forks if: | - (github.event_name != 'pull_request') - || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) + (github.event_name != 'pull_request') + || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) name: long running runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - uses: actions/setup-node@v2 - with: - node-version: 12 - - - name: Install all dependencies and symlink for ep_etherpad-lite - run: src/bin/installDeps.sh - - - name: Install etherpad-load-test - run: sudo npm install -g etherpad-load-test - - - # configures some settings and runs npm run test - - name: Run load test - run: src/tests/frontend/travis/runnerLoadTest.sh 5000 5 + - + name: Checkout repository + uses: actions/checkout@v4 + - + uses: actions/setup-node@v4 + with: + node-version: 20 + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 9.0.4 + run_install: false + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Only install direct dependencies + run: pnpm config set auto-install-peers false + - + name: Install all dependencies and symlink for ep_etherpad-lite + run: bin/installDeps.sh + - + name: Install etherpad-load-test + run: sudo npm install -g etherpad-load-test-socket-io + - + name: Run load test + run: src/tests/frontend/travis/runnerLoadTest.sh 5000 5 diff --git a/.github/workflows/perform-type-check.yml b/.github/workflows/perform-type-check.yml new file mode 100644 index 000000000..ba35dec32 --- /dev/null +++ b/.github/workflows/perform-type-check.yml @@ -0,0 +1,52 @@ +name: "Perform type checks" + +# any branch is useful for testing before a PR is submitted +on: + push: + paths-ignore: + - "doc/**" + pull_request: + paths-ignore: + - "doc/**" + +permissions: + contents: read + + +jobs: + performTypeCheck: + if: | + (github.event_name != 'pull_request') + || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) + name: perform type check + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 9.0.4 + run_install: false + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Only install direct dependencies + run: pnpm config set auto-install-peers false + - + name: Install all dependencies and symlink for ep_etherpad-lite + run: ./bin/installDeps.sh + - name: Perform type check + working-directory: ./src + run: npm run ts-check diff --git a/.github/workflows/rate-limit.yml b/.github/workflows/rate-limit.yml index 0849f8e06..003a10000 100644 --- a/.github/workflows/rate-limit.yml +++ b/.github/workflows/rate-limit.yml @@ -1,43 +1,71 @@ name: "rate limit" # any branch is useful for testing before a PR is submitted -on: [push, pull_request] +on: + push: + paths-ignore: + - "doc/**" + pull_request: + paths-ignore: + - "doc/**" + +permissions: + contents: read jobs: ratelimit: # run on pushes to any branch # run on PRs from external forks if: | - (github.event_name != 'pull_request') - || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) + (github.event_name != 'pull_request') + || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) name: test runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v2 + - + name: Checkout repository + uses: actions/checkout@v4 + - + uses: actions/setup-node@v4 + with: + node-version: 20 + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 9.0.4 + run_install: false + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- - - uses: actions/setup-node@v2 - with: - node-version: 12 - - - name: docker network - run: docker network create --subnet=172.23.42.0/16 ep_net - - - name: build docker image - run: | - docker build -f Dockerfile -t epl-debian-slim . - docker build -f src/tests/ratelimit/Dockerfile.nginx -t nginx-latest . - docker build -f src/tests/ratelimit/Dockerfile.anotherip -t anotherip . - - name: run docker images - run: | - docker run --name etherpad-docker -p 9000:9001 --rm --network ep_net --ip 172.23.42.2 -e 'TRUST_PROXY=true' epl-debian-slim & - docker run -p 8081:80 --rm --network ep_net --ip 172.23.42.1 -d nginx-latest - docker run --rm --network ep_net --ip 172.23.42.3 --name anotherip -dt anotherip - - - name: install dependencies and create symlink for ep_etherpad-lite - run: src/bin/installDeps.sh - - - name: run rate limit test - run: | - cd src/tests/ratelimit - ./testlimits.sh + - + name: docker network + run: docker network create --subnet=172.23.0.0/16 ep_net + - + name: build docker image + run: | + docker build -f Dockerfile -t epl-debian-slim --build-arg NODE_ENV=develop . + docker build -f src/tests/ratelimit/Dockerfile.nginx -t nginx-latest . + docker build -f src/tests/ratelimit/Dockerfile.anotherip -t anotherip . + - + name: run docker images + run: | + docker run --name etherpad-docker -p 9000:9001 --rm --network ep_net --ip 172.23.42.2 -e 'TRUST_PROXY=true' epl-debian-slim & + docker run -p 8081:80 --rm --network ep_net --ip 172.23.42.1 -d nginx-latest + docker run --rm --network ep_net --ip 172.23.42.3 --name anotherip -dt anotherip + - + name: install dependencies and create symlink for ep_etherpad-lite + run: bin/installDeps.sh + - + name: run rate limit test + run: | + cd src/tests/ratelimit + ./testlimits.sh diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..60249bb15 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,18 @@ +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '30 6 * * *' +permissions: + issues: write + pull-requests: write +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + close-issue-label: wontfix + close-pr-label: wontfix + days-before-close: -1 + exempt-issue-labels: 'pinned,security,Bug,Serious Bug,Minor bug,Black hole bug,Special case Bug,Upstream bug,Feature Request' + exempt-pr-labels: 'pinned,security,Bug,Serious Bug,Minor bug,Black hole bug,Special case Bug,Upstream bug,Feature Request' diff --git a/.github/workflows/upgrade-from-latest-release.yml b/.github/workflows/upgrade-from-latest-release.yml index e57e63ebb..08421e7ec 100644 --- a/.github/workflows/upgrade-from-latest-release.yml +++ b/.github/workflows/upgrade-from-latest-release.yml @@ -1,91 +1,116 @@ name: "Upgrade from latest release" # any branch is useful for testing before a PR is submitted -on: [push, pull_request] +on: + push: + paths-ignore: + - "doc/**" + pull_request: + paths-ignore: + - "doc/**" + +permissions: + contents: read jobs: withpluginsLinux: # run on pushes to any branch # run on PRs from external forks if: | - (github.event_name != 'pull_request') - || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) + (github.event_name != 'pull_request') + || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) name: Linux with Plugins runs-on: ubuntu-latest - strategy: fail-fast: false matrix: - node: [12, 14, 16] - + node: [20, 22, 23] steps: - - name: Check out latest release - uses: actions/checkout@v2 - with: - ref: master - - - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node }} - - - name: Install Etherpad plugins - # The --legacy-peer-deps flag is required to work around a bug in npm v7: - # https://github.com/npm/cli/issues/2199 - run: > - npm install --no-save --legacy-peer-deps - ep_align - ep_author_hover - ep_cursortrace - ep_font_size - ep_hash_auth - ep_headings2 - ep_image_upload - ep_markdown - ep_readonly_guest - ep_set_title_on_pad - ep_spellcheck - ep_subscript_and_superscript - ep_table_of_contents - - # This must be run after installing the plugins, otherwise npm will try to - # hoist common dependencies by removing them from src/node_modules and - # installing them in the top-level node_modules. As of v6.14.10, npm's hoist - # logic appears to be buggy, because it sometimes removes dependencies from - # src/node_modules but fails to add them to the top-level node_modules. Even - # if npm correctly hoists the dependencies, the hoisting seems to confuse - # tools such as `npm outdated`, `npm update`, and some ESLint rules. - - name: Install all dependencies and symlink for ep_etherpad-lite - run: src/bin/installDeps.sh - - - name: Run the backend tests - run: cd src && npm test - - # Because actions/checkout@v2 is called with "ref: master" and without - # "fetch-depth: 0", the local clone does not have the ${GITHUB_SHA} commit. - # Fetch ${GITHUB_REF} to get the ${GITHUB_SHA} commit. Note that a plain - # "git fetch" only fetches "normal" references (refs/heads/* and - # refs/tags/*), and for pull requests none of the normal references include - # ${GITHUB_SHA}, so we have to explicitly tell Git to fetch ${GITHUB_REF}. - - name: Fetch the new Git commits - run: git fetch --depth=1 origin "${GITHUB_REF}" - - - name: Upgrade to the new Git revision - # For pull requests, ${GITHUB_SHA} is the automatically generated merge - # commit that merges the PR's source branch to its destination branch. - run: git checkout "${GITHUB_SHA}" - - - name: Install all dependencies and symlink for ep_etherpad-lite - run: src/bin/installDeps.sh - - - name: Run the backend tests - run: cd src && npm test - - - name: Install Cypress - run: npm install cypress -g - - - name: Run Etherpad & Test Frontend - run: | - node src/node/server.js & - curl --connect-timeout 10 --max-time 20 --retry 5 --retry-delay 10 --retry-max-time 60 --retry-connrefused http://127.0.0.1:9001/p/test - cd src/tests/frontend - cypress run --spec cypress/integration/test.js --config-file cypress/cypress.json + - + name: Check out latest release + uses: actions/checkout@v4 + with: + ref: develop #FIXME change to master when doing release + - + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 9.0.4 + run_install: false + - name: Only install direct dependencies + run: pnpm config set auto-install-peers false + - name: Install libreoffice + uses: awalsh128/cache-apt-pkgs-action@v1.5.0 + with: + packages: libreoffice libreoffice-pdfimport + version: 1.0 + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Only install direct dependencies + run: pnpm config set auto-install-peers false + - + name: Install libreoffice + uses: awalsh128/cache-apt-pkgs-action@v1.5.0 + with: + packages: libreoffice libreoffice-pdfimport + version: 1.0 + - + name: Install all dependencies and symlink for ep_etherpad-lite + run: bin/installDeps.sh + - name: Install admin ui + working-directory: admin + run: pnpm install + - name: Build admin ui + working-directory: admin + run: pnpm build + - + name: Install Etherpad plugins + run: > + pnpm run install-plugins + ep_align + ep_author_hover + ep_cursortrace + ep_font_size + ep_hash_auth + ep_headings2 + ep_markdown + ep_readonly_guest + ep_set_title_on_pad + ep_spellcheck + ep_subscript_and_superscript + ep_table_of_contents + - + name: Run the backend tests + run: pnpm run test + - + name: Install all dependencies and symlink for ep_etherpad-lite + run: ./bin/installDeps.sh + # Because actions/checkout@v4 is called with "ref: master" and without + # "fetch-depth: 0", the local clone does not have the ${GITHUB_SHA} + # commit. Fetch ${GITHUB_REF} to get the ${GITHUB_SHA} commit. Note that a + # plain "git fetch" only fetches "normal" references (refs/heads/* and + # refs/tags/*), and for pull requests none of the normal references + # include ${GITHUB_SHA}, so we have to explicitly tell Git to fetch + # ${GITHUB_REF}. + - + name: Fetch the new Git commits + run: git fetch --depth=1 origin "${GITHUB_REF}" + - + name: Upgrade to the new Git revision + # For pull requests, ${GITHUB_SHA} is the automatically generated merge + # commit that merges the PR's source branch to its destination branch. + run: git checkout "${GITHUB_SHA}" + - name: Run the backend tests + run: pnpm run test diff --git a/.github/workflows/windows-installer.yml b/.github/workflows/windows-installer.yml deleted file mode 100644 index 37d86a9a6..000000000 --- a/.github/workflows/windows-installer.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: "Windows Installer" - -# any branch is useful for testing before a PR is submitted -on: [push, pull_request] - -jobs: - build: - # run on pushes to any branch - # run on PRs from external forks - if: | - (github.event_name != 'pull_request') - || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) - - name: Build Zip & Exe - runs-on: windows-latest - - steps: - - uses: msys2/setup-msys2@v2 - with: - path-type: inherit - install: >- - zip - - - name: Checkout repository - uses: actions/checkout@v2 - - - uses: actions/setup-node@v2 - with: - node-version: 12 - - - name: Install all dependencies and symlink for ep_etherpad-lite - shell: msys2 {0} - run: src/bin/installDeps.sh - - - name: Run the backend tests - shell: msys2 {0} - run: cd src && npm test - - - name: Build the .zip - shell: msys2 {0} - run: src/bin/buildForWindows.sh - - - name: Extract the .zip into folder - run: 7z x etherpad-lite-win.zip -oetherpad-lite-new - - - name: Grab nsis config - run: git clone https://github.com/ether/etherpad_nsis.git - - - name: Create installer - uses: joncloud/makensis-action@v3.4 - with: - script-file: 'etherpad_nsis/etherpad.nsi' - - - name: Check something.. - run: ls etherpad_nsis - - - name: Archive production artifacts - uses: actions/upload-artifact@v2 - with: - name: etherpad-server-windows.exe - path: etherpad_nsis/etherpad-server-windows.exe diff --git a/.github/workflows/windows-zip.yml b/.github/workflows/windows-zip.yml deleted file mode 100644 index 42a99a191..000000000 --- a/.github/workflows/windows-zip.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: "Windows Zip" - -# any branch is useful for testing before a PR is submitted -on: [push, pull_request] - -jobs: - build: - # run on pushes to any branch - # run on PRs from external forks - if: | - (github.event_name != 'pull_request') - || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) - name: Build - runs-on: windows-latest - - steps: - - uses: msys2/setup-msys2@v2 - with: - path-type: inherit - install: >- - zip - - - name: Checkout repository - uses: actions/checkout@v2 - - - uses: actions/setup-node@v2 - with: - node-version: 12 - - - name: Install all dependencies and symlink for ep_etherpad-lite - shell: msys2 {0} - run: src/bin/installDeps.sh - - - name: Run the backend tests - shell: msys2 {0} - run: cd src && npm test - - - name: Build the .zip - shell: msys2 {0} - run: src/bin/buildForWindows.sh - - - name: Archive production artifacts - uses: actions/upload-artifact@v2 - with: - name: etherpad-lite-win.zip - path: etherpad-lite-win.zip - - - deploy: - # run on pushes to any branch - # run on PRs from external forks - if: | - (github.event_name != 'pull_request') - || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) - name: Deploy - needs: build - runs-on: windows-latest - - steps: - - name: Download zip - uses: actions/download-artifact@v2 - with: - name: etherpad-lite-win.zip - - - name: Extract Etherpad - run: 7z x etherpad-lite-win.zip -oetherpad - - - name: Install Cypress - run: npm install cypress -g - - - name: Run Etherpad - run: | - cd etherpad - node node_modules\ep_etherpad-lite\node\server.js & - curl --connect-timeout 10 --max-time 20 --retry 5 --retry-delay 10 --retry-max-time 60 --retry-connrefused http://127.0.0.1:9001/p/test - cd src\tests\frontend - cypress run --spec cypress\integration\test.js --config-file cypress\cypress.json diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml new file mode 100644 index 000000000..c0ce8b8bd --- /dev/null +++ b/.github/workflows/windows.yml @@ -0,0 +1,86 @@ +name: "Windows Build" + +# any branch is useful for testing before a PR is submitted +on: + push: + paths-ignore: + - "doc/**" + pull_request: + paths-ignore: + - "doc/**" + +permissions: + contents: read + +jobs: + build-zip: + permissions: write-all + # run on pushes to any branch + # run on PRs from external forks + if: | + (github.event_name != 'pull_request') + || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) + name: Build .zip + runs-on: windows-latest + steps: + - + uses: msys2/setup-msys2@v2 + with: + path-type: inherit + install: >- + zip + - + name: Checkout repository + uses: actions/checkout@v4 + - + uses: actions/setup-node@v4 + with: + node-version: 22 + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 9.0.4 + run_install: false + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Only install direct dependencies + run: pnpm config set auto-install-peers false + - + name: Install all dependencies and symlink for ep_etherpad-lite + shell: msys2 {0} + run: bin/installDeps.sh + - + name: Run the backend tests + shell: msys2 {0} + working-directory: src + run: pnpm test + - + name: Run Etherpad + working-directory: src + run: | + pnpm i + pnpm exec playwright install --with-deps + pnpm run prod & + curl --connect-timeout 10 --max-time 20 --retry 5 --retry-delay 10 --retry-max-time 60 --retry-connrefused http://127.0.0.1:9001/p/test + pnpm exec playwright install chromium --with-deps + pnpm run test-ui --project=chromium + # On release, create release + - name: Generate Changelog + if: ${{startsWith(github.ref, 'refs/tags/v') }} + working-directory: bin + run: pnpm run generateChangelog ${{ github.ref }} > ${{ github.workspace }}-CHANGELOG.txt + - name: Release + uses: softprops/action-gh-release@v2 + if: ${{startsWith(github.ref, 'refs/tags/v') }} + with: + body_path: ${{ github.workspace }}-CHANGELOG.txt + make_latest: true diff --git a/.gitignore b/.gitignore index 60638c50a..71584e76b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,12 @@ +/etherpad-win.exe +/etherpad-win.zip node_modules /settings.json !settings.json.template APIKEY.txt SESSIONKEY.txt -etherpad-lite-win.zip var/dirty.db +.env *~ *.patch npm-debug.log @@ -20,3 +22,9 @@ out/ /src/bin/convertSettings.json /src/bin/etherpad-1.deb /src/bin/node.exe +plugin_packages +/src/templates/admin +/src/test-results +playwright-report +state.json +/src/static/oidc diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..f301fedf9 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +auto-install-peers=false diff --git a/.travis.yml b/.travis.yml index 44a8693bb..ca8c5380f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -55,7 +55,7 @@ jobs: - *set_loglevel_warn - *enable_admin_tests - "src/tests/frontend/travis/sauce_tunnel.sh" - - "src/bin/installDeps.sh" + - "bin/installDeps.sh" - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" script: - "./src/tests/frontend/travis/runner.sh" @@ -63,22 +63,22 @@ jobs: install: - *install_libreoffice - *set_loglevel_warn - - "src/bin/installDeps.sh" - - "cd src && npm install && cd -" + - "bin/installDeps.sh" + - "cd src && pnpm install && cd -" script: - - "cd src && npm test" + - "cd src && pnpm test" - name: "Test the Dockerfile" install: - - "cd src && npm install && cd -" + - "cd src && pnpm install && cd -" script: - "docker build -t etherpad:test ." - "docker run -d -p 9001:9001 etherpad:test && sleep 3" - - "cd src && npm run test-container" + - "cd src && pnpm run test-container" - name: "Load test Etherpad without Plugins" install: - *set_loglevel_warn - - "src/bin/installDeps.sh" - - "cd src && npm install && cd -" + - "bin/installDeps.sh" + - "cd src && pnpm install && cd -" - "npm install -g etherpad-load-test" script: - "src/tests/frontend/travis/runnerLoadTest.sh" @@ -90,7 +90,7 @@ jobs: - *set_loglevel_warn - *enable_admin_tests - "src/tests/frontend/travis/sauce_tunnel.sh" - - "src/bin/installDeps.sh" + - "bin/installDeps.sh" - "rm src/tests/frontend/specs/*" - *install_plugins - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" @@ -105,22 +105,22 @@ jobs: install: - *install_libreoffice - *set_loglevel_warn - - "src/bin/installDeps.sh" + - "bin/installDeps.sh" - *install_plugins - - "cd src && npm install && cd -" + - "cd src && pnpm install && cd -" script: - - "cd src && npm test" + - "cd src && pnpm test" - name: "Test the Dockerfile" install: - - "cd src && npm install && cd -" + - "cd src && pnpm install && cd -" script: - "docker build -t etherpad:test ." - "docker run -d -p 9001:9001 etherpad:test && sleep 3" - - "cd src && npm run test-container" + - "cd src && pnpm run test-container" - name: "Load test Etherpad with Plugins" install: - *set_loglevel_warn - - "src/bin/installDeps.sh" + - "bin/installDeps.sh" - *install_plugins - "cd src && npm install && cd -" - "npm install -g etherpad-load-test" @@ -135,7 +135,7 @@ jobs: - "docker run -p 8081:80 --rm --network ep_net --ip 172.23.42.1 -d nginx-latest" - "docker run --name etherpad-docker -p 9000:9001 --rm --network ep_net --ip 172.23.42.2 -e 'TRUST_PROXY=true' epl-debian-slim &" - "docker run --rm --network ep_net --ip 172.23.42.3 --name anotherip -dt anotherip" - - "./src/bin/installDeps.sh" + - "./bin/installDeps.sh" script: - "cd src/tests/ratelimit && bash testlimits.sh" diff --git a/CHANGELOG.md b/CHANGELOG.md index 450f8e616..e0a70e7dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,402 @@ +# 2.3.0 + +### Notable enhancements and fixes + +- Added possibility to cluster Etherpads behind reverse proxy. There is now a new reverse proxy designed for Etherpads that handles multiple Etherpads and the created pads in them. It will assign the pad assignement to an Etherpad at random but once the choice was made it will always reverse proxy the same backend. This allows to host multiple concurrent Etherpads and benefit from multi core systems even though one Etherpad is singlethreaded. +- Added reverse proxy configuration for replacing Nginx. In the past there were some issues with nginx and its configuration. This reverse proxy allows you to handle your configuration with ease. + +If you want to find out more about the reverse proxy method check out the repository https://github.com/ether/etherpad-proxy . It also contains a sample docker-compose file with three Etherpads and one etherpad-proxy. Of course you need to adapt the settings.json.template to your liking and map it into the reverse proxy image before you are ready :). + + +- Added client authorization to work with Etherpad. Before it would get blocked because it doesn't have the required claim. As this is now fixed etherpad-proxy can also work with your new OAuth2 configuration and retrieve a token via client credentials flow. + + + + +# 2.2.7 + + +### Notable enhancements and fixes + +- We migrated all important pages to React 19 and React Router v7 + +Besides that only dependency updates. + + + -> Have a merry Christmas and a happy new year. 🎄 🎁 + + +# 2.2.6 + +### Notable enhancements and fixes + +- Added option to delete a pad by the creator. This option can be found in the settings menu. When you click on it you get a confirm dialog and after that you have the chance to completely erase the pad. + + +# 2.2.5 + +### Notable enhancements and fixes + +- Fixed timeslider not scrolling when the revision count is a multiple of 100 +- Added new Restful API for version 2 of Etherpad. It is available at /api-docs + + +# 2.2.4 + +### Notable enhancements and fixes + +- Switched to new SQLite backend +- Fixed rusty-store-kv module not found + + +# 2.2.3 + +### Notable enhancements and fixes + +- Introduced a new in process database `rustydb` that represents a fast key value store written in Rust. +- Readded window._ as a shortcut for getting text +- Added support for migrating any ueberdb database to another. You can now switch as you please. See here: https://docs.etherpad.org/cli.html +- Further Typescript movements +- A lot of security issues fixed and reviewed in this release. Please update. + + +# 2.2.2 + +### Notable enhancements and fixes + +- Removal of Etherpad require kernel: We finally managed to include esbuild to bundle our frontend code together. So no matter how many plugins your server has it is always one JavaScript file. This boosts performance dramatically. +- Added log layoutType: This lets you print the log in either colored or basic (black and white text) +- Introduced esbuild for bundling CSS files +- Cache all files to be bundled in memory for faster load speed + + +# 2.1.1 + + +### Notable enhancements and fixes + +- Fixed failing Docker build when checked out as git submodule. Thanks to @neurolabs +- Fixed: Fallback to websocket and polling when unknown(old) config is present for socket io +- Fixed: Next page disabled if zero page by @samyakj023 +- On CTRL+CLICK bring the window back to focus by Helder Sepulveda + +# 2.1.0 + +### Notable enhancements and fixes + +- Added PWA support. You can now add your Etherpad instance to your home screen on your mobile device or desktop. +- Fixed live plugin manager versions clashing. Thanks to @yacchin1205 +- Fixed a bug in the pad panel where pagination was not working correctly when sorting by pad name + +### Compatibility changes + +- Reintroduced APIKey.txt support. You can now switch between APIKey and OAuth2.0 authentication. This can be toggled with the setting authenticationMethod. The default is OAuth2. If you want to use the APIKey method you can set that to `apikey`. + + +# 2.0.3 + +### Notable enhancements and fixes + +- Added documentation for replacing apikeys with oauth2 +- Bumped live plugin manager to 0.20.0. Thanks to @fgreinacher +- Added better documentation for using docker-compose with Etherpad + + + +# 2.0.2 + +### Notable enhancements and fixes + +- Fixed the locale loading in the admin panel +- Added OAuth2.0 support for the Etherpad API. You can now log in into the Etherpad API with your admin user using OAuth2 + +### Compatibility changes + +- The tests now require generating a token from the OAuth secret. You can find the `generateJWTToken` in the common.ts script for plugin endpoint updates. + + +# 2.0.1 + +### Notable enhancements and fixes + +- Fixed a bug where a plugin depending on a scoped dependency would not install successfully. + + +# 2.0.0 + + +### Compatibility changes + +- Socket io has been updated to 4.7.5. This means that the json.send function won't work anymore and needs to be changed to .emit('message', myObj) +- Deprecating npm version 6 in favor of pnpm: We have made the decision to switch to the well established pnpm (https://pnpm.io/). It works by symlinking dependencies into a global directory allowing you to have a cleaner and more reliable environment. +- Introducing Typescript to the Etherpad core: Etherpad core logic has been rewritten in Typescript allowing for compiler checking of errors. +- Rewritten Admin Panel: The Admin panel has been rewritten in React and now features a more pleasant user experience. It now also features an integrated pad searching with sorting functionality. + +### Notable enhancements and fixes + +* Bugfixes + - Live Plugin Manager: The live plugin manager caused problems when a plugin had depdendencies defined. This issue is now resolved. + +* Enhancements + - pnpm Workspaces: In addition to pnpm we introduced workspaces. A clean way to manage multiple bounded contexts like the admin panel or the bin folder. + - Bin folder: The bin folder has been moved from the src folder to the root folder. This change was necessary as the contained scripts do not represent core functionality of the user. + - Starting Etherpad: Etherpad can now be started with a single command: `pnpm run prod` in the root directory. + - Installing Etherpad: Etherpad no longer symlinks itself in the root directory. This is now also taken care by pnpm, and it just creates a node_modules folder with the src directory`s ep_etherpad-lite folder + - Plugins can now be installed simply via the command: `pnpm run plugins i first-plugin second-plugin` or if you want to install from path you can do: + `pnpm run plugins i --path ../path-to-plugin` + + +# 1.9.7 + +### Notable enhancements and fixes + +* Added Live Plugin Manager: Plugins are now installed into a separate folder on the host system. This folder is called `plugin_packages`. +That way the plugins are separated from the normal etherpad installation. +* Make repairPad.js more verbose +* Fixed favicon not being loaded correctly + +# 1.9.6 + +### Notable enhancements and fixes + +* Prevent etherpad crash when update server is not reachable +* Use npm@6 in Docker build +* Fix setting the log level in settings.json + + +# 1.9.5 + +### Compatibility changes + +* This version deprecates NodeJS16 as it reached its end of life and won't receive any updates. So to get started with Etherpad v1.9.5 you need NodeJS 18 and above. +* The bundled windows NodeJS version has been bumped to the current LTS version 20. + +### Notable enhancements and fixes + +* The support for the tidy program to tidy up HTML files has been removed. This decision was made because it hasn't been updated for years and also caused an incompability when exporting a pad with Abiword. + + +# 1.9.4 + +### Compatibility changes + +* Log4js has been updated to the latest version. As it involved a bump of 6 major version. + A lot has changed since then. Most notably the console appender has been deprecated. You can find out more about it [here](https://github.com/log4js-node/log4js-node) + +### Notable enhancements and fixes + +* Fix for MySQL: The logger calls were incorrectly configured leading to a crash when e.g. somebody uses a different encoding than standard MySQL encoding. + +# 1.9.3 + +### Compability changes + +* express-rate-limit has been bumped to 7.0.0: This involves the breaking change that "max: 0" +in the importExportRateLimiting is set to always trigger. So set it to your desired value. +If you haven't changed that value in the settings.json you are all set. + +### Notable enhancements and fixes + +* Bugfixes + * Fix etherpad crashing with mongodb database + +* Enhancements + * Add surrealdb database support. You can find out more about this database [here](https://surrealdb.com). + * Make sqlite faster: The sqlite library has been switched to better-sqlite3. This should lead to better performance. + +# 1.9.2 + +### Notable enhancements and fixes + +* Security + * Enable session key rotation: This setting can be enabled in the settings.json. It changes the signing key for the cookie authentication in a fixed interval. + +* Bugfixes + * Fix appendRevision when creating a new pad via the API without a text. + + +* Enhancements + * Bump JQuery to version 3.7 + * Update elasticsearch connector to version 8 + +### Compatibility changes + +* No compability changes as JQuery maintains excellent backwards compatibility. + +#### For plugin authors + +* Please update to JQuery 3.7. There is an excellent deprecation guide over [here](https://api.jquery.com/category/deprecated/). Version 3.1 to 3.7 are relevant for the upgrade. + +# 1.9.1 + +### Notable enhancements and fixes + +* Security + * Limit requested revisions in timeslider and export to head revision. (affects v1.9.0) + +* Bugfixes + * revisions in `CHANGESET_REQ` (timeslider) and export (txt, html, custom) + are now checked to be numbers. + * bump sql for audit fix +* Enhancements + * Add keybinding meta-backspace to delete to beginning of line + * Fix automatic Windows build via GitHub Actions + * Enable docs to be build cross platform thanks to asciidoctor + +### Compatibility changes +* tests: drop windows 7 test coverage & use chrome latest for admin tests +* Require Node 16 for Etherpad and target Node 20 for testing + + +# 1.9.0 + +### Notable enhancements and fixes + +* Windows build: + * The bundled `node.exe` was upgraded from v12 to v16. + * The bundled `node.exe` is now a 64-bit executable. If you need the 32-bit + version you must download and install Node.js yourself. +* Improvements to login session management: + * `express_sid` cookies and `sessionstorage:*` database records are no longer + created unless `requireAuthentication` is `true` (or a plugin causes them to + be created). + * Login sessions now have a finite lifetime by default (10 days after + leaving). + * `sessionstorage:*` database records are automatically deleted when the login + session expires (with some exceptions that will be fixed in the future). + * Requests for static content (e.g., `/robots.txt`) and special pages (e.g., + the HTTP API, `/stats`) no longer create login session state. + * The secret used to sign the `express_sid` cookie is now automatically + regenerated every day (called *key rotation*) by default. If key rotation is + enabled, the now-deprecated `SESSIONKEY.txt` file can be safely deleted + after Etherpad starts up (its content is read and saved to the database and + used to validate signatures from old cookies until they expire). +* The following settings from `settings.json` are now applied as expected (they + were unintentionally ignored before): + * `padOptions.lang` + * `padOptions.showChat` + * `padOptions.userColor` + * `padOptions.userName` +* HTTP API: + * Fixed the return value of `getText` when called with a specific revision. + * Fixed a potential attribute pool corruption bug with + `copyPadWithoutHistory`. + * Mappings created by `createGroupIfNotExistsFor` are now removed from the + database when the group is deleted. + * Fixed race conditions in the `setText`, `appendText`, and `restoreRevision` + functions. + * Added an optional `authorId` parameter to `appendText`, + `copyPadWithoutHistory`, `createGroupPad`, `createPad`, `restoreRevision`, + `setHTML`, and `setText`, and bumped the latest API version to 1.3.0. +* Fixed a crash if the database is busy enough to cause a query timeout. +* New `/health` endpoint for getting information about Etherpad's health (see + [draft-inadarei-api-health-check-06](https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html)). +* Docker now uses the new `/health` endpoint for health checks, which avoids + issues when authentication is enabled. It also avoids the unnecessary creation + of database records for managing browser sessions. +* When copying a pad, the pad's records are copied in batches to avoid database + timeouts with large pads. +* Exporting a large pad to `.etherpad` format should be faster thanks to bulk + database record fetches. +* When importing an `.etherpad` file, records are now saved to the database in + batches to avoid database timeouts with large pads. + +#### For plugin authors + +* New `expressPreSession` server-side hook. +* Pad server-side hook changes: + * `padCheck`: New hook. + * `padCopy`: New `srcPad` and `dstPad` context properties. + * `padDefaultContent`: New hook. + * `padRemove`: New `pad` context property. +* The `db` property on Pad objects is now public. +* New `getAuthorId` server-side hook. +* New APIs for processing attributes: `ep_etherpad-lite/static/js/attributes` + (low-level API) and `ep_etherpad-lite/static/js/AttributeMap` (high-level + API). +* The `import` server-side hook has a new `ImportError` context property. +* New `exportEtherpad` and `importEtherpad` server-side hooks. +* The `handleMessageSecurity` and `handleMessage` server-side hooks have a new + `sessionInfo` context property that includes the user's author ID, the pad ID, + and whether the user only has read-only access. +* The `handleMessageSecurity` server-side hook can now be used to grant write + access for the current message only. +* The `init_` server-side hooks have a new `logger` context + property that plugins can use to log messages. +* Prevent infinite loop when exiting the server +* Bump dependencies + + +### Compatibility changes + +* Node.js v14.15.0 or later is now required. +* The default login session expiration (applicable if `requireAuthentication` is + `true`) changed from never to 10 days after the user leaves. + +#### For plugin authors + +* The `client` context property for the `handleMessageSecurity` and + `handleMessage` server-side hooks is deprecated; use the `socket` context + property instead. +* Pad server-side hook changes: + * `padCopy`: + * The `originalPad` context property is deprecated; use `srcPad` instead. + * The `destinationID` context property is deprecated; use `dstPad.id` + instead. + * `padCreate`: The `author` context property is deprecated; use the new + `authorId` context property instead. Also, the hook now runs asynchronously. + * `padLoad`: Now runs when a temporary Pad object is created during import. + Also, it now runs asynchronously. + * `padRemove`: The `padID` context property is deprecated; use `pad.id` + instead. + * `padUpdate`: The `author` context property is deprecated; use the new + `authorId` context property instead. Also, the hook now runs asynchronously. +* Returning `true` from a `handleMessageSecurity` hook function is deprecated; + return `'permitOnce'` instead. +* Changes to the `src/static/js/Changeset.js` library: + * The following attribute processing functions are deprecated (use the new + attribute APIs instead): + * `attribsAttributeValue()` + * `eachAttribNumber()` + * `makeAttribsString()` + * `opAttributeValue()` + * `opIterator()`: Deprecated in favor of the new `deserializeOps()` generator + function. + * `appendATextToAssembler()`: Deprecated in favor of the new `opsFromAText()` + generator function. + * `newOp()`: Deprecated in favor of the new `Op` class. +* The `AuthorManager.getAuthor4Token()` function is deprecated; use the new + `AuthorManager.getAuthorId()` function instead. +* The exported database records covered by the `exportEtherpadAdditionalContent` + server-side hook now include keys like `${customPrefix}:${padId}:*`, not just + `${customPrefix}:${padId}`. +* Plugin locales should overwrite core's locales Stale +* Plugin locales overwrite core locales + +# 1.8.18 + +Released: 2022-05-05 + +### Notable enhancements and fixes + + * Upgraded ueberDB to fix a regression with CouchDB. + +# 1.8.17 + +Released: 2022-02-23 + +### Security fixes + +* Fixed a vunlerability in the `CHANGESET_REQ` message handler that allowed a + user with any access to read any pad if the pad ID is known. + +### Notable enhancements and fixes + +* Fixed a bug that caused all pad edit messages received at the server to go + through a single queue. Now there is a separate queue per pad as intended, + which should reduce message processing latency when many pads are active at + the same time. + # 1.8.16 ### Security fixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 15dfae99d..57ff57403 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -124,6 +124,7 @@ You can build the docs e.g. produce html, using `make docs`. At some point in th Front-end tests are found in the `tests/frontend/` folder in the repository. Run them by pointing your browser to `/tests/frontend`. Back-end tests can be run from the `src` directory, via `npm test`. +You can use `npm test -- --inspect-brk` and navigate to `edge://inspect` or `chrome://inspect` to debug the tests. ## Things you can help with Etherpad is much more than software. So if you aren't a developer then worry not, there is still a LOT you can do! A big part of what we do is community engagement. You can help in the following ways diff --git a/Dockerfile b/Dockerfile index c6339ae72..eccecab90 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,10 +3,37 @@ # https://github.com/ether/etherpad-lite # # Author: muxator +ARG BUILD_ENV=git -FROM node:14-buster-slim +FROM node:alpine AS adminbuild +RUN npm install -g pnpm@latest +WORKDIR /opt/etherpad-lite +COPY . . +RUN pnpm install +RUN pnpm run build:ui + + +FROM node:alpine AS build LABEL maintainer="Etherpad team, https://github.com/ether/etherpad-lite" +# Set these arguments when building the image from behind a proxy +ARG http_proxy= +ARG https_proxy= +ARG no_proxy= + +ARG TIMEZONE= + +RUN \ + [ -z "${TIMEZONE}" ] || { \ + apk add --no-cache tzdata && \ + cp /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && \ + echo "${TIMEZONE}" > /etc/timezone; \ + } +ENV TIMEZONE=${TIMEZONE} + +# Control the configuration file to be copied into the container. +ARG SETTINGS=./settings.json.docker + # plugins to install while building the container. By default no plugins are # installed. # If given a value, it has to be a space-separated, quoted list of plugin names. @@ -15,6 +42,22 @@ LABEL maintainer="Etherpad team, https://github.com/ether/etherpad-lite" # ETHERPAD_PLUGINS="ep_codepad ep_author_neat" ARG ETHERPAD_PLUGINS= +# local plugins to install while building the container. By default no plugins are +# installed. +# If given a value, it has to be a space-separated, quoted list of plugin names. +# +# EXAMPLE: +# ETHERPAD_LOCAL_PLUGINS="../ep_my_plugin ../ep_another_plugin" +ARG ETHERPAD_LOCAL_PLUGINS= + +# github plugins to install while building the container. By default no plugins are +# installed. +# If given a value, it has to be a space-separated, quoted list of plugin names. +# +# EXAMPLE: +# ETHERPAD_GITHUB_PLUGINS="ether/ep_plugin" +ARG ETHERPAD_GITHUB_PLUGINS= + # Control whether abiword will be installed, enabling exports to DOC/PDF/ODT formats. # By default, it is not installed. # If given any value, abiword will be installed. @@ -31,11 +74,8 @@ ARG INSTALL_ABIWORD= # INSTALL_LIBREOFFICE=true ARG INSTALL_SOFFICE= -# By default, Etherpad container is built and run in "production" mode. This is -# leaner (development dependencies are not installed) and runs faster (among -# other things, assets are minified & compressed). -ENV NODE_ENV=production - +# Install dependencies required for modifying access. +RUN apk add --no-cache shadow bash # Follow the principle of least privilege: run as unprivileged user. # # Running as non-root enables running this image in platforms like OpenShift @@ -47,6 +87,7 @@ ARG EP_HOME= ARG EP_UID=5001 ARG EP_GID=0 ARG EP_SHELL= + RUN groupadd --system ${EP_GID:+--gid "${EP_GID}" --non-unique} etherpad && \ useradd --system ${EP_UID:+--uid "${EP_UID}" --non-unique} --gid etherpad \ ${EP_HOME:+--home-dir "${EP_HOME}"} --create-home \ @@ -57,45 +98,93 @@ RUN mkdir -p "${EP_DIR}" && chown etherpad:etherpad "${EP_DIR}" # the mkdir is needed for configuration of openjdk-11-jre-headless, see # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=863199 -RUN export DEBIAN_FRONTEND=noninteractive; \ +RUN \ mkdir -p /usr/share/man/man1 && \ - apt-get -qq update && \ - apt-get -qq --no-install-recommends install \ + npm install pnpm@latest -g && \ + apk update && apk upgrade && \ + apk add --no-cache \ ca-certificates \ - git \ curl \ - ${INSTALL_ABIWORD:+abiword} \ - ${INSTALL_SOFFICE:+libreoffice} \ - && \ - apt-get -qq clean && \ - rm -rf /var/lib/apt/lists/* + git \ + ${INSTALL_ABIWORD:+abiword abiword-plugin-command} \ + ${INSTALL_SOFFICE:+libreoffice openjdk8-jre libreoffice-common} USER etherpad WORKDIR "${EP_DIR}" -COPY --chown=etherpad:etherpad ./ ./ +# etherpads version feature requires this. Only copy what is really needed +COPY --chown=etherpad:etherpad ${SETTINGS} ./settings.json +COPY --chown=etherpad:etherpad ./var ./var +COPY --chown=etherpad:etherpad ./bin ./bin +COPY --chown=etherpad:etherpad ./pnpm-workspace.yaml ./package.json ./ -# Plugins must be installed before installing Etherpad's dependencies, otherwise -# npm will try to hoist common dependencies by removing them from -# src/node_modules and installing them in the top-level node_modules. As of -# v6.14.10, npm's hoist logic appears to be buggy, because it sometimes removes -# dependencies from src/node_modules but fails to add them to the top-level -# node_modules. Even if npm correctly hoists the dependencies, the hoisting -# seems to confuse tools such as `npm outdated`, `npm update`, and some ESLint -# rules. -RUN { [ -z "${ETHERPAD_PLUGINS}" ] || \ - npm install --no-save ${ETHERPAD_PLUGINS}; } && \ - src/bin/installDeps.sh && \ - rm -rf ~/.npm + + +FROM build AS build_git +ONBUILD COPY --chown=etherpad:etherpad ./.git/HEA[D] ./.git/HEAD +ONBUILD COPY --chown=etherpad:etherpad ./.git/ref[s] ./.git/refs + +FROM build AS build_copy + + + + +FROM build_${BUILD_ENV} AS development + +ARG ETHERPAD_PLUGINS= +ARG ETHERPAD_LOCAL_PLUGINS= +ARG ETHERPAD_LOCAL_PLUGINS_ENV= +ARG ETHERPAD_GITHUB_PLUGINS= + +COPY --chown=etherpad:etherpad ./src/ ./src/ +COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/ templates/admin./src/templates/admin +COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/static/oidc ./src/static/oidc + +COPY --chown=etherpad:etherpad ./local_plugin[s] ./local_plugins/ + +RUN bash -c ./bin/installLocalPlugins.sh + +RUN bin/installDeps.sh && \ + if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_GITHUB_PLUGINS}" ]; then \ + pnpm run plugins i ${ETHERPAD_PLUGINS} ${ETHERPAD_GITHUB_PLUGINS:+--github ${ETHERPAD_GITHUB_PLUGINS}}; \ + fi + + +FROM build_${BUILD_ENV} AS production + +ARG ETHERPAD_PLUGINS= +ARG ETHERPAD_LOCAL_PLUGINS= +ARG ETHERPAD_LOCAL_PLUGINS_ENV= +ARG ETHERPAD_GITHUB_PLUGINS= + +ENV NODE_ENV=production +ENV ETHERPAD_PRODUCTION=true + +COPY --chown=etherpad:etherpad ./src ./src +COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/templates/admin ./src/templates/admin +COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/static/oidc ./src/static/oidc + +COPY --chown=etherpad:etherpad ./local_plugin[s] ./local_plugins/ + +RUN bash -c ./bin/installLocalPlugins.sh + +RUN bin/installDeps.sh && \ + if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_GITHUB_PLUGINS}" ]; then \ + pnpm run plugins i ${ETHERPAD_PLUGINS} ${ETHERPAD_GITHUB_PLUGINS:+--github ${ETHERPAD_GITHUB_PLUGINS}}; \ + fi # Copy the configuration file. -COPY --chown=etherpad:etherpad ./settings.json.docker "${EP_DIR}"/settings.json +COPY --chown=etherpad:etherpad ${SETTINGS} "${EP_DIR}"/settings.json # Fix group permissions -RUN chmod -R g=u . +# Note: For some reason increases image size from 257 to 334. +# RUN chmod -R g=u . -HEALTHCHECK --interval=20s --timeout=3s CMD curl -f http://localhost:9001 || exit 1 +USER etherpad + +HEALTHCHECK --interval=5s --timeout=3s \ + CMD curl --silent http://localhost:9001/health | grep -E "pass|ok|up" > /dev/null || exit 1 EXPOSE 9001 -CMD ["node", "src/node/server.js"] +CMD ["pnpm", "run", "prod"] diff --git a/Makefile b/Makefile deleted file mode 100644 index bc79166b3..000000000 --- a/Makefile +++ /dev/null @@ -1,32 +0,0 @@ -doc_sources = $(wildcard doc/*/*.md) $(wildcard doc/*.md) -outdoc_files = $(addprefix out/,$(doc_sources:.md=.html)) - -docassets = $(addprefix out/,$(wildcard doc/assets/*)) - -VERSION = $(shell node -e "console.log( require('./src/package.json').version )") -UNAME := $(shell uname -s) - -ensure_marked_is_installed: - set -eu; \ - hash npm; \ - if [ $(shell npm list --prefix src/bin/doc >/dev/null 2>/dev/null; echo $$?) -ne "0" ]; then \ - npm ci --prefix=src/bin/doc; \ - fi - -docs: ensure_marked_is_installed $(outdoc_files) $(docassets) - -out/doc/assets/%: doc/assets/% - mkdir -p $(@D) - cp $< $@ - -out/doc/%.html: doc/%.md - mkdir -p $(@D) - node src/bin/doc/generate.js --format=html --template=doc/template.html $< > $@ -ifeq ($(UNAME),Darwin) - sed -i '' 's/__VERSION__/${VERSION}/' $@ -else - sed -i 's/__VERSION__/${VERSION}/' $@ -endif - -clean: - rm -rf out/ diff --git a/README.md b/README.md index e0c110b3f..32e183081 100644 --- a/README.md +++ b/README.md @@ -1,185 +1,317 @@ -# A real-time collaborative editor for the web +# Etherpad: A real-time collaborative editor for the web -![Demo Etherpad Animated Jif](doc/images/etherpad_demo.gif "Etherpad in action") +![Demo Etherpad Animated Jif](doc/public/etherpad_demo.gif "Etherpad in action") -# About -Etherpad is a real-time collaborative editor [scalable to thousands of simultaneous real time users](http://scale.etherpad.org/). It provides [full data export](https://github.com/ether/etherpad-lite/wiki/Understanding-Etherpad's-Full-Data-Export-capabilities) capabilities, and runs on _your_ server, under _your_ control. +## About -# Try it out -Etherpad is extremely flexible providing you the means to modify it to solve whatever problem your community has. We provide some demo instances for you try different experiences available within Etherpad. Pad content is automatically removed after 24 hours. +Etherpad is a real-time collaborative editor [scalable to thousands of +simultaneous real time users](http://scale.etherpad.org/). It provides [full +data +export](https://github.com/ether/etherpad-lite/wiki/Understanding-Etherpad's-Full-Data-Export-capabilities) +capabilities, and runs on _your_ server, under _your_ control. -* [Rich Editing](https://rich.etherpad.com) - A full rich text WYSIWYG editor. -* [Minimalist editor](https://minimalist.etherpad.com) - A minimalist editor that can be embedded within your tool. -* [Dark Mode](https://dark.etherpad.com) - Theme settings to have Etherpad start in dark mode, ideal for using Etherpad at night or for long durations. -* [Images](https://image.etherpad.com) - Plugins to improve provide Image support within a pad. -* [Video Chat](https://video.etherpad.com) - Plugins to enable Video and Audio chat in a pad. -* [Collaboration++](https://collab.etherpad.com) - Plugins to improve the really-real time collaboration experience, suitable for busy pads. -* [Document Analysis](https://analysis.etherpad.com) - Plugins to improve author and document analysis during and post creation. -* [Scale](https://shard.etherpad.com) - Etherpad running at scale with pad sharding which allows Etherpad to scale to ∞ number of Active Pads with up to ~20,000 edits per second, per pad. +## Try it out -# Project Status +Wikimedia provide a [public Etherpad instance for you to Try Etherpad out.](https://etherpad.wikimedia.org) or [use another public Etherpad instance to see other features](https://github.com/ether/etherpad-lite/wiki/Sites-That-Run-Etherpad#sites-that-run-etherpad) + +## Project Status + +We're looking for maintainers and have some funding available. Please contact John McLear if you can help. ### Code Quality -[![Code Quality](https://github.com/ether/etherpad-lite/actions/workflows/codeql-analysis.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/codeql-analysis.yml) [![Total alerts](https://img.shields.io/lgtm/alerts/g/ether/etherpad-lite.svg?logo=lgtm&logoWidth=18&color=%2344b492)](https://lgtm.com/projects/g/ether/etherpad-lite/alerts/) [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/ether/etherpad-lite.svg?logo=lgtm&logoWidth=18&color=%2344b492)](https://lgtm.com/projects/g/ether/etherpad-lite/context:javascript) [![package.lock](https://github.com/ether/etherpad-lite/actions/workflows/lint-package-lock.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/lint-package-lock.yml) + +[![Code Quality](https://github.com/ether/etherpad-lite/actions/workflows/codeql-analysis.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/codeql-analysis.yml) ### Testing -[![Backend tests](https://github.com/ether/etherpad-lite/actions/workflows/backend-tests.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/backend-tests.yml) [![Simulated Load](https://github.com/ether/etherpad-lite/actions/workflows/load-test.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/load-test.yml) [![Rate Limit](https://github.com/ether/etherpad-lite/actions/workflows/rate-limit.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/rate-limit.yml) [![Windows Zip](https://github.com/ether/etherpad-lite/actions/workflows/windows-zip.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/windows-zip.yml) [![Docker file](https://github.com/ether/etherpad-lite/actions/workflows/dockerfile.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/dockerfile.yml) -[![Frontend admin tests powered by Sauce Labs](https://github.com/ether/etherpad-lite/actions/workflows/frontend-admin-tests.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/frontend-admin-tests.yml) [![Frontend tests powered by Sauce Labs](https://github.com/ether/etherpad-lite/actions/workflows/frontend-tests.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/frontend-tests.yml) [![Sauce Test Status](https://saucelabs.com/buildstatus/etherpad.svg)](https://saucelabs.com/u/etherpad) [![Windows Installer](https://github.com/ether/etherpad-lite/actions/workflows/windows-installer.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/windows-installer.yml) + +[![Backend tests](https://github.com/ether/etherpad-lite/actions/workflows/backend-tests.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/backend-tests.yml) +[![Simulated Load](https://github.com/ether/etherpad-lite/actions/workflows/load-test.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/load-test.yml) +[![Rate Limit](https://github.com/ether/etherpad-lite/actions/workflows/rate-limit.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/rate-limit.yml) +[![Docker file](https://github.com/ether/etherpad-lite/actions/workflows/dockerfile.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/dockerfile.yml) +[![Frontend admin tests powered by Sauce Labs](https://github.com/ether/etherpad-lite/actions/workflows/frontend-admin-tests.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/frontend-admin-tests.yml) +[![Frontend tests powered by Sauce Labs](https://github.com/ether/etherpad-lite/actions/workflows/frontend-tests.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/frontend-tests.yml) +[![Sauce Test Status](https://saucelabs.com/buildstatus/etherpad.svg)](https://saucelabs.com/u/etherpad) +[![Windows Build](https://github.com/ether/etherpad-lite/actions/workflows/windows.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/windows.yml) ### Engagement -Docker Pulls [![Discord](https://img.shields.io/discord/741309013593030667?color=%2344b492)](https://discord.com/invite/daEjfhw) [![Etherpad plugins](https://img.shields.io/endpoint?url=https%3A%2F%2Fstatic.etherpad.org%2Fshields.json&color=%2344b492 "Etherpad plugins")](https://static.etherpad.org/index.html) ![Languages](https://img.shields.io/static/v1?label=Languages&message=105&color=%2344b492) ![Translation Coverage](https://img.shields.io/static/v1?label=Languages&message=98%&color=%2344b492) -# Installation +[![Docker Pulls](https://img.shields.io/docker/pulls/etherpad/etherpad?color=%2344b492)](https://hub.docker.com/r/etherpad/etherpad) +[![Discord](https://img.shields.io/discord/741309013593030667?color=%2344b492)](https://discord.com/invite/daEjfhw) +[![Etherpad plugins](https://img.shields.io/endpoint?url=https%3A%2F%2Fstatic.etherpad.org%2Fshields.json&color=%2344b492 "Etherpad plugins")](https://static.etherpad.org/index.html) +![Languages](https://img.shields.io/static/v1?label=Languages&message=105&color=%2344b492) +![Translation Coverage](https://img.shields.io/static/v1?label=Languages&message=98%&color=%2344b492) -## Requirements -- [Node.js](https://nodejs.org/) >= **12.13.0**. +## Installation -## GNU/Linux and other UNIX-like systems +### Docker-Compose -### Quick install on Debian/Ubuntu -``` -curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - -sudo apt install -y nodejs -git clone --branch master https://github.com/ether/etherpad-lite.git && -cd etherpad-lite && -src/bin/run.sh +```yaml +services: + app: + user: "0:0" + image: etherpad/etherpad:latest + tty: true + stdin_open: true + volumes: + - plugins:/opt/etherpad-lite/src/plugin_packages + - etherpad-var:/opt/etherpad-lite/var + depends_on: + - postgres + environment: + NODE_ENV: production + ADMIN_PASSWORD: ${DOCKER_COMPOSE_APP_ADMIN_PASSWORD:-admin} + DB_CHARSET: ${DOCKER_COMPOSE_APP_DB_CHARSET:-utf8mb4} + DB_HOST: postgres + DB_NAME: ${DOCKER_COMPOSE_POSTGRES_DATABASE:-etherpad} + DB_PASS: ${DOCKER_COMPOSE_POSTGRES_PASSWORD:-admin} + DB_PORT: ${DOCKER_COMPOSE_POSTGRES_PORT:-5432} + DB_TYPE: "postgres" + DB_USER: ${DOCKER_COMPOSE_POSTGRES_USER:-admin} + # For now, the env var DEFAULT_PAD_TEXT cannot be unset or empty; it seems to be mandatory in the latest version of etherpad + DEFAULT_PAD_TEXT: ${DOCKER_COMPOSE_APP_DEFAULT_PAD_TEXT:- } + DISABLE_IP_LOGGING: ${DOCKER_COMPOSE_APP_DISABLE_IP_LOGGING:-false} + SOFFICE: ${DOCKER_COMPOSE_APP_SOFFICE:-null} + TRUST_PROXY: ${DOCKER_COMPOSE_APP_TRUST_PROXY:-true} + restart: always + ports: + - "${DOCKER_COMPOSE_APP_PORT_PUBLISHED:-9001}:${DOCKER_COMPOSE_APP_PORT_TARGET:-9001}" + + postgres: + image: postgres:15-alpine + environment: + POSTGRES_DB: ${DOCKER_COMPOSE_POSTGRES_DATABASE:-etherpad} + POSTGRES_PASSWORD: ${DOCKER_COMPOSE_POSTGRES_PASSWORD:-admin} + POSTGRES_PORT: ${DOCKER_COMPOSE_POSTGRES_PORT:-5432} + POSTGRES_USER: ${DOCKER_COMPOSE_POSTGRES_USER:-admin} + PGDATA: /var/lib/postgresql/data/pgdata + restart: always + # Exposing the port is not needed unless you want to access this database instance from the host. + # Be careful when other postgres docker container are running on the same port + # ports: + # - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + +volumes: + postgres_data: + plugins: + etherpad-var: ``` -### Manual install +### Requirements -You'll need Git and [Node.js](https://nodejs.org/) installed. +[Node.js](https://nodejs.org/) >= **18.18.2**. -**As any user (we recommend creating a separate user called etherpad):** +### Windows, macOS, Linux -1. Move to a folder where you want to install Etherpad. Clone the git repository: `git clone --branch master git://github.com/ether/etherpad-lite.git` -2. Change into the new directory containing the cloned source code: `cd etherpad-lite` -3. run `src/bin/run.sh` and open in your browser. +1. Download the latest Node.js runtime from [nodejs.org](https://nodejs.org/). +2. Install pnpm: `npm install -g pnpm` (Administrator privileges may be required). +3. Clone the repository: `git clone -b master` +4. Run `pnpm i` +5. Run `pnpm run build:etherpad` +6. Run `pnpm run prod` +7. Visit `http://localhost:9001` in your browser. -To update to the latest released version, execute `git pull origin`. The next -start with `src/bin/run.sh` will update the dependencies. +### Docker container -[Next steps](#next-steps). +Find [here](doc/docker.adoc) information on running Etherpad in a container. -## Windows +## Plugins -### Prebuilt Windows package -This package runs on any Windows machine. You can perform a manual installation via git for development purposes, but as this uses symlinks which performs unreliably on Windows, please stick to the prebuilt package if possible. +Etherpad is very customizable through plugins. -1. [Download the latest Windows package](https://etherpad.org/#download) -2. Extract the folder +![Basic install](doc/public/etherpad_basic.png "Basic Installation") -Run `start.bat` and open in your browser. You like it? [Next steps](#next-steps). +![Full Features](doc/public/etherpad_full_features.png "You can add a lot of plugins !") -### Manually install on Windows -You'll need [node.js](https://nodejs.org) and (optionally, though recommended) git. +### Available Plugins -1. Grab the source, either - - download - - or `git clone --branch master https://github.com/ether/etherpad-lite.git` -2. With a "Run as administrator" command prompt execute - `src\bin\installOnWindows.bat` +For a list of available plugins, see the [plugins +site](https://static.etherpad.org). -Now, run `start.bat` and open in your browser. +### Plugin Installation -Update to the latest version with `git pull origin`, then run -`src\bin\installOnWindows.bat`, again. +You can install plugins from the admin web interface (e.g., +http://127.0.0.1:9001/admin/plugins). -If cloning to a subdirectory within another project, you may need to do the following: +Alternatively, you can install plugins from the command line: -1. Start the server manually (e.g. `node src/node/server.js`) -2. Edit the db `filename` in `settings.json` to the relative directory with the file (e.g. `application/lib/etherpad-lite/var/dirty.db`) -3. Add auto-generated files to the main project `.gitignore` - -## Docker container - -Find [here](doc/docker.md) information on running Etherpad in a container. - -# Next Steps - -## Tweak the settings -You can modify the settings in `settings.json`. -If you need to handle multiple settings files, you can pass the path to a -settings file to `src/bin/run.sh` using the `-s|--settings` option: this allows -you to run multiple Etherpad instances from the same installation. -Similarly, `--credentials` can be used to give a settings override file, `--apikey` to give a different APIKEY.txt file and `--sessionkey` to give a non-default SESSIONKEY.txt. -**Each configuration parameter can also be set via an environment variable**, using the syntax `"${ENV_VAR}"` or `"${ENV_VAR:default_value}"`. For details, refer to `settings.json.template`. -Once you have access to your `/admin` section settings can be modified through the web browser. - -If you are planning to use Etherpad in a production environment, you should use a dedicated database such as `mysql`, since the `dirtyDB` database driver is only for testing and/or development purposes. - -## Secure your installation -If you have enabled authentication in `users` section in `settings.json`, it is a good security practice to **store hashes instead of plain text passwords** in that file. This is _especially_ advised if you are running a production installation. - -Please install [ep_hash_auth plugin](https://www.npmjs.com/package/ep_hash_auth) and configure it. -If you prefer, `ep_hash_auth` also gives you the option of storing the users in a custom directory in the file system, without having to edit `settings.json` and restart Etherpad each time. - -## Customize functionalities with plugins - -![Basic install](doc/images/etherpad_basic.png "Basic Installation") - -![Full Features](doc/images/etherpad_full_features.png "You can add a lot of plugins !") - -Etherpad is very customizable through plugins. Instructions for installing themes and plugins can be found in [the plugin wiki article](https://github.com/ether/etherpad-lite/wiki/Available-Plugins). - -## Getting the full features -Run the following command in your Etherpad folder to get all of the features visible in the demo gif: - -``` -npm install --no-save --legacy-peer-deps ep_headings2 ep_markdown ep_comments_page ep_align ep_font_color ep_webrtc ep_embedded_hyperlinks2 +```sh +cd /path/to/etherpad-lite +pnpm run plugins i ep_${plugin_name} ``` -## Customize the style with skin variants +Also see [the plugin wiki +article](https://github.com/ether/etherpad-lite/wiki/Available-Plugins). -Open in your browser and start playing ! +### Suggested Plugins -![Skin Variant](doc/images/etherpad_skin_variants.gif "Skin variants") +Run the following command in your Etherpad folder to get all of the features +visible in the above demo gif: + +```sh +pnpm run plugins i \ + ep_align \ + ep_comments_page \ + ep_embedded_hyperlinks2 \ + ep_font_color \ + ep_headings2 \ + ep_markdown \ + ep_webrtc +``` + +For user authentication, you are encouraged to run an [OpenID +Connect](https://openid.net/connect/) identity provider (OP) and install the +following plugins: + + * [ep_openid_connect](https://github.com/ether/ep_openid_connect#readme) to + authenticate against your OP. + * [ep_guest](https://github.com/ether/ep_guest#readme) to create a + "guest" account that has limited access (e.g., read-only access). + * [ep_user_displayname](https://github.com/ether/ep_user_displayname#readme) + to automatically populate each user's displayed name from your OP. + * [ep_stable_authorid](https://github.com/ether/ep_stable_authorid#readme) so + that each user's chosen color, display name, comment ownership, etc. is + strongly linked to their account. + +### Upgrade Etherpad + +Run the following command in your Etherpad folder to upgrade + +1. Stop any running Etherpad (manual, systemd ...) +2. Get present version +```sh +git -P tag --contains +``` +3. List versions available +```sh +git -P tag --list "v*" --merged +``` +4. Select the version +```sh +git checkout v2.2.5 +git switch -c v2.2.5 +``` +5. Upgrade Etherpad +```sh +./bin/run.sh +``` +6. Stop with [CTRL-C] +7. Restart your Etherpad service + +## Next Steps + +### Tweak the settings + +You can modify the settings in `settings.json`. If you need to handle multiple +settings files, you can pass the path to a settings file to `bin/run.sh` +using the `-s|--settings` option: this allows you to run multiple Etherpad +instances from the same installation. Similarly, `--credentials` can be used to +give a settings override file, `--apikey` to give a different APIKEY.txt file +and `--sessionkey` to give a non-default `SESSIONKEY.txt`. **Each configuration +parameter can also be set via an environment variable**, using the syntax +`"${ENV_VAR}"` or `"${ENV_VAR:default_value}"`. For details, refer to +`settings.json.template`. Once you have access to your `/admin` section, +settings can be modified through the web browser. + +If you are planning to use Etherpad in a production environment, you should use +a dedicated database such as `mysql`, since the `dirtyDB` database driver is +only for testing and/or development purposes. + +### Secure your installation + +If you have enabled authentication in `users` section in `settings.json`, it is +a good security practice to **store hashes instead of plain text passwords** in +that file. This is _especially_ advised if you are running a production +installation. + +Please install [ep_hash_auth plugin](https://www.npmjs.com/package/ep_hash_auth) +and configure it. If you prefer, `ep_hash_auth` also gives you the option of +storing the users in a custom directory in the file system, without having to +edit `settings.json` and restart Etherpad each time. + +### Customize the style with skin variants + +Open http://127.0.0.1:9001/p/test#skinvariantsbuilder in your browser and start +playing! + +![Skin Variant](doc/public/etherpad_skin_variants.gif "Skin variants") ## Helpful resources -The [wiki](https://github.com/ether/etherpad-lite/wiki) is your one-stop resource for Tutorials and How-to's. + +The [wiki](https://github.com/ether/etherpad-lite/wiki) is your one-stop +resource for Tutorials and How-to's. Documentation can be found in `doc/`. -# Development +## Development -## Things you should know +### Things you should know -You can debug Etherpad using `src/bin/debugRun.sh`. +You can debug Etherpad using `bin/debugRun.sh`. -You can run Etherpad quickly launching `src/bin/fastRun.sh`. It's convenient for +You can run Etherpad quickly launching `bin/fastRun.sh`. It's convenient for developers and advanced users. Be aware that it will skip the dependencies -update, so remember to run `src/bin/installDeps.sh` after installing a new +update, so remember to run `bin/installDeps.sh` after installing a new dependency or upgrading version. -If you want to find out how Etherpad's `Easysync` works (the library that makes it really realtime), start with this [PDF](https://github.com/ether/etherpad-lite/raw/master/doc/easysync/easysync-full-description.pdf) (complex, but worth reading). +If you want to find out how Etherpad's `Easysync` works (the library that makes +it really realtime), start with this +[PDF](https://github.com/ether/etherpad-lite/raw/master/doc/easysync/easysync-full-description.pdf) +(complex, but worth reading). -## Contributing -Read our [**Developer Guidelines**](https://github.com/ether/etherpad-lite/blob/master/CONTRIBUTING.md) +### Contributing -# Get in touch -The official channel for contacting the development team is via the [Github issues](https://github.com/ether/etherpad-lite/issues). +Read our [**Developer +Guidelines**](https://github.com/ether/etherpad-lite/blob/master/CONTRIBUTING.md) -For **responsible disclosure of vulnerabilities**, please write a mail to the maintainers (a.mux@inwind.it and contact@etherpad.org). -Join the official [Etherpad Discord Channel](https://discord.com/invite/daEjfhw) +### HTTP API -# HTTP API -Etherpad is designed to be easily embeddable and provides a [HTTP API](https://github.com/ether/etherpad-lite/wiki/HTTP-API) -that allows your web application to manage pads, users and groups. It is recommended to use the [available client implementations](https://github.com/ether/etherpad-lite/wiki/HTTP-API-client-libraries) in order to interact with this API. +Etherpad is designed to be easily embeddable and provides a [HTTP +API](https://github.com/ether/etherpad-lite/wiki/HTTP-API) that allows your web +application to manage pads, users and groups. It is recommended to use the +[available client +implementations](https://github.com/ether/etherpad-lite/wiki/HTTP-API-client-libraries) +in order to interact with this API. -OpenAPI (previously swagger) definitions for the API are exposed under `/api/openapi.json`. +OpenAPI (previously swagger) definitions for the API are exposed under +`/api/openapi.json`. -# jQuery plugin -There is a [jQuery plugin](https://github.com/ether/etherpad-lite-jquery-plugin) that helps you to embed Pads into your website. +### jQuery plugin -# Plugin Framework -Etherpad offers a plugin framework, allowing you to easily add your own features. By default your Etherpad is extremely light-weight and it's up to you to customize your experience. Once you have Etherpad installed you should [visit the plugin page](https://static.etherpad.org/) and take control. +There is a [jQuery plugin](https://github.com/ether/etherpad-lite-jquery-plugin) +that helps you to embed Pads into your website. -# Translations / Localizations (i18n / l10n) -Etherpad comes with translations into all languages thanks to the team at [TranslateWiki](https://translatewiki.net/). +### Plugin Framework -If you require translations in [plugins](https://static.etherpad.org/) please send pull request to each plugin individually. +Etherpad offers a plugin framework, allowing you to easily add your own +features. By default your Etherpad is extremely light-weight and it's up to you +to customize your experience. Once you have Etherpad installed you should [visit +the plugin page](https://static.etherpad.org/) and take control. + +### Translations / Localizations (i18n / l10n) + +Etherpad comes with translations into all languages thanks to the team at +[TranslateWiki](https://translatewiki.net/). + +If you require translations in [plugins](https://static.etherpad.org/) please +send pull request to each plugin individually. + +## FAQ -# FAQ Visit the **[FAQ](https://github.com/ether/etherpad-lite/wiki/FAQ)**. -# License +## Get in touch + +The official channel for contacting the development team is via the [GitHub +issues](https://github.com/ether/etherpad-lite/issues). + +For **responsible disclosure of vulnerabilities**, please write a mail to the +maintainers (a.mux@inwind.it and contact@etherpad.org). + +Join the official [Etherpad Discord +Channel](https://discord.com/invite/daEjfhw). + +## License + [Apache License v2](http://www.apache.org/licenses/LICENSE-2.0.html) diff --git a/admin/.eslintrc.cjs b/admin/.eslintrc.cjs new file mode 100644 index 000000000..d6c953795 --- /dev/null +++ b/admin/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/admin/.gitignore b/admin/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/admin/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/admin/README.md b/admin/README.md new file mode 100644 index 000000000..0d6babedd --- /dev/null +++ b/admin/README.md @@ -0,0 +1,30 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default { + // other rules... + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +} +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/admin/index.html b/admin/index.html new file mode 100644 index 000000000..8863894ed --- /dev/null +++ b/admin/index.html @@ -0,0 +1,14 @@ + + + + + + Etherpad Admin Dashboard + + + +
+
+ + + diff --git a/admin/package.json b/admin/package.json new file mode 100644 index 000000000..df62616b4 --- /dev/null +++ b/admin/package.json @@ -0,0 +1,42 @@ +{ + "name": "admin", + "private": true, + "version": "2.3.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "build-copy": "tsc && vite build --outDir ../src/templates/admin --emptyOutDir", + "preview": "vite preview" + }, + "dependencies": { + "@radix-ui/react-switch": "^1.2.4" + }, + "devDependencies": { + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-toast": "^1.2.14", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@typescript-eslint/eslint-plugin": "^8.34.0", + "@typescript-eslint/parser": "^8.34.0", + "@vitejs/plugin-react-swc": "^3.10.2", + "eslint": "^9.28.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "i18next": "^25.2.1", + "i18next-browser-languagedetector": "^8.2.0", + "lucide-react": "^0.515.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-hook-form": "^7.57.0", + "react-i18next": "^15.5.3", + "react-router-dom": "^7.6.2", + "socket.io-client": "^4.8.1", + "typescript": "^5.8.2", + "vite": "^6.3.5", + "vite-plugin-static-copy": "^3.0.0", + "vite-plugin-svgr": "^4.3.0", + "zustand": "^5.0.5" + } +} diff --git a/admin/public/Karla-Bold.ttf b/admin/public/Karla-Bold.ttf new file mode 100644 index 000000000..2348e0072 Binary files /dev/null and b/admin/public/Karla-Bold.ttf differ diff --git a/admin/public/Karla-BoldItalic.ttf b/admin/public/Karla-BoldItalic.ttf new file mode 100644 index 000000000..3c0e045ec Binary files /dev/null and b/admin/public/Karla-BoldItalic.ttf differ diff --git a/admin/public/Karla-ExtraBold.ttf b/admin/public/Karla-ExtraBold.ttf new file mode 100644 index 000000000..f18471195 Binary files /dev/null and b/admin/public/Karla-ExtraBold.ttf differ diff --git a/admin/public/Karla-ExtraBoldItalic.ttf b/admin/public/Karla-ExtraBoldItalic.ttf new file mode 100644 index 000000000..3799659c0 Binary files /dev/null and b/admin/public/Karla-ExtraBoldItalic.ttf differ diff --git a/admin/public/Karla-ExtraLight.ttf b/admin/public/Karla-ExtraLight.ttf new file mode 100644 index 000000000..0f8642c02 Binary files /dev/null and b/admin/public/Karla-ExtraLight.ttf differ diff --git a/admin/public/Karla-ExtraLightItalic.ttf b/admin/public/Karla-ExtraLightItalic.ttf new file mode 100644 index 000000000..bb328e175 Binary files /dev/null and b/admin/public/Karla-ExtraLightItalic.ttf differ diff --git a/admin/public/Karla-Italic.ttf b/admin/public/Karla-Italic.ttf new file mode 100644 index 000000000..1853cbe4e Binary files /dev/null and b/admin/public/Karla-Italic.ttf differ diff --git a/admin/public/Karla-Light.ttf b/admin/public/Karla-Light.ttf new file mode 100644 index 000000000..46457ece7 Binary files /dev/null and b/admin/public/Karla-Light.ttf differ diff --git a/admin/public/Karla-LightItalic.ttf b/admin/public/Karla-LightItalic.ttf new file mode 100644 index 000000000..3b0f01ff1 Binary files /dev/null and b/admin/public/Karla-LightItalic.ttf differ diff --git a/admin/public/Karla-Medium.ttf b/admin/public/Karla-Medium.ttf new file mode 100644 index 000000000..9066b49c4 Binary files /dev/null and b/admin/public/Karla-Medium.ttf differ diff --git a/admin/public/Karla-MediumItalic.ttf b/admin/public/Karla-MediumItalic.ttf new file mode 100644 index 000000000..ea9535355 Binary files /dev/null and b/admin/public/Karla-MediumItalic.ttf differ diff --git a/admin/public/Karla-Regular.ttf b/admin/public/Karla-Regular.ttf new file mode 100644 index 000000000..c164d0047 Binary files /dev/null and b/admin/public/Karla-Regular.ttf differ diff --git a/admin/public/Karla-SemiBold.ttf b/admin/public/Karla-SemiBold.ttf new file mode 100644 index 000000000..82e0c5abf Binary files /dev/null and b/admin/public/Karla-SemiBold.ttf differ diff --git a/admin/public/Karla-SemiBoldItalic.ttf b/admin/public/Karla-SemiBoldItalic.ttf new file mode 100644 index 000000000..77c19ef69 Binary files /dev/null and b/admin/public/Karla-SemiBoldItalic.ttf differ diff --git a/admin/public/ep_admin_pads/ar.json b/admin/public/ep_admin_pads/ar.json new file mode 100644 index 000000000..746946edf --- /dev/null +++ b/admin/public/ep_admin_pads/ar.json @@ -0,0 +1,28 @@ +{ + "@metadata": { + "authors": [ + "Meno25", + "محمد أحمد عبد الفتاح" + ] + }, + "ep_adminpads2_action": "فعل", + "ep_adminpads2_autoupdate-label": "التحديث التلقائي على تغييرات الوسادة", + "ep_adminpads2_autoupdate.title": "لتمكين أو تعطيل التحديثات التلقائية للاستعلام الحالي.", + "ep_adminpads2_confirm": "هل تريد حقًا حذف الوسادة {{padID}}؟", + "ep_adminpads2_delete.value": "حذف", + "ep_adminpads2_last-edited": "آخر تعديل", + "ep_adminpads2_loading": "جارٍ التحميل...", + "ep_adminpads2_manage-pads": "إدارة الفوط", + "ep_adminpads2_no-results": "لا توجد نتائج.", + "ep_adminpads2_pad-user-count": "عدد المستخدمين الوسادة", + "ep_adminpads2_padname": "بادنام", + "ep_adminpads2_search-box.placeholder": "مصطلح البحث", + "ep_adminpads2_search-button.value": "بحث", + "ep_adminpads2_search-done": "اكتمل البحث", + "ep_adminpads2_search-error-explanation": "واجه الخادم خطأً أثناء البحث عن منصات:", + "ep_adminpads2_search-error-title": "فشل في الحصول على قائمة الوسادة", + "ep_adminpads2_search-heading": "ابحث عن الفوط", + "ep_adminpads2_title": "إدارة الوسادة", + "ep_adminpads2_unknown-error": "خطأ غير معروف", + "ep_adminpads2_unknown-status": "حالة غير معروفة" +} diff --git a/admin/public/ep_admin_pads/bn.json b/admin/public/ep_admin_pads/bn.json new file mode 100644 index 000000000..0048b52bb --- /dev/null +++ b/admin/public/ep_admin_pads/bn.json @@ -0,0 +1,23 @@ +{ + "@metadata": { + "authors": [ + "আজিজ", + "আফতাবুজ্জামান" + ] + }, + "ep_adminpads2_action": "কার্য", + "ep_adminpads2_delete.value": "মুছে ফেলুন", + "ep_adminpads2_last-edited": "সর্বশেষ সম্পাদিত", + "ep_adminpads2_loading": "লোড হচ্ছে...", + "ep_adminpads2_manage-pads": "প্যাড পরিচালনা করুন", + "ep_adminpads2_no-results": "ফলাফল নেই", + "ep_adminpads2_padname": "প্যাডের নাম", + "ep_adminpads2_search-button.value": "অনুসন্ধান", + "ep_adminpads2_search-done": "অনুসন্ধান সম্পূর্ণ", + "ep_adminpads2_search-error-explanation": "প্যাড অনুসন্ধান করার সময় সার্ভার একটি ত্রুটির সম্মুখীন হয়েছে:", + "ep_adminpads2_search-error-title": "প্যাডের তালিকা পেতে ব্যর্থ", + "ep_adminpads2_search-heading": "প্যাড অনুসন্ধান করুন", + "ep_adminpads2_title": "প্যাড প্রশাসন", + "ep_adminpads2_unknown-error": "অজানা ত্রুটি", + "ep_adminpads2_unknown-status": "অজানা অবস্থা" +} diff --git a/admin/public/ep_admin_pads/ca.json b/admin/public/ep_admin_pads/ca.json new file mode 100644 index 000000000..1d4e34216 --- /dev/null +++ b/admin/public/ep_admin_pads/ca.json @@ -0,0 +1,27 @@ +{ + "@metadata": { + "authors": [ + "Mguix" + ] + }, + "ep_adminpads2_action": "Acció", + "ep_adminpads2_autoupdate-label": "Actualització automàtica en cas de canvis de pad", + "ep_adminpads2_autoupdate.title": "Activa o desactiva les actualitzacions automàtiques per a la consulta actual.", + "ep_adminpads2_confirm": "Esteu segur que voleu suprimir el pad {{padID}}?", + "ep_adminpads2_delete.value": "Esborrar", + "ep_adminpads2_last-edited": "Darrera modificació", + "ep_adminpads2_loading": "S’està carregant…", + "ep_adminpads2_manage-pads": "Gestiona els pads", + "ep_adminpads2_no-results": "No hi ha cap resultat", + "ep_adminpads2_pad-user-count": "Nombre d'usuaris de pads", + "ep_adminpads2_padname": "Nom del pad", + "ep_adminpads2_search-box.placeholder": "Terme de cerca", + "ep_adminpads2_search-button.value": "Cercar", + "ep_adminpads2_search-done": "Cerca completa", + "ep_adminpads2_search-error-explanation": "El servidor ha trobat un error mentre buscava pads:", + "ep_adminpads2_search-error-title": "No s'ha pogut obtenir la llista del pad", + "ep_adminpads2_search-heading": "Cerca pads", + "ep_adminpads2_title": "Administració del pad", + "ep_adminpads2_unknown-error": "Error desconegut", + "ep_adminpads2_unknown-status": "Estat desconegut" +} diff --git a/admin/public/ep_admin_pads/cs.json b/admin/public/ep_admin_pads/cs.json new file mode 100644 index 000000000..19e92894d --- /dev/null +++ b/admin/public/ep_admin_pads/cs.json @@ -0,0 +1,27 @@ +{ + "@metadata": { + "authors": [ + "Spotter" + ] + }, + "ep_adminpads2_action": "Akce", + "ep_adminpads2_autoupdate-label": "Automatická aktualizace změn Padu", + "ep_adminpads2_autoupdate.title": "Povolí nebo zakáže automatické aktualizace pro aktuální dotaz.", + "ep_adminpads2_confirm": "Opravdu chcete odstranit pad {{padID}}?", + "ep_adminpads2_delete.value": "Smazat", + "ep_adminpads2_last-edited": "Naposledy upraveno", + "ep_adminpads2_loading": "Načítání…", + "ep_adminpads2_manage-pads": "Spravovat pady", + "ep_adminpads2_no-results": "Žádné výsledky", + "ep_adminpads2_pad-user-count": "Počet uživatelů padu", + "ep_adminpads2_padname": "Název padu", + "ep_adminpads2_search-box.placeholder": "Hledaný výraz", + "ep_adminpads2_search-button.value": "Hledat", + "ep_adminpads2_search-done": "Hledání dokončeno", + "ep_adminpads2_search-error-explanation": "Při hledání padů došlo k chybě serveru:", + "ep_adminpads2_search-error-title": "Seznam padů se nepodařilo získat", + "ep_adminpads2_search-heading": "Hledat pady", + "ep_adminpads2_title": "Správa Padu", + "ep_adminpads2_unknown-error": "Neznámá chyba", + "ep_adminpads2_unknown-status": "Neznámý stav" +} diff --git a/admin/public/ep_admin_pads/cy.json b/admin/public/ep_admin_pads/cy.json new file mode 100644 index 000000000..02546da90 --- /dev/null +++ b/admin/public/ep_admin_pads/cy.json @@ -0,0 +1,27 @@ +{ + "@metadata": { + "authors": [ + "Robin Owain" + ] + }, + "ep_adminpads2_action": "Gweithred", + "ep_adminpads2_autoupdate-label": "Diweddaru newidiadau pad yn otomatig", + "ep_adminpads2_autoupdate.title": "Galluogi neu analluogi diweddaru'r ymholiad cyfredol.", + "ep_adminpads2_confirm": "Siwr eich bod am ddileu'r pad {{padID}}?", + "ep_adminpads2_delete.value": "Dileu", + "ep_adminpads2_last-edited": "Golygwyd ddiwethaf", + "ep_adminpads2_loading": "Wrthi'n llwytho...", + "ep_adminpads2_manage-pads": "Rheoli'r padiau", + "ep_adminpads2_no-results": "Dim canlyniad", + "ep_adminpads2_pad-user-count": "Cyfri defnyddiwr pad", + "ep_adminpads2_padname": "Enwpad", + "ep_adminpads2_search-box.placeholder": "Term chwilio", + "ep_adminpads2_search-button.value": "Chwilio", + "ep_adminpads2_search-done": "Wedi gorffen", + "ep_adminpads2_search-error-explanation": "Nam ar y gweinydd wrth chwilio'r padiau:", + "ep_adminpads2_search-error-title": "Methwyd a chael y rhestr pad", + "ep_adminpads2_search-heading": "Chwilio am badiau", + "ep_adminpads2_title": "Gweinyddiaeth y pad", + "ep_adminpads2_unknown-error": "Nam o ryw fath", + "ep_adminpads2_unknown-status": "Statws anhysbys" +} diff --git a/admin/public/ep_admin_pads/da.json b/admin/public/ep_admin_pads/da.json new file mode 100644 index 000000000..a5303b9cb --- /dev/null +++ b/admin/public/ep_admin_pads/da.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Saederup92" + ] + }, + "ep_adminpads2_action": "Handling", + "ep_adminpads2_delete.value": "Slet", + "ep_adminpads2_last-edited": "Sidst redigeret", + "ep_adminpads2_loading": "Indlæser...", + "ep_adminpads2_no-results": "Ingen resultater", + "ep_adminpads2_unknown-error": "Ukendt fejl", + "ep_adminpads2_unknown-status": "Ukendt status" +} diff --git a/admin/public/ep_admin_pads/de.json b/admin/public/ep_admin_pads/de.json new file mode 100644 index 000000000..67dd73ddf --- /dev/null +++ b/admin/public/ep_admin_pads/de.json @@ -0,0 +1,33 @@ +{ + "@metadata": { + "authors": [ + "Brettchenweber", + "Justman10000", + "Lorisobi", + "SamTV", + "Umlaut", + "Zunkelty" + ] + }, + "ep_adminpads2_action": "Aktion", + "ep_adminpads2_autoupdate-label": "Automatisch bei Pad-Änderungen updaten", + "ep_adminpads2_autoupdate.title": "Aktiviert oder deaktiviert automatische Aktualisierungen für die aktuelle Abfrage.", + "ep_adminpads2_confirm": "Willst du das Pad {{padID}} wirklich löschen?", + "ep_adminpads2_delete.value": "Löschen", + "ep_adminpads2_cleanup": "Historie aufräumen", + "ep_adminpads2_last-edited": "Zuletzt bearbeitet", + "ep_adminpads2_loading": "Lädt...", + "ep_adminpads2_manage-pads": "Pads verwalten", + "ep_adminpads2_no-results": "Keine Ergebnisse", + "ep_adminpads2_pad-user-count": "Nutzerzahl des Pads", + "ep_adminpads2_padname": "Padname", + "ep_adminpads2_search-box.placeholder": "Suchbegriff", + "ep_adminpads2_search-button.value": "Suche", + "ep_adminpads2_search-done": "Suche vollendet", + "ep_adminpads2_search-error-explanation": "Der Server ist bei der Suche nach Pads auf einen Fehler gestoßen:", + "ep_adminpads2_search-error-title": "Pad-Liste konnte nicht abgerufen werden", + "ep_adminpads2_search-heading": "Nach Pads suchen", + "ep_adminpads2_title": "Pad-Verwaltung", + "ep_adminpads2_unknown-error": "Unbekannter Fehler", + "ep_adminpads2_unknown-status": "Unbekannter Status" +} diff --git a/admin/public/ep_admin_pads/diq.json b/admin/public/ep_admin_pads/diq.json new file mode 100644 index 000000000..983680965 --- /dev/null +++ b/admin/public/ep_admin_pads/diq.json @@ -0,0 +1,28 @@ +{ + "@metadata": { + "authors": [ + "1917 Ekim Devrimi", + "Mirzali" + ] + }, + "ep_adminpads2_action": "Hereketi", + "ep_adminpads2_autoupdate-label": "Vurnayışanê pedi otomatik rocane kerê", + "ep_adminpads2_autoupdate.title": "Persê mewcudi rê rocaneyışanê otomatika aktiv ke ya zi dewrê ra vecê", + "ep_adminpads2_confirm": "Şıma qayılê pedê {{padID}} bıesternê?", + "ep_adminpads2_delete.value": "Bestere", + "ep_adminpads2_last-edited": "Vurnayışo peyên", + "ep_adminpads2_loading": "Bar beno...", + "ep_adminpads2_manage-pads": "Pedan idare kerê", + "ep_adminpads2_no-results": "Netice çıniyo", + "ep_adminpads2_pad-user-count": "Amarê karberanê pedi", + "ep_adminpads2_padname": "Padname", + "ep_adminpads2_search-box.placeholder": "termê cıgêrayış", + "ep_adminpads2_search-button.value": "Cı geyre", + "ep_adminpads2_search-done": "Cıgeyrayışi temam", + "ep_adminpads2_search-error-explanation": "Server cıgeyrayışê pedan de yew xetaya raşt ame", + "ep_adminpads2_search-error-title": "Lista pedi nêgêriye", + "ep_adminpads2_search-heading": "Pedan cıgeyrayış", + "ep_adminpads2_title": "İdarey pedi", + "ep_adminpads2_unknown-error": "Xetaya nêzanıtiye", + "ep_adminpads2_unknown-status": "Weziyeto nêzanaye" +} diff --git a/admin/public/ep_admin_pads/dsb.json b/admin/public/ep_admin_pads/dsb.json new file mode 100644 index 000000000..363732a20 --- /dev/null +++ b/admin/public/ep_admin_pads/dsb.json @@ -0,0 +1,27 @@ +{ + "@metadata": { + "authors": [ + "Michawiki" + ] + }, + "ep_adminpads2_action": "Akcija", + "ep_adminpads2_autoupdate-label": "Pśi změnach na zapisniku awtomatiski aktualizěrowaś", + "ep_adminpads2_autoupdate.title": "Zmóžnja abo znjemóžnja awtomatiske aktualizacije za aktualne wótpšašowanje.", + "ep_adminpads2_confirm": "Cośo napšawdu zapisnik {{padID}} lašowaś?", + "ep_adminpads2_delete.value": "Lašowaś", + "ep_adminpads2_last-edited": "Slědna změna", + "ep_adminpads2_loading": "Zacytujo se...", + "ep_adminpads2_manage-pads": "Zapisniki zastojaś", + "ep_adminpads2_no-results": "Žedne wuslědki", + "ep_adminpads2_pad-user-count": "Licba wužywarjow zapisnika", + "ep_adminpads2_padname": "Mě zapisnika", + "ep_adminpads2_search-box.placeholder": "Pytańske zapśimjeśe", + "ep_adminpads2_search-button.value": "Pytaś", + "ep_adminpads2_search-done": "Pytanje dokóńcone", + "ep_adminpads2_search-error-explanation": "Serwer jo starcył na zmólku, mjaztym až jo pytał za zapisnikami:", + "ep_adminpads2_search-error-title": "Lisćina zapisnikow njedajo se wobstaraś", + "ep_adminpads2_search-heading": "Za zapisnikami pytaś", + "ep_adminpads2_title": "Zapisnikowa administracija", + "ep_adminpads2_unknown-error": "Njeznata zmólka", + "ep_adminpads2_unknown-status": "Njeznaty status" +} diff --git a/admin/public/ep_admin_pads/el.json b/admin/public/ep_admin_pads/el.json new file mode 100644 index 000000000..77b6af3dd --- /dev/null +++ b/admin/public/ep_admin_pads/el.json @@ -0,0 +1,16 @@ +{ + "@metadata": { + "authors": [ + "Norhorn" + ] + }, + "ep_adminpads2_delete.value": "Διαγραφή", + "ep_adminpads2_last-edited": "Τελευταία απεξεργασία", + "ep_adminpads2_loading": "Φόρτωση…", + "ep_adminpads2_no-results": "Κανένα αποτέλεσμα", + "ep_adminpads2_search-box.placeholder": "Αναζήτηση όρων", + "ep_adminpads2_search-button.value": "Αναζήτηση", + "ep_adminpads2_search-done": "Ολοκλήρωση αναζήτησης", + "ep_adminpads2_unknown-error": "Άγνωστο σφάλμα", + "ep_adminpads2_unknown-status": "Άγνωστη κατάσταση" +} diff --git a/admin/public/ep_admin_pads/en.json b/admin/public/ep_admin_pads/en.json new file mode 100644 index 000000000..76354c640 --- /dev/null +++ b/admin/public/ep_admin_pads/en.json @@ -0,0 +1,23 @@ +{ + "ep_adminpads2_action": "Action", + "ep_adminpads2_autoupdate-label": "Auto-update on pad changes", + "ep_adminpads2_autoupdate.title": "Enables or disables automatic updates for the current query.", + "ep_adminpads2_confirm": "Do you really want to delete the pad {{padID}}?", + "ep_adminpads2_delete.value": "Delete", + "ep_adminpads2_cleanup": "Cleanup revisions", + "ep_adminpads2_last-edited": "Last edited", + "ep_adminpads2_loading": "Loading…", + "ep_adminpads2_manage-pads": "Manage pads", + "ep_adminpads2_no-results": "No results", + "ep_adminpads2_pad-user-count": "Pad user count", + "ep_adminpads2_padname": "Padname", + "ep_adminpads2_search-box.placeholder": "Search term", + "ep_adminpads2_search-button.value": "Search", + "ep_adminpads2_search-done": "Search complete", + "ep_adminpads2_search-error-explanation": "The server encountered an error while searching for pads:", + "ep_adminpads2_search-error-title": "Failed to get pad list", + "ep_adminpads2_search-heading": "Search for pads", + "ep_adminpads2_title": "Pad administration", + "ep_adminpads2_unknown-error": "Unknown error", + "ep_adminpads2_unknown-status": "Unknown status" +} diff --git a/admin/public/ep_admin_pads/eu.json b/admin/public/ep_admin_pads/eu.json new file mode 100644 index 000000000..71d9dfe79 --- /dev/null +++ b/admin/public/ep_admin_pads/eu.json @@ -0,0 +1,27 @@ +{ + "@metadata": { + "authors": [ + "Izendegi" + ] + }, + "ep_adminpads2_action": "Ekintza", + "ep_adminpads2_autoupdate-label": "Automatikoki eguneratu pad-aren aldaketak daudenean", + "ep_adminpads2_autoupdate.title": "Oraingo kontsultarako eguneratze automatikoak gaitu edo desgaitzen du.", + "ep_adminpads2_confirm": "Ziur zaude {{padID}} pad-a ezabatu nahi duzula?", + "ep_adminpads2_delete.value": "Ezabatu", + "ep_adminpads2_last-edited": "Azkenengoz editatua", + "ep_adminpads2_loading": "Kargatzen...", + "ep_adminpads2_manage-pads": "Kudeatu pad-ak", + "ep_adminpads2_no-results": "Emaitzarik ez", + "ep_adminpads2_pad-user-count": "Pad-erabiltzaile kopurua", + "ep_adminpads2_padname": "Pad-izena", + "ep_adminpads2_search-box.placeholder": "Bilaketa testua", + "ep_adminpads2_search-button.value": "Bilatu", + "ep_adminpads2_search-done": "Bilaketa osatu da", + "ep_adminpads2_search-error-explanation": "Zerbitzariak errore bat izan du pad-ak bilatzean:", + "ep_adminpads2_search-error-title": "Pad-zerrenda eskuratzeak huts egin du", + "ep_adminpads2_search-heading": "Bilatu pad-ak", + "ep_adminpads2_title": "Pad-en kudeaketa", + "ep_adminpads2_unknown-error": "Errore ezezaguna", + "ep_adminpads2_unknown-status": "Egoera ezezaguna" +} diff --git a/admin/public/ep_admin_pads/ff.json b/admin/public/ep_admin_pads/ff.json new file mode 100644 index 000000000..8cb5aea99 --- /dev/null +++ b/admin/public/ep_admin_pads/ff.json @@ -0,0 +1,27 @@ +{ + "@metadata": { + "authors": [ + "Ibrahima Malal Sarr" + ] + }, + "ep_adminpads2_action": "Baɗal", + "ep_adminpads2_autoupdate-label": "Hesɗitin e jaajol tuma baylagol faɗo", + "ep_adminpads2_autoupdate.title": "Hurminat walla daaƴa kesɗitine jaaje wonannde ɗaɓɓitannde wonaande.", + "ep_adminpads2_confirm": "Aɗa yiɗi e jaati momtude faɗo {{padID}}?", + "ep_adminpads2_delete.value": "Momtu", + "ep_adminpads2_last-edited": "Taƴtaa sakket", + "ep_adminpads2_loading": "Nana loowa…", + "ep_adminpads2_manage-pads": "Toppito paɗe", + "ep_adminpads2_no-results": "Alaa njaltudi", + "ep_adminpads2_pad-user-count": "Limoore huutorɓe faɗo", + "ep_adminpads2_padname": "Innde faɗo", + "ep_adminpads2_search-box.placeholder": "Helmere njiilaw", + "ep_adminpads2_search-button.value": "Yiylo", + "ep_adminpads2_search-done": "Njiylaw timmii", + "ep_adminpads2_search-error-explanation": "Sarworde ndee hawrii e juumre tuma nde yiylotoo faɗo:", + "ep_adminpads2_search-error-title": "Horiima heɓde doggol paɗe", + "ep_adminpads2_search-heading": "Yiylo paɗe", + "ep_adminpads2_title": "Yiylorde paɗe", + "ep_adminpads2_unknown-error": "Juumre nde anndaaka", + "ep_adminpads2_unknown-status": "Ngonka ka anndaaka" +} diff --git a/admin/public/ep_admin_pads/fi.json b/admin/public/ep_admin_pads/fi.json new file mode 100644 index 000000000..708b2bef8 --- /dev/null +++ b/admin/public/ep_admin_pads/fi.json @@ -0,0 +1,27 @@ +{ + "@metadata": { + "authors": [ + "Artnay", + "Kyykaarme", + "MITO", + "Maantietäjä", + "Yupik" + ] + }, + "ep_adminpads2_action": "Toiminto", + "ep_adminpads2_delete.value": "Poista", + "ep_adminpads2_last-edited": "Viimeksi muokattu", + "ep_adminpads2_loading": "Ladataan...", + "ep_adminpads2_manage-pads": "Hallitse muistioita", + "ep_adminpads2_no-results": "Ei tuloksia", + "ep_adminpads2_pad-user-count": "Pad-käyttäjien määrä", + "ep_adminpads2_padname": "Muistion nimi", + "ep_adminpads2_search-box.placeholder": "Haettava teksti", + "ep_adminpads2_search-button.value": "Etsi", + "ep_adminpads2_search-done": "Haku valmis", + "ep_adminpads2_search-error-explanation": "Palvelimessa tapahtui virhe etsiessään muistioita:", + "ep_adminpads2_search-error-title": "Pad-luettelon hakeminen epäonnistui", + "ep_adminpads2_search-heading": "Etsi sisältöä", + "ep_adminpads2_unknown-error": "Tuntematon virhe", + "ep_adminpads2_unknown-status": "Tuntematon tila" +} diff --git a/admin/public/ep_admin_pads/fr.json b/admin/public/ep_admin_pads/fr.json new file mode 100644 index 000000000..e6c8a8703 --- /dev/null +++ b/admin/public/ep_admin_pads/fr.json @@ -0,0 +1,27 @@ +{ + "@metadata": { + "authors": [ + "Verdy p" + ] + }, + "ep_adminpads2_action": "Action", + "ep_adminpads2_autoupdate-label": "Mise à jour automatique en cas de changements du bloc-notes", + "ep_adminpads2_autoupdate.title": "Active ou désactive les mises à jour automatiques pour la requête actuelle.", + "ep_adminpads2_confirm": "Voulez-vous vraiment supprimer le bloc-notes {{padID}} ?", + "ep_adminpads2_delete.value": "Supprimer", + "ep_adminpads2_last-edited": "Dernière modification", + "ep_adminpads2_loading": "Chargement en cours...", + "ep_adminpads2_manage-pads": "Gérer les bloc-notes", + "ep_adminpads2_no-results": "Aucun résultat", + "ep_adminpads2_pad-user-count": "Nombre d’utilisateurs du bloc-notes", + "ep_adminpads2_padname": "Nom du bloc-notes", + "ep_adminpads2_search-box.placeholder": "Terme de recherche", + "ep_adminpads2_search-button.value": "Rechercher", + "ep_adminpads2_search-done": "Recherche terminée", + "ep_adminpads2_search-error-explanation": "Le serveur a rencontré une erreur en cherchant des blocs-notes :", + "ep_adminpads2_search-error-title": "Échec d’obtention de la liste de blocs-notes", + "ep_adminpads2_search-heading": "Rechercher des blocs-notes", + "ep_adminpads2_title": "Administration du bloc-notes", + "ep_adminpads2_unknown-error": "Erreur inconnue", + "ep_adminpads2_unknown-status": "État inconnu" +} diff --git a/admin/public/ep_admin_pads/gl.json b/admin/public/ep_admin_pads/gl.json new file mode 100644 index 000000000..5e6b66549 --- /dev/null +++ b/admin/public/ep_admin_pads/gl.json @@ -0,0 +1,27 @@ +{ + "@metadata": { + "authors": [ + "Ghose" + ] + }, + "ep_adminpads2_action": "Accións", + "ep_adminpads2_autoupdate-label": "Actualización automática dos cambios", + "ep_adminpads2_autoupdate.title": "Activa ou desactiva as actualizacións automáticas para a consulta actual.", + "ep_adminpads2_confirm": "Tes a certeza de querer eliminar o pad {{padID}}?", + "ep_adminpads2_delete.value": "Eliminar", + "ep_adminpads2_last-edited": "Última edición", + "ep_adminpads2_loading": "Cargando…", + "ep_adminpads2_manage-pads": "Xestionar pads", + "ep_adminpads2_no-results": "Sen resultados", + "ep_adminpads2_pad-user-count": "Usuarias neste pad", + "ep_adminpads2_padname": "Nome do pad", + "ep_adminpads2_search-box.placeholder": "Buscar termo", + "ep_adminpads2_search-button.value": "Buscar", + "ep_adminpads2_search-done": "Busca completa", + "ep_adminpads2_search-error-explanation": "O servidor atopou un fallo cando buscaba pads:", + "ep_adminpads2_search-error-title": "Non se obtivo a lista de pads", + "ep_adminpads2_search-heading": "Buscar pads", + "ep_adminpads2_title": "Administración do pad", + "ep_adminpads2_unknown-error": "Erro descoñecido", + "ep_adminpads2_unknown-status": "Estado descoñecido" +} diff --git a/admin/public/ep_admin_pads/he.json b/admin/public/ep_admin_pads/he.json new file mode 100644 index 000000000..8b506946b --- /dev/null +++ b/admin/public/ep_admin_pads/he.json @@ -0,0 +1,27 @@ +{ + "@metadata": { + "authors": [ + "YaronSh" + ] + }, + "ep_adminpads2_action": "פעולה", + "ep_adminpads2_autoupdate-label": "לעדכן אוטומטית כשהמחברת נערכת", + "ep_adminpads2_autoupdate.title": "הפעלה או השבתה של עדכונים אוטומטיים לשאילתה הנוכחית.", + "ep_adminpads2_confirm": "למחוק את המחברת {{padID}}?", + "ep_adminpads2_delete.value": "מחיקה", + "ep_adminpads2_last-edited": "עריכה אחרונה", + "ep_adminpads2_loading": "בטעינה…", + "ep_adminpads2_manage-pads": "ניהול מחברות", + "ep_adminpads2_no-results": "אין תוצאות", + "ep_adminpads2_pad-user-count": "ספירת משתמשים במחברת", + "ep_adminpads2_padname": "שם המחברת", + "ep_adminpads2_search-box.placeholder": "הביטוי לחיפוש", + "ep_adminpads2_search-button.value": "חיפוש", + "ep_adminpads2_search-done": "החיפוש הושלם", + "ep_adminpads2_search-error-explanation": "השרת נתקל בשגיאה בעת חיפוש מחברות:", + "ep_adminpads2_search-error-title": "קבלת רשימת המחברות נכשלה", + "ep_adminpads2_search-heading": "חיפוש אחר מחברות", + "ep_adminpads2_title": "ניהול מחברות", + "ep_adminpads2_unknown-error": "שגיאה בלתי־ידועה", + "ep_adminpads2_unknown-status": "מצב לא ידוע" +} diff --git a/admin/public/ep_admin_pads/hsb.json b/admin/public/ep_admin_pads/hsb.json new file mode 100644 index 000000000..a6c29611f --- /dev/null +++ b/admin/public/ep_admin_pads/hsb.json @@ -0,0 +1,27 @@ +{ + "@metadata": { + "authors": [ + "Michawiki" + ] + }, + "ep_adminpads2_action": "Akcija", + "ep_adminpads2_autoupdate-label": "Při změnach na zapisniku awtomatisce aktualizować", + "ep_adminpads2_autoupdate.title": "Zmóžnja abo znjemóžnja awtomatiske aktualizacije za aktualne wotprašowanje.", + "ep_adminpads2_confirm": "Chceće woprawdźe zapisnik {{padID}} zhašeć?", + "ep_adminpads2_delete.value": "Zhašeć", + "ep_adminpads2_last-edited": "Poslednja změna", + "ep_adminpads2_loading": "Začituje so...", + "ep_adminpads2_manage-pads": "Zapisniki rjadować", + "ep_adminpads2_no-results": "Žane wuslědki.", + "ep_adminpads2_pad-user-count": "Ličba wužiwarjow zapisnika", + "ep_adminpads2_padname": "Mjeno zapisnika", + "ep_adminpads2_search-box.placeholder": "Pytanske zapřijeće", + "ep_adminpads2_search-button.value": "Pytać", + "ep_adminpads2_search-done": "Pytanje dokónčene", + "ep_adminpads2_search-error-explanation": "Serwer je na zmylk storčił, mjeztym zo je za zapisnikami pytał:", + "ep_adminpads2_search-error-title": "Lisćina zapisnikow njeda so wobstarać", + "ep_adminpads2_search-heading": "Za zapisnikami pytać", + "ep_adminpads2_title": "Zapisnikowa administracija", + "ep_adminpads2_unknown-error": "Njeznaty zmylk", + "ep_adminpads2_unknown-status": "Njeznaty status" +} diff --git a/admin/public/ep_admin_pads/hu.json b/admin/public/ep_admin_pads/hu.json new file mode 100644 index 000000000..9210761bc --- /dev/null +++ b/admin/public/ep_admin_pads/hu.json @@ -0,0 +1,25 @@ +{ + "@metadata": { + "authors": [] + }, + "ep_adminpads2_action": "Művelet", + "ep_adminpads2_autoupdate-label": "Változáskor jegyzetfüzet önműködő frissítése", + "ep_adminpads2_autoupdate.title": "Önműködő frissítése az jelenlegi lekérdezéshez be- vagy kikapcsolása.", + "ep_adminpads2_confirm": "Biztosan törölni szeretné a(z) {{padID}} jegyzetfüzetet?", + "ep_adminpads2_delete.value": "Törlés", + "ep_adminpads2_last-edited": "Utoljára szerkesztve", + "ep_adminpads2_loading": "Betöltés folyamatban…", + "ep_adminpads2_manage-pads": "Jegyzetfüzetek kezelése", + "ep_adminpads2_no-results": "Nincs találat", + "ep_adminpads2_pad-user-count": "Jegyzetfüzet felhasználók száma", + "ep_adminpads2_padname": "Jegyzetfüzet név", + "ep_adminpads2_search-box.placeholder": "Keresési kifejezés", + "ep_adminpads2_search-button.value": "Keresés", + "ep_adminpads2_search-done": "Keresés befejezve", + "ep_adminpads2_search-error-explanation": "A kiszolgáló hibát észlelt a jegyzetfüzetek keresésekor:", + "ep_adminpads2_search-error-title": "Nem sikerült lekérni a jegyzetfüzet listát", + "ep_adminpads2_search-heading": "Jegyzetfüzetek keresése", + "ep_adminpads2_title": "Jegyzetfüzet felügyelete", + "ep_adminpads2_unknown-error": "Ismeretlen hiba", + "ep_adminpads2_unknown-status": "Ismeretlen állapot" +} diff --git a/admin/public/ep_admin_pads/ia.json b/admin/public/ep_admin_pads/ia.json new file mode 100644 index 000000000..f0e00e5ca --- /dev/null +++ b/admin/public/ep_admin_pads/ia.json @@ -0,0 +1,27 @@ +{ + "@metadata": { + "authors": [ + "McDutchie" + ] + }, + "ep_adminpads2_action": "Action", + "ep_adminpads2_autoupdate-label": "Actualisar automaticamente le pad in caso de cambiamentos", + "ep_adminpads2_autoupdate.title": "Activa o disactiva le actualisationes automatic pro le consulta actual.", + "ep_adminpads2_confirm": "Es tu secur de voler deler le pad {{padID}}?", + "ep_adminpads2_delete.value": "Deler", + "ep_adminpads2_last-edited": "Ultime modification", + "ep_adminpads2_loading": "Cargamento in curso…", + "ep_adminpads2_manage-pads": "Gerer pads", + "ep_adminpads2_no-results": "Nulle resultato", + "ep_adminpads2_pad-user-count": "Numero de usatores del pad", + "ep_adminpads2_padname": "Nomine del pad", + "ep_adminpads2_search-box.placeholder": "Termino de recerca", + "ep_adminpads2_search-button.value": "Cercar", + "ep_adminpads2_search-done": "Recerca terminate", + "ep_adminpads2_search-error-explanation": "Le servitor ha incontrate un error durante le recerca de pads:", + "ep_adminpads2_search-error-title": "Non poteva obtener le lista de pads", + "ep_adminpads2_search-heading": "Cercar pads", + "ep_adminpads2_title": "Administration de pads", + "ep_adminpads2_unknown-error": "Error incognite", + "ep_adminpads2_unknown-status": "Stato incognite" +} diff --git a/admin/public/ep_admin_pads/it.json b/admin/public/ep_admin_pads/it.json new file mode 100644 index 000000000..493cbb4d5 --- /dev/null +++ b/admin/public/ep_admin_pads/it.json @@ -0,0 +1,16 @@ +{ + "@metadata": { + "authors": [ + "Beta16", + "Luca.favorido" + ] + }, + "ep_adminpads2_action": "Azione", + "ep_adminpads2_delete.value": "Cancella", + "ep_adminpads2_last-edited": "Ultima modifica", + "ep_adminpads2_loading": "Caricamento…", + "ep_adminpads2_no-results": "Nessun risultato", + "ep_adminpads2_search-button.value": "Cerca", + "ep_adminpads2_unknown-error": "Errore sconosciuto", + "ep_adminpads2_unknown-status": "Stato sconosciuto" +} diff --git a/admin/public/ep_admin_pads/kn.json b/admin/public/ep_admin_pads/kn.json new file mode 100644 index 000000000..1e9019611 --- /dev/null +++ b/admin/public/ep_admin_pads/kn.json @@ -0,0 +1,13 @@ +{ + "@metadata": { + "authors": [ + "ಮಲ್ನಾಡಾಚ್ ಕೊಂಕ್ಣೊ" + ] + }, + "ep_adminpads2_action": "ಕ್ರಿಯೆ", + "ep_adminpads2_delete.value": "ಅಳಿಸು", + "ep_adminpads2_loading": "ತುಂಬಿಸಲಾಗುತ್ತಿದೆ…", + "ep_adminpads2_no-results": "ಯಾವ ಫಲಿತಾಂಶಗಳೂ ಇಲ್ಲ", + "ep_adminpads2_search-button.value": "ಹುಡುಕು", + "ep_adminpads2_unknown-error": "ಅಪರಿಚಿತ ದೋಷ" +} diff --git a/admin/public/ep_admin_pads/ko.json b/admin/public/ep_admin_pads/ko.json new file mode 100644 index 000000000..9ab8feed3 --- /dev/null +++ b/admin/public/ep_admin_pads/ko.json @@ -0,0 +1,28 @@ +{ + "@metadata": { + "authors": [ + "Ykhwong", + "그냥기여자" + ] + }, + "ep_adminpads2_action": "동작", + "ep_adminpads2_autoupdate-label": "패드 변경 시 자동 업데이트", + "ep_adminpads2_autoupdate.title": "현재 쿼리의 자동 업데이트를 활성화하거나 비활성화합니다.", + "ep_adminpads2_confirm": "{{padID}} 패드를 삭제하시겠습니까?", + "ep_adminpads2_delete.value": "삭제", + "ep_adminpads2_last-edited": "최근 편집", + "ep_adminpads2_loading": "불러오는 중...", + "ep_adminpads2_manage-pads": "패드 관리", + "ep_adminpads2_no-results": "결과 없음", + "ep_adminpads2_pad-user-count": "패드 사용자 수", + "ep_adminpads2_padname": "패드 이름", + "ep_adminpads2_search-box.placeholder": "검색어", + "ep_adminpads2_search-button.value": "검색", + "ep_adminpads2_search-done": "검색 완료", + "ep_adminpads2_search-error-explanation": "패드 검색 중 서버에 오류가 발생했습니다:", + "ep_adminpads2_search-error-title": "패드 목록 가져오기 실패", + "ep_adminpads2_search-heading": "패드 검색", + "ep_adminpads2_title": "패드 관리", + "ep_adminpads2_unknown-error": "알 수 없는 오류", + "ep_adminpads2_unknown-status": "알 수 없는 상태" +} diff --git a/admin/public/ep_admin_pads/krc.json b/admin/public/ep_admin_pads/krc.json new file mode 100644 index 000000000..2caf4f099 --- /dev/null +++ b/admin/public/ep_admin_pads/krc.json @@ -0,0 +1,27 @@ +{ + "@metadata": { + "authors": [ + "Къарачайлы" + ] + }, + "ep_adminpads2_action": "Этиу", + "ep_adminpads2_autoupdate-label": "Блокнот тюрлендириулеринде автомат халда джангыртыу", + "ep_adminpads2_autoupdate.title": "Баргъан излем ючюн автомат халда джангыртыуланы джандын неда джукълат.", + "ep_adminpads2_confirm": "{{padID}} блокнотну керти да кетерирге излеймисиз?", + "ep_adminpads2_delete.value": "Кетер", + "ep_adminpads2_last-edited": "Ахыр тюзетиу", + "ep_adminpads2_loading": "Джюклениу…", + "ep_adminpads2_manage-pads": "Блокнотланы оноуун эт", + "ep_adminpads2_no-results": "Эсебле джокъдула", + "ep_adminpads2_pad-user-count": "Блокнот хайырланыучуланы саны", + "ep_adminpads2_padname": "Блокнот ат", + "ep_adminpads2_search-box.placeholder": "Терминни изле", + "ep_adminpads2_search-button.value": "Изле", + "ep_adminpads2_search-done": "Излеу тамамланды", + "ep_adminpads2_search-error-explanation": "Сервер, блокнотланы излеген заманда халат табды:", + "ep_adminpads2_search-error-title": "Блокнот тизмеси алынамады", + "ep_adminpads2_search-heading": "Блокнотла ючюн излеу", + "ep_adminpads2_title": "Блокнот башчылыкъ", + "ep_adminpads2_unknown-error": "Билинмеген халат", + "ep_adminpads2_unknown-status": "Билинмеген турум" +} diff --git a/admin/public/ep_admin_pads/lb.json b/admin/public/ep_admin_pads/lb.json new file mode 100644 index 000000000..61aa2588d --- /dev/null +++ b/admin/public/ep_admin_pads/lb.json @@ -0,0 +1,16 @@ +{ + "@metadata": { + "authors": [ + "Robby", + "Volvox" + ] + }, + "ep_adminpads2_confirm": "Wëllt Dir de Pad {{padID}} wierklech läschen?", + "ep_adminpads2_delete.value": "Läschen", + "ep_adminpads2_loading": "Lueden...", + "ep_adminpads2_no-results": "Keng Resultater", + "ep_adminpads2_padname": "Padnumm", + "ep_adminpads2_search-box.placeholder": "Sichbegrëff", + "ep_adminpads2_search-button.value": "Sichen", + "ep_adminpads2_unknown-error": "Onbekannte Feeler" +} diff --git a/admin/public/ep_admin_pads/lt.json b/admin/public/ep_admin_pads/lt.json new file mode 100644 index 000000000..59b2a13b3 --- /dev/null +++ b/admin/public/ep_admin_pads/lt.json @@ -0,0 +1,27 @@ +{ + "@metadata": { + "authors": [ + "Nokeoo" + ] + }, + "ep_adminpads2_action": "Veiksmas", + "ep_adminpads2_autoupdate-label": "Automatinis bloknoto keitimų naujinimas", + "ep_adminpads2_autoupdate.title": "Įjungia arba išjungia automatinius dabartinės užklausos atnaujinimus.", + "ep_adminpads2_confirm": "Ar tikrai norite ištrinti bloknotą {{padID}}?", + "ep_adminpads2_delete.value": "Ištrinti", + "ep_adminpads2_last-edited": "Paskutinis pakeitimas", + "ep_adminpads2_loading": "Įkeliama…", + "ep_adminpads2_manage-pads": "Tvarkyti bloknotą", + "ep_adminpads2_no-results": "Nėra rezultatų", + "ep_adminpads2_pad-user-count": "Bloknoto naudotojų skaičius", + "ep_adminpads2_padname": "Bloknoto pavadinimas", + "ep_adminpads2_search-box.placeholder": "Paieškos terminas", + "ep_adminpads2_search-button.value": "Paieška", + "ep_adminpads2_search-done": "Paieška baigta", + "ep_adminpads2_search-error-explanation": "Serveris susidūrė su klaida ieškant bloknotų:", + "ep_adminpads2_search-error-title": "Nepavyko gauti bloknotų sąrašo", + "ep_adminpads2_search-heading": "Ieškokite bloknotų", + "ep_adminpads2_title": "Bloknotų administravimas", + "ep_adminpads2_unknown-error": "Nežinoma klaida", + "ep_adminpads2_unknown-status": "Nežinoma būsena" +} diff --git a/admin/public/ep_admin_pads/mk.json b/admin/public/ep_admin_pads/mk.json new file mode 100644 index 000000000..72affd86c --- /dev/null +++ b/admin/public/ep_admin_pads/mk.json @@ -0,0 +1,27 @@ +{ + "@metadata": { + "authors": [ + "Bjankuloski06" + ] + }, + "ep_adminpads2_action": "Дејство", + "ep_adminpads2_autoupdate-label": "Самоподнова при измени во тетратката", + "ep_adminpads2_autoupdate.title": "Овозможува или оневозможува самоподнова на тековното барање.", + "ep_adminpads2_confirm": "Дали навистина сакате да ја избришете тетратката {{padID}}?", + "ep_adminpads2_delete.value": "Избриши", + "ep_adminpads2_last-edited": "Последно уредување", + "ep_adminpads2_loading": "Вчитувам…", + "ep_adminpads2_manage-pads": "Раководење со тетратки", + "ep_adminpads2_no-results": "Нема исход", + "ep_adminpads2_pad-user-count": "Корисници на тетратката", + "ep_adminpads2_padname": "Назив на тетратката", + "ep_adminpads2_search-box.placeholder": "Пребаран поим", + "ep_adminpads2_search-button.value": "Пребарај", + "ep_adminpads2_search-done": "Пребарувањето заврши", + "ep_adminpads2_search-error-explanation": "Опслужувачот наиде на грешка при пребарувањето на тетратки:", + "ep_adminpads2_search-error-title": "Не можев да го добијам списокот на тетратки", + "ep_adminpads2_search-heading": "Пребарај по тетратките", + "ep_adminpads2_title": "Администрација на тетратки", + "ep_adminpads2_unknown-error": "Непозната грешка", + "ep_adminpads2_unknown-status": "Непозната состојба" +} diff --git a/admin/public/ep_admin_pads/my.json b/admin/public/ep_admin_pads/my.json new file mode 100644 index 000000000..6b94ba702 --- /dev/null +++ b/admin/public/ep_admin_pads/my.json @@ -0,0 +1,27 @@ +{ + "@metadata": { + "authors": [ + "Andibecker" + ] + }, + "ep_adminpads2_action": "လုပ်ဆောင်ချက်", + "ep_adminpads2_autoupdate-label": "pad အပြောင်းအလဲများတွင်အလိုအလျောက်အပ်ဒိတ်လုပ်ပါ", + "ep_adminpads2_autoupdate.title": "လက်ရှိမေးမြန်းမှုအတွက်အလိုအလျောက်အပ်ဒိတ်များကိုဖွင့်ပါသို့မဟုတ်ပိတ်ပါ။", + "ep_adminpads2_confirm": "pad {{padID}} ကိုသင်တကယ်ဖျက်ချင်လား။", + "ep_adminpads2_delete.value": "ဖျက်ပါ", + "ep_adminpads2_last-edited": "နောက်ဆုံးတည်းဖြတ်သည်", + "ep_adminpads2_loading": "ဖွင့်နေသည်…", + "ep_adminpads2_manage-pads": "pads များကိုစီမံပါ", + "ep_adminpads2_no-results": "ရလဒ်မရှိပါ", + "ep_adminpads2_pad-user-count": "Pad အသုံးပြုသူအရေအတွက်", + "ep_adminpads2_padname": "Padname", + "ep_adminpads2_search-box.placeholder": "ဝေါဟာရရှာဖွေပါ", + "ep_adminpads2_search-button.value": "ရှာဖွေပါ", + "ep_adminpads2_search-done": "ရှာဖွေမှုပြီးပါပြီ", + "ep_adminpads2_search-error-explanation": "pads များကိုရှာဖွေစဉ်ဆာဗာသည်အမှားတစ်ခုကြုံခဲ့သည်။", + "ep_adminpads2_search-error-title": "pad စာရင်းရယူရန်မအောင်မြင်ပါ", + "ep_adminpads2_search-heading": "pads များကိုရှာဖွေပါ", + "ep_adminpads2_title": "Pad စီမံခန့်ခွဲမှု", + "ep_adminpads2_unknown-error": "အမည်မသိအမှား", + "ep_adminpads2_unknown-status": "အခြေအနေမသိ" +} diff --git a/admin/public/ep_admin_pads/nb.json b/admin/public/ep_admin_pads/nb.json new file mode 100644 index 000000000..acd194397 --- /dev/null +++ b/admin/public/ep_admin_pads/nb.json @@ -0,0 +1,13 @@ +{ + "@metadata": { + "authors": [ + "EdoAug" + ] + }, + "ep_adminpads2_action": "Handling", + "ep_adminpads2_last-edited": "Sist redigert", + "ep_adminpads2_loading": "Laster …", + "ep_adminpads2_no-results": "Ingen resultater", + "ep_adminpads2_search-button.value": "Søk", + "ep_adminpads2_search-done": "Søk fullført" +} diff --git a/admin/public/ep_admin_pads/nl.json b/admin/public/ep_admin_pads/nl.json new file mode 100644 index 000000000..f4d97b351 --- /dev/null +++ b/admin/public/ep_admin_pads/nl.json @@ -0,0 +1,29 @@ +{ + "@metadata": { + "authors": [ + "Aranka", + "McDutchie", + "Spinster" + ] + }, + "ep_adminpads2_action": "Handeling", + "ep_adminpads2_autoupdate-label": "Automatisch bijwerken bij aanpassingen aan de pad", + "ep_adminpads2_autoupdate.title": "Schakelt automatische updates voor de huidige query in of uit.", + "ep_adminpads2_confirm": "Wil je de pad {{padID}} echt verwijderen?", + "ep_adminpads2_delete.value": "Verwijderen", + "ep_adminpads2_last-edited": "Laatst bewerkt", + "ep_adminpads2_loading": "Bezig met laden...", + "ep_adminpads2_manage-pads": "Pads beheren", + "ep_adminpads2_no-results": "Geen resultaten", + "ep_adminpads2_pad-user-count": "Aantal gebruikers van de pad", + "ep_adminpads2_padname": "Naam van de pad", + "ep_adminpads2_search-box.placeholder": "Zoekterm", + "ep_adminpads2_search-button.value": "Zoeken", + "ep_adminpads2_search-done": "Zoekopdracht voltooid", + "ep_adminpads2_search-error-explanation": "De server heeft een fout aangetroffen tijdens het zoeken naar pads:", + "ep_adminpads2_search-error-title": "Kan lijst met pads niet ophalen", + "ep_adminpads2_search-heading": "Pads zoeken", + "ep_adminpads2_title": "Administratie van pad", + "ep_adminpads2_unknown-error": "Onbekende fout", + "ep_adminpads2_unknown-status": "Onbekende status" +} diff --git a/admin/public/ep_admin_pads/oc.json b/admin/public/ep_admin_pads/oc.json new file mode 100644 index 000000000..ae0169faf --- /dev/null +++ b/admin/public/ep_admin_pads/oc.json @@ -0,0 +1,21 @@ +{ + "@metadata": { + "authors": [ + "Quentí" + ] + }, + "ep_adminpads2_action": "Accion", + "ep_adminpads2_delete.value": "Suprimir", + "ep_adminpads2_last-edited": "Darrièra edicion", + "ep_adminpads2_loading": "Cargament…", + "ep_adminpads2_manage-pads": "Gerir los pads", + "ep_adminpads2_no-results": "Pas cap de resultat", + "ep_adminpads2_padname": "Nom del pad", + "ep_adminpads2_search-box.placeholder": "Tèrme de recèrca", + "ep_adminpads2_search-button.value": "Recercar", + "ep_adminpads2_search-done": "Recèrca acabada", + "ep_adminpads2_search-heading": "Cercar de pads", + "ep_adminpads2_title": "Administracion de pad", + "ep_adminpads2_unknown-error": "Error desconeguda", + "ep_adminpads2_unknown-status": "Estat desconegut" +} diff --git a/admin/public/ep_admin_pads/pms.json b/admin/public/ep_admin_pads/pms.json new file mode 100644 index 000000000..ac0542b85 --- /dev/null +++ b/admin/public/ep_admin_pads/pms.json @@ -0,0 +1,27 @@ +{ + "@metadata": { + "authors": [ + "Borichèt" + ] + }, + "ep_adminpads2_action": "Assion", + "ep_adminpads2_autoupdate-label": "Agiornament automàtich an sle modìfiche ëd plancia", + "ep_adminpads2_autoupdate.title": "Abilité o disabilité j'agiornament automàtich për l'arcesta atual.", + "ep_adminpads2_confirm": "Veul-lo për da bon dëscancelé la plancia {{padID}}?", + "ep_adminpads2_delete.value": "Dëscancelé", + "ep_adminpads2_last-edited": "Modificà l'ùltima vira", + "ep_adminpads2_loading": "Cariament…", + "ep_adminpads2_manage-pads": "Gestì le plance", + "ep_adminpads2_no-results": "Gnun arzultà", + "ep_adminpads2_pad-user-count": "Conteur ëd plancia dl'utent", + "ep_adminpads2_padname": "Nòm ëd plancia", + "ep_adminpads2_search-box.placeholder": "Tèrmin d'arserca", + "ep_adminpads2_search-button.value": "Arserca", + "ep_adminpads2_search-done": "Arserca completà", + "ep_adminpads2_search-error-explanation": "Ël servent a l'ha rancontrà n'eror an sërcand dle plance:", + "ep_adminpads2_search-error-title": "Falì a oten-e la lista ëd plance", + "ep_adminpads2_search-heading": "Arserca ëd plance", + "ep_adminpads2_title": "Aministrassion ëd plance", + "ep_adminpads2_unknown-error": "Eror nen conossù", + "ep_adminpads2_unknown-status": "Statù nen conossù" +} diff --git a/admin/public/ep_admin_pads/pt-br.json b/admin/public/ep_admin_pads/pt-br.json new file mode 100644 index 000000000..28a7874ee --- /dev/null +++ b/admin/public/ep_admin_pads/pt-br.json @@ -0,0 +1,30 @@ +{ + "@metadata": { + "authors": [ + "Duke of Wikipädia", + "Eduardo Addad de Oliveira", + "Eduardoaddad", + "YuriNikolai" + ] + }, + "ep_adminpads2_action": "Ação", + "ep_adminpads2_autoupdate-label": "Atualizar notas automaticamente", + "ep_adminpads2_autoupdate.title": "Habilita ou desabilita atualizações automáticas para a consulta atual.", + "ep_adminpads2_confirm": "Você realmente deseja excluir a nota {{padID}}?", + "ep_adminpads2_delete.value": "Excluir", + "ep_adminpads2_last-edited": "Última edição", + "ep_adminpads2_loading": "Carregando…", + "ep_adminpads2_manage-pads": "Gerenciar notas", + "ep_adminpads2_no-results": "Sem resultados", + "ep_adminpads2_pad-user-count": "Número de utilizadores na nota", + "ep_adminpads2_padname": "Nome da nota", + "ep_adminpads2_search-box.placeholder": "Termo de pesquisa", + "ep_adminpads2_search-button.value": "Pesquisar", + "ep_adminpads2_search-done": "Busca completa", + "ep_adminpads2_search-error-explanation": "O servidor encontrou um erro enquanto procurava por notas:", + "ep_adminpads2_search-error-title": "Falha ao buscar lista de notas", + "ep_adminpads2_search-heading": "Pesquisar por notas", + "ep_adminpads2_title": "Administração de notas", + "ep_adminpads2_unknown-error": "Erro desconhecido", + "ep_adminpads2_unknown-status": "Status desconhecido" +} diff --git a/admin/public/ep_admin_pads/pt.json b/admin/public/ep_admin_pads/pt.json new file mode 100644 index 000000000..b7abf2f3f --- /dev/null +++ b/admin/public/ep_admin_pads/pt.json @@ -0,0 +1,27 @@ +{ + "@metadata": { + "authors": [ + "Guilha" + ] + }, + "ep_adminpads2_action": "Ação", + "ep_adminpads2_autoupdate-label": "Atualizar automaticamente as notas", + "ep_adminpads2_autoupdate.title": "Ativa ou desativa atualizações automáticas na consulta atual.", + "ep_adminpads2_confirm": "Tencionas mesmo eliminar a nota {{padID}}?", + "ep_adminpads2_delete.value": "Eliminar", + "ep_adminpads2_last-edited": "Última edição", + "ep_adminpads2_loading": "A carregar...", + "ep_adminpads2_manage-pads": "Gerir notas", + "ep_adminpads2_no-results": "Sem resultados", + "ep_adminpads2_pad-user-count": "Número de utilizadores na nota", + "ep_adminpads2_padname": "Nome da nota", + "ep_adminpads2_search-box.placeholder": "Procurar termo", + "ep_adminpads2_search-button.value": "Procurar", + "ep_adminpads2_search-done": "Procura completa", + "ep_adminpads2_search-error-explanation": "O servidor encontrou um erro enquanto procurava por notas:", + "ep_adminpads2_search-error-title": "Falha ao obter lista de notas", + "ep_adminpads2_search-heading": "Procurar por notas", + "ep_adminpads2_title": "Administração da nota", + "ep_adminpads2_unknown-error": "Erro desconhecido", + "ep_adminpads2_unknown-status": "Estado desconhecido" +} diff --git a/admin/public/ep_admin_pads/qqq.json b/admin/public/ep_admin_pads/qqq.json new file mode 100644 index 000000000..de36e2ae6 --- /dev/null +++ b/admin/public/ep_admin_pads/qqq.json @@ -0,0 +1,10 @@ +{ + "@metadata": { + "authors": [ + "BryanDavis" + ] + }, + "ep_adminpads2_action": "{{Identical|Action}}", + "ep_adminpads2_delete.value": "{{Identical|Delete}}", + "ep_adminpads2_search-button.value": "{{Identical|Search}}" +} diff --git a/admin/public/ep_admin_pads/ru.json b/admin/public/ep_admin_pads/ru.json new file mode 100644 index 000000000..6d0d163d0 --- /dev/null +++ b/admin/public/ep_admin_pads/ru.json @@ -0,0 +1,31 @@ +{ + "@metadata": { + "authors": [ + "DDPAT", + "Ice bulldog", + "Megakott", + "Okras", + "Pacha Tchernof" + ] + }, + "ep_adminpads2_action": "Действие", + "ep_adminpads2_autoupdate-label": "Автообновление при изменении документа", + "ep_adminpads2_autoupdate.title": "Включает или отключает автоматические обновления для текущего запроса.", + "ep_adminpads2_confirm": "Вы действительно хотите удалить документ {{padID}}?", + "ep_adminpads2_delete.value": "Удалить", + "ep_adminpads2_last-edited": "Последнее изменение", + "ep_adminpads2_loading": "Загружается…", + "ep_adminpads2_manage-pads": "Управление документами", + "ep_adminpads2_no-results": "Нет результатов", + "ep_adminpads2_pad-user-count": "Количество пользователей документа", + "ep_adminpads2_padname": "Название документа", + "ep_adminpads2_search-box.placeholder": "Искать термин", + "ep_adminpads2_search-button.value": "Найти", + "ep_adminpads2_search-done": "Поиск завершён", + "ep_adminpads2_search-error-explanation": "Сервер обнаружил ошибку при поиске документов:", + "ep_adminpads2_search-error-title": "Не удалось получить список документов", + "ep_adminpads2_search-heading": "Поиск документов", + "ep_adminpads2_title": "Администрирование документов", + "ep_adminpads2_unknown-error": "Неизвестная ошибка", + "ep_adminpads2_unknown-status": "Неизвестный статус" +} diff --git a/admin/public/ep_admin_pads/sc.json b/admin/public/ep_admin_pads/sc.json new file mode 100644 index 000000000..a37bba5a2 --- /dev/null +++ b/admin/public/ep_admin_pads/sc.json @@ -0,0 +1,27 @@ +{ + "@metadata": { + "authors": [ + "Adr mm" + ] + }, + "ep_adminpads2_action": "Atzione", + "ep_adminpads2_autoupdate-label": "Atualizatzione automàtica de is modìficas de su pad", + "ep_adminpads2_autoupdate.title": "Ativat o disativat is atualizatziones automàticas pro sa chirca atuale.", + "ep_adminpads2_confirm": "Seguru chi boles cantzellare su pad {{padID}}?", + "ep_adminpads2_delete.value": "Cantzella", + "ep_adminpads2_last-edited": "Ùrtima modìfica", + "ep_adminpads2_loading": "Carrighende...", + "ep_adminpads2_manage-pads": "Gesti is pads", + "ep_adminpads2_no-results": "Nissunu resurtadu", + "ep_adminpads2_pad-user-count": "Nùmeru de utentes de pads", + "ep_adminpads2_padname": "Nòmine de su pad", + "ep_adminpads2_search-box.placeholder": "Tèrmine de chirca", + "ep_adminpads2_search-button.value": "Chirca", + "ep_adminpads2_search-done": "Chirca cumpleta", + "ep_adminpads2_search-error-explanation": "Su serbidore at agatadu un'errore chirchende pads:", + "ep_adminpads2_search-error-title": "Impossìbile otènnere sa lista de pads", + "ep_adminpads2_search-heading": "Chirca pads", + "ep_adminpads2_title": "Amministratzione de su pad", + "ep_adminpads2_unknown-error": "Errore disconnotu", + "ep_adminpads2_unknown-status": "Istadu disconnotu" +} diff --git a/admin/public/ep_admin_pads/sdc.json b/admin/public/ep_admin_pads/sdc.json new file mode 100644 index 000000000..c4672fd7f --- /dev/null +++ b/admin/public/ep_admin_pads/sdc.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "F Samaritani" + ] + }, + "ep_adminpads2_action": "Azioni", + "ep_adminpads2_delete.value": "Canzella", + "ep_adminpads2_loading": "carrigghendi...", + "ep_adminpads2_no-results": "Nisciun risulthaddu", + "ep_adminpads2_search-button.value": "Zercha", + "ep_adminpads2_search-heading": "Zirchà dati", + "ep_adminpads2_unknown-error": "Errori ischunisciddu" +} diff --git a/admin/public/ep_admin_pads/sk.json b/admin/public/ep_admin_pads/sk.json new file mode 100644 index 000000000..ab0392d4e --- /dev/null +++ b/admin/public/ep_admin_pads/sk.json @@ -0,0 +1,27 @@ +{ + "@metadata": { + "authors": [ + "Yardom78" + ] + }, + "ep_adminpads2_action": "Akcia", + "ep_adminpads2_autoupdate-label": "Automatická aktualizácia zmien na poznámkovom bloku", + "ep_adminpads2_autoupdate.title": "Zapne alebo vypne automatickú aktualizáciu.", + "ep_adminpads2_confirm": "Skutočne chcete vymazať poznámkový blok {{padID}}?", + "ep_adminpads2_delete.value": "Vymazať", + "ep_adminpads2_last-edited": "Posledná úprava", + "ep_adminpads2_loading": "Načítavanie...", + "ep_adminpads2_manage-pads": "Spravovať poznámkové bloky", + "ep_adminpads2_no-results": "Žiadne výsledky", + "ep_adminpads2_pad-user-count": "Počet používateľov poznámkového bloku", + "ep_adminpads2_padname": "Názov poznámkového bloku", + "ep_adminpads2_search-box.placeholder": "Hľadať výraz", + "ep_adminpads2_search-button.value": "Hľadať", + "ep_adminpads2_search-done": "Hľadanie dokončené", + "ep_adminpads2_search-error-explanation": "Pri hľadaní poznámkového bloku došlo k chybe:", + "ep_adminpads2_search-error-title": "Nepodarilo sa získať zoznam poznámkových blokov", + "ep_adminpads2_search-heading": "Hľadať poznámkový blok", + "ep_adminpads2_title": "Správa poznámkového bloku", + "ep_adminpads2_unknown-error": "Neznáma chyba", + "ep_adminpads2_unknown-status": "Neznámy stav" +} diff --git a/admin/public/ep_admin_pads/skr-arab.json b/admin/public/ep_admin_pads/skr-arab.json new file mode 100644 index 000000000..08162f849 --- /dev/null +++ b/admin/public/ep_admin_pads/skr-arab.json @@ -0,0 +1,20 @@ +{ + "@metadata": { + "authors": [ + "Saraiki" + ] + }, + "ep_adminpads2_action": "عمل", + "ep_adminpads2_delete.value": "مٹاؤ", + "ep_adminpads2_last-edited": "چھیکڑی تبدیلی", + "ep_adminpads2_loading": "لوڈ تھین٘دا پئے۔۔۔", + "ep_adminpads2_manage-pads": "پیڈ منیج کرو", + "ep_adminpads2_no-results": "کوئی نتیجہ کائنی", + "ep_adminpads2_padname": "پیڈ ناں", + "ep_adminpads2_search-box.placeholder": "ٹرم ڳولو", + "ep_adminpads2_search-button.value": "ڳولو", + "ep_adminpads2_search-done": "ڳولݨ پورا تھیا", + "ep_adminpads2_search-heading": "پیڈاں دی ڳول", + "ep_adminpads2_unknown-error": "نامعلوم غلطی", + "ep_adminpads2_unknown-status": "نامعلوم حالت" +} diff --git a/admin/public/ep_admin_pads/sl.json b/admin/public/ep_admin_pads/sl.json new file mode 100644 index 000000000..3bebe1972 --- /dev/null +++ b/admin/public/ep_admin_pads/sl.json @@ -0,0 +1,28 @@ +{ + "@metadata": { + "authors": [ + "Eleassar", + "HairyFotr" + ] + }, + "ep_adminpads2_action": "Dejanje", + "ep_adminpads2_autoupdate-label": "Samodejno posodabljanje ob spremembah blokcev", + "ep_adminpads2_autoupdate.title": "Omogoči ali onemogoči samodejne posodobitve za trenutno poizvedbo.", + "ep_adminpads2_confirm": "Ali res želite izbrisati blokec {{padID}}?", + "ep_adminpads2_delete.value": "Izbriši", + "ep_adminpads2_last-edited": "Zadnje urejanje", + "ep_adminpads2_loading": "Nalaganje ...", + "ep_adminpads2_manage-pads": "Upravljanje blokcev", + "ep_adminpads2_no-results": "Ni zadetkov", + "ep_adminpads2_pad-user-count": "Število urejevalcev blokca", + "ep_adminpads2_padname": "Ime blokca", + "ep_adminpads2_search-box.placeholder": "Iskalni izraz", + "ep_adminpads2_search-button.value": "Išči", + "ep_adminpads2_search-done": "Iskanje končano", + "ep_adminpads2_search-error-explanation": "Strežnik je med iskanjem blokcev naletel na napako:", + "ep_adminpads2_search-error-title": "Ni bilo mogoče pridobiti seznama blokcev", + "ep_adminpads2_search-heading": "Iskanje blokcev", + "ep_adminpads2_title": "Upravljanje blokcev", + "ep_adminpads2_unknown-error": "Neznana napaka", + "ep_adminpads2_unknown-status": "Neznano stanje" +} diff --git a/admin/public/ep_admin_pads/smn.json b/admin/public/ep_admin_pads/smn.json new file mode 100644 index 000000000..9d57cc73c --- /dev/null +++ b/admin/public/ep_admin_pads/smn.json @@ -0,0 +1,13 @@ +{ + "@metadata": { + "authors": [ + "Yupik" + ] + }, + "ep_adminpads2_delete.value": "Siho", + "ep_adminpads2_last-edited": "Majemustáá nubástittum", + "ep_adminpads2_search-box.placeholder": "Uuccâmsääni", + "ep_adminpads2_search-button.value": "Uusâ", + "ep_adminpads2_unknown-error": "Tubdâmettum feilâ", + "ep_adminpads2_unknown-status": "Tubdâmettum tile" +} diff --git a/admin/public/ep_admin_pads/sms.json b/admin/public/ep_admin_pads/sms.json new file mode 100644 index 000000000..8d3cf5797 --- /dev/null +++ b/admin/public/ep_admin_pads/sms.json @@ -0,0 +1,16 @@ +{ + "@metadata": { + "authors": [ + "Yupik" + ] + }, + "ep_adminpads2_delete.value": "Jaukkâd", + "ep_adminpads2_last-edited": "Mââimõssân muttum", + "ep_adminpads2_no-results": "Ij käunnʼjam ni mii", + "ep_adminpads2_padname": "Mošttʼtõspõʹmmai nõmm", + "ep_adminpads2_search-box.placeholder": "Ooccâmsääʹnn", + "ep_adminpads2_search-button.value": "Ooʒʒ", + "ep_adminpads2_search-heading": "Ooʒʒ mošttʼtõspõʹmmjid", + "ep_adminpads2_unknown-error": "Toobdteʹmes vââʹǩǩ", + "ep_adminpads2_unknown-status": "Toobdteʹmes status" +} diff --git a/admin/public/ep_admin_pads/sq.json b/admin/public/ep_admin_pads/sq.json new file mode 100644 index 000000000..cc4740763 --- /dev/null +++ b/admin/public/ep_admin_pads/sq.json @@ -0,0 +1,27 @@ +{ + "@metadata": { + "authors": [ + "Besnik b" + ] + }, + "ep_adminpads2_action": "Veprim", + "ep_adminpads2_autoupdate-label": "Vetëpërditësohu, kur nga ndryshime blloku", + "ep_adminpads2_autoupdate.title": "Aktivizon ose çaktivizon përditësim të automatizuara për kërkesën e tanishme.", + "ep_adminpads2_confirm": "Doni vërtet të fshihet blloku {{padID}}?", + "ep_adminpads2_delete.value": "Fshije", + "ep_adminpads2_last-edited": "Përpunuar së fundi më", + "ep_adminpads2_loading": "Po ngarkohet…", + "ep_adminpads2_manage-pads": "Administroni blloqe", + "ep_adminpads2_no-results": "S’ka përfundime", + "ep_adminpads2_pad-user-count": "Numër përdoruesish blloku", + "ep_adminpads2_padname": "Emër blloku", + "ep_adminpads2_search-box.placeholder": "Term kërkimi", + "ep_adminpads2_search-button.value": "Kërko", + "ep_adminpads2_search-done": "Kërkim i plotë", + "ep_adminpads2_search-error-explanation": "Shërbyesi hasi një gabim teksa kërkohej për blloqe:", + "ep_adminpads2_search-error-title": "S’u arrit të merrej listë blloqesh", + "ep_adminpads2_search-heading": "Kërkoni për blloqe", + "ep_adminpads2_title": "Administrim blloku", + "ep_adminpads2_unknown-error": "Gabim i panjohur", + "ep_adminpads2_unknown-status": "Gjendje e panjohur" +} diff --git a/admin/public/ep_admin_pads/sv.json b/admin/public/ep_admin_pads/sv.json new file mode 100644 index 000000000..e77aaf2c4 --- /dev/null +++ b/admin/public/ep_admin_pads/sv.json @@ -0,0 +1,28 @@ +{ + "@metadata": { + "authors": [ + "Bengtsson96", + "WikiPhoenix" + ] + }, + "ep_adminpads2_action": "Åtgärd", + "ep_adminpads2_autoupdate-label": "Uppdatera automatiskt när blocket ändras", + "ep_adminpads2_autoupdate.title": "Aktivera eller inaktivera automatiska uppdatering för nuvarande förfrågan.", + "ep_adminpads2_confirm": "Vill du verkligen radera blocket {{padID}}?", + "ep_adminpads2_delete.value": "Radera", + "ep_adminpads2_last-edited": "Senast redigerad", + "ep_adminpads2_loading": "Läser in …", + "ep_adminpads2_manage-pads": "Hantera block", + "ep_adminpads2_no-results": "Inga resultat", + "ep_adminpads2_pad-user-count": "Antal blockanvändare", + "ep_adminpads2_padname": "Blocknamn", + "ep_adminpads2_search-box.placeholder": "Sökord", + "ep_adminpads2_search-button.value": "Sök", + "ep_adminpads2_search-done": "Sökning slutförd", + "ep_adminpads2_search-error-explanation": "Servern stötte på ett fel vid sökning efter block:", + "ep_adminpads2_search-error-title": "Misslyckades att hämta blocklista", + "ep_adminpads2_search-heading": "Sök efter block", + "ep_adminpads2_title": "Blockadministration", + "ep_adminpads2_unknown-error": "Okänt fel", + "ep_adminpads2_unknown-status": "Okänd status" +} diff --git a/admin/public/ep_admin_pads/sw.json b/admin/public/ep_admin_pads/sw.json new file mode 100644 index 000000000..f1beeecbb --- /dev/null +++ b/admin/public/ep_admin_pads/sw.json @@ -0,0 +1,27 @@ +{ + "@metadata": { + "authors": [ + "Andibecker" + ] + }, + "ep_adminpads2_action": "Hatua", + "ep_adminpads2_autoupdate-label": "Sasisha kiotomatiki kwenye mabadiliko ya pedi", + "ep_adminpads2_autoupdate.title": "Huwasha au kulemaza sasisho otomatiki kwa hoja ya sasa.", + "ep_adminpads2_confirm": "Je! Kweli unataka kufuta pedi {{padID}}?", + "ep_adminpads2_delete.value": "Futa", + "ep_adminpads2_last-edited": "Ilihaririwa mwisho", + "ep_adminpads2_loading": "Inapakia...", + "ep_adminpads2_manage-pads": "Dhibiti pedi", + "ep_adminpads2_no-results": "Hakuna matokeo", + "ep_adminpads2_pad-user-count": "Hesabu ya mtumiaji wa pedi", + "ep_adminpads2_padname": "Jina la utani", + "ep_adminpads2_search-box.placeholder": "Neno la utaftaji", + "ep_adminpads2_search-button.value": "Tafuta", + "ep_adminpads2_search-done": "Utafutaji umekamilika", + "ep_adminpads2_search-error-explanation": "Seva ilipata hitilafu wakati wa kutafuta pedi:", + "ep_adminpads2_search-error-title": "Imeshindwa kupata orodha ya pedi", + "ep_adminpads2_search-heading": "Tafuta pedi", + "ep_adminpads2_title": "Usimamizi wa pedi", + "ep_adminpads2_unknown-error": "Hitilafu isiyojulikana", + "ep_adminpads2_unknown-status": "Hali isiyojulikana" +} diff --git a/admin/public/ep_admin_pads/th.json b/admin/public/ep_admin_pads/th.json new file mode 100644 index 000000000..693e3f797 --- /dev/null +++ b/admin/public/ep_admin_pads/th.json @@ -0,0 +1,27 @@ +{ + "@metadata": { + "authors": [ + "Andibecker" + ] + }, + "ep_adminpads2_action": "การกระทำ", + "ep_adminpads2_autoupdate-label": "อัปเดตอัตโนมัติเมื่อเปลี่ยนแผ่น", + "ep_adminpads2_autoupdate.title": "เปิดหรือปิดการอัปเดตอัตโนมัติสำหรับคิวรีปัจจุบัน", + "ep_adminpads2_confirm": "คุณต้องการลบแพด {{padID}} จริงหรือไม่", + "ep_adminpads2_delete.value": "ลบ", + "ep_adminpads2_last-edited": "แก้ไขล่าสุด", + "ep_adminpads2_loading": "กำลังโหลด…", + "ep_adminpads2_manage-pads": "จัดการแผ่นรอง", + "ep_adminpads2_no-results": "ไม่มีผลลัพธ์", + "ep_adminpads2_pad-user-count": "จำนวนผู้ใช้แพด", + "ep_adminpads2_padname": "นามแฝง", + "ep_adminpads2_search-box.placeholder": "คำที่ต้องการค้นหา", + "ep_adminpads2_search-button.value": "ค้นหา", + "ep_adminpads2_search-done": "ค้นหาเสร็จสมบูรณ์", + "ep_adminpads2_search-error-explanation": "เซิร์ฟเวอร์พบข้อผิดพลาดขณะค้นหาแผ่นอิเล็กโทรด:", + "ep_adminpads2_search-error-title": "ไม่สามารถรับรายการแผ่นรอง", + "ep_adminpads2_search-heading": "ค้นหาแผ่นรอง", + "ep_adminpads2_title": "การบริหารแผ่น", + "ep_adminpads2_unknown-error": "ข้อผิดพลาดที่ไม่รู้จัก", + "ep_adminpads2_unknown-status": "ไม่ทราบสถานะ" +} diff --git a/admin/public/ep_admin_pads/tl.json b/admin/public/ep_admin_pads/tl.json new file mode 100644 index 000000000..238e01236 --- /dev/null +++ b/admin/public/ep_admin_pads/tl.json @@ -0,0 +1,17 @@ +{ + "@metadata": { + "authors": [ + "Mrkczr" + ] + }, + "ep_adminpads2_action": "Kilos", + "ep_adminpads2_delete.value": "Burahin", + "ep_adminpads2_last-edited": "Huling binago", + "ep_adminpads2_loading": "Naglo-load...", + "ep_adminpads2_no-results": "Walang mga resulta", + "ep_adminpads2_search-box.placeholder": "Mga katagang hahanapin:", + "ep_adminpads2_search-button.value": "Hanapin", + "ep_adminpads2_search-done": "Natapos na ang paghahanap", + "ep_adminpads2_unknown-error": "Hindi nalalamang kamalian", + "ep_adminpads2_unknown-status": "Hindi alam na katayuan" +} diff --git a/admin/public/ep_admin_pads/tr.json b/admin/public/ep_admin_pads/tr.json new file mode 100644 index 000000000..7e2e9d402 --- /dev/null +++ b/admin/public/ep_admin_pads/tr.json @@ -0,0 +1,28 @@ +{ + "@metadata": { + "authors": [ + "Hedda", + "MuratTheTurkish" + ] + }, + "ep_adminpads2_action": "Eylem", + "ep_adminpads2_autoupdate-label": "Bloknot değişikliklerinde otomatik güncelleme", + "ep_adminpads2_autoupdate.title": "Mevcut sorgu için otomatik güncellemeleri etkinleştirir veya devre dışı bırakır.", + "ep_adminpads2_confirm": "{{padID}} bloknotunu gerçekten silmek istiyor musunuz?", + "ep_adminpads2_delete.value": "Sil", + "ep_adminpads2_last-edited": "Son düzenleme", + "ep_adminpads2_loading": "Yükleniyor...", + "ep_adminpads2_manage-pads": "Bloknotları yönet", + "ep_adminpads2_no-results": "Sonuç yok", + "ep_adminpads2_pad-user-count": "Bloknot kullanıcı sayısı", + "ep_adminpads2_padname": "Bloknot adı", + "ep_adminpads2_search-box.placeholder": "Terimi ara", + "ep_adminpads2_search-button.value": "Ara", + "ep_adminpads2_search-done": "Arama tamamlandı", + "ep_adminpads2_search-error-explanation": "Sunucu, bloknotları ararken bir hatayla karşılaştı:", + "ep_adminpads2_search-error-title": "Bloknot listesi alınamadı", + "ep_adminpads2_search-heading": "Bloknotları ara", + "ep_adminpads2_title": "Bloknot yönetimi", + "ep_adminpads2_unknown-error": "Bilinmeyen hata", + "ep_adminpads2_unknown-status": "Bilinmeyen durum" +} diff --git a/admin/public/ep_admin_pads/uk.json b/admin/public/ep_admin_pads/uk.json new file mode 100644 index 000000000..c5c95f722 --- /dev/null +++ b/admin/public/ep_admin_pads/uk.json @@ -0,0 +1,28 @@ +{ + "@metadata": { + "authors": [ + "DDPAT", + "Ice bulldog" + ] + }, + "ep_adminpads2_action": "Дія", + "ep_adminpads2_autoupdate-label": "Автоматичне оновлення при зміні майданчика", + "ep_adminpads2_autoupdate.title": "Вмикає або вимикає автоматичне оновлення поточного запиту.", + "ep_adminpads2_confirm": "Ви дійсно хочете видалити панель {{padID}}?", + "ep_adminpads2_delete.value": "Видалити", + "ep_adminpads2_last-edited": "Останнє редагування", + "ep_adminpads2_loading": "Завантаження…", + "ep_adminpads2_manage-pads": "Управління майданчиками", + "ep_adminpads2_no-results": "Немає результатів", + "ep_adminpads2_pad-user-count": "Кількість майданчиків користувача", + "ep_adminpads2_padname": "Назва майданчика", + "ep_adminpads2_search-box.placeholder": "Пошуковий термін", + "ep_adminpads2_search-button.value": "Пошук", + "ep_adminpads2_search-done": "Пошук завершено", + "ep_adminpads2_search-error-explanation": "Під час пошуку педів сервер виявив помилку:", + "ep_adminpads2_search-error-title": "Не вдалося отримати список панелей", + "ep_adminpads2_search-heading": "Пошук майданчиків", + "ep_adminpads2_title": "Введення майданчиків", + "ep_adminpads2_unknown-error": "Невідома помилка", + "ep_adminpads2_unknown-status": "Невідомий статус" +} diff --git a/admin/public/ep_admin_pads/zh-hans.json b/admin/public/ep_admin_pads/zh-hans.json new file mode 100644 index 000000000..cdf0d945f --- /dev/null +++ b/admin/public/ep_admin_pads/zh-hans.json @@ -0,0 +1,29 @@ +{ + "@metadata": { + "authors": [ + "GuoPC", + "Lakejason0", + "沈澄心" + ] + }, + "ep_adminpads2_action": "操作", + "ep_adminpads2_autoupdate-label": "在记事本更改时自动更新", + "ep_adminpads2_autoupdate.title": "启用或禁用目前查询的自动更新", + "ep_adminpads2_confirm": "您确定要删除记事本 {{padID}}?", + "ep_adminpads2_delete.value": "删除", + "ep_adminpads2_last-edited": "上次编辑于", + "ep_adminpads2_loading": "正在加载…", + "ep_adminpads2_manage-pads": "管理记事本", + "ep_adminpads2_no-results": "没有结果", + "ep_adminpads2_pad-user-count": "记事本用户数", + "ep_adminpads2_padname": "记事本名称", + "ep_adminpads2_search-box.placeholder": "搜索关键词", + "ep_adminpads2_search-button.value": "搜索", + "ep_adminpads2_search-done": "搜索完成", + "ep_adminpads2_search-error-explanation": "搜索记事本时服务器发生错误:", + "ep_adminpads2_search-error-title": "获取记事本列表失败", + "ep_adminpads2_search-heading": "搜索记事本", + "ep_adminpads2_title": "记事本管理", + "ep_adminpads2_unknown-error": "未知错误", + "ep_adminpads2_unknown-status": "未知状态" +} diff --git a/admin/public/ep_admin_pads/zh-hant.json b/admin/public/ep_admin_pads/zh-hant.json new file mode 100644 index 000000000..daeed55f5 --- /dev/null +++ b/admin/public/ep_admin_pads/zh-hant.json @@ -0,0 +1,28 @@ +{ + "@metadata": { + "authors": [ + "HellojoeAoPS", + "Kly" + ] + }, + "ep_adminpads2_action": "操作", + "ep_adminpads2_autoupdate-label": "在記事本更改時自動更新", + "ep_adminpads2_autoupdate.title": "啟用或停用目前查詢的自動更新。", + "ep_adminpads2_confirm": "您確定要刪除記事本 {{padID}}?", + "ep_adminpads2_delete.value": "刪除", + "ep_adminpads2_last-edited": "上一次編輯", + "ep_adminpads2_loading": "載入中…", + "ep_adminpads2_manage-pads": "管理記事本", + "ep_adminpads2_no-results": "沒有結果", + "ep_adminpads2_pad-user-count": "記事本使用者數", + "ep_adminpads2_padname": "記事本名稱", + "ep_adminpads2_search-box.placeholder": "搜尋關鍵字", + "ep_adminpads2_search-button.value": "搜尋", + "ep_adminpads2_search-done": "搜尋完成", + "ep_adminpads2_search-error-explanation": "當搜尋記事本時伺服器發生錯誤:", + "ep_adminpads2_search-error-title": "取得記事本清單失敗", + "ep_adminpads2_search-heading": "搜尋記事本", + "ep_adminpads2_title": "記事本管理", + "ep_adminpads2_unknown-error": "不明錯誤", + "ep_adminpads2_unknown-status": "不明狀態" +} diff --git a/admin/public/fond.jpg b/admin/public/fond.jpg new file mode 100644 index 000000000..81357c7bb Binary files /dev/null and b/admin/public/fond.jpg differ diff --git a/admin/src/App.css b/admin/src/App.css new file mode 100644 index 000000000..e69de29bb diff --git a/admin/src/App.tsx b/admin/src/App.tsx new file mode 100644 index 000000000..ae23ab3d3 --- /dev/null +++ b/admin/src/App.tsx @@ -0,0 +1,120 @@ +import {useEffect, useState} from 'react' +import './App.css' +import {connect} from 'socket.io-client' +import {isJSONClean} from './utils/utils.ts' +import {NavLink, Outlet, useNavigate} from "react-router-dom"; +import {useStore} from "./store/store.ts"; +import {LoadingScreen} from "./utils/LoadingScreen.tsx"; +import {Trans, useTranslation} from "react-i18next"; +import {Cable, Construction, Crown, NotepadText, Wrench, PhoneCall, LucideMenu} from "lucide-react"; + +const WS_URL = import.meta.env.DEV ? 'http://localhost:9001' : '' +export const App = () => { + const setSettings = useStore(state => state.setSettings); + const {t} = useTranslation() + const navigate = useNavigate() + const [sidebarOpen, setSidebarOpen] = useState(true) + + useEffect(() => { + fetch('/admin-auth/', { + method: 'POST' + }).then((value) => { + if (!value.ok) { + navigate('/login') + } + }).catch(() => { + navigate('/login') + }) + }, []); + + useEffect(() => { + document.title = t('admin.page-title') + + useStore.getState().setShowLoading(true); + const settingSocket = connect(`${WS_URL}/settings`, { + transports: ['websocket'], + }); + + const pluginsSocket = connect(`${WS_URL}/pluginfw/installer`, { + transports: ['websocket'], + }) + + pluginsSocket.on('connect', () => { + useStore.getState().setPluginsSocket(pluginsSocket); + }); + + + settingSocket.on('connect', () => { + useStore.getState().setSettingsSocket(settingSocket); + useStore.getState().setShowLoading(false) + settingSocket.emit('load'); + console.log('connected'); + }); + + settingSocket.on('disconnect', (reason) => { + // The settingSocket.io client will automatically try to reconnect for all reasons other than "io + // server disconnect". + useStore.getState().setShowLoading(true) + if (reason === 'io server disconnect') { + settingSocket.connect(); + } + }); + + settingSocket.on('settings', (settings) => { + /* Check whether the settings.json is authorized to be viewed */ + if (settings.results === 'NOT_ALLOWED') { + console.log('Not allowed to view settings.json') + return; + } + + /* Check to make sure the JSON is clean before proceeding */ + if (isJSONClean(settings.results)) { + setSettings(settings.results); + } else { + alert('Invalid JSON'); + } + useStore.getState().setShowLoading(false); + }); + + settingSocket.on('saveprogress', (status) => { + console.log(status) + }) + + return () => { + settingSocket.disconnect(); + pluginsSocket.disconnect() + } + }, []); + + return
+ +
+
+ + +

Etherpad

+
+
    { + if (window.innerWidth < 768) { + setSidebarOpen(false) + } + }}> +
  • +
  • +
  • +
  • +
  • Communication
  • +
+
+
+ +
+ +
+
+} + +export default App diff --git a/admin/src/components/IconButton.tsx b/admin/src/components/IconButton.tsx new file mode 100644 index 000000000..876a55779 --- /dev/null +++ b/admin/src/components/IconButton.tsx @@ -0,0 +1,16 @@ +import {FC, JSX, ReactElement} from "react"; + +export type IconButtonProps = { + icon: JSX.Element, + title: string|ReactElement, + onClick: ()=>void, + className?: string, + disabled?: boolean +} + +export const IconButton:FC = ({icon,className,onClick,title, disabled})=>{ + return +} diff --git a/admin/src/components/SearchField.tsx b/admin/src/components/SearchField.tsx new file mode 100644 index 000000000..62a965d40 --- /dev/null +++ b/admin/src/components/SearchField.tsx @@ -0,0 +1,14 @@ +import {ChangeEventHandler, FC} from "react"; +import {Search} from 'lucide-react' +export type SearchFieldProps = { + value: string, + onChange: ChangeEventHandler, + placeholder?: string +} + +export const SearchField:FC = ({onChange,value, placeholder})=>{ + return + + + +} diff --git a/admin/src/components/ShoutType.ts b/admin/src/components/ShoutType.ts new file mode 100644 index 000000000..f7e8b1df3 --- /dev/null +++ b/admin/src/components/ShoutType.ts @@ -0,0 +1,13 @@ +export type ShoutType = { + type: string, + data:{ + type: string, + payload: { + message: { + message: string, + sticky: boolean + }, + timestamp: number + } + } +} diff --git a/admin/src/index.css b/admin/src/index.css new file mode 100644 index 000000000..acc0d2e97 --- /dev/null +++ b/admin/src/index.css @@ -0,0 +1,870 @@ +:root { + --etherpad-color: #0f775b; + --etherpad-comp: #9C8840; + --etherpad-light: #99FF99; + --sidebar-width: 20em; +} + +@font-face { + font-family: Karla; + src: url(/Karla-Regular.ttf); +} + +html, body, #root { + box-sizing: border-box; + height: 100%; + font-family: "Karla", sans-serif; +} + +*, *:before, *:after { + box-sizing: inherit; + font-size: 16px; +} + +body { + margin: 0; + color: #333; + font: 14px helvetica, sans-serif; + background: #eee; +} + +div.menu { + left: 0; + transition: left .3s; + height: 100vh; + font-size: 16px; + font-weight: bolder; + display: flex; + align-items: center; + justify-content: center; + width: var(--sidebar-width); + z-index: 99; + position: fixed; +} + + +.icon-button { + display: flex; + gap: 10px; + background-color: var(--etherpad-color); + color: white; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; +} + +.icon-button svg { + align-self: center; +} + +.icon-button span { + align-self: center; +} + + +div.menu span:first-child { + display: flex; + justify-content: center; +} + +div.menu span:first-child svg { + margin-right: 10px; + align-self: center; +} + + +div.menu h1 { + font-size: 50px; + text-align: center; +} + +.inner-menu { + border-radius: 0 20px 20px 0; + padding: 10px; + flex-grow: 100; + background-color: var(--etherpad-comp); + color: white; + height: 100vh; +} + +div.menu ul { + color: white; + padding: 0; +} + +div.menu li a { + display: flex; + gap: 10px; + margin-bottom: 20px; +} + +div.menu svg { + align-self: center; +} + +div.menu li { + padding: 10px; + color: white; + list-style: none; + margin-left: 3px; + line-height: 3; +} + + +div.menu li:has(.active) { + background-color: #9C885C; +} + +div.menu li a { + color: lightgray; +} + + +div.innerwrapper { + transition: margin-left .3s; + isolation: isolate; + background-color: #F0F0F0; + overflow: auto; + height: 100vh; + flex-grow: 100; + margin-left: var(--sidebar-width); + padding: 20px 20px 20px; +} + +div.innerwrapper-err { + display: none; +} + +#wrapper { + background: none repeat scroll 0px 0px #FFFFFF; + box-shadow: 0px 1px 10px rgba(0, 0, 0, 0.2); + min-height: 100%; /*always display a scrollbar*/ +} + +h1 { + font-size: 29px; +} + +h2 { + font-size: 24px; +} + +.separator { + margin: 10px 0; + height: 1px; + background: #aaa; + background: -webkit-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff); + background: -moz-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff); + background: -ms-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff); + background: -o-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff); +} + +form { + margin-bottom: 0; +} + +#inner { + width: 300px; + margin: 0 auto; +} + +input { + font-weight: bold; + font-size: 15px; +} + + +.sort { + cursor: pointer; +} + +.sort:after { + content: '▲▼' +} + +.sort.up:after { + content: '▲' +} + +.sort.down:after { + content: '▼' +} + + +#installed-plugins thead tr th:nth-child(3) { + width: 15%; +} + +table { + border: 1px solid #ddd; + border-radius: 3px; + border-spacing: 0; + width: 100%; + margin: 20px 0; +} + +.table-container { + width: 100%; + overflow: auto; + max-height: 90vh; +} + + +#available-plugins th:first-child, #available-plugins th:nth-child(2) { + text-align: center; +} + +td, th { + padding: 5px; +} + +.template { + display: none; +} + +#installed-plugins td > div { + position: relative; /* Allows us to position the loading indicator relative to this row */ + display: inline-block; /*make this fill the whole cell*/ + width: 100%; +} + +.messages { + height: 5em; +} + +.messages * { + display: none; + text-align: center; +} + +.messages .fetching { + display: block; +} + +.progress { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + padding: auto; + + background: rgb(255, 255, 255); + display: none; +} + +#search-progress.progress { + padding-top: 20%; + background: rgba(255, 255, 255, 0.3); +} + +.progress * { + display: block; + margin: 0 auto; + text-align: center; + color: #666; +} + + +.settings-page { + display: flex; + flex-direction: column; + gap: 20px; + height: 100%; +} + +.settings { + flex-grow: max(1, 1); + outline: none; + width: 100%; + resize: none; + font-family: monospace; +} + +#response { + display: inline; +} + +a:link, a:visited, a:hover, a:focus { + color: #333333; + text-decoration: none; +} + +a:focus, a:hover { + text-decoration: underline; +} + +.installed-results a:link, +.search-results a:link, +.installed-results a:visited, +.search-results a:visited, +.installed-results a:hover, +.search-results a:hover, +.installed-results a:focus, +.search-results a:focus { + text-decoration: underline; +} + +.installed-results a:focus, +.search-results a:focus, +.installed-results a:hover, +.search-results a:hover { + text-decoration: none; +} + +pre { + white-space: pre-wrap; + word-wrap: break-word; +} + + +#icon-button { + color: var(--etherpad-color); + top: 10px; + background-color: transparent; + border: none; + z-index: 99; + position: absolute; + left: 10px; +} + + +.inner-menu span:nth-child(2) { + display: flex; + margin-top: 30px; +} + +#wrapper.closed .menu { + left: calc(-1 * var(--sidebar-width)); +} + +#wrapper.closed .innerwrapper { + margin-left: 0; +} + +@media (max-width: 800px) { + + div.innerwrapper { + margin-left: 0; + } + + .inner-menu { + border-radius: 0; + } + + div.menu { + height: auto; + border-right: none; + --sidebar-width: 100%; + float: left; + } + + table { + border: none; + } + + table, thead, tbody, td, tr { + display: block; + } + + thead tr { + display: none; + } + + tr { + border: 1px solid #ccc; + margin-bottom: 5px; + border-radius: 3px; + } + + td { + border: none; + border-bottom: 1px solid #eee; + position: relative; + padding-left: 50%; + white-space: normal; + text-align: left; + } + + td.name { + word-wrap: break-word; + } + + td:before { + position: absolute; + top: 6px; + left: 6px; + text-align: left; + padding-right: 10px; + white-space: nowrap; + font-weight: bold; + content: attr(data-label); + } + + td:last-child { + border-bottom: none; + } + + table input[type="button"] { + float: none; + } +} + + +.settings-button-bar { + margin-top: 10px; + display: flex; + gap: 10px; +} + +.login-background { + background-image: url("/fond.jpg"); + background-position: center; + background-repeat: no-repeat; + background-size: cover; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background-color: #f0f0f0; +} + +.login-inner-box div { + margin-top: 1rem; +} + +.login-inner-box [type=submit] { + margin-top: 2rem; +} + + +.login-textinput { + width: 100%; + padding: 10px; + background-color: #fffacc; + border-radius: 5px; + border: 1px solid #ccc; + margin-bottom: 10px; +} + +.login-box { + padding: 20px; + border-radius: 40px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + background-color: #fff; +} + + +@media (max-width: 900px) { + .login-box { + width: 90% + } +} + +.login-inner-box { + position: relative; + padding: 20px; +} + +.login-title { + padding: 0; + margin: 0; + text-align: center; + color: var(--etherpad-color); + font-size: 4rem; + font-weight: 1000; +} + +.login-button { + padding: 10px; + background-color: var(--etherpad-color); + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + width: 100%; + height: 40px; +} + +.dialog-overlay { + position: fixed; + inset: 0; + background-color: white; + z-index: 100; +} + + +.dialog-confirm-overlay { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 100; +} + + +.dialog-confirm-content { + position: fixed; + top: 50%; + left: 50%; + background-color: white; + transform: translate(-50%, -50%); + padding: 20px; + z-index: 101; +} + + +.dialog-content { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + padding: 20px; + z-index: 101; +} + +.dialog-title { + color: var(--etherpad-color); + font-size: 2em; + margin-bottom: 20px; +} + + +.ToastViewport { + position: fixed; + top: 10px; + right: 20px; + display: flex; + flex-direction: column; + gap: 10px; + width: 390px; + max-width: 100vw; + margin: 0; + list-style: none; + z-index: 2147483647; + outline: none; +} + +.ToastRootSuccess { + background-color: lawngreen; +} + +.ToastRootFailure { + background-color: red; +} + +.ToastRootFailure > .ToastTitle { + color: white; +} + +.ToastRoot { + border-radius: 20px; + box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px; + padding: 15px; + display: grid; + grid-template-areas: 'title action' 'description action'; + grid-template-columns: auto max-content; + column-gap: 15px; + align-items: center; +} + +.ToastRoot[data-state='open'] { + animation: slideIn 150ms cubic-bezier(0.16, 1, 0.3, 1); +} + +.ToastRoot[data-state='closed'] { + animation: hide 100ms ease-in; +} + +.ToastRoot[data-swipe='move'] { + transform: translateX(var(--radix-toast-swipe-move-x)); +} + +.ToastRoot[data-swipe='cancel'] { + transform: translateX(0); + transition: transform 200ms ease-out; +} + +.ToastRoot[data-swipe='end'] { + animation: swipeOut 100ms ease-out; +} + +@keyframes hide { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +@keyframes slideIn { + from { + transform: translateX(calc(100% + var(--viewport-padding))); + } + to { + transform: translateX(0); + } +} + +@keyframes swipeOut { + from { + transform: translateX(var(--radix-toast-swipe-end-x)); + } + to { + transform: translateX(calc(100% + var(--viewport-padding))); + } +} + +.ToastTitle { + grid-area: title; + margin-bottom: 5px; + font-weight: 500; + color: var(--slate-12); + padding: 10px; + font-size: 15px; +} + +.ToastDescription { + grid-area: description; + margin: 0; + color: var(--slate-11); + font-size: 13px; + line-height: 1.3; +} + +.ToastAction { + grid-area: action; +} + +.help-block { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 20px +} + +.search-field { + position: relative; +} + +.search-field input { + border-color: transparent; + border-radius: 20px; + height: 2.5rem; + width: 100%; + padding: 5px 5px 5px 30px; +} + +.search-field input:focus { + outline: none; +} + + +.send-message { + position: relative; +} + +.send-message input { + width: auto; +} + +.send-message { +} + +.send-message svg { + position: absolute; + right: 3px; + bottom: -3px; + left: auto !important; +} + +.search-field svg { + position: absolute; + left: 3px; + bottom: -3px; +} + + +.search-field svg { + color: gray +} + +table { + margin: 25px 0; + font-size: 0.9em; + font-family: sans-serif; + min-width: 400px; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.15); +} + +th:first-child { + border-top-left-radius: 10px; +} + +th:last-child { + border-top-right-radius: 10px; +} + +table thead tr { + font-size: 25px; + background-color: var(--etherpad-color); + color: #ffffff; + text-align: left; +} + +table tbody tr { + border-bottom: 1px solid #dddddd; +} + +table tr:nth-child(even) td { + background-color: lightgray; +} + +table tr td { + padding: 12px 15px; +} + +table tbody tr:nth-of-type(even) { + background-color: #f3f3f3; +} + +table tbody tr:last-of-type { + border-bottom: 2px solid #009879; +} + +table tbody tr.active-row { + font-weight: bold; + color: #009879; +} + + +.pad-pagination { + display: flex; + justify-content: center; + gap: 10px; + margin-top: 20px; +} + +.pad-pagination button { + display: flex; + padding: 10px 20px; + border-radius: 5px; + border: none; + color: black; + cursor: pointer; +} + + +.pad-pagination button:disabled { + background: transparent; + color: lightgrey; + cursor: not-allowed; +} + +.pad-pagination span { + align-self: center; +} + +.pad-pagination > span { + font-size: 20px; +} + + +.login-page .login-form .input-control input[type=text], .login-page .login-form .input-control input[type=email], .login-page .login-form .input-control input[type=password], .login-page .signup-form .input-control input[type=text], .login-page .signup-form .input-control input[type=email], .login-page .signup-form .input-control input[type=password], .login-page .forgot-form .input-control input[type=text], .login-page .forgot-form .input-control input[type=email], .login-page .forgot-form .input-control input[type=password] { + width: 100%; + padding: 12px 20px; + margin: 8px 0; + display: inline-block; + border-bottom: 2px solid #ccc; + border-top: 0; + border-left: 0; + border-right: 0; + -webkit-box-sizing: border-box; + box-sizing: border-box; + border-radius: 5px; + font-size: 14px; + color: #666; + background-color: #f8f8f8; + -webkit-transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; +} + +input, button, select, optgroup, textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +.icon-input { + position: relative; +} + +.icon-input svg { + position: absolute; + top: 50%; + transform: translateY(-50%); + right: 10px; + color: #666; +} + + +.SwitchRoot { + align-self: center; + width: 60px; + height: 30px; + background-color: black; + border-radius: 9999px; + position: relative; + box-shadow: 0 2px 10px var(--black-a7); + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +.SwitchRoot:focus { + box-shadow: 0 0 0 2px black; +} + +.SwitchRoot[data-state='checked'] { + background-color: var(--etherpad-color); +} + +.SwitchThumb { + display: block; + width: 20px; + height: 20px; + background-color: white; + border-radius: 9999px; + box-shadow: 0 2px 2px var(--black-a7); + transition: transform 100ms; + transform: translateX(2px); + will-change: transform; +} + +.SwitchThumb[data-state='checked'] { + transform: translateX(25px); +} + +.Label { + color: white; + font-size: 15px; + line-height: 1; +} + +.message { + position: relative; + padding: 10px; + border: 1px solid #e0e0e0; + margin: 10px 20px 10px 10px; + border-radius: 10px 0 10px 10px; + background-color: var(--etherpad-color); + color: white +} + +.search-pads { + text-align: center; +} + +.search-pads-body tr td:last-child { + display: flex; + justify-content: center; +} diff --git a/admin/src/localization/i18n.ts b/admin/src/localization/i18n.ts new file mode 100644 index 000000000..67ae140e7 --- /dev/null +++ b/admin/src/localization/i18n.ts @@ -0,0 +1,57 @@ +import i18n from 'i18next' +import {initReactI18next} from "react-i18next"; +import LanguageDetector from 'i18next-browser-languagedetector' + + +import { BackendModule } from 'i18next'; + +const LazyImportPlugin: BackendModule = { + type: 'backend', + init: function () { + }, + read: async function (language, namespace, callback) { + + let baseURL = import.meta.env.BASE_URL + if(namespace === "translation") { + // If default we load the translation file + baseURL+=`/locales/${language}.json` + } else { + // Else we load the former plugin translation file + baseURL+=`/${namespace}/${language}.json` + } + + const localeJSON = await fetch(baseURL, { + cache: "force-cache" + }) + let json; + + try { + json = JSON.parse(await localeJSON.text()) + } catch(e) { + callback(new Error("Error loading"), null); + } + + + callback(null, json); + }, + + save: function () { + }, + + create: function () { + /* save the missing translation */ + }, +}; + +i18n + .use(LanguageDetector) + .use(LazyImportPlugin) + .use(initReactI18next) + .init( + { + ns: ['translation','ep_admin_pads'], + fallbackLng: 'en' + } + ) + +export default i18n diff --git a/admin/src/main.tsx b/admin/src/main.tsx new file mode 100644 index 000000000..5efc26de6 --- /dev/null +++ b/admin/src/main.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.tsx' +import './index.css' +import {createBrowserRouter, createRoutesFromElements, Route, RouterProvider} from "react-router-dom"; +import {HomePage} from "./pages/HomePage.tsx"; +import {SettingsPage} from "./pages/SettingsPage.tsx"; +import {LoginScreen} from "./pages/LoginScreen.tsx"; +import {HelpPage} from "./pages/HelpPage.tsx"; +import * as Toast from '@radix-ui/react-toast' +import {I18nextProvider} from "react-i18next"; +import i18n from "./localization/i18n.ts"; +import {PadPage} from "./pages/PadPage.tsx"; +import {ToastDialog} from "./utils/Toast.tsx"; +import {ShoutPage} from "./pages/ShoutPage.tsx"; + +const router = createBrowserRouter(createRoutesFromElements( + <>}> + }/> + }/> + }/> + }/> + }/> + }/> + + }/> + +), { + basename: import.meta.env.BASE_URL +}) + + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + + , +) diff --git a/admin/src/pages/HelpPage.tsx b/admin/src/pages/HelpPage.tsx new file mode 100644 index 000000000..dd9695b0a --- /dev/null +++ b/admin/src/pages/HelpPage.tsx @@ -0,0 +1,70 @@ +import {Trans} from "react-i18next"; +import {useStore} from "../store/store.ts"; +import {useEffect, useState} from "react"; +import {HelpObj} from "./Plugin.ts"; + +export const HelpPage = () => { + const settingsSocket = useStore(state=>state.settingsSocket) + const [helpData, setHelpData] = useState(); + + useEffect(() => { + if(!settingsSocket) return; + settingsSocket?.on('reply:help', (data) => { + setHelpData(data) + }); + + settingsSocket?.emit('help'); + }, [settingsSocket]); + + const renderHooks = (hooks:Record>) => { + return Object.keys(hooks).map((hookName, i) => { + return
+

{hookName}

+
    + {Object.keys(hooks[hookName]).map((hook, i) =>
  • {hook} +
      + {Object.keys(hooks[hookName][hook]).map((subHook, i) =>
    • {subHook}
    • )} +
    +
  • )} +
+
+ }) + } + + + if (!helpData) return
+ + return
+

+
+
+
{helpData?.epVersion}
+
+
{helpData.latestVersion}
+
Git sha
+
{helpData.gitCommit}
+
+

+
    + {helpData.installedPlugins.map((plugin, i) =>
  • {plugin}
  • )} +
+ +

+
    + {helpData.installedParts.map((part, i) =>
  • {part}
  • )} +
+ +

+ { + renderHooks(helpData.installedServerHooks) + } + +

+ + { + renderHooks(helpData.installedClientHooks) + } +

+ +
+} diff --git a/admin/src/pages/HomePage.tsx b/admin/src/pages/HomePage.tsx new file mode 100644 index 000000000..f5ce0a5ab --- /dev/null +++ b/admin/src/pages/HomePage.tsx @@ -0,0 +1,247 @@ +import {useStore} from "../store/store.ts"; +import {useEffect, useMemo, useState} from "react"; +import {InstalledPlugin, PluginDef, SearchParams} from "./Plugin.ts"; +import {useDebounce} from "../utils/useDebounce.ts"; +import {Trans, useTranslation} from "react-i18next"; +import {SearchField} from "../components/SearchField.tsx"; +import {ArrowUpFromDot, Download, Trash} from "lucide-react"; +import {IconButton} from "../components/IconButton.tsx"; +import {determineSorting} from "../utils/sorting.ts"; + + +export const HomePage = () => { + const pluginsSocket = useStore(state=>state.pluginsSocket) + const [plugins,setPlugins] = useState([]) + const installedPlugins = useStore(state=>state.installedPlugins) + const setInstalledPlugins = useStore(state=>state.setInstalledPlugins) + const [searchParams, setSearchParams] = useState({ + offset: 0, + limit: 99999, + sortBy: 'name', + sortDir: 'asc', + searchTerm: '' + }) + + const filteredInstallablePlugins = useMemo(()=>{ + return plugins.sort((a, b)=>{ + if(searchParams.sortBy === "version"){ + if(searchParams.sortDir === "asc"){ + return a.version.localeCompare(b.version) + } + return b.version.localeCompare(a.version) + } + + if(searchParams.sortBy === "last-updated"){ + if(searchParams.sortDir === "asc"){ + return a.time.localeCompare(b.time) + } + return b.time.localeCompare(a.time) + } + + + if (searchParams.sortBy === "name") { + if(searchParams.sortDir === "asc"){ + return a.name.localeCompare(b.name) + } + return b.name.localeCompare(a.name) + } + return 0 + }) + }, [plugins, searchParams]) + + const sortedInstalledPlugins = useMemo(()=>{ + return useStore.getState().installedPlugins.sort((a, b)=>{ + + if(a.name < b.name){ + return -1 + } + if(a.name > b.name){ + return 1 + } + return 0 + }) + + } ,[installedPlugins, searchParams]) + + const [searchTerm, setSearchTerm] = useState('') + const {t} = useTranslation() + + + useEffect(() => { + if(!pluginsSocket){ + return + } + + pluginsSocket.on('results:installed', (data:{ + installed: InstalledPlugin[] + })=>{ + setInstalledPlugins(data.installed) + }) + + pluginsSocket.on('results:updatable', (data) => { + const newInstalledPlugins = useStore.getState().installedPlugins.map(plugin => { + if (data.updatable.includes(plugin.name)) { + return { + ...plugin, + updatable: true + } + } + return plugin + }) + setInstalledPlugins(newInstalledPlugins) + }) + + pluginsSocket.on('finished:install', () => { + pluginsSocket!.emit('getInstalled'); + }) + + pluginsSocket.on('finished:uninstall', () => { + console.log("Finished uninstall") + }) + + + // Reload on reconnect + pluginsSocket.on('connect', ()=>{ + // Initial retrieval of installed plugins + pluginsSocket.emit('getInstalled'); + pluginsSocket.emit('search', searchParams) + }) + + pluginsSocket.emit('getInstalled'); + + // check for updates every 5mins + const interval = setInterval(() => { + pluginsSocket.emit('checkUpdates'); + }, 1000 * 60 * 5); + + return ()=>{ + clearInterval(interval) + } + }, [pluginsSocket]); + + + useEffect(() => { + if (!pluginsSocket) { + return + } + pluginsSocket?.emit('search', searchParams) + pluginsSocket!.on('results:search', (data: { + results: PluginDef[] + }) => { + setPlugins(data.results) + }) + pluginsSocket!.on('results:searcherror', (data: {error: string}) => { + console.log(data.error) + useStore.getState().setToastState({ + open: true, + title: "Error retrieving plugins", + success: false + }) + }) + }, [searchParams, pluginsSocket]); + + const uninstallPlugin = (pluginName: string)=>{ + pluginsSocket!.emit('uninstall', pluginName); + // Remove plugin + setInstalledPlugins(installedPlugins.filter(i=>i.name !== pluginName)) + } + + const installPlugin = (pluginName: string)=>{ + pluginsSocket!.emit('install', pluginName); + setPlugins(plugins.filter(plugin=>plugin.name !== pluginName)) + } + + useDebounce(()=>{ + setSearchParams({ + ...searchParams, + offset: 0, + searchTerm: searchTerm + }) + }, 500, [searchTerm]) + + + return
+

+ +

+ + + + + + + + + + + {sortedInstalledPlugins.map((plugin, index) => { + return + + + + + })} + +
{plugin.name}{plugin.version} + { + plugin.updatable ? + installPlugin(plugin.name)} icon={} title="Update"> + : } title={} onClick={() => uninstallPlugin(plugin.name)}/> + } +
+ + +

+ {setSearchTerm(v.target.value)}} placeholder={t('admin_plugins.available_search.placeholder')} value={searchTerm}/> + +
+ + + + + + + + + + + + {(filteredInstallablePlugins.length > 0) ? + filteredInstallablePlugins.map((plugin) => { + return + + + + + + + }) + : + + } + +
{ + setSearchParams({ + ...searchParams, + sortBy: 'name', + sortDir: searchParams.sortDir === "asc"? "desc": "asc" + }) + }}> + { + setSearchParams({ + ...searchParams, + sortBy: 'version', + sortDir: searchParams.sortDir === "asc"? "desc": "asc" + }) + }}>{ + setSearchParams({ + ...searchParams, + sortBy: 'last-updated', + sortDir: searchParams.sortDir === "asc"? "desc": "asc" + }) + }}>
{plugin.name}{plugin.description}{plugin.version}{plugin.time} + } onClick={() => installPlugin(plugin.name)} title={}/> +
{searchTerm == '' ? : }
+
+
+} diff --git a/admin/src/pages/LoginScreen.tsx b/admin/src/pages/LoginScreen.tsx new file mode 100644 index 000000000..61ac8993e --- /dev/null +++ b/admin/src/pages/LoginScreen.tsx @@ -0,0 +1,61 @@ +import {useStore} from "../store/store.ts"; +import {useNavigate} from "react-router-dom"; +import {SubmitHandler, useForm} from "react-hook-form"; +import {Eye, EyeOff} from "lucide-react"; +import {useState} from "react"; + +type Inputs = { + username: string + password: string +} + +export const LoginScreen = ()=>{ + const navigate = useNavigate() + const [passwordVisible, setPasswordVisible] = useState(false) + + const { + register, + handleSubmit} = useForm() + + const login: SubmitHandler = ({username,password})=>{ + fetch('/admin-auth/', { + method: 'POST', + headers:{ + Authorization: `Basic ${btoa(`${username}:${password}`)}` + } + }).then(r=>{ + if(!r.ok) { + useStore.getState().setToastState({ + open: true, + title: "Login failed", + success: false + }) + } else { + navigate('/') + } + }).catch(e=>{ + console.error(e) + }) + } + + return
+
+

Etherpad

+
+
Username
+ +
Password
+ + + {passwordVisible? setPasswordVisible(!passwordVisible)}/> : + setPasswordVisible(!passwordVisible)}/>} + + +
+
+
+} diff --git a/admin/src/pages/PadPage.tsx b/admin/src/pages/PadPage.tsx new file mode 100644 index 000000000..b5db854f5 --- /dev/null +++ b/admin/src/pages/PadPage.tsx @@ -0,0 +1,221 @@ +import {Trans, useTranslation} from "react-i18next"; +import {useEffect, useMemo, useState} from "react"; +import {useStore} from "../store/store.ts"; +import {PadSearchQuery, PadSearchResult} from "../utils/PadSearch.ts"; +import {useDebounce} from "../utils/useDebounce.ts"; +import {determineSorting} from "../utils/sorting.ts"; +import * as Dialog from "@radix-ui/react-dialog"; +import {IconButton} from "../components/IconButton.tsx"; +import {ChevronLeft, ChevronRight, Eye, Trash2, FileStack} from "lucide-react"; +import {SearchField} from "../components/SearchField.tsx"; + +export const PadPage = ()=>{ + const settingsSocket = useStore(state=>state.settingsSocket) + const [searchParams, setSearchParams] = useState({ + offset: 0, + limit: 12, + pattern: '', + sortBy: 'padName', + ascending: true + }) + const {t} = useTranslation() + const [searchTerm, setSearchTerm] = useState('') + const pads = useStore(state=>state.pads) + const [currentPage, setCurrentPage] = useState(0) + const [deleteDialog, setDeleteDialog] = useState(false) + const [errorText, setErrorText] = useState(null) + const [padToDelete, setPadToDelete] = useState('') + const pages = useMemo(()=>{ + if(!pads){ + return 0; + } + + return Math.ceil(pads!.total / searchParams.limit) + },[pads, searchParams.limit]) + + useDebounce(()=>{ + setSearchParams({ + ...searchParams, + pattern: searchTerm + }) + + }, 500, [searchTerm]) + + useEffect(() => { + if(!settingsSocket){ + return + } + + settingsSocket.emit('padLoad', searchParams) + + }, [settingsSocket, searchParams]); + + useEffect(() => { + if(!settingsSocket){ + return + } + + settingsSocket.on('results:padLoad', (data: PadSearchResult)=>{ + useStore.getState().setPads(data); + }) + + + settingsSocket.on('results:deletePad', (padID: string)=>{ + const newPads = useStore.getState().pads?.results?.filter((pad)=>{ + return pad.padName !== padID + }) + useStore.getState().setPads({ + total: useStore.getState().pads!.total-1, + results: newPads + }) + }) + + settingsSocket.on('results:cleanupPadRevisions', (data)=>{ + let newPads = useStore.getState().pads?.results ?? [] + + if (data.error) { + setErrorText(data.error) + return + } + + newPads.forEach((pad)=>{ + if (pad.padName === data.padId) { + pad.revisionNumber = data.keepRevisions + } + }) + + useStore.getState().setPads({ + results: newPads, + total: useStore.getState().pads!.total + }) + }) + }, [settingsSocket, pads]); + + const deletePad = (padID: string)=>{ + settingsSocket?.emit('deletePad', padID) + } + + const cleanupPad = (padID: string)=>{ + settingsSocket?.emit('cleanupPadRevisions', padID) + } + + + return
+ + + +
+
+
+ {t("ep_admin_pads:ep_adminpads2_confirm", { + padID: padToDelete, + })} +
+
+ + +
+
+
+
+
+ + + + +
+
Error occured: {errorText}
+
+ +
+
+
+
+
+

+ setSearchTerm(v.target.value)} placeholder={t('ep_admin_pads:ep_adminpads2_search-heading')}/> + + + + + + + + + + + + { + pads?.results?.map((pad)=>{ + return + + + + + + + }) + } + +
{ + setSearchParams({ + ...searchParams, + sortBy: 'padName', + ascending: !searchParams.ascending + }) + }}>{ + setSearchParams({ + ...searchParams, + sortBy: 'userCount', + ascending: !searchParams.ascending + }) + }}>{ + setSearchParams({ + ...searchParams, + sortBy: 'lastEdited', + ascending: !searchParams.ascending + }) + }}>{ + setSearchParams({ + ...searchParams, + sortBy: 'revisionNumber', + ascending: !searchParams.ascending + }) + }}>Revision number
{pad.padName}{pad.userCount}{new Date(pad.lastEdited).toLocaleString()}{pad.revisionNumber} +
+ } title={} onClick={()=>{ + setPadToDelete(pad.padName) + setDeleteDialog(true) + }}/> + } title={} onClick={()=>{ + cleanupPad(pad.padName) + }}/> + } title="view" onClick={()=>window.open(`/p/${pad.padName}`, '_blank')}/> +
+
+
+ + {currentPage+1} out of {pages} + +
+
+} diff --git a/admin/src/pages/Plugin.ts b/admin/src/pages/Plugin.ts new file mode 100644 index 000000000..f5563863b --- /dev/null +++ b/admin/src/pages/Plugin.ts @@ -0,0 +1,36 @@ +export type PluginDef = { + name: string, + description: string, + version: string, + time: string, + official: boolean, +} + + +export type InstalledPlugin = { + name: string, + path: string, + realPath: string, + version:string, + updatable?: boolean +} + + +export type SearchParams = { + searchTerm: string, + offset: number, + limit: number, + sortBy: 'name'|'version'|'last-updated', + sortDir: 'asc'|'desc' +} + + +export type HelpObj = { + epVersion: string + gitCommit: string + installedClientHooks: Record>, + installedParts: string[], + installedPlugins: string[], + installedServerHooks: Record, + latestVersion: string +} diff --git a/admin/src/pages/SettingsPage.tsx b/admin/src/pages/SettingsPage.tsx new file mode 100644 index 000000000..f781f67e1 --- /dev/null +++ b/admin/src/pages/SettingsPage.tsx @@ -0,0 +1,50 @@ +import {useStore} from "../store/store.ts"; +import {isJSONClean, cleanComments} from "../utils/utils.ts"; +import {Trans} from "react-i18next"; +import {IconButton} from "../components/IconButton.tsx"; +import {RotateCw, Save} from "lucide-react"; + +export const SettingsPage = ()=>{ + const settingsSocket = useStore(state=>state.settingsSocket) + const settings = cleanComments(useStore(state=>state.settings)) + + return
+

+ "; - support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; -} )(); -var documentElement = document.documentElement; - - - -var - rkeyEvent = /^key/, - rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/, - rtypenamespace = /^([^.]*)(?:\.(.+)|)/; - -function returnTrue() { - return true; -} - -function returnFalse() { - return false; -} - -// Support: IE <=9 only -// See #13393 for more info -function safeActiveElement() { - try { - return document.activeElement; - } catch ( err ) { } -} - -function on( elem, types, selector, data, fn, one ) { - var origFn, type; - - // Types can be a map of types/handlers - if ( typeof types === "object" ) { - - // ( types-Object, selector, data ) - if ( typeof selector !== "string" ) { - - // ( types-Object, data ) - data = data || selector; - selector = undefined; - } - for ( type in types ) { - on( elem, type, selector, data, types[ type ], one ); - } - return elem; - } - - if ( data == null && fn == null ) { - - // ( types, fn ) - fn = selector; - data = selector = undefined; - } else if ( fn == null ) { - if ( typeof selector === "string" ) { - - // ( types, selector, fn ) - fn = data; - data = undefined; - } else { - - // ( types, data, fn ) - fn = data; - data = selector; - selector = undefined; - } - } - if ( fn === false ) { - fn = returnFalse; - } else if ( !fn ) { - return elem; - } - - if ( one === 1 ) { - origFn = fn; - fn = function( event ) { - - // Can use an empty set, since event contains the info - jQuery().off( event ); - return origFn.apply( this, arguments ); - }; - - // Use same guid so caller can remove using origFn - fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); - } - return elem.each( function() { - jQuery.event.add( this, types, fn, data, selector ); - } ); -} - -/* - * Helper functions for managing events -- not part of the public interface. - * Props to Dean Edwards' addEvent library for many of the ideas. - */ -jQuery.event = { - - global: {}, - - add: function( elem, types, handler, data, selector ) { - - var handleObjIn, eventHandle, tmp, - events, t, handleObj, - special, handlers, type, namespaces, origType, - elemData = dataPriv.get( elem ); - - // Don't attach events to noData or text/comment nodes (but allow plain objects) - if ( !elemData ) { - return; - } - - // Caller can pass in an object of custom data in lieu of the handler - if ( handler.handler ) { - handleObjIn = handler; - handler = handleObjIn.handler; - selector = handleObjIn.selector; - } - - // Ensure that invalid selectors throw exceptions at attach time - // Evaluate against documentElement in case elem is a non-element node (e.g., document) - if ( selector ) { - jQuery.find.matchesSelector( documentElement, selector ); - } - - // Make sure that the handler has a unique ID, used to find/remove it later - if ( !handler.guid ) { - handler.guid = jQuery.guid++; - } - - // Init the element's event structure and main handler, if this is the first - if ( !( events = elemData.events ) ) { - events = elemData.events = {}; - } - if ( !( eventHandle = elemData.handle ) ) { - eventHandle = elemData.handle = function( e ) { - - // Discard the second event of a jQuery.event.trigger() and - // when an event is called after a page has unloaded - return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? - jQuery.event.dispatch.apply( elem, arguments ) : undefined; - }; - } - - // Handle multiple events separated by a space - types = ( types || "" ).match( rnotwhite ) || [ "" ]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[ t ] ) || []; - type = origType = tmp[ 1 ]; - namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); - - // There *must* be a type, no attaching namespace-only handlers - if ( !type ) { - continue; - } - - // If event changes its type, use the special event handlers for the changed type - special = jQuery.event.special[ type ] || {}; - - // If selector defined, determine special event api type, otherwise given type - type = ( selector ? special.delegateType : special.bindType ) || type; - - // Update special based on newly reset type - special = jQuery.event.special[ type ] || {}; - - // handleObj is passed to all event handlers - handleObj = jQuery.extend( { - type: type, - origType: origType, - data: data, - handler: handler, - guid: handler.guid, - selector: selector, - needsContext: selector && jQuery.expr.match.needsContext.test( selector ), - namespace: namespaces.join( "." ) - }, handleObjIn ); - - // Init the event handler queue if we're the first - if ( !( handlers = events[ type ] ) ) { - handlers = events[ type ] = []; - handlers.delegateCount = 0; - - // Only use addEventListener if the special events handler returns false - if ( !special.setup || - special.setup.call( elem, data, namespaces, eventHandle ) === false ) { - - if ( elem.addEventListener ) { - elem.addEventListener( type, eventHandle ); - } - } - } - - if ( special.add ) { - special.add.call( elem, handleObj ); - - if ( !handleObj.handler.guid ) { - handleObj.handler.guid = handler.guid; - } - } - - // Add to the element's handler list, delegates in front - if ( selector ) { - handlers.splice( handlers.delegateCount++, 0, handleObj ); - } else { - handlers.push( handleObj ); - } - - // Keep track of which events have ever been used, for event optimization - jQuery.event.global[ type ] = true; - } - - }, - - // Detach an event or set of events from an element - remove: function( elem, types, handler, selector, mappedTypes ) { - - var j, origCount, tmp, - events, t, handleObj, - special, handlers, type, namespaces, origType, - elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); - - if ( !elemData || !( events = elemData.events ) ) { - return; - } - - // Once for each type.namespace in types; type may be omitted - types = ( types || "" ).match( rnotwhite ) || [ "" ]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[ t ] ) || []; - type = origType = tmp[ 1 ]; - namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); - - // Unbind all events (on this namespace, if provided) for the element - if ( !type ) { - for ( type in events ) { - jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); - } - continue; - } - - special = jQuery.event.special[ type ] || {}; - type = ( selector ? special.delegateType : special.bindType ) || type; - handlers = events[ type ] || []; - tmp = tmp[ 2 ] && - new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); - - // Remove matching events - origCount = j = handlers.length; - while ( j-- ) { - handleObj = handlers[ j ]; - - if ( ( mappedTypes || origType === handleObj.origType ) && - ( !handler || handler.guid === handleObj.guid ) && - ( !tmp || tmp.test( handleObj.namespace ) ) && - ( !selector || selector === handleObj.selector || - selector === "**" && handleObj.selector ) ) { - handlers.splice( j, 1 ); - - if ( handleObj.selector ) { - handlers.delegateCount--; - } - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } - } - } - - // Remove generic event handler if we removed something and no more handlers exist - // (avoids potential for endless recursion during removal of special event handlers) - if ( origCount && !handlers.length ) { - if ( !special.teardown || - special.teardown.call( elem, namespaces, elemData.handle ) === false ) { - - jQuery.removeEvent( elem, type, elemData.handle ); - } - - delete events[ type ]; - } - } - - // Remove data and the expando if it's no longer used - if ( jQuery.isEmptyObject( events ) ) { - dataPriv.remove( elem, "handle events" ); - } - }, - - dispatch: function( nativeEvent ) { - - // Make a writable jQuery.Event from the native event object - var event = jQuery.event.fix( nativeEvent ); - - var i, j, ret, matched, handleObj, handlerQueue, - args = new Array( arguments.length ), - handlers = ( dataPriv.get( this, "events" ) || {} )[ event.type ] || [], - special = jQuery.event.special[ event.type ] || {}; - - // Use the fix-ed jQuery.Event rather than the (read-only) native event - args[ 0 ] = event; - - for ( i = 1; i < arguments.length; i++ ) { - args[ i ] = arguments[ i ]; - } - - event.delegateTarget = this; - - // Call the preDispatch hook for the mapped type, and let it bail if desired - if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { - return; - } - - // Determine handlers - handlerQueue = jQuery.event.handlers.call( this, event, handlers ); - - // Run delegates first; they may want to stop propagation beneath us - i = 0; - while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { - event.currentTarget = matched.elem; - - j = 0; - while ( ( handleObj = matched.handlers[ j++ ] ) && - !event.isImmediatePropagationStopped() ) { - - // Triggered event must either 1) have no namespace, or 2) have namespace(s) - // a subset or equal to those in the bound event (both can have no namespace). - if ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) { - - event.handleObj = handleObj; - event.data = handleObj.data; - - ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || - handleObj.handler ).apply( matched.elem, args ); - - if ( ret !== undefined ) { - if ( ( event.result = ret ) === false ) { - event.preventDefault(); - event.stopPropagation(); - } - } - } - } - } - - // Call the postDispatch hook for the mapped type - if ( special.postDispatch ) { - special.postDispatch.call( this, event ); - } - - return event.result; - }, - - handlers: function( event, handlers ) { - var i, matches, sel, handleObj, - handlerQueue = [], - delegateCount = handlers.delegateCount, - cur = event.target; - - // Support: IE <=9 - // Find delegate handlers - // Black-hole SVG instance trees (#13180) - // - // Support: Firefox <=42 - // Avoid non-left-click in FF but don't block IE radio events (#3861, gh-2343) - if ( delegateCount && cur.nodeType && - ( event.type !== "click" || isNaN( event.button ) || event.button < 1 ) ) { - - for ( ; cur !== this; cur = cur.parentNode || this ) { - - // Don't check non-elements (#13208) - // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) - if ( cur.nodeType === 1 && ( cur.disabled !== true || event.type !== "click" ) ) { - matches = []; - for ( i = 0; i < delegateCount; i++ ) { - handleObj = handlers[ i ]; - - // Don't conflict with Object.prototype properties (#13203) - sel = handleObj.selector + " "; - - if ( matches[ sel ] === undefined ) { - matches[ sel ] = handleObj.needsContext ? - jQuery( sel, this ).index( cur ) > -1 : - jQuery.find( sel, this, null, [ cur ] ).length; - } - if ( matches[ sel ] ) { - matches.push( handleObj ); - } - } - if ( matches.length ) { - handlerQueue.push( { elem: cur, handlers: matches } ); - } - } - } - } - - // Add the remaining (directly-bound) handlers - if ( delegateCount < handlers.length ) { - handlerQueue.push( { elem: this, handlers: handlers.slice( delegateCount ) } ); - } - - return handlerQueue; - }, - - addProp: function( name, hook ) { - Object.defineProperty( jQuery.Event.prototype, name, { - enumerable: true, - configurable: true, - - get: jQuery.isFunction( hook ) ? - function() { - if ( this.originalEvent ) { - return hook( this.originalEvent ); - } - } : - function() { - if ( this.originalEvent ) { - return this.originalEvent[ name ]; - } - }, - - set: function( value ) { - Object.defineProperty( this, name, { - enumerable: true, - configurable: true, - writable: true, - value: value - } ); - } - } ); - }, - - fix: function( originalEvent ) { - return originalEvent[ jQuery.expando ] ? - originalEvent : - new jQuery.Event( originalEvent ); - }, - - special: { - load: { - - // Prevent triggered image.load events from bubbling to window.load - noBubble: true - }, - focus: { - - // Fire native event if possible so blur/focus sequence is correct - trigger: function() { - if ( this !== safeActiveElement() && this.focus ) { - this.focus(); - return false; - } - }, - delegateType: "focusin" - }, - blur: { - trigger: function() { - if ( this === safeActiveElement() && this.blur ) { - this.blur(); - return false; - } - }, - delegateType: "focusout" - }, - click: { - - // For checkbox, fire native event so checked state will be right - trigger: function() { - if ( this.type === "checkbox" && this.click && jQuery.nodeName( this, "input" ) ) { - this.click(); - return false; - } - }, - - // For cross-browser consistency, don't fire native .click() on links - _default: function( event ) { - return jQuery.nodeName( event.target, "a" ); - } - }, - - beforeunload: { - postDispatch: function( event ) { - - // Support: Firefox 20+ - // Firefox doesn't alert if the returnValue field is not set. - if ( event.result !== undefined && event.originalEvent ) { - event.originalEvent.returnValue = event.result; - } - } - } - } -}; - -jQuery.removeEvent = function( elem, type, handle ) { - - // This "if" is needed for plain objects - if ( elem.removeEventListener ) { - elem.removeEventListener( type, handle ); - } -}; - -jQuery.Event = function( src, props ) { - - // Allow instantiation without the 'new' keyword - if ( !( this instanceof jQuery.Event ) ) { - return new jQuery.Event( src, props ); - } - - // Event object - if ( src && src.type ) { - this.originalEvent = src; - this.type = src.type; - - // Events bubbling up the document may have been marked as prevented - // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = src.defaultPrevented || - src.defaultPrevented === undefined && - - // Support: Android <=2.3 only - src.returnValue === false ? - returnTrue : - returnFalse; - - // Create target properties - // Support: Safari <=6 - 7 only - // Target should not be a text node (#504, #13143) - this.target = ( src.target && src.target.nodeType === 3 ) ? - src.target.parentNode : - src.target; - - this.currentTarget = src.currentTarget; - this.relatedTarget = src.relatedTarget; - - // Event type - } else { - this.type = src; - } - - // Put explicitly provided properties onto the event object - if ( props ) { - jQuery.extend( this, props ); - } - - // Create a timestamp if incoming event doesn't have one - this.timeStamp = src && src.timeStamp || jQuery.now(); - - // Mark it as fixed - this[ jQuery.expando ] = true; -}; - -// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding -// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html -jQuery.Event.prototype = { - constructor: jQuery.Event, - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse, - isSimulated: false, - - preventDefault: function() { - var e = this.originalEvent; - - this.isDefaultPrevented = returnTrue; - - if ( e && !this.isSimulated ) { - e.preventDefault(); - } - }, - stopPropagation: function() { - var e = this.originalEvent; - - this.isPropagationStopped = returnTrue; - - if ( e && !this.isSimulated ) { - e.stopPropagation(); - } - }, - stopImmediatePropagation: function() { - var e = this.originalEvent; - - this.isImmediatePropagationStopped = returnTrue; - - if ( e && !this.isSimulated ) { - e.stopImmediatePropagation(); - } - - this.stopPropagation(); - } -}; - -// Includes all common event props including KeyEvent and MouseEvent specific props -jQuery.each( { - altKey: true, - bubbles: true, - cancelable: true, - changedTouches: true, - ctrlKey: true, - detail: true, - eventPhase: true, - metaKey: true, - pageX: true, - pageY: true, - shiftKey: true, - view: true, - "char": true, - charCode: true, - key: true, - keyCode: true, - button: true, - buttons: true, - clientX: true, - clientY: true, - offsetX: true, - offsetY: true, - pointerId: true, - pointerType: true, - screenX: true, - screenY: true, - targetTouches: true, - toElement: true, - touches: true, - - which: function( event ) { - var button = event.button; - - // Add which for key events - if ( event.which == null && rkeyEvent.test( event.type ) ) { - return event.charCode != null ? event.charCode : event.keyCode; - } - - // Add which for click: 1 === left; 2 === middle; 3 === right - if ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) { - return ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); - } - - return event.which; - } -}, jQuery.event.addProp ); - -// Create mouseenter/leave events using mouseover/out and event-time checks -// so that event delegation works in jQuery. -// Do the same for pointerenter/pointerleave and pointerover/pointerout -// -// Support: Safari 7 only -// Safari sends mouseenter too often; see: -// https://bugs.chromium.org/p/chromium/issues/detail?id=470258 -// for the description of the bug (it existed in older Chrome versions as well). -jQuery.each( { - mouseenter: "mouseover", - mouseleave: "mouseout", - pointerenter: "pointerover", - pointerleave: "pointerout" -}, function( orig, fix ) { - jQuery.event.special[ orig ] = { - delegateType: fix, - bindType: fix, - - handle: function( event ) { - var ret, - target = this, - related = event.relatedTarget, - handleObj = event.handleObj; - - // For mouseenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { - event.type = handleObj.origType; - ret = handleObj.handler.apply( this, arguments ); - event.type = fix; - } - return ret; - } - }; -} ); - -jQuery.fn.extend( { - - on: function( types, selector, data, fn ) { - return on( this, types, selector, data, fn ); - }, - one: function( types, selector, data, fn ) { - return on( this, types, selector, data, fn, 1 ); - }, - off: function( types, selector, fn ) { - var handleObj, type; - if ( types && types.preventDefault && types.handleObj ) { - - // ( event ) dispatched jQuery.Event - handleObj = types.handleObj; - jQuery( types.delegateTarget ).off( - handleObj.namespace ? - handleObj.origType + "." + handleObj.namespace : - handleObj.origType, - handleObj.selector, - handleObj.handler - ); - return this; - } - if ( typeof types === "object" ) { - - // ( types-object [, selector] ) - for ( type in types ) { - this.off( type, selector, types[ type ] ); - } - return this; - } - if ( selector === false || typeof selector === "function" ) { - - // ( types [, fn] ) - fn = selector; - selector = undefined; - } - if ( fn === false ) { - fn = returnFalse; - } - return this.each( function() { - jQuery.event.remove( this, types, fn, selector ); - } ); - } -} ); - - -var - rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi, - - // Support: IE <=10 - 11, Edge 12 - 13 - // In IE/Edge using regex groups here causes severe slowdowns. - // See https://connect.microsoft.com/IE/feedback/details/1736512/ - rnoInnerhtml = /\s*$/g; - -function manipulationTarget( elem, content ) { - if ( jQuery.nodeName( elem, "table" ) && - jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) { - - return elem.getElementsByTagName( "tbody" )[ 0 ] || elem; - } - - return elem; -} - -// Replace/restore the type attribute of script elements for safe DOM manipulation -function disableScript( elem ) { - elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; - return elem; -} -function restoreScript( elem ) { - var match = rscriptTypeMasked.exec( elem.type ); - - if ( match ) { - elem.type = match[ 1 ]; - } else { - elem.removeAttribute( "type" ); - } - - return elem; -} - -function cloneCopyEvent( src, dest ) { - var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events; - - if ( dest.nodeType !== 1 ) { - return; - } - - // 1. Copy private data: events, handlers, etc. - if ( dataPriv.hasData( src ) ) { - pdataOld = dataPriv.access( src ); - pdataCur = dataPriv.set( dest, pdataOld ); - events = pdataOld.events; - - if ( events ) { - delete pdataCur.handle; - pdataCur.events = {}; - - for ( type in events ) { - for ( i = 0, l = events[ type ].length; i < l; i++ ) { - jQuery.event.add( dest, type, events[ type ][ i ] ); - } - } - } - } - - // 2. Copy user data - if ( dataUser.hasData( src ) ) { - udataOld = dataUser.access( src ); - udataCur = jQuery.extend( {}, udataOld ); - - dataUser.set( dest, udataCur ); - } -} - -// Fix IE bugs, see support tests -function fixInput( src, dest ) { - var nodeName = dest.nodeName.toLowerCase(); - - // Fails to persist the checked state of a cloned checkbox or radio button. - if ( nodeName === "input" && rcheckableType.test( src.type ) ) { - dest.checked = src.checked; - - // Fails to return the selected option to the default selected state when cloning options - } else if ( nodeName === "input" || nodeName === "textarea" ) { - dest.defaultValue = src.defaultValue; - } -} - -function domManip( collection, args, callback, ignored ) { - - // Flatten any nested arrays - args = concat.apply( [], args ); - - var fragment, first, scripts, hasScripts, node, doc, - i = 0, - l = collection.length, - iNoClone = l - 1, - value = args[ 0 ], - isFunction = jQuery.isFunction( value ); - - // We can't cloneNode fragments that contain checked, in WebKit - if ( isFunction || - ( l > 1 && typeof value === "string" && - !support.checkClone && rchecked.test( value ) ) ) { - return collection.each( function( index ) { - var self = collection.eq( index ); - if ( isFunction ) { - args[ 0 ] = value.call( this, index, self.html() ); - } - domManip( self, args, callback, ignored ); - } ); - } - - if ( l ) { - fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); - first = fragment.firstChild; - - if ( fragment.childNodes.length === 1 ) { - fragment = first; - } - - // Require either new content or an interest in ignored elements to invoke the callback - if ( first || ignored ) { - scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); - hasScripts = scripts.length; - - // Use the original fragment for the last item - // instead of the first because it can end up - // being emptied incorrectly in certain situations (#8070). - for ( ; i < l; i++ ) { - node = fragment; - - if ( i !== iNoClone ) { - node = jQuery.clone( node, true, true ); - - // Keep references to cloned scripts for later restoration - if ( hasScripts ) { - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - jQuery.merge( scripts, getAll( node, "script" ) ); - } - } - - callback.call( collection[ i ], node, i ); - } - - if ( hasScripts ) { - doc = scripts[ scripts.length - 1 ].ownerDocument; - - // Reenable scripts - jQuery.map( scripts, restoreScript ); - - // Evaluate executable scripts on first document insertion - for ( i = 0; i < hasScripts; i++ ) { - node = scripts[ i ]; - if ( rscriptType.test( node.type || "" ) && - !dataPriv.access( node, "globalEval" ) && - jQuery.contains( doc, node ) ) { - - if ( node.src ) { - - // Optional AJAX dependency, but won't run scripts if not present - if ( jQuery._evalUrl ) { - jQuery._evalUrl( node.src ); - } - } else { - DOMEval( node.textContent.replace( rcleanScript, "" ), doc ); - } - } - } - } - } - } - - return collection; -} - -function remove( elem, selector, keepData ) { - var node, - nodes = selector ? jQuery.filter( selector, elem ) : elem, - i = 0; - - for ( ; ( node = nodes[ i ] ) != null; i++ ) { - if ( !keepData && node.nodeType === 1 ) { - jQuery.cleanData( getAll( node ) ); - } - - if ( node.parentNode ) { - if ( keepData && jQuery.contains( node.ownerDocument, node ) ) { - setGlobalEval( getAll( node, "script" ) ); - } - node.parentNode.removeChild( node ); - } - } - - return elem; -} - -jQuery.extend( { - htmlPrefilter: function( html ) { - return html.replace( rxhtmlTag, "<$1>" ); - }, - - clone: function( elem, dataAndEvents, deepDataAndEvents ) { - var i, l, srcElements, destElements, - clone = elem.cloneNode( true ), - inPage = jQuery.contains( elem.ownerDocument, elem ); - - // Fix IE cloning issues - if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && - !jQuery.isXMLDoc( elem ) ) { - - // We eschew Sizzle here for performance reasons: https://jsperf.com/getall-vs-sizzle/2 - destElements = getAll( clone ); - srcElements = getAll( elem ); - - for ( i = 0, l = srcElements.length; i < l; i++ ) { - fixInput( srcElements[ i ], destElements[ i ] ); - } - } - - // Copy the events from the original to the clone - if ( dataAndEvents ) { - if ( deepDataAndEvents ) { - srcElements = srcElements || getAll( elem ); - destElements = destElements || getAll( clone ); - - for ( i = 0, l = srcElements.length; i < l; i++ ) { - cloneCopyEvent( srcElements[ i ], destElements[ i ] ); - } - } else { - cloneCopyEvent( elem, clone ); - } - } - - // Preserve script evaluation history - destElements = getAll( clone, "script" ); - if ( destElements.length > 0 ) { - setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); - } - - // Return the cloned set - return clone; - }, - - cleanData: function( elems ) { - var data, elem, type, - special = jQuery.event.special, - i = 0; - - for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { - if ( acceptData( elem ) ) { - if ( ( data = elem[ dataPriv.expando ] ) ) { - if ( data.events ) { - for ( type in data.events ) { - if ( special[ type ] ) { - jQuery.event.remove( elem, type ); - - // This is a shortcut to avoid jQuery.event.remove's overhead - } else { - jQuery.removeEvent( elem, type, data.handle ); - } - } - } - - // Support: Chrome <=35 - 45+ - // Assign undefined instead of using delete, see Data#remove - elem[ dataPriv.expando ] = undefined; - } - if ( elem[ dataUser.expando ] ) { - - // Support: Chrome <=35 - 45+ - // Assign undefined instead of using delete, see Data#remove - elem[ dataUser.expando ] = undefined; - } - } - } - } -} ); - -jQuery.fn.extend( { - detach: function( selector ) { - return remove( this, selector, true ); - }, - - remove: function( selector ) { - return remove( this, selector ); - }, - - text: function( value ) { - return access( this, function( value ) { - return value === undefined ? - jQuery.text( this ) : - this.empty().each( function() { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - this.textContent = value; - } - } ); - }, null, value, arguments.length ); - }, - - append: function() { - return domManip( this, arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.appendChild( elem ); - } - } ); - }, - - prepend: function() { - return domManip( this, arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.insertBefore( elem, target.firstChild ); - } - } ); - }, - - before: function() { - return domManip( this, arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this ); - } - } ); - }, - - after: function() { - return domManip( this, arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this.nextSibling ); - } - } ); - }, - - empty: function() { - var elem, - i = 0; - - for ( ; ( elem = this[ i ] ) != null; i++ ) { - if ( elem.nodeType === 1 ) { - - // Prevent memory leaks - jQuery.cleanData( getAll( elem, false ) ); - - // Remove any remaining nodes - elem.textContent = ""; - } - } - - return this; - }, - - clone: function( dataAndEvents, deepDataAndEvents ) { - dataAndEvents = dataAndEvents == null ? false : dataAndEvents; - deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; - - return this.map( function() { - return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); - } ); - }, - - html: function( value ) { - return access( this, function( value ) { - var elem = this[ 0 ] || {}, - i = 0, - l = this.length; - - if ( value === undefined && elem.nodeType === 1 ) { - return elem.innerHTML; - } - - // See if we can take a shortcut and just use innerHTML - if ( typeof value === "string" && !rnoInnerhtml.test( value ) && - !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { - - value = jQuery.htmlPrefilter( value ); - - try { - for ( ; i < l; i++ ) { - elem = this[ i ] || {}; - - // Remove element nodes and prevent memory leaks - if ( elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem, false ) ); - elem.innerHTML = value; - } - } - - elem = 0; - - // If using innerHTML throws an exception, use the fallback method - } catch ( e ) {} - } - - if ( elem ) { - this.empty().append( value ); - } - }, null, value, arguments.length ); - }, - - replaceWith: function() { - var ignored = []; - - // Make the changes, replacing each non-ignored context element with the new content - return domManip( this, arguments, function( elem ) { - var parent = this.parentNode; - - if ( jQuery.inArray( this, ignored ) < 0 ) { - jQuery.cleanData( getAll( this ) ); - if ( parent ) { - parent.replaceChild( elem, this ); - } - } - - // Force callback invocation - }, ignored ); - } -} ); - -jQuery.each( { - appendTo: "append", - prependTo: "prepend", - insertBefore: "before", - insertAfter: "after", - replaceAll: "replaceWith" -}, function( name, original ) { - jQuery.fn[ name ] = function( selector ) { - var elems, - ret = [], - insert = jQuery( selector ), - last = insert.length - 1, - i = 0; - - for ( ; i <= last; i++ ) { - elems = i === last ? this : this.clone( true ); - jQuery( insert[ i ] )[ original ]( elems ); - - // Support: Android <=4.0 only, PhantomJS 1 only - // .get() because push.apply(_, arraylike) throws on ancient WebKit - push.apply( ret, elems.get() ); - } - - return this.pushStack( ret ); - }; -} ); -var rmargin = ( /^margin/ ); - -var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); - -var getStyles = function( elem ) { - - // Support: IE <=11 only, Firefox <=30 (#15098, #14150) - // IE throws on elements created in popups - // FF meanwhile throws on frame elements through "defaultView.getComputedStyle" - var view = elem.ownerDocument.defaultView; - - if ( !view || !view.opener ) { - view = window; - } - - return view.getComputedStyle( elem ); - }; - - - -( function() { - - // Executing both pixelPosition & boxSizingReliable tests require only one layout - // so they're executed at the same time to save the second computation. - function computeStyleTests() { - - // This is a singleton, we need to execute it only once - if ( !div ) { - return; - } - - div.style.cssText = - "box-sizing:border-box;" + - "position:relative;display:block;" + - "margin:auto;border:1px;padding:1px;" + - "top:1%;width:50%"; - div.innerHTML = ""; - documentElement.appendChild( container ); - - var divStyle = window.getComputedStyle( div ); - pixelPositionVal = divStyle.top !== "1%"; - - // Support: Android 4.0 - 4.3 only, Firefox <=3 - 44 - reliableMarginLeftVal = divStyle.marginLeft === "2px"; - boxSizingReliableVal = divStyle.width === "4px"; - - // Support: Android 4.0 - 4.3 only - // Some styles come back with percentage values, even though they shouldn't - div.style.marginRight = "50%"; - pixelMarginRightVal = divStyle.marginRight === "4px"; - - documentElement.removeChild( container ); - - // Nullify the div so it wouldn't be stored in the memory and - // it will also be a sign that checks already performed - div = null; - } - - var pixelPositionVal, boxSizingReliableVal, pixelMarginRightVal, reliableMarginLeftVal, - container = document.createElement( "div" ), - div = document.createElement( "div" ); - - // Finish early in limited (non-browser) environments - if ( !div.style ) { - return; - } - - // Support: IE <=9 - 11 only - // Style of cloned element affects source element cloned (#8908) - div.style.backgroundClip = "content-box"; - div.cloneNode( true ).style.backgroundClip = ""; - support.clearCloneStyle = div.style.backgroundClip === "content-box"; - - container.style.cssText = "border:0;width:8px;height:0;top:0;left:-9999px;" + - "padding:0;margin-top:1px;position:absolute"; - container.appendChild( div ); - - jQuery.extend( support, { - pixelPosition: function() { - computeStyleTests(); - return pixelPositionVal; - }, - boxSizingReliable: function() { - computeStyleTests(); - return boxSizingReliableVal; - }, - pixelMarginRight: function() { - computeStyleTests(); - return pixelMarginRightVal; - }, - reliableMarginLeft: function() { - computeStyleTests(); - return reliableMarginLeftVal; - } - } ); -} )(); - - -function curCSS( elem, name, computed ) { - var width, minWidth, maxWidth, ret, - style = elem.style; - - computed = computed || getStyles( elem ); - - // Support: IE <=9 only - // getPropertyValue is only needed for .css('filter') (#12537) - if ( computed ) { - ret = computed.getPropertyValue( name ) || computed[ name ]; - - if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { - ret = jQuery.style( elem, name ); - } - - // A tribute to the "awesome hack by Dean Edwards" - // Android Browser returns percentage for some values, - // but width seems to be reliably pixels. - // This is against the CSSOM draft spec: - // https://drafts.csswg.org/cssom/#resolved-values - if ( !support.pixelMarginRight() && rnumnonpx.test( ret ) && rmargin.test( name ) ) { - - // Remember the original values - width = style.width; - minWidth = style.minWidth; - maxWidth = style.maxWidth; - - // Put in the new values to get a computed value out - style.minWidth = style.maxWidth = style.width = ret; - ret = computed.width; - - // Revert the changed values - style.width = width; - style.minWidth = minWidth; - style.maxWidth = maxWidth; - } - } - - return ret !== undefined ? - - // Support: IE <=9 - 11 only - // IE returns zIndex value as an integer. - ret + "" : - ret; -} - - -function addGetHookIf( conditionFn, hookFn ) { - - // Define the hook, we'll check on the first run if it's really needed. - return { - get: function() { - if ( conditionFn() ) { - - // Hook not needed (or it's not possible to use it due - // to missing dependency), remove it. - delete this.get; - return; - } - - // Hook needed; redefine it so that the support test is not executed again. - return ( this.get = hookFn ).apply( this, arguments ); - } - }; -} - - -var - - // Swappable if display is none or starts with table - // except "table", "table-cell", or "table-caption" - // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display - rdisplayswap = /^(none|table(?!-c[ea]).+)/, - cssShow = { position: "absolute", visibility: "hidden", display: "block" }, - cssNormalTransform = { - letterSpacing: "0", - fontWeight: "400" - }, - - cssPrefixes = [ "Webkit", "Moz", "ms" ], - emptyStyle = document.createElement( "div" ).style; - -// Return a css property mapped to a potentially vendor prefixed property -function vendorPropName( name ) { - - // Shortcut for names that are not vendor prefixed - if ( name in emptyStyle ) { - return name; - } - - // Check for vendor prefixed names - var capName = name[ 0 ].toUpperCase() + name.slice( 1 ), - i = cssPrefixes.length; - - while ( i-- ) { - name = cssPrefixes[ i ] + capName; - if ( name in emptyStyle ) { - return name; - } - } -} - -function setPositiveNumber( elem, value, subtract ) { - - // Any relative (+/-) values have already been - // normalized at this point - var matches = rcssNum.exec( value ); - return matches ? - - // Guard against undefined "subtract", e.g., when used as in cssHooks - Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) : - value; -} - -function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) { - var i = extra === ( isBorderBox ? "border" : "content" ) ? - - // If we already have the right measurement, avoid augmentation - 4 : - - // Otherwise initialize for horizontal or vertical properties - name === "width" ? 1 : 0, - - val = 0; - - for ( ; i < 4; i += 2 ) { - - // Both box models exclude margin, so add it if we want it - if ( extra === "margin" ) { - val += jQuery.css( elem, extra + cssExpand[ i ], true, styles ); - } - - if ( isBorderBox ) { - - // border-box includes padding, so remove it if we want content - if ( extra === "content" ) { - val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - } - - // At this point, extra isn't border nor margin, so remove border - if ( extra !== "margin" ) { - val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - } else { - - // At this point, extra isn't content, so add padding - val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - - // At this point, extra isn't content nor padding, so add border - if ( extra !== "padding" ) { - val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - } - } - - return val; -} - -function getWidthOrHeight( elem, name, extra ) { - - // Start with offset property, which is equivalent to the border-box value - var val, - valueIsBorderBox = true, - styles = getStyles( elem ), - isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; - - // Support: IE <=11 only - // Running getBoundingClientRect on a disconnected node - // in IE throws an error. - if ( elem.getClientRects().length ) { - val = elem.getBoundingClientRect()[ name ]; - } - - // Some non-html elements return undefined for offsetWidth, so check for null/undefined - // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285 - // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668 - if ( val <= 0 || val == null ) { - - // Fall back to computed then uncomputed css if necessary - val = curCSS( elem, name, styles ); - if ( val < 0 || val == null ) { - val = elem.style[ name ]; - } - - // Computed unit is not pixels. Stop here and return. - if ( rnumnonpx.test( val ) ) { - return val; - } - - // Check for style in case a browser which returns unreliable values - // for getComputedStyle silently falls back to the reliable elem.style - valueIsBorderBox = isBorderBox && - ( support.boxSizingReliable() || val === elem.style[ name ] ); - - // Normalize "", auto, and prepare for extra - val = parseFloat( val ) || 0; - } - - // Use the active box-sizing model to add/subtract irrelevant styles - return ( val + - augmentWidthOrHeight( - elem, - name, - extra || ( isBorderBox ? "border" : "content" ), - valueIsBorderBox, - styles - ) - ) + "px"; -} - -jQuery.extend( { - - // Add in style property hooks for overriding the default - // behavior of getting and setting a style property - cssHooks: { - opacity: { - get: function( elem, computed ) { - if ( computed ) { - - // We should always get a number back from opacity - var ret = curCSS( elem, "opacity" ); - return ret === "" ? "1" : ret; - } - } - } - }, - - // Don't automatically add "px" to these possibly-unitless properties - cssNumber: { - "animationIterationCount": true, - "columnCount": true, - "fillOpacity": true, - "flexGrow": true, - "flexShrink": true, - "fontWeight": true, - "lineHeight": true, - "opacity": true, - "order": true, - "orphans": true, - "widows": true, - "zIndex": true, - "zoom": true - }, - - // Add in properties whose names you wish to fix before - // setting or getting the value - cssProps: { - "float": "cssFloat" - }, - - // Get and set the style property on a DOM Node - style: function( elem, name, value, extra ) { - - // Don't set styles on text and comment nodes - if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { - return; - } - - // Make sure that we're working with the right name - var ret, type, hooks, - origName = jQuery.camelCase( name ), - style = elem.style; - - name = jQuery.cssProps[ origName ] || - ( jQuery.cssProps[ origName ] = vendorPropName( origName ) || origName ); - - // Gets hook for the prefixed version, then unprefixed version - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // Check if we're setting a value - if ( value !== undefined ) { - type = typeof value; - - // Convert "+=" or "-=" to relative numbers (#7345) - if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) { - value = adjustCSS( elem, name, ret ); - - // Fixes bug #9237 - type = "number"; - } - - // Make sure that null and NaN values aren't set (#7116) - if ( value == null || value !== value ) { - return; - } - - // If a number was passed in, add the unit (except for certain CSS properties) - if ( type === "number" ) { - value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" ); - } - - // background-* props affect original clone's values - if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) { - style[ name ] = "inherit"; - } - - // If a hook was provided, use that value, otherwise just set the specified value - if ( !hooks || !( "set" in hooks ) || - ( value = hooks.set( elem, value, extra ) ) !== undefined ) { - - style[ name ] = value; - } - - } else { - - // If a hook was provided get the non-computed value from there - if ( hooks && "get" in hooks && - ( ret = hooks.get( elem, false, extra ) ) !== undefined ) { - - return ret; - } - - // Otherwise just get the value from the style object - return style[ name ]; - } - }, - - css: function( elem, name, extra, styles ) { - var val, num, hooks, - origName = jQuery.camelCase( name ); - - // Make sure that we're working with the right name - name = jQuery.cssProps[ origName ] || - ( jQuery.cssProps[ origName ] = vendorPropName( origName ) || origName ); - - // Try prefixed name followed by the unprefixed name - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // If a hook was provided get the computed value from there - if ( hooks && "get" in hooks ) { - val = hooks.get( elem, true, extra ); - } - - // Otherwise, if a way to get the computed value exists, use that - if ( val === undefined ) { - val = curCSS( elem, name, styles ); - } - - // Convert "normal" to computed value - if ( val === "normal" && name in cssNormalTransform ) { - val = cssNormalTransform[ name ]; - } - - // Make numeric if forced or a qualifier was provided and val looks numeric - if ( extra === "" || extra ) { - num = parseFloat( val ); - return extra === true || isFinite( num ) ? num || 0 : val; - } - return val; - } -} ); - -jQuery.each( [ "height", "width" ], function( i, name ) { - jQuery.cssHooks[ name ] = { - get: function( elem, computed, extra ) { - if ( computed ) { - - // Certain elements can have dimension info if we invisibly show them - // but it must have a current display style that would benefit - return rdisplayswap.test( jQuery.css( elem, "display" ) ) && - - // Support: Safari 8+ - // Table columns in Safari have non-zero offsetWidth & zero - // getBoundingClientRect().width unless display is changed. - // Support: IE <=11 only - // Running getBoundingClientRect on a disconnected node - // in IE throws an error. - ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? - swap( elem, cssShow, function() { - return getWidthOrHeight( elem, name, extra ); - } ) : - getWidthOrHeight( elem, name, extra ); - } - }, - - set: function( elem, value, extra ) { - var matches, - styles = extra && getStyles( elem ), - subtract = extra && augmentWidthOrHeight( - elem, - name, - extra, - jQuery.css( elem, "boxSizing", false, styles ) === "border-box", - styles - ); - - // Convert to pixels if value adjustment is needed - if ( subtract && ( matches = rcssNum.exec( value ) ) && - ( matches[ 3 ] || "px" ) !== "px" ) { - - elem.style[ name ] = value; - value = jQuery.css( elem, name ); - } - - return setPositiveNumber( elem, value, subtract ); - } - }; -} ); - -jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft, - function( elem, computed ) { - if ( computed ) { - return ( parseFloat( curCSS( elem, "marginLeft" ) ) || - elem.getBoundingClientRect().left - - swap( elem, { marginLeft: 0 }, function() { - return elem.getBoundingClientRect().left; - } ) - ) + "px"; - } - } -); - -// These hooks are used by animate to expand properties -jQuery.each( { - margin: "", - padding: "", - border: "Width" -}, function( prefix, suffix ) { - jQuery.cssHooks[ prefix + suffix ] = { - expand: function( value ) { - var i = 0, - expanded = {}, - - // Assumes a single number if not a string - parts = typeof value === "string" ? value.split( " " ) : [ value ]; - - for ( ; i < 4; i++ ) { - expanded[ prefix + cssExpand[ i ] + suffix ] = - parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; - } - - return expanded; - } - }; - - if ( !rmargin.test( prefix ) ) { - jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; - } -} ); - -jQuery.fn.extend( { - css: function( name, value ) { - return access( this, function( elem, name, value ) { - var styles, len, - map = {}, - i = 0; - - if ( jQuery.isArray( name ) ) { - styles = getStyles( elem ); - len = name.length; - - for ( ; i < len; i++ ) { - map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); - } - - return map; - } - - return value !== undefined ? - jQuery.style( elem, name, value ) : - jQuery.css( elem, name ); - }, name, value, arguments.length > 1 ); - } -} ); - - -function Tween( elem, options, prop, end, easing ) { - return new Tween.prototype.init( elem, options, prop, end, easing ); -} -jQuery.Tween = Tween; - -Tween.prototype = { - constructor: Tween, - init: function( elem, options, prop, end, easing, unit ) { - this.elem = elem; - this.prop = prop; - this.easing = easing || jQuery.easing._default; - this.options = options; - this.start = this.now = this.cur(); - this.end = end; - this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); - }, - cur: function() { - var hooks = Tween.propHooks[ this.prop ]; - - return hooks && hooks.get ? - hooks.get( this ) : - Tween.propHooks._default.get( this ); - }, - run: function( percent ) { - var eased, - hooks = Tween.propHooks[ this.prop ]; - - if ( this.options.duration ) { - this.pos = eased = jQuery.easing[ this.easing ]( - percent, this.options.duration * percent, 0, 1, this.options.duration - ); - } else { - this.pos = eased = percent; - } - this.now = ( this.end - this.start ) * eased + this.start; - - if ( this.options.step ) { - this.options.step.call( this.elem, this.now, this ); - } - - if ( hooks && hooks.set ) { - hooks.set( this ); - } else { - Tween.propHooks._default.set( this ); - } - return this; - } -}; - -Tween.prototype.init.prototype = Tween.prototype; - -Tween.propHooks = { - _default: { - get: function( tween ) { - var result; - - // Use a property on the element directly when it is not a DOM element, - // or when there is no matching style property that exists. - if ( tween.elem.nodeType !== 1 || - tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) { - return tween.elem[ tween.prop ]; - } - - // Passing an empty string as a 3rd parameter to .css will automatically - // attempt a parseFloat and fallback to a string if the parse fails. - // Simple values such as "10px" are parsed to Float; - // complex values such as "rotate(1rad)" are returned as-is. - result = jQuery.css( tween.elem, tween.prop, "" ); - - // Empty strings, null, undefined and "auto" are converted to 0. - return !result || result === "auto" ? 0 : result; - }, - set: function( tween ) { - - // Use step hook for back compat. - // Use cssHook if its there. - // Use .style if available and use plain properties where available. - if ( jQuery.fx.step[ tween.prop ] ) { - jQuery.fx.step[ tween.prop ]( tween ); - } else if ( tween.elem.nodeType === 1 && - ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || - jQuery.cssHooks[ tween.prop ] ) ) { - jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); - } else { - tween.elem[ tween.prop ] = tween.now; - } - } - } -}; - -// Support: IE <=9 only -// Panic based approach to setting things on disconnected nodes -Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { - set: function( tween ) { - if ( tween.elem.nodeType && tween.elem.parentNode ) { - tween.elem[ tween.prop ] = tween.now; - } - } -}; - -jQuery.easing = { - linear: function( p ) { - return p; - }, - swing: function( p ) { - return 0.5 - Math.cos( p * Math.PI ) / 2; - }, - _default: "swing" -}; - -jQuery.fx = Tween.prototype.init; - -// Back compat <1.8 extension point -jQuery.fx.step = {}; - - - - -var - fxNow, timerId, - rfxtypes = /^(?:toggle|show|hide)$/, - rrun = /queueHooks$/; - -function raf() { - if ( timerId ) { - window.requestAnimationFrame( raf ); - jQuery.fx.tick(); - } -} - -// Animations created synchronously will run synchronously -function createFxNow() { - window.setTimeout( function() { - fxNow = undefined; - } ); - return ( fxNow = jQuery.now() ); -} - -// Generate parameters to create a standard animation -function genFx( type, includeWidth ) { - var which, - i = 0, - attrs = { height: type }; - - // If we include width, step value is 1 to do all cssExpand values, - // otherwise step value is 2 to skip over Left and Right - includeWidth = includeWidth ? 1 : 0; - for ( ; i < 4 ; i += 2 - includeWidth ) { - which = cssExpand[ i ]; - attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; - } - - if ( includeWidth ) { - attrs.opacity = attrs.width = type; - } - - return attrs; -} - -function createTween( value, prop, animation ) { - var tween, - collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ), - index = 0, - length = collection.length; - for ( ; index < length; index++ ) { - if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) { - - // We're done with this property - return tween; - } - } -} - -function defaultPrefilter( elem, props, opts ) { - /* jshint validthis: true */ - var prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display, - isBox = "width" in props || "height" in props, - anim = this, - orig = {}, - style = elem.style, - hidden = elem.nodeType && isHiddenWithinTree( elem ), - dataShow = dataPriv.get( elem, "fxshow" ); - - // Queue-skipping animations hijack the fx hooks - if ( !opts.queue ) { - hooks = jQuery._queueHooks( elem, "fx" ); - if ( hooks.unqueued == null ) { - hooks.unqueued = 0; - oldfire = hooks.empty.fire; - hooks.empty.fire = function() { - if ( !hooks.unqueued ) { - oldfire(); - } - }; - } - hooks.unqueued++; - - anim.always( function() { - - // Ensure the complete handler is called before this completes - anim.always( function() { - hooks.unqueued--; - if ( !jQuery.queue( elem, "fx" ).length ) { - hooks.empty.fire(); - } - } ); - } ); - } - - // Detect show/hide animations - for ( prop in props ) { - value = props[ prop ]; - if ( rfxtypes.test( value ) ) { - delete props[ prop ]; - toggle = toggle || value === "toggle"; - if ( value === ( hidden ? "hide" : "show" ) ) { - - // Pretend to be hidden if this is a "show" and - // there is still data from a stopped show/hide - if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) { - hidden = true; - - // Ignore all other no-op show/hide data - } else { - continue; - } - } - orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); - } - } - - // Bail out if this is a no-op like .hide().hide() - propTween = !jQuery.isEmptyObject( props ); - if ( !propTween && jQuery.isEmptyObject( orig ) ) { - return; - } - - // Restrict "overflow" and "display" styles during box animations - if ( isBox && elem.nodeType === 1 ) { - - // Support: IE <=9 - 11, Edge 12 - 13 - // Record all 3 overflow attributes because IE does not infer the shorthand - // from identically-valued overflowX and overflowY - opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; - - // Identify a display type, preferring old show/hide data over the CSS cascade - restoreDisplay = dataShow && dataShow.display; - if ( restoreDisplay == null ) { - restoreDisplay = dataPriv.get( elem, "display" ); - } - display = jQuery.css( elem, "display" ); - if ( display === "none" ) { - if ( restoreDisplay ) { - display = restoreDisplay; - } else { - - // Get nonempty value(s) by temporarily forcing visibility - showHide( [ elem ], true ); - restoreDisplay = elem.style.display || restoreDisplay; - display = jQuery.css( elem, "display" ); - showHide( [ elem ] ); - } - } - - // Animate inline elements as inline-block - if ( display === "inline" || display === "inline-block" && restoreDisplay != null ) { - if ( jQuery.css( elem, "float" ) === "none" ) { - - // Restore the original display value at the end of pure show/hide animations - if ( !propTween ) { - anim.done( function() { - style.display = restoreDisplay; - } ); - if ( restoreDisplay == null ) { - display = style.display; - restoreDisplay = display === "none" ? "" : display; - } - } - style.display = "inline-block"; - } - } - } - - if ( opts.overflow ) { - style.overflow = "hidden"; - anim.always( function() { - style.overflow = opts.overflow[ 0 ]; - style.overflowX = opts.overflow[ 1 ]; - style.overflowY = opts.overflow[ 2 ]; - } ); - } - - // Implement show/hide animations - propTween = false; - for ( prop in orig ) { - - // General show/hide setup for this element animation - if ( !propTween ) { - if ( dataShow ) { - if ( "hidden" in dataShow ) { - hidden = dataShow.hidden; - } - } else { - dataShow = dataPriv.access( elem, "fxshow", { display: restoreDisplay } ); - } - - // Store hidden/visible for toggle so `.stop().toggle()` "reverses" - if ( toggle ) { - dataShow.hidden = !hidden; - } - - // Show elements before animating them - if ( hidden ) { - showHide( [ elem ], true ); - } - - /* jshint -W083 */ - anim.done( function() { - - // The final step of a "hide" animation is actually hiding the element - if ( !hidden ) { - showHide( [ elem ] ); - } - dataPriv.remove( elem, "fxshow" ); - for ( prop in orig ) { - jQuery.style( elem, prop, orig[ prop ] ); - } - } ); - } - - // Per-property setup - propTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); - if ( !( prop in dataShow ) ) { - dataShow[ prop ] = propTween.start; - if ( hidden ) { - propTween.end = propTween.start; - propTween.start = 0; - } - } - } -} - -function propFilter( props, specialEasing ) { - var index, name, easing, value, hooks; - - // camelCase, specialEasing and expand cssHook pass - for ( index in props ) { - name = jQuery.camelCase( index ); - easing = specialEasing[ name ]; - value = props[ index ]; - if ( jQuery.isArray( value ) ) { - easing = value[ 1 ]; - value = props[ index ] = value[ 0 ]; - } - - if ( index !== name ) { - props[ name ] = value; - delete props[ index ]; - } - - hooks = jQuery.cssHooks[ name ]; - if ( hooks && "expand" in hooks ) { - value = hooks.expand( value ); - delete props[ name ]; - - // Not quite $.extend, this won't overwrite existing keys. - // Reusing 'index' because we have the correct "name" - for ( index in value ) { - if ( !( index in props ) ) { - props[ index ] = value[ index ]; - specialEasing[ index ] = easing; - } - } - } else { - specialEasing[ name ] = easing; - } - } -} - -function Animation( elem, properties, options ) { - var result, - stopped, - index = 0, - length = Animation.prefilters.length, - deferred = jQuery.Deferred().always( function() { - - // Don't match elem in the :animated selector - delete tick.elem; - } ), - tick = function() { - if ( stopped ) { - return false; - } - var currentTime = fxNow || createFxNow(), - remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), - - // Support: Android 2.3 only - // Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497) - temp = remaining / animation.duration || 0, - percent = 1 - temp, - index = 0, - length = animation.tweens.length; - - for ( ; index < length ; index++ ) { - animation.tweens[ index ].run( percent ); - } - - deferred.notifyWith( elem, [ animation, percent, remaining ] ); - - if ( percent < 1 && length ) { - return remaining; - } else { - deferred.resolveWith( elem, [ animation ] ); - return false; - } - }, - animation = deferred.promise( { - elem: elem, - props: jQuery.extend( {}, properties ), - opts: jQuery.extend( true, { - specialEasing: {}, - easing: jQuery.easing._default - }, options ), - originalProperties: properties, - originalOptions: options, - startTime: fxNow || createFxNow(), - duration: options.duration, - tweens: [], - createTween: function( prop, end ) { - var tween = jQuery.Tween( elem, animation.opts, prop, end, - animation.opts.specialEasing[ prop ] || animation.opts.easing ); - animation.tweens.push( tween ); - return tween; - }, - stop: function( gotoEnd ) { - var index = 0, - - // If we are going to the end, we want to run all the tweens - // otherwise we skip this part - length = gotoEnd ? animation.tweens.length : 0; - if ( stopped ) { - return this; - } - stopped = true; - for ( ; index < length ; index++ ) { - animation.tweens[ index ].run( 1 ); - } - - // Resolve when we played the last frame; otherwise, reject - if ( gotoEnd ) { - deferred.notifyWith( elem, [ animation, 1, 0 ] ); - deferred.resolveWith( elem, [ animation, gotoEnd ] ); - } else { - deferred.rejectWith( elem, [ animation, gotoEnd ] ); - } - return this; - } - } ), - props = animation.props; - - propFilter( props, animation.opts.specialEasing ); - - for ( ; index < length ; index++ ) { - result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts ); - if ( result ) { - if ( jQuery.isFunction( result.stop ) ) { - jQuery._queueHooks( animation.elem, animation.opts.queue ).stop = - jQuery.proxy( result.stop, result ); - } - return result; - } - } - - jQuery.map( props, createTween, animation ); - - if ( jQuery.isFunction( animation.opts.start ) ) { - animation.opts.start.call( elem, animation ); - } - - jQuery.fx.timer( - jQuery.extend( tick, { - elem: elem, - anim: animation, - queue: animation.opts.queue - } ) - ); - - // attach callbacks from options - return animation.progress( animation.opts.progress ) - .done( animation.opts.done, animation.opts.complete ) - .fail( animation.opts.fail ) - .always( animation.opts.always ); -} - -jQuery.Animation = jQuery.extend( Animation, { - - tweeners: { - "*": [ function( prop, value ) { - var tween = this.createTween( prop, value ); - adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween ); - return tween; - } ] - }, - - tweener: function( props, callback ) { - if ( jQuery.isFunction( props ) ) { - callback = props; - props = [ "*" ]; - } else { - props = props.match( rnotwhite ); - } - - var prop, - index = 0, - length = props.length; - - for ( ; index < length ; index++ ) { - prop = props[ index ]; - Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || []; - Animation.tweeners[ prop ].unshift( callback ); - } - }, - - prefilters: [ defaultPrefilter ], - - prefilter: function( callback, prepend ) { - if ( prepend ) { - Animation.prefilters.unshift( callback ); - } else { - Animation.prefilters.push( callback ); - } - } -} ); - -jQuery.speed = function( speed, easing, fn ) { - var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { - complete: fn || !fn && easing || - jQuery.isFunction( speed ) && speed, - duration: speed, - easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing - }; - - // Go to the end state if fx are off or if document is hidden - if ( jQuery.fx.off || document.hidden ) { - opt.duration = 0; - - } else { - opt.duration = typeof opt.duration === "number" ? - opt.duration : opt.duration in jQuery.fx.speeds ? - jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default; - } - - // Normalize opt.queue - true/undefined/null -> "fx" - if ( opt.queue == null || opt.queue === true ) { - opt.queue = "fx"; - } - - // Queueing - opt.old = opt.complete; - - opt.complete = function() { - if ( jQuery.isFunction( opt.old ) ) { - opt.old.call( this ); - } - - if ( opt.queue ) { - jQuery.dequeue( this, opt.queue ); - } - }; - - return opt; -}; - -jQuery.fn.extend( { - fadeTo: function( speed, to, easing, callback ) { - - // Show any hidden elements after setting opacity to 0 - return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show() - - // Animate to the value specified - .end().animate( { opacity: to }, speed, easing, callback ); - }, - animate: function( prop, speed, easing, callback ) { - var empty = jQuery.isEmptyObject( prop ), - optall = jQuery.speed( speed, easing, callback ), - doAnimation = function() { - - // Operate on a copy of prop so per-property easing won't be lost - var anim = Animation( this, jQuery.extend( {}, prop ), optall ); - - // Empty animations, or finishing resolves immediately - if ( empty || dataPriv.get( this, "finish" ) ) { - anim.stop( true ); - } - }; - doAnimation.finish = doAnimation; - - return empty || optall.queue === false ? - this.each( doAnimation ) : - this.queue( optall.queue, doAnimation ); - }, - stop: function( type, clearQueue, gotoEnd ) { - var stopQueue = function( hooks ) { - var stop = hooks.stop; - delete hooks.stop; - stop( gotoEnd ); - }; - - if ( typeof type !== "string" ) { - gotoEnd = clearQueue; - clearQueue = type; - type = undefined; - } - if ( clearQueue && type !== false ) { - this.queue( type || "fx", [] ); - } - - return this.each( function() { - var dequeue = true, - index = type != null && type + "queueHooks", - timers = jQuery.timers, - data = dataPriv.get( this ); - - if ( index ) { - if ( data[ index ] && data[ index ].stop ) { - stopQueue( data[ index ] ); - } - } else { - for ( index in data ) { - if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { - stopQueue( data[ index ] ); - } - } - } - - for ( index = timers.length; index--; ) { - if ( timers[ index ].elem === this && - ( type == null || timers[ index ].queue === type ) ) { - - timers[ index ].anim.stop( gotoEnd ); - dequeue = false; - timers.splice( index, 1 ); - } - } - - // Start the next in the queue if the last step wasn't forced. - // Timers currently will call their complete callbacks, which - // will dequeue but only if they were gotoEnd. - if ( dequeue || !gotoEnd ) { - jQuery.dequeue( this, type ); - } - } ); - }, - finish: function( type ) { - if ( type !== false ) { - type = type || "fx"; - } - return this.each( function() { - var index, - data = dataPriv.get( this ), - queue = data[ type + "queue" ], - hooks = data[ type + "queueHooks" ], - timers = jQuery.timers, - length = queue ? queue.length : 0; - - // Enable finishing flag on private data - data.finish = true; - - // Empty the queue first - jQuery.queue( this, type, [] ); - - if ( hooks && hooks.stop ) { - hooks.stop.call( this, true ); - } - - // Look for any active animations, and finish them - for ( index = timers.length; index--; ) { - if ( timers[ index ].elem === this && timers[ index ].queue === type ) { - timers[ index ].anim.stop( true ); - timers.splice( index, 1 ); - } - } - - // Look for any animations in the old queue and finish them - for ( index = 0; index < length; index++ ) { - if ( queue[ index ] && queue[ index ].finish ) { - queue[ index ].finish.call( this ); - } - } - - // Turn off finishing flag - delete data.finish; - } ); - } -} ); - -jQuery.each( [ "toggle", "show", "hide" ], function( i, name ) { - var cssFn = jQuery.fn[ name ]; - jQuery.fn[ name ] = function( speed, easing, callback ) { - return speed == null || typeof speed === "boolean" ? - cssFn.apply( this, arguments ) : - this.animate( genFx( name, true ), speed, easing, callback ); - }; -} ); - -// Generate shortcuts for custom animations -jQuery.each( { - slideDown: genFx( "show" ), - slideUp: genFx( "hide" ), - slideToggle: genFx( "toggle" ), - fadeIn: { opacity: "show" }, - fadeOut: { opacity: "hide" }, - fadeToggle: { opacity: "toggle" } -}, function( name, props ) { - jQuery.fn[ name ] = function( speed, easing, callback ) { - return this.animate( props, speed, easing, callback ); - }; -} ); - -jQuery.timers = []; -jQuery.fx.tick = function() { - var timer, - i = 0, - timers = jQuery.timers; - - fxNow = jQuery.now(); - - for ( ; i < timers.length; i++ ) { - timer = timers[ i ]; - - // Checks the timer has not already been removed - if ( !timer() && timers[ i ] === timer ) { - timers.splice( i--, 1 ); - } - } - - if ( !timers.length ) { - jQuery.fx.stop(); - } - fxNow = undefined; -}; - -jQuery.fx.timer = function( timer ) { - jQuery.timers.push( timer ); - if ( timer() ) { - jQuery.fx.start(); - } else { - jQuery.timers.pop(); - } -}; - -jQuery.fx.interval = 13; -jQuery.fx.start = function() { - if ( !timerId ) { - timerId = window.requestAnimationFrame ? - window.requestAnimationFrame( raf ) : - window.setInterval( jQuery.fx.tick, jQuery.fx.interval ); - } -}; - -jQuery.fx.stop = function() { - if ( window.cancelAnimationFrame ) { - window.cancelAnimationFrame( timerId ); - } else { - window.clearInterval( timerId ); - } - - timerId = null; -}; - -jQuery.fx.speeds = { - slow: 600, - fast: 200, - - // Default speed - _default: 400 -}; - - -// Based off of the plugin by Clint Helfers, with permission. -// https://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/ -jQuery.fn.delay = function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; - type = type || "fx"; - - return this.queue( type, function( next, hooks ) { - var timeout = window.setTimeout( next, time ); - hooks.stop = function() { - window.clearTimeout( timeout ); - }; - } ); -}; - - -( function() { - var input = document.createElement( "input" ), - select = document.createElement( "select" ), - opt = select.appendChild( document.createElement( "option" ) ); - - input.type = "checkbox"; - - // Support: Android <=4.3 only - // Default value for a checkbox should be "on" - support.checkOn = input.value !== ""; - - // Support: IE <=11 only - // Must access selectedIndex to make default options select - support.optSelected = opt.selected; - - // Support: IE <=11 only - // An input loses its value after becoming a radio - input = document.createElement( "input" ); - input.value = "t"; - input.type = "radio"; - support.radioValue = input.value === "t"; -} )(); - - -var boolHook, - attrHandle = jQuery.expr.attrHandle; - -jQuery.fn.extend( { - attr: function( name, value ) { - return access( this, jQuery.attr, name, value, arguments.length > 1 ); - }, - - removeAttr: function( name ) { - return this.each( function() { - jQuery.removeAttr( this, name ); - } ); - } -} ); - -jQuery.extend( { - attr: function( elem, name, value ) { - var ret, hooks, - nType = elem.nodeType; - - // Don't get/set attributes on text, comment and attribute nodes - if ( nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - // Fallback to prop when attributes are not supported - if ( typeof elem.getAttribute === "undefined" ) { - return jQuery.prop( elem, name, value ); - } - - // Attribute hooks are determined by the lowercase version - // Grab necessary hook if one is defined - if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { - hooks = jQuery.attrHooks[ name.toLowerCase() ] || - ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined ); - } - - if ( value !== undefined ) { - if ( value === null ) { - jQuery.removeAttr( elem, name ); - return; - } - - if ( hooks && "set" in hooks && - ( ret = hooks.set( elem, value, name ) ) !== undefined ) { - return ret; - } - - elem.setAttribute( name, value + "" ); - return value; - } - - if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { - return ret; - } - - ret = jQuery.find.attr( elem, name ); - - // Non-existent attributes return null, we normalize to undefined - return ret == null ? undefined : ret; - }, - - attrHooks: { - type: { - set: function( elem, value ) { - if ( !support.radioValue && value === "radio" && - jQuery.nodeName( elem, "input" ) ) { - var val = elem.value; - elem.setAttribute( "type", value ); - if ( val ) { - elem.value = val; - } - return value; - } - } - } - }, - - removeAttr: function( elem, value ) { - var name, - i = 0, - attrNames = value && value.match( rnotwhite ); - - if ( attrNames && elem.nodeType === 1 ) { - while ( ( name = attrNames[ i++ ] ) ) { - elem.removeAttribute( name ); - } - } - } -} ); - -// Hooks for boolean attributes -boolHook = { - set: function( elem, value, name ) { - if ( value === false ) { - - // Remove boolean attributes when set to false - jQuery.removeAttr( elem, name ); - } else { - elem.setAttribute( name, name ); - } - return name; - } -}; - -jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) { - var getter = attrHandle[ name ] || jQuery.find.attr; - - attrHandle[ name ] = function( elem, name, isXML ) { - var ret, handle, - lowercaseName = name.toLowerCase(); - - if ( !isXML ) { - - // Avoid an infinite loop by temporarily removing this function from the getter - handle = attrHandle[ lowercaseName ]; - attrHandle[ lowercaseName ] = ret; - ret = getter( elem, name, isXML ) != null ? - lowercaseName : - null; - attrHandle[ lowercaseName ] = handle; - } - return ret; - }; -} ); - - - - -var rfocusable = /^(?:input|select|textarea|button)$/i, - rclickable = /^(?:a|area)$/i; - -jQuery.fn.extend( { - prop: function( name, value ) { - return access( this, jQuery.prop, name, value, arguments.length > 1 ); - }, - - removeProp: function( name ) { - return this.each( function() { - delete this[ jQuery.propFix[ name ] || name ]; - } ); - } -} ); - -jQuery.extend( { - prop: function( elem, name, value ) { - var ret, hooks, - nType = elem.nodeType; - - // Don't get/set properties on text, comment and attribute nodes - if ( nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { - - // Fix name and attach hooks - name = jQuery.propFix[ name ] || name; - hooks = jQuery.propHooks[ name ]; - } - - if ( value !== undefined ) { - if ( hooks && "set" in hooks && - ( ret = hooks.set( elem, value, name ) ) !== undefined ) { - return ret; - } - - return ( elem[ name ] = value ); - } - - if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { - return ret; - } - - return elem[ name ]; - }, - - propHooks: { - tabIndex: { - get: function( elem ) { - - // Support: IE <=9 - 11 only - // elem.tabIndex doesn't always return the - // correct value when it hasn't been explicitly set - // https://web.archive.org/web/20141116233347/http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ - // Use proper attribute retrieval(#12072) - var tabindex = jQuery.find.attr( elem, "tabindex" ); - - return tabindex ? - parseInt( tabindex, 10 ) : - rfocusable.test( elem.nodeName ) || - rclickable.test( elem.nodeName ) && elem.href ? - 0 : - -1; - } - } - }, - - propFix: { - "for": "htmlFor", - "class": "className" - } -} ); - -// Support: IE <=11 only -// Accessing the selectedIndex property -// forces the browser to respect setting selected -// on the option -// The getter ensures a default option is selected -// when in an optgroup -if ( !support.optSelected ) { - jQuery.propHooks.selected = { - get: function( elem ) { - var parent = elem.parentNode; - if ( parent && parent.parentNode ) { - parent.parentNode.selectedIndex; - } - return null; - }, - set: function( elem ) { - var parent = elem.parentNode; - if ( parent ) { - parent.selectedIndex; - - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } - } - }; -} - -jQuery.each( [ - "tabIndex", - "readOnly", - "maxLength", - "cellSpacing", - "cellPadding", - "rowSpan", - "colSpan", - "useMap", - "frameBorder", - "contentEditable" -], function() { - jQuery.propFix[ this.toLowerCase() ] = this; -} ); - - - - -var rclass = /[\t\r\n\f]/g; - -function getClass( elem ) { - return elem.getAttribute && elem.getAttribute( "class" ) || ""; -} - -jQuery.fn.extend( { - addClass: function( value ) { - var classes, elem, cur, curValue, clazz, j, finalValue, - i = 0; - - if ( jQuery.isFunction( value ) ) { - return this.each( function( j ) { - jQuery( this ).addClass( value.call( this, j, getClass( this ) ) ); - } ); - } - - if ( typeof value === "string" && value ) { - classes = value.match( rnotwhite ) || []; - - while ( ( elem = this[ i++ ] ) ) { - curValue = getClass( elem ); - cur = elem.nodeType === 1 && - ( " " + curValue + " " ).replace( rclass, " " ); - - if ( cur ) { - j = 0; - while ( ( clazz = classes[ j++ ] ) ) { - if ( cur.indexOf( " " + clazz + " " ) < 0 ) { - cur += clazz + " "; - } - } - - // Only assign if different to avoid unneeded rendering. - finalValue = jQuery.trim( cur ); - if ( curValue !== finalValue ) { - elem.setAttribute( "class", finalValue ); - } - } - } - } - - return this; - }, - - removeClass: function( value ) { - var classes, elem, cur, curValue, clazz, j, finalValue, - i = 0; - - if ( jQuery.isFunction( value ) ) { - return this.each( function( j ) { - jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) ); - } ); - } - - if ( !arguments.length ) { - return this.attr( "class", "" ); - } - - if ( typeof value === "string" && value ) { - classes = value.match( rnotwhite ) || []; - - while ( ( elem = this[ i++ ] ) ) { - curValue = getClass( elem ); - - // This expression is here for better compressibility (see addClass) - cur = elem.nodeType === 1 && - ( " " + curValue + " " ).replace( rclass, " " ); - - if ( cur ) { - j = 0; - while ( ( clazz = classes[ j++ ] ) ) { - - // Remove *all* instances - while ( cur.indexOf( " " + clazz + " " ) > -1 ) { - cur = cur.replace( " " + clazz + " ", " " ); - } - } - - // Only assign if different to avoid unneeded rendering. - finalValue = jQuery.trim( cur ); - if ( curValue !== finalValue ) { - elem.setAttribute( "class", finalValue ); - } - } - } - } - - return this; - }, - - toggleClass: function( value, stateVal ) { - var type = typeof value; - - if ( typeof stateVal === "boolean" && type === "string" ) { - return stateVal ? this.addClass( value ) : this.removeClass( value ); - } - - if ( jQuery.isFunction( value ) ) { - return this.each( function( i ) { - jQuery( this ).toggleClass( - value.call( this, i, getClass( this ), stateVal ), - stateVal - ); - } ); - } - - return this.each( function() { - var className, i, self, classNames; - - if ( type === "string" ) { - - // Toggle individual class names - i = 0; - self = jQuery( this ); - classNames = value.match( rnotwhite ) || []; - - while ( ( className = classNames[ i++ ] ) ) { - - // Check each className given, space separated list - if ( self.hasClass( className ) ) { - self.removeClass( className ); - } else { - self.addClass( className ); - } - } - - // Toggle whole class name - } else if ( value === undefined || type === "boolean" ) { - className = getClass( this ); - if ( className ) { - - // Store className if set - dataPriv.set( this, "__className__", className ); - } - - // If the element has a class name or if we're passed `false`, - // then remove the whole classname (if there was one, the above saved it). - // Otherwise bring back whatever was previously saved (if anything), - // falling back to the empty string if nothing was stored. - if ( this.setAttribute ) { - this.setAttribute( "class", - className || value === false ? - "" : - dataPriv.get( this, "__className__" ) || "" - ); - } - } - } ); - }, - - hasClass: function( selector ) { - var className, elem, - i = 0; - - className = " " + selector + " "; - while ( ( elem = this[ i++ ] ) ) { - if ( elem.nodeType === 1 && - ( " " + getClass( elem ) + " " ).replace( rclass, " " ) - .indexOf( className ) > -1 - ) { - return true; - } - } - - return false; - } -} ); - - - - -var rreturn = /\r/g, - rspaces = /[\x20\t\r\n\f]+/g; - -jQuery.fn.extend( { - val: function( value ) { - var hooks, ret, isFunction, - elem = this[ 0 ]; - - if ( !arguments.length ) { - if ( elem ) { - hooks = jQuery.valHooks[ elem.type ] || - jQuery.valHooks[ elem.nodeName.toLowerCase() ]; - - if ( hooks && - "get" in hooks && - ( ret = hooks.get( elem, "value" ) ) !== undefined - ) { - return ret; - } - - ret = elem.value; - - return typeof ret === "string" ? - - // Handle most common string cases - ret.replace( rreturn, "" ) : - - // Handle cases where value is null/undef or number - ret == null ? "" : ret; - } - - return; - } - - isFunction = jQuery.isFunction( value ); - - return this.each( function( i ) { - var val; - - if ( this.nodeType !== 1 ) { - return; - } - - if ( isFunction ) { - val = value.call( this, i, jQuery( this ).val() ); - } else { - val = value; - } - - // Treat null/undefined as ""; convert numbers to string - if ( val == null ) { - val = ""; - - } else if ( typeof val === "number" ) { - val += ""; - - } else if ( jQuery.isArray( val ) ) { - val = jQuery.map( val, function( value ) { - return value == null ? "" : value + ""; - } ); - } - - hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; - - // If set returns undefined, fall back to normal setting - if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) { - this.value = val; - } - } ); - } -} ); - -jQuery.extend( { - valHooks: { - option: { - get: function( elem ) { - - var val = jQuery.find.attr( elem, "value" ); - return val != null ? - val : - - // Support: IE <=10 - 11 only - // option.text throws exceptions (#14686, #14858) - // Strip and collapse whitespace - // https://html.spec.whatwg.org/#strip-and-collapse-whitespace - jQuery.trim( jQuery.text( elem ) ).replace( rspaces, " " ); - } - }, - select: { - get: function( elem ) { - var value, option, - options = elem.options, - index = elem.selectedIndex, - one = elem.type === "select-one", - values = one ? null : [], - max = one ? index + 1 : options.length, - i = index < 0 ? - max : - one ? index : 0; - - // Loop through all the selected options - for ( ; i < max; i++ ) { - option = options[ i ]; - - // Support: IE <=9 only - // IE8-9 doesn't update selected after form reset (#2551) - if ( ( option.selected || i === index ) && - - // Don't return options that are disabled or in a disabled optgroup - !option.disabled && - ( !option.parentNode.disabled || - !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { - - // Get the specific value for the option - value = jQuery( option ).val(); - - // We don't need an array for one selects - if ( one ) { - return value; - } - - // Multi-Selects return an array - values.push( value ); - } - } - - return values; - }, - - set: function( elem, value ) { - var optionSet, option, - options = elem.options, - values = jQuery.makeArray( value ), - i = options.length; - - while ( i-- ) { - option = options[ i ]; - if ( option.selected = - jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1 - ) { - optionSet = true; - } - } - - // Force browsers to behave consistently when non-matching value is set - if ( !optionSet ) { - elem.selectedIndex = -1; - } - return values; - } - } - } -} ); - -// Radios and checkboxes getter/setter -jQuery.each( [ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = { - set: function( elem, value ) { - if ( jQuery.isArray( value ) ) { - return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 ); - } - } - }; - if ( !support.checkOn ) { - jQuery.valHooks[ this ].get = function( elem ) { - return elem.getAttribute( "value" ) === null ? "on" : elem.value; - }; - } -} ); - - - - -// Return jQuery for attributes-only inclusion - - -var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/; - -jQuery.extend( jQuery.event, { - - trigger: function( event, data, elem, onlyHandlers ) { - - var i, cur, tmp, bubbleType, ontype, handle, special, - eventPath = [ elem || document ], - type = hasOwn.call( event, "type" ) ? event.type : event, - namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; - - cur = tmp = elem = elem || document; - - // Don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - // focus/blur morphs to focusin/out; ensure we're not firing them right now - if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { - return; - } - - if ( type.indexOf( "." ) > -1 ) { - - // Namespaced trigger; create a regexp to match event type in handle() - namespaces = type.split( "." ); - type = namespaces.shift(); - namespaces.sort(); - } - ontype = type.indexOf( ":" ) < 0 && "on" + type; - - // Caller can pass in a jQuery.Event object, Object, or just an event type string - event = event[ jQuery.expando ] ? - event : - new jQuery.Event( type, typeof event === "object" && event ); - - // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) - event.isTrigger = onlyHandlers ? 2 : 3; - event.namespace = namespaces.join( "." ); - event.rnamespace = event.namespace ? - new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) : - null; - - // Clean up the event in case it is being reused - event.result = undefined; - if ( !event.target ) { - event.target = elem; - } - - // Clone any incoming data and prepend the event, creating the handler arg list - data = data == null ? - [ event ] : - jQuery.makeArray( data, [ event ] ); - - // Allow special events to draw outside the lines - special = jQuery.event.special[ type ] || {}; - if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { - return; - } - - // Determine event propagation path in advance, per W3C events spec (#9951) - // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) - if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { - - bubbleType = special.delegateType || type; - if ( !rfocusMorph.test( bubbleType + type ) ) { - cur = cur.parentNode; - } - for ( ; cur; cur = cur.parentNode ) { - eventPath.push( cur ); - tmp = cur; - } - - // Only add window if we got to document (e.g., not plain obj or detached DOM) - if ( tmp === ( elem.ownerDocument || document ) ) { - eventPath.push( tmp.defaultView || tmp.parentWindow || window ); - } - } - - // Fire handlers on the event path - i = 0; - while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { - - event.type = i > 1 ? - bubbleType : - special.bindType || type; - - // jQuery handler - handle = ( dataPriv.get( cur, "events" ) || {} )[ event.type ] && - dataPriv.get( cur, "handle" ); - if ( handle ) { - handle.apply( cur, data ); - } - - // Native handler - handle = ontype && cur[ ontype ]; - if ( handle && handle.apply && acceptData( cur ) ) { - event.result = handle.apply( cur, data ); - if ( event.result === false ) { - event.preventDefault(); - } - } - } - event.type = type; - - // If nobody prevented the default action, do it now - if ( !onlyHandlers && !event.isDefaultPrevented() ) { - - if ( ( !special._default || - special._default.apply( eventPath.pop(), data ) === false ) && - acceptData( elem ) ) { - - // Call a native DOM method on the target with the same name as the event. - // Don't do default actions on window, that's where global variables be (#6170) - if ( ontype && jQuery.isFunction( elem[ type ] ) && !jQuery.isWindow( elem ) ) { - - // Don't re-trigger an onFOO event when we call its FOO() method - tmp = elem[ ontype ]; - - if ( tmp ) { - elem[ ontype ] = null; - } - - // Prevent re-triggering of the same event, since we already bubbled it above - jQuery.event.triggered = type; - elem[ type ](); - jQuery.event.triggered = undefined; - - if ( tmp ) { - elem[ ontype ] = tmp; - } - } - } - } - - return event.result; - }, - - // Piggyback on a donor event to simulate a different one - // Used only for `focus(in | out)` events - simulate: function( type, elem, event ) { - var e = jQuery.extend( - new jQuery.Event(), - event, - { - type: type, - isSimulated: true - } - ); - - jQuery.event.trigger( e, null, elem ); - } - -} ); - -jQuery.fn.extend( { - - trigger: function( type, data ) { - return this.each( function() { - jQuery.event.trigger( type, data, this ); - } ); - }, - triggerHandler: function( type, data ) { - var elem = this[ 0 ]; - if ( elem ) { - return jQuery.event.trigger( type, data, elem, true ); - } - } -} ); - - -jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " + - "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + - "change select submit keydown keypress keyup contextmenu" ).split( " " ), - function( i, name ) { - - // Handle event binding - jQuery.fn[ name ] = function( data, fn ) { - return arguments.length > 0 ? - this.on( name, null, data, fn ) : - this.trigger( name ); - }; -} ); - -jQuery.fn.extend( { - hover: function( fnOver, fnOut ) { - return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); - } -} ); - - - - -support.focusin = "onfocusin" in window; - - -// Support: Firefox <=44 -// Firefox doesn't have focus(in | out) events -// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787 -// -// Support: Chrome <=48 - 49, Safari <=9.0 - 9.1 -// focus(in | out) events fire after focus & blur events, -// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order -// Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857 -if ( !support.focusin ) { - jQuery.each( { focus: "focusin", blur: "focusout" }, function( orig, fix ) { - - // Attach a single capturing handler on the document while someone wants focusin/focusout - var handler = function( event ) { - jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) ); - }; - - jQuery.event.special[ fix ] = { - setup: function() { - var doc = this.ownerDocument || this, - attaches = dataPriv.access( doc, fix ); - - if ( !attaches ) { - doc.addEventListener( orig, handler, true ); - } - dataPriv.access( doc, fix, ( attaches || 0 ) + 1 ); - }, - teardown: function() { - var doc = this.ownerDocument || this, - attaches = dataPriv.access( doc, fix ) - 1; - - if ( !attaches ) { - doc.removeEventListener( orig, handler, true ); - dataPriv.remove( doc, fix ); - - } else { - dataPriv.access( doc, fix, attaches ); - } - } - }; - } ); -} -var location = window.location; - -var nonce = jQuery.now(); - -var rquery = ( /\?/ ); - - - -// Cross-browser xml parsing -jQuery.parseXML = function( data ) { - var xml; - if ( !data || typeof data !== "string" ) { - return null; - } - - // Support: IE 9 - 11 only - // IE throws on parseFromString with invalid input. - try { - xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); - } catch ( e ) { - xml = undefined; - } - - if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) { - jQuery.error( "Invalid XML: " + data ); - } - return xml; -}; - - -var - rbracket = /\[\]$/, - rCRLF = /\r?\n/g, - rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, - rsubmittable = /^(?:input|select|textarea|keygen)/i; - -function buildParams( prefix, obj, traditional, add ) { - var name; - - if ( jQuery.isArray( obj ) ) { - - // Serialize array item. - jQuery.each( obj, function( i, v ) { - if ( traditional || rbracket.test( prefix ) ) { - - // Treat each array item as a scalar. - add( prefix, v ); - - } else { - - // Item is non-scalar (array or object), encode its numeric index. - buildParams( - prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]", - v, - traditional, - add - ); - } - } ); - - } else if ( !traditional && jQuery.type( obj ) === "object" ) { - - // Serialize object item. - for ( name in obj ) { - buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); - } - - } else { - - // Serialize scalar item. - add( prefix, obj ); - } -} - -// Serialize an array of form elements or a set of -// key/values into a query string -jQuery.param = function( a, traditional ) { - var prefix, - s = [], - add = function( key, valueOrFunction ) { - - // If value is a function, invoke it and use its return value - var value = jQuery.isFunction( valueOrFunction ) ? - valueOrFunction() : - valueOrFunction; - - s[ s.length ] = encodeURIComponent( key ) + "=" + - encodeURIComponent( value == null ? "" : value ); - }; - - // If an array was passed in, assume that it is an array of form elements. - if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { - - // Serialize the form elements - jQuery.each( a, function() { - add( this.name, this.value ); - } ); - - } else { - - // If traditional, encode the "old" way (the way 1.3.2 or older - // did it), otherwise encode params recursively. - for ( prefix in a ) { - buildParams( prefix, a[ prefix ], traditional, add ); - } - } - - // Return the resulting serialization - return s.join( "&" ); -}; - -jQuery.fn.extend( { - serialize: function() { - return jQuery.param( this.serializeArray() ); - }, - serializeArray: function() { - return this.map( function() { - - // Can add propHook for "elements" to filter or add form elements - var elements = jQuery.prop( this, "elements" ); - return elements ? jQuery.makeArray( elements ) : this; - } ) - .filter( function() { - var type = this.type; - - // Use .is( ":disabled" ) so that fieldset[disabled] works - return this.name && !jQuery( this ).is( ":disabled" ) && - rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && - ( this.checked || !rcheckableType.test( type ) ); - } ) - .map( function( i, elem ) { - var val = jQuery( this ).val(); - - return val == null ? - null : - jQuery.isArray( val ) ? - jQuery.map( val, function( val ) { - return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; - } ) : - { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; - } ).get(); - } -} ); - - -var - r20 = /%20/g, - rhash = /#.*$/, - rts = /([?&])_=[^&]*/, - rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg, - - // #7653, #8125, #8152: local protocol detection - rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, - rnoContent = /^(?:GET|HEAD)$/, - rprotocol = /^\/\//, - - /* Prefilters - * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) - * 2) These are called: - * - BEFORE asking for a transport - * - AFTER param serialization (s.data is a string if s.processData is true) - * 3) key is the dataType - * 4) the catchall symbol "*" can be used - * 5) execution will start with transport dataType and THEN continue down to "*" if needed - */ - prefilters = {}, - - /* Transports bindings - * 1) key is the dataType - * 2) the catchall symbol "*" can be used - * 3) selection will start with transport dataType and THEN go to "*" if needed - */ - transports = {}, - - // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression - allTypes = "*/".concat( "*" ), - - // Anchor tag for parsing the document origin - originAnchor = document.createElement( "a" ); - originAnchor.href = location.href; - -// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport -function addToPrefiltersOrTransports( structure ) { - - // dataTypeExpression is optional and defaults to "*" - return function( dataTypeExpression, func ) { - - if ( typeof dataTypeExpression !== "string" ) { - func = dataTypeExpression; - dataTypeExpression = "*"; - } - - var dataType, - i = 0, - dataTypes = dataTypeExpression.toLowerCase().match( rnotwhite ) || []; - - if ( jQuery.isFunction( func ) ) { - - // For each dataType in the dataTypeExpression - while ( ( dataType = dataTypes[ i++ ] ) ) { - - // Prepend if requested - if ( dataType[ 0 ] === "+" ) { - dataType = dataType.slice( 1 ) || "*"; - ( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func ); - - // Otherwise append - } else { - ( structure[ dataType ] = structure[ dataType ] || [] ).push( func ); - } - } - } - }; -} - -// Base inspection function for prefilters and transports -function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) { - - var inspected = {}, - seekingTransport = ( structure === transports ); - - function inspect( dataType ) { - var selected; - inspected[ dataType ] = true; - jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) { - var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR ); - if ( typeof dataTypeOrTransport === "string" && - !seekingTransport && !inspected[ dataTypeOrTransport ] ) { - - options.dataTypes.unshift( dataTypeOrTransport ); - inspect( dataTypeOrTransport ); - return false; - } else if ( seekingTransport ) { - return !( selected = dataTypeOrTransport ); - } - } ); - return selected; - } - - return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" ); -} - -// A special extend for ajax options -// that takes "flat" options (not to be deep extended) -// Fixes #9887 -function ajaxExtend( target, src ) { - var key, deep, - flatOptions = jQuery.ajaxSettings.flatOptions || {}; - - for ( key in src ) { - if ( src[ key ] !== undefined ) { - ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; - } - } - if ( deep ) { - jQuery.extend( true, target, deep ); - } - - return target; -} - -/* Handles responses to an ajax request: - * - finds the right dataType (mediates between content-type and expected dataType) - * - returns the corresponding response - */ -function ajaxHandleResponses( s, jqXHR, responses ) { - - var ct, type, finalDataType, firstDataType, - contents = s.contents, - dataTypes = s.dataTypes; - - // Remove auto dataType and get content-type in the process - while ( dataTypes[ 0 ] === "*" ) { - dataTypes.shift(); - if ( ct === undefined ) { - ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" ); - } - } - - // Check if we're dealing with a known content-type - if ( ct ) { - for ( type in contents ) { - if ( contents[ type ] && contents[ type ].test( ct ) ) { - dataTypes.unshift( type ); - break; - } - } - } - - // Check to see if we have a response for the expected dataType - if ( dataTypes[ 0 ] in responses ) { - finalDataType = dataTypes[ 0 ]; - } else { - - // Try convertible dataTypes - for ( type in responses ) { - if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) { - finalDataType = type; - break; - } - if ( !firstDataType ) { - firstDataType = type; - } - } - - // Or just use first one - finalDataType = finalDataType || firstDataType; - } - - // If we found a dataType - // We add the dataType to the list if needed - // and return the corresponding response - if ( finalDataType ) { - if ( finalDataType !== dataTypes[ 0 ] ) { - dataTypes.unshift( finalDataType ); - } - return responses[ finalDataType ]; - } -} - -/* Chain conversions given the request and the original response - * Also sets the responseXXX fields on the jqXHR instance - */ -function ajaxConvert( s, response, jqXHR, isSuccess ) { - var conv2, current, conv, tmp, prev, - converters = {}, - - // Work with a copy of dataTypes in case we need to modify it for conversion - dataTypes = s.dataTypes.slice(); - - // Create converters map with lowercased keys - if ( dataTypes[ 1 ] ) { - for ( conv in s.converters ) { - converters[ conv.toLowerCase() ] = s.converters[ conv ]; - } - } - - current = dataTypes.shift(); - - // Convert to each sequential dataType - while ( current ) { - - if ( s.responseFields[ current ] ) { - jqXHR[ s.responseFields[ current ] ] = response; - } - - // Apply the dataFilter if provided - if ( !prev && isSuccess && s.dataFilter ) { - response = s.dataFilter( response, s.dataType ); - } - - prev = current; - current = dataTypes.shift(); - - if ( current ) { - - // There's only work to do if current dataType is non-auto - if ( current === "*" ) { - - current = prev; - - // Convert response if prev dataType is non-auto and differs from current - } else if ( prev !== "*" && prev !== current ) { - - // Seek a direct converter - conv = converters[ prev + " " + current ] || converters[ "* " + current ]; - - // If none found, seek a pair - if ( !conv ) { - for ( conv2 in converters ) { - - // If conv2 outputs current - tmp = conv2.split( " " ); - if ( tmp[ 1 ] === current ) { - - // If prev can be converted to accepted input - conv = converters[ prev + " " + tmp[ 0 ] ] || - converters[ "* " + tmp[ 0 ] ]; - if ( conv ) { - - // Condense equivalence converters - if ( conv === true ) { - conv = converters[ conv2 ]; - - // Otherwise, insert the intermediate dataType - } else if ( converters[ conv2 ] !== true ) { - current = tmp[ 0 ]; - dataTypes.unshift( tmp[ 1 ] ); - } - break; - } - } - } - } - - // Apply converter (if not an equivalence) - if ( conv !== true ) { - - // Unless errors are allowed to bubble, catch and return them - if ( conv && s.throws ) { - response = conv( response ); - } else { - try { - response = conv( response ); - } catch ( e ) { - return { - state: "parsererror", - error: conv ? e : "No conversion from " + prev + " to " + current - }; - } - } - } - } - } - } - - return { state: "success", data: response }; -} - -jQuery.extend( { - - // Counter for holding the number of active queries - active: 0, - - // Last-Modified header cache for next request - lastModified: {}, - etag: {}, - - ajaxSettings: { - url: location.href, - type: "GET", - isLocal: rlocalProtocol.test( location.protocol ), - global: true, - processData: true, - async: true, - contentType: "application/x-www-form-urlencoded; charset=UTF-8", - /* - timeout: 0, - data: null, - dataType: null, - username: null, - password: null, - cache: null, - throws: false, - traditional: false, - headers: {}, - */ - - accepts: { - "*": allTypes, - text: "text/plain", - html: "text/html", - xml: "application/xml, text/xml", - json: "application/json, text/javascript" - }, - - contents: { - xml: /\bxml\b/, - html: /\bhtml/, - json: /\bjson\b/ - }, - - responseFields: { - xml: "responseXML", - text: "responseText", - json: "responseJSON" - }, - - // Data converters - // Keys separate source (or catchall "*") and destination types with a single space - converters: { - - // Convert anything to text - "* text": String, - - // Text to html (true = no transformation) - "text html": true, - - // Evaluate text as a json expression - "text json": JSON.parse, - - // Parse text as xml - "text xml": jQuery.parseXML - }, - - // For options that shouldn't be deep extended: - // you can add your own custom options here if - // and when you create one that shouldn't be - // deep extended (see ajaxExtend) - flatOptions: { - url: true, - context: true - } - }, - - // Creates a full fledged settings object into target - // with both ajaxSettings and settings fields. - // If target is omitted, writes into ajaxSettings. - ajaxSetup: function( target, settings ) { - return settings ? - - // Building a settings object - ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) : - - // Extending ajaxSettings - ajaxExtend( jQuery.ajaxSettings, target ); - }, - - ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), - ajaxTransport: addToPrefiltersOrTransports( transports ), - - // Main method - ajax: function( url, options ) { - - // If url is an object, simulate pre-1.5 signature - if ( typeof url === "object" ) { - options = url; - url = undefined; - } - - // Force options to be an object - options = options || {}; - - var transport, - - // URL without anti-cache param - cacheURL, - - // Response headers - responseHeadersString, - responseHeaders, - - // timeout handle - timeoutTimer, - - // Url cleanup var - urlAnchor, - - // Request state (becomes false upon send and true upon completion) - completed, - - // To know if global events are to be dispatched - fireGlobals, - - // Loop variable - i, - - // uncached part of the url - uncached, - - // Create the final options object - s = jQuery.ajaxSetup( {}, options ), - - // Callbacks context - callbackContext = s.context || s, - - // Context for global events is callbackContext if it is a DOM node or jQuery collection - globalEventContext = s.context && - ( callbackContext.nodeType || callbackContext.jquery ) ? - jQuery( callbackContext ) : - jQuery.event, - - // Deferreds - deferred = jQuery.Deferred(), - completeDeferred = jQuery.Callbacks( "once memory" ), - - // Status-dependent callbacks - statusCode = s.statusCode || {}, - - // Headers (they are sent all at once) - requestHeaders = {}, - requestHeadersNames = {}, - - // Default abort message - strAbort = "canceled", - - // Fake xhr - jqXHR = { - readyState: 0, - - // Builds headers hashtable if needed - getResponseHeader: function( key ) { - var match; - if ( completed ) { - if ( !responseHeaders ) { - responseHeaders = {}; - while ( ( match = rheaders.exec( responseHeadersString ) ) ) { - responseHeaders[ match[ 1 ].toLowerCase() ] = match[ 2 ]; - } - } - match = responseHeaders[ key.toLowerCase() ]; - } - return match == null ? null : match; - }, - - // Raw string - getAllResponseHeaders: function() { - return completed ? responseHeadersString : null; - }, - - // Caches the header - setRequestHeader: function( name, value ) { - if ( completed == null ) { - name = requestHeadersNames[ name.toLowerCase() ] = - requestHeadersNames[ name.toLowerCase() ] || name; - requestHeaders[ name ] = value; - } - return this; - }, - - // Overrides response content-type header - overrideMimeType: function( type ) { - if ( completed == null ) { - s.mimeType = type; - } - return this; - }, - - // Status-dependent callbacks - statusCode: function( map ) { - var code; - if ( map ) { - if ( completed ) { - - // Execute the appropriate callbacks - jqXHR.always( map[ jqXHR.status ] ); - } else { - - // Lazy-add the new callbacks in a way that preserves old ones - for ( code in map ) { - statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; - } - } - } - return this; - }, - - // Cancel the request - abort: function( statusText ) { - var finalText = statusText || strAbort; - if ( transport ) { - transport.abort( finalText ); - } - done( 0, finalText ); - return this; - } - }; - - // Attach deferreds - deferred.promise( jqXHR ); - - // Add protocol if not provided (prefilters might expect it) - // Handle falsy url in the settings object (#10093: consistency with old signature) - // We also use the url parameter if available - s.url = ( ( url || s.url || location.href ) + "" ) - .replace( rprotocol, location.protocol + "//" ); - - // Alias method option to type as per ticket #12004 - s.type = options.method || options.type || s.method || s.type; - - // Extract dataTypes list - s.dataTypes = ( s.dataType || "*" ).toLowerCase().match( rnotwhite ) || [ "" ]; - - // A cross-domain request is in order when the origin doesn't match the current origin. - if ( s.crossDomain == null ) { - urlAnchor = document.createElement( "a" ); - - // Support: IE <=8 - 11, Edge 12 - 13 - // IE throws exception on accessing the href property if url is malformed, - // e.g. http://example.com:80x/ - try { - urlAnchor.href = s.url; - - // Support: IE <=8 - 11 only - // Anchor's host property isn't correctly set when s.url is relative - urlAnchor.href = urlAnchor.href; - s.crossDomain = originAnchor.protocol + "//" + originAnchor.host !== - urlAnchor.protocol + "//" + urlAnchor.host; - } catch ( e ) { - - // If there is an error parsing the URL, assume it is crossDomain, - // it can be rejected by the transport if it is invalid - s.crossDomain = true; - } - } - - // Convert data if not already a string - if ( s.data && s.processData && typeof s.data !== "string" ) { - s.data = jQuery.param( s.data, s.traditional ); - } - - // Apply prefilters - inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); - - // If request was aborted inside a prefilter, stop there - if ( completed ) { - return jqXHR; - } - - // We can fire global events as of now if asked to - // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118) - fireGlobals = jQuery.event && s.global; - - // Watch for a new set of requests - if ( fireGlobals && jQuery.active++ === 0 ) { - jQuery.event.trigger( "ajaxStart" ); - } - - // Uppercase the type - s.type = s.type.toUpperCase(); - - // Determine if request has content - s.hasContent = !rnoContent.test( s.type ); - - // Save the URL in case we're toying with the If-Modified-Since - // and/or If-None-Match header later on - // Remove hash to simplify url manipulation - cacheURL = s.url.replace( rhash, "" ); - - // More options handling for requests with no content - if ( !s.hasContent ) { - - // Remember the hash so we can put it back - uncached = s.url.slice( cacheURL.length ); - - // If data is available, append data to url - if ( s.data ) { - cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data; - - // #9682: remove data so that it's not used in an eventual retry - delete s.data; - } - - // Add anti-cache in uncached url if needed - if ( s.cache === false ) { - cacheURL = cacheURL.replace( rts, "" ); - uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce++ ) + uncached; - } - - // Put hash and anti-cache on the URL that will be requested (gh-1732) - s.url = cacheURL + uncached; - - // Change '%20' to '+' if this is encoded form body content (gh-2658) - } else if ( s.data && s.processData && - ( s.contentType || "" ).indexOf( "application/x-www-form-urlencoded" ) === 0 ) { - s.data = s.data.replace( r20, "+" ); - } - - // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. - if ( s.ifModified ) { - if ( jQuery.lastModified[ cacheURL ] ) { - jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] ); - } - if ( jQuery.etag[ cacheURL ] ) { - jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] ); - } - } - - // Set the correct header, if data is being sent - if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { - jqXHR.setRequestHeader( "Content-Type", s.contentType ); - } - - // Set the Accepts header for the server, depending on the dataType - jqXHR.setRequestHeader( - "Accept", - s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ? - s.accepts[ s.dataTypes[ 0 ] ] + - ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : - s.accepts[ "*" ] - ); - - // Check for headers option - for ( i in s.headers ) { - jqXHR.setRequestHeader( i, s.headers[ i ] ); - } - - // Allow custom headers/mimetypes and early abort - if ( s.beforeSend && - ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) { - - // Abort if not done already and return - return jqXHR.abort(); - } - - // Aborting is no longer a cancellation - strAbort = "abort"; - - // Install callbacks on deferreds - completeDeferred.add( s.complete ); - jqXHR.done( s.success ); - jqXHR.fail( s.error ); - - // Get transport - transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); - - // If no transport, we auto-abort - if ( !transport ) { - done( -1, "No Transport" ); - } else { - jqXHR.readyState = 1; - - // Send global event - if ( fireGlobals ) { - globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); - } - - // If request was aborted inside ajaxSend, stop there - if ( completed ) { - return jqXHR; - } - - // Timeout - if ( s.async && s.timeout > 0 ) { - timeoutTimer = window.setTimeout( function() { - jqXHR.abort( "timeout" ); - }, s.timeout ); - } - - try { - completed = false; - transport.send( requestHeaders, done ); - } catch ( e ) { - - // Rethrow post-completion exceptions - if ( completed ) { - throw e; - } - - // Propagate others as results - done( -1, e ); - } - } - - // Callback for when everything is done - function done( status, nativeStatusText, responses, headers ) { - var isSuccess, success, error, response, modified, - statusText = nativeStatusText; - - // Ignore repeat invocations - if ( completed ) { - return; - } - - completed = true; - - // Clear timeout if it exists - if ( timeoutTimer ) { - window.clearTimeout( timeoutTimer ); - } - - // Dereference transport for early garbage collection - // (no matter how long the jqXHR object will be used) - transport = undefined; - - // Cache response headers - responseHeadersString = headers || ""; - - // Set readyState - jqXHR.readyState = status > 0 ? 4 : 0; - - // Determine if successful - isSuccess = status >= 200 && status < 300 || status === 304; - - // Get response data - if ( responses ) { - response = ajaxHandleResponses( s, jqXHR, responses ); - } - - // Convert no matter what (that way responseXXX fields are always set) - response = ajaxConvert( s, response, jqXHR, isSuccess ); - - // If successful, handle type chaining - if ( isSuccess ) { - - // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. - if ( s.ifModified ) { - modified = jqXHR.getResponseHeader( "Last-Modified" ); - if ( modified ) { - jQuery.lastModified[ cacheURL ] = modified; - } - modified = jqXHR.getResponseHeader( "etag" ); - if ( modified ) { - jQuery.etag[ cacheURL ] = modified; - } - } - - // if no content - if ( status === 204 || s.type === "HEAD" ) { - statusText = "nocontent"; - - // if not modified - } else if ( status === 304 ) { - statusText = "notmodified"; - - // If we have data, let's convert it - } else { - statusText = response.state; - success = response.data; - error = response.error; - isSuccess = !error; - } - } else { - - // Extract error from statusText and normalize for non-aborts - error = statusText; - if ( status || !statusText ) { - statusText = "error"; - if ( status < 0 ) { - status = 0; - } - } - } - - // Set data for the fake xhr object - jqXHR.status = status; - jqXHR.statusText = ( nativeStatusText || statusText ) + ""; - - // Success/Error - if ( isSuccess ) { - deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); - } else { - deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); - } - - // Status-dependent callbacks - jqXHR.statusCode( statusCode ); - statusCode = undefined; - - if ( fireGlobals ) { - globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError", - [ jqXHR, s, isSuccess ? success : error ] ); - } - - // Complete - completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); - - if ( fireGlobals ) { - globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); - - // Handle the global AJAX counter - if ( !( --jQuery.active ) ) { - jQuery.event.trigger( "ajaxStop" ); - } - } - } - - return jqXHR; - }, - - getJSON: function( url, data, callback ) { - return jQuery.get( url, data, callback, "json" ); - }, - - getScript: function( url, callback ) { - return jQuery.get( url, undefined, callback, "script" ); - } -} ); - -jQuery.each( [ "get", "post" ], function( i, method ) { - jQuery[ method ] = function( url, data, callback, type ) { - - // Shift arguments if data argument was omitted - if ( jQuery.isFunction( data ) ) { - type = type || callback; - callback = data; - data = undefined; - } - - // The url can be an options object (which then must have .url) - return jQuery.ajax( jQuery.extend( { - url: url, - type: method, - dataType: type, - data: data, - success: callback - }, jQuery.isPlainObject( url ) && url ) ); - }; -} ); - - -jQuery._evalUrl = function( url ) { - return jQuery.ajax( { - url: url, - - // Make this explicit, since user can override this through ajaxSetup (#11264) - type: "GET", - dataType: "script", - cache: true, - async: false, - global: false, - "throws": true - } ); -}; - - -jQuery.fn.extend( { - wrapAll: function( html ) { - var wrap; - - if ( this[ 0 ] ) { - if ( jQuery.isFunction( html ) ) { - html = html.call( this[ 0 ] ); - } - - // The elements to wrap the target around - wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true ); - - if ( this[ 0 ].parentNode ) { - wrap.insertBefore( this[ 0 ] ); - } - - wrap.map( function() { - var elem = this; - - while ( elem.firstElementChild ) { - elem = elem.firstElementChild; - } - - return elem; - } ).append( this ); - } - - return this; - }, - - wrapInner: function( html ) { - if ( jQuery.isFunction( html ) ) { - return this.each( function( i ) { - jQuery( this ).wrapInner( html.call( this, i ) ); - } ); - } - - return this.each( function() { - var self = jQuery( this ), - contents = self.contents(); - - if ( contents.length ) { - contents.wrapAll( html ); - - } else { - self.append( html ); - } - } ); - }, - - wrap: function( html ) { - var isFunction = jQuery.isFunction( html ); - - return this.each( function( i ) { - jQuery( this ).wrapAll( isFunction ? html.call( this, i ) : html ); - } ); - }, - - unwrap: function( selector ) { - this.parent( selector ).not( "body" ).each( function() { - jQuery( this ).replaceWith( this.childNodes ); - } ); - return this; - } -} ); - - -jQuery.expr.pseudos.hidden = function( elem ) { - return !jQuery.expr.pseudos.visible( elem ); -}; -jQuery.expr.pseudos.visible = function( elem ) { - return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); -}; - - - - -jQuery.ajaxSettings.xhr = function() { - try { - return new window.XMLHttpRequest(); - } catch ( e ) {} -}; - -var xhrSuccessStatus = { - - // File protocol always yields status code 0, assume 200 - 0: 200, - - // Support: IE <=9 only - // #1450: sometimes IE returns 1223 when it should be 204 - 1223: 204 - }, - xhrSupported = jQuery.ajaxSettings.xhr(); - -support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); -support.ajax = xhrSupported = !!xhrSupported; - -jQuery.ajaxTransport( function( options ) { - var callback, errorCallback; - - // Cross domain only allowed if supported through XMLHttpRequest - if ( support.cors || xhrSupported && !options.crossDomain ) { - return { - send: function( headers, complete ) { - var i, - xhr = options.xhr(); - - xhr.open( - options.type, - options.url, - options.async, - options.username, - options.password - ); - - // Apply custom fields if provided - if ( options.xhrFields ) { - for ( i in options.xhrFields ) { - xhr[ i ] = options.xhrFields[ i ]; - } - } - - // Override mime type if needed - if ( options.mimeType && xhr.overrideMimeType ) { - xhr.overrideMimeType( options.mimeType ); - } - - // X-Requested-With header - // For cross-domain requests, seeing as conditions for a preflight are - // akin to a jigsaw puzzle, we simply never set it to be sure. - // (it can always be set on a per-request basis or even using ajaxSetup) - // For same-domain requests, won't change header if already provided. - if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) { - headers[ "X-Requested-With" ] = "XMLHttpRequest"; - } - - // Set headers - for ( i in headers ) { - xhr.setRequestHeader( i, headers[ i ] ); - } - - // Callback - callback = function( type ) { - return function() { - if ( callback ) { - callback = errorCallback = xhr.onload = - xhr.onerror = xhr.onabort = xhr.onreadystatechange = null; - - if ( type === "abort" ) { - xhr.abort(); - } else if ( type === "error" ) { - - // Support: IE <=9 only - // On a manual native abort, IE9 throws - // errors on any property access that is not readyState - if ( typeof xhr.status !== "number" ) { - complete( 0, "error" ); - } else { - complete( - - // File: protocol always yields status 0; see #8605, #14207 - xhr.status, - xhr.statusText - ); - } - } else { - complete( - xhrSuccessStatus[ xhr.status ] || xhr.status, - xhr.statusText, - - // Support: IE <=9 only - // IE9 has no XHR2 but throws on binary (trac-11426) - // For XHR2 non-text, let the caller handle it (gh-2498) - ( xhr.responseType || "text" ) !== "text" || - typeof xhr.responseText !== "string" ? - { binary: xhr.response } : - { text: xhr.responseText }, - xhr.getAllResponseHeaders() - ); - } - } - }; - }; - - // Listen to events - xhr.onload = callback(); - errorCallback = xhr.onerror = callback( "error" ); - - // Support: IE 9 only - // Use onreadystatechange to replace onabort - // to handle uncaught aborts - if ( xhr.onabort !== undefined ) { - xhr.onabort = errorCallback; - } else { - xhr.onreadystatechange = function() { - - // Check readyState before timeout as it changes - if ( xhr.readyState === 4 ) { - - // Allow onerror to be called first, - // but that will not handle a native abort - // Also, save errorCallback to a variable - // as xhr.onerror cannot be accessed - window.setTimeout( function() { - if ( callback ) { - errorCallback(); - } - } ); - } - }; - } - - // Create the abort callback - callback = callback( "abort" ); - - try { - - // Do send the request (this may raise an exception) - xhr.send( options.hasContent && options.data || null ); - } catch ( e ) { - - // #14683: Only rethrow if this hasn't been notified as an error yet - if ( callback ) { - throw e; - } - } - }, - - abort: function() { - if ( callback ) { - callback(); - } - } - }; - } -} ); - - - - -// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432) -jQuery.ajaxPrefilter( function( s ) { - if ( s.crossDomain ) { - s.contents.script = false; - } -} ); - -// Install script dataType -jQuery.ajaxSetup( { - accepts: { - script: "text/javascript, application/javascript, " + - "application/ecmascript, application/x-ecmascript" - }, - contents: { - script: /\b(?:java|ecma)script\b/ - }, - converters: { - "text script": function( text ) { - jQuery.globalEval( text ); - return text; - } - } -} ); - -// Handle cache's special case and crossDomain -jQuery.ajaxPrefilter( "script", function( s ) { - if ( s.cache === undefined ) { - s.cache = false; - } - if ( s.crossDomain ) { - s.type = "GET"; - } -} ); - -// Bind script tag hack transport -jQuery.ajaxTransport( "script", function( s ) { - - // This transport only deals with cross domain requests - if ( s.crossDomain ) { - var script, callback; - return { - send: function( _, complete ) { - script = jQuery( " - - - - - - -
- -
- - - diff --git a/src/templates/admin/plugins-info.html b/src/templates/admin/plugins-info.html deleted file mode 100644 index 57e41f852..000000000 --- a/src/templates/admin/plugins-info.html +++ /dev/null @@ -1,47 +0,0 @@ - - - - Plugin information - Etherpad - - - - - - - -
- - -
-

Etherpad version

-

Version number: <%= epVersion %>

-

Latest available version: <%= latestVersion %>

-

Git sha: <%= gitCommit %>

- -

Installed plugins

- <%- installedPlugins %> - -

Installed parts

- <%- installedParts %> - -

Installed hooks

-

Server-side hooks

- <%- installedServerHooks %> - -

Client-side hooks

- <%- installedClientHooks %> - -
-
- - - diff --git a/src/templates/admin/plugins.html b/src/templates/admin/plugins.html deleted file mode 100644 index 278304faf..000000000 --- a/src/templates/admin/plugins.html +++ /dev/null @@ -1,121 +0,0 @@ - - - - Plugin manager - Etherpad - - - - - - - - - - - -
- - <% if (errors.length) { %> -
- <% errors.forEach(function (item) { %> -
<%= item.toString() %>
- <% }) %> -
- <% } %> - - - -
-

Installed plugins

- - - - - - - - - - - - - - - - - - - - - - -
NameDescriptionVersion
-
- -

-
-
-

You haven't installed any plugins yet.

-


Fetching installed plugins…

-
- -
-
- -

Available plugins

-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - -
NameDescriptionVersionLast update
-
- -

-
-
-
-

 

-

No plugins found.

-


Fetching…

-
-
-
- -
-
- - - diff --git a/src/templates/admin/settings.html b/src/templates/admin/settings.html deleted file mode 100644 index ffa4172f7..000000000 --- a/src/templates/admin/settings.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - Settings - Etherpad - - - - - - - - - - - - - -
- - <% if (errors.length) { %> -
- <% errors.forEach(function (item) { %> -
<%= item.toString() %>
- <% }) %> -
- <% } %> - - - - - - -
-

-
- -
- - - diff --git a/src/templates/export_html.html b/src/templates/export_html.html index fb14edd0a..3ac27e9e8 100644 --- a/src/templates/export_html.html +++ b/src/templates/export_html.html @@ -2,10 +2,11 @@ <%- padId %> - - - - + + + + + empty
', - wantLineAttribs: ['+5'], + wantAlines: ['+5'], wantText: ['empty'], }, - lineWithMultipleSpaces: { + { description: 'Multiple spaces should be preserved', html: 'Text with more than one space.
', - wantLineAttribs: ['+10'], + wantAlines: ['+10'], wantText: ['Text with more than one space.'], }, - lineWithMultipleNonBreakingAndNormalSpaces: { + { description: 'non-breaking and normal space should be preserved', html: 'Text with  more   than  one space.
', - wantLineAttribs: ['+10'], + wantAlines: ['+10'], wantText: ['Text with more than one space.'], }, - multiplenbsp: { + { description: 'Multiple nbsp should be preserved', html: '  
', - wantLineAttribs: ['+2'], + wantAlines: ['+2'], wantText: [' '], }, - multipleNonBreakingSpaceBetweenWords: { + { description: 'Multiple nbsp between words ', html: '  word1  word2   word3
', - wantLineAttribs: ['+m'], + wantAlines: ['+m'], wantText: [' word1 word2 word3'], }, - nonBreakingSpacePreceededBySpaceBetweenWords: { + { description: 'A non-breaking space preceded by a normal space', html: '  word1  word2  word3
', - wantLineAttribs: ['+l'], + wantAlines: ['+l'], wantText: [' word1 word2 word3'], }, - nonBreakingSpaceFollowededBySpaceBetweenWords: { + { description: 'A non-breaking space followed by a normal space', html: '  word1  word2  word3
', - wantLineAttribs: ['+l'], + wantAlines: ['+l'], wantText: [' word1 word2 word3'], }, - spacesAfterNewline: { + { description: 'Don\'t collapse spaces that follow a newline', html: 'something
something
', - wantLineAttribs: ['+9', '+m'], + wantAlines: ['+9', '+m'], wantText: ['something', ' something'], }, - spacesAfterNewlineP: { + { description: 'Don\'t collapse spaces that follow a empty paragraph', html: 'something

something
', - wantLineAttribs: ['+9', '', '+m'], + wantAlines: ['+9', '', '+m'], wantText: ['something', '', ' something'], }, - spacesAtEndOfLine: { + { description: 'Don\'t collapse spaces that preceed/follow a newline', html: 'something
something
', - wantLineAttribs: ['+l', '+m'], + wantAlines: ['+l', '+m'], wantText: ['something ', ' something'], }, - spacesAtEndOfLineP: { + { description: 'Don\'t collapse spaces that preceed/follow a empty paragraph', html: 'something

something
', - wantLineAttribs: ['+l', '', '+m'], + wantAlines: ['+l', '', '+m'], wantText: ['something ', '', ' something'], }, - nonBreakingSpacesAfterNewlines: { + { description: 'Don\'t collapse non-breaking spaces that follow a newline', html: 'something
   something
', - wantLineAttribs: ['+9', '+c'], + wantAlines: ['+9', '+c'], wantText: ['something', ' something'], }, - nonBreakingSpacesAfterNewlinesP: { + { description: 'Don\'t collapse non-breaking spaces that follow a paragraph', html: 'something

   something
', - wantLineAttribs: ['+9', '', '+c'], + wantAlines: ['+9', '', '+c'], wantText: ['something', '', ' something'], }, - preserveSpacesInsideElements: { + { description: 'Preserve all spaces when multiple are present', html: 'Need more space s !
', - wantLineAttribs: ['+h*0+4+2'], + wantAlines: ['+h*1+4+2'], wantText: ['Need more space s !'], }, - preserveSpacesAcrossNewlines: { + { description: 'Newlines and multiple spaces across newlines should be preserved', html: ` Need @@ -201,25 +261,25 @@ const tests = { space s !
`, - wantLineAttribs: ['+19*0+4+b'], + wantAlines: ['+19*1+4+b'], wantText: ['Need more space s !'], }, - multipleNewLinesAtBeginning: { + { description: 'Multiple new lines at the beginning should be preserved', html: '

first line

second line
', - wantLineAttribs: ['', '', '', '', '+a', '', '+b'], + wantAlines: ['', '', '', '', '+a', '', '+b'], wantText: ['', '', '', '', 'first line', '', 'second line'], }, - multiLineParagraph: { + { description: 'A paragraph with multiple lines should not loose spaces when lines are combined', html: `

а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь

`, - wantLineAttribs: ['+1t'], + wantAlines: ['+1t'], wantText: ['а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь'], }, - multiLineParagraphWithPre: { + { description: 'lines in preformatted text should be kept intact', html: `

а б в г ґ д е є ж з и і ї й к л м н о

multiple
@@ -229,7 +289,7 @@ pre
 

п р с т у ф х ц ч ш щ ю я ь

`, - wantLineAttribs: ['+11', '+8', '+5', '+2', '+3', '+r'], + wantAlines: ['+11', '+8', '+5', '+2', '+3', '+r'], wantText: [ 'а б в г ґ д е є ж з и і ї й к л м н о', 'multiple', @@ -239,85 +299,95 @@ pre 'п р с т у ф х ц ч ш щ ю я ь', ], }, - preIntroducesASpace: { + { description: 'pre should be on a new line not preceded by a space', html: `

1

preline
 
`, - wantLineAttribs: ['+6', '+7'], + wantAlines: ['+6', '+7'], wantText: [' 1 ', 'preline'], }, - dontDeleteSpaceInsideElements: { + { description: 'Preserve spaces on the beginning and end of a element', html: 'Need more space s !
', - wantLineAttribs: ['+f*0+3+1'], + wantAlines: ['+f*1+3+1'], wantText: ['Need more space s !'], }, - dontDeleteSpaceOutsideElements: { + { description: 'Preserve spaces outside elements', html: 'Need more space s !
', - wantLineAttribs: ['+g*0+1+2'], + wantAlines: ['+g*1+1+2'], wantText: ['Need more space s !'], }, - dontDeleteSpaceAtEndOfElement: { + { description: 'Preserve spaces at the end of an element', html: 'Need more space s !
', - wantLineAttribs: ['+g*0+2+1'], + wantAlines: ['+g*1+2+1'], wantText: ['Need more space s !'], }, - dontDeleteSpaceAtBeginOfElements: { + { description: 'Preserve spaces at the start of an element', html: 'Need more space s !
', - wantLineAttribs: ['+f*0+2+2'], + wantAlines: ['+f*1+2+2'], wantText: ['Need more space s !'], }, -}; +]; describe(__filename, function () { - for (const test of Object.keys(tests)) { - const testObj = tests[test]; - describe(test, function () { - if (testObj.disabled) { - return xit('DISABLED:', test, function (done) { - done(); - }); - } + for (const tc of testCases) { + describe(tc.description, function () { + let apool: AttributePool; + let result: { + lines: string[], + lineAttribs: string[], + }; - it(testObj.description, async function () { - const {window: {document}} = new jsdom.JSDOM(testObj.html); - // Create an empty attribute pool - const apool = new AttributePool(); - // Convert a dom tree into a list of lines and attribute liens - // using the content collector object + before(async function () { + if (tc.disabled) return this.skip(); + const {window: {document}} = new jsdom.JSDOM(tc.html); + apool = new AttributePool(); + // To reduce test fragility, the attribute pool is seeded with `knownAttribs`, and all + // attributes in `tc.wantAlines` must be in `knownAttribs`. (This guarantees that attribute + // numbers do not change if the attribute processing code changes.) + for (const attrib of knownAttribs) apool.putAttrib(attrib); + for (const aline of tc.wantAlines) { + for (const op of Changeset.deserializeOps(aline)) { + for (const n of attributes.decodeAttribString(op.attribs)) { + assert(n < knownAttribs.length); + } + } + } const cc = contentcollector.makeContentCollector(true, null, apool); cc.collectContent(document.body); - const result = cc.finish(); - const gotAttributes = result.lineAttribs; - const wantAttributes = testObj.wantLineAttribs; - const gotText = new Array(result.lines); - const wantText = testObj.wantText; + result = cc.finish(); + }); - assert.deepEqual(gotText[0], wantText); - assert.deepEqual(gotAttributes, wantAttributes); + it('text matches', async function () { + assert.deepEqual(result.lines, tc.wantText); + }); + + it('alines match', async function () { + assert.deepEqual(result.lineAttribs, tc.wantAlines); + }); + + it('attributes are sorted in canonical order', async function () { + const gotAttribs:string[][][] = []; + const wantAttribs = []; + for (const aline of result.lineAttribs) { + const gotAlineAttribs:string[][] = []; + gotAttribs.push(gotAlineAttribs); + const wantAlineAttribs:Attribute[] = []; + wantAttribs.push(wantAlineAttribs); + for (const op of Changeset.deserializeOps(aline)) { + const gotOpAttribs = [...attributes.attribsFromString(op.attribs, apool)] as unknown as Attribute; + gotAlineAttribs.push(gotOpAttribs); + // @ts-ignore + wantAlineAttribs.push(attributes.sort([...gotOpAttribs])); + } + } + assert.deepEqual(gotAttribs, wantAttribs); }); }); } }); - - -function arraysEqual(a, b) { - if (a === b) return true; - if (a == null || b == null) return false; - if (a.length !== b.length) return false; - - // If you don't care about the order of the elements inside - // the array, you should sort both arrays here. - // Please note that calling sort on an array will modify that array. - // you might want to clone your array first. - - for (let i = 0; i < a.length; ++i) { - if (a[i] !== b[i]) return false; - } - return true; -} diff --git a/src/tests/backend/specs/crypto.ts b/src/tests/backend/specs/crypto.ts new file mode 100644 index 000000000..62d79f1b3 --- /dev/null +++ b/src/tests/backend/specs/crypto.ts @@ -0,0 +1,10 @@ +'use strict'; + + +import {Buffer} from 'buffer'; +import nodeCrypto from 'crypto'; +import util from 'util'; + +const nodeHkdf = nodeCrypto.hkdf ? util.promisify(nodeCrypto.hkdf) : null; + +const ab2hex = (ab:string) => Buffer.from(ab).toString('hex'); diff --git a/src/tests/backend/specs/export.js b/src/tests/backend/specs/export.ts similarity index 84% rename from src/tests/backend/specs/export.js rename to src/tests/backend/specs/export.ts index d2fcde131..de436f88c 100644 --- a/src/tests/backend/specs/export.js +++ b/src/tests/backend/specs/export.ts @@ -1,12 +1,14 @@ 'use strict'; +import {MapArrayType} from "../../../node/types/MapType"; + const common = require('../common'); const padManager = require('../../../node/db/PadManager'); const settings = require('../../../node/utils/Settings'); describe(__filename, function () { - let agent; - const settingsBackup = {}; + let agent:any; + const settingsBackup:MapArrayType = {}; before(async function () { agent = await common.init(); diff --git a/src/tests/backend/specs/favicon.js b/src/tests/backend/specs/favicon.ts similarity index 89% rename from src/tests/backend/specs/favicon.js rename to src/tests/backend/specs/favicon.ts index a5e3095de..6b6230b4b 100644 --- a/src/tests/backend/specs/favicon.js +++ b/src/tests/backend/specs/favicon.ts @@ -1,5 +1,7 @@ 'use strict'; +import {MapArrayType} from "../../../node/types/MapType"; + const assert = require('assert').strict; const common = require('../common'); const fs = require('fs'); @@ -9,12 +11,12 @@ const settings = require('../../../node/utils/Settings'); const superagent = require('superagent'); describe(__filename, function () { - let agent; - let backupSettings; - let skinDir; - let wantCustomIcon; - let wantDefaultIcon; - let wantSkinIcon; + let agent:any; + let backupSettings:MapArrayType; + let skinDir: string; + let wantCustomIcon: boolean; + let wantDefaultIcon: boolean; + let wantSkinIcon: boolean; before(async function () { agent = await common.init(); @@ -51,6 +53,12 @@ describe(__filename, function () { assert(gotIcon.equals(wantCustomIcon)); }); + it('uses custom favicon from url', async function () { + settings.favicon = 'https://etherpad.org/favicon.ico'; + await agent.get('/favicon.ico') + .expect(302); + }); + it('uses custom favicon if set (absolute pathname)', async function () { settings.favicon = path.join(__dirname, 'favicon-test-custom.png'); assert(path.isAbsolute(settings.favicon)); diff --git a/src/tests/backend/specs/health.ts b/src/tests/backend/specs/health.ts new file mode 100644 index 000000000..97364a7e5 --- /dev/null +++ b/src/tests/backend/specs/health.ts @@ -0,0 +1,58 @@ +'use strict'; + +import {MapArrayType} from "../../../node/types/MapType"; + +const assert = require('assert').strict; +const common = require('../common'); +const settings = require('../../../node/utils/Settings'); +const superagent = require('superagent'); + +describe(__filename, function () { + let agent:any; + const backup:MapArrayType = {}; + + const getHealth = () => agent.get('/health') + .accept('application/health+json') + .buffer(true) + .parse(superagent.parse['application/json']) + .expect(200) + .expect((res:any) => assert.equal(res.type, 'application/health+json')); + + before(async function () { + agent = await common.init(); + }); + + beforeEach(async function () { + backup.settings = {}; + for (const setting of ['requireAuthentication', 'requireAuthorization']) { + backup.settings[setting] = settings[setting]; + } + }); + + afterEach(async function () { + Object.assign(settings, backup.settings); + }); + + it('/health works', async function () { + const res = await getHealth(); + assert.equal(res.body.status, 'pass'); + assert.equal(res.body.releaseId, settings.getEpVersion()); + }); + + it('auth is not required', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + const res = await getHealth(); + assert.equal(res.body.status, 'pass'); + }); + + // We actually want to test that no express-session state is created, but that is difficult to do + // without intrusive changes or unpleasant ueberdb digging. Instead, we assume that the lack of a + // cookie means that no express-session state was created (how would express-session look up the + // session state if no ID was returned to the client?). + it('no cookie is returned', async function () { + const res = await getHealth(); + const cookie = res.headers['set-cookie']; + assert(cookie == null, `unexpected Set-Cookie: ${cookie}`); + }); +}); diff --git a/src/tests/backend/specs/hooks.js b/src/tests/backend/specs/hooks.ts similarity index 81% rename from src/tests/backend/specs/hooks.js rename to src/tests/backend/specs/hooks.ts index 3120911ae..07c6e262e 100644 --- a/src/tests/backend/specs/hooks.js +++ b/src/tests/backend/specs/hooks.ts @@ -1,15 +1,37 @@ 'use strict'; -const assert = require('../assert-legacy').strict; +import {strict as assert} from 'assert'; const hooks = require('../../../static/js/pluginfw/hooks'); const plugins = require('../../../static/js/pluginfw/plugin_defs'); -const sinon = require('sinon'); +import sinon from 'sinon'; +import {MapArrayType} from "../../../node/types/MapType"; + + +interface ExtendedConsole extends Console { + warn: { + (message?: any, ...optionalParams: any[]): void; + callCount: number; + getCall: (i: number) => {args: any[]}; + }; + error: { + (message?: any, ...optionalParams: any[]): void; + callCount: number; + getCall: (i: number) => {args: any[]}; + callsFake: (fn: Function) => void; + getCalls: () => {args: any[]}[]; + }; +} + +declare var console: ExtendedConsole; describe(__filename, function () { + + + const hookName = 'testHook'; const hookFnName = 'testPluginFileName:testHookFunctionName'; let testHooks; // Convenience shorthand for plugins.hooks[hookName]. - let hook; // Convenience shorthand for plugins.hooks[hookName][0]. + let hook: any; // Convenience shorthand for plugins.hooks[hookName][0]. beforeEach(async function () { // Make sure these are not already set so that we don't accidentally step on someone else's @@ -32,12 +54,12 @@ describe(__filename, function () { delete hooks.exportedForTestingOnly.deprecationWarned[hookFnName]; }); - const makeHook = (ret) => ({ + const makeHook = (ret?:any) => ({ hook_name: hookName, // Many tests will likely want to change this. Unfortunately, we can't use a convenience // wrapper like `(...args) => hookFn(..args)` because the hooks look at Function.length and // change behavior depending on the number of parameters. - hook_fn: (hn, ctx, cb) => cb(ret), + hook_fn: (hn:Function, ctx:any, cb:Function) => cb(ret), hook_fn_name: hookFnName, part: {plugin: 'testPluginName'}, }); @@ -46,43 +68,43 @@ describe(__filename, function () { const supportedSyncHookFunctions = [ { name: 'return non-Promise value, with callback parameter', - fn: (hn, ctx, cb) => 'val', + fn: (hn:Function, ctx:any, cb:Function) => 'val', want: 'val', syncOk: true, }, { name: 'return non-Promise value, without callback parameter', - fn: (hn, ctx) => 'val', + fn: (hn:Function, ctx:any) => 'val', want: 'val', syncOk: true, }, { name: 'return undefined, without callback parameter', - fn: (hn, ctx) => {}, + fn: (hn:Function, ctx:any) => {}, want: undefined, syncOk: true, }, { name: 'pass non-Promise value to callback', - fn: (hn, ctx, cb) => { cb('val'); }, + fn: (hn:Function, ctx:any, cb:Function) => { cb('val'); }, want: 'val', syncOk: true, }, { name: 'pass undefined to callback', - fn: (hn, ctx, cb) => { cb(); }, + fn: (hn:Function, ctx:any, cb:Function) => { cb(); }, want: undefined, syncOk: true, }, { name: 'return the value returned from the callback', - fn: (hn, ctx, cb) => cb('val'), + fn: (hn:Function, ctx:any, cb:Function) => cb('val'), want: 'val', syncOk: true, }, { name: 'throw', - fn: (hn, ctx, cb) => { throw new Error('test exception'); }, + fn: (hn:Function, ctx:any, cb:Function) => { throw new Error('test exception'); }, wantErr: 'test exception', syncOk: true, }, @@ -93,20 +115,20 @@ describe(__filename, function () { describe('basic behavior', function () { it('passes hook name', async function () { - hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; + hook.hook_fn = (hn: string) => { assert.equal(hn, hookName); }; callHookFnSync(hook); }); it('passes context', async function () { for (const val of ['value', null, undefined]) { - hook.hook_fn = (hn, ctx) => { assert.equal(ctx, val); }; + hook.hook_fn = (hn: string, ctx:string) => { assert.equal(ctx, val); }; callHookFnSync(hook, val); } }); it('returns the value provided to the callback', async function () { for (const val of ['value', null, undefined]) { - hook.hook_fn = (hn, ctx, cb) => { cb(ctx); }; + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { cb(ctx); }; assert.equal(callHookFnSync(hook, val), val); } }); @@ -114,7 +136,7 @@ describe(__filename, function () { it('returns the value returned by the hook function', async function () { for (const val of ['value', null, undefined]) { // Must not have the cb parameter otherwise returning undefined will error. - hook.hook_fn = (hn, ctx) => ctx; + hook.hook_fn = (hn: string, ctx: any) => ctx; assert.equal(callHookFnSync(hook, val), val); } }); @@ -125,7 +147,7 @@ describe(__filename, function () { }); it('callback returns undefined', async function () { - hook.hook_fn = (hn, ctx, cb) => { assert.equal(cb('foo'), undefined); }; + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { assert.equal(cb('foo'), undefined); }; callHookFnSync(hook); }); @@ -134,7 +156,9 @@ describe(__filename, function () { hooks.deprecationNotices[hookName] = 'test deprecation'; callHookFnSync(hook); assert.equal(hooks.exportedForTestingOnly.deprecationWarned[hookFnName], true); + // @ts-ignore assert.equal(console.warn.callCount, 1); + // @ts-ignore assert.match(console.warn.getCall(0).args[0], /test deprecation/); }); }); @@ -166,7 +190,7 @@ describe(__filename, function () { name: 'never settles -> buggy hook detected', // Note that returning undefined without calling the callback is permitted if the function // has 2 or fewer parameters, so this test function must have 3 parameters. - fn: (hn, ctx, cb) => {}, + fn: (hn:Function, ctx:any, cb:Function) => {}, wantVal: undefined, wantError: /UNSETTLED FUNCTION BUG/, }, @@ -178,7 +202,7 @@ describe(__filename, function () { }, { name: 'passes a Promise to cb -> buggy hook detected', - fn: (hn, ctx, cb) => cb(promise2), + fn: (hn:Function, ctx:any, cb:Function) => cb(promise2), wantVal: promise2, wantError: /PROHIBITED PROMISE BUG/, }, @@ -209,20 +233,20 @@ describe(__filename, function () { const behaviors = [ { name: 'throw', - fn: (cb, err, val) => { throw err; }, + fn: (cb: Function, err:any, val: string) => { throw err; }, rejects: true, }, { name: 'return value', - fn: (cb, err, val) => val, + fn: (cb: Function, err:any, val: string) => val, }, { name: 'immediately call cb(value)', - fn: (cb, err, val) => cb(val), + fn: (cb: Function, err:any, val: string) => cb(val), }, { name: 'defer call to cb(value)', - fn: (cb, err, val) => { process.nextTick(cb, val); }, + fn: (cb: Function, err:any, val: string) => { process.nextTick(cb, val); }, async: true, }, ]; @@ -237,7 +261,7 @@ describe(__filename, function () { if (step1.async && step2.async) continue; it(`${step1.name} then ${step2.name} (diff. outcomes) -> log+throw`, async function () { - hook.hook_fn = (hn, ctx, cb) => { + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { step1.fn(cb, new Error(ctx.ret1), ctx.ret1); return step2.fn(cb, new Error(ctx.ret2), ctx.ret2); }; @@ -245,7 +269,7 @@ describe(__filename, function () { // Temporarily remove unhandled error listeners so that the errors we expect to see // don't trigger a test failure (or terminate node). const events = ['uncaughtException', 'unhandledRejection']; - const listenerBackups = {}; + const listenerBackups:MapArrayType = {}; for (const event of events) { listenerBackups[event] = process.rawListeners(event); process.removeAllListeners(event); @@ -256,17 +280,18 @@ describe(__filename, function () { // a throw (in which case the double settle is deferred so that the caller sees the // original error). const wantAsyncErr = step1.async || step2.async || step2.rejects; - let tempListener; - let asyncErr; + let tempListener:Function; + let asyncErr:Error|undefined; try { - const seenErrPromise = new Promise((resolve) => { - tempListener = (err) => { + const seenErrPromise = new Promise((resolve) => { + tempListener = (err:any) => { assert.equal(asyncErr, undefined); asyncErr = err; resolve(); }; if (!wantAsyncErr) resolve(); }); + // @ts-ignore events.forEach((event) => process.on(event, tempListener)); const call = () => callHookFnSync(hook, {ret1: 'val1', ret2: 'val2'}); if (step2.rejects) { @@ -280,6 +305,7 @@ describe(__filename, function () { } finally { // Restore the original listeners. for (const event of events) { + // @ts-ignore process.off(event, tempListener); for (const listener of listenerBackups[event]) { process.on(event, listener); @@ -301,7 +327,7 @@ describe(__filename, function () { it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async function () { const err = new Error('val'); - hook.hook_fn = (hn, ctx, cb) => { + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { step1.fn(cb, err, 'val'); return step2.fn(cb, err, 'val'); }; @@ -331,23 +357,23 @@ describe(__filename, function () { }); it('passes hook name', async function () { - hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; + hook.hook_fn = (hn:string) => { assert.equal(hn, hookName); }; hooks.callAll(hookName); }); it('undefined context -> {}', async function () { - hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hook.hook_fn = (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); }; hooks.callAll(hookName); }); it('null context -> {}', async function () { - hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hook.hook_fn = (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); }; hooks.callAll(hookName, null); }); it('context unmodified', async function () { const wantContext = {}; - hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); }; + hook.hook_fn = (hn: string, ctx: any) => { assert.equal(ctx, wantContext); }; hooks.callAll(hookName, wantContext); }); }); @@ -401,28 +427,28 @@ describe(__filename, function () { }); it('passes hook name => {}', async function () { - hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; + hook.hook_fn = (hn: string) => { assert.equal(hn, hookName); }; hooks.callFirst(hookName); }); it('undefined context => {}', async function () { - hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hook.hook_fn = (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); }; hooks.callFirst(hookName); }); it('null context => {}', async function () { - hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hook.hook_fn = (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); }; hooks.callFirst(hookName, null); }); it('context unmodified', async function () { const wantContext = {}; - hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); }; + hook.hook_fn = (hn: string, ctx: any) => { assert.equal(ctx, wantContext); }; hooks.callFirst(hookName, wantContext); }); it('predicate never satisfied -> calls all in order', async function () { - const gotCalls = []; + const gotCalls:MapArrayType = []; testHooks.length = 0; for (let i = 0; i < 3; i++) { const hook = makeHook(); @@ -466,7 +492,7 @@ describe(__filename, function () { it('value can be passed via callback', async function () { const want = {}; - hook.hook_fn = (hn, ctx, cb) => { cb(want); }; + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { cb(want); }; const got = hooks.callFirst(hookName); assert.deepEqual(got, [want]); assert.equal(got[0], want); // Note: *NOT* deepEqual! @@ -478,20 +504,20 @@ describe(__filename, function () { describe('basic behavior', function () { it('passes hook name', async function () { - hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; + hook.hook_fn = (hn:string) => { assert.equal(hn, hookName); }; await callHookFnAsync(hook); }); it('passes context', async function () { for (const val of ['value', null, undefined]) { - hook.hook_fn = (hn, ctx) => { assert.equal(ctx, val); }; + hook.hook_fn = (hn: string, ctx: any) => { assert.equal(ctx, val); }; await callHookFnAsync(hook, val); } }); it('returns the value provided to the callback', async function () { for (const val of ['value', null, undefined]) { - hook.hook_fn = (hn, ctx, cb) => { cb(ctx); }; + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { cb(ctx); }; assert.equal(await callHookFnAsync(hook, val), val); assert.equal(await callHookFnAsync(hook, Promise.resolve(val)), val); } @@ -500,7 +526,7 @@ describe(__filename, function () { it('returns the value returned by the hook function', async function () { for (const val of ['value', null, undefined]) { // Must not have the cb parameter otherwise returning undefined will never resolve. - hook.hook_fn = (hn, ctx) => ctx; + hook.hook_fn = (hn: string, ctx: any) => ctx; assert.equal(await callHookFnAsync(hook, val), val); assert.equal(await callHookFnAsync(hook, Promise.resolve(val)), val); } @@ -512,17 +538,17 @@ describe(__filename, function () { }); it('rejects if rejected Promise passed to callback', async function () { - hook.hook_fn = (hn, ctx, cb) => cb(Promise.reject(new Error('test exception'))); + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => cb(Promise.reject(new Error('test exception'))); await assert.rejects(callHookFnAsync(hook), {message: 'test exception'}); }); it('rejects if rejected Promise returned', async function () { - hook.hook_fn = (hn, ctx, cb) => Promise.reject(new Error('test exception')); + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => Promise.reject(new Error('test exception')); await assert.rejects(callHookFnAsync(hook), {message: 'test exception'}); }); it('callback returns undefined', async function () { - hook.hook_fn = (hn, ctx, cb) => { assert.equal(cb('foo'), undefined); }; + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { assert.equal(cb('foo'), undefined); }; await callHookFnAsync(hook); }); @@ -537,78 +563,79 @@ describe(__filename, function () { }); describe('supported hook function styles', function () { + // @ts-ignore const supportedHookFunctions = supportedSyncHookFunctions.concat([ { name: 'legacy async cb', - fn: (hn, ctx, cb) => { process.nextTick(cb, 'val'); }, + fn: (hn:Function, ctx:any, cb:Function) => { process.nextTick(cb, 'val'); }, want: 'val', }, // Already resolved Promises: { name: 'return resolved Promise, with callback parameter', - fn: (hn, ctx, cb) => Promise.resolve('val'), + fn: (hn:Function, ctx:any, cb:Function) => Promise.resolve('val'), want: 'val', }, { name: 'return resolved Promise, without callback parameter', - fn: (hn, ctx) => Promise.resolve('val'), + fn: (hn: string, ctx: any) => Promise.resolve('val'), want: 'val', }, { name: 'pass resolved Promise to callback', - fn: (hn, ctx, cb) => { cb(Promise.resolve('val')); }, + fn: (hn:Function, ctx:any, cb:Function) => { cb(Promise.resolve('val')); }, want: 'val', }, // Not yet resolved Promises: { name: 'return unresolved Promise, with callback parameter', - fn: (hn, ctx, cb) => new Promise((resolve) => process.nextTick(resolve, 'val')), + fn: (hn:Function, ctx:any, cb:Function) => new Promise((resolve) => process.nextTick(resolve, 'val')), want: 'val', }, { name: 'return unresolved Promise, without callback parameter', - fn: (hn, ctx) => new Promise((resolve) => process.nextTick(resolve, 'val')), + fn: (hn: string, ctx: any) => new Promise((resolve) => process.nextTick(resolve, 'val')), want: 'val', }, { name: 'pass unresolved Promise to callback', - fn: (hn, ctx, cb) => { cb(new Promise((resolve) => process.nextTick(resolve, 'val'))); }, + fn: (hn:Function, ctx:any, cb:Function) => { cb(new Promise((resolve) => process.nextTick(resolve, 'val'))); }, want: 'val', }, // Already rejected Promises: { name: 'return rejected Promise, with callback parameter', - fn: (hn, ctx, cb) => Promise.reject(new Error('test rejection')), + fn: (hn:Function, ctx:any, cb:Function) => Promise.reject(new Error('test rejection')), wantErr: 'test rejection', }, { name: 'return rejected Promise, without callback parameter', - fn: (hn, ctx) => Promise.reject(new Error('test rejection')), + fn: (hn: string, ctx: any) => Promise.reject(new Error('test rejection')), wantErr: 'test rejection', }, { name: 'pass rejected Promise to callback', - fn: (hn, ctx, cb) => { cb(Promise.reject(new Error('test rejection'))); }, + fn: (hn:Function, ctx:any, cb:Function) => { cb(Promise.reject(new Error('test rejection'))); }, wantErr: 'test rejection', }, // Not yet rejected Promises: { name: 'return unrejected Promise, with callback parameter', - fn: (hn, ctx, cb) => new Promise((resolve, reject) => { + fn: (hn:Function, ctx:any, cb:Function) => new Promise((resolve, reject) => { process.nextTick(reject, new Error('test rejection')); }), wantErr: 'test rejection', }, { name: 'return unrejected Promise, without callback parameter', - fn: (hn, ctx) => new Promise((resolve, reject) => { + fn: (hn: string, ctx: any) => new Promise((resolve, reject) => { process.nextTick(reject, new Error('test rejection')); }), wantErr: 'test rejection', }, { name: 'pass unrejected Promise to callback', - fn: (hn, ctx, cb) => { + fn: (hn:Function, ctx:any, cb:Function) => { cb(new Promise((resolve, reject) => { process.nextTick(reject, new Error('test rejection')); })); @@ -654,13 +681,13 @@ describe(__filename, function () { const behaviors = [ { name: 'throw', - fn: (cb, err, val) => { throw err; }, + fn: (cb: Function, err:any, val: string) => { throw err; }, rejects: true, when: 0, }, { name: 'return value', - fn: (cb, err, val) => val, + fn: (cb: Function, err:any, val: string) => val, // This behavior has a later relative settle time vs. the 'throw' behavior because 'throw' // immediately settles the hook function, whereas the 'return value' case is settled by a // .then() function attached to a Promise. EcmaScript guarantees that a .then() function @@ -670,14 +697,14 @@ describe(__filename, function () { }, { name: 'immediately call cb(value)', - fn: (cb, err, val) => cb(val), + fn: (cb: Function, err:any, val: string) => cb(val), // This behavior has the same relative time as the 'return value' case because it too is // settled by a .then() function attached to a Promise. when: 1, }, { name: 'return resolvedPromise', - fn: (cb, err, val) => Promise.resolve(val), + fn: (cb: Function, err:any, val: string) => Promise.resolve(val), // This behavior has the same relative time as the 'return value' case because the return // value is wrapped in a Promise via Promise.resolve(). The EcmaScript standard guarantees // that Promise.resolve(Promise.resolve(value)) is equivalent to Promise.resolve(value), @@ -687,62 +714,62 @@ describe(__filename, function () { }, { name: 'immediately call cb(resolvedPromise)', - fn: (cb, err, val) => cb(Promise.resolve(val)), + fn: (cb: Function, err:any, val: string) => cb(Promise.resolve(val)), when: 1, }, { name: 'return rejectedPromise', - fn: (cb, err, val) => Promise.reject(err), + fn: (cb: Function, err:any, val: string) => Promise.reject(err), rejects: true, when: 1, }, { name: 'immediately call cb(rejectedPromise)', - fn: (cb, err, val) => cb(Promise.reject(err)), + fn: (cb: Function, err:any, val: string) => cb(Promise.reject(err)), rejects: true, when: 1, }, { name: 'return unresolvedPromise', - fn: (cb, err, val) => new Promise((resolve) => process.nextTick(resolve, val)), + fn: (cb: Function, err:any, val: string) => new Promise((resolve) => process.nextTick(resolve, val)), when: 2, }, { name: 'immediately call cb(unresolvedPromise)', - fn: (cb, err, val) => cb(new Promise((resolve) => process.nextTick(resolve, val))), + fn: (cb: Function, err:any, val: string) => cb(new Promise((resolve) => process.nextTick(resolve, val))), when: 2, }, { name: 'return unrejectedPromise', - fn: (cb, err, val) => new Promise((resolve, reject) => process.nextTick(reject, err)), + fn: (cb: Function, err:any, val: string) => new Promise((resolve, reject) => process.nextTick(reject, err)), rejects: true, when: 2, }, { name: 'immediately call cb(unrejectedPromise)', - fn: (cb, err, val) => cb(new Promise((resolve, reject) => process.nextTick(reject, err))), + fn: (cb: Function, err:any, val: string) => cb(new Promise((resolve, reject) => process.nextTick(reject, err))), rejects: true, when: 2, }, { name: 'defer call to cb(value)', - fn: (cb, err, val) => { process.nextTick(cb, val); }, + fn: (cb: Function, err:any, val: string) => { process.nextTick(cb, val); }, when: 2, }, { name: 'defer call to cb(resolvedPromise)', - fn: (cb, err, val) => { process.nextTick(cb, Promise.resolve(val)); }, + fn: (cb: Function, err:any, val: string) => { process.nextTick(cb, Promise.resolve(val)); }, when: 2, }, { name: 'defer call to cb(rejectedPromise)', - fn: (cb, err, val) => { process.nextTick(cb, Promise.reject(err)); }, + fn: (cb: Function, err:any, val: string) => { process.nextTick(cb, Promise.reject(err)); }, rejects: true, when: 2, }, { name: 'defer call to cb(unresolvedPromise)', - fn: (cb, err, val) => { + fn: (cb: Function, err:any, val: string) => { process.nextTick(() => { cb(new Promise((resolve) => process.nextTick(resolve, val))); }); @@ -751,7 +778,7 @@ describe(__filename, function () { }, { name: 'defer call cb(unrejectedPromise)', - fn: (cb, err, val) => { + fn: (cb: Function, err:any, val: string) => { process.nextTick(() => { cb(new Promise((resolve, reject) => process.nextTick(reject, err))); }); @@ -766,7 +793,7 @@ describe(__filename, function () { if (step1.name.startsWith('return ') || step1.name === 'throw') continue; for (const step2 of behaviors) { it(`${step1.name} then ${step2.name} (diff. outcomes) -> log+throw`, async function () { - hook.hook_fn = (hn, ctx, cb) => { + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { step1.fn(cb, new Error(ctx.ret1), ctx.ret1); return step2.fn(cb, new Error(ctx.ret2), ctx.ret2); }; @@ -778,16 +805,16 @@ describe(__filename, function () { process.removeAllListeners(event); let tempListener; - let asyncErr; + let asyncErr: Error; try { - const seenErrPromise = new Promise((resolve) => { - tempListener = (err) => { + const seenErrPromise = new Promise((resolve) => { + tempListener = (err:any) => { assert.equal(asyncErr, undefined); asyncErr = err; resolve(); }; }); - process.on(event, tempListener); + process.on(event, tempListener!); const step1Wins = step1.when <= step2.when; const winningStep = step1Wins ? step1 : step2; const winningVal = step1Wins ? 'val1' : 'val2'; @@ -800,15 +827,16 @@ describe(__filename, function () { await seenErrPromise; } finally { // Restore the original listeners. - process.off(event, tempListener); + process.off(event, tempListener!); for (const listener of listenersBackup) { - process.on(event, listener); + process.on(event, listener as any); } } assert.equal(console.error.callCount, 1, `Got errors:\n${ console.error.getCalls().map((call) => call.args[0]).join('\n')}`); assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/); + // @ts-ignore assert(asyncErr instanceof Error); assert.match(asyncErr.message, /DOUBLE SETTLE BUG/); }); @@ -820,7 +848,7 @@ describe(__filename, function () { it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async function () { const err = new Error('val'); - hook.hook_fn = (hn, ctx, cb) => { + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { step1.fn(cb, err, 'val'); return step2.fn(cb, err, 'val'); }; @@ -846,12 +874,19 @@ describe(__filename, function () { it('calls all asynchronously, returns values in order', async function () { testHooks.length = 0; // Delete the boilerplate hook -- this test doesn't use it. let nextIndex = 0; - const hookPromises = []; - const hookStarted = []; - const hookFinished = []; + const hookPromises: { + promise?: Promise, + resolve?: Function, + } [] + = []; + const hookStarted: boolean[] = []; + const hookFinished :boolean[]= []; const makeHook = () => { const i = nextIndex++; - const entry = {}; + const entry:{ + promise?: Promise, + resolve?: Function, + } = {}; hookStarted[i] = false; hookFinished[i] = false; hookPromises[i] = entry; @@ -870,31 +905,31 @@ describe(__filename, function () { const p = hooks.aCallAll(hookName); assert.deepEqual(hookStarted, [true, true]); assert.deepEqual(hookFinished, [false, false]); - hookPromises[1].resolve(); + hookPromises[1].resolve!(); await hookPromises[1].promise; assert.deepEqual(hookFinished, [false, true]); - hookPromises[0].resolve(); + hookPromises[0].resolve!(); assert.deepEqual(await p, [0, 1]); }); it('passes hook name', async function () { - hook.hook_fn = async (hn) => { assert.equal(hn, hookName); }; + hook.hook_fn = async (hn:string) => { assert.equal(hn, hookName); }; await hooks.aCallAll(hookName); }); it('undefined context -> {}', async function () { - hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hook.hook_fn = async (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); }; await hooks.aCallAll(hookName); }); it('null context -> {}', async function () { - hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hook.hook_fn = async (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); }; await hooks.aCallAll(hookName, null); }); it('context unmodified', async function () { const wantContext = {}; - hook.hook_fn = async (hn, ctx) => { assert.equal(ctx, wantContext); }; + hook.hook_fn = async (hn: string, ctx: any) => { assert.equal(ctx, wantContext); }; await hooks.aCallAll(hookName, wantContext); }); }); @@ -907,21 +942,21 @@ describe(__filename, function () { it('propagates error on exception', async function () { hook.hook_fn = () => { throw new Error('test exception'); }; - await hooks.aCallAll(hookName, {}, (err) => { + await hooks.aCallAll(hookName, {}, (err:any) => { assert(err instanceof Error); assert.equal(err.message, 'test exception'); }); }); it('propagages null error on success', async function () { - await hooks.aCallAll(hookName, {}, (err) => { + await hooks.aCallAll(hookName, {}, (err:any) => { assert(err == null, `got non-null error: ${err}`); }); }); it('propagages results on success', async function () { hook.hook_fn = () => 'val'; - await hooks.aCallAll(hookName, {}, (err, results) => { + await hooks.aCallAll(hookName, {}, (err:any, results:any) => { assert.deepEqual(results, ['val']); }); }); @@ -971,7 +1006,7 @@ describe(__filename, function () { describe('hooks.callAllSerial', function () { describe('basic behavior', function () { it('calls all asynchronously, serially, in order', async function () { - const gotCalls = []; + const gotCalls:number[] = []; testHooks.length = 0; for (let i = 0; i < 3; i++) { const hook = makeHook(); @@ -993,23 +1028,23 @@ describe(__filename, function () { }); it('passes hook name', async function () { - hook.hook_fn = async (hn) => { assert.equal(hn, hookName); }; + hook.hook_fn = async (hn:string) => { assert.equal(hn, hookName); }; await hooks.callAllSerial(hookName); }); it('undefined context -> {}', async function () { - hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hook.hook_fn = async (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); }; await hooks.callAllSerial(hookName); }); it('null context -> {}', async function () { - hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hook.hook_fn = async (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); }; await hooks.callAllSerial(hookName, null); }); it('context unmodified', async function () { const wantContext = {}; - hook.hook_fn = async (hn, ctx) => { assert.equal(ctx, wantContext); }; + hook.hook_fn = async (hn: string, ctx: any) => { assert.equal(ctx, wantContext); }; await hooks.callAllSerial(hookName, wantContext); }); }); @@ -1063,28 +1098,28 @@ describe(__filename, function () { }); it('passes hook name => {}', async function () { - hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; + hook.hook_fn = (hn:string) => { assert.equal(hn, hookName); }; await hooks.aCallFirst(hookName); }); it('undefined context => {}', async function () { - hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hook.hook_fn = (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); }; await hooks.aCallFirst(hookName); }); it('null context => {}', async function () { - hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hook.hook_fn = (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); }; await hooks.aCallFirst(hookName, null); }); it('context unmodified', async function () { const wantContext = {}; - hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); }; + hook.hook_fn = (hn: string, ctx: any) => { assert.equal(ctx, wantContext); }; await hooks.aCallFirst(hookName, wantContext); }); it('default predicate: predicate never satisfied -> calls all in order', async function () { - const gotCalls = []; + const gotCalls:number[] = []; testHooks.length = 0; for (let i = 0; i < 3; i++) { const hook = makeHook(); @@ -1096,7 +1131,7 @@ describe(__filename, function () { }); it('calls hook functions serially', async function () { - const gotCalls = []; + const gotCalls: number[] = []; testHooks.length = 0; for (let i = 0; i < 3; i++) { const hook = makeHook(); @@ -1104,7 +1139,7 @@ describe(__filename, function () { gotCalls.push(i); // Check gotCalls asynchronously to ensure that the next hook function does not start // executing before this hook function has resolved. - return await new Promise((resolve) => { + return await new Promise((resolve) => { setImmediate(() => { assert.deepEqual(gotCalls, [...Array(i + 1).keys()]); resolve(); @@ -1145,7 +1180,7 @@ describe(__filename, function () { testHooks.length = 0; testHooks.push(makeHook(0), makeHook(1), makeHook(2)); let got = 0; - await hooks.aCallFirst(hookName, null, null, (val) => { ++got; return false; }); + await hooks.aCallFirst(hookName, null, null, (val:string) => { ++got; return false; }); assert.equal(got, 3); }); @@ -1153,7 +1188,7 @@ describe(__filename, function () { testHooks.length = 0; testHooks.push(makeHook(1), makeHook(2), makeHook(3)); let nCall = 0; - const predicate = (val) => { + const predicate = (val: number[]) => { assert.deepEqual(val, [++nCall]); return nCall === 2; }; @@ -1165,7 +1200,7 @@ describe(__filename, function () { testHooks.length = 0; testHooks.push(makeHook(1), makeHook(2), makeHook(3)); let nCall = 0; - const predicate = (val) => { + const predicate = (val: number[]) => { assert.deepEqual(val, [++nCall]); return nCall === 2 ? {} : null; }; @@ -1176,18 +1211,18 @@ describe(__filename, function () { it('custom predicate: array value passed unmodified to predicate', async function () { const want = [0]; hook.hook_fn = () => want; - const predicate = (got) => { assert.equal(got, want); }; // Note: *NOT* deepEqual! + const predicate = (got: []) => { assert.equal(got, want); }; // Note: *NOT* deepEqual! await hooks.aCallFirst(hookName, null, null, predicate); }); it('custom predicate: normalized value passed to predicate (undefined)', async function () { - const predicate = (got) => { assert.deepEqual(got, []); }; + const predicate = (got: []) => { assert.deepEqual(got, []); }; await hooks.aCallFirst(hookName, null, null, predicate); }); it('custom predicate: normalized value passed to predicate (null)', async function () { hook.hook_fn = () => null; - const predicate = (got) => { assert.deepEqual(got, [null]); }; + const predicate = (got: []) => { assert.deepEqual(got, [null]); }; await hooks.aCallFirst(hookName, null, null, predicate); }); @@ -1200,7 +1235,7 @@ describe(__filename, function () { it('value can be passed via callback', async function () { const want = {}; - hook.hook_fn = (hn, ctx, cb) => { cb(want); }; + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { cb(want); }; const got = await hooks.aCallFirst(hookName); assert.deepEqual(got, [want]); assert.equal(got[0], want); // Note: *NOT* deepEqual! diff --git a/src/tests/backend/specs/lowerCasePadIds.ts b/src/tests/backend/specs/lowerCasePadIds.ts new file mode 100644 index 000000000..c85d16c3f --- /dev/null +++ b/src/tests/backend/specs/lowerCasePadIds.ts @@ -0,0 +1,90 @@ +'use strict'; + +const assert = require('assert').strict; +const common = require('../common'); +const padManager = require('../../../node/db/PadManager'); +const settings = require('../../../node/utils/Settings'); + +describe(__filename, function () { + let agent:any; + const cleanUpPads = async () => { + const {padIDs} = await padManager.listAllPads(); + await Promise.all(padIDs.map(async (padId: string) => { + if (await padManager.doesPadExist(padId)) { + const pad = await padManager.getPad(padId); + await pad.remove(); + } + })); + }; + let backup:any; + + before(async function () { + backup = settings.lowerCasePadIds; + agent = await common.init(); + }); + beforeEach(async function () { + await cleanUpPads(); + }); + afterEach(async function () { + await cleanUpPads(); + }); + after(async function () { + settings.lowerCasePadIds = backup; + }); + + describe('not activated', function () { + beforeEach(async function () { + settings.lowerCasePadIds = false; + }); + + + it('do nothing', async function () { + await agent.get('/p/UPPERCASEpad') + .expect(200); + }); + }); + + describe('activated', function () { + beforeEach(async function () { + settings.lowerCasePadIds = true; + }); + it('lowercase pad ids', async function () { + await agent.get('/p/UPPERCASEpad') + .expect(302) + .expect('location', 'uppercasepad'); + }); + + it('keeps old pads accessible', async function () { + Object.assign(settings, { + lowerCasePadIds: false, + }); + await padManager.getPad('ALREADYexistingPad', 'oldpad'); + await padManager.getPad('alreadyexistingpad', 'newpad'); + Object.assign(settings, { + lowerCasePadIds: true, + }); + + const oldPad = await agent.get('/p/ALREADYexistingPad').expect(200); + const oldPadSocket = await common.connect(oldPad); + const oldPadHandshake = await common.handshake(oldPadSocket, 'ALREADYexistingPad'); + assert.equal(oldPadHandshake.data.padId, 'ALREADYexistingPad'); + assert.equal(oldPadHandshake.data.collab_client_vars.initialAttributedText.text, 'oldpad\n'); + + const newPad = await agent.get('/p/alreadyexistingpad').expect(200); + const newPadSocket = await common.connect(newPad); + const newPadHandshake = await common.handshake(newPadSocket, 'alreadyexistingpad'); + assert.equal(newPadHandshake.data.padId, 'alreadyexistingpad'); + assert.equal(newPadHandshake.data.collab_client_vars.initialAttributedText.text, 'newpad\n'); + }); + + it('disallow creation of different case pad-name via socket connection', async function () { + await padManager.getPad('maliciousattempt', 'attempt'); + + const newPad = await agent.get('/p/maliciousattempt').expect(200); + const newPadSocket = await common.connect(newPad); + const newPadHandshake = await common.handshake(newPadSocket, 'MaliciousAttempt'); + + assert.equal(newPadHandshake.data.collab_client_vars.initialAttributedText.text, 'attempt\n'); + }); + }); +}); diff --git a/src/tests/backend/specs/messages.ts b/src/tests/backend/specs/messages.ts new file mode 100644 index 000000000..9d91b2342 --- /dev/null +++ b/src/tests/backend/specs/messages.ts @@ -0,0 +1,258 @@ +'use strict'; + +import {PadType} from "../../../node/types/PadType"; +import {MapArrayType} from "../../../node/types/MapType"; + +const assert = require('assert').strict; +const common = require('../common'); +const padManager = require('../../../node/db/PadManager'); +const plugins = require('../../../static/js/pluginfw/plugin_defs'); +const readOnlyManager = require('../../../node/db/ReadOnlyManager'); + +describe(__filename, function () { + let agent:any; + let pad:PadType|null; + let padId: string; + let roPadId: string; + let rev: number; + let socket: any; + let roSocket: any; + const backups:MapArrayType = {}; + + before(async function () { + agent = await common.init(); + }); + + beforeEach(async function () { + backups.hooks = {handleMessageSecurity: plugins.hooks.handleMessageSecurity}; + plugins.hooks.handleMessageSecurity = []; + padId = common.randomString(); + assert(!await padManager.doesPadExist(padId)); + pad = await padManager.getPad(padId, 'dummy text\n'); + await pad!.setText('\n'); // Make sure the pad is created. + assert.equal(pad!.text(), '\n'); + let res = await agent.get(`/p/${padId}`).expect(200); + socket = await common.connect(res); + const {type, data: clientVars} = await common.handshake(socket, padId); + assert.equal(type, 'CLIENT_VARS'); + rev = clientVars.collab_client_vars.rev; + + roPadId = await readOnlyManager.getReadOnlyId(padId); + res = await agent.get(`/p/${roPadId}`).expect(200); + roSocket = await common.connect(res); + await common.handshake(roSocket, roPadId); + await new Promise(resolve => setTimeout(resolve, 1000)); + }); + + afterEach(async function () { + Object.assign(plugins.hooks, backups.hooks); + if (socket != null) socket.close(); + socket = null; + if (roSocket != null) roSocket.close(); + roSocket = null; + if (pad != null) await pad.remove(); + pad = null; + }); + + describe('CHANGESET_REQ', function () { + it('users are unable to read changesets from other pads', async function () { + const otherPadId = `${padId}other`; + assert(!await padManager.doesPadExist(otherPadId)); + const otherPad = await padManager.getPad(otherPadId, 'other text\n'); + try { + await otherPad.setText('other text\n'); + const resP = common.waitForSocketEvent(roSocket, 'message'); + await common.sendMessage(roSocket, { + component: 'pad', + padId: otherPadId, // The server should ignore this. + type: 'CHANGESET_REQ', + data: { + granularity: 1, + start: 0, + requestID: 'requestId', + }, + }); + const res = await resP; + assert.equal(res.type, 'CHANGESET_REQ'); + assert.equal(res.data.requestID, 'requestId'); + // Should match padId's text, not otherPadId's text. + assert.match(res.data.forwardsChangesets[0], /^[^$]*\$dummy text\n/); + } finally { + await otherPad.remove(); + } + }); + + it('CHANGESET_REQ: verify revNum is a number (regression)', async function () { + const otherPadId = `${padId}other`; + assert(!await padManager.doesPadExist(otherPadId)); + const otherPad = await padManager.getPad(otherPadId, 'other text\n'); + let errorCatched = 0; + try { + await otherPad.setText('other text\n'); + await common.sendMessage(roSocket, { + component: 'pad', + padId: otherPadId, // The server should ignore this. + type: 'CHANGESET_REQ', + data: { + granularity: 1, + start: 'test123', + requestID: 'requestId', + }, + }); + assert.equal('This code should never run', 1); + } + catch(e:any) { + assert.match(e.message, /rev is not a number/); + errorCatched = 1; + } + finally { + await otherPad.remove(); + assert.equal(errorCatched, 1); + } + }); + + it('CHANGESET_REQ: revNum is converted to number if possible (regression)', async function () { + const otherPadId = `${padId}other`; + assert(!await padManager.doesPadExist(otherPadId)); + const otherPad = await padManager.getPad(otherPadId, 'other text\n'); + try { + await otherPad.setText('other text\n'); + const resP = common.waitForSocketEvent(roSocket, 'message'); + await common.sendMessage(roSocket, { + component: 'pad', + padId: otherPadId, // The server should ignore this. + type: 'CHANGESET_REQ', + data: { + granularity: 1, + start: '1test123', + requestID: 'requestId', + }, + }); + const res = await resP; + assert.equal(res.type, 'CHANGESET_REQ'); + assert.equal(res.data.requestID, 'requestId'); + assert.equal(res.data.start, 1); + } + finally { + await otherPad.remove(); + } + }); + + it('CHANGESET_REQ: revNum 2 is converted to head rev 1 (regression)', async function () { + const otherPadId = `${padId}other`; + assert(!await padManager.doesPadExist(otherPadId)); + const otherPad = await padManager.getPad(otherPadId, 'other text\n'); + try { + await otherPad.setText('other text\n'); + const resP = common.waitForSocketEvent(roSocket, 'message'); + await common.sendMessage(roSocket, { + component: 'pad', + padId: otherPadId, // The server should ignore this. + type: 'CHANGESET_REQ', + data: { + granularity: 1, + start: '2', + requestID: 'requestId', + }, + }); + const res = await resP; + assert.equal(res.type, 'CHANGESET_REQ'); + assert.equal(res.data.requestID, 'requestId'); + assert.equal(res.data.start, 1); + } + finally { + await otherPad.remove(); + } + }); + }); + + describe('USER_CHANGES', function () { + const sendUserChanges = + async (socket:any, cs:any) => await common.sendUserChanges(socket, {baseRev: rev, changeset: cs}); + const assertAccepted = async (socket:any, wantRev: number) => { + await common.waitForAcceptCommit(socket, wantRev); + rev = wantRev; + }; + const assertRejected = async (socket:any) => { + const msg = await common.waitForSocketEvent(socket, 'message'); + assert.deepEqual(msg, {disconnect: 'badChangeset'}); + }; + + it('changes are applied', async function () { + await Promise.all([ + assertAccepted(socket, rev + 1), + sendUserChanges(socket, 'Z:1>5+5$hello'), + ]); + assert.equal(pad!.text(), 'hello\n'); + }); + + it('bad changeset is rejected', async function () { + await Promise.all([ + assertRejected(socket), + sendUserChanges(socket, 'this is not a valid changeset'), + ]); + }); + + it('retransmission is accepted, has no effect', async function () { + const cs = 'Z:1>5+5$hello'; + await Promise.all([ + assertAccepted(socket, rev + 1), + sendUserChanges(socket, cs), + ]); + --rev; + await Promise.all([ + assertAccepted(socket, rev + 1), + sendUserChanges(socket, cs), + ]); + assert.equal(pad!.text(), 'hello\n'); + }); + + it('identity changeset is accepted, has no effect', async function () { + await Promise.all([ + assertAccepted(socket, rev + 1), + sendUserChanges(socket, 'Z:1>5+5$hello'), + ]); + await Promise.all([ + assertAccepted(socket, rev), + sendUserChanges(socket, 'Z:6>0$'), + ]); + assert.equal(pad!.text(), 'hello\n'); + }); + + it('non-identity changeset with no net change is accepted, has no effect', async function () { + await Promise.all([ + assertAccepted(socket, rev + 1), + sendUserChanges(socket, 'Z:1>5+5$hello'), + ]); + await Promise.all([ + assertAccepted(socket, rev), + sendUserChanges(socket, 'Z:6>0-5+5$hello'), + ]); + assert.equal(pad!.text(), 'hello\n'); + }); + + it('handleMessageSecurity can grant one-time write access', async function () { + const cs = 'Z:1>5+5$hello'; + const errRegEx = /write attempt on read-only pad/; + // First try to send a change and verify that it was dropped. + await assert.rejects(sendUserChanges(roSocket, cs), errRegEx); + // sendUserChanges() waits for message ack, so if the message was accepted then head should + // have already incremented by the time we get here. + assert.equal(pad!.head, rev); // Not incremented. + + // Now allow the change. + plugins.hooks.handleMessageSecurity.push({hook_fn: () => 'permitOnce'}); + await Promise.all([ + assertAccepted(roSocket, rev + 1), + sendUserChanges(roSocket, cs), + ]); + assert.equal(pad!.text(), 'hello\n'); + + // The next change should be dropped. + plugins.hooks.handleMessageSecurity = []; + await assert.rejects(sendUserChanges(roSocket, 'Z:6>6=5+6$ world'), errRegEx); + assert.equal(pad!.head, rev); // Not incremented. + assert.equal(pad!.text(), 'hello\n'); + }); + }); +}); diff --git a/src/tests/backend/specs/pads-with-spaces.js b/src/tests/backend/specs/pads-with-spaces.ts similarity index 91% rename from src/tests/backend/specs/pads-with-spaces.js rename to src/tests/backend/specs/pads-with-spaces.ts index 0db99865b..cfadca1b9 100644 --- a/src/tests/backend/specs/pads-with-spaces.js +++ b/src/tests/backend/specs/pads-with-spaces.ts @@ -1,9 +1,8 @@ 'use strict'; const common = require('../common'); -const assert = require('../assert-legacy').strict; -let agent; +let agent:any; describe(__filename, function () { before(async function () { diff --git a/src/tests/backend/specs/regression-db.js b/src/tests/backend/specs/regression-db.ts similarity index 78% rename from src/tests/backend/specs/regression-db.js rename to src/tests/backend/specs/regression-db.ts index 388b8346a..ba50e5240 100644 --- a/src/tests/backend/specs/regression-db.js +++ b/src/tests/backend/specs/regression-db.ts @@ -1,20 +1,20 @@ 'use strict'; const AuthorManager = require('../../../node/db/AuthorManager'); -const assert = require('assert').strict; +import {strict as assert} from "assert"; const common = require('../common'); const db = require('../../../node/db/DB'); describe(__filename, function () { - let setBackup; + let setBackup: Function; before(async function () { await common.init(); setBackup = db.set; - db.set = async (...args) => { + db.set = async (...args:any) => { // delay db.set - await new Promise((resolve) => { setTimeout(() => resolve(), 500); }); + await new Promise((resolve) => { setTimeout(() => resolve(), 500); }); return await setBackup.call(db, ...args); }; }); diff --git a/src/tests/backend/specs/settings.js b/src/tests/backend/specs/settings.ts similarity index 62% rename from src/tests/backend/specs/settings.js rename to src/tests/backend/specs/settings.ts index e737f4f34..d6dcaf71a 100644 --- a/src/tests/backend/specs/settings.js +++ b/src/tests/backend/specs/settings.ts @@ -2,12 +2,12 @@ const assert = require('assert').strict; const {parseSettings} = require('../../../node/utils/Settings').exportedForTestingOnly; -const path = require('path'); -const process = require('process'); +import path from 'path'; +import process from 'process'; describe(__filename, function () { describe('parseSettings', function () { - let settings; + let settings: any; const envVarSubstTestCases = [ {name: 'true', val: 'true', var: 'SET_VAR_TRUE', want: true}, {name: 'false', val: 'false', var: 'SET_VAR_FALSE', want: false}, @@ -58,4 +58,35 @@ describe(__filename, function () { }); }); }); + + + describe("Parse plugin settings", function () { + + before(async function () { + process.env["EP__ADMIN__PASSWORD"] = "test" + }) + + it('should parse plugin settings', async function () { + let settings = parseSettings(path.join(__dirname, 'settings.json'), true); + assert.equal(settings.ADMIN.PASSWORD, "test"); + }) + + it('should bundle settings with same path', async function () { + process.env["EP__ADMIN__USERNAME"] = "test" + let settings = parseSettings(path.join(__dirname, 'settings.json'), true); + assert.deepEqual(settings.ADMIN, {PASSWORD: "test", USERNAME: "test"}); + }) + + it("Can set the ep themes", async function () { + process.env["EP__ep_themes__default_theme"] = "hacker" + let settings = parseSettings(path.join(__dirname, 'settings.json'), true); + assert.deepEqual(settings.ep_themes, {"default_theme": "hacker"}); + }) + + it("can set the ep_webrtc settings", async function () { + process.env["EP__ep_webrtc__enabled"] = "true" + let settings = parseSettings(path.join(__dirname, 'settings.json'), true); + assert.deepEqual(settings.ep_webrtc, {"enabled": true}); + }) + }) }); diff --git a/src/tests/backend/specs/socketio.js b/src/tests/backend/specs/socketio.ts similarity index 85% rename from src/tests/backend/specs/socketio.js rename to src/tests/backend/specs/socketio.ts index 15f561774..cde554e5e 100644 --- a/src/tests/backend/specs/socketio.js +++ b/src/tests/backend/specs/socketio.ts @@ -1,5 +1,7 @@ 'use strict'; +import {MapArrayType} from "../../../node/types/MapType"; + const assert = require('assert').strict; const common = require('../common'); const padManager = require('../../../node/db/PadManager'); @@ -10,9 +12,9 @@ const socketIoRouter = require('../../../node/handler/SocketIORouter'); describe(__filename, function () { this.timeout(30000); - let agent; - let authorize; - const backups = {}; + let agent: any; + let authorize:Function; + const backups:MapArrayType = {}; const cleanUpPads = async () => { const padIds = ['pad', 'other-pad', 'päd']; await Promise.all(padIds.map(async (padId) => { @@ -22,7 +24,7 @@ describe(__filename, function () { } })); }; - let socket; + let socket:any; before(async function () { agent = await common.init(); }); beforeEach(async function () { @@ -44,7 +46,7 @@ describe(__filename, function () { }; assert(socket == null); authorize = () => true; - plugins.hooks.authorize = [{hook_fn: (hookName, {req}, cb) => cb([authorize(req)])}]; + plugins.hooks.authorize = [{hook_fn: (hookName: string, {req}:any, cb:Function) => cb([authorize(req)])}]; await cleanUpPads(); }); afterEach(async function () { @@ -84,7 +86,7 @@ describe(__filename, function () { for (const authn of [false, true]) { const desc = authn ? 'authn user' : '!authn anonymous'; it(`${desc} read-only /p/pad -> 200, ok`, async function () { - const get = (ep) => { + const get = (ep: string) => { let res = agent.get(ep); if (authn) res = res.auth('user', 'user-password'); return res.expect(200); @@ -163,7 +165,9 @@ describe(__filename, function () { }); it('authorization bypass attempt -> error', async function () { // Only allowed to access /p/pad. - authorize = (req) => req.path === '/p/pad'; + authorize = (req:{ + path: string, + }) => req.path === '/p/pad'; settings.requireAuthentication = true; settings.requireAuthorization = true; // First authenticate and establish a session. @@ -321,45 +325,46 @@ describe(__filename, function () { describe('SocketIORouter.js', function () { const Module = class { - setSocketIO(io) {} - handleConnect(socket) {} - handleDisconnect(socket) {} - handleMessage(socket, message) {} + setSocketIO(io:any) {} + handleConnect(socket:any) {} + handleDisconnect(socket:any) {} + handleMessage(socket:any, message:string) {} }; afterEach(async function () { - socketIoRouter.deleteComponent(this.test.fullTitle()); - socketIoRouter.deleteComponent(`${this.test.fullTitle()} #2`); + socketIoRouter.deleteComponent(this.test!.fullTitle()); + socketIoRouter.deleteComponent(`${this.test!.fullTitle()} #2`); }); it('setSocketIO', async function () { let ioServer; - socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { - setSocketIO(io) { ioServer = io; } + socketIoRouter.addComponent(this.test!.fullTitle(), new class extends Module { + setSocketIO(io:any) { ioServer = io; } }()); assert(ioServer != null); }); it('handleConnect', async function () { let serverSocket; - socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { - handleConnect(socket) { serverSocket = socket; } + socketIoRouter.addComponent(this.test!.fullTitle(), new class extends Module { + handleConnect(socket:any) { serverSocket = socket; } }()); socket = await common.connect(); assert(serverSocket != null); }); it('handleDisconnect', async function () { - let resolveConnected; + let resolveConnected: (value: void | PromiseLike) => void ; const connected = new Promise((resolve) => resolveConnected = resolve); - let resolveDisconnected; - const disconnected = new Promise((resolve) => resolveDisconnected = resolve); - socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { - handleConnect(socket) { + let resolveDisconnected: (value: void | PromiseLike) => void ; + const disconnected = new Promise((resolve) => resolveDisconnected = resolve); + socketIoRouter.addComponent(this.test!.fullTitle(), new class extends Module { + private _socket: any; + handleConnect(socket:any) { this._socket = socket; resolveConnected(); } - handleDisconnect(socket) { + handleDisconnect(socket:any) { assert(socket != null); // There might be lingering disconnect events from sockets created by other tests. if (this._socket == null || socket.id !== this._socket.id) return; @@ -375,40 +380,43 @@ describe(__filename, function () { }); it('handleMessage (success)', async function () { - let serverSocket; + let serverSocket:any; const want = { - component: this.test.fullTitle(), + component: this.test!.fullTitle(), foo: {bar: 'asdf'}, }; - let rx; + let rx:Function; const got = new Promise((resolve) => { rx = resolve; }); - socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { - handleConnect(socket) { serverSocket = socket; } - handleMessage(socket, message) { assert.equal(socket, serverSocket); rx(message); } + socketIoRouter.addComponent(this.test!.fullTitle(), new class extends Module { + handleConnect(socket:any) { serverSocket = socket; } + handleMessage(socket:any, message:string) { assert.equal(socket, serverSocket); rx(message); } }()); - socketIoRouter.addComponent(`${this.test.fullTitle()} #2`, new class extends Module { - handleMessage(socket, message) { assert.fail('wrong handler called'); } + socketIoRouter.addComponent(`${this.test!.fullTitle()} #2`, new class extends Module { + handleMessage(socket:any, message:any) { assert.fail('wrong handler called'); } }()); socket = await common.connect(); - socket.send(want); + socket.emit('message', want); assert.deepEqual(await got, want); }); - const tx = async (socket, message = {}) => await new Promise((resolve, reject) => { + const tx = async (socket:any, message = {}) => await new Promise((resolve, reject) => { const AckErr = class extends Error { - constructor(name, ...args) { super(...args); this.name = name; } + constructor(name: string, ...args:any) { super(...args); this.name = name; } }; - socket.send(message, - (errj, val) => errj != null ? reject(new AckErr(errj.name, errj.message)) : resolve(val)); + socket.emit('message', message, + (errj: { + message: string, + name: string, + }, val: any) => errj != null ? reject(new AckErr(errj.name, errj.message)) : resolve(val)); }); it('handleMessage with ack (success)', async function () { const want = 'value'; - socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { - handleMessage(socket, msg) { return want; } + socketIoRouter.addComponent(this.test!.fullTitle(), new class extends Module { + handleMessage(socket:any, msg:any) { return want; } }()); socket = await common.connect(); - const got = await tx(socket, {component: this.test.fullTitle()}); + const got = await tx(socket, {component: this.test!.fullTitle()}); assert.equal(got, want); }); @@ -416,11 +424,11 @@ describe(__filename, function () { const InjectedError = class extends Error { constructor() { super('injected test error'); this.name = 'InjectedError'; } }; - socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { - handleMessage(socket, msg) { throw new InjectedError(); } + socketIoRouter.addComponent(this.test!.fullTitle(), new class extends Module { + handleMessage(socket:any, msg:any) { throw new InjectedError(); } }()); socket = await common.connect(); - await assert.rejects(tx(socket, {component: this.test.fullTitle()}), new InjectedError()); + await assert.rejects(tx(socket, {component: this.test!.fullTitle()}), new InjectedError()); }); }); }); diff --git a/src/tests/backend/specs/specialpages.js b/src/tests/backend/specs/specialpages.ts similarity index 86% rename from src/tests/backend/specs/specialpages.js rename to src/tests/backend/specs/specialpages.ts index 93c8b3bc4..fbb446c49 100644 --- a/src/tests/backend/specs/specialpages.js +++ b/src/tests/backend/specs/specialpages.ts @@ -1,12 +1,16 @@ 'use strict'; +import {MapArrayType} from "../../../node/types/MapType"; + const common = require('../common'); const settings = require('../../../node/utils/Settings'); + + describe(__filename, function () { this.timeout(30000); - let agent; - const backups = {}; + let agent:any; + const backups:MapArrayType = {}; before(async function () { agent = await common.init(); }); beforeEach(async function () { backups.settings = {}; diff --git a/src/tests/backend/specs/webaccess.js b/src/tests/backend/specs/webaccess.ts similarity index 81% rename from src/tests/backend/specs/webaccess.js rename to src/tests/backend/specs/webaccess.ts index 7594b57e3..96c2265fc 100644 --- a/src/tests/backend/specs/webaccess.js +++ b/src/tests/backend/specs/webaccess.ts @@ -1,5 +1,9 @@ 'use strict'; +import {MapArrayType} from "../../../node/types/MapType"; +import {Func} from "mocha"; +import {SettingsUser} from "../../../node/types/SettingsUser"; + const assert = require('assert').strict; const common = require('../common'); const plugins = require('../../../static/js/pluginfw/plugin_defs'); @@ -7,11 +11,11 @@ const settings = require('../../../node/utils/Settings'); describe(__filename, function () { this.timeout(30000); - let agent; - const backups = {}; + let agent:any; + const backups:MapArrayType = {}; const authHookNames = ['preAuthorize', 'authenticate', 'authorize']; const failHookNames = ['preAuthzFailure', 'authnFailure', 'authzFailure', 'authFailure']; - const makeHook = (hookName, hookFn) => ({ + const makeHook = (hookName: string, hookFn:Function) => ({ hook_fn: hookFn, hook_fn_name: `fake_plugin/${hookName}`, hook_name: hookName, @@ -19,6 +23,7 @@ describe(__filename, function () { }); before(async function () { agent = await common.init(); }); + beforeEach(async function () { backups.hooks = {}; for (const hookName of authHookNames.concat(failHookNames)) { @@ -34,8 +39,9 @@ describe(__filename, function () { settings.users = { admin: {password: 'admin-password', is_admin: true}, user: {password: 'user-password'}, - }; + } satisfies SettingsUser; }); + afterEach(async function () { Object.assign(plugins.hooks, backups.hooks); Object.assign(settings, backups.settings); @@ -47,55 +53,71 @@ describe(__filename, function () { settings.requireAuthorization = false; await agent.get('/').expect(200); }); - it('!authn !authz anonymous /admin/ -> 401', async function () { + + it('!authn !authz anonymous /admin-auth// -> 401', async function () { settings.requireAuthentication = false; settings.requireAuthorization = false; - await agent.get('/admin/').expect(401); + await agent.get('/admin-auth/').expect(401); }); + it('authn !authz anonymous / -> 401', async function () { settings.requireAuthentication = true; settings.requireAuthorization = false; await agent.get('/').expect(401); }); + it('authn !authz user / -> 200', async function () { settings.requireAuthentication = true; settings.requireAuthorization = false; await agent.get('/').auth('user', 'user-password').expect(200); }); - it('authn !authz user /admin/ -> 403', async function () { + + it('authn !authz user //admin-auth// -> 403', async function () { settings.requireAuthentication = true; settings.requireAuthorization = false; - await agent.get('/admin/').auth('user', 'user-password').expect(403); + await agent.get('/admin-auth//').auth('user', 'user-password').expect(403); }); + it('authn !authz admin / -> 200', async function () { settings.requireAuthentication = true; settings.requireAuthorization = false; await agent.get('/').auth('admin', 'admin-password').expect(200); }); - it('authn !authz admin /admin/ -> 200', async function () { + + it('authn !authz admin /admin-auth/ -> 200', async function () { settings.requireAuthentication = true; settings.requireAuthorization = false; - await agent.get('/admin/').auth('admin', 'admin-password').expect(200); + await agent.get('/admin-auth/').auth('admin', 'admin-password').expect(200); }); + + it('authn authz anonymous /robots.txt -> 200', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + await agent.get('/robots.txt').expect(200); + }); + it('authn authz user / -> 403', async function () { settings.requireAuthentication = true; settings.requireAuthorization = true; await agent.get('/').auth('user', 'user-password').expect(403); }); - it('authn authz user /admin/ -> 403', async function () { + + it('authn authz user //admin-auth// -> 403', async function () { settings.requireAuthentication = true; settings.requireAuthorization = true; - await agent.get('/admin/').auth('user', 'user-password').expect(403); + await agent.get('/admin-auth//').auth('user', 'user-password').expect(403); }); + it('authn authz admin / -> 200', async function () { settings.requireAuthentication = true; settings.requireAuthorization = true; await agent.get('/').auth('admin', 'admin-password').expect(200); }); - it('authn authz admin /admin/ -> 200', async function () { + + it('authn authz admin /admin-auth/ -> 200', async function () { settings.requireAuthentication = true; settings.requireAuthorization = true; - await agent.get('/admin/').auth('admin', 'admin-password').expect(200); + await agent.get('/admin-auth/').auth('admin', 'admin-password').expect(200); }); describe('login fails if password is nullish', function () { @@ -108,7 +130,7 @@ describe(__filename, function () { it(`admin password: ${adminPassword} credentials: ${creds}`, async function () { settings.users.admin.password = adminPassword; const encCreds = Buffer.from(creds).toString('base64'); - await agent.get('/admin/').set('Authorization', `Basic ${encCreds}`).expect(401); + await agent.get('/admin-auth/').set('Authorization', `Basic ${encCreds}`).expect(401); }); } } @@ -116,16 +138,21 @@ describe(__filename, function () { }); describe('webaccess: preAuthorize, authenticate, and authorize hooks', function () { - let callOrder; + let callOrder:string[]; const Handler = class { - constructor(hookName, suffix) { + private called: boolean; + private readonly hookName: string; + private readonly innerHandle: Function; + private readonly id: string; + private readonly checkContext: Function; + constructor(hookName:string, suffix: string) { this.called = false; this.hookName = hookName; this.innerHandle = () => []; this.id = hookName + suffix; this.checkContext = () => {}; } - handle(hookName, context, cb) { + handle(hookName: string, context: any, cb:Function) { assert.equal(hookName, this.hookName); assert(context != null); assert(context.req != null); @@ -135,10 +162,10 @@ describe(__filename, function () { assert(!this.called); this.called = true; callOrder.push(this.id); - return cb(this.innerHandle(context.req)); + return cb(this.innerHandle(context)); } }; - const handlers = {}; + const handlers:MapArrayType = {}; beforeEach(async function () { callOrder = []; @@ -165,6 +192,7 @@ describe(__filename, function () { // Note: The preAuthorize hook always runs even if requireAuthorization is false. assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']); }); + it('bypasses authenticate and authorize hooks when true is returned', async function () { settings.requireAuthentication = true; settings.requireAuthorization = true; @@ -172,6 +200,7 @@ describe(__filename, function () { await agent.get('/').expect(200); assert.deepEqual(callOrder, ['preAuthorize_0']); }); + it('bypasses authenticate and authorize hooks when false is returned', async function () { settings.requireAuthentication = true; settings.requireAuthorization = true; @@ -179,34 +208,48 @@ describe(__filename, function () { await agent.get('/').expect(403); assert.deepEqual(callOrder, ['preAuthorize_0']); }); - it('bypasses authenticate and authorize hooks for static content, defers', async function () { + + it('bypasses authenticate and authorize hooks when next is called', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + handlers.preAuthorize[0].innerHandle = ({next}:{ + next: Function + }) => next(); + await agent.get('/').expect(200); + assert.deepEqual(callOrder, ['preAuthorize_0']); + }); + + it('static content (expressPreSession) bypasses all auth checks', async function () { settings.requireAuthentication = true; settings.requireAuthorization = true; await agent.get('/static/robots.txt').expect(200); - assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']); + assert.deepEqual(callOrder, []); }); + it('cannot grant access to /admin', async function () { handlers.preAuthorize[0].innerHandle = () => [true]; - await agent.get('/admin/').expect(401); + await agent.get('/admin-auth/').expect(401); // Notes: // * preAuthorize[1] is called despite preAuthorize[0] returning a non-empty list because - // 'true' entries are ignored for /admin/* requests. - // * The authenticate hook always runs for /admin/* requests even if + // 'true' entries are ignored for /admin-auth//* requests. + // * The authenticate hook always runs for /admin-auth//* requests even if // settings.requireAuthentication is false. assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0', 'authenticate_1']); }); - it('can deny access to /admin', async function () { + + it('can deny access to /admin-auth/', async function () { handlers.preAuthorize[0].innerHandle = () => [false]; - await agent.get('/admin/').auth('admin', 'admin-password').expect(403); + await agent.get('/admin-auth/').auth('admin', 'admin-password').expect(403); assert.deepEqual(callOrder, ['preAuthorize_0']); }); + it('runs preAuthzFailure hook when access is denied', async function () { handlers.preAuthorize[0].innerHandle = () => [false]; let called = false; - plugins.hooks.preAuthzFailure = [makeHook('preAuthzFailure', (hookName, {req, res}, cb) => { + plugins.hooks.preAuthzFailure = [makeHook('preAuthzFailure', (hookName: string, {req, res}:any, cb:Function) => { assert.equal(hookName, 'preAuthzFailure'); assert(req != null); assert(res != null); @@ -215,9 +258,10 @@ describe(__filename, function () { res.status(200).send('injected'); return cb([true]); })]; - await agent.get('/admin/').auth('admin', 'admin-password').expect(200, 'injected'); + await agent.get('/admin-auth//').auth('admin', 'admin-password').expect(200, 'injected'); assert(called); }); + it('returns 500 if an exception is thrown', async function () { handlers.preAuthorize[0].innerHandle = () => { throw new Error('exception test'); }; await agent.get('/').expect(500); @@ -230,19 +274,21 @@ describe(__filename, function () { settings.requireAuthorization = false; }); - it('is not called if !requireAuthentication and not /admin/*', async function () { + it('is not called if !requireAuthentication and not /admin-auth/*', async function () { settings.requireAuthentication = false; await agent.get('/').expect(200); assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']); }); - it('is called if !requireAuthentication and /admin/*', async function () { + + it('is called if !requireAuthentication and /admin-auth//*', async function () { settings.requireAuthentication = false; - await agent.get('/admin/').expect(401); + await agent.get('/admin-auth/').expect(401); assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0', 'authenticate_1']); }); + it('defers if empty list returned', async function () { await agent.get('/').expect(401); assert.deepEqual(callOrder, ['preAuthorize_0', @@ -250,18 +296,21 @@ describe(__filename, function () { 'authenticate_0', 'authenticate_1']); }); + it('does not defer if return [true], 200', async function () { - handlers.authenticate[0].innerHandle = (req) => { req.session.user = {}; return [true]; }; + handlers.authenticate[0].innerHandle = ({req}:any) => { req.session.user = {}; return [true]; }; await agent.get('/').expect(200); // Note: authenticate_1 was not called because authenticate_0 handled it. assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); }); + it('does not defer if return [false], 401', async function () { - handlers.authenticate[0].innerHandle = (req) => [false]; + handlers.authenticate[0].innerHandle = () => [false]; await agent.get('/').expect(401); // Note: authenticate_1 was not called because authenticate_0 handled it. assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); }); + it('falls back to HTTP basic auth', async function () { await agent.get('/').auth('user', 'user-password').expect(200); assert.deepEqual(callOrder, ['preAuthorize_0', @@ -269,8 +318,11 @@ describe(__filename, function () { 'authenticate_0', 'authenticate_1']); }); + it('passes settings.users in context', async function () { - handlers.authenticate[0].checkContext = ({users}) => { + handlers.authenticate[0].checkContext = ({users}:{ + users: SettingsUser + }) => { assert.equal(users, settings.users); }; await agent.get('/').expect(401); @@ -279,8 +331,13 @@ describe(__filename, function () { 'authenticate_0', 'authenticate_1']); }); + it('passes user, password in context if provided', async function () { - handlers.authenticate[0].checkContext = ({username, password}) => { + handlers.authenticate[0].checkContext = ({username, password}:{ + username: string, + password: string + + }) => { assert.equal(username, 'user'); assert.equal(password, 'user-password'); }; @@ -290,8 +347,12 @@ describe(__filename, function () { 'authenticate_0', 'authenticate_1']); }); + it('does not pass user, password in context if not provided', async function () { - handlers.authenticate[0].checkContext = ({username, password}) => { + handlers.authenticate[0].checkContext = ({username, password}:{ + username: string, + password: string + }) => { assert(username == null); assert(password == null); }; @@ -301,11 +362,13 @@ describe(__filename, function () { 'authenticate_0', 'authenticate_1']); }); + it('errors if req.session.user is not created', async function () { handlers.authenticate[0].innerHandle = () => [true]; await agent.get('/').expect(500); assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); }); + it('returns 500 if an exception is thrown', async function () { handlers.authenticate[0].innerHandle = () => { throw new Error('exception test'); }; await agent.get('/').expect(500); @@ -327,14 +390,16 @@ describe(__filename, function () { 'authenticate_0', 'authenticate_1']); }); + it('is not called if !requireAuthorization (/admin)', async function () { settings.requireAuthorization = false; - await agent.get('/admin/').auth('admin', 'admin-password').expect(200); + await agent.get('/admin-auth/').auth('admin', 'admin-password').expect(200); assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0', 'authenticate_1']); }); + it('defers if empty list returned', async function () { await agent.get('/').auth('user', 'user-password').expect(403); assert.deepEqual(callOrder, ['preAuthorize_0', @@ -344,6 +409,7 @@ describe(__filename, function () { 'authorize_0', 'authorize_1']); }); + it('does not defer if return [true], 200', async function () { handlers.authorize[0].innerHandle = () => [true]; await agent.get('/').auth('user', 'user-password').expect(200); @@ -354,8 +420,9 @@ describe(__filename, function () { 'authenticate_1', 'authorize_0']); }); + it('does not defer if return [false], 403', async function () { - handlers.authorize[0].innerHandle = (req) => [false]; + handlers.authorize[0].innerHandle = () => [false]; await agent.get('/').auth('user', 'user-password').expect(403); // Note: authorize_1 was not called because authorize_0 handled it. assert.deepEqual(callOrder, ['preAuthorize_0', @@ -364,8 +431,11 @@ describe(__filename, function () { 'authenticate_1', 'authorize_0']); }); + it('passes req.path in context', async function () { - handlers.authorize[0].checkContext = ({resource}) => { + handlers.authorize[0].checkContext = ({resource}:{ + resource: string + }) => { assert.equal(resource, '/'); }; await agent.get('/').auth('user', 'user-password').expect(403); @@ -376,6 +446,7 @@ describe(__filename, function () { 'authorize_0', 'authorize_1']); }); + it('returns 500 if an exception is thrown', async function () { handlers.authorize[0].innerHandle = () => { throw new Error('exception test'); }; await agent.get('/').auth('user', 'user-password').expect(500); @@ -390,12 +461,15 @@ describe(__filename, function () { describe('webaccess: authnFailure, authzFailure, authFailure hooks', function () { const Handler = class { - constructor(hookName) { + private hookName: string; + private shouldHandle: boolean; + private called: boolean; + constructor(hookName: string) { this.hookName = hookName; this.shouldHandle = false; this.called = false; } - handle(hookName, context, cb) { + handle(hookName: string, context:any, cb: Function) { assert.equal(hookName, this.hookName); assert(context != null); assert(context.req != null); @@ -409,7 +483,7 @@ describe(__filename, function () { return cb([]); } }; - const handlers = {}; + const handlers:MapArrayType = {}; beforeEach(async function () { failHookNames.forEach((hookName) => { @@ -428,6 +502,7 @@ describe(__filename, function () { assert(!handlers.authzFailure.called); assert(handlers.authFailure.called); }); + it('authn fail, authnFailure handles', async function () { handlers.authnFailure.shouldHandle = true; await agent.get('/').expect(200, 'authnFailure'); @@ -435,6 +510,7 @@ describe(__filename, function () { assert(!handlers.authzFailure.called); assert(!handlers.authFailure.called); }); + it('authn fail, authFailure handles', async function () { handlers.authFailure.shouldHandle = true; await agent.get('/').expect(200, 'authFailure'); @@ -442,6 +518,7 @@ describe(__filename, function () { assert(!handlers.authzFailure.called); assert(handlers.authFailure.called); }); + it('authnFailure trumps authFailure', async function () { handlers.authnFailure.shouldHandle = true; handlers.authFailure.shouldHandle = true; @@ -457,6 +534,7 @@ describe(__filename, function () { assert(handlers.authzFailure.called); assert(handlers.authFailure.called); }); + it('authz fail, authzFailure handles', async function () { handlers.authzFailure.shouldHandle = true; await agent.get('/').auth('user', 'user-password').expect(200, 'authzFailure'); @@ -464,6 +542,7 @@ describe(__filename, function () { assert(handlers.authzFailure.called); assert(!handlers.authFailure.called); }); + it('authz fail, authFailure handles', async function () { handlers.authFailure.shouldHandle = true; await agent.get('/').auth('user', 'user-password').expect(200, 'authFailure'); @@ -471,6 +550,7 @@ describe(__filename, function () { assert(handlers.authzFailure.called); assert(handlers.authFailure.called); }); + it('authzFailure trumps authFailure', async function () { handlers.authzFailure.shouldHandle = true; handlers.authFailure.shouldHandle = true; diff --git a/src/tests/container/specs/api/pad.js b/src/tests/container/specs/api/pad.js index 04067f0e3..f6ff8ebf5 100644 --- a/src/tests/container/specs/api/pad.js +++ b/src/tests/container/specs/api/pad.js @@ -31,8 +31,8 @@ describe('API Versioning', function () { }); describe('Permission', function () { - it('errors with invalid APIKey', function (done) { - api.get(`/api/${apiVersion}/createPad?apikey=wrong_password&padID=test`) + it('errors with invalid OAuth token', function (done) { + api.get(`/api/${apiVersion}/createPad?padID=test`) .expect(401, done); }); }); diff --git a/src/tests/frontend-new/admin-spec/adminsettings.spec.ts b/src/tests/frontend-new/admin-spec/adminsettings.spec.ts new file mode 100644 index 000000000..4c28874dc --- /dev/null +++ b/src/tests/frontend-new/admin-spec/adminsettings.spec.ts @@ -0,0 +1,60 @@ +import {expect, test} from "@playwright/test"; +import {loginToAdmin, restartEtherpad, saveSettings} from "../helper/adminhelper"; + +test.beforeEach(async ({ page })=>{ + await loginToAdmin(page, 'admin', 'changeme1'); +}) + +test.describe('admin settings',()=> { + + + test('Are Settings visible, populated, does save work', async ({page}) => { + await page.goto('http://localhost:9001/admin/settings'); + await page.waitForSelector('.settings'); + const settings = page.locator('.settings'); + await expect(settings).not.toBeEmpty(); + + const settingsVal = await settings.inputValue() + const settingsLength = settingsVal.length + + await settings.fill(`{"title": "Etherpad123"}`) + const newValue = await settings.inputValue() + expect(newValue).toContain('{"title": "Etherpad123"}') + expect(newValue.length).toEqual(24) + await saveSettings(page) + + // Check if the changes were actually saved + await page.reload() + await page.waitForSelector('.settings'); + await expect(settings).not.toBeEmpty(); + + const newSettings = page.locator('.settings'); + + const newSettingsVal = await newSettings.inputValue() + expect(newSettingsVal).toContain('{"title": "Etherpad123"}') + + + // Change back to old settings + await newSettings.fill(settingsVal) + await saveSettings(page) + + await page.reload() + await page.waitForSelector('.settings'); + await expect(settings).not.toBeEmpty(); + const oldSettings = page.locator('.settings'); + const oldSettingsVal = await oldSettings.inputValue() + expect(oldSettingsVal).toEqual(settingsVal) + expect(oldSettingsVal.length).toEqual(settingsLength) + }) + + test('restart works', async function ({page}) { + await page.goto('http://localhost:9001/admin/settings'); + await page.waitForSelector('.settings') + await restartEtherpad(page) + await page.waitForSelector('.settings') + const settings = page.locator('.settings'); + await expect(settings).not.toBeEmpty(); + await page.waitForSelector('.menu') + await page.waitForTimeout(5000) + }); +}) diff --git a/src/tests/frontend-new/admin-spec/admintroubleshooting.spec.ts b/src/tests/frontend-new/admin-spec/admintroubleshooting.spec.ts new file mode 100644 index 000000000..9155e9cbd --- /dev/null +++ b/src/tests/frontend-new/admin-spec/admintroubleshooting.spec.ts @@ -0,0 +1,39 @@ +import {expect, test} from "@playwright/test"; +import {loginToAdmin} from "../helper/adminhelper"; + +test.beforeEach(async ({ page })=>{ + await loginToAdmin(page, 'admin', 'changeme1'); + await page.goto('http://localhost:9001/admin/help') +}) + +test('Shows troubleshooting page manager', async ({page}) => { + await page.goto('http://localhost:9001/admin/help') + await page.waitForSelector('.menu') + const menu = page.locator('.menu'); + await expect(menu.locator('li')).toHaveCount(5); +}) + +test('Shows a version number', async function ({page}) { + await page.goto('http://localhost:9001/admin/help') + await page.waitForSelector('.menu') + const helper = page.locator('.help-block').locator('div').nth(1) + const version = (await helper.textContent())!.split('.'); + expect(version.length).toBe(3) +}); + +test('Lists installed parts', async function ({page}) { + await page.goto('http://localhost:9001/admin/help') + await page.waitForSelector('.menu') + await page.waitForSelector('.innerwrapper ul') + const parts = page.locator('.innerwrapper ul').nth(1); + expect(await parts.textContent()).toContain('ep_etherpad-lite/adminsettings'); +}); + +test('Lists installed hooks', async function ({page}) { + await page.goto('http://localhost:9001/admin/help') + await page.waitForSelector('.menu') + await page.waitForSelector('.innerwrapper ul') + const helper = page.locator('.innerwrapper ul').nth(2); + expect(await helper.textContent()).toContain('express'); +}); + diff --git a/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts b/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts new file mode 100644 index 000000000..c1121d41b --- /dev/null +++ b/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts @@ -0,0 +1,112 @@ +import {expect, test} from "@playwright/test"; +import {loginToAdmin} from "../helper/adminhelper"; + +test.beforeEach(async ({ page })=>{ + await loginToAdmin(page, 'admin', 'changeme1'); + await page.goto('http://localhost:9001/admin/plugins') +}) + + +test.describe('Plugins page', ()=> { + + test('List some plugins', async ({page}) => { + await page.waitForSelector('.search-field'); + const pluginTable = page.locator('table tbody').nth(1); + await expect(pluginTable).not.toBeEmpty() + const plugins = await pluginTable.locator('tr').count() + expect(plugins).toBeGreaterThan(10) + }) + + test('Searches for a plugin', async ({page}) => { + await page.waitForSelector('.search-field'); + await page.click('.search-field') + await page.keyboard.type('ep_font_color3') + await page.keyboard.press('Enter') + const pluginTable = page.locator('table tbody').nth(1); + await expect(pluginTable.locator('tr')).toHaveCount(1) + await expect(pluginTable.locator('tr').first()).toContainText('ep_font_color3') + }) + + + test('Attempt to Install and Uninstall a plugin', async ({page}) => { + await page.waitForSelector('.search-field'); + const pluginTable = page.locator('table tbody').nth(1); + await expect(pluginTable).not.toBeEmpty({ + timeout: 15000 + }) + const plugins = await pluginTable.locator('tr').count() + expect(plugins).toBeGreaterThan(10) + + // Now everything is loaded, lets install a plugin + + await page.click('.search-field') + await page.keyboard.type('ep_font_color3') + await page.keyboard.press('Enter') + + await expect(pluginTable.locator('tr')).toHaveCount(1) + const pluginRow = pluginTable.locator('tr').first() + await expect(pluginRow).toContainText('ep_font_color3') + + // Select Installation button + await pluginRow.locator('td').nth(4).locator('button').first().click() + await page.waitForTimeout(100) + await page.waitForSelector('table tbody') + const installedPlugins = page.locator('table tbody').first() + const installedPluginsRows = installedPlugins.locator('tr') + await expect(installedPluginsRows).toHaveCount(2, { + timeout: 15000 + }) + + const installedPluginRow = installedPluginsRows.nth(1) + + await expect(installedPluginRow).toContainText('ep_font_color3') + await installedPluginRow.locator('td').nth(2).locator('button').first().click() + + // Wait for the uninstallation to complete + await expect(installedPluginsRows).toHaveCount(1, { + timeout: 15000 + }) + await page.waitForTimeout(5000) + }) +}) + + +/* + it('Attempt to Update a plugin', async function () { + this.timeout(280000); + + await helper.waitForPromise(() => helper.admin$('.results').children().length > 50, 20000); + + if (helper.admin$('.ep_align').length === 0) this.skip(); + + await helper.waitForPromise( + () => helper.admin$('.ep_align .version').text().split('.').length >= 2); + + const minorVersionBefore = + parseInt(helper.admin$('.ep_align .version').text().split('.')[1]); + + if (!minorVersionBefore) { + throw new Error('Unable to get minor number of plugin, is the plugin installed?'); + } + + if (minorVersionBefore !== 2) this.skip(); + + helper.waitForPromise( + () => helper.admin$('.ep_align .do-update').length === 1); + + await timeout(500); // HACK! Please submit better fix.. + const $doUpdateButton = helper.admin$('.ep_align .do-update'); + $doUpdateButton.trigger('click'); + + // ensure its showing as Updating + await helper.waitForPromise( + () => helper.admin$('.ep_align .message').text() === 'Updating'); + + // Ensure it's a higher minor version IE 0.3.x as 0.2.x was installed + // Coverage for https://github.com/ether/etherpad-lite/issues/4536 + await helper.waitForPromise(() => parseInt(helper.admin$('.ep_align .version') + .text() + .split('.')[1]) > minorVersionBefore, 60000, 1000); + // allow 50 seconds, check every 1 second. + }); + */ diff --git a/src/tests/frontend-new/helper/adminhelper.ts b/src/tests/frontend-new/helper/adminhelper.ts new file mode 100644 index 000000000..8f2242f89 --- /dev/null +++ b/src/tests/frontend-new/helper/adminhelper.ts @@ -0,0 +1,32 @@ +import {expect, Page} from "@playwright/test"; + +export const loginToAdmin = async (page: Page, username: string, password: string) => { + + await page.goto('http://localhost:9001/admin/'); + + await page.waitForSelector('input[name="username"]'); + await page.fill('input[name="username"]', username); + await page.fill('input[name="password"]', password); + await page.click('input[type="submit"]'); +} + + +export const saveSettings = async (page: Page) => { + // Click save + await page.locator('.settings-button-bar').locator('button').first().click() + await page.waitForSelector('.ToastRootSuccess') +} + +export const restartEtherpad = async (page: Page) => { + // Click restart + const restartButton = page.locator('.settings-button-bar').locator('.settingsButton').nth(1) + const settings = page.locator('.settings'); + await expect(settings).not.toBeEmpty(); + await expect(restartButton).toBeVisible() + await page.locator('.settings-button-bar') + .locator('.settingsButton') + .nth(1) + .click() + await page.waitForTimeout(500) + await page.waitForSelector('.settings') +} diff --git a/src/tests/frontend-new/helper/padHelper.ts b/src/tests/frontend-new/helper/padHelper.ts new file mode 100644 index 000000000..f52cd0a35 --- /dev/null +++ b/src/tests/frontend-new/helper/padHelper.ts @@ -0,0 +1,157 @@ +import {Frame, Locator, Page} from "@playwright/test"; +import {MapArrayType} from "../../../node/types/MapType"; +import {randomUUID} from "node:crypto"; + +export const getPadOuter = async (page: Page): Promise => { + return page.frame('ace_outer')!; +} + +export const getPadBody = async (page: Page): Promise => { + return page.frame('ace_inner')!.locator('#innerdocbody') +} + +export const selectAllText = async (page: Page) => { + await page.keyboard.down('Control'); + await page.keyboard.press('A'); + await page.keyboard.up('Control'); +} + +export const toggleUserList = async (page: Page) => { + await page.locator("button[data-l10n-id='pad.toolbar.showusers.title']").click() +} + +export const setUserName = async (page: Page, userName: string) => { + await page.waitForSelector('[class="popup popup-show"]') + await page.click("input[data-l10n-id='pad.userlist.entername']"); + await page.keyboard.type(userName); +} + + +export const showChat = async (page: Page) => { + const chatIcon = page.locator("#chaticon") + const classes = await chatIcon.getAttribute('class') + if (classes && !classes.includes('visible')) return + await chatIcon.click() + await page.waitForFunction(`!document.querySelector('#chaticon').classList.contains('visible')`) +} + +export const getCurrentChatMessageCount = async (page: Page) => { + return await page.locator('#chattext').locator('p').count() +} + +export const getChatUserName = async (page: Page) => { + return await page.locator('#chattext') + .locator('p') + .locator('b') + .innerText() +} + +export const getChatMessage = async (page: Page) => { + return (await page.locator('#chattext') + .locator('p') + .textContent({}))! + .split(await getChatTime(page))[1] + +} + + +export const getChatTime = async (page: Page) => { + return await page.locator('#chattext') + .locator('p') + .locator('.time') + .innerText() +} + +export const sendChatMessage = async (page: Page, message: string) => { + let currentChatCount = await getCurrentChatMessageCount(page) + + const chatInput = page.locator('#chatinput') + await chatInput.click() + await page.keyboard.type(message) + await page.keyboard.press('Enter') + if(message === "") return + await page.waitForFunction(`document.querySelector('#chattext').querySelectorAll('p').length >${currentChatCount}`) +} + +export const isChatBoxShown = async (page: Page):Promise => { + const classes = await page.locator('#chatbox').getAttribute('class') + return classes !==null && classes.includes('visible') +} + +export const isChatBoxSticky = async (page: Page):Promise => { + const classes = await page.locator('#chatbox').getAttribute('class') + console.log('Chat', classes && classes.includes('stickyChat')) + return classes !==null && classes.includes('stickyChat') +} + +export const hideChat = async (page: Page) => { + if(!await isChatBoxShown(page)|| await isChatBoxSticky(page)) return + await page.locator('#titlecross').click() + await page.waitForFunction(`!document.querySelector('#chatbox').classList.contains('stickyChat')`) + +} + +export const enableStickyChatviaIcon = async (page: Page) => { + if(await isChatBoxSticky(page)) return + await page.locator('#titlesticky').click() + await page.waitForFunction(`document.querySelector('#chatbox').classList.contains('stickyChat')`) +} + +export const disableStickyChatviaIcon = async (page: Page) => { + if(!await isChatBoxSticky(page)) return + await page.locator('#titlecross').click() + await page.waitForFunction(`!document.querySelector('#chatbox').classList.contains('stickyChat')`) +} + + +export const appendQueryParams = async (page: Page, queryParameters: MapArrayType) => { + const searchParams = new URLSearchParams(page.url().split('?')[1]); + Object.keys(queryParameters).forEach((key) => { + searchParams.append(key, queryParameters[key]); + }); + await page.goto(page.url()+"?"+ searchParams.toString()); + await page.waitForSelector('iframe[name="ace_outer"]'); +} + +export const goToNewPad = async (page: Page) => { + // create a new pad before each test run + const padId = "FRONTEND_TESTS"+randomUUID(); + await page.goto('http://localhost:9001/p/'+padId); + await page.waitForSelector('iframe[name="ace_outer"]'); + return padId; +} + +export const goToPad = async (page: Page, padId: string) => { + await page.goto('http://localhost:9001/p/'+padId); + await page.waitForSelector('iframe[name="ace_outer"]'); +} + + +export const clearPadContent = async (page: Page) => { + const body = await getPadBody(page); + await body.click(); + await page.keyboard.down('Control'); + await page.keyboard.press('A'); + await page.keyboard.up('Control'); + await page.keyboard.press('Delete'); +} + +export const writeToPad = async (page: Page, text: string) => { + const body = await getPadBody(page); + await body.click(); + await page.keyboard.type(text); +} + +export const clearAuthorship = async (page: Page) => { + await page.locator("button[data-l10n-id='pad.toolbar.clearAuthorship.title']").click() +} + +export const undoChanges = async (page: Page) => { + await page.keyboard.down('Control'); + await page.keyboard.press('z'); + await page.keyboard.up('Control'); +} + +export const pressUndoButton = async (page: Page) => { + await page.locator('.buttonicon-undo').click() +} diff --git a/src/tests/frontend-new/helper/settingsHelper.ts b/src/tests/frontend-new/helper/settingsHelper.ts new file mode 100644 index 000000000..729dd48f6 --- /dev/null +++ b/src/tests/frontend-new/helper/settingsHelper.ts @@ -0,0 +1,35 @@ +import {Page} from "@playwright/test"; + +export const isSettingsShown = async (page: Page) => { + const classes = await page.locator('#settings').getAttribute('class') + return classes && classes.includes('popup-show') +} + + +export const showSettings = async (page: Page) => { + if(await isSettingsShown(page)) return + await page.locator("button[data-l10n-id='pad.toolbar.settings.title']").click() + await page.waitForFunction(`document.querySelector('#settings').classList.contains('popup-show')`) +} + +export const hideSettings = async (page: Page) => { + if(!await isSettingsShown(page)) return + await page.locator("button[data-l10n-id='pad.toolbar.settings.title']").click() + await page.waitForFunction(`!document.querySelector('#settings').classList.contains('popup-show')`) +} + +export const enableStickyChatviaSettings = async (page: Page) => { + const stickyChat = page.locator('#options-stickychat') + const checked = await stickyChat.isChecked() + if(checked) return + await stickyChat.check({force: true}) + await page.waitForSelector('#options-stickychat:checked') +} + +export const disableStickyChat = async (page: Page) => { + const stickyChat = page.locator('#options-stickychat') + const checked = await stickyChat.isChecked() + if(!checked) return + await stickyChat.uncheck({force: true}) + await page.waitForSelector('#options-stickychat:not(:checked)') +} diff --git a/src/tests/frontend-new/helper/timeslider.ts b/src/tests/frontend-new/helper/timeslider.ts new file mode 100644 index 000000000..e193048e0 --- /dev/null +++ b/src/tests/frontend-new/helper/timeslider.ts @@ -0,0 +1,20 @@ +import {Page} from "@playwright/test"; + +/** + * Sets the src-attribute of the main iframe to the timeslider + * In case a revision is given, sets the timeslider to this specific revision. + * Defaults to going to the last revision. + * It waits until the timer is filled with date and time, because it's one of the + * last things that happen during timeslider load + * + * @param page + * @param {number} [revision] the optional revision + * @returns {Promise} + * @todo for some reason this does only work the first time, you cannot + * goto rev 0 and then via the same method to rev 5. Use buttons instead + */ +export const gotoTimeslider = async (page: Page, revision: number): Promise => { + let revisionString = Number.isInteger(revision) ? `#${revision}` : ''; + await page.goto(`${page.url()}/timeslider${revisionString}`); + await page.waitForSelector('#timer') +}; diff --git a/src/tests/frontend-new/specs/alphabet.spec.ts b/src/tests/frontend-new/specs/alphabet.spec.ts new file mode 100644 index 000000000..fcd8f7f9d --- /dev/null +++ b/src/tests/frontend-new/specs/alphabet.spec.ts @@ -0,0 +1,27 @@ +import {expect, Page, test} from "@playwright/test"; +import {clearPadContent, getPadBody, getPadOuter, goToNewPad} from "../helper/padHelper"; + +test.beforeEach(async ({ page })=>{ + // create a new pad before each test run + await goToNewPad(page); +}) + +test.describe('All the alphabet works n stuff', () => { + const expectedString = 'abcdefghijklmnopqrstuvwxyz'; + + test('when you enter any char it appears right', async ({page}) => { + + // get the inner iframe + const innerFrame = await getPadBody(page!); + + await innerFrame.click(); + + // delete possible old content + await clearPadContent(page!); + + + await page.keyboard.type(expectedString); + const text = await innerFrame.locator('div').innerText(); + expect(text).toBe(expectedString); + }); +}); diff --git a/src/tests/frontend-new/specs/bold.spec.ts b/src/tests/frontend-new/specs/bold.spec.ts new file mode 100644 index 000000000..6c1769da2 --- /dev/null +++ b/src/tests/frontend-new/specs/bold.spec.ts @@ -0,0 +1,50 @@ +import {expect, test} from "@playwright/test"; +import {randomInt} from "node:crypto"; +import {getPadBody, goToNewPad, selectAllText} from "../helper/padHelper"; +import exp from "node:constants"; + +test.beforeEach(async ({ page })=>{ + await goToNewPad(page); +}) + +test.describe('bold button', ()=>{ + + test('makes text bold on click', async ({page}) => { +// get the inner iframe + const innerFrame = await getPadBody(page); + + await innerFrame.click() + // Select pad text + await selectAllText(page); + await page.keyboard.type("Hi Etherpad"); + await selectAllText(page); + + // click the bold button + await page.locator("button[data-l10n-id='pad.toolbar.bold.title']").click(); + + + // check if the text is bold + expect(await innerFrame.locator('b').innerText()).toBe('Hi Etherpad'); + }) + + test('makes text bold on keypress', async ({page}) => { + // get the inner iframe + const innerFrame = await getPadBody(page); + + await innerFrame.click() + // Select pad text + await selectAllText(page); + await page.keyboard.type("Hi Etherpad"); + await selectAllText(page); + + // Press CTRL + B + await page.keyboard.down('Control'); + await page.keyboard.press('b'); + await page.keyboard.up('Control'); + + + // check if the text is bold + expect(await innerFrame.locator('b').innerText()).toBe('Hi Etherpad'); + }) + +}) diff --git a/src/tests/frontend-new/specs/change_user_color.spec.ts b/src/tests/frontend-new/specs/change_user_color.spec.ts new file mode 100644 index 000000000..bc6b609a1 --- /dev/null +++ b/src/tests/frontend-new/specs/change_user_color.spec.ts @@ -0,0 +1,103 @@ +import {expect, test} from "@playwright/test"; +import {goToNewPad, sendChatMessage, showChat} from "../helper/padHelper"; + +test.beforeEach(async ({page}) => { + await goToNewPad(page); +}) + +test.describe('change user color', function () { + + test('Color picker matches original color and remembers the user color after a refresh', + async function ({page}) { + + // click on the settings button to make settings visible + let $userButton = page.locator('.buttonicon-showusers'); + await $userButton.click() + + let $userSwatch = page.locator('#myswatch'); + await $userSwatch.click() + // Change the color value of the Farbtastic color picker + + const $colorPickerSave = page.locator('#mycolorpickersave'); + let $colorPickerPreview = page.locator('#mycolorpickerpreview'); + + // Same color represented in two different ways + const testColorHash = '#abcdef'; + const testColorRGB = 'rgb(171, 205, 239)'; + + // Check that the color picker matches the automatically assigned random color on the swatch. + // NOTE: This has a tiny chance of creating a false positive for passing in the + // off-chance the randomly assigned color is the same as the test color. + expect(await $colorPickerPreview.getAttribute('style')).toContain(await $userSwatch.getAttribute('style')); + + // The swatch updates as the test color is picked. + await page.evaluate((testRGBColor) => { + document.getElementById('mycolorpickerpreview')!.style.backgroundColor = testRGBColor; + }, testColorRGB + ) + + await $colorPickerSave.click(); + + // give it a second to save the color on the server side + await page.waitForTimeout(1000) + + + // get a new pad, but don't clear the cookies + await goToNewPad(page) + + + // click on the settings button to make settings visible + await $userButton.click() + + await $userSwatch.click() + + + + expect(await $colorPickerPreview.getAttribute('style')).toContain(await $userSwatch.getAttribute('style')); + }); + + test('Own user color is shown when you enter a chat', async function ({page}) { + + const colorOption = page.locator('#options-colorscheck'); + if (!(await colorOption.isChecked())) { + await colorOption.check(); + } + + // click on the settings button to make settings visible + const $userButton = page.locator('.buttonicon-showusers'); + await $userButton.click() + + const $userSwatch = page.locator('#myswatch'); + await $userSwatch.click() + + const $colorPickerSave = page.locator('#mycolorpickersave'); + + // Same color represented in two different ways + const testColorHash = '#abcdef'; + const testColorRGB = 'rgb(171, 205, 239)'; + + // The swatch updates as the test color is picked. + await page.evaluate((testRGBColor) => { + document.getElementById('mycolorpickerpreview')!.style.backgroundColor = testRGBColor; + }, testColorRGB + ) + + + await $colorPickerSave.click(); + // click on the chat button to make chat visible + await showChat(page) + await sendChatMessage(page, 'O hi'); + + // wait until the chat message shows up + const chatP = page.locator('#chattext').locator('p') + const chatText = await chatP.innerText(); + + expect(chatText).toContain('O hi'); + + const color = await chatP.evaluate((el) => { + return window.getComputedStyle(el).getPropertyValue('background-color'); + }, chatText); + + expect(color).toBe(testColorRGB); + }); +}); diff --git a/src/tests/frontend-new/specs/change_user_name.spec.ts b/src/tests/frontend-new/specs/change_user_name.spec.ts new file mode 100644 index 000000000..bf7ea95c3 --- /dev/null +++ b/src/tests/frontend-new/specs/change_user_name.spec.ts @@ -0,0 +1,35 @@ +import {expect, test} from "@playwright/test"; +import {randomInt} from "node:crypto"; +import {goToNewPad, sendChatMessage, setUserName, showChat, toggleUserList} from "../helper/padHelper"; + +test.beforeEach(async ({ page })=>{ + // create a new pad before each test run + await goToNewPad(page); +}) + + +test("Remembers the username after a refresh", async ({page}) => { + await toggleUserList(page); + await setUserName(page,'😃') + await toggleUserList(page) + + await page.reload(); + await toggleUserList(page); + const usernameField = page.locator("input[data-l10n-id='pad.userlist.entername']"); + await expect(usernameField).toHaveValue('😃'); +}) + + +test('Own user name is shown when you enter a chat', async ({page})=> { + const chatMessage = 'O hi'; + + await toggleUserList(page); + await setUserName(page,'😃'); + await toggleUserList(page); + + await showChat(page); + await sendChatMessage(page,chatMessage); + const chatText = await page.locator('#chattext').locator('p').innerText(); + expect(chatText).toContain('😃') + expect(chatText).toContain(chatMessage) +}); diff --git a/src/tests/frontend-new/specs/chat.spec.ts b/src/tests/frontend-new/specs/chat.spec.ts new file mode 100644 index 000000000..4d4f1bd1c --- /dev/null +++ b/src/tests/frontend-new/specs/chat.spec.ts @@ -0,0 +1,116 @@ +import {expect, test} from "@playwright/test"; +import {randomInt} from "node:crypto"; +import { + appendQueryParams, + disableStickyChatviaIcon, + enableStickyChatviaIcon, + getChatMessage, + getChatTime, + getChatUserName, + getCurrentChatMessageCount, goToNewPad, hideChat, isChatBoxShown, isChatBoxSticky, + sendChatMessage, + showChat, +} from "../helper/padHelper"; +import {disableStickyChat, enableStickyChatviaSettings, hideSettings, showSettings} from "../helper/settingsHelper"; + + +test.beforeEach(async ({ page })=>{ + await goToNewPad(page); +}) + + +test('opens chat, sends a message, makes sure it exists on the page and hides chat', async ({page}) => { + const chatValue = "JohnMcLear" + + // Open chat + await showChat(page); + await sendChatMessage(page, chatValue); + + expect(await getCurrentChatMessageCount(page)).toBe(1); + const username = await getChatUserName(page) + const time = await getChatTime(page) + const chatMessage = await getChatMessage(page) + + expect(username).toBe('unnamed:'); + const regex = new RegExp('^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$'); + expect(time).toMatch(regex); + expect(chatMessage).toBe(" "+chatValue); +}) + +test("makes sure that an empty message can't be sent", async function ({page}) { + const chatValue = 'mluto'; + + await showChat(page); + + await sendChatMessage(page,""); + // Send a message + await sendChatMessage(page,chatValue); + + expect(await getCurrentChatMessageCount(page)).toBe(1); + + // check that the received message is not the empty one + const username = await getChatUserName(page) + const time = await getChatTime(page); + const chatMessage = await getChatMessage(page); + + expect(username).toBe('unnamed:'); + const regex = new RegExp('^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$'); + expect(time).toMatch(regex); + expect(chatMessage).toBe(" "+chatValue); +}); + +test('makes chat stick to right side of the screen via settings, remove sticky via settings, close it', async ({page}) =>{ + await showSettings(page); + + await enableStickyChatviaSettings(page); + expect(await isChatBoxShown(page)).toBe(true); + expect(await isChatBoxSticky(page)).toBe(true); + + await disableStickyChat(page); + expect(await isChatBoxShown(page)).toBe(true); + expect(await isChatBoxSticky(page)).toBe(false); + await hideSettings(page); + await hideChat(page); + expect(await isChatBoxShown(page)).toBe(false); + expect(await isChatBoxSticky(page)).toBe(false); +}); + +test('makes chat stick to right side of the screen via icon on the top right, ' + + 'remove sticky via icon, close it', async function ({page}) { + await showChat(page); + + await enableStickyChatviaIcon(page); + expect(await isChatBoxShown(page)).toBe(true); + expect(await isChatBoxSticky(page)).toBe(true); + + await disableStickyChatviaIcon(page); + expect(await isChatBoxShown(page)).toBe(true); + expect(await isChatBoxSticky(page)).toBe(false); + + await hideChat(page); + expect(await isChatBoxSticky(page)).toBe(false); + expect(await isChatBoxShown(page)).toBe(false); +}); + + +test('Checks showChat=false URL Parameter hides chat then' + + ' when removed it shows chat', async function ({page}) { + + // get a new pad, but don't clear the cookies + await appendQueryParams(page, { + showChat: 'false' + }); + + const chaticon = page.locator('#chaticon') + + + // chat should be hidden. + expect(await chaticon.isVisible()).toBe(false); + + // get a new pad, but don't clear the cookies + await goToNewPad(page); + const secondChatIcon = page.locator('#chaticon') + + // chat should be visible. + expect(await secondChatIcon.isVisible()).toBe(true) +}); diff --git a/src/tests/frontend-new/specs/clear_authorship_color.spec.ts b/src/tests/frontend-new/specs/clear_authorship_color.spec.ts new file mode 100644 index 000000000..6a999a57e --- /dev/null +++ b/src/tests/frontend-new/specs/clear_authorship_color.spec.ts @@ -0,0 +1,87 @@ +import {expect, test} from "@playwright/test"; +import { + clearAuthorship, + clearPadContent, + getPadBody, + goToNewPad, pressUndoButton, + selectAllText, + undoChanges, + writeToPad +} from "../helper/padHelper"; + +test.beforeEach(async ({ page })=>{ + // create a new pad before each test run + await goToNewPad(page); +}) + +test('clear authorship color', async ({page}) => { + // get the inner iframe + const innerFrame = await getPadBody(page); + const padText = "Hello" + + // type some text + await clearPadContent(page); + await writeToPad(page, padText); + const retrievedClasses = await innerFrame.locator('div span').nth(0).getAttribute('class') + expect(retrievedClasses).toContain('author'); + + // select the text + await innerFrame.click() + await selectAllText(page); + + await clearAuthorship(page); + // does the first div include an author class? + const firstDivClass = await innerFrame.locator('div').nth(0).getAttribute('class'); + expect(firstDivClass).not.toContain('author'); + const classes = page.locator('div.disconnected') + expect(await classes.isVisible()).toBe(false) +}) + + +test("makes text clear authorship colors and checks it can't be undone", async function ({page}) { + const innnerPad = await getPadBody(page); + const padText = "Hello" + + // type some text + await clearPadContent(page); + await writeToPad(page, padText); + + // get the first text element out of the inner iframe + const firstDivClass = innnerPad.locator('div').nth(0) + const retrievedClasses = await innnerPad.locator('div span').nth(0).getAttribute('class') + expect(retrievedClasses).toContain('author'); + + + await firstDivClass.focus() + await clearAuthorship(page); + expect(await firstDivClass.getAttribute('class')).not.toContain('author'); + + await undoChanges(page); + const changedFirstDiv = innnerPad.locator('div').nth(0) + expect(await changedFirstDiv.getAttribute('class')).not.toContain('author'); + + + await pressUndoButton(page); + const secondChangedFirstDiv = innnerPad.locator('div').nth(0) + expect(await secondChangedFirstDiv.getAttribute('class')).not.toContain('author'); +}); + + +// Test for https://github.com/ether/etherpad-lite/issues/5128 +test('clears authorship when first line has line attributes', async function ({page}) { + // Make sure there is text with author info. The first line must have a line attribute. + const padBody = await getPadBody(page); + await padBody.click() + await clearPadContent(page); + await writeToPad(page,'Hello') + await page.locator('.buttonicon-insertunorderedlist').click(); + const retrievedClasses = await padBody.locator('div span').nth(0).getAttribute('class') + expect(retrievedClasses).toContain('author'); + await padBody.click() + await selectAllText(page); + await clearAuthorship(page); + const retrievedClasses2 = await padBody.locator('div span').nth(0).getAttribute('class') + expect(retrievedClasses2).not.toContain('author'); + + expect(await page.locator('[class*="author-"]').count()).toBe(0) +}); diff --git a/src/tests/frontend-new/specs/collab_client.spec.ts b/src/tests/frontend-new/specs/collab_client.spec.ts new file mode 100644 index 000000000..5cc9c1ec3 --- /dev/null +++ b/src/tests/frontend-new/specs/collab_client.spec.ts @@ -0,0 +1,94 @@ +import {clearPadContent, getPadBody, goToNewPad, goToPad, writeToPad} from "../helper/padHelper"; +import {expect, Page, test} from "@playwright/test"; + +let padId = ""; + +test.beforeEach(async ({ page })=>{ + // create a new pad before each test run + padId = await goToNewPad(page); + const body = await getPadBody(page); + await body.click(); + await clearPadContent(page); + await writeToPad(page, "Hello World"); + await page.keyboard.press('Enter'); + await writeToPad(page, "Hello World"); + await page.keyboard.press('Enter'); + await writeToPad(page, "Hello World"); + await page.keyboard.press('Enter'); + await writeToPad(page, "Hello World"); + await page.keyboard.press('Enter'); + await writeToPad(page, "Hello World"); + await page.keyboard.press('Enter'); +}) + +test.describe('Messages in the COLLABROOM', function () { + const user1Text = 'text created by user 1'; + const user2Text = 'text created by user 2'; + + const replaceLineText = async (lineNumber: number, newText: string, page: Page) => { + const body = await getPadBody(page) + + const div = body.locator('div').nth(lineNumber) + + // simulate key presses to delete content + await div.locator('span').selectText() // select all + await page.keyboard.press('Backspace') // clear the first line + await page.keyboard.type(newText) // insert the string + }; + + test('bug #4978 regression test', async function ({browser}) { + // The bug was triggered by receiving a change from another user while simultaneously composing + // a character and waiting for an acknowledgement of a previously sent change. + + // User 1 + const context1 = await browser.newContext(); + const page1 = await context1.newPage(); + await goToPad(page1, padId) + const body1 = await getPadBody(page1) + // Perform actions as User 1... + + // User 2 + const context2 = await browser.newContext(); + const page2 = await context2.newPage(); + await goToPad(page2, padId) + const body2 = await getPadBody(page1) + + await replaceLineText(0, user1Text,page1); + + const text = await body2.locator('div').nth(0).textContent() + const res = text === user1Text + expect(res).toBe(true) + + // User 1 starts a character composition. + + + await replaceLineText(1, user2Text, page2) + + await expect(body1.locator('div').nth(1)).toHaveText(user2Text) + + + // Users 1 and 2 make some more changes. + await replaceLineText(3, user2Text, page2); + + await expect(body1.locator('div').nth(3)).toHaveText(user2Text) + + await replaceLineText(2, user1Text, page1); + await expect(body2.locator('div').nth(2)).toHaveText(user1Text) + + // All changes should appear in both views. + const expectedLines = [ + user1Text, + user2Text, + user1Text, + user2Text, + ]; + + for (let i=0;i{ + // create a new pad before each test run + await goToNewPad(page); +}) + + +test('delete keystroke', async ({page}) => { + const padText = "Hello World this is a test" + const body = await getPadBody(page) + await body.click() + await clearPadContent(page) + await page.keyboard.type(padText) + // Navigate to the end of the text + await page.keyboard.press('End'); + // Delete the last character + await page.keyboard.press('Backspace'); + const text = await body.locator('div').innerText(); + expect(text).toBe(padText.slice(0, -1)); +}) diff --git a/src/tests/frontend-new/specs/embed_value.spec.ts b/src/tests/frontend-new/specs/embed_value.spec.ts new file mode 100644 index 000000000..a65276cc9 --- /dev/null +++ b/src/tests/frontend-new/specs/embed_value.spec.ts @@ -0,0 +1,136 @@ +import {expect, Page, test} from "@playwright/test"; +import {goToNewPad} from "../helper/padHelper"; + +test.beforeEach(async ({ page })=>{ + // create a new pad before each test run + await goToNewPad(page); +}) + +test.describe('embed links', function () { + const objectify = function (str: string) { + const hash = {}; + const parts = str.split('&'); + for (let i = 0; i < parts.length; i++) { + const keyValue = parts[i].split('='); + // @ts-ignore + hash[keyValue[0]] = keyValue[1]; + } + return hash; + }; + + const checkiFrameCode = async function (embedCode: string, readonly: boolean, page: Page) { + // turn the code into an html element + + await page.setContent(embedCode, {waitUntil: 'load'}) + const locator = page.locator('body').locator('iframe').last() + + + // read and check the frame attributes + const width = await locator.getAttribute('width'); + const height = await locator.getAttribute('height'); + const name = await locator.getAttribute('name'); + expect(width).toBe('100%'); + expect(height).toBe('600'); + expect(name).toBe(readonly ? 'embed_readonly' : 'embed_readwrite'); + + // parse the url + const src = (await locator.getAttribute('src'))!; + const questionMark = src.indexOf('?'); + const url = src.substring(0, questionMark); + const paramsStr = src.substring(questionMark + 1); + const params = objectify(paramsStr); + + const expectedParams = { + showControls: 'true', + showChat: 'true', + showLineNumbers: 'true', + useMonospaceFont: 'false', + }; + + // check the url + if (readonly) { + expect(url.indexOf('r.') > 0).toBe(true); + } else { + expect(url).toBe(await page.evaluate(() => window.location.href)); + } + + // check if all parts of the url are like expected + expect(params).toEqual(expectedParams); + }; + + test.describe('read and write', function () { + test.beforeEach(async ({ page })=>{ + // create a new pad before each test run + await goToNewPad(page); + }) + test('the share link is the actual pad url', async function ({page}) { + + const shareButton = page.locator('.buttonicon-embed') + // open share dropdown + await shareButton.click() + + // get the link of the share field + the actual pad url and compare them + const shareLink = await page.locator('#linkinput').inputValue() + const padURL = page.url(); + expect(shareLink).toBe(padURL); + }); + + test('is an iframe with the correct url parameters and correct size', async function ({page}) { + + const shareButton = page.locator('.buttonicon-embed') + await shareButton.click() + + // get the link of the share field + the actual pad url and compare them + const embedCode = await page.locator('#embedinput').inputValue() + + + await checkiFrameCode(embedCode, false, page); + }); + }); + + test.describe('when read only option is set', function () { + test.beforeEach(async ({ page })=>{ + // create a new pad before each test run + await goToNewPad(page); + }) + + test('the share link shows a read only url', async function ({page}) { + + // open share dropdown + const shareButton = page.locator('.buttonicon-embed') + await shareButton.click() + const readonlyCheckbox = page.locator('#readonlyinput') + await readonlyCheckbox.click({ + force: true + }) + await page.waitForSelector('#readonlyinput:checked') + + // get the link of the share field + the actual pad url and compare them + const shareLink = await page.locator('#linkinput').inputValue() + const containsReadOnlyLink = shareLink.indexOf('r.') > 0; + expect(containsReadOnlyLink).toBe(true); + }); + + test('the embed as iframe code is an iframe with the correct url parameters and correct size', async function ({page}) { + + + // open share dropdown + const shareButton = page.locator('.buttonicon-embed') + await shareButton.click() + + // check read only checkbox, a bit hacky + const readonlyCheckbox = page.locator('#readonlyinput') + await readonlyCheckbox.click({ + force: true + }) + + await page.waitForSelector('#readonlyinput:checked') + + + // get the link of the share field + the actual pad url and compare them + const embedCode = await page.locator('#embedinput').inputValue() + + await checkiFrameCode(embedCode, true, page); + }); + }) +}) diff --git a/src/tests/frontend-new/specs/enter.spec.ts b/src/tests/frontend-new/specs/enter.spec.ts new file mode 100644 index 000000000..fd9c732c2 --- /dev/null +++ b/src/tests/frontend-new/specs/enter.spec.ts @@ -0,0 +1,63 @@ +'use strict'; +import {expect, test} from "@playwright/test"; +import {getPadBody, goToNewPad, writeToPad} from "../helper/padHelper"; + +test.beforeEach(async ({ page })=>{ + await goToNewPad(page); +}) + +test.describe('enter keystroke', function () { + + test('creates a new line & puts cursor onto a new line', async function ({page}) { + const padBody = await getPadBody(page); + + // get the first text element out of the inner iframe + const firstTextElement = padBody.locator('div').nth(0) + + // get the original string value minus the last char + const originalTextValue = await firstTextElement.textContent(); + + // simulate key presses to enter content + await firstTextElement.click() + await page.keyboard.press('Home'); + await page.keyboard.press('Enter'); + + const updatedFirstElement = padBody.locator('div').nth(0) + expect(await updatedFirstElement.textContent()).toBe('') + + const newSecondLine = padBody.locator('div').nth(1); + // expect the second line to be the same as the original first line. + expect(await newSecondLine.textContent()).toBe(originalTextValue); + }); + + test('enter is always visible after event', async function ({page}) { + const padBody = await getPadBody(page); + const originalLength = await padBody.locator('div').count(); + let lastLine = padBody.locator('div').last(); + + // simulate key presses to enter content + let i = 0; + const numberOfLines = 15; + while (i < numberOfLines) { + lastLine = padBody.locator('div').last(); + await lastLine.focus(); + await page.keyboard.press('End'); + await page.keyboard.press('Enter'); + + // check we can see the caret.. + i++; + } + + expect(await padBody.locator('div').count()).toBe(numberOfLines + originalLength); + + // is edited line fully visible? + const lastDiv = padBody.locator('div').last() + const lastDivOffset = await lastDiv.boundingBox(); + const bottomOfLastLine = lastDivOffset!.y + lastDivOffset!.height; + const scrolledWindow = page.frames()[0]; + const windowOffset = await scrolledWindow.evaluate(() => window.pageYOffset); + const windowHeight = await scrolledWindow.evaluate(() => window.innerHeight); + + expect(windowOffset + windowHeight).toBeGreaterThan(bottomOfLastLine); + }); +}); diff --git a/src/tests/frontend-new/specs/font_type.spec.ts b/src/tests/frontend-new/specs/font_type.spec.ts new file mode 100644 index 000000000..a2772da99 --- /dev/null +++ b/src/tests/frontend-new/specs/font_type.spec.ts @@ -0,0 +1,39 @@ +import {expect, test} from "@playwright/test"; +import {getPadBody, goToNewPad} from "../helper/padHelper"; +import {showSettings} from "../helper/settingsHelper"; + +test.beforeEach(async ({ page })=>{ + // create a new pad before each test run + await goToNewPad(page); +}) + + +test.describe('font select', function () { + // create a new pad before each test run + + test('makes text RobotoMono', async function ({page}) { + // click on the settings button to make settings visible + await showSettings(page); + + // get the font menu and RobotoMono option + const viewFontMenu = page.locator('#viewfontmenu'); + + // select RobotoMono and fire change event + // $RobotoMonooption.attr('selected','selected'); + // commenting out above will break safari test + const dropdown = page.locator('.dropdowns-container .dropdown-line .current').nth(0) + await dropdown.click() + await page.locator('li:text("RobotoMono")').click() + + await viewFontMenu.dispatchEvent('change'); + const padBody = await getPadBody(page) + const color = await padBody.evaluate((e) => { + return window.getComputedStyle(e).getPropertyValue("font-family") + }) + + + // check if font changed to RobotoMono + const containsStr = color.toLowerCase().indexOf('robotomono'); + expect(containsStr).not.toBe(-1); + }); +}); diff --git a/src/tests/frontend-new/specs/indentation.spec.ts b/src/tests/frontend-new/specs/indentation.spec.ts new file mode 100644 index 000000000..3e94dbad3 --- /dev/null +++ b/src/tests/frontend-new/specs/indentation.spec.ts @@ -0,0 +1,241 @@ +import {expect, test} from "@playwright/test"; +import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper"; + +test.beforeEach(async ({ page })=>{ + await goToNewPad(page); +}) + +test.describe('indentation button', function () { + test('indent text with keypress', async function ({page}) { + const padBody = await getPadBody(page); + + // get the first text element out of the inner iframe + const $firstTextElement = padBody.locator('div').first(); + + // select this text element + await $firstTextElement.selectText() + + await page.keyboard.press('Tab'); + + const uls = padBody.locator('div').first().locator('ul li') + await expect(uls).toHaveCount(1); + }); + + test('indent text with button', async function ({page}) { + const padBody = await getPadBody(page); + await page.locator('.buttonicon-indent').click() + + const uls = padBody.locator('div').first().locator('ul') + await expect(uls).toHaveCount(1); + }); + + + test('keeps the indent on enter for the new line', async function ({page}) { + const padBody = await getPadBody(page); + await padBody.click() + await clearPadContent(page) + + await page.locator('.buttonicon-indent').click() + + // type a bit, make a line break and type again + await padBody.locator('div').first().focus() + await page.keyboard.type('line 1') + await page.keyboard.press('Enter'); + await page.keyboard.type('line 2') + await page.keyboard.press('Enter'); + + const $newSecondLine = padBody.locator('div span').nth(1) + + const hasULElement = padBody.locator('ul li') + + await expect(hasULElement).toHaveCount(3); + await expect($newSecondLine).toHaveText('line 2'); + }); + + + test('indents text with spaces on enter if previous line ends ' + + "with ':', '[', '(', or '{'", async function ({page}) { + const padBody = await getPadBody(page); + await padBody.click() + await clearPadContent(page) + // type a bit, make a line break and type again + const $firstTextElement = padBody.locator('div').first(); + await writeToPad(page, "line with ':'"); + await page.keyboard.press('Enter'); + await writeToPad(page, "line with '['"); + await page.keyboard.press('Enter'); + await writeToPad(page, "line with '('"); + await page.keyboard.press('Enter'); + await writeToPad(page, "line with '{{}'"); + + await expect(padBody.locator('div').nth(3)).toHaveText("line with '{{}'"); + + // we validate bottom to top for easier implementation + + + // curly braces + const $lineWithCurlyBraces = padBody.locator('div').nth(3) + await $lineWithCurlyBraces.click(); + await page.keyboard.press('End'); + await page.keyboard.type('{{'); + + // cannot use sendkeys('{enter}') here, browser does not read the command properly + await page.keyboard.press('Enter'); + + expect(await padBody.locator('div').nth(4).textContent()).toMatch(/\s{4}/); // tab === 4 spaces + + + + // parenthesis + const $lineWithParenthesis = padBody.locator('div').nth(2) + await $lineWithParenthesis.click(); + await page.keyboard.press('End'); + await page.keyboard.type('('); + await page.keyboard.press('Enter'); + const $lineAfterParenthesis = padBody.locator('div').nth(3) + expect(await $lineAfterParenthesis.textContent()).toMatch(/\s{4}/); + + // bracket + const $lineWithBracket = padBody.locator('div').nth(1) + await $lineWithBracket.click(); + await page.keyboard.press('End'); + await page.keyboard.type('['); + await page.keyboard.press('Enter'); + const $lineAfterBracket = padBody.locator('div').nth(2); + expect(await $lineAfterBracket.textContent()).toMatch(/\s{4}/); + + // colon + const $lineWithColon = padBody.locator('div').first(); + await $lineWithColon.click(); + await page.keyboard.press('End'); + await page.keyboard.type(':'); + await page.keyboard.press('Enter'); + const $lineAfterColon = padBody.locator('div').nth(1); + expect(await $lineAfterColon.textContent()).toMatch(/\s{4}/); + }); + + test('appends indentation to the indent of previous line if previous line ends ' + + "with ':', '[', '(', or '{'", async function ({page}) { + const padBody = await getPadBody(page); + await padBody.click() + await clearPadContent(page) + + // type a bit, make a line break and type again + await writeToPad(page, " line with some indentation and ':'") + await page.keyboard.press('Enter'); + await writeToPad(page, "line 2") + + const $lineWithColon = padBody.locator('div').first(); + await $lineWithColon.click(); + await page.keyboard.press('End'); + await page.keyboard.type(':'); + await page.keyboard.press('Enter'); + + const $lineAfterColon = padBody.locator('div').nth(1); + // previous line indentation + regular tab (4 spaces) + expect(await $lineAfterColon.textContent()).toMatch(/\s{6}/); + }); + + test("issue #2772 shows '*' when multiple indented lines " + + ' receive a style and are outdented', async function ({page}) { + + const padBody = await getPadBody(page); + await padBody.click() + await clearPadContent(page) + + const inner = padBody.locator('div').first(); + // make sure pad has more than one line + await inner.click() + await page.keyboard.type('First'); + await page.keyboard.press('Enter'); + await page.keyboard.type('Second'); + + + // indent first 2 lines + await padBody.locator('div').nth(0).selectText(); + await page.locator('.buttonicon-indent').click() + + await padBody.locator('div').nth(1).selectText(); + await page.locator('.buttonicon-indent').click() + + + await expect(padBody.locator('ul li')).toHaveCount(2); + + + // apply bold + await padBody.locator('div').nth(0).selectText(); + await page.locator('.buttonicon-bold').click() + + await padBody.locator('div').nth(1).selectText(); + await page.locator('.buttonicon-bold').click() + + await expect(padBody.locator('div b')).toHaveCount(2); + + // outdent first 2 lines + await padBody.locator('div').nth(0).selectText(); + await page.locator('.buttonicon-outdent').click() + + await padBody.locator('div').nth(1).selectText(); + await page.locator('.buttonicon-outdent').click() + + await expect(padBody.locator('ul li')).toHaveCount(0); + + // check if '*' is displayed + const secondLine = padBody.locator('div').nth(1); + await expect(secondLine).toHaveText('Second'); + }); + + test('makes text indented and outdented', async function ({page}) { + // get the inner iframe + + const padBody = await getPadBody(page); + + // get the first text element out of the inner iframe + let firstTextElement = padBody.locator('div').first(); + + // select this text element + await firstTextElement.selectText() + + // get the indentation button and click it + await page.locator('.buttonicon-indent').click() + + let newFirstTextElement = padBody.locator('div').first(); + + // is there a list-indent class element now? + await expect(newFirstTextElement.locator('ul')).toHaveCount(1); + + await expect(newFirstTextElement.locator('li')).toHaveCount(1); + + // indent again + await page.locator('.buttonicon-indent').click() + + newFirstTextElement = padBody.locator('div').first(); + + + // is there a list-indent class element now? + const ulList = newFirstTextElement.locator('ul').first() + await expect(ulList).toHaveCount(1); + // expect it to be part of a list + expect(await ulList.getAttribute('class')).toBe('list-indent2'); + + // make sure the text hasn't changed + expect(await newFirstTextElement.textContent()).toBe(await firstTextElement.textContent()); + + + // test outdent + + // get the unindentation button and click it twice + newFirstTextElement = padBody.locator('div').first(); + await newFirstTextElement.selectText() + await page.locator('.buttonicon-outdent').click() + await page.locator('.buttonicon-outdent').click() + + newFirstTextElement = padBody.locator('div').first(); + + // is there a list-indent class element now? + await expect(newFirstTextElement.locator('ul')).toHaveCount(0); + + // make sure the text hasn't changed + expect(await newFirstTextElement.textContent()).toEqual(await firstTextElement.textContent()); + }); +}); diff --git a/src/tests/frontend-new/specs/inner_height.spec.ts b/src/tests/frontend-new/specs/inner_height.spec.ts new file mode 100644 index 000000000..3baa7e49b --- /dev/null +++ b/src/tests/frontend-new/specs/inner_height.spec.ts @@ -0,0 +1,56 @@ +'use strict'; + +import {expect, test} from "@playwright/test"; +import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper"; + +test.beforeEach(async ({ page })=>{ + await goToNewPad(page); +}) + +test.describe('height regression after ace.js refactoring', function () { + + test('clientHeight should equal scrollHeight with few lines', async function ({page}) { + const padBody = await getPadBody(page); + await padBody.click() + await clearPadContent(page) + + const iframe = page.locator('iframe').first() + const scrollHeight = await iframe.evaluate((element) => { + return element.scrollHeight; + }) + + const clientHeight = await iframe.evaluate((element) => { + return element.clientHeight; + }) + + + expect(clientHeight).toEqual(scrollHeight); + }); + + test('client height should be less than scrollHeight with many lines', async function ({page}) { + const padBody = await getPadBody(page); + await padBody.click() + await clearPadContent(page) + + await writeToPad(page,'Test line\n' + + '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' + + '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' + + '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' + + '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' + + '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' + + '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' + + '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'); + + const iframe = page.locator('iframe').first() + const scrollHeight = await iframe.evaluate((element) => { + return element.scrollHeight; + }) + + const clientHeight = await iframe.evaluate((element) => { + return element.clientHeight; + }) + + // Need to poll because the heights take some time to settle. + expect(clientHeight).toBeLessThanOrEqual(scrollHeight); + }); +}); diff --git a/src/tests/frontend-new/specs/italic.spec.ts b/src/tests/frontend-new/specs/italic.spec.ts new file mode 100644 index 000000000..dc69f0e38 --- /dev/null +++ b/src/tests/frontend-new/specs/italic.spec.ts @@ -0,0 +1,65 @@ +import {expect, test} from "@playwright/test"; +import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper"; + +test.beforeEach(async ({ page })=>{ + await goToNewPad(page); +}) + +test.describe('italic some text', function () { + + test('makes text italic using button', async function ({page}) { + const padBody = await getPadBody(page); + await padBody.click() + await clearPadContent(page) + + // get the first text element out of the inner iframe + const $firstTextElement = padBody.locator('div').first(); + await $firstTextElement.click() + await writeToPad(page, 'Foo') + + // select this text element + await padBody.click() + await page.keyboard.press('Control+A'); + + // get the bold button and click it + const $boldButton = page.locator('.buttonicon-italic'); + await $boldButton.click(); + + // ace creates a new dom element when you press a button, just get the first text element again + const $newFirstTextElement = padBody.locator('div').first(); + + // is there a element now? + // expect it to be italic + await expect($newFirstTextElement.locator('i')).toHaveCount(1); + + + // make sure the text hasn't changed + expect(await $newFirstTextElement.textContent()).toEqual(await $firstTextElement.textContent()); + }); + + test('makes text italic using keypress', async function ({page}) { + const padBody = await getPadBody(page); + await padBody.click() + await clearPadContent(page) + + // get the first text element out of the inner iframe + const $firstTextElement = padBody.locator('div').first(); + + // select this text element + await writeToPad(page, 'Foo') + + await page.keyboard.press('Control+A'); + + await page.keyboard.press('Control+I'); + + // ace creates a new dom element when you press a button, just get the first text element again + const $newFirstTextElement = padBody.locator('div').first(); + + // is there a element now? + // expect it to be italic + await expect($newFirstTextElement.locator('i')).toHaveCount(1); + + // make sure the text hasn't changed + expect(await $newFirstTextElement.textContent()).toBe(await $firstTextElement.textContent()); + }); +}); diff --git a/src/tests/frontend-new/specs/language.spec.ts b/src/tests/frontend-new/specs/language.spec.ts new file mode 100644 index 000000000..87da86b13 --- /dev/null +++ b/src/tests/frontend-new/specs/language.spec.ts @@ -0,0 +1,88 @@ +import {expect, test} from "@playwright/test"; +import {getPadBody, goToNewPad} from "../helper/padHelper"; +import {showSettings} from "../helper/settingsHelper"; + +test.beforeEach(async ({ page, browser })=>{ + const context = await browser.newContext() + await context.clearCookies() + await goToNewPad(page); +}) + + + +test.describe('Language select and change', function () { + + // Destroy language cookies + test('makes text german', async function ({page}) { + // click on the settings button to make settings visible + await showSettings(page) + + // click the language button + const languageDropDown = page.locator('.nice-select').nth(1) + + await languageDropDown.click() + await page.locator('.nice-select').locator('[data-value=de]').click() + await expect(languageDropDown.locator('.current')).toHaveText('Deutsch') + + // select german + await page.locator('.buttonicon-bold').evaluate((el) => el.parentElement!.title === 'Fett (Strg-B)'); + }); + + test('makes text English', async function ({page}) { + + await showSettings(page) + + // click the language button + await page.locator('.nice-select').nth(1).locator('.current').click() + await page.locator('.nice-select').locator('[data-value=de]').click() + + // select german + await page.locator('.buttonicon-bold').evaluate((el) => el.parentElement!.title === 'Fett (Strg-B)'); + + + // change to english + await page.locator('.nice-select').nth(1).locator('.current').click() + await page.locator('.nice-select').locator('[data-value=en]').click() + + // check if the language is now English + await page.locator('.buttonicon-bold').evaluate((el) => el.parentElement!.title !== 'Fett (Strg-B)'); + }); + + test('changes direction when picking an rtl lang', async function ({page}) { + + await showSettings(page) + + // click the language button + await page.locator('.nice-select').nth(1).locator('.current').click() + await page.locator('.nice-select').locator('[data-value=de]').click() + + // select german + await page.locator('.buttonicon-bold').evaluate((el) => el.parentElement!.title === 'Fett (Strg-B)'); + + // click the language button + await page.locator('.nice-select').nth(1).locator('.current').click() + // select arabic + // $languageoption.attr('selected','selected'); // Breaks the test.. + await page.locator('.nice-select').locator('[data-value=ar]').click() + + await page.waitForSelector('html[dir="rtl"]') + }); + + test('changes direction when picking an ltr lang', async function ({page}) { + await showSettings(page) + + // change to english + const languageDropDown = page.locator('.nice-select').nth(1) + await languageDropDown.locator('.current').click() + await languageDropDown.locator('[data-value=en]').click() + + await expect(languageDropDown.locator('.current')).toHaveText('English') + + // check if the language is now English + await page.locator('.buttonicon-bold').evaluate((el) => el.parentElement!.title !== 'Fett (Strg-B)'); + + + await page.waitForSelector('html[dir="ltr"]') + + }); +}); diff --git a/src/tests/frontend-new/specs/ordered_list.spec.ts b/src/tests/frontend-new/specs/ordered_list.spec.ts new file mode 100644 index 000000000..04e996e66 --- /dev/null +++ b/src/tests/frontend-new/specs/ordered_list.spec.ts @@ -0,0 +1,109 @@ +import {expect, test} from "@playwright/test"; +import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper"; + +test.beforeEach(async ({ page })=>{ + await goToNewPad(page); +}) + + +test.describe('ordered_list.js', function () { + + test('issue #4748 keeps numbers increment on OL', async function ({page}) { + const padBody = await getPadBody(page); + await clearPadContent(page) + await writeToPad(page, 'Line 1') + await page.keyboard.press('Enter') + await writeToPad(page, 'Line 2') + + const $insertorderedlistButton = page.locator('.buttonicon-insertorderedlist') + await padBody.locator('div').first().selectText() + await $insertorderedlistButton.first().click(); + + const secondLine = padBody.locator('div').nth(1) + + await secondLine.selectText() + await $insertorderedlistButton.click(); + + expect(await secondLine.locator('ol').getAttribute('start')).toEqual('2'); + }); + + test('issue #1125 keeps the numbered list on enter for the new line', async function ({page}) { + // EMULATES PASTING INTO A PAD + const padBody = await getPadBody(page); + await clearPadContent(page) + await expect(padBody.locator('div')).toHaveCount(1) + const $insertorderedlistButton = page.locator('.buttonicon-insertorderedlist') + await $insertorderedlistButton.click(); + + // type a bit, make a line break and type again + const firstTextElement = padBody.locator('div').first() + await firstTextElement.click() + await writeToPad(page, 'line 1') + await page.keyboard.press('Enter') + await writeToPad(page, 'line 2') + await page.keyboard.press('Enter') + + await expect(padBody.locator('div span').nth(1)).toHaveText('line 2'); + + const $newSecondLine = padBody.locator('div').nth(1) + expect(await $newSecondLine.locator('ol li').count()).toEqual(1); + await expect($newSecondLine.locator('ol li').nth(0)).toHaveText('line 2'); + const hasLineNumber = await $newSecondLine.locator('ol').getAttribute('start'); + // This doesn't work because pasting in content doesn't work + expect(Number(hasLineNumber)).toBe(2); + }); + }); + + test.describe('Pressing Tab in an OL increases and decreases indentation', function () { + + test('indent and de-indent list item with keypress', async function ({page}) { + const padBody = await getPadBody(page); + + // get the first text element out of the inner iframe + const $firstTextElement = padBody.locator('div').first(); + + // select this text element + await $firstTextElement.selectText() + + const $insertorderedlistButton = page.locator('.buttonicon-insertorderedlist') + await $insertorderedlistButton.click() + + await page.keyboard.press('Tab') + + await expect(padBody.locator('div').first().locator('.list-number2')).toHaveCount(1) + + await page.keyboard.press('Shift+Tab') + + + await expect(padBody.locator('div').first().locator('.list-number1')).toHaveCount(1) + }); + }); + + + test.describe('Pressing indent/outdent button in an OL increases and ' + + 'decreases indentation and bullet / ol formatting', function () { + + test('indent and de-indent list item with indent button', async function ({page}) { + const padBody = await getPadBody(page); + + // get the first text element out of the inner iframe + const $firstTextElement = padBody.locator('div').first(); + + // select this text element + await $firstTextElement.selectText() + + const $insertorderedlistButton = page.locator('.buttonicon-insertorderedlist') + await $insertorderedlistButton.click() + + const $indentButton = page.locator('.buttonicon-indent') + await $indentButton.dblclick() // make it indented twice + + const outdentButton = page.locator('.buttonicon-outdent') + + await expect(padBody.locator('div').first().locator('.list-number3')).toHaveCount(1) + + await outdentButton.click(); // make it deindented to 1 + + await expect(padBody.locator('div').first().locator('.list-number2')).toHaveCount(1) + }); + }); diff --git a/src/tests/frontend-new/specs/redo.spec.ts b/src/tests/frontend-new/specs/redo.spec.ts new file mode 100644 index 000000000..b3df70c69 --- /dev/null +++ b/src/tests/frontend-new/specs/redo.spec.ts @@ -0,0 +1,65 @@ +import {expect, test} from "@playwright/test"; +import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper"; + +test.beforeEach(async ({ page })=>{ + await goToNewPad(page); +}) + + +test.describe('undo button then redo button', function () { + + + test('redo some typing with button', async function ({page}) { + const padBody = await getPadBody(page); + + // get the first text element inside the editable space + const $firstTextElement = padBody.locator('div span').first(); + const originalValue = await $firstTextElement.textContent(); // get the original value + const newString = 'Foo'; + + await $firstTextElement.focus() + expect(await $firstTextElement.textContent()).toContain(originalValue); + await padBody.click() + await clearPadContent(page) + await writeToPad(page, newString); // send line 1 to the pad + + const modifiedValue = await $firstTextElement.textContent(); // get the modified value + expect(modifiedValue).not.toBe(originalValue); // expect the value to change + + // get undo and redo buttons // click the buttons + await page.locator('.buttonicon-undo').click() // removes foo + await page.locator('.buttonicon-redo').click() // resends foo + + await expect($firstTextElement).toHaveText(newString); + + const finalValue = await padBody.locator('div').first().textContent(); + expect(finalValue).toBe(modifiedValue); // expect the value to change + }); + + test('redo some typing with keypress', async function ({page}) { + const padBody = await getPadBody(page); + + // get the first text element inside the editable space + const $firstTextElement = padBody.locator('div span').first(); + const originalValue = await $firstTextElement.textContent(); // get the original value + const newString = 'Foo'; + + await padBody.click() + await clearPadContent(page) + await writeToPad(page, newString); // send line 1 to the pad + const modifiedValue = await $firstTextElement.textContent(); // get the modified value + expect(modifiedValue).not.toBe(originalValue); // expect the value to change + + // undo the change + await padBody.click() + await page.keyboard.press('Control+Z'); + + await page.keyboard.press('Control+Y'); // redo the change + + + await expect($firstTextElement).toHaveText(newString); + + const finalValue = await padBody.locator('div').first().textContent(); + expect(finalValue).toBe(modifiedValue); // expect the value to change + }); +}); diff --git a/src/tests/frontend-new/specs/strikethrough.spec.ts b/src/tests/frontend-new/specs/strikethrough.spec.ts new file mode 100644 index 000000000..a4f68b4a7 --- /dev/null +++ b/src/tests/frontend-new/specs/strikethrough.spec.ts @@ -0,0 +1,30 @@ +import {expect, test} from "@playwright/test"; +import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper"; + +test.beforeEach(async ({ page })=>{ + await goToNewPad(page); +}) + +test.describe('strikethrough button', function () { + + test('makes text strikethrough', async function ({page}) { + const padBody = await getPadBody(page); + + // get the first text element out of the inner iframe + const $firstTextElement = padBody.locator('div').first(); + + // select this text element + await $firstTextElement.selectText() + + // get the strikethrough button and click it + await page.locator('.buttonicon-strikethrough').click(); + + // ace creates a new dom element when you press a button, just get the first text element again + + // is there a element now? + await expect($firstTextElement.locator('s')).toHaveCount(1); + + // make sure the text hasn't changed + expect(await $firstTextElement.textContent()).toEqual(await $firstTextElement.textContent()); + }); +}); diff --git a/src/tests/frontend-new/specs/timeslider.spec.ts b/src/tests/frontend-new/specs/timeslider.spec.ts new file mode 100644 index 000000000..317398f18 --- /dev/null +++ b/src/tests/frontend-new/specs/timeslider.spec.ts @@ -0,0 +1,37 @@ +import {expect, test} from "@playwright/test"; +import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper"; + +test.beforeEach(async ({ page })=>{ + // create a new pad before each test run + await goToNewPad(page); +}) + + +// deactivated, we need a nice way to get the timeslider, this is ugly +test.describe('timeslider button takes you to the timeslider of a pad', function () { + + test('timeslider contained in URL', async function ({page}) { + const padBody = await getPadBody(page); + await clearPadContent(page) + await writeToPad(page, 'Foo'); // send line 1 to the pad + + // get the first text element inside the editable space + const $firstTextElement = padBody.locator('div span').first(); + const originalValue = await $firstTextElement.textContent(); // get the original value + await $firstTextElement.click() + await writeToPad(page, 'Testing'); // send line 1 to the pad + + const modifiedValue = await $firstTextElement.textContent(); // get the modified value + expect(modifiedValue).not.toBe(originalValue); // expect the value to change + + const $timesliderButton = page.locator('.buttonicon-history'); + await $timesliderButton.click(); // So click the timeslider link + + await page.waitForSelector('#timeslider-wrapper') + + const iFrameURL = page.url(); // get the url + const inTimeslider = iFrameURL.indexOf('timeslider') !== -1; + + expect(inTimeslider).toBe(true); // expect the value to change + }); +}); diff --git a/src/tests/frontend-new/specs/timeslider_follow.spec.ts b/src/tests/frontend-new/specs/timeslider_follow.spec.ts new file mode 100644 index 000000000..9f104b884 --- /dev/null +++ b/src/tests/frontend-new/specs/timeslider_follow.spec.ts @@ -0,0 +1,76 @@ +'use strict'; +import {expect, Page, test} from "@playwright/test"; +import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper"; +import {gotoTimeslider} from "../helper/timeslider"; + +test.beforeEach(async ({ page })=>{ + await goToNewPad(page); +}) + + +test.describe('timeslider follow', function () { + + // TODO needs test if content is also followed, when user a makes edits + // while user b is in the timeslider + test("content as it's added to timeslider", async function ({page}) { + // send 6 revisions + const revs = 6; + const message = 'a\n\n\n\n\n\n\n\n\n\n'; + const newLines = message.split('\n').length; + for (let i = 0; i < revs; i++) { + await writeToPad(page, message) + } + + await gotoTimeslider(page,0); + expect(page.url()).toContain('#0'); + + const originalTop = await page.evaluate(() => { + return window.document.querySelector('#innerdocbody')!.getBoundingClientRect().top; + }); + + // set to follow contents as it arrives + await page.check('#options-followContents'); + await page.click('#playpause_button_icon'); + + // wait for the scroll + await page.waitForTimeout(1000) + + const currentOffset = await page.evaluate(() => { + return window.document.querySelector('#innerdocbody')!.getBoundingClientRect().top; + }); + + expect(currentOffset).toBeLessThanOrEqual(originalTop); + }); + + /** + * Tests for bug described in #4389 + * The goal is to scroll to the first line that contains a change right before + * the change is applied. + */ + test('only to lines that exist in the pad view, regression test for #4389', async function ({page}) { + const padBody = await getPadBody(page) + await padBody.click() + + await clearPadContent(page) + + await writeToPad(page,'Test line\n' + + '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' + + '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' + + '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'); + await padBody.locator('div').nth(40).click(); + await writeToPad(page, 'Another test line'); + + + await gotoTimeslider(page, 200); + + // set to follow contents as it arrives + await page.check('#options-followContents'); + + await page.waitForTimeout(1000) + + const oldYPosition = await page.locator('#editorcontainerbox').evaluate((el) => { + return el.scrollTop; + }) + expect(oldYPosition).toBe(0); + }); +}); diff --git a/src/tests/frontend-new/specs/undo.spec.ts b/src/tests/frontend-new/specs/undo.spec.ts new file mode 100644 index 000000000..cdbc12083 --- /dev/null +++ b/src/tests/frontend-new/specs/undo.spec.ts @@ -0,0 +1,56 @@ +'use strict'; + +import {expect, test} from "@playwright/test"; +import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper"; + +test.beforeEach(async ({ page })=>{ + await goToNewPad(page); +}) + + +test.describe('undo button', function () { + + test('undo some typing by clicking undo button', async function ({page}) { + const padBody = await getPadBody(page); + await padBody.click() + await clearPadContent(page) + + + // get the first text element inside the editable space + const firstTextElement = padBody.locator('div').first() + const originalValue = await firstTextElement.textContent(); // get the original value + await firstTextElement.focus() + + await writeToPad(page, 'foo'); // send line 1 to the pad + + const modifiedValue = await firstTextElement.textContent(); // get the modified value + expect(modifiedValue).not.toBe(originalValue); // expect the value to change + + // get clear authorship button as a variable + const undoButton = page.locator('.buttonicon-undo') + await undoButton.click() // click the button + + await expect(firstTextElement).toHaveText(originalValue!); + }); + + test('undo some typing using a keypress', async function ({page}) { + const padBody = await getPadBody(page); + await padBody.click() + await clearPadContent(page) + + // get the first text element inside the editable space + const firstTextElement = padBody.locator('div').first() + const originalValue = await firstTextElement.textContent(); // get the original value + + await firstTextElement.focus() + await writeToPad(page, 'foo'); // send line 1 to the pad + const modifiedValue = await firstTextElement.textContent(); // get the modified value + expect(modifiedValue).not.toBe(originalValue); // expect the value to change + + // undo the change + await page.keyboard.press('Control+Z'); + await page.waitForTimeout(1000) + + await expect(firstTextElement).toHaveText(originalValue!); + }); +}); diff --git a/src/tests/frontend-new/specs/unordered_list.spec.ts b/src/tests/frontend-new/specs/unordered_list.spec.ts new file mode 100644 index 000000000..a2465e5af --- /dev/null +++ b/src/tests/frontend-new/specs/unordered_list.spec.ts @@ -0,0 +1,127 @@ +import {expect, test} from "@playwright/test"; +import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper"; + +test.beforeEach(async ({ page })=>{ + // create a new pad before each test run + await goToNewPad(page); +}) + +test.describe('unordered_list.js', function () { + test.describe('assign unordered list', function () { + test('insert unordered list text then removes by outdent', async function ({page}) { + const padBody = await getPadBody(page); + const originalText = await padBody.locator('div').first().textContent(); + + const $insertunorderedlistButton = page.locator('.buttonicon-insertunorderedlist'); + await $insertunorderedlistButton.click(); + + await expect(padBody.locator('div').first()).toHaveText(originalText!); + await expect(padBody.locator('div ul li')).toHaveCount(1); + + // remove indentation by bullet and ensure text string remains the same + const $outdentButton = page.locator('.buttonicon-outdent'); + await $outdentButton.click(); + await expect(padBody.locator('div').first()).toHaveText(originalText!); + }); + }); + + test.describe('unassign unordered list', function () { + // create a new pad before each test run + + + test('insert unordered list text then remove by clicking list again', async function ({page}) { + const padBody = await getPadBody(page); + const originalText = await padBody.locator('div').first().textContent(); + + await padBody.locator('div').first().selectText() + const $insertunorderedlistButton = page.locator('.buttonicon-insertunorderedlist'); + await $insertunorderedlistButton.click(); + + await expect(padBody.locator('div').first()).toHaveText(originalText!); + await expect(padBody.locator('div ul li')).toHaveCount(1); + + // remove indentation by bullet and ensure text string remains the same + await $insertunorderedlistButton.click(); + await expect(padBody.locator('div').locator('ul')).toHaveCount(0) + }); + }); + + + test.describe('keep unordered list on enter key', function () { + + test('Keeps the unordered list on enter for the new line', async function ({page}) { + const padBody = await getPadBody(page); + await clearPadContent(page) + await expect(padBody.locator('div')).toHaveCount(1) + + const $insertorderedlistButton = page.locator('.buttonicon-insertunorderedlist') + await $insertorderedlistButton.click(); + + // type a bit, make a line break and type again + const $firstTextElement = padBody.locator('div').first(); + await $firstTextElement.click() + await page.keyboard.type('line 1'); + await page.keyboard.press('Enter'); + await page.keyboard.type('line 2'); + await page.keyboard.press('Enter'); + + await expect(padBody.locator('div span')).toHaveCount(2); + + + const $newSecondLine = padBody.locator('div').nth(1) + await expect($newSecondLine.locator('ul')).toHaveCount(1); + await expect($newSecondLine).toHaveText('line 2'); + }); + }); + + test.describe('Pressing Tab in an UL increases and decreases indentation', function () { + + test('indent and de-indent list item with keypress', async function ({page}) { + const padBody = await getPadBody(page); + await clearPadContent(page) + + // get the first text element out of the inner iframe + const $firstTextElement = padBody.locator('div').first(); + + // select this text element + await $firstTextElement.selectText(); + + const $insertunorderedlistButton = page.locator('.buttonicon-insertunorderedlist'); + await $insertunorderedlistButton.click(); + + await padBody.locator('div').first().click(); + await page.keyboard.press('Home'); + await page.keyboard.press('Tab'); + await expect(padBody.locator('div').first().locator('.list-bullet2')).toHaveCount(1); + + await page.keyboard.press('Shift+Tab'); + + await expect(padBody.locator('div').first().locator('.list-bullet1')).toHaveCount(1); + }); + }); + + test.describe('Pressing indent/outdent button in an UL increases and decreases indentation ' + + 'and bullet / ol formatting', function () { + + test('indent and de-indent list item with indent button', async function ({page}) { + const padBody = await getPadBody(page); + + // get the first text element out of the inner iframe + const $firstTextElement = padBody.locator('div').first(); + + // select this text element + await $firstTextElement.selectText(); + + const $insertunorderedlistButton = page.locator('.buttonicon-insertunorderedlist'); + await $insertunorderedlistButton.click(); + + await page.locator('.buttonicon-indent').click(); + + await expect(padBody.locator('div').first().locator('.list-bullet2')).toHaveCount(1); + const outdentButton = page.locator('.buttonicon-outdent'); + await outdentButton.click(); + + await expect(padBody.locator('div').first().locator('.list-bullet1')).toHaveCount(1); + }); + }); +}); diff --git a/src/tests/frontend-new/specs/urls_become_clickable.spec.ts b/src/tests/frontend-new/specs/urls_become_clickable.spec.ts new file mode 100644 index 000000000..0397502bc --- /dev/null +++ b/src/tests/frontend-new/specs/urls_become_clickable.spec.ts @@ -0,0 +1,51 @@ +import {expect, test} from "@playwright/test"; +import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper"; + +test.beforeEach(async ({ page })=>{ + await goToNewPad(page); +}) + +test.describe('entering a URL makes a link', function () { + for (const url of ['https://etherpad.org', 'www.etherpad.org', 'https://www.etherpad.org']) { + test(url, async function ({page}) { + const padBody = await getPadBody(page); + await clearPadContent(page) + const url = 'https://etherpad.org'; + await writeToPad(page, url); + await expect(padBody.locator('div').first()).toHaveText(url); + await expect(padBody.locator('a')).toHaveText(url); + await expect(padBody.locator('a')).toHaveAttribute('href', url); + }); + } +}); + + +test.describe('special characters inside URL', async function () { + for (const char of '-:@_.,~%+/?=&#!;()[]$\'*') { + const url = `https://etherpad.org/${char}foo`; + test(url, async function ({page}) { + const padBody = await getPadBody(page); + await clearPadContent(page) + await padBody.click() + await clearPadContent(page) + await writeToPad(page, url); + await expect(padBody.locator('div').first()).toHaveText(url); + await expect(padBody.locator('a')).toHaveText(url); + await expect(padBody.locator('a')).toHaveAttribute('href', url); + }); + } +}); + +test.describe('punctuation after URL is ignored', ()=> { + for (const char of ':.,;?!)]\'*') { + const want = 'https://etherpad.org'; + const input = want + char; + test(input, async function ({page}) { + const padBody = await getPadBody(page); + await clearPadContent(page) + await writeToPad(page, input); + await expect(padBody.locator('a')).toHaveCount(1); + await expect(padBody.locator('a')).toHaveAttribute('href', want); + }); + } +}); diff --git a/src/tests/frontend/cypress/cypress.config.js b/src/tests/frontend/cypress/cypress.config.js new file mode 100644 index 000000000..3754350de --- /dev/null +++ b/src/tests/frontend/cypress/cypress.config.js @@ -0,0 +1,9 @@ +const { defineConfig } = require('cypress') + +module.exports = defineConfig({ + e2e: { + baseUrl: "http://127.0.0.1:9001", + supportFile: false, + specPattern: 'tests/frontend/cypress/integration/**/*.js' + } +}) diff --git a/src/tests/frontend/cypress/cypress.json b/src/tests/frontend/cypress/cypress.json deleted file mode 100644 index 780d73fae..000000000 --- a/src/tests/frontend/cypress/cypress.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "baseUrl": "http://127.0.0.1:9001" -} diff --git a/src/tests/frontend/easysync-helper.js b/src/tests/frontend/easysync-helper.js new file mode 100644 index 000000000..b4f770963 --- /dev/null +++ b/src/tests/frontend/easysync-helper.js @@ -0,0 +1,222 @@ +'use strict'; + +const Changeset = require('../../static/js/Changeset'); +const AttributePool = require('../../static/js/AttributePool'); + +const randInt = (maxValue) => Math.floor(Math.random() * maxValue); + +const poolOrArray = (attribs) => { + if (attribs.getAttrib) { + return attribs; // it's already an attrib pool + } else { + // assume it's an array of attrib strings to be split and added + const p = new AttributePool(); + attribs.forEach((kv) => { + p.putAttrib(kv.split(',')); + }); + return p; + } +}; +exports.poolOrArray = poolOrArray; + +const randomInlineString = (len) => { + const assem = Changeset.stringAssembler(); + for (let i = 0; i < len; i++) { + assem.append(String.fromCharCode(randInt(26) + 97)); + } + return assem.toString(); +}; + +const randomMultiline = (approxMaxLines, approxMaxCols) => { + const numParts = randInt(approxMaxLines * 2) + 1; + const txt = Changeset.stringAssembler(); + txt.append(randInt(2) ? '\n' : ''); + for (let i = 0; i < numParts; i++) { + if ((i % 2) === 0) { + if (randInt(10)) { + txt.append(randomInlineString(randInt(approxMaxCols) + 1)); + } else { + txt.append('\n'); + } + } else { + txt.append('\n'); + } + } + return txt.toString(); +}; +exports.randomMultiline = randomMultiline; + +const randomStringOperation = (numCharsLeft) => { + let result; + switch (randInt(11)) { + case 0: + { + // insert char + result = { + insert: randomInlineString(1), + }; + break; + } + case 1: + { + // delete char + result = { + remove: 1, + }; + break; + } + case 2: + { + // skip char + result = { + skip: 1, + }; + break; + } + case 3: + { + // insert small + result = { + insert: randomInlineString(randInt(4) + 1), + }; + break; + } + case 4: + { + // delete small + result = { + remove: randInt(4) + 1, + }; + break; + } + case 5: + { + // skip small + result = { + skip: randInt(4) + 1, + }; + break; + } + case 6: + { + // insert multiline; + result = { + insert: randomMultiline(5, 20), + }; + break; + } + case 7: + { + // delete multiline + result = { + remove: Math.round(numCharsLeft * Math.random() * Math.random()), + }; + break; + } + case 8: + { + // skip multiline + result = { + skip: Math.round(numCharsLeft * Math.random() * Math.random()), + }; + break; + } + case 9: + { + // delete to end + result = { + remove: numCharsLeft, + }; + break; + } + case 10: + { + // skip to end + result = { + skip: numCharsLeft, + }; + break; + } + } + const maxOrig = numCharsLeft - 1; + if ('remove' in result) { + result.remove = Math.min(result.remove, maxOrig); + } else if ('skip' in result) { + result.skip = Math.min(result.skip, maxOrig); + } + return result; +}; + +const randomTwoPropAttribs = (opcode) => { + // assumes attrib pool like ['apple,','apple,true','banana,','banana,true'] + if (opcode === '-' || randInt(3)) { + return ''; + } else if (randInt(3)) { // eslint-disable-line no-dupe-else-if + if (opcode === '+' || randInt(2)) { + return `*${Changeset.numToString(randInt(2) * 2 + 1)}`; + } else { + return `*${Changeset.numToString(randInt(2) * 2)}`; + } + } else if (opcode === '+' || randInt(4) === 0) { + return '*1*3'; + } else { + return ['*0*2', '*0*3', '*1*2'][randInt(3)]; + } +}; + +const randomTestChangeset = (origText, withAttribs) => { + const charBank = Changeset.stringAssembler(); + let textLeft = origText; // always keep final newline + const outTextAssem = Changeset.stringAssembler(); + const opAssem = Changeset.smartOpAssembler(); + const oldLen = origText.length; + + const nextOp = new Changeset.Op(); + + const appendMultilineOp = (opcode, txt) => { + nextOp.opcode = opcode; + if (withAttribs) { + nextOp.attribs = randomTwoPropAttribs(opcode); + } + txt.replace(/\n|[^\n]+/g, (t) => { + if (t === '\n') { + nextOp.chars = 1; + nextOp.lines = 1; + opAssem.append(nextOp); + } else { + nextOp.chars = t.length; + nextOp.lines = 0; + opAssem.append(nextOp); + } + return ''; + }); + }; + + const doOp = () => { + const o = randomStringOperation(textLeft.length); + if (o.insert) { + const txt = o.insert; + charBank.append(txt); + outTextAssem.append(txt); + appendMultilineOp('+', txt); + } else if (o.skip) { + const txt = textLeft.substring(0, o.skip); + textLeft = textLeft.substring(o.skip); + outTextAssem.append(txt); + appendMultilineOp('=', txt); + } else if (o.remove) { + const txt = textLeft.substring(0, o.remove); + textLeft = textLeft.substring(o.remove); + appendMultilineOp('-', txt); + } + }; + + while (textLeft.length > 1) doOp(); + for (let i = 0; i < 5; i++) doOp(); // do some more (only insertions will happen) + const outText = `${outTextAssem.toString()}\n`; + opAssem.endDocument(); + const cs = Changeset.pack(oldLen, outText.length, opAssem.toString(), charBank.toString()); + Changeset.checkRep(cs); + return [cs, outText]; +}; +exports.randomTestChangeset = randomTestChangeset; diff --git a/src/tests/frontend/helper.js b/src/tests/frontend/helper.js index dbb5d4b2d..18981897e 100644 --- a/src/tests/frontend/helper.js +++ b/src/tests/frontend/helper.js @@ -181,7 +181,9 @@ const helper = {}; helper.padOuter$.fx.off = true; helper.padInner$.fx.off = true; - return opts.id; + // Don't return opts.id -- the server might have redirected the browser to a transformed version + // of the requested pad ID. + return helper.padChrome$.window.clientVars.padId; }; helper.newAdmin = async (page) => { diff --git a/src/tests/frontend/helper/methods.js b/src/tests/frontend/helper/methods.ts similarity index 94% rename from src/tests/frontend/helper/methods.js rename to src/tests/frontend/helper/methods.ts index b828cd601..f1da92371 100644 --- a/src/tests/frontend/helper/methods.js +++ b/src/tests/frontend/helper/methods.ts @@ -1,4 +1,4 @@ -'use strict'; +// @ts-nocheck /** * Spys on socket.io messages and saves them into several arrays @@ -37,7 +37,7 @@ helper.edit = async (message, line) => { await helper.withFastCommit(async (incorp) => { helper.linesDiv()[line].sendkeys(message); incorp(); - await helper.waitForPromise(() => editsNum + 1 === helper.commits.length); + await helper.waitForPromise(() => editsNum + 1 === helper.commits.length, 10000); }); }; @@ -94,7 +94,7 @@ helper.sendChatMessage = async (message) => { */ helper.showSettings = async () => { if (helper.isSettingsShown()) return; - helper.settingsButton().click(); + helper.settingsButton().trigger('click'); await helper.waitForPromise(() => helper.isSettingsShown(), 2000); }; @@ -106,7 +106,7 @@ helper.showSettings = async () => { */ helper.hideSettings = async () => { if (!helper.isSettingsShown()) return; - helper.settingsButton().click(); + helper.settingsButton().trigger('click'); await helper.waitForPromise(() => !helper.isSettingsShown(), 2000); }; @@ -119,7 +119,7 @@ helper.hideSettings = async () => { helper.enableStickyChatviaSettings = async () => { const stickyChat = helper.padChrome$('#options-stickychat'); if (!helper.isSettingsShown() || stickyChat.is(':checked')) return; - stickyChat.click(); + stickyChat.trigger('click'); await helper.waitForPromise(() => helper.isChatboxSticky(), 2000); }; @@ -132,7 +132,7 @@ helper.enableStickyChatviaSettings = async () => { helper.disableStickyChatviaSettings = async () => { const stickyChat = helper.padChrome$('#options-stickychat'); if (!helper.isSettingsShown() || !stickyChat.is(':checked')) return; - stickyChat.click(); + stickyChat.trigger('click'); await helper.waitForPromise(() => !helper.isChatboxSticky(), 2000); }; @@ -145,7 +145,7 @@ helper.disableStickyChatviaSettings = async () => { helper.enableStickyChatviaIcon = async () => { const stickyChat = helper.padChrome$('#titlesticky'); if (!helper.isChatboxShown() || helper.isChatboxSticky()) return; - stickyChat.click(); + stickyChat.trigger('click'); await helper.waitForPromise(() => helper.isChatboxSticky(), 2000); }; @@ -157,7 +157,7 @@ helper.enableStickyChatviaIcon = async () => { */ helper.disableStickyChatviaIcon = async () => { if (!helper.isChatboxShown() || !helper.isChatboxSticky()) return; - helper.titlecross().click(); + helper.titlecross().trigger('click'); await helper.waitForPromise(() => !helper.isChatboxSticky(), 2000); }; @@ -175,9 +175,8 @@ helper.disableStickyChatviaIcon = async () => { */ helper.gotoTimeslider = async (revision) => { revision = Number.isInteger(revision) ? `#${revision}` : ''; - const iframe = $('#iframe-container iframe'); - iframe.attr('src', `${iframe.attr('src')}/timeslider${revision}`); - + helper.padChrome$.window.location.href = + `${helper.padChrome$.window.location.pathname}/timeslider${revision}`; await helper.waitForPromise(() => helper.timesliderTimerTime() && !Number.isNaN(new Date(helper.timesliderTimerTime()).getTime()), 10000); }; diff --git a/src/tests/frontend/helper/multipleUsers.js b/src/tests/frontend/helper/multipleUsers.ts similarity index 99% rename from src/tests/frontend/helper/multipleUsers.js rename to src/tests/frontend/helper/multipleUsers.ts index 831bf403e..261b8f63c 100644 --- a/src/tests/frontend/helper/multipleUsers.js +++ b/src/tests/frontend/helper/multipleUsers.ts @@ -1,4 +1,4 @@ -'use strict'; +// @ts-nocheck const getCookies = () => helper.padChrome$.window.require('ep_etherpad-lite/static/js/pad_utils').Cookies; diff --git a/src/tests/frontend/helper/ui.js b/src/tests/frontend/helper/ui.ts similarity index 95% rename from src/tests/frontend/helper/ui.js rename to src/tests/frontend/helper/ui.ts index 0524a95c2..69e6b7d40 100644 --- a/src/tests/frontend/helper/ui.js +++ b/src/tests/frontend/helper/ui.ts @@ -1,3 +1,4 @@ +// @ts-nocheck 'use strict'; /** @@ -16,7 +17,7 @@ helper.contentWindow = () => $('#iframe-container iframe')[0].contentWindow; helper.showChat = async () => { const chaticon = helper.chatIcon(); if (!chaticon.hasClass('visible')) return; - chaticon.click(); + chaticon.trigger('click'); await helper.waitForPromise(() => !chaticon.hasClass('visible'), 2000); }; @@ -27,7 +28,7 @@ helper.showChat = async () => { */ helper.hideChat = async () => { if (!helper.isChatboxShown() || helper.isChatboxSticky()) return; - helper.titlecross().click(); + helper.titlecross().trigger('click'); await helper.waitForPromise(() => !helper.isChatboxShown(), 2000); }; @@ -80,7 +81,7 @@ helper.settingsButton = helper.toggleUserList = async () => { const isVisible = helper.userListShown(); const button = helper.padChrome$("button[data-l10n-id='pad.toolbar.showusers.title']"); - button.click(); + button.trigger('click'); await helper.waitForPromise(() => !isVisible); }; @@ -104,9 +105,9 @@ helper.userListShown = () => helper.padChrome$('div#users').hasClass('popup-show */ helper.setUserName = async (name) => { const userElement = helper.usernameField(); - userElement.click(); + userElement.trigger('click'); userElement.val(name); - userElement.blur(); + userElement.trigger('blur'); await helper.waitForPromise(() => !helper.usernameField().hasClass('editactive')); }; diff --git a/src/tests/frontend/index.html b/src/tests/frontend/index.html index 22c42ab15..a03677b26 100644 --- a/src/tests/frontend/index.html +++ b/src/tests/frontend/index.html @@ -14,7 +14,6 @@
- @@ -22,7 +21,7 @@ - + diff --git a/src/tests/frontend/lib/expect.js b/src/tests/frontend/lib/expect.js deleted file mode 100644 index c647cf2be..000000000 --- a/src/tests/frontend/lib/expect.js +++ /dev/null @@ -1,1247 +0,0 @@ - -(function (global, module) { - - if ('undefined' == typeof module) { - var module = { exports: {} } - , exports = module.exports - } - - /** - * Exports. - */ - - module.exports = expect; - expect.Assertion = Assertion; - - /** - * Exports version. - */ - - expect.version = '0.1.2'; - - /** - * Possible assertion flags. - */ - - var flags = { - not: ['to', 'be', 'have', 'include', 'only'] - , to: ['be', 'have', 'include', 'only', 'not'] - , only: ['have'] - , have: ['own'] - , be: ['an'] - }; - - function expect (obj) { - return new Assertion(obj); - } - - /** - * Constructor - * - * @api private - */ - - function Assertion (obj, flag, parent) { - this.obj = obj; - this.flags = {}; - - if (undefined != parent) { - this.flags[flag] = true; - - for (var i in parent.flags) { - if (parent.flags.hasOwnProperty(i)) { - this.flags[i] = true; - } - } - } - - var $flags = flag ? flags[flag] : keys(flags) - , self = this - - if ($flags) { - for (var i = 0, l = $flags.length; i < l; i++) { - // avoid recursion - if (this.flags[$flags[i]]) continue; - - var name = $flags[i] - , assertion = new Assertion(this.obj, name, this) - - if ('function' == typeof Assertion.prototype[name]) { - // clone the function, make sure we dont touch the prot reference - var old = this[name]; - this[name] = function () { - return old.apply(self, arguments); - } - - for (var fn in Assertion.prototype) { - if (Assertion.prototype.hasOwnProperty(fn) && fn != name) { - this[name][fn] = bind(assertion[fn], assertion); - } - } - } else { - this[name] = assertion; - } - } - } - }; - - /** - * Performs an assertion - * - * @api private - */ - - Assertion.prototype.assert = function (truth, msg, error) { - var msg = this.flags.not ? error : msg - , ok = this.flags.not ? !truth : truth; - - if (!ok) { - throw new Error(msg.call(this)); - } - - this.and = new Assertion(this.obj); - }; - - /** - * Check if the value is truthy - * - * @api public - */ - - Assertion.prototype.ok = function () { - this.assert( - !!this.obj - , function(){ return 'expected ' + i(this.obj) + ' to be truthy' } - , function(){ return 'expected ' + i(this.obj) + ' to be falsy' }); - }; - - /** - * Assert that the function throws. - * - * @param {Function|RegExp} callback, or regexp to match error string against - * @api public - */ - - Assertion.prototype.throwError = - Assertion.prototype.throwException = function (fn) { - expect(this.obj).to.be.a('function'); - - var thrown = false - , not = this.flags.not - - try { - this.obj(); - } catch (e) { - if ('function' == typeof fn) { - fn(e); - } else if ('object' == typeof fn) { - var subject = 'string' == typeof e ? e : e.message; - if (not) { - expect(subject).to.not.match(fn); - } else { - expect(subject).to.match(fn); - } - } - thrown = true; - } - - if ('object' == typeof fn && not) { - // in the presence of a matcher, ensure the `not` only applies to - // the matching. - this.flags.not = false; - } - - var name = this.obj.name || 'fn'; - this.assert( - thrown - , function(){ return 'expected ' + name + ' to throw an exception' } - , function(){ return 'expected ' + name + ' not to throw an exception' }); - }; - - /** - * Checks if the array is empty. - * - * @api public - */ - - Assertion.prototype.empty = function () { - var expectation; - - if ('object' == typeof this.obj && null !== this.obj && !isArray(this.obj)) { - if ('number' == typeof this.obj.length) { - expectation = !this.obj.length; - } else { - expectation = !keys(this.obj).length; - } - } else { - if ('string' != typeof this.obj) { - expect(this.obj).to.be.an('object'); - } - - expect(this.obj).to.have.property('length'); - expectation = !this.obj.length; - } - - this.assert( - expectation - , function(){ return 'expected ' + i(this.obj) + ' to be empty' } - , function(){ return 'expected ' + i(this.obj) + ' to not be empty' }); - return this; - }; - - /** - * Checks if the obj exactly equals another. - * - * @api public - */ - - Assertion.prototype.be = - Assertion.prototype.equal = function (obj) { - this.assert( - obj === this.obj - , function(){ return 'expected ' + i(this.obj) + ' to equal ' + i(obj) } - , function(){ return 'expected ' + i(this.obj) + ' to not equal ' + i(obj) }); - return this; - }; - - /** - * Checks if the obj sortof equals another. - * - * @api public - */ - - Assertion.prototype.eql = function (obj) { - this.assert( - expect.eql(obj, this.obj) - , function(){ return 'expected ' + i(this.obj) + ' to sort of equal ' + i(obj) } - , function(){ return 'expected ' + i(this.obj) + ' to sort of not equal ' + i(obj) }); - return this; - }; - - /** - * Assert within start to finish (inclusive). - * - * @param {Number} start - * @param {Number} finish - * @api public - */ - - Assertion.prototype.within = function (start, finish) { - var range = start + '..' + finish; - this.assert( - this.obj >= start && this.obj <= finish - , function(){ return 'expected ' + i(this.obj) + ' to be within ' + range } - , function(){ return 'expected ' + i(this.obj) + ' to not be within ' + range }); - return this; - }; - - /** - * Assert typeof / instance of - * - * @api public - */ - - Assertion.prototype.a = - Assertion.prototype.an = function (type) { - if ('string' == typeof type) { - // proper english in error msg - var n = /^[aeiou]/.test(type) ? 'n' : ''; - - // typeof with support for 'array' - this.assert( - 'array' == type ? isArray(this.obj) : - 'object' == type - ? 'object' == typeof this.obj && null !== this.obj - : type == typeof this.obj - , function(){ return 'expected ' + i(this.obj) + ' to be a' + n + ' ' + type } - , function(){ return 'expected ' + i(this.obj) + ' not to be a' + n + ' ' + type }); - } else { - // instanceof - var name = type.name || 'supplied constructor'; - this.assert( - this.obj instanceof type - , function(){ return 'expected ' + i(this.obj) + ' to be an instance of ' + name } - , function(){ return 'expected ' + i(this.obj) + ' not to be an instance of ' + name }); - } - - return this; - }; - - /** - * Assert numeric value above _n_. - * - * @param {Number} n - * @api public - */ - - Assertion.prototype.greaterThan = - Assertion.prototype.above = function (n) { - this.assert( - this.obj > n - , function(){ return 'expected ' + i(this.obj) + ' to be above ' + n } - , function(){ return 'expected ' + i(this.obj) + ' to be below ' + n }); - return this; - }; - - /** - * Assert numeric value below _n_. - * - * @param {Number} n - * @api public - */ - - Assertion.prototype.lessThan = - Assertion.prototype.below = function (n) { - this.assert( - this.obj < n - , function(){ return 'expected ' + i(this.obj) + ' to be below ' + n } - , function(){ return 'expected ' + i(this.obj) + ' to be above ' + n }); - return this; - }; - - /** - * Assert string value matches _regexp_. - * - * @param {RegExp} regexp - * @api public - */ - - Assertion.prototype.match = function (regexp) { - this.assert( - regexp.exec(this.obj) - , function(){ return 'expected ' + i(this.obj) + ' to match ' + regexp } - , function(){ return 'expected ' + i(this.obj) + ' not to match ' + regexp }); - return this; - }; - - /** - * Assert property "length" exists and has value of _n_. - * - * @param {Number} n - * @api public - */ - - Assertion.prototype.length = function (n) { - expect(this.obj).to.have.property('length'); - var len = this.obj.length; - this.assert( - n == len - , function(){ return 'expected ' + i(this.obj) + ' to have a length of ' + n + ' but got ' + len } - , function(){ return 'expected ' + i(this.obj) + ' to not have a length of ' + len }); - return this; - }; - - /** - * Assert property _name_ exists, with optional _val_. - * - * @param {String} name - * @param {Mixed} val - * @api public - */ - - Assertion.prototype.property = function (name, val) { - if (this.flags.own) { - this.assert( - Object.prototype.hasOwnProperty.call(this.obj, name) - , function(){ return 'expected ' + i(this.obj) + ' to have own property ' + i(name) } - , function(){ return 'expected ' + i(this.obj) + ' to not have own property ' + i(name) }); - return this; - } - - if (this.flags.not && undefined !== val) { - if (undefined === this.obj[name]) { - throw new Error(i(this.obj) + ' has no property ' + i(name)); - } - } else { - var hasProp; - try { - hasProp = name in this.obj - } catch (e) { - hasProp = undefined !== this.obj[name] - } - - this.assert( - hasProp - , function(){ return 'expected ' + i(this.obj) + ' to have a property ' + i(name) } - , function(){ return 'expected ' + i(this.obj) + ' to not have a property ' + i(name) }); - } - - if (undefined !== val) { - this.assert( - val === this.obj[name] - , function(){ return 'expected ' + i(this.obj) + ' to have a property ' + i(name) - + ' of ' + i(val) + ', but got ' + i(this.obj[name]) } - , function(){ return 'expected ' + i(this.obj) + ' to not have a property ' + i(name) - + ' of ' + i(val) }); - } - - this.obj = this.obj[name]; - return this; - }; - - /** - * Assert that the array contains _obj_ or string contains _obj_. - * - * @param {Mixed} obj|string - * @api public - */ - - Assertion.prototype.string = - Assertion.prototype.contain = function (obj) { - if ('string' == typeof this.obj) { - this.assert( - ~this.obj.indexOf(obj) - , function(){ return 'expected ' + i(this.obj) + ' to contain ' + i(obj) } - , function(){ return 'expected ' + i(this.obj) + ' to not contain ' + i(obj) }); - } else { - this.assert( - ~indexOf(this.obj, obj) - , function(){ return 'expected ' + i(this.obj) + ' to contain ' + i(obj) } - , function(){ return 'expected ' + i(this.obj) + ' to not contain ' + i(obj) }); - } - return this; - }; - - /** - * Assert exact keys or inclusion of keys by using - * the `.own` modifier. - * - * @param {Array|String ...} keys - * @api public - */ - - Assertion.prototype.key = - Assertion.prototype.keys = function ($keys) { - var str - , ok = true; - - $keys = isArray($keys) - ? $keys - : Array.prototype.slice.call(arguments); - - if (!$keys.length) throw new Error('keys required'); - - var actual = keys(this.obj) - , len = $keys.length; - - // Inclusion - ok = every($keys, function (key) { - return ~indexOf(actual, key); - }); - - // Strict - if (!this.flags.not && this.flags.only) { - ok = ok && $keys.length == actual.length; - } - - // Key string - if (len > 1) { - $keys = map($keys, function (key) { - return i(key); - }); - var last = $keys.pop(); - str = $keys.join(', ') + ', and ' + last; - } else { - str = i($keys[0]); - } - - // Form - str = (len > 1 ? 'keys ' : 'key ') + str; - - // Have / include - str = (!this.flags.only ? 'include ' : 'only have ') + str; - - // Assertion - this.assert( - ok - , function(){ return 'expected ' + i(this.obj) + ' to ' + str } - , function(){ return 'expected ' + i(this.obj) + ' to not ' + str }); - - return this; - }; - /** - * Assert a failure. - * - * @param {String ...} custom message - * @api public - */ - Assertion.prototype.fail = function (msg) { - msg = msg || "explicit failure"; - this.assert(false, msg, msg); - return this; - }; - - /** - * Function bind implementation. - */ - - function bind (fn, scope) { - return function () { - return fn.apply(scope, arguments); - } - } - - /** - * Array every compatibility - * - * @see bit.ly/5Fq1N2 - * @api public - */ - - function every (arr, fn, thisObj) { - var scope = thisObj || global; - for (var i = 0, j = arr.length; i < j; ++i) { - if (!fn.call(scope, arr[i], i, arr)) { - return false; - } - } - return true; - }; - - /** - * Array indexOf compatibility. - * - * @see bit.ly/a5Dxa2 - * @api public - */ - - function indexOf (arr, o, i) { - if (Array.prototype.indexOf) { - return Array.prototype.indexOf.call(arr, o, i); - } - - if (arr.length === undefined) { - return -1; - } - - for (var j = arr.length, i = i < 0 ? i + j < 0 ? 0 : i + j : i || 0 - ; i < j && arr[i] !== o; i++); - - return j <= i ? -1 : i; - }; - - // https://gist.github.com/1044128/ - var getOuterHTML = function(element) { - if ('outerHTML' in element) return element.outerHTML; - var ns = "http://www.w3.org/1999/xhtml"; - var container = document.createElementNS(ns, '_'); - var elemProto = (window.HTMLElement || window.Element).prototype; - var xmlSerializer = new XMLSerializer(); - var html; - if (document.xmlVersion) { - return xmlSerializer.serializeToString(element); - } else { - container.appendChild(element.cloneNode(false)); - html = container.innerHTML.replace('><', '>' + element.innerHTML + '<'); - container.innerHTML = ''; - return html; - } - }; - - // Returns true if object is a DOM element. - var isDOMElement = function (object) { - if (typeof HTMLElement === 'object') { - return object instanceof HTMLElement; - } else { - return object && - typeof object === 'object' && - object.nodeType === 1 && - typeof object.nodeName === 'string'; - } - }; - - /** - * Inspects an object. - * - * @see taken from node.js `util` module (copyright Joyent, MIT license) - * @api private - */ - - function i (obj, showHidden, depth) { - var seen = []; - - function stylize (str) { - return str; - }; - - function format (value, recurseTimes) { - // Provide a hook for user-specified inspect functions. - // Check that value is an object with an inspect function on it - if (value && typeof value.inspect === 'function' && - // Filter out the util module, it's inspect function is special - value !== exports && - // Also filter out any prototype objects using the circular check. - !(value.constructor && value.constructor.prototype === value)) { - return value.inspect(recurseTimes); - } - - // Primitive types cannot have properties - switch (typeof value) { - case 'undefined': - return stylize('undefined', 'undefined'); - - case 'string': - var simple = '\'' + json.stringify(value).replace(/^"|"$/g, '') - .replace(/'/g, "\\'") - .replace(/\\"/g, '"') + '\''; - return stylize(simple, 'string'); - - case 'number': - return stylize('' + value, 'number'); - - case 'boolean': - return stylize('' + value, 'boolean'); - } - // For some reason typeof null is "object", so special case here. - if (value === null) { - return stylize('null', 'null'); - } - - if (isDOMElement(value)) { - return getOuterHTML(value); - } - - // Look up the keys of the object. - var visible_keys = keys(value); - var $keys = showHidden ? Object.getOwnPropertyNames(value) : visible_keys; - - // Functions without properties can be shortcutted. - if (typeof value === 'function' && $keys.length === 0) { - if (isRegExp(value)) { - return stylize('' + value, 'regexp'); - } else { - var name = value.name ? ': ' + value.name : ''; - return stylize('[Function' + name + ']', 'special'); - } - } - - // Dates without properties can be shortcutted - if (isDate(value) && $keys.length === 0) { - return stylize(value.toUTCString(), 'date'); - } - - var base, type, braces; - // Determine the object type - if (isArray(value)) { - type = 'Array'; - braces = ['[', ']']; - } else { - type = 'Object'; - braces = ['{', '}']; - } - - // Make functions say that they are functions - if (typeof value === 'function') { - var n = value.name ? ': ' + value.name : ''; - base = (isRegExp(value)) ? ' ' + value : ' [Function' + n + ']'; - } else { - base = ''; - } - - // Make dates with properties first say the date - if (isDate(value)) { - base = ' ' + value.toUTCString(); - } - - if ($keys.length === 0) { - return braces[0] + base + braces[1]; - } - - if (recurseTimes < 0) { - if (isRegExp(value)) { - return stylize('' + value, 'regexp'); - } else { - return stylize('[Object]', 'special'); - } - } - - seen.push(value); - - var output = map($keys, function (key) { - var name, str; - if (value.__lookupGetter__) { - if (value.__lookupGetter__(key)) { - if (value.__lookupSetter__(key)) { - str = stylize('[Getter/Setter]', 'special'); - } else { - str = stylize('[Getter]', 'special'); - } - } else { - if (value.__lookupSetter__(key)) { - str = stylize('[Setter]', 'special'); - } - } - } - if (indexOf(visible_keys, key) < 0) { - name = '[' + key + ']'; - } - if (!str) { - if (indexOf(seen, value[key]) < 0) { - if (recurseTimes === null) { - str = format(value[key]); - } else { - str = format(value[key], recurseTimes - 1); - } - if (str.indexOf('\n') > -1) { - if (isArray(value)) { - str = map(str.split('\n'), function (line) { - return ' ' + line; - }).join('\n').substr(2); - } else { - str = '\n' + map(str.split('\n'), function (line) { - return ' ' + line; - }).join('\n'); - } - } - } else { - str = stylize('[Circular]', 'special'); - } - } - if (typeof name === 'undefined') { - if (type === 'Array' && key.match(/^\d+$/)) { - return str; - } - name = json.stringify('' + key); - if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { - name = name.substr(1, name.length - 2); - name = stylize(name, 'name'); - } else { - name = name.replace(/'/g, "\\'") - .replace(/\\"/g, '"') - .replace(/(^"|"$)/g, "'"); - name = stylize(name, 'string'); - } - } - - return name + ': ' + str; - }); - - seen.pop(); - - var numLinesEst = 0; - var length = reduce(output, function (prev, cur) { - numLinesEst++; - if (indexOf(cur, '\n') >= 0) numLinesEst++; - return prev + cur.length + 1; - }, 0); - - if (length > 50) { - output = braces[0] + - (base === '' ? '' : base + '\n ') + - ' ' + - output.join(',\n ') + - ' ' + - braces[1]; - - } else { - output = braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; - } - - return output; - } - return format(obj, (typeof depth === 'undefined' ? 2 : depth)); - }; - - function isArray (ar) { - return Object.prototype.toString.call(ar) == '[object Array]'; - }; - - function isRegExp(re) { - var s = '' + re; - return re instanceof RegExp || // easy case - // duck-type for context-switching evalcx case - typeof(re) === 'function' && - re.constructor.name === 'RegExp' && - re.compile && - re.test && - re.exec && - s.match(/^\/.*\/[gim]{0,3}$/); - }; - - function isDate(d) { - if (d instanceof Date) return true; - return false; - }; - - function keys (obj) { - if (Object.keys) { - return Object.keys(obj); - } - - var keys = []; - - for (var i in obj) { - if (Object.prototype.hasOwnProperty.call(obj, i)) { - keys.push(i); - } - } - - return keys; - } - - function map (arr, mapper, that) { - if (Array.prototype.map) { - return Array.prototype.map.call(arr, mapper, that); - } - - var other= new Array(arr.length); - - for (var i= 0, n = arr.length; i= 2) { - var rv = arguments[1]; - } else { - do { - if (i in this) { - rv = this[i++]; - break; - } - - // if array contains no values, no initial value to return - if (++i >= len) - throw new TypeError(); - } while (true); - } - - for (; i < len; i++) { - if (i in this) - rv = fun.call(null, rv, this[i], i, this); - } - - return rv; - }; - - /** - * Asserts deep equality - * - * @see taken from node.js `assert` module (copyright Joyent, MIT license) - * @api private - */ - - expect.eql = function eql (actual, expected) { - // 7.1. All identical values are equivalent, as determined by ===. - if (actual === expected) { - return true; - } else if ('undefined' != typeof Buffer - && Buffer.isBuffer(actual) && Buffer.isBuffer(expected)) { - if (actual.length != expected.length) return false; - - for (var i = 0; i < actual.length; i++) { - if (actual[i] !== expected[i]) return false; - } - - return true; - - // 7.2. If the expected value is a Date object, the actual value is - // equivalent if it is also a Date object that refers to the same time. - } else if (actual instanceof Date && expected instanceof Date) { - return actual.getTime() === expected.getTime(); - - // 7.3. Other pairs that do not both pass typeof value == "object", - // equivalence is determined by ==. - } else if (typeof actual != 'object' && typeof expected != 'object') { - return actual == expected; - - // 7.4. For all other Object pairs, including Array objects, equivalence is - // determined by having the same number of owned properties (as verified - // with Object.prototype.hasOwnProperty.call), the same set of keys - // (although not necessarily the same order), equivalent values for every - // corresponding key, and an identical "prototype" property. Note: this - // accounts for both named and indexed properties on Arrays. - } else { - return objEquiv(actual, expected); - } - } - - function isUndefinedOrNull (value) { - return value === null || value === undefined; - } - - function isArguments (object) { - return Object.prototype.toString.call(object) == '[object Arguments]'; - } - - function objEquiv (a, b) { - if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) - return false; - // an identical "prototype" property. - if (a.prototype !== b.prototype) return false; - //~~~I've managed to break Object.keys through screwy arguments passing. - // Converting to array solves the problem. - if (isArguments(a)) { - if (!isArguments(b)) { - return false; - } - a = pSlice.call(a); - b = pSlice.call(b); - return expect.eql(a, b); - } - try{ - var ka = keys(a), - kb = keys(b), - key, i; - } catch (e) {//happens when one is a string literal and the other isn't - return false; - } - // having the same number of owned properties (keys incorporates hasOwnProperty) - if (ka.length != kb.length) - return false; - //the same set of keys (although not necessarily the same order), - ka.sort(); - kb.sort(); - //~~~cheap key test - for (i = ka.length - 1; i >= 0; i--) { - if (ka[i] != kb[i]) - return false; - } - //equivalent values for every corresponding key, and - //~~~possibly expensive deep test - for (i = ka.length - 1; i >= 0; i--) { - key = ka[i]; - if (!expect.eql(a[key], b[key])) - return false; - } - return true; - } - - var json = (function () { - "use strict"; - - if ('object' == typeof JSON && JSON.parse && JSON.stringify) { - return { - parse: nativeJSON.parse - , stringify: nativeJSON.stringify - } - } - - var JSON = {}; - - function f(n) { - // Format integers to have at least two digits. - return n < 10 ? '0' + n : n; - } - - function date(d, key) { - return isFinite(d.valueOf()) ? - d.getUTCFullYear() + '-' + - f(d.getUTCMonth() + 1) + '-' + - f(d.getUTCDate()) + 'T' + - f(d.getUTCHours()) + ':' + - f(d.getUTCMinutes()) + ':' + - f(d.getUTCSeconds()) + 'Z' : null; - }; - - var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, - escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, - gap, - indent, - meta = { // table of character substitutions - '\b': '\\b', - '\t': '\\t', - '\n': '\\n', - '\f': '\\f', - '\r': '\\r', - '"' : '\\"', - '\\': '\\\\' - }, - rep; - - - function quote(string) { - - // If the string contains no control characters, no quote characters, and no - // backslash characters, then we can safely slap some quotes around it. - // Otherwise we must also replace the offending characters with safe escape - // sequences. - - escapable.lastIndex = 0; - return escapable.test(string) ? '"' + string.replace(escapable, function (a) { - var c = meta[a]; - return typeof c === 'string' ? c : - '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); - }) + '"' : '"' + string + '"'; - } - - - function str(key, holder) { - - // Produce a string from holder[key]. - - var i, // The loop counter. - k, // The member key. - v, // The member value. - length, - mind = gap, - partial, - value = holder[key]; - - // If the value has a toJSON method, call it to obtain a replacement value. - - if (value instanceof Date) { - value = date(key); - } - - // If we were called with a replacer function, then call the replacer to - // obtain a replacement value. - - if (typeof rep === 'function') { - value = rep.call(holder, key, value); - } - - // What happens next depends on the value's type. - - switch (typeof value) { - case 'string': - return quote(value); - - case 'number': - - // JSON numbers must be finite. Encode non-finite numbers as null. - - return isFinite(value) ? String(value) : 'null'; - - case 'boolean': - case 'null': - - // If the value is a boolean or null, convert it to a string. Note: - // typeof null does not produce 'null'. The case is included here in - // the remote chance that this gets fixed someday. - - return String(value); - - // If the type is 'object', we might be dealing with an object or an array or - // null. - - case 'object': - - // Due to a specification blunder in ECMAScript, typeof null is 'object', - // so watch out for that case. - - if (!value) { - return 'null'; - } - - // Make an array to hold the partial results of stringifying this object value. - - gap += indent; - partial = []; - - // Is the value an array? - - if (Object.prototype.toString.apply(value) === '[object Array]') { - - // The value is an array. Stringify every element. Use null as a placeholder - // for non-JSON values. - - length = value.length; - for (i = 0; i < length; i += 1) { - partial[i] = str(i, value) || 'null'; - } - - // Join all of the elements together, separated with commas, and wrap them in - // brackets. - - v = partial.length === 0 ? '[]' : gap ? - '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : - '[' + partial.join(',') + ']'; - gap = mind; - return v; - } - - // If the replacer is an array, use it to select the members to be stringified. - - if (rep && typeof rep === 'object') { - length = rep.length; - for (i = 0; i < length; i += 1) { - if (typeof rep[i] === 'string') { - k = rep[i]; - v = str(k, value); - if (v) { - partial.push(quote(k) + (gap ? ': ' : ':') + v); - } - } - } - } else { - - // Otherwise, iterate through all of the keys in the object. - - for (k in value) { - if (Object.prototype.hasOwnProperty.call(value, k)) { - v = str(k, value); - if (v) { - partial.push(quote(k) + (gap ? ': ' : ':') + v); - } - } - } - } - - // Join all of the member texts together, separated with commas, - // and wrap them in braces. - - v = partial.length === 0 ? '{}' : gap ? - '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : - '{' + partial.join(',') + '}'; - gap = mind; - return v; - } - } - - // If the JSON object does not yet have a stringify method, give it one. - - JSON.stringify = function (value, replacer, space) { - - // The stringify method takes a value and an optional replacer, and an optional - // space parameter, and returns a JSON text. The replacer can be a function - // that can replace values, or an array of strings that will select the keys. - // A default replacer method can be provided. Use of the space parameter can - // produce text that is more easily readable. - - var i; - gap = ''; - indent = ''; - - // If the space parameter is a number, make an indent string containing that - // many spaces. - - if (typeof space === 'number') { - for (i = 0; i < space; i += 1) { - indent += ' '; - } - - // If the space parameter is a string, it will be used as the indent string. - - } else if (typeof space === 'string') { - indent = space; - } - - // If there is a replacer, it must be a function or an array. - // Otherwise, throw an error. - - rep = replacer; - if (replacer && typeof replacer !== 'function' && - (typeof replacer !== 'object' || - typeof replacer.length !== 'number')) { - throw new Error('JSON.stringify'); - } - - // Make a fake root object containing our value under the key of ''. - // Return the result of stringifying the value. - - return str('', {'': value}); - }; - - // If the JSON object does not yet have a parse method, give it one. - - JSON.parse = function (text, reviver) { - // The parse method takes a text and an optional reviver function, and returns - // a JavaScript value if the text is a valid JSON text. - - var j; - - function walk(holder, key) { - - // The walk method is used to recursively walk the resulting structure so - // that modifications can be made. - - var k, v, value = holder[key]; - if (value && typeof value === 'object') { - for (k in value) { - if (Object.prototype.hasOwnProperty.call(value, k)) { - v = walk(value, k); - if (v !== undefined) { - value[k] = v; - } else { - delete value[k]; - } - } - } - } - return reviver.call(holder, key, value); - } - - - // Parsing happens in four stages. In the first stage, we replace certain - // Unicode characters with escape sequences. JavaScript handles many characters - // incorrectly, either silently deleting them, or treating them as line endings. - - text = String(text); - cx.lastIndex = 0; - if (cx.test(text)) { - text = text.replace(cx, function (a) { - return '\\u' + - ('0000' + a.charCodeAt(0).toString(16)).slice(-4); - }); - } - - // In the second stage, we run the text against regular expressions that look - // for non-JSON patterns. We are especially concerned with '()' and 'new' - // because they can cause invocation, and '=' because it can cause mutation. - // But just to be safe, we want to reject all unexpected forms. - - // We split the second stage into 4 regexp operations in order to work around - // crippling inefficiencies in IE's and Safari's regexp engines. First we - // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we - // replace all simple value tokens with ']' characters. Third, we delete all - // open brackets that follow a colon or comma or that begin the text. Finally, - // we look to see that the remaining characters are only whitespace or ']' or - // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. - - if (/^[\],:{}\s]*$/ - .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') - .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') - .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { - - // In the third stage we use the eval function to compile the text into a - // JavaScript structure. The '{' operator is subject to a syntactic ambiguity - // in JavaScript: it can begin a block or an object literal. We wrap the text - // in parens to eliminate the ambiguity. - - j = eval('(' + text + ')'); - - // In the optional fourth stage, we recursively walk the new structure, passing - // each name/value pair to a reviver function for possible transformation. - - return typeof reviver === 'function' ? - walk({'': j}, '') : j; - } - - // If the text is not JSON parseable, then a SyntaxError is thrown. - - throw new SyntaxError('JSON.parse'); - }; - - return JSON; - })(); - - if ('undefined' != typeof window) { - window.expect = module.exports; - } - -})( - this - , 'undefined' != typeof module ? module : {} - , 'undefined' != typeof exports ? exports : {} -); diff --git a/src/tests/frontend/lib/mocha.js b/src/tests/frontend/lib/mocha.js deleted file mode 100644 index 031b6e446..000000000 --- a/src/tests/frontend/lib/mocha.js +++ /dev/null @@ -1,18115 +0,0 @@ -(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i} promise determining if browser notification - * permissible when fulfilled. - */ -function isPermitted() { - var permitted = { - granted: function allow() { - return Promise.resolve(true); - }, - denied: function deny() { - return Promise.resolve(false); - }, - default: function ask() { - return Notification.requestPermission().then(function(permission) { - return permission === 'granted'; - }); - } - }; - - return permitted[Notification.permission](); -} - -/** - * @summary - * Determines if notification should proceed. - * - * @description - * Notification shall not proceed unless `value` is true. - * - * `value` will equal one of: - *
    - *
  • true (from `isPermitted`)
  • - *
  • false (from `isPermitted`)
  • - *
  • undefined (from `Promise.race`)
  • - *
- * - * @private - * @param {boolean|undefined} value - Determines if notification permissible. - * @returns {Promise} Notification can proceed - */ -function canNotify(value) { - if (!value) { - var why = value === false ? 'blocked' : 'unacknowledged'; - var reason = 'not permitted by user (' + why + ')'; - return Promise.reject(new Error(reason)); - } - return Promise.resolve(); -} - -/** - * Displays the notification. - * - * @private - * @param {Runner} runner - Runner instance. - */ -function display(runner) { - var stats = runner.stats; - var symbol = { - cross: '\u274C', - tick: '\u2705' - }; - var logo = require('../../package').notifyLogo; - var _message; - var message; - var title; - - if (stats.failures) { - _message = stats.failures + ' of ' + stats.tests + ' tests failed'; - message = symbol.cross + ' ' + _message; - title = 'Failed'; - } else { - _message = stats.passes + ' tests passed in ' + stats.duration + 'ms'; - message = symbol.tick + ' ' + _message; - title = 'Passed'; - } - - // Send notification - var options = { - badge: logo, - body: message, - dir: 'ltr', - icon: logo, - lang: 'en-US', - name: 'mocha', - requireInteraction: false, - timestamp: Date.now() - }; - var notification = new Notification(title, options); - - // Autoclose after brief delay (makes various browsers act same) - var FORCE_DURATION = 4000; - setTimeout(notification.close.bind(notification), FORCE_DURATION); -} - -/** - * As notifications are tangential to our purpose, just log the error. - * - * @private - * @param {Error} err - Why notification didn't happen. - */ -function notPermitted(err) { - console.error('notification error:', err.message); -} - -}).call(this,require('_process'),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"../../package":90,"../runner":34,"_process":69}],3:[function(require,module,exports){ -'use strict'; - -/** - * Expose `Progress`. - */ - -module.exports = Progress; - -/** - * Initialize a new `Progress` indicator. - */ -function Progress() { - this.percent = 0; - this.size(0); - this.fontSize(11); - this.font('helvetica, arial, sans-serif'); -} - -/** - * Set progress size to `size`. - * - * @public - * @param {number} size - * @return {Progress} Progress instance. - */ -Progress.prototype.size = function(size) { - this._size = size; - return this; -}; - -/** - * Set text to `text`. - * - * @public - * @param {string} text - * @return {Progress} Progress instance. - */ -Progress.prototype.text = function(text) { - this._text = text; - return this; -}; - -/** - * Set font size to `size`. - * - * @public - * @param {number} size - * @return {Progress} Progress instance. - */ -Progress.prototype.fontSize = function(size) { - this._fontSize = size; - return this; -}; - -/** - * Set font to `family`. - * - * @param {string} family - * @return {Progress} Progress instance. - */ -Progress.prototype.font = function(family) { - this._font = family; - return this; -}; - -/** - * Update percentage to `n`. - * - * @param {number} n - * @return {Progress} Progress instance. - */ -Progress.prototype.update = function(n) { - this.percent = n; - return this; -}; - -/** - * Draw on `ctx`. - * - * @param {CanvasRenderingContext2d} ctx - * @return {Progress} Progress instance. - */ -Progress.prototype.draw = function(ctx) { - try { - var percent = Math.min(this.percent, 100); - var size = this._size; - var half = size / 2; - var x = half; - var y = half; - var rad = half - 1; - var fontSize = this._fontSize; - - ctx.font = fontSize + 'px ' + this._font; - - var angle = Math.PI * 2 * (percent / 100); - ctx.clearRect(0, 0, size, size); - - // outer circle - ctx.strokeStyle = '#9f9f9f'; - ctx.beginPath(); - ctx.arc(x, y, rad, 0, angle, false); - ctx.stroke(); - - // inner circle - ctx.strokeStyle = '#eee'; - ctx.beginPath(); - ctx.arc(x, y, rad - 1, 0, angle, true); - ctx.stroke(); - - // text - var text = this._text || (percent | 0) + '%'; - var w = ctx.measureText(text).width; - - ctx.fillText(text, x - w / 2 + 1, y + fontSize / 2 - 1); - } catch (ignore) { - // don't fail if we can't render progress - } - return this; -}; - -},{}],4:[function(require,module,exports){ -(function (global){ -'use strict'; - -exports.isatty = function isatty() { - return true; -}; - -exports.getWindowSize = function getWindowSize() { - if ('innerHeight' in global) { - return [global.innerHeight, global.innerWidth]; - } - // In a Web Worker, the DOM Window is not available. - return [640, 480]; -}; - -}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{}],5:[function(require,module,exports){ -'use strict'; -/** - * @module Context - */ -/** - * Expose `Context`. - */ - -module.exports = Context; - -/** - * Initialize a new `Context`. - * - * @private - */ -function Context() {} - -/** - * Set or get the context `Runnable` to `runnable`. - * - * @private - * @param {Runnable} runnable - * @return {Context} context - */ -Context.prototype.runnable = function(runnable) { - if (!arguments.length) { - return this._runnable; - } - this.test = this._runnable = runnable; - return this; -}; - -/** - * Set or get test timeout `ms`. - * - * @private - * @param {number} ms - * @return {Context} self - */ -Context.prototype.timeout = function(ms) { - if (!arguments.length) { - return this.runnable().timeout(); - } - this.runnable().timeout(ms); - return this; -}; - -/** - * Set test timeout `enabled`. - * - * @private - * @param {boolean} enabled - * @return {Context} self - */ -Context.prototype.enableTimeouts = function(enabled) { - if (!arguments.length) { - return this.runnable().enableTimeouts(); - } - this.runnable().enableTimeouts(enabled); - return this; -}; - -/** - * Set or get test slowness threshold `ms`. - * - * @private - * @param {number} ms - * @return {Context} self - */ -Context.prototype.slow = function(ms) { - if (!arguments.length) { - return this.runnable().slow(); - } - this.runnable().slow(ms); - return this; -}; - -/** - * Mark a test as skipped. - * - * @private - * @throws Pending - */ -Context.prototype.skip = function() { - this.runnable().skip(); -}; - -/** - * Set or get a number of allowed retries on failed tests - * - * @private - * @param {number} n - * @return {Context} self - */ -Context.prototype.retries = function(n) { - if (!arguments.length) { - return this.runnable().retries(); - } - this.runnable().retries(n); - return this; -}; - -},{}],6:[function(require,module,exports){ -'use strict'; -/** - * @module Errors - */ -/** - * Factory functions to create throwable error objects - */ - -/** - * Creates an error object to be thrown when no files to be tested could be found using specified pattern. - * - * @public - * @param {string} message - Error message to be displayed. - * @param {string} pattern - User-specified argument value. - * @returns {Error} instance detailing the error condition - */ -function createNoFilesMatchPatternError(message, pattern) { - var err = new Error(message); - err.code = 'ERR_MOCHA_NO_FILES_MATCH_PATTERN'; - err.pattern = pattern; - return err; -} - -/** - * Creates an error object to be thrown when the reporter specified in the options was not found. - * - * @public - * @param {string} message - Error message to be displayed. - * @param {string} reporter - User-specified reporter value. - * @returns {Error} instance detailing the error condition - */ -function createInvalidReporterError(message, reporter) { - var err = new TypeError(message); - err.code = 'ERR_MOCHA_INVALID_REPORTER'; - err.reporter = reporter; - return err; -} - -/** - * Creates an error object to be thrown when the interface specified in the options was not found. - * - * @public - * @param {string} message - Error message to be displayed. - * @param {string} ui - User-specified interface value. - * @returns {Error} instance detailing the error condition - */ -function createInvalidInterfaceError(message, ui) { - var err = new Error(message); - err.code = 'ERR_MOCHA_INVALID_INTERFACE'; - err.interface = ui; - return err; -} - -/** - * Creates an error object to be thrown when a behavior, option, or parameter is unsupported. - * - * @public - * @param {string} message - Error message to be displayed. - * @returns {Error} instance detailing the error condition - */ -function createUnsupportedError(message) { - var err = new Error(message); - err.code = 'ERR_MOCHA_UNSUPPORTED'; - return err; -} - -/** - * Creates an error object to be thrown when an argument is missing. - * - * @public - * @param {string} message - Error message to be displayed. - * @param {string} argument - Argument name. - * @param {string} expected - Expected argument datatype. - * @returns {Error} instance detailing the error condition - */ -function createMissingArgumentError(message, argument, expected) { - return createInvalidArgumentTypeError(message, argument, expected); -} - -/** - * Creates an error object to be thrown when an argument did not use the supported type - * - * @public - * @param {string} message - Error message to be displayed. - * @param {string} argument - Argument name. - * @param {string} expected - Expected argument datatype. - * @returns {Error} instance detailing the error condition - */ -function createInvalidArgumentTypeError(message, argument, expected) { - var err = new TypeError(message); - err.code = 'ERR_MOCHA_INVALID_ARG_TYPE'; - err.argument = argument; - err.expected = expected; - err.actual = typeof argument; - return err; -} - -/** - * Creates an error object to be thrown when an argument did not use the supported value - * - * @public - * @param {string} message - Error message to be displayed. - * @param {string} argument - Argument name. - * @param {string} value - Argument value. - * @param {string} [reason] - Why value is invalid. - * @returns {Error} instance detailing the error condition - */ -function createInvalidArgumentValueError(message, argument, value, reason) { - var err = new TypeError(message); - err.code = 'ERR_MOCHA_INVALID_ARG_VALUE'; - err.argument = argument; - err.value = value; - err.reason = typeof reason !== 'undefined' ? reason : 'is invalid'; - return err; -} - -/** - * Creates an error object to be thrown when an exception was caught, but the `Error` is falsy or undefined. - * - * @public - * @param {string} message - Error message to be displayed. - * @returns {Error} instance detailing the error condition - */ -function createInvalidExceptionError(message, value) { - var err = new Error(message); - err.code = 'ERR_MOCHA_INVALID_EXCEPTION'; - err.valueType = typeof value; - err.value = value; - return err; -} - -module.exports = { - createInvalidArgumentTypeError: createInvalidArgumentTypeError, - createInvalidArgumentValueError: createInvalidArgumentValueError, - createInvalidExceptionError: createInvalidExceptionError, - createInvalidInterfaceError: createInvalidInterfaceError, - createInvalidReporterError: createInvalidReporterError, - createMissingArgumentError: createMissingArgumentError, - createNoFilesMatchPatternError: createNoFilesMatchPatternError, - createUnsupportedError: createUnsupportedError -}; - -},{}],7:[function(require,module,exports){ -'use strict'; - -var Runnable = require('./runnable'); -var inherits = require('./utils').inherits; - -/** - * Expose `Hook`. - */ - -module.exports = Hook; - -/** - * Initialize a new `Hook` with the given `title` and callback `fn` - * - * @class - * @extends Runnable - * @param {String} title - * @param {Function} fn - */ -function Hook(title, fn) { - Runnable.call(this, title, fn); - this.type = 'hook'; -} - -/** - * Inherit from `Runnable.prototype`. - */ -inherits(Hook, Runnable); - -/** - * Get or set the test `err`. - * - * @memberof Hook - * @public - * @param {Error} err - * @return {Error} - */ -Hook.prototype.error = function(err) { - if (!arguments.length) { - err = this._error; - this._error = null; - return err; - } - - this._error = err; -}; - -},{"./runnable":33,"./utils":38}],8:[function(require,module,exports){ -'use strict'; - -var Test = require('../test'); -var EVENT_FILE_PRE_REQUIRE = require('../suite').constants - .EVENT_FILE_PRE_REQUIRE; - -/** - * BDD-style interface: - * - * describe('Array', function() { - * describe('#indexOf()', function() { - * it('should return -1 when not present', function() { - * // ... - * }); - * - * it('should return the index when present', function() { - * // ... - * }); - * }); - * }); - * - * @param {Suite} suite Root suite. - */ -module.exports = function bddInterface(suite) { - var suites = [suite]; - - suite.on(EVENT_FILE_PRE_REQUIRE, function(context, file, mocha) { - var common = require('./common')(suites, context, mocha); - - context.before = common.before; - context.after = common.after; - context.beforeEach = common.beforeEach; - context.afterEach = common.afterEach; - context.run = mocha.options.delay && common.runWithSuite(suite); - /** - * Describe a "suite" with the given `title` - * and callback `fn` containing nested suites - * and/or tests. - */ - - context.describe = context.context = function(title, fn) { - return common.suite.create({ - title: title, - file: file, - fn: fn - }); - }; - - /** - * Pending describe. - */ - - context.xdescribe = context.xcontext = context.describe.skip = function( - title, - fn - ) { - return common.suite.skip({ - title: title, - file: file, - fn: fn - }); - }; - - /** - * Exclusive suite. - */ - - context.describe.only = function(title, fn) { - return common.suite.only({ - title: title, - file: file, - fn: fn - }); - }; - - /** - * Describe a specification or test-case - * with the given `title` and callback `fn` - * acting as a thunk. - */ - - context.it = context.specify = function(title, fn) { - var suite = suites[0]; - if (suite.isPending()) { - fn = null; - } - var test = new Test(title, fn); - test.file = file; - suite.addTest(test); - return test; - }; - - /** - * Exclusive test-case. - */ - - context.it.only = function(title, fn) { - return common.test.only(mocha, context.it(title, fn)); - }; - - /** - * Pending test case. - */ - - context.xit = context.xspecify = context.it.skip = function(title) { - return context.it(title); - }; - - /** - * Number of attempts to retry. - */ - context.it.retries = function(n) { - context.retries(n); - }; - }); -}; - -module.exports.description = 'BDD or RSpec style [default]'; - -},{"../suite":36,"../test":37,"./common":9}],9:[function(require,module,exports){ -'use strict'; - -var Suite = require('../suite'); -var errors = require('../errors'); -var createMissingArgumentError = errors.createMissingArgumentError; - -/** - * Functions common to more than one interface. - * - * @param {Suite[]} suites - * @param {Context} context - * @param {Mocha} mocha - * @return {Object} An object containing common functions. - */ -module.exports = function(suites, context, mocha) { - /** - * Check if the suite should be tested. - * - * @private - * @param {Suite} suite - suite to check - * @returns {boolean} - */ - function shouldBeTested(suite) { - return ( - !mocha.options.grep || - (mocha.options.grep && - mocha.options.grep.test(suite.fullTitle()) && - !mocha.options.invert) - ); - } - - return { - /** - * This is only present if flag --delay is passed into Mocha. It triggers - * root suite execution. - * - * @param {Suite} suite The root suite. - * @return {Function} A function which runs the root suite - */ - runWithSuite: function runWithSuite(suite) { - return function run() { - suite.run(); - }; - }, - - /** - * Execute before running tests. - * - * @param {string} name - * @param {Function} fn - */ - before: function(name, fn) { - suites[0].beforeAll(name, fn); - }, - - /** - * Execute after running tests. - * - * @param {string} name - * @param {Function} fn - */ - after: function(name, fn) { - suites[0].afterAll(name, fn); - }, - - /** - * Execute before each test case. - * - * @param {string} name - * @param {Function} fn - */ - beforeEach: function(name, fn) { - suites[0].beforeEach(name, fn); - }, - - /** - * Execute after each test case. - * - * @param {string} name - * @param {Function} fn - */ - afterEach: function(name, fn) { - suites[0].afterEach(name, fn); - }, - - suite: { - /** - * Create an exclusive Suite; convenience function - * See docstring for create() below. - * - * @param {Object} opts - * @returns {Suite} - */ - only: function only(opts) { - opts.isOnly = true; - return this.create(opts); - }, - - /** - * Create a Suite, but skip it; convenience function - * See docstring for create() below. - * - * @param {Object} opts - * @returns {Suite} - */ - skip: function skip(opts) { - opts.pending = true; - return this.create(opts); - }, - - /** - * Creates a suite. - * - * @param {Object} opts Options - * @param {string} opts.title Title of Suite - * @param {Function} [opts.fn] Suite Function (not always applicable) - * @param {boolean} [opts.pending] Is Suite pending? - * @param {string} [opts.file] Filepath where this Suite resides - * @param {boolean} [opts.isOnly] Is Suite exclusive? - * @returns {Suite} - */ - create: function create(opts) { - var suite = Suite.create(suites[0], opts.title); - suite.pending = Boolean(opts.pending); - suite.file = opts.file; - suites.unshift(suite); - if (opts.isOnly) { - if (mocha.options.forbidOnly && shouldBeTested(suite)) { - throw new Error('`.only` forbidden'); - } - - suite.parent.appendOnlySuite(suite); - } - if (suite.pending) { - if (mocha.options.forbidPending && shouldBeTested(suite)) { - throw new Error('Pending test forbidden'); - } - } - if (typeof opts.fn === 'function') { - opts.fn.call(suite); - suites.shift(); - } else if (typeof opts.fn === 'undefined' && !suite.pending) { - throw createMissingArgumentError( - 'Suite "' + - suite.fullTitle() + - '" was defined but no callback was supplied. ' + - 'Supply a callback or explicitly skip the suite.', - 'callback', - 'function' - ); - } else if (!opts.fn && suite.pending) { - suites.shift(); - } - - return suite; - } - }, - - test: { - /** - * Exclusive test-case. - * - * @param {Object} mocha - * @param {Function} test - * @returns {*} - */ - only: function(mocha, test) { - test.parent.appendOnlyTest(test); - return test; - }, - - /** - * Pending test case. - * - * @param {string} title - */ - skip: function(title) { - context.test(title); - }, - - /** - * Number of retry attempts - * - * @param {number} n - */ - retries: function(n) { - context.retries(n); - } - } - }; -}; - -},{"../errors":6,"../suite":36}],10:[function(require,module,exports){ -'use strict'; -var Suite = require('../suite'); -var Test = require('../test'); - -/** - * Exports-style (as Node.js module) interface: - * - * exports.Array = { - * '#indexOf()': { - * 'should return -1 when the value is not present': function() { - * - * }, - * - * 'should return the correct index when the value is present': function() { - * - * } - * } - * }; - * - * @param {Suite} suite Root suite. - */ -module.exports = function(suite) { - var suites = [suite]; - - suite.on(Suite.constants.EVENT_FILE_REQUIRE, visit); - - function visit(obj, file) { - var suite; - for (var key in obj) { - if (typeof obj[key] === 'function') { - var fn = obj[key]; - switch (key) { - case 'before': - suites[0].beforeAll(fn); - break; - case 'after': - suites[0].afterAll(fn); - break; - case 'beforeEach': - suites[0].beforeEach(fn); - break; - case 'afterEach': - suites[0].afterEach(fn); - break; - default: - var test = new Test(key, fn); - test.file = file; - suites[0].addTest(test); - } - } else { - suite = Suite.create(suites[0], key); - suites.unshift(suite); - visit(obj[key], file); - suites.shift(); - } - } - } -}; - -module.exports.description = 'Node.js module ("exports") style'; - -},{"../suite":36,"../test":37}],11:[function(require,module,exports){ -'use strict'; - -exports.bdd = require('./bdd'); -exports.tdd = require('./tdd'); -exports.qunit = require('./qunit'); -exports.exports = require('./exports'); - -},{"./bdd":8,"./exports":10,"./qunit":12,"./tdd":13}],12:[function(require,module,exports){ -'use strict'; - -var Test = require('../test'); -var EVENT_FILE_PRE_REQUIRE = require('../suite').constants - .EVENT_FILE_PRE_REQUIRE; - -/** - * QUnit-style interface: - * - * suite('Array'); - * - * test('#length', function() { - * var arr = [1,2,3]; - * ok(arr.length == 3); - * }); - * - * test('#indexOf()', function() { - * var arr = [1,2,3]; - * ok(arr.indexOf(1) == 0); - * ok(arr.indexOf(2) == 1); - * ok(arr.indexOf(3) == 2); - * }); - * - * suite('String'); - * - * test('#length', function() { - * ok('foo'.length == 3); - * }); - * - * @param {Suite} suite Root suite. - */ -module.exports = function qUnitInterface(suite) { - var suites = [suite]; - - suite.on(EVENT_FILE_PRE_REQUIRE, function(context, file, mocha) { - var common = require('./common')(suites, context, mocha); - - context.before = common.before; - context.after = common.after; - context.beforeEach = common.beforeEach; - context.afterEach = common.afterEach; - context.run = mocha.options.delay && common.runWithSuite(suite); - /** - * Describe a "suite" with the given `title`. - */ - - context.suite = function(title) { - if (suites.length > 1) { - suites.shift(); - } - return common.suite.create({ - title: title, - file: file, - fn: false - }); - }; - - /** - * Exclusive Suite. - */ - - context.suite.only = function(title) { - if (suites.length > 1) { - suites.shift(); - } - return common.suite.only({ - title: title, - file: file, - fn: false - }); - }; - - /** - * Describe a specification or test-case - * with the given `title` and callback `fn` - * acting as a thunk. - */ - - context.test = function(title, fn) { - var test = new Test(title, fn); - test.file = file; - suites[0].addTest(test); - return test; - }; - - /** - * Exclusive test-case. - */ - - context.test.only = function(title, fn) { - return common.test.only(mocha, context.test(title, fn)); - }; - - context.test.skip = common.test.skip; - context.test.retries = common.test.retries; - }); -}; - -module.exports.description = 'QUnit style'; - -},{"../suite":36,"../test":37,"./common":9}],13:[function(require,module,exports){ -'use strict'; - -var Test = require('../test'); -var EVENT_FILE_PRE_REQUIRE = require('../suite').constants - .EVENT_FILE_PRE_REQUIRE; - -/** - * TDD-style interface: - * - * suite('Array', function() { - * suite('#indexOf()', function() { - * suiteSetup(function() { - * - * }); - * - * test('should return -1 when not present', function() { - * - * }); - * - * test('should return the index when present', function() { - * - * }); - * - * suiteTeardown(function() { - * - * }); - * }); - * }); - * - * @param {Suite} suite Root suite. - */ -module.exports = function(suite) { - var suites = [suite]; - - suite.on(EVENT_FILE_PRE_REQUIRE, function(context, file, mocha) { - var common = require('./common')(suites, context, mocha); - - context.setup = common.beforeEach; - context.teardown = common.afterEach; - context.suiteSetup = common.before; - context.suiteTeardown = common.after; - context.run = mocha.options.delay && common.runWithSuite(suite); - - /** - * Describe a "suite" with the given `title` and callback `fn` containing - * nested suites and/or tests. - */ - context.suite = function(title, fn) { - return common.suite.create({ - title: title, - file: file, - fn: fn - }); - }; - - /** - * Pending suite. - */ - context.suite.skip = function(title, fn) { - return common.suite.skip({ - title: title, - file: file, - fn: fn - }); - }; - - /** - * Exclusive test-case. - */ - context.suite.only = function(title, fn) { - return common.suite.only({ - title: title, - file: file, - fn: fn - }); - }; - - /** - * Describe a specification or test-case with the given `title` and - * callback `fn` acting as a thunk. - */ - context.test = function(title, fn) { - var suite = suites[0]; - if (suite.isPending()) { - fn = null; - } - var test = new Test(title, fn); - test.file = file; - suite.addTest(test); - return test; - }; - - /** - * Exclusive test-case. - */ - - context.test.only = function(title, fn) { - return common.test.only(mocha, context.test(title, fn)); - }; - - context.test.skip = common.test.skip; - context.test.retries = common.test.retries; - }); -}; - -module.exports.description = - 'traditional "suite"/"test" instead of BDD\'s "describe"/"it"'; - -},{"../suite":36,"../test":37,"./common":9}],14:[function(require,module,exports){ -(function (process,global){ -'use strict'; - -/*! - * mocha - * Copyright(c) 2011 TJ Holowaychuk - * MIT Licensed - */ - -var escapeRe = require('escape-string-regexp'); -var path = require('path'); -var builtinReporters = require('./reporters'); -var growl = require('./growl'); -var utils = require('./utils'); -var mocharc = require('./mocharc.json'); -var errors = require('./errors'); -var Suite = require('./suite'); -var esmUtils = utils.supportsEsModules() ? require('./esm-utils') : undefined; -var createStatsCollector = require('./stats-collector'); -var createInvalidReporterError = errors.createInvalidReporterError; -var createInvalidInterfaceError = errors.createInvalidInterfaceError; -var EVENT_FILE_PRE_REQUIRE = Suite.constants.EVENT_FILE_PRE_REQUIRE; -var EVENT_FILE_POST_REQUIRE = Suite.constants.EVENT_FILE_POST_REQUIRE; -var EVENT_FILE_REQUIRE = Suite.constants.EVENT_FILE_REQUIRE; -var sQuote = utils.sQuote; - -exports = module.exports = Mocha; - -/** - * To require local UIs and reporters when running in node. - */ - -if (!process.browser) { - var cwd = process.cwd(); - module.paths.push(cwd, path.join(cwd, 'node_modules')); -} - -/** - * Expose internals. - */ - -/** - * @public - * @class utils - * @memberof Mocha - */ -exports.utils = utils; -exports.interfaces = require('./interfaces'); -/** - * @public - * @memberof Mocha - */ -exports.reporters = builtinReporters; -exports.Runnable = require('./runnable'); -exports.Context = require('./context'); -/** - * - * @memberof Mocha - */ -exports.Runner = require('./runner'); -exports.Suite = Suite; -exports.Hook = require('./hook'); -exports.Test = require('./test'); - -/** - * Constructs a new Mocha instance with `options`. - * - * @public - * @class Mocha - * @param {Object} [options] - Settings object. - * @param {boolean} [options.allowUncaught] - Propagate uncaught errors? - * @param {boolean} [options.asyncOnly] - Force `done` callback or promise? - * @param {boolean} [options.bail] - Bail after first test failure? - * @param {boolean} [options.checkLeaks] - Check for global variable leaks? - * @param {boolean} [options.color] - Color TTY output from reporter? - * @param {boolean} [options.delay] - Delay root suite execution? - * @param {boolean} [options.diff] - Show diff on failure? - * @param {string} [options.fgrep] - Test filter given string. - * @param {boolean} [options.forbidOnly] - Tests marked `only` fail the suite? - * @param {boolean} [options.forbidPending] - Pending tests fail the suite? - * @param {boolean} [options.fullTrace] - Full stacktrace upon failure? - * @param {string[]} [options.global] - Variables expected in global scope. - * @param {RegExp|string} [options.grep] - Test filter given regular expression. - * @param {boolean} [options.growl] - Enable desktop notifications? - * @param {boolean} [options.inlineDiffs] - Display inline diffs? - * @param {boolean} [options.invert] - Invert test filter matches? - * @param {boolean} [options.noHighlighting] - Disable syntax highlighting? - * @param {string|constructor} [options.reporter] - Reporter name or constructor. - * @param {Object} [options.reporterOption] - Reporter settings object. - * @param {number} [options.retries] - Number of times to retry failed tests. - * @param {number} [options.slow] - Slow threshold value. - * @param {number|string} [options.timeout] - Timeout threshold value. - * @param {string} [options.ui] - Interface name. - */ -function Mocha(options) { - options = utils.assign({}, mocharc, options || {}); - this.files = []; - this.options = options; - // root suite - this.suite = new exports.Suite('', new exports.Context(), true); - - this.grep(options.grep) - .fgrep(options.fgrep) - .ui(options.ui) - .reporter( - options.reporter, - options.reporterOption || options.reporterOptions // reporterOptions was previously the only way to specify options to reporter - ) - .slow(options.slow) - .global(options.global); - - // this guard exists because Suite#timeout does not consider `undefined` to be valid input - if (typeof options.timeout !== 'undefined') { - this.timeout(options.timeout === false ? 0 : options.timeout); - } - - if ('retries' in options) { - this.retries(options.retries); - } - - [ - 'allowUncaught', - 'asyncOnly', - 'bail', - 'checkLeaks', - 'color', - 'delay', - 'diff', - 'forbidOnly', - 'forbidPending', - 'fullTrace', - 'growl', - 'inlineDiffs', - 'invert' - ].forEach(function(opt) { - if (options[opt]) { - this[opt](); - } - }, this); -} - -/** - * Enables or disables bailing on the first failure. - * - * @public - * @see [CLI option](../#-bail-b) - * @param {boolean} [bail=true] - Whether to bail on first error. - * @returns {Mocha} this - * @chainable - */ -Mocha.prototype.bail = function(bail) { - this.suite.bail(bail !== false); - return this; -}; - -/** - * @summary - * Adds `file` to be loaded for execution. - * - * @description - * Useful for generic setup code that must be included within test suite. - * - * @public - * @see [CLI option](../#-file-filedirectoryglob) - * @param {string} file - Pathname of file to be loaded. - * @returns {Mocha} this - * @chainable - */ -Mocha.prototype.addFile = function(file) { - this.files.push(file); - return this; -}; - -/** - * Sets reporter to `reporter`, defaults to "spec". - * - * @public - * @see [CLI option](../#-reporter-name-r-name) - * @see [Reporters](../#reporters) - * @param {String|Function} reporter - Reporter name or constructor. - * @param {Object} [reporterOptions] - Options used to configure the reporter. - * @returns {Mocha} this - * @chainable - * @throws {Error} if requested reporter cannot be loaded - * @example - * - * // Use XUnit reporter and direct its output to file - * mocha.reporter('xunit', { output: '/path/to/testspec.xunit.xml' }); - */ -Mocha.prototype.reporter = function(reporter, reporterOptions) { - if (typeof reporter === 'function') { - this._reporter = reporter; - } else { - reporter = reporter || 'spec'; - var _reporter; - // Try to load a built-in reporter. - if (builtinReporters[reporter]) { - _reporter = builtinReporters[reporter]; - } - // Try to load reporters from process.cwd() and node_modules - if (!_reporter) { - try { - _reporter = require(reporter); - } catch (err) { - if ( - err.code !== 'MODULE_NOT_FOUND' || - err.message.indexOf('Cannot find module') !== -1 - ) { - // Try to load reporters from a path (absolute or relative) - try { - _reporter = require(path.resolve(process.cwd(), reporter)); - } catch (_err) { - _err.code !== 'MODULE_NOT_FOUND' || - _err.message.indexOf('Cannot find module') !== -1 - ? console.warn(sQuote(reporter) + ' reporter not found') - : console.warn( - sQuote(reporter) + - ' reporter blew up with error:\n' + - err.stack - ); - } - } else { - console.warn( - sQuote(reporter) + ' reporter blew up with error:\n' + err.stack - ); - } - } - } - if (!_reporter) { - throw createInvalidReporterError( - 'invalid reporter ' + sQuote(reporter), - reporter - ); - } - this._reporter = _reporter; - } - this.options.reporterOption = reporterOptions; - // alias option name is used in public reporters xunit/tap/progress - this.options.reporterOptions = reporterOptions; - return this; -}; - -/** - * Sets test UI `name`, defaults to "bdd". - * - * @public - * @see [CLI option](../#-ui-name-u-name) - * @see [Interface DSLs](../#interfaces) - * @param {string|Function} [ui=bdd] - Interface name or class. - * @returns {Mocha} this - * @chainable - * @throws {Error} if requested interface cannot be loaded - */ -Mocha.prototype.ui = function(ui) { - var bindInterface; - if (typeof ui === 'function') { - bindInterface = ui; - } else { - ui = ui || 'bdd'; - bindInterface = exports.interfaces[ui]; - if (!bindInterface) { - try { - bindInterface = require(ui); - } catch (err) { - throw createInvalidInterfaceError( - 'invalid interface ' + sQuote(ui), - ui - ); - } - } - } - bindInterface(this.suite); - - this.suite.on(EVENT_FILE_PRE_REQUIRE, function(context) { - exports.afterEach = context.afterEach || context.teardown; - exports.after = context.after || context.suiteTeardown; - exports.beforeEach = context.beforeEach || context.setup; - exports.before = context.before || context.suiteSetup; - exports.describe = context.describe || context.suite; - exports.it = context.it || context.test; - exports.xit = context.xit || (context.test && context.test.skip); - exports.setup = context.setup || context.beforeEach; - exports.suiteSetup = context.suiteSetup || context.before; - exports.suiteTeardown = context.suiteTeardown || context.after; - exports.suite = context.suite || context.describe; - exports.teardown = context.teardown || context.afterEach; - exports.test = context.test || context.it; - exports.run = context.run; - }); - - return this; -}; - -/** - * Loads `files` prior to execution. Does not support ES Modules. - * - * @description - * The implementation relies on Node's `require` to execute - * the test interface functions and will be subject to its cache. - * Supports only CommonJS modules. To load ES modules, use Mocha#loadFilesAsync. - * - * @private - * @see {@link Mocha#addFile} - * @see {@link Mocha#run} - * @see {@link Mocha#unloadFiles} - * @see {@link Mocha#loadFilesAsync} - * @param {Function} [fn] - Callback invoked upon completion. - */ -Mocha.prototype.loadFiles = function(fn) { - var self = this; - var suite = this.suite; - this.files.forEach(function(file) { - file = path.resolve(file); - suite.emit(EVENT_FILE_PRE_REQUIRE, global, file, self); - suite.emit(EVENT_FILE_REQUIRE, require(file), file, self); - suite.emit(EVENT_FILE_POST_REQUIRE, global, file, self); - }); - fn && fn(); -}; - -/** - * Loads `files` prior to execution. Supports Node ES Modules. - * - * @description - * The implementation relies on Node's `require` and `import` to execute - * the test interface functions and will be subject to its cache. - * Supports both CJS and ESM modules. - * - * @public - * @see {@link Mocha#addFile} - * @see {@link Mocha#run} - * @see {@link Mocha#unloadFiles} - * @returns {Promise} - * @example - * - * // loads ESM (and CJS) test files asynchronously, then runs root suite - * mocha.loadFilesAsync() - * .then(() => mocha.run(failures => process.exitCode = failures ? 1 : 0)) - * .catch(() => process.exitCode = 1); - */ -Mocha.prototype.loadFilesAsync = function() { - var self = this; - var suite = this.suite; - this.loadAsync = true; - - if (!esmUtils) { - return new Promise(function(resolve) { - self.loadFiles(resolve); - }); - } - - return esmUtils.loadFilesAsync( - this.files, - function(file) { - suite.emit(EVENT_FILE_PRE_REQUIRE, global, file, self); - }, - function(file, resultModule) { - suite.emit(EVENT_FILE_REQUIRE, resultModule, file, self); - suite.emit(EVENT_FILE_POST_REQUIRE, global, file, self); - } - ); -}; - -/** - * Removes a previously loaded file from Node's `require` cache. - * - * @private - * @static - * @see {@link Mocha#unloadFiles} - * @param {string} file - Pathname of file to be unloaded. - */ -Mocha.unloadFile = function(file) { - delete require.cache[require.resolve(file)]; -}; - -/** - * Unloads `files` from Node's `require` cache. - * - * @description - * This allows required files to be "freshly" reloaded, providing the ability - * to reuse a Mocha instance programmatically. - * Note: does not clear ESM module files from the cache - * - * Intended for consumers — not used internally - * - * @public - * @see {@link Mocha#run} - * @returns {Mocha} this - * @chainable - */ -Mocha.prototype.unloadFiles = function() { - this.files.forEach(Mocha.unloadFile); - return this; -}; - -/** - * Sets `grep` filter after escaping RegExp special characters. - * - * @public - * @see {@link Mocha#grep} - * @param {string} str - Value to be converted to a regexp. - * @returns {Mocha} this - * @chainable - * @example - * - * // Select tests whose full title begins with `"foo"` followed by a period - * mocha.fgrep('foo.'); - */ -Mocha.prototype.fgrep = function(str) { - if (!str) { - return this; - } - return this.grep(new RegExp(escapeRe(str))); -}; - -/** - * @summary - * Sets `grep` filter used to select specific tests for execution. - * - * @description - * If `re` is a regexp-like string, it will be converted to regexp. - * The regexp is tested against the full title of each test (i.e., the - * name of the test preceded by titles of each its ancestral suites). - * As such, using an exact-match fixed pattern against the - * test name itself will not yield any matches. - *
- * Previous filter value will be overwritten on each call! - * - * @public - * @see [CLI option](../#-grep-regexp-g-regexp) - * @see {@link Mocha#fgrep} - * @see {@link Mocha#invert} - * @param {RegExp|String} re - Regular expression used to select tests. - * @return {Mocha} this - * @chainable - * @example - * - * // Select tests whose full title contains `"match"`, ignoring case - * mocha.grep(/match/i); - * @example - * - * // Same as above but with regexp-like string argument - * mocha.grep('/match/i'); - * @example - * - * // ## Anti-example - * // Given embedded test `it('only-this-test')`... - * mocha.grep('/^only-this-test$/'); // NO! Use `.only()` to do this! - */ -Mocha.prototype.grep = function(re) { - if (utils.isString(re)) { - // extract args if it's regex-like, i.e: [string, pattern, flag] - var arg = re.match(/^\/(.*)\/(g|i|)$|.*/); - this.options.grep = new RegExp(arg[1] || arg[0], arg[2]); - } else { - this.options.grep = re; - } - return this; -}; - -/** - * Inverts `grep` matches. - * - * @public - * @see {@link Mocha#grep} - * @return {Mocha} this - * @chainable - * @example - * - * // Select tests whose full title does *not* contain `"match"`, ignoring case - * mocha.grep(/match/i).invert(); - */ -Mocha.prototype.invert = function() { - this.options.invert = true; - return this; -}; - -/** - * Enables or disables ignoring global leaks. - * - * @deprecated since v7.0.0 - * @public - * @see {@link Mocha#checkLeaks} - * @param {boolean} [ignoreLeaks=false] - Whether to ignore global leaks. - * @return {Mocha} this - * @chainable - */ -Mocha.prototype.ignoreLeaks = function(ignoreLeaks) { - utils.deprecate( - '"ignoreLeaks()" is DEPRECATED, please use "checkLeaks()" instead.' - ); - this.options.checkLeaks = !ignoreLeaks; - return this; -}; - -/** - * Enables or disables checking for global variables leaked while running tests. - * - * @public - * @see [CLI option](../#-check-leaks) - * @param {boolean} [checkLeaks=true] - Whether to check for global variable leaks. - * @return {Mocha} this - * @chainable - */ -Mocha.prototype.checkLeaks = function(checkLeaks) { - this.options.checkLeaks = checkLeaks !== false; - return this; -}; - -/** - * Displays full stack trace upon test failure. - * - * @public - * @see [CLI option](../#-full-trace) - * @param {boolean} [fullTrace=true] - Whether to print full stacktrace upon failure. - * @return {Mocha} this - * @chainable - */ -Mocha.prototype.fullTrace = function(fullTrace) { - this.options.fullTrace = fullTrace !== false; - return this; -}; - -/** - * Enables desktop notification support if prerequisite software installed. - * - * @public - * @see [CLI option](../#-growl-g) - * @return {Mocha} this - * @chainable - */ -Mocha.prototype.growl = function() { - this.options.growl = this.isGrowlCapable(); - if (!this.options.growl) { - var detail = process.browser - ? 'notification support not available in this browser...' - : 'notification support prerequisites not installed...'; - console.error(detail + ' cannot enable!'); - } - return this; -}; - -/** - * @summary - * Determines if Growl support seems likely. - * - * @description - * Not available when run in browser. - * - * @private - * @see {@link Growl#isCapable} - * @see {@link Mocha#growl} - * @return {boolean} whether Growl support can be expected - */ -Mocha.prototype.isGrowlCapable = growl.isCapable; - -/** - * Implements desktop notifications using a pseudo-reporter. - * - * @private - * @see {@link Mocha#growl} - * @see {@link Growl#notify} - * @param {Runner} runner - Runner instance. - */ -Mocha.prototype._growl = growl.notify; - -/** - * Specifies whitelist of variable names to be expected in global scope. - * - * @public - * @see [CLI option](../#-global-variable-name) - * @see {@link Mocha#checkLeaks} - * @param {String[]|String} global - Accepted global variable name(s). - * @return {Mocha} this - * @chainable - * @example - * - * // Specify variables to be expected in global scope - * mocha.global(['jQuery', 'MyLib']); - */ -Mocha.prototype.global = function(global) { - this.options.global = (this.options.global || []) - .concat(global) - .filter(Boolean) - .filter(function(elt, idx, arr) { - return arr.indexOf(elt) === idx; - }); - return this; -}; -// for backwards compability, 'globals' is an alias of 'global' -Mocha.prototype.globals = Mocha.prototype.global; - -/** - * Enables or disables TTY color output by screen-oriented reporters. - * - * @deprecated since v7.0.0 - * @public - * @see {@link Mocha#color} - * @param {boolean} colors - Whether to enable color output. - * @return {Mocha} this - * @chainable - */ -Mocha.prototype.useColors = function(colors) { - utils.deprecate('"useColors()" is DEPRECATED, please use "color()" instead.'); - if (colors !== undefined) { - this.options.color = colors; - } - return this; -}; - -/** - * Enables or disables TTY color output by screen-oriented reporters. - * - * @public - * @see [CLI option](../#-color-c-colors) - * @param {boolean} [color=true] - Whether to enable color output. - * @return {Mocha} this - * @chainable - */ -Mocha.prototype.color = function(color) { - this.options.color = color !== false; - return this; -}; - -/** - * Determines if reporter should use inline diffs (rather than +/-) - * in test failure output. - * - * @deprecated since v7.0.0 - * @public - * @see {@link Mocha#inlineDiffs} - * @param {boolean} [inlineDiffs=false] - Whether to use inline diffs. - * @return {Mocha} this - * @chainable - */ -Mocha.prototype.useInlineDiffs = function(inlineDiffs) { - utils.deprecate( - '"useInlineDiffs()" is DEPRECATED, please use "inlineDiffs()" instead.' - ); - this.options.inlineDiffs = inlineDiffs !== undefined && inlineDiffs; - return this; -}; - -/** - * Enables or disables reporter to use inline diffs (rather than +/-) - * in test failure output. - * - * @public - * @see [CLI option](../#-inline-diffs) - * @param {boolean} [inlineDiffs=true] - Whether to use inline diffs. - * @return {Mocha} this - * @chainable - */ -Mocha.prototype.inlineDiffs = function(inlineDiffs) { - this.options.inlineDiffs = inlineDiffs !== false; - return this; -}; - -/** - * Determines if reporter should include diffs in test failure output. - * - * @deprecated since v7.0.0 - * @public - * @see {@link Mocha#diff} - * @param {boolean} [hideDiff=false] - Whether to hide diffs. - * @return {Mocha} this - * @chainable - */ -Mocha.prototype.hideDiff = function(hideDiff) { - utils.deprecate('"hideDiff()" is DEPRECATED, please use "diff()" instead.'); - this.options.diff = !(hideDiff === true); - return this; -}; - -/** - * Enables or disables reporter to include diff in test failure output. - * - * @public - * @see [CLI option](../#-diff) - * @param {boolean} [diff=true] - Whether to show diff on failure. - * @return {Mocha} this - * @chainable - */ -Mocha.prototype.diff = function(diff) { - this.options.diff = diff !== false; - return this; -}; - -/** - * @summary - * Sets timeout threshold value. - * - * @description - * A string argument can use shorthand (such as "2s") and will be converted. - * If the value is `0`, timeouts will be disabled. - * - * @public - * @see [CLI option](../#-timeout-ms-t-ms) - * @see [Timeouts](../#timeouts) - * @see {@link Mocha#enableTimeouts} - * @param {number|string} msecs - Timeout threshold value. - * @return {Mocha} this - * @chainable - * @example - * - * // Sets timeout to one second - * mocha.timeout(1000); - * @example - * - * // Same as above but using string argument - * mocha.timeout('1s'); - */ -Mocha.prototype.timeout = function(msecs) { - this.suite.timeout(msecs); - return this; -}; - -/** - * Sets the number of times to retry failed tests. - * - * @public - * @see [CLI option](../#-retries-n) - * @see [Retry Tests](../#retry-tests) - * @param {number} retry - Number of times to retry failed tests. - * @return {Mocha} this - * @chainable - * @example - * - * // Allow any failed test to retry one more time - * mocha.retries(1); - */ -Mocha.prototype.retries = function(n) { - this.suite.retries(n); - return this; -}; - -/** - * Sets slowness threshold value. - * - * @public - * @see [CLI option](../#-slow-ms-s-ms) - * @param {number} msecs - Slowness threshold value. - * @return {Mocha} this - * @chainable - * @example - * - * // Sets "slow" threshold to half a second - * mocha.slow(500); - * @example - * - * // Same as above but using string argument - * mocha.slow('0.5s'); - */ -Mocha.prototype.slow = function(msecs) { - this.suite.slow(msecs); - return this; -}; - -/** - * Enables or disables timeouts. - * - * @public - * @see [CLI option](../#-timeout-ms-t-ms) - * @param {boolean} enableTimeouts - Whether to enable timeouts. - * @return {Mocha} this - * @chainable - */ -Mocha.prototype.enableTimeouts = function(enableTimeouts) { - this.suite.enableTimeouts( - arguments.length && enableTimeouts !== undefined ? enableTimeouts : true - ); - return this; -}; - -/** - * Forces all tests to either accept a `done` callback or return a promise. - * - * @public - * @see [CLI option](../#-async-only-a) - * @param {boolean} [asyncOnly=true] - Wether to force `done` callback or promise. - * @return {Mocha} this - * @chainable - */ -Mocha.prototype.asyncOnly = function(asyncOnly) { - this.options.asyncOnly = asyncOnly !== false; - return this; -}; - -/** - * Disables syntax highlighting (in browser). - * - * @public - * @return {Mocha} this - * @chainable - */ -Mocha.prototype.noHighlighting = function() { - this.options.noHighlighting = true; - return this; -}; - -/** - * Enables or disables uncaught errors to propagate. - * - * @public - * @see [CLI option](../#-allow-uncaught) - * @param {boolean} [allowUncaught=true] - Whether to propagate uncaught errors. - * @return {Mocha} this - * @chainable - */ -Mocha.prototype.allowUncaught = function(allowUncaught) { - this.options.allowUncaught = allowUncaught !== false; - return this; -}; - -/** - * @summary - * Delays root suite execution. - * - * @description - * Used to perform asynch operations before any suites are run. - * - * @public - * @see [delayed root suite](../#delayed-root-suite) - * @returns {Mocha} this - * @chainable - */ -Mocha.prototype.delay = function delay() { - this.options.delay = true; - return this; -}; - -/** - * Causes tests marked `only` to fail the suite. - * - * @public - * @see [CLI option](../#-forbid-only) - * @param {boolean} [forbidOnly=true] - Whether tests marked `only` fail the suite. - * @returns {Mocha} this - * @chainable - */ -Mocha.prototype.forbidOnly = function(forbidOnly) { - this.options.forbidOnly = forbidOnly !== false; - return this; -}; - -/** - * Causes pending tests and tests marked `skip` to fail the suite. - * - * @public - * @see [CLI option](../#-forbid-pending) - * @param {boolean} [forbidPending=true] - Whether pending tests fail the suite. - * @returns {Mocha} this - * @chainable - */ -Mocha.prototype.forbidPending = function(forbidPending) { - this.options.forbidPending = forbidPending !== false; - return this; -}; - -/** - * Mocha version as specified by "package.json". - * - * @name Mocha#version - * @type string - * @readonly - */ -Object.defineProperty(Mocha.prototype, 'version', { - value: require('../package.json').version, - configurable: false, - enumerable: true, - writable: false -}); - -/** - * Callback to be invoked when test execution is complete. - * - * @callback DoneCB - * @param {number} failures - Number of failures that occurred. - */ - -/** - * Runs root suite and invokes `fn()` when complete. - * - * @description - * To run tests multiple times (or to run tests in files that are - * already in the `require` cache), make sure to clear them from - * the cache first! - * - * @public - * @see {@link Mocha#unloadFiles} - * @see {@link Runner#run} - * @param {DoneCB} [fn] - Callback invoked when test execution completed. - * @returns {Runner} runner instance - * @example - * - * // exit with non-zero status if there were test failures - * mocha.run(failures => process.exitCode = failures ? 1 : 0); - */ -Mocha.prototype.run = function(fn) { - if (this.files.length && !this.loadAsync) { - this.loadFiles(); - } - var suite = this.suite; - var options = this.options; - options.files = this.files; - var runner = new exports.Runner(suite, options.delay); - createStatsCollector(runner); - var reporter = new this._reporter(runner, options); - runner.checkLeaks = options.checkLeaks === true; - runner.fullStackTrace = options.fullTrace; - runner.asyncOnly = options.asyncOnly; - runner.allowUncaught = options.allowUncaught; - runner.forbidOnly = options.forbidOnly; - runner.forbidPending = options.forbidPending; - if (options.grep) { - runner.grep(options.grep, options.invert); - } - if (options.global) { - runner.globals(options.global); - } - if (options.growl) { - this._growl(runner); - } - if (options.color !== undefined) { - exports.reporters.Base.useColors = options.color; - } - exports.reporters.Base.inlineDiffs = options.inlineDiffs; - exports.reporters.Base.hideDiff = !options.diff; - - function done(failures) { - fn = fn || utils.noop; - if (reporter.done) { - reporter.done(failures, fn); - } else { - fn(failures); - } - } - - return runner.run(done); -}; - -}).call(this,require('_process'),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"../package.json":90,"./context":5,"./errors":6,"./esm-utils":42,"./growl":2,"./hook":7,"./interfaces":11,"./mocharc.json":15,"./reporters":21,"./runnable":33,"./runner":34,"./stats-collector":35,"./suite":36,"./test":37,"./utils":38,"_process":69,"escape-string-regexp":49,"path":42}],15:[function(require,module,exports){ -module.exports={ - "diff": true, - "extension": ["js", "cjs", "mjs"], - "opts": "./test/mocha.opts", - "package": "./package.json", - "reporter": "spec", - "slow": 75, - "timeout": 2000, - "ui": "bdd", - "watch-ignore": ["node_modules", ".git"] -} - -},{}],16:[function(require,module,exports){ -'use strict'; - -module.exports = Pending; - -/** - * Initialize a new `Pending` error with the given message. - * - * @param {string} message - */ -function Pending(message) { - this.message = message; -} - -},{}],17:[function(require,module,exports){ -(function (process){ -'use strict'; -/** - * @module Base - */ -/** - * Module dependencies. - */ - -var tty = require('tty'); -var diff = require('diff'); -var milliseconds = require('ms'); -var utils = require('../utils'); -var supportsColor = process.browser ? null : require('supports-color'); -var constants = require('../runner').constants; -var EVENT_TEST_PASS = constants.EVENT_TEST_PASS; -var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL; - -/** - * Expose `Base`. - */ - -exports = module.exports = Base; - -/** - * Check if both stdio streams are associated with a tty. - */ - -var isatty = process.stdout.isTTY && process.stderr.isTTY; - -/** - * Save log references to avoid tests interfering (see GH-3604). - */ -var consoleLog = console.log; - -/** - * Enable coloring by default, except in the browser interface. - */ - -exports.useColors = - !process.browser && - (supportsColor.stdout || process.env.MOCHA_COLORS !== undefined); - -/** - * Inline diffs instead of +/- - */ - -exports.inlineDiffs = false; - -/** - * Default color map. - */ - -exports.colors = { - pass: 90, - fail: 31, - 'bright pass': 92, - 'bright fail': 91, - 'bright yellow': 93, - pending: 36, - suite: 0, - 'error title': 0, - 'error message': 31, - 'error stack': 90, - checkmark: 32, - fast: 90, - medium: 33, - slow: 31, - green: 32, - light: 90, - 'diff gutter': 90, - 'diff added': 32, - 'diff removed': 31 -}; - -/** - * Default symbol map. - */ - -exports.symbols = { - ok: '✓', - err: '✖', - dot: '․', - comma: ',', - bang: '!' -}; - -// With node.js on Windows: use symbols available in terminal default fonts -if (process.platform === 'win32') { - exports.symbols.ok = '\u221A'; - exports.symbols.err = '\u00D7'; - exports.symbols.dot = '.'; -} - -/** - * Color `str` with the given `type`, - * allowing colors to be disabled, - * as well as user-defined color - * schemes. - * - * @private - * @param {string} type - * @param {string} str - * @return {string} - */ -var color = (exports.color = function(type, str) { - if (!exports.useColors) { - return String(str); - } - return '\u001b[' + exports.colors[type] + 'm' + str + '\u001b[0m'; -}); - -/** - * Expose term window size, with some defaults for when stderr is not a tty. - */ - -exports.window = { - width: 75 -}; - -if (isatty) { - exports.window.width = process.stdout.getWindowSize - ? process.stdout.getWindowSize(1)[0] - : tty.getWindowSize()[1]; -} - -/** - * Expose some basic cursor interactions that are common among reporters. - */ - -exports.cursor = { - hide: function() { - isatty && process.stdout.write('\u001b[?25l'); - }, - - show: function() { - isatty && process.stdout.write('\u001b[?25h'); - }, - - deleteLine: function() { - isatty && process.stdout.write('\u001b[2K'); - }, - - beginningOfLine: function() { - isatty && process.stdout.write('\u001b[0G'); - }, - - CR: function() { - if (isatty) { - exports.cursor.deleteLine(); - exports.cursor.beginningOfLine(); - } else { - process.stdout.write('\r'); - } - } -}; - -var showDiff = (exports.showDiff = function(err) { - return ( - err && - err.showDiff !== false && - sameType(err.actual, err.expected) && - err.expected !== undefined - ); -}); - -function stringifyDiffObjs(err) { - if (!utils.isString(err.actual) || !utils.isString(err.expected)) { - err.actual = utils.stringify(err.actual); - err.expected = utils.stringify(err.expected); - } -} - -/** - * Returns a diff between 2 strings with coloured ANSI output. - * - * @description - * The diff will be either inline or unified dependent on the value - * of `Base.inlineDiff`. - * - * @param {string} actual - * @param {string} expected - * @return {string} Diff - */ -var generateDiff = (exports.generateDiff = function(actual, expected) { - try { - return exports.inlineDiffs - ? inlineDiff(actual, expected) - : unifiedDiff(actual, expected); - } catch (err) { - var msg = - '\n ' + - color('diff added', '+ expected') + - ' ' + - color('diff removed', '- actual: failed to generate Mocha diff') + - '\n'; - return msg; - } -}); - -/** - * Outputs the given `failures` as a list. - * - * @public - * @memberof Mocha.reporters.Base - * @variation 1 - * @param {Object[]} failures - Each is Test instance with corresponding - * Error property - */ -exports.list = function(failures) { - var multipleErr, multipleTest; - Base.consoleLog(); - failures.forEach(function(test, i) { - // format - var fmt = - color('error title', ' %s) %s:\n') + - color('error message', ' %s') + - color('error stack', '\n%s\n'); - - // msg - var msg; - var err; - if (test.err && test.err.multiple) { - if (multipleTest !== test) { - multipleTest = test; - multipleErr = [test.err].concat(test.err.multiple); - } - err = multipleErr.shift(); - } else { - err = test.err; - } - var message; - if (err.message && typeof err.message.toString === 'function') { - message = err.message + ''; - } else if (typeof err.inspect === 'function') { - message = err.inspect() + ''; - } else { - message = ''; - } - var stack = err.stack || message; - var index = message ? stack.indexOf(message) : -1; - - if (index === -1) { - msg = message; - } else { - index += message.length; - msg = stack.slice(0, index); - // remove msg from stack - stack = stack.slice(index + 1); - } - - // uncaught - if (err.uncaught) { - msg = 'Uncaught ' + msg; - } - // explicitly show diff - if (!exports.hideDiff && showDiff(err)) { - stringifyDiffObjs(err); - fmt = - color('error title', ' %s) %s:\n%s') + color('error stack', '\n%s\n'); - var match = message.match(/^([^:]+): expected/); - msg = '\n ' + color('error message', match ? match[1] : msg); - - msg += generateDiff(err.actual, err.expected); - } - - // indent stack trace - stack = stack.replace(/^/gm, ' '); - - // indented test title - var testTitle = ''; - test.titlePath().forEach(function(str, index) { - if (index !== 0) { - testTitle += '\n '; - } - for (var i = 0; i < index; i++) { - testTitle += ' '; - } - testTitle += str; - }); - - Base.consoleLog(fmt, i + 1, testTitle, msg, stack); - }); -}; - -/** - * Constructs a new `Base` reporter instance. - * - * @description - * All other reporters generally inherit from this reporter. - * - * @public - * @class - * @memberof Mocha.reporters - * @param {Runner} runner - Instance triggers reporter actions. - * @param {Object} [options] - runner options - */ -function Base(runner, options) { - var failures = (this.failures = []); - - if (!runner) { - throw new TypeError('Missing runner argument'); - } - this.options = options || {}; - this.runner = runner; - this.stats = runner.stats; // assigned so Reporters keep a closer reference - - runner.on(EVENT_TEST_PASS, function(test) { - if (test.duration > test.slow()) { - test.speed = 'slow'; - } else if (test.duration > test.slow() / 2) { - test.speed = 'medium'; - } else { - test.speed = 'fast'; - } - }); - - runner.on(EVENT_TEST_FAIL, function(test, err) { - if (showDiff(err)) { - stringifyDiffObjs(err); - } - // more than one error per test - if (test.err && err instanceof Error) { - test.err.multiple = (test.err.multiple || []).concat(err); - } else { - test.err = err; - } - failures.push(test); - }); -} - -/** - * Outputs common epilogue used by many of the bundled reporters. - * - * @public - * @memberof Mocha.reporters - */ -Base.prototype.epilogue = function() { - var stats = this.stats; - var fmt; - - Base.consoleLog(); - - // passes - fmt = - color('bright pass', ' ') + - color('green', ' %d passing') + - color('light', ' (%s)'); - - Base.consoleLog(fmt, stats.passes || 0, milliseconds(stats.duration)); - - // pending - if (stats.pending) { - fmt = color('pending', ' ') + color('pending', ' %d pending'); - - Base.consoleLog(fmt, stats.pending); - } - - // failures - if (stats.failures) { - fmt = color('fail', ' %d failing'); - - Base.consoleLog(fmt, stats.failures); - - Base.list(this.failures); - Base.consoleLog(); - } - - Base.consoleLog(); -}; - -/** - * Pads the given `str` to `len`. - * - * @private - * @param {string} str - * @param {string} len - * @return {string} - */ -function pad(str, len) { - str = String(str); - return Array(len - str.length + 1).join(' ') + str; -} - -/** - * Returns inline diff between 2 strings with coloured ANSI output. - * - * @private - * @param {String} actual - * @param {String} expected - * @return {string} Diff - */ -function inlineDiff(actual, expected) { - var msg = errorDiff(actual, expected); - - // linenos - var lines = msg.split('\n'); - if (lines.length > 4) { - var width = String(lines.length).length; - msg = lines - .map(function(str, i) { - return pad(++i, width) + ' |' + ' ' + str; - }) - .join('\n'); - } - - // legend - msg = - '\n' + - color('diff removed', 'actual') + - ' ' + - color('diff added', 'expected') + - '\n\n' + - msg + - '\n'; - - // indent - msg = msg.replace(/^/gm, ' '); - return msg; -} - -/** - * Returns unified diff between two strings with coloured ANSI output. - * - * @private - * @param {String} actual - * @param {String} expected - * @return {string} The diff. - */ -function unifiedDiff(actual, expected) { - var indent = ' '; - function cleanUp(line) { - if (line[0] === '+') { - return indent + colorLines('diff added', line); - } - if (line[0] === '-') { - return indent + colorLines('diff removed', line); - } - if (line.match(/@@/)) { - return '--'; - } - if (line.match(/\\ No newline/)) { - return null; - } - return indent + line; - } - function notBlank(line) { - return typeof line !== 'undefined' && line !== null; - } - var msg = diff.createPatch('string', actual, expected); - var lines = msg.split('\n').splice(5); - return ( - '\n ' + - colorLines('diff added', '+ expected') + - ' ' + - colorLines('diff removed', '- actual') + - '\n\n' + - lines - .map(cleanUp) - .filter(notBlank) - .join('\n') - ); -} - -/** - * Returns character diff for `err`. - * - * @private - * @param {String} actual - * @param {String} expected - * @return {string} the diff - */ -function errorDiff(actual, expected) { - return diff - .diffWordsWithSpace(actual, expected) - .map(function(str) { - if (str.added) { - return colorLines('diff added', str.value); - } - if (str.removed) { - return colorLines('diff removed', str.value); - } - return str.value; - }) - .join(''); -} - -/** - * Colors lines for `str`, using the color `name`. - * - * @private - * @param {string} name - * @param {string} str - * @return {string} - */ -function colorLines(name, str) { - return str - .split('\n') - .map(function(str) { - return color(name, str); - }) - .join('\n'); -} - -/** - * Object#toString reference. - */ -var objToString = Object.prototype.toString; - -/** - * Checks that a / b have the same type. - * - * @private - * @param {Object} a - * @param {Object} b - * @return {boolean} - */ -function sameType(a, b) { - return objToString.call(a) === objToString.call(b); -} - -Base.consoleLog = consoleLog; - -Base.abstract = true; - -}).call(this,require('_process')) -},{"../runner":34,"../utils":38,"_process":69,"diff":48,"ms":60,"supports-color":42,"tty":4}],18:[function(require,module,exports){ -'use strict'; -/** - * @module Doc - */ -/** - * Module dependencies. - */ - -var Base = require('./base'); -var utils = require('../utils'); -var constants = require('../runner').constants; -var EVENT_TEST_PASS = constants.EVENT_TEST_PASS; -var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL; -var EVENT_SUITE_BEGIN = constants.EVENT_SUITE_BEGIN; -var EVENT_SUITE_END = constants.EVENT_SUITE_END; - -/** - * Expose `Doc`. - */ - -exports = module.exports = Doc; - -/** - * Constructs a new `Doc` reporter instance. - * - * @public - * @class - * @memberof Mocha.reporters - * @extends Mocha.reporters.Base - * @param {Runner} runner - Instance triggers reporter actions. - * @param {Object} [options] - runner options - */ -function Doc(runner, options) { - Base.call(this, runner, options); - - var indents = 2; - - function indent() { - return Array(indents).join(' '); - } - - runner.on(EVENT_SUITE_BEGIN, function(suite) { - if (suite.root) { - return; - } - ++indents; - Base.consoleLog('%s
', indent()); - ++indents; - Base.consoleLog('%s

%s

', indent(), utils.escape(suite.title)); - Base.consoleLog('%s
', indent()); - }); - - runner.on(EVENT_SUITE_END, function(suite) { - if (suite.root) { - return; - } - Base.consoleLog('%s
', indent()); - --indents; - Base.consoleLog('%s
', indent()); - --indents; - }); - - runner.on(EVENT_TEST_PASS, function(test) { - Base.consoleLog('%s
%s
', indent(), utils.escape(test.title)); - var code = utils.escape(utils.clean(test.body)); - Base.consoleLog('%s
%s
', indent(), code); - }); - - runner.on(EVENT_TEST_FAIL, function(test, err) { - Base.consoleLog( - '%s
%s
', - indent(), - utils.escape(test.title) - ); - var code = utils.escape(utils.clean(test.body)); - Base.consoleLog( - '%s
%s
', - indent(), - code - ); - Base.consoleLog( - '%s
%s
', - indent(), - utils.escape(err) - ); - }); -} - -Doc.description = 'HTML documentation'; - -},{"../runner":34,"../utils":38,"./base":17}],19:[function(require,module,exports){ -(function (process){ -'use strict'; -/** - * @module Dot - */ -/** - * Module dependencies. - */ - -var Base = require('./base'); -var inherits = require('../utils').inherits; -var constants = require('../runner').constants; -var EVENT_TEST_PASS = constants.EVENT_TEST_PASS; -var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL; -var EVENT_RUN_BEGIN = constants.EVENT_RUN_BEGIN; -var EVENT_TEST_PENDING = constants.EVENT_TEST_PENDING; -var EVENT_RUN_END = constants.EVENT_RUN_END; - -/** - * Expose `Dot`. - */ - -exports = module.exports = Dot; - -/** - * Constructs a new `Dot` reporter instance. - * - * @public - * @class - * @memberof Mocha.reporters - * @extends Mocha.reporters.Base - * @param {Runner} runner - Instance triggers reporter actions. - * @param {Object} [options] - runner options - */ -function Dot(runner, options) { - Base.call(this, runner, options); - - var self = this; - var width = (Base.window.width * 0.75) | 0; - var n = -1; - - runner.on(EVENT_RUN_BEGIN, function() { - process.stdout.write('\n'); - }); - - runner.on(EVENT_TEST_PENDING, function() { - if (++n % width === 0) { - process.stdout.write('\n '); - } - process.stdout.write(Base.color('pending', Base.symbols.comma)); - }); - - runner.on(EVENT_TEST_PASS, function(test) { - if (++n % width === 0) { - process.stdout.write('\n '); - } - if (test.speed === 'slow') { - process.stdout.write(Base.color('bright yellow', Base.symbols.dot)); - } else { - process.stdout.write(Base.color(test.speed, Base.symbols.dot)); - } - }); - - runner.on(EVENT_TEST_FAIL, function() { - if (++n % width === 0) { - process.stdout.write('\n '); - } - process.stdout.write(Base.color('fail', Base.symbols.bang)); - }); - - runner.once(EVENT_RUN_END, function() { - process.stdout.write('\n'); - self.epilogue(); - }); -} - -/** - * Inherit from `Base.prototype`. - */ -inherits(Dot, Base); - -Dot.description = 'dot matrix representation'; - -}).call(this,require('_process')) -},{"../runner":34,"../utils":38,"./base":17,"_process":69}],20:[function(require,module,exports){ -(function (global){ -'use strict'; - -/* eslint-env browser */ -/** - * @module HTML - */ -/** - * Module dependencies. - */ - -var Base = require('./base'); -var utils = require('../utils'); -var Progress = require('../browser/progress'); -var escapeRe = require('escape-string-regexp'); -var constants = require('../runner').constants; -var EVENT_TEST_PASS = constants.EVENT_TEST_PASS; -var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL; -var EVENT_SUITE_BEGIN = constants.EVENT_SUITE_BEGIN; -var EVENT_SUITE_END = constants.EVENT_SUITE_END; -var EVENT_TEST_PENDING = constants.EVENT_TEST_PENDING; -var escape = utils.escape; - -/** - * Save timer references to avoid Sinon interfering (see GH-237). - */ - -var Date = global.Date; - -/** - * Expose `HTML`. - */ - -exports = module.exports = HTML; - -/** - * Stats template. - */ - -var statsTemplate = - ''; - -var playIcon = '‣'; - -/** - * Constructs a new `HTML` reporter instance. - * - * @public - * @class - * @memberof Mocha.reporters - * @extends Mocha.reporters.Base - * @param {Runner} runner - Instance triggers reporter actions. - * @param {Object} [options] - runner options - */ -function HTML(runner, options) { - Base.call(this, runner, options); - - var self = this; - var stats = this.stats; - var stat = fragment(statsTemplate); - var items = stat.getElementsByTagName('li'); - var passes = items[1].getElementsByTagName('em')[0]; - var passesLink = items[1].getElementsByTagName('a')[0]; - var failures = items[2].getElementsByTagName('em')[0]; - var failuresLink = items[2].getElementsByTagName('a')[0]; - var duration = items[3].getElementsByTagName('em')[0]; - var canvas = stat.getElementsByTagName('canvas')[0]; - var report = fragment('
    '); - var stack = [report]; - var progress; - var ctx; - var root = document.getElementById('mocha'); - - if (canvas.getContext) { - var ratio = window.devicePixelRatio || 1; - canvas.style.width = canvas.width; - canvas.style.height = canvas.height; - canvas.width *= ratio; - canvas.height *= ratio; - ctx = canvas.getContext('2d'); - ctx.scale(ratio, ratio); - progress = new Progress(); - } - - if (!root) { - return error('#mocha div missing, add it to your document'); - } - - // pass toggle - on(passesLink, 'click', function(evt) { - evt.preventDefault(); - unhide(); - var name = /pass/.test(report.className) ? '' : ' pass'; - report.className = report.className.replace(/fail|pass/g, '') + name; - if (report.className.trim()) { - hideSuitesWithout('test pass'); - } - }); - - // failure toggle - on(failuresLink, 'click', function(evt) { - evt.preventDefault(); - unhide(); - var name = /fail/.test(report.className) ? '' : ' fail'; - report.className = report.className.replace(/fail|pass/g, '') + name; - if (report.className.trim()) { - hideSuitesWithout('test fail'); - } - }); - - root.appendChild(stat); - root.appendChild(report); - - if (progress) { - progress.size(40); - } - - runner.on(EVENT_SUITE_BEGIN, function(suite) { - if (suite.root) { - return; - } - - // suite - var url = self.suiteURL(suite); - var el = fragment( - '
  • %s

  • ', - url, - escape(suite.title) - ); - - // container - stack[0].appendChild(el); - stack.unshift(document.createElement('ul')); - el.appendChild(stack[0]); - }); - - runner.on(EVENT_SUITE_END, function(suite) { - if (suite.root) { - updateStats(); - return; - } - stack.shift(); - }); - - runner.on(EVENT_TEST_PASS, function(test) { - var url = self.testURL(test); - var markup = - '
  • %e

    %ems ' + - '' + - playIcon + - '
  • '; - var el = fragment(markup, test.speed, test.title, test.duration, url); - self.addCodeToggle(el, test.body); - appendToStack(el); - updateStats(); - }); - - runner.on(EVENT_TEST_FAIL, function(test) { - var el = fragment( - '
  • %e ' + - playIcon + - '

  • ', - test.title, - self.testURL(test) - ); - var stackString; // Note: Includes leading newline - var message = test.err.toString(); - - // <=IE7 stringifies to [Object Error]. Since it can be overloaded, we - // check for the result of the stringifying. - if (message === '[object Error]') { - message = test.err.message; - } - - if (test.err.stack) { - var indexOfMessage = test.err.stack.indexOf(test.err.message); - if (indexOfMessage === -1) { - stackString = test.err.stack; - } else { - stackString = test.err.stack.substr( - test.err.message.length + indexOfMessage - ); - } - } else if (test.err.sourceURL && test.err.line !== undefined) { - // Safari doesn't give you a stack. Let's at least provide a source line. - stackString = '\n(' + test.err.sourceURL + ':' + test.err.line + ')'; - } - - stackString = stackString || ''; - - if (test.err.htmlMessage && stackString) { - el.appendChild( - fragment( - '
    %s\n
    %e
    ', - test.err.htmlMessage, - stackString - ) - ); - } else if (test.err.htmlMessage) { - el.appendChild( - fragment('
    %s
    ', test.err.htmlMessage) - ); - } else { - el.appendChild( - fragment('
    %e%e
    ', message, stackString) - ); - } - - self.addCodeToggle(el, test.body); - appendToStack(el); - updateStats(); - }); - - runner.on(EVENT_TEST_PENDING, function(test) { - var el = fragment( - '
  • %e

  • ', - test.title - ); - appendToStack(el); - updateStats(); - }); - - function appendToStack(el) { - // Don't call .appendChild if #mocha-report was already .shift()'ed off the stack. - if (stack[0]) { - stack[0].appendChild(el); - } - } - - function updateStats() { - // TODO: add to stats - var percent = ((stats.tests / runner.total) * 100) | 0; - if (progress) { - progress.update(percent).draw(ctx); - } - - // update stats - var ms = new Date() - stats.start; - text(passes, stats.passes); - text(failures, stats.failures); - text(duration, (ms / 1000).toFixed(2)); - } -} - -/** - * Makes a URL, preserving querystring ("search") parameters. - * - * @param {string} s - * @return {string} A new URL. - */ -function makeUrl(s) { - var search = window.location.search; - - // Remove previous grep query parameter if present - if (search) { - search = search.replace(/[?&]grep=[^&\s]*/g, '').replace(/^&/, '?'); - } - - return ( - window.location.pathname + - (search ? search + '&' : '?') + - 'grep=' + - encodeURIComponent(escapeRe(s)) - ); -} - -/** - * Provide suite URL. - * - * @param {Object} [suite] - */ -HTML.prototype.suiteURL = function(suite) { - return makeUrl(suite.fullTitle()); -}; - -/** - * Provide test URL. - * - * @param {Object} [test] - */ -HTML.prototype.testURL = function(test) { - return makeUrl(test.fullTitle()); -}; - -/** - * Adds code toggle functionality for the provided test's list element. - * - * @param {HTMLLIElement} el - * @param {string} contents - */ -HTML.prototype.addCodeToggle = function(el, contents) { - var h2 = el.getElementsByTagName('h2')[0]; - - on(h2, 'click', function() { - pre.style.display = pre.style.display === 'none' ? 'block' : 'none'; - }); - - var pre = fragment('
    %e
    ', utils.clean(contents)); - el.appendChild(pre); - pre.style.display = 'none'; -}; - -/** - * Display error `msg`. - * - * @param {string} msg - */ -function error(msg) { - document.body.appendChild(fragment('
    %s
    ', msg)); -} - -/** - * Return a DOM fragment from `html`. - * - * @param {string} html - */ -function fragment(html) { - var args = arguments; - var div = document.createElement('div'); - var i = 1; - - div.innerHTML = html.replace(/%([se])/g, function(_, type) { - switch (type) { - case 's': - return String(args[i++]); - case 'e': - return escape(args[i++]); - // no default - } - }); - - return div.firstChild; -} - -/** - * Check for suites that do not have elements - * with `classname`, and hide them. - * - * @param {text} classname - */ -function hideSuitesWithout(classname) { - var suites = document.getElementsByClassName('suite'); - for (var i = 0; i < suites.length; i++) { - var els = suites[i].getElementsByClassName(classname); - if (!els.length) { - suites[i].className += ' hidden'; - } - } -} - -/** - * Unhide .hidden suites. - */ -function unhide() { - var els = document.getElementsByClassName('suite hidden'); - while (els.length > 0) { - els[0].className = els[0].className.replace('suite hidden', 'suite'); - } -} - -/** - * Set an element's text contents. - * - * @param {HTMLElement} el - * @param {string} contents - */ -function text(el, contents) { - if (el.textContent) { - el.textContent = contents; - } else { - el.innerText = contents; - } -} - -/** - * Listen on `event` with callback `fn`. - */ -function on(el, event, fn) { - if (el.addEventListener) { - el.addEventListener(event, fn, false); - } else { - el.attachEvent('on' + event, fn); - } -} - -HTML.browserOnly = true; - -}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"../browser/progress":3,"../runner":34,"../utils":38,"./base":17,"escape-string-regexp":49}],21:[function(require,module,exports){ -'use strict'; - -// Alias exports to a their normalized format Mocha#reporter to prevent a need -// for dynamic (try/catch) requires, which Browserify doesn't handle. -exports.Base = exports.base = require('./base'); -exports.Dot = exports.dot = require('./dot'); -exports.Doc = exports.doc = require('./doc'); -exports.TAP = exports.tap = require('./tap'); -exports.JSON = exports.json = require('./json'); -exports.HTML = exports.html = require('./html'); -exports.List = exports.list = require('./list'); -exports.Min = exports.min = require('./min'); -exports.Spec = exports.spec = require('./spec'); -exports.Nyan = exports.nyan = require('./nyan'); -exports.XUnit = exports.xunit = require('./xunit'); -exports.Markdown = exports.markdown = require('./markdown'); -exports.Progress = exports.progress = require('./progress'); -exports.Landing = exports.landing = require('./landing'); -exports.JSONStream = exports['json-stream'] = require('./json-stream'); - -},{"./base":17,"./doc":18,"./dot":19,"./html":20,"./json":23,"./json-stream":22,"./landing":24,"./list":25,"./markdown":26,"./min":27,"./nyan":28,"./progress":29,"./spec":30,"./tap":31,"./xunit":32}],22:[function(require,module,exports){ -(function (process){ -'use strict'; -/** - * @module JSONStream - */ -/** - * Module dependencies. - */ - -var Base = require('./base'); -var constants = require('../runner').constants; -var EVENT_TEST_PASS = constants.EVENT_TEST_PASS; -var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL; -var EVENT_RUN_BEGIN = constants.EVENT_RUN_BEGIN; -var EVENT_RUN_END = constants.EVENT_RUN_END; - -/** - * Expose `JSONStream`. - */ - -exports = module.exports = JSONStream; - -/** - * Constructs a new `JSONStream` reporter instance. - * - * @public - * @class - * @memberof Mocha.reporters - * @extends Mocha.reporters.Base - * @param {Runner} runner - Instance triggers reporter actions. - * @param {Object} [options] - runner options - */ -function JSONStream(runner, options) { - Base.call(this, runner, options); - - var self = this; - var total = runner.total; - - runner.once(EVENT_RUN_BEGIN, function() { - writeEvent(['start', {total: total}]); - }); - - runner.on(EVENT_TEST_PASS, function(test) { - writeEvent(['pass', clean(test)]); - }); - - runner.on(EVENT_TEST_FAIL, function(test, err) { - test = clean(test); - test.err = err.message; - test.stack = err.stack || null; - writeEvent(['fail', test]); - }); - - runner.once(EVENT_RUN_END, function() { - writeEvent(['end', self.stats]); - }); -} - -/** - * Mocha event to be written to the output stream. - * @typedef {Array} JSONStream~MochaEvent - */ - -/** - * Writes Mocha event to reporter output stream. - * - * @private - * @param {JSONStream~MochaEvent} event - Mocha event to be output. - */ -function writeEvent(event) { - process.stdout.write(JSON.stringify(event) + '\n'); -} - -/** - * Returns an object literal representation of `test` - * free of cyclic properties, etc. - * - * @private - * @param {Test} test - Instance used as data source. - * @return {Object} object containing pared-down test instance data - */ -function clean(test) { - return { - title: test.title, - fullTitle: test.fullTitle(), - duration: test.duration, - currentRetry: test.currentRetry() - }; -} - -JSONStream.description = 'newline delimited JSON events'; - -}).call(this,require('_process')) -},{"../runner":34,"./base":17,"_process":69}],23:[function(require,module,exports){ -(function (process){ -'use strict'; -/** - * @module JSON - */ -/** - * Module dependencies. - */ - -var Base = require('./base'); -var constants = require('../runner').constants; -var EVENT_TEST_PASS = constants.EVENT_TEST_PASS; -var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL; -var EVENT_TEST_END = constants.EVENT_TEST_END; -var EVENT_RUN_END = constants.EVENT_RUN_END; -var EVENT_TEST_PENDING = constants.EVENT_TEST_PENDING; - -/** - * Expose `JSON`. - */ - -exports = module.exports = JSONReporter; - -/** - * Constructs a new `JSON` reporter instance. - * - * @public - * @class JSON - * @memberof Mocha.reporters - * @extends Mocha.reporters.Base - * @param {Runner} runner - Instance triggers reporter actions. - * @param {Object} [options] - runner options - */ -function JSONReporter(runner, options) { - Base.call(this, runner, options); - - var self = this; - var tests = []; - var pending = []; - var failures = []; - var passes = []; - - runner.on(EVENT_TEST_END, function(test) { - tests.push(test); - }); - - runner.on(EVENT_TEST_PASS, function(test) { - passes.push(test); - }); - - runner.on(EVENT_TEST_FAIL, function(test) { - failures.push(test); - }); - - runner.on(EVENT_TEST_PENDING, function(test) { - pending.push(test); - }); - - runner.once(EVENT_RUN_END, function() { - var obj = { - stats: self.stats, - tests: tests.map(clean), - pending: pending.map(clean), - failures: failures.map(clean), - passes: passes.map(clean) - }; - - runner.testResults = obj; - - process.stdout.write(JSON.stringify(obj, null, 2)); - }); -} - -/** - * Return a plain-object representation of `test` - * free of cyclic properties etc. - * - * @private - * @param {Object} test - * @return {Object} - */ -function clean(test) { - var err = test.err || {}; - if (err instanceof Error) { - err = errorJSON(err); - } - - return { - title: test.title, - fullTitle: test.fullTitle(), - duration: test.duration, - currentRetry: test.currentRetry(), - err: cleanCycles(err) - }; -} - -/** - * Replaces any circular references inside `obj` with '[object Object]' - * - * @private - * @param {Object} obj - * @return {Object} - */ -function cleanCycles(obj) { - var cache = []; - return JSON.parse( - JSON.stringify(obj, function(key, value) { - if (typeof value === 'object' && value !== null) { - if (cache.indexOf(value) !== -1) { - // Instead of going in a circle, we'll print [object Object] - return '' + value; - } - cache.push(value); - } - - return value; - }) - ); -} - -/** - * Transform an Error object into a JSON object. - * - * @private - * @param {Error} err - * @return {Object} - */ -function errorJSON(err) { - var res = {}; - Object.getOwnPropertyNames(err).forEach(function(key) { - res[key] = err[key]; - }, err); - return res; -} - -JSONReporter.description = 'single JSON object'; - -}).call(this,require('_process')) -},{"../runner":34,"./base":17,"_process":69}],24:[function(require,module,exports){ -(function (process){ -'use strict'; -/** - * @module Landing - */ -/** - * Module dependencies. - */ - -var Base = require('./base'); -var inherits = require('../utils').inherits; -var constants = require('../runner').constants; -var EVENT_RUN_BEGIN = constants.EVENT_RUN_BEGIN; -var EVENT_RUN_END = constants.EVENT_RUN_END; -var EVENT_TEST_END = constants.EVENT_TEST_END; -var STATE_FAILED = require('../runnable').constants.STATE_FAILED; - -var cursor = Base.cursor; -var color = Base.color; - -/** - * Expose `Landing`. - */ - -exports = module.exports = Landing; - -/** - * Airplane color. - */ - -Base.colors.plane = 0; - -/** - * Airplane crash color. - */ - -Base.colors['plane crash'] = 31; - -/** - * Runway color. - */ - -Base.colors.runway = 90; - -/** - * Constructs a new `Landing` reporter instance. - * - * @public - * @class - * @memberof Mocha.reporters - * @extends Mocha.reporters.Base - * @param {Runner} runner - Instance triggers reporter actions. - * @param {Object} [options] - runner options - */ -function Landing(runner, options) { - Base.call(this, runner, options); - - var self = this; - var width = (Base.window.width * 0.75) | 0; - var total = runner.total; - var stream = process.stdout; - var plane = color('plane', '✈'); - var crashed = -1; - var n = 0; - - function runway() { - var buf = Array(width).join('-'); - return ' ' + color('runway', buf); - } - - runner.on(EVENT_RUN_BEGIN, function() { - stream.write('\n\n\n '); - cursor.hide(); - }); - - runner.on(EVENT_TEST_END, function(test) { - // check if the plane crashed - var col = crashed === -1 ? ((width * ++n) / total) | 0 : crashed; - - // show the crash - if (test.state === STATE_FAILED) { - plane = color('plane crash', '✈'); - crashed = col; - } - - // render landing strip - stream.write('\u001b[' + (width + 1) + 'D\u001b[2A'); - stream.write(runway()); - stream.write('\n '); - stream.write(color('runway', Array(col).join('⋅'))); - stream.write(plane); - stream.write(color('runway', Array(width - col).join('⋅') + '\n')); - stream.write(runway()); - stream.write('\u001b[0m'); - }); - - runner.once(EVENT_RUN_END, function() { - cursor.show(); - process.stdout.write('\n'); - self.epilogue(); - }); -} - -/** - * Inherit from `Base.prototype`. - */ -inherits(Landing, Base); - -Landing.description = 'Unicode landing strip'; - -}).call(this,require('_process')) -},{"../runnable":33,"../runner":34,"../utils":38,"./base":17,"_process":69}],25:[function(require,module,exports){ -(function (process){ -'use strict'; -/** - * @module List - */ -/** - * Module dependencies. - */ - -var Base = require('./base'); -var inherits = require('../utils').inherits; -var constants = require('../runner').constants; -var EVENT_RUN_BEGIN = constants.EVENT_RUN_BEGIN; -var EVENT_RUN_END = constants.EVENT_RUN_END; -var EVENT_TEST_BEGIN = constants.EVENT_TEST_BEGIN; -var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL; -var EVENT_TEST_PASS = constants.EVENT_TEST_PASS; -var EVENT_TEST_PENDING = constants.EVENT_TEST_PENDING; -var color = Base.color; -var cursor = Base.cursor; - -/** - * Expose `List`. - */ - -exports = module.exports = List; - -/** - * Constructs a new `List` reporter instance. - * - * @public - * @class - * @memberof Mocha.reporters - * @extends Mocha.reporters.Base - * @param {Runner} runner - Instance triggers reporter actions. - * @param {Object} [options] - runner options - */ -function List(runner, options) { - Base.call(this, runner, options); - - var self = this; - var n = 0; - - runner.on(EVENT_RUN_BEGIN, function() { - Base.consoleLog(); - }); - - runner.on(EVENT_TEST_BEGIN, function(test) { - process.stdout.write(color('pass', ' ' + test.fullTitle() + ': ')); - }); - - runner.on(EVENT_TEST_PENDING, function(test) { - var fmt = color('checkmark', ' -') + color('pending', ' %s'); - Base.consoleLog(fmt, test.fullTitle()); - }); - - runner.on(EVENT_TEST_PASS, function(test) { - var fmt = - color('checkmark', ' ' + Base.symbols.ok) + - color('pass', ' %s: ') + - color(test.speed, '%dms'); - cursor.CR(); - Base.consoleLog(fmt, test.fullTitle(), test.duration); - }); - - runner.on(EVENT_TEST_FAIL, function(test) { - cursor.CR(); - Base.consoleLog(color('fail', ' %d) %s'), ++n, test.fullTitle()); - }); - - runner.once(EVENT_RUN_END, self.epilogue.bind(self)); -} - -/** - * Inherit from `Base.prototype`. - */ -inherits(List, Base); - -List.description = 'like "spec" reporter but flat'; - -}).call(this,require('_process')) -},{"../runner":34,"../utils":38,"./base":17,"_process":69}],26:[function(require,module,exports){ -(function (process){ -'use strict'; -/** - * @module Markdown - */ -/** - * Module dependencies. - */ - -var Base = require('./base'); -var utils = require('../utils'); -var constants = require('../runner').constants; -var EVENT_RUN_END = constants.EVENT_RUN_END; -var EVENT_SUITE_BEGIN = constants.EVENT_SUITE_BEGIN; -var EVENT_SUITE_END = constants.EVENT_SUITE_END; -var EVENT_TEST_PASS = constants.EVENT_TEST_PASS; - -/** - * Constants - */ - -var SUITE_PREFIX = '$'; - -/** - * Expose `Markdown`. - */ - -exports = module.exports = Markdown; - -/** - * Constructs a new `Markdown` reporter instance. - * - * @public - * @class - * @memberof Mocha.reporters - * @extends Mocha.reporters.Base - * @param {Runner} runner - Instance triggers reporter actions. - * @param {Object} [options] - runner options - */ -function Markdown(runner, options) { - Base.call(this, runner, options); - - var level = 0; - var buf = ''; - - function title(str) { - return Array(level).join('#') + ' ' + str; - } - - function mapTOC(suite, obj) { - var ret = obj; - var key = SUITE_PREFIX + suite.title; - - obj = obj[key] = obj[key] || {suite: suite}; - suite.suites.forEach(function(suite) { - mapTOC(suite, obj); - }); - - return ret; - } - - function stringifyTOC(obj, level) { - ++level; - var buf = ''; - var link; - for (var key in obj) { - if (key === 'suite') { - continue; - } - if (key !== SUITE_PREFIX) { - link = ' - [' + key.substring(1) + ']'; - link += '(#' + utils.slug(obj[key].suite.fullTitle()) + ')\n'; - buf += Array(level).join(' ') + link; - } - buf += stringifyTOC(obj[key], level); - } - return buf; - } - - function generateTOC(suite) { - var obj = mapTOC(suite, {}); - return stringifyTOC(obj, 0); - } - - generateTOC(runner.suite); - - runner.on(EVENT_SUITE_BEGIN, function(suite) { - ++level; - var slug = utils.slug(suite.fullTitle()); - buf += '' + '\n'; - buf += title(suite.title) + '\n'; - }); - - runner.on(EVENT_SUITE_END, function() { - --level; - }); - - runner.on(EVENT_TEST_PASS, function(test) { - var code = utils.clean(test.body); - buf += test.title + '.\n'; - buf += '\n```js\n'; - buf += code + '\n'; - buf += '```\n\n'; - }); - - runner.once(EVENT_RUN_END, function() { - process.stdout.write('# TOC\n'); - process.stdout.write(generateTOC(runner.suite)); - process.stdout.write(buf); - }); -} - -Markdown.description = 'GitHub Flavored Markdown'; - -}).call(this,require('_process')) -},{"../runner":34,"../utils":38,"./base":17,"_process":69}],27:[function(require,module,exports){ -(function (process){ -'use strict'; -/** - * @module Min - */ -/** - * Module dependencies. - */ - -var Base = require('./base'); -var inherits = require('../utils').inherits; -var constants = require('../runner').constants; -var EVENT_RUN_END = constants.EVENT_RUN_END; -var EVENT_RUN_BEGIN = constants.EVENT_RUN_BEGIN; - -/** - * Expose `Min`. - */ - -exports = module.exports = Min; - -/** - * Constructs a new `Min` reporter instance. - * - * @description - * This minimal test reporter is best used with '--watch'. - * - * @public - * @class - * @memberof Mocha.reporters - * @extends Mocha.reporters.Base - * @param {Runner} runner - Instance triggers reporter actions. - * @param {Object} [options] - runner options - */ -function Min(runner, options) { - Base.call(this, runner, options); - - runner.on(EVENT_RUN_BEGIN, function() { - // clear screen - process.stdout.write('\u001b[2J'); - // set cursor position - process.stdout.write('\u001b[1;3H'); - }); - - runner.once(EVENT_RUN_END, this.epilogue.bind(this)); -} - -/** - * Inherit from `Base.prototype`. - */ -inherits(Min, Base); - -Min.description = 'essentially just a summary'; - -}).call(this,require('_process')) -},{"../runner":34,"../utils":38,"./base":17,"_process":69}],28:[function(require,module,exports){ -(function (process){ -'use strict'; -/** - * @module Nyan - */ -/** - * Module dependencies. - */ - -var Base = require('./base'); -var constants = require('../runner').constants; -var inherits = require('../utils').inherits; -var EVENT_RUN_BEGIN = constants.EVENT_RUN_BEGIN; -var EVENT_TEST_PENDING = constants.EVENT_TEST_PENDING; -var EVENT_TEST_PASS = constants.EVENT_TEST_PASS; -var EVENT_RUN_END = constants.EVENT_RUN_END; -var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL; - -/** - * Expose `Dot`. - */ - -exports = module.exports = NyanCat; - -/** - * Constructs a new `Nyan` reporter instance. - * - * @public - * @class Nyan - * @memberof Mocha.reporters - * @extends Mocha.reporters.Base - * @param {Runner} runner - Instance triggers reporter actions. - * @param {Object} [options] - runner options - */ -function NyanCat(runner, options) { - Base.call(this, runner, options); - - var self = this; - var width = (Base.window.width * 0.75) | 0; - var nyanCatWidth = (this.nyanCatWidth = 11); - - this.colorIndex = 0; - this.numberOfLines = 4; - this.rainbowColors = self.generateColors(); - this.scoreboardWidth = 5; - this.tick = 0; - this.trajectories = [[], [], [], []]; - this.trajectoryWidthMax = width - nyanCatWidth; - - runner.on(EVENT_RUN_BEGIN, function() { - Base.cursor.hide(); - self.draw(); - }); - - runner.on(EVENT_TEST_PENDING, function() { - self.draw(); - }); - - runner.on(EVENT_TEST_PASS, function() { - self.draw(); - }); - - runner.on(EVENT_TEST_FAIL, function() { - self.draw(); - }); - - runner.once(EVENT_RUN_END, function() { - Base.cursor.show(); - for (var i = 0; i < self.numberOfLines; i++) { - write('\n'); - } - self.epilogue(); - }); -} - -/** - * Inherit from `Base.prototype`. - */ -inherits(NyanCat, Base); - -/** - * Draw the nyan cat - * - * @private - */ - -NyanCat.prototype.draw = function() { - this.appendRainbow(); - this.drawScoreboard(); - this.drawRainbow(); - this.drawNyanCat(); - this.tick = !this.tick; -}; - -/** - * Draw the "scoreboard" showing the number - * of passes, failures and pending tests. - * - * @private - */ - -NyanCat.prototype.drawScoreboard = function() { - var stats = this.stats; - - function draw(type, n) { - write(' '); - write(Base.color(type, n)); - write('\n'); - } - - draw('green', stats.passes); - draw('fail', stats.failures); - draw('pending', stats.pending); - write('\n'); - - this.cursorUp(this.numberOfLines); -}; - -/** - * Append the rainbow. - * - * @private - */ - -NyanCat.prototype.appendRainbow = function() { - var segment = this.tick ? '_' : '-'; - var rainbowified = this.rainbowify(segment); - - for (var index = 0; index < this.numberOfLines; index++) { - var trajectory = this.trajectories[index]; - if (trajectory.length >= this.trajectoryWidthMax) { - trajectory.shift(); - } - trajectory.push(rainbowified); - } -}; - -/** - * Draw the rainbow. - * - * @private - */ - -NyanCat.prototype.drawRainbow = function() { - var self = this; - - this.trajectories.forEach(function(line) { - write('\u001b[' + self.scoreboardWidth + 'C'); - write(line.join('')); - write('\n'); - }); - - this.cursorUp(this.numberOfLines); -}; - -/** - * Draw the nyan cat - * - * @private - */ -NyanCat.prototype.drawNyanCat = function() { - var self = this; - var startWidth = this.scoreboardWidth + this.trajectories[0].length; - var dist = '\u001b[' + startWidth + 'C'; - var padding = ''; - - write(dist); - write('_,------,'); - write('\n'); - - write(dist); - padding = self.tick ? ' ' : ' '; - write('_|' + padding + '/\\_/\\ '); - write('\n'); - - write(dist); - padding = self.tick ? '_' : '__'; - var tail = self.tick ? '~' : '^'; - write(tail + '|' + padding + this.face() + ' '); - write('\n'); - - write(dist); - padding = self.tick ? ' ' : ' '; - write(padding + '"" "" '); - write('\n'); - - this.cursorUp(this.numberOfLines); -}; - -/** - * Draw nyan cat face. - * - * @private - * @return {string} - */ - -NyanCat.prototype.face = function() { - var stats = this.stats; - if (stats.failures) { - return '( x .x)'; - } else if (stats.pending) { - return '( o .o)'; - } else if (stats.passes) { - return '( ^ .^)'; - } - return '( - .-)'; -}; - -/** - * Move cursor up `n`. - * - * @private - * @param {number} n - */ - -NyanCat.prototype.cursorUp = function(n) { - write('\u001b[' + n + 'A'); -}; - -/** - * Move cursor down `n`. - * - * @private - * @param {number} n - */ - -NyanCat.prototype.cursorDown = function(n) { - write('\u001b[' + n + 'B'); -}; - -/** - * Generate rainbow colors. - * - * @private - * @return {Array} - */ -NyanCat.prototype.generateColors = function() { - var colors = []; - - for (var i = 0; i < 6 * 7; i++) { - var pi3 = Math.floor(Math.PI / 3); - var n = i * (1.0 / 6); - var r = Math.floor(3 * Math.sin(n) + 3); - var g = Math.floor(3 * Math.sin(n + 2 * pi3) + 3); - var b = Math.floor(3 * Math.sin(n + 4 * pi3) + 3); - colors.push(36 * r + 6 * g + b + 16); - } - - return colors; -}; - -/** - * Apply rainbow to the given `str`. - * - * @private - * @param {string} str - * @return {string} - */ -NyanCat.prototype.rainbowify = function(str) { - if (!Base.useColors) { - return str; - } - var color = this.rainbowColors[this.colorIndex % this.rainbowColors.length]; - this.colorIndex += 1; - return '\u001b[38;5;' + color + 'm' + str + '\u001b[0m'; -}; - -/** - * Stdout helper. - * - * @param {string} string A message to write to stdout. - */ -function write(string) { - process.stdout.write(string); -} - -NyanCat.description = '"nyan cat"'; - -}).call(this,require('_process')) -},{"../runner":34,"../utils":38,"./base":17,"_process":69}],29:[function(require,module,exports){ -(function (process){ -'use strict'; -/** - * @module Progress - */ -/** - * Module dependencies. - */ - -var Base = require('./base'); -var constants = require('../runner').constants; -var EVENT_RUN_BEGIN = constants.EVENT_RUN_BEGIN; -var EVENT_TEST_END = constants.EVENT_TEST_END; -var EVENT_RUN_END = constants.EVENT_RUN_END; -var inherits = require('../utils').inherits; -var color = Base.color; -var cursor = Base.cursor; - -/** - * Expose `Progress`. - */ - -exports = module.exports = Progress; - -/** - * General progress bar color. - */ - -Base.colors.progress = 90; - -/** - * Constructs a new `Progress` reporter instance. - * - * @public - * @class - * @memberof Mocha.reporters - * @extends Mocha.reporters.Base - * @param {Runner} runner - Instance triggers reporter actions. - * @param {Object} [options] - runner options - */ -function Progress(runner, options) { - Base.call(this, runner, options); - - var self = this; - var width = (Base.window.width * 0.5) | 0; - var total = runner.total; - var complete = 0; - var lastN = -1; - - // default chars - options = options || {}; - var reporterOptions = options.reporterOptions || {}; - - options.open = reporterOptions.open || '['; - options.complete = reporterOptions.complete || '▬'; - options.incomplete = reporterOptions.incomplete || Base.symbols.dot; - options.close = reporterOptions.close || ']'; - options.verbose = reporterOptions.verbose || false; - - // tests started - runner.on(EVENT_RUN_BEGIN, function() { - process.stdout.write('\n'); - cursor.hide(); - }); - - // tests complete - runner.on(EVENT_TEST_END, function() { - complete++; - - var percent = complete / total; - var n = (width * percent) | 0; - var i = width - n; - - if (n === lastN && !options.verbose) { - // Don't re-render the line if it hasn't changed - return; - } - lastN = n; - - cursor.CR(); - process.stdout.write('\u001b[J'); - process.stdout.write(color('progress', ' ' + options.open)); - process.stdout.write(Array(n).join(options.complete)); - process.stdout.write(Array(i).join(options.incomplete)); - process.stdout.write(color('progress', options.close)); - if (options.verbose) { - process.stdout.write(color('progress', ' ' + complete + ' of ' + total)); - } - }); - - // tests are complete, output some stats - // and the failures if any - runner.once(EVENT_RUN_END, function() { - cursor.show(); - process.stdout.write('\n'); - self.epilogue(); - }); -} - -/** - * Inherit from `Base.prototype`. - */ -inherits(Progress, Base); - -Progress.description = 'a progress bar'; - -}).call(this,require('_process')) -},{"../runner":34,"../utils":38,"./base":17,"_process":69}],30:[function(require,module,exports){ -'use strict'; -/** - * @module Spec - */ -/** - * Module dependencies. - */ - -var Base = require('./base'); -var constants = require('../runner').constants; -var EVENT_RUN_BEGIN = constants.EVENT_RUN_BEGIN; -var EVENT_RUN_END = constants.EVENT_RUN_END; -var EVENT_SUITE_BEGIN = constants.EVENT_SUITE_BEGIN; -var EVENT_SUITE_END = constants.EVENT_SUITE_END; -var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL; -var EVENT_TEST_PASS = constants.EVENT_TEST_PASS; -var EVENT_TEST_PENDING = constants.EVENT_TEST_PENDING; -var inherits = require('../utils').inherits; -var color = Base.color; - -/** - * Expose `Spec`. - */ - -exports = module.exports = Spec; - -/** - * Constructs a new `Spec` reporter instance. - * - * @public - * @class - * @memberof Mocha.reporters - * @extends Mocha.reporters.Base - * @param {Runner} runner - Instance triggers reporter actions. - * @param {Object} [options] - runner options - */ -function Spec(runner, options) { - Base.call(this, runner, options); - - var self = this; - var indents = 0; - var n = 0; - - function indent() { - return Array(indents).join(' '); - } - - runner.on(EVENT_RUN_BEGIN, function() { - Base.consoleLog(); - }); - - runner.on(EVENT_SUITE_BEGIN, function(suite) { - ++indents; - Base.consoleLog(color('suite', '%s%s'), indent(), suite.title); - }); - - runner.on(EVENT_SUITE_END, function() { - --indents; - if (indents === 1) { - Base.consoleLog(); - } - }); - - runner.on(EVENT_TEST_PENDING, function(test) { - var fmt = indent() + color('pending', ' - %s'); - Base.consoleLog(fmt, test.title); - }); - - runner.on(EVENT_TEST_PASS, function(test) { - var fmt; - if (test.speed === 'fast') { - fmt = - indent() + - color('checkmark', ' ' + Base.symbols.ok) + - color('pass', ' %s'); - Base.consoleLog(fmt, test.title); - } else { - fmt = - indent() + - color('checkmark', ' ' + Base.symbols.ok) + - color('pass', ' %s') + - color(test.speed, ' (%dms)'); - Base.consoleLog(fmt, test.title, test.duration); - } - }); - - runner.on(EVENT_TEST_FAIL, function(test) { - Base.consoleLog(indent() + color('fail', ' %d) %s'), ++n, test.title); - }); - - runner.once(EVENT_RUN_END, self.epilogue.bind(self)); -} - -/** - * Inherit from `Base.prototype`. - */ -inherits(Spec, Base); - -Spec.description = 'hierarchical & verbose [default]'; - -},{"../runner":34,"../utils":38,"./base":17}],31:[function(require,module,exports){ -(function (process){ -'use strict'; -/** - * @module TAP - */ -/** - * Module dependencies. - */ - -var util = require('util'); -var Base = require('./base'); -var constants = require('../runner').constants; -var EVENT_TEST_PASS = constants.EVENT_TEST_PASS; -var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL; -var EVENT_RUN_BEGIN = constants.EVENT_RUN_BEGIN; -var EVENT_RUN_END = constants.EVENT_RUN_END; -var EVENT_TEST_PENDING = constants.EVENT_TEST_PENDING; -var EVENT_TEST_END = constants.EVENT_TEST_END; -var inherits = require('../utils').inherits; -var sprintf = util.format; - -/** - * Expose `TAP`. - */ - -exports = module.exports = TAP; - -/** - * Constructs a new `TAP` reporter instance. - * - * @public - * @class - * @memberof Mocha.reporters - * @extends Mocha.reporters.Base - * @param {Runner} runner - Instance triggers reporter actions. - * @param {Object} [options] - runner options - */ -function TAP(runner, options) { - Base.call(this, runner, options); - - var self = this; - var n = 1; - - var tapVersion = '12'; - if (options && options.reporterOptions) { - if (options.reporterOptions.tapVersion) { - tapVersion = options.reporterOptions.tapVersion.toString(); - } - } - - this._producer = createProducer(tapVersion); - - runner.once(EVENT_RUN_BEGIN, function() { - var ntests = runner.grepTotal(runner.suite); - self._producer.writeVersion(); - self._producer.writePlan(ntests); - }); - - runner.on(EVENT_TEST_END, function() { - ++n; - }); - - runner.on(EVENT_TEST_PENDING, function(test) { - self._producer.writePending(n, test); - }); - - runner.on(EVENT_TEST_PASS, function(test) { - self._producer.writePass(n, test); - }); - - runner.on(EVENT_TEST_FAIL, function(test, err) { - self._producer.writeFail(n, test, err); - }); - - runner.once(EVENT_RUN_END, function() { - self._producer.writeEpilogue(runner.stats); - }); -} - -/** - * Inherit from `Base.prototype`. - */ -inherits(TAP, Base); - -/** - * Returns a TAP-safe title of `test`. - * - * @private - * @param {Test} test - Test instance. - * @return {String} title with any hash character removed - */ -function title(test) { - return test.fullTitle().replace(/#/g, ''); -} - -/** - * Writes newline-terminated formatted string to reporter output stream. - * - * @private - * @param {string} format - `printf`-like format string - * @param {...*} [varArgs] - Format string arguments - */ -function println(format, varArgs) { - var vargs = Array.from(arguments); - vargs[0] += '\n'; - process.stdout.write(sprintf.apply(null, vargs)); -} - -/** - * Returns a `tapVersion`-appropriate TAP producer instance, if possible. - * - * @private - * @param {string} tapVersion - Version of TAP specification to produce. - * @returns {TAPProducer} specification-appropriate instance - * @throws {Error} if specification version has no associated producer. - */ -function createProducer(tapVersion) { - var producers = { - '12': new TAP12Producer(), - '13': new TAP13Producer() - }; - var producer = producers[tapVersion]; - - if (!producer) { - throw new Error( - 'invalid or unsupported TAP version: ' + JSON.stringify(tapVersion) - ); - } - - return producer; -} - -/** - * @summary - * Constructs a new TAPProducer. - * - * @description - * Only to be used as an abstract base class. - * - * @private - * @constructor - */ -function TAPProducer() {} - -/** - * Writes the TAP version to reporter output stream. - * - * @abstract - */ -TAPProducer.prototype.writeVersion = function() {}; - -/** - * Writes the plan to reporter output stream. - * - * @abstract - * @param {number} ntests - Number of tests that are planned to run. - */ -TAPProducer.prototype.writePlan = function(ntests) { - println('%d..%d', 1, ntests); -}; - -/** - * Writes that test passed to reporter output stream. - * - * @abstract - * @param {number} n - Index of test that passed. - * @param {Test} test - Instance containing test information. - */ -TAPProducer.prototype.writePass = function(n, test) { - println('ok %d %s', n, title(test)); -}; - -/** - * Writes that test was skipped to reporter output stream. - * - * @abstract - * @param {number} n - Index of test that was skipped. - * @param {Test} test - Instance containing test information. - */ -TAPProducer.prototype.writePending = function(n, test) { - println('ok %d %s # SKIP -', n, title(test)); -}; - -/** - * Writes that test failed to reporter output stream. - * - * @abstract - * @param {number} n - Index of test that failed. - * @param {Test} test - Instance containing test information. - * @param {Error} err - Reason the test failed. - */ -TAPProducer.prototype.writeFail = function(n, test, err) { - println('not ok %d %s', n, title(test)); -}; - -/** - * Writes the summary epilogue to reporter output stream. - * - * @abstract - * @param {Object} stats - Object containing run statistics. - */ -TAPProducer.prototype.writeEpilogue = function(stats) { - // :TBD: Why is this not counting pending tests? - println('# tests ' + (stats.passes + stats.failures)); - println('# pass ' + stats.passes); - // :TBD: Why are we not showing pending results? - println('# fail ' + stats.failures); -}; - -/** - * @summary - * Constructs a new TAP12Producer. - * - * @description - * Produces output conforming to the TAP12 specification. - * - * @private - * @constructor - * @extends TAPProducer - * @see {@link https://testanything.org/tap-specification.html|Specification} - */ -function TAP12Producer() { - /** - * Writes that test failed to reporter output stream, with error formatting. - * @override - */ - this.writeFail = function(n, test, err) { - TAPProducer.prototype.writeFail.call(this, n, test, err); - if (err.message) { - println(err.message.replace(/^/gm, ' ')); - } - if (err.stack) { - println(err.stack.replace(/^/gm, ' ')); - } - }; -} - -/** - * Inherit from `TAPProducer.prototype`. - */ -inherits(TAP12Producer, TAPProducer); - -/** - * @summary - * Constructs a new TAP13Producer. - * - * @description - * Produces output conforming to the TAP13 specification. - * - * @private - * @constructor - * @extends TAPProducer - * @see {@link https://testanything.org/tap-version-13-specification.html|Specification} - */ -function TAP13Producer() { - /** - * Writes the TAP version to reporter output stream. - * @override - */ - this.writeVersion = function() { - println('TAP version 13'); - }; - - /** - * Writes that test failed to reporter output stream, with error formatting. - * @override - */ - this.writeFail = function(n, test, err) { - TAPProducer.prototype.writeFail.call(this, n, test, err); - var emitYamlBlock = err.message != null || err.stack != null; - if (emitYamlBlock) { - println(indent(1) + '---'); - if (err.message) { - println(indent(2) + 'message: |-'); - println(err.message.replace(/^/gm, indent(3))); - } - if (err.stack) { - println(indent(2) + 'stack: |-'); - println(err.stack.replace(/^/gm, indent(3))); - } - println(indent(1) + '...'); - } - }; - - function indent(level) { - return Array(level + 1).join(' '); - } -} - -/** - * Inherit from `TAPProducer.prototype`. - */ -inherits(TAP13Producer, TAPProducer); - -TAP.description = 'TAP-compatible output'; - -}).call(this,require('_process')) -},{"../runner":34,"../utils":38,"./base":17,"_process":69,"util":89}],32:[function(require,module,exports){ -(function (process,global){ -'use strict'; -/** - * @module XUnit - */ -/** - * Module dependencies. - */ - -var Base = require('./base'); -var utils = require('../utils'); -var fs = require('fs'); -var mkdirp = require('mkdirp'); -var path = require('path'); -var errors = require('../errors'); -var createUnsupportedError = errors.createUnsupportedError; -var constants = require('../runner').constants; -var EVENT_TEST_PASS = constants.EVENT_TEST_PASS; -var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL; -var EVENT_RUN_END = constants.EVENT_RUN_END; -var EVENT_TEST_PENDING = constants.EVENT_TEST_PENDING; -var STATE_FAILED = require('../runnable').constants.STATE_FAILED; -var inherits = utils.inherits; -var escape = utils.escape; - -/** - * Save timer references to avoid Sinon interfering (see GH-237). - */ -var Date = global.Date; - -/** - * Expose `XUnit`. - */ - -exports = module.exports = XUnit; - -/** - * Constructs a new `XUnit` reporter instance. - * - * @public - * @class - * @memberof Mocha.reporters - * @extends Mocha.reporters.Base - * @param {Runner} runner - Instance triggers reporter actions. - * @param {Object} [options] - runner options - */ -function XUnit(runner, options) { - Base.call(this, runner, options); - - var stats = this.stats; - var tests = []; - var self = this; - - // the name of the test suite, as it will appear in the resulting XML file - var suiteName; - - // the default name of the test suite if none is provided - var DEFAULT_SUITE_NAME = 'Mocha Tests'; - - if (options && options.reporterOptions) { - if (options.reporterOptions.output) { - if (!fs.createWriteStream) { - throw createUnsupportedError('file output not supported in browser'); - } - - mkdirp.sync(path.dirname(options.reporterOptions.output)); - self.fileStream = fs.createWriteStream(options.reporterOptions.output); - } - - // get the suite name from the reporter options (if provided) - suiteName = options.reporterOptions.suiteName; - } - - // fall back to the default suite name - suiteName = suiteName || DEFAULT_SUITE_NAME; - - runner.on(EVENT_TEST_PENDING, function(test) { - tests.push(test); - }); - - runner.on(EVENT_TEST_PASS, function(test) { - tests.push(test); - }); - - runner.on(EVENT_TEST_FAIL, function(test) { - tests.push(test); - }); - - runner.once(EVENT_RUN_END, function() { - self.write( - tag( - 'testsuite', - { - name: suiteName, - tests: stats.tests, - failures: 0, - errors: stats.failures, - skipped: stats.tests - stats.failures - stats.passes, - timestamp: new Date().toUTCString(), - time: stats.duration / 1000 || 0 - }, - false - ) - ); - - tests.forEach(function(t) { - self.test(t); - }); - - self.write(''); - }); -} - -/** - * Inherit from `Base.prototype`. - */ -inherits(XUnit, Base); - -/** - * Override done to close the stream (if it's a file). - * - * @param failures - * @param {Function} fn - */ -XUnit.prototype.done = function(failures, fn) { - if (this.fileStream) { - this.fileStream.end(function() { - fn(failures); - }); - } else { - fn(failures); - } -}; - -/** - * Write out the given line. - * - * @param {string} line - */ -XUnit.prototype.write = function(line) { - if (this.fileStream) { - this.fileStream.write(line + '\n'); - } else if (typeof process === 'object' && process.stdout) { - process.stdout.write(line + '\n'); - } else { - Base.consoleLog(line); - } -}; - -/** - * Output tag for the given `test.` - * - * @param {Test} test - */ -XUnit.prototype.test = function(test) { - Base.useColors = false; - - var attrs = { - classname: test.parent.fullTitle(), - name: test.title, - time: test.duration / 1000 || 0 - }; - - if (test.state === STATE_FAILED) { - var err = test.err; - var diff = - !Base.hideDiff && Base.showDiff(err) - ? '\n' + Base.generateDiff(err.actual, err.expected) - : ''; - this.write( - tag( - 'testcase', - attrs, - false, - tag( - 'failure', - {}, - false, - escape(err.message) + escape(diff) + '\n' + escape(err.stack) - ) - ) - ); - } else if (test.isPending()) { - this.write(tag('testcase', attrs, false, tag('skipped', {}, true))); - } else { - this.write(tag('testcase', attrs, true)); - } -}; - -/** - * HTML tag helper. - * - * @param name - * @param attrs - * @param close - * @param content - * @return {string} - */ -function tag(name, attrs, close, content) { - var end = close ? '/>' : '>'; - var pairs = []; - var tag; - - for (var key in attrs) { - if (Object.prototype.hasOwnProperty.call(attrs, key)) { - pairs.push(key + '="' + escape(attrs[key]) + '"'); - } - } - - tag = '<' + name + (pairs.length ? ' ' + pairs.join(' ') : '') + end; - if (content) { - tag += content + '0, 2^31-1]. - * If clamped value matches either range endpoint, timeouts will be disabled. - * - * @private - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#Maximum_delay_value} - * @param {number|string} ms - Timeout threshold value. - * @returns {Runnable} this - * @chainable - */ -Runnable.prototype.timeout = function(ms) { - if (!arguments.length) { - return this._timeout; - } - if (typeof ms === 'string') { - ms = milliseconds(ms); - } - - // Clamp to range - var INT_MAX = Math.pow(2, 31) - 1; - var range = [0, INT_MAX]; - ms = utils.clamp(ms, range); - - // see #1652 for reasoning - if (ms === range[0] || ms === range[1]) { - this._enableTimeouts = false; - } - debug('timeout %d', ms); - this._timeout = ms; - if (this.timer) { - this.resetTimeout(); - } - return this; -}; - -/** - * Set or get slow `ms`. - * - * @private - * @param {number|string} ms - * @return {Runnable|number} ms or Runnable instance. - */ -Runnable.prototype.slow = function(ms) { - if (!arguments.length || typeof ms === 'undefined') { - return this._slow; - } - if (typeof ms === 'string') { - ms = milliseconds(ms); - } - debug('slow %d', ms); - this._slow = ms; - return this; -}; - -/** - * Set and get whether timeout is `enabled`. - * - * @private - * @param {boolean} enabled - * @return {Runnable|boolean} enabled or Runnable instance. - */ -Runnable.prototype.enableTimeouts = function(enabled) { - if (!arguments.length) { - return this._enableTimeouts; - } - debug('enableTimeouts %s', enabled); - this._enableTimeouts = enabled; - return this; -}; - -/** - * Halt and mark as pending. - * - * @memberof Mocha.Runnable - * @public - */ -Runnable.prototype.skip = function() { - this.pending = true; - throw new Pending('sync skip; aborting execution'); -}; - -/** - * Check if this runnable or its parent suite is marked as pending. - * - * @private - */ -Runnable.prototype.isPending = function() { - return this.pending || (this.parent && this.parent.isPending()); -}; - -/** - * Return `true` if this Runnable has failed. - * @return {boolean} - * @private - */ -Runnable.prototype.isFailed = function() { - return !this.isPending() && this.state === constants.STATE_FAILED; -}; - -/** - * Return `true` if this Runnable has passed. - * @return {boolean} - * @private - */ -Runnable.prototype.isPassed = function() { - return !this.isPending() && this.state === constants.STATE_PASSED; -}; - -/** - * Set or get number of retries. - * - * @private - */ -Runnable.prototype.retries = function(n) { - if (!arguments.length) { - return this._retries; - } - this._retries = n; -}; - -/** - * Set or get current retry - * - * @private - */ -Runnable.prototype.currentRetry = function(n) { - if (!arguments.length) { - return this._currentRetry; - } - this._currentRetry = n; -}; - -/** - * Return the full title generated by recursively concatenating the parent's - * full title. - * - * @memberof Mocha.Runnable - * @public - * @return {string} - */ -Runnable.prototype.fullTitle = function() { - return this.titlePath().join(' '); -}; - -/** - * Return the title path generated by concatenating the parent's title path with the title. - * - * @memberof Mocha.Runnable - * @public - * @return {string} - */ -Runnable.prototype.titlePath = function() { - return this.parent.titlePath().concat([this.title]); -}; - -/** - * Clear the timeout. - * - * @private - */ -Runnable.prototype.clearTimeout = function() { - clearTimeout(this.timer); -}; - -/** - * Reset the timeout. - * - * @private - */ -Runnable.prototype.resetTimeout = function() { - var self = this; - var ms = this.timeout() || 1e9; - - if (!this._enableTimeouts) { - return; - } - this.clearTimeout(); - this.timer = setTimeout(function() { - if (!self._enableTimeouts) { - return; - } - self.callback(self._timeoutError(ms)); - self.timedOut = true; - }, ms); -}; - -/** - * Set or get a list of whitelisted globals for this test run. - * - * @private - * @param {string[]} globals - */ -Runnable.prototype.globals = function(globals) { - if (!arguments.length) { - return this._allowedGlobals; - } - this._allowedGlobals = globals; -}; - -/** - * Run the test and invoke `fn(err)`. - * - * @param {Function} fn - * @private - */ -Runnable.prototype.run = function(fn) { - var self = this; - var start = new Date(); - var ctx = this.ctx; - var finished; - var emitted; - - // Sometimes the ctx exists, but it is not runnable - if (ctx && ctx.runnable) { - ctx.runnable(this); - } - - // called multiple times - function multiple(err) { - if (emitted) { - return; - } - emitted = true; - var msg = 'done() called multiple times'; - if (err && err.message) { - err.message += " (and Mocha's " + msg + ')'; - self.emit('error', err); - } else { - self.emit('error', new Error(msg)); - } - } - - // finished - function done(err) { - var ms = self.timeout(); - if (self.timedOut) { - return; - } - - if (finished) { - return multiple(err); - } - - self.clearTimeout(); - self.duration = new Date() - start; - finished = true; - if (!err && self.duration > ms && self._enableTimeouts) { - err = self._timeoutError(ms); - } - fn(err); - } - - // for .resetTimeout() and Runner#uncaught() - this.callback = done; - - if (this.fn && typeof this.fn.call !== 'function') { - done( - new TypeError( - 'A runnable must be passed a function as its second argument.' - ) - ); - return; - } - - // explicit async with `done` argument - if (this.async) { - this.resetTimeout(); - - // allows skip() to be used in an explicit async context - this.skip = function asyncSkip() { - this.pending = true; - done(); - // halt execution, the uncaught handler will ignore the failure. - throw new Pending('async skip; aborting execution'); - }; - - try { - callFnAsync(this.fn); - } catch (err) { - // handles async runnables which actually run synchronously - emitted = true; - if (err instanceof Pending) { - return; // done() is already called in this.skip() - } else if (this.allowUncaught) { - throw err; - } - done(Runnable.toValueOrError(err)); - } - return; - } - - // sync or promise-returning - try { - if (this.isPending()) { - done(); - } else { - callFn(this.fn); - } - } catch (err) { - emitted = true; - if (err instanceof Pending) { - return done(); - } else if (this.allowUncaught) { - throw err; - } - done(Runnable.toValueOrError(err)); - } - - function callFn(fn) { - var result = fn.call(ctx); - if (result && typeof result.then === 'function') { - self.resetTimeout(); - result.then( - function() { - done(); - // Return null so libraries like bluebird do not warn about - // subsequently constructed Promises. - return null; - }, - function(reason) { - done(reason || new Error('Promise rejected with no or falsy reason')); - } - ); - } else { - if (self.asyncOnly) { - return done( - new Error( - '--async-only option in use without declaring `done()` or returning a promise' - ) - ); - } - - done(); - } - } - - function callFnAsync(fn) { - var result = fn.call(ctx, function(err) { - if (err instanceof Error || toString.call(err) === '[object Error]') { - return done(err); - } - if (err) { - if (Object.prototype.toString.call(err) === '[object Object]') { - return done( - new Error('done() invoked with non-Error: ' + JSON.stringify(err)) - ); - } - return done(new Error('done() invoked with non-Error: ' + err)); - } - if (result && utils.isPromise(result)) { - return done( - new Error( - 'Resolution method is overspecified. Specify a callback *or* return a Promise; not both.' - ) - ); - } - - done(); - }); - } -}; - -/** - * Instantiates a "timeout" error - * - * @param {number} ms - Timeout (in milliseconds) - * @returns {Error} a "timeout" error - * @private - */ -Runnable.prototype._timeoutError = function(ms) { - var msg = - 'Timeout of ' + - ms + - 'ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.'; - if (this.file) { - msg += ' (' + this.file + ')'; - } - return new Error(msg); -}; - -var constants = utils.defineConstants( - /** - * {@link Runnable}-related constants. - * @public - * @memberof Runnable - * @readonly - * @static - * @alias constants - * @enum {string} - */ - { - /** - * Value of `state` prop when a `Runnable` has failed - */ - STATE_FAILED: 'failed', - /** - * Value of `state` prop when a `Runnable` has passed - */ - STATE_PASSED: 'passed' - } -); - -/** - * Given `value`, return identity if truthy, otherwise create an "invalid exception" error and return that. - * @param {*} [value] - Value to return, if present - * @returns {*|Error} `value`, otherwise an `Error` - * @private - */ -Runnable.toValueOrError = function(value) { - return ( - value || - createInvalidExceptionError( - 'Runnable failed with falsy or undefined exception. Please throw an Error instead.', - value - ) - ); -}; - -Runnable.constants = constants; - -}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"./errors":6,"./pending":16,"./utils":38,"debug":45,"events":50,"ms":60}],34:[function(require,module,exports){ -(function (process,global){ -'use strict'; - -/** - * Module dependencies. - */ -var util = require('util'); -var EventEmitter = require('events').EventEmitter; -var Pending = require('./pending'); -var utils = require('./utils'); -var inherits = utils.inherits; -var debug = require('debug')('mocha:runner'); -var Runnable = require('./runnable'); -var Suite = require('./suite'); -var HOOK_TYPE_BEFORE_EACH = Suite.constants.HOOK_TYPE_BEFORE_EACH; -var HOOK_TYPE_AFTER_EACH = Suite.constants.HOOK_TYPE_AFTER_EACH; -var HOOK_TYPE_AFTER_ALL = Suite.constants.HOOK_TYPE_AFTER_ALL; -var HOOK_TYPE_BEFORE_ALL = Suite.constants.HOOK_TYPE_BEFORE_ALL; -var EVENT_ROOT_SUITE_RUN = Suite.constants.EVENT_ROOT_SUITE_RUN; -var STATE_FAILED = Runnable.constants.STATE_FAILED; -var STATE_PASSED = Runnable.constants.STATE_PASSED; -var dQuote = utils.dQuote; -var sQuote = utils.sQuote; -var stackFilter = utils.stackTraceFilter(); -var stringify = utils.stringify; -var type = utils.type; -var errors = require('./errors'); -var createInvalidExceptionError = errors.createInvalidExceptionError; -var createUnsupportedError = errors.createUnsupportedError; - -/** - * Non-enumerable globals. - * @readonly - */ -var globals = [ - 'setTimeout', - 'clearTimeout', - 'setInterval', - 'clearInterval', - 'XMLHttpRequest', - 'Date', - 'setImmediate', - 'clearImmediate' -]; - -var constants = utils.defineConstants( - /** - * {@link Runner}-related constants. - * @public - * @memberof Runner - * @readonly - * @alias constants - * @static - * @enum {string} - */ - { - /** - * Emitted when {@link Hook} execution begins - */ - EVENT_HOOK_BEGIN: 'hook', - /** - * Emitted when {@link Hook} execution ends - */ - EVENT_HOOK_END: 'hook end', - /** - * Emitted when Root {@link Suite} execution begins (all files have been parsed and hooks/tests are ready for execution) - */ - EVENT_RUN_BEGIN: 'start', - /** - * Emitted when Root {@link Suite} execution has been delayed via `delay` option - */ - EVENT_DELAY_BEGIN: 'waiting', - /** - * Emitted when delayed Root {@link Suite} execution is triggered by user via `global.run()` - */ - EVENT_DELAY_END: 'ready', - /** - * Emitted when Root {@link Suite} execution ends - */ - EVENT_RUN_END: 'end', - /** - * Emitted when {@link Suite} execution begins - */ - EVENT_SUITE_BEGIN: 'suite', - /** - * Emitted when {@link Suite} execution ends - */ - EVENT_SUITE_END: 'suite end', - /** - * Emitted when {@link Test} execution begins - */ - EVENT_TEST_BEGIN: 'test', - /** - * Emitted when {@link Test} execution ends - */ - EVENT_TEST_END: 'test end', - /** - * Emitted when {@link Test} execution fails - */ - EVENT_TEST_FAIL: 'fail', - /** - * Emitted when {@link Test} execution succeeds - */ - EVENT_TEST_PASS: 'pass', - /** - * Emitted when {@link Test} becomes pending - */ - EVENT_TEST_PENDING: 'pending', - /** - * Emitted when {@link Test} execution has failed, but will retry - */ - EVENT_TEST_RETRY: 'retry' - } -); - -module.exports = Runner; - -/** - * Initialize a `Runner` at the Root {@link Suite}, which represents a hierarchy of {@link Suite|Suites} and {@link Test|Tests}. - * - * @extends external:EventEmitter - * @public - * @class - * @param {Suite} suite Root suite - * @param {boolean} [delay] Whether or not to delay execution of root suite - * until ready. - */ -function Runner(suite, delay) { - var self = this; - this._globals = []; - this._abort = false; - this._delay = delay; - this.suite = suite; - this.started = false; - this.total = suite.total(); - this.failures = 0; - this.on(constants.EVENT_TEST_END, function(test) { - if (test.type === 'test' && test.retriedTest() && test.parent) { - var idx = - test.parent.tests && test.parent.tests.indexOf(test.retriedTest()); - if (idx > -1) test.parent.tests[idx] = test; - } - self.checkGlobals(test); - }); - this.on(constants.EVENT_HOOK_END, function(hook) { - self.checkGlobals(hook); - }); - this._defaultGrep = /.*/; - this.grep(this._defaultGrep); - this.globals(this.globalProps()); -} - -/** - * Wrapper for setImmediate, process.nextTick, or browser polyfill. - * - * @param {Function} fn - * @private - */ -Runner.immediately = global.setImmediate || process.nextTick; - -/** - * Inherit from `EventEmitter.prototype`. - */ -inherits(Runner, EventEmitter); - -/** - * Run tests with full titles matching `re`. Updates runner.total - * with number of tests matched. - * - * @public - * @memberof Runner - * @param {RegExp} re - * @param {boolean} invert - * @return {Runner} Runner instance. - */ -Runner.prototype.grep = function(re, invert) { - debug('grep %s', re); - this._grep = re; - this._invert = invert; - this.total = this.grepTotal(this.suite); - return this; -}; - -/** - * Returns the number of tests matching the grep search for the - * given suite. - * - * @memberof Runner - * @public - * @param {Suite} suite - * @return {number} - */ -Runner.prototype.grepTotal = function(suite) { - var self = this; - var total = 0; - - suite.eachTest(function(test) { - var match = self._grep.test(test.fullTitle()); - if (self._invert) { - match = !match; - } - if (match) { - total++; - } - }); - - return total; -}; - -/** - * Return a list of global properties. - * - * @return {Array} - * @private - */ -Runner.prototype.globalProps = function() { - var props = Object.keys(global); - - // non-enumerables - for (var i = 0; i < globals.length; ++i) { - if (~props.indexOf(globals[i])) { - continue; - } - props.push(globals[i]); - } - - return props; -}; - -/** - * Allow the given `arr` of globals. - * - * @public - * @memberof Runner - * @param {Array} arr - * @return {Runner} Runner instance. - */ -Runner.prototype.globals = function(arr) { - if (!arguments.length) { - return this._globals; - } - debug('globals %j', arr); - this._globals = this._globals.concat(arr); - return this; -}; - -/** - * Check for global variable leaks. - * - * @private - */ -Runner.prototype.checkGlobals = function(test) { - if (!this.checkLeaks) { - return; - } - var ok = this._globals; - - var globals = this.globalProps(); - var leaks; - - if (test) { - ok = ok.concat(test._allowedGlobals || []); - } - - if (this.prevGlobalsLength === globals.length) { - return; - } - this.prevGlobalsLength = globals.length; - - leaks = filterLeaks(ok, globals); - this._globals = this._globals.concat(leaks); - - if (leaks.length) { - var msg = 'global leak(s) detected: %s'; - var error = new Error(util.format(msg, leaks.map(sQuote).join(', '))); - this.fail(test, error); - } -}; - -/** - * Fail the given `test`. - * - * @private - * @param {Test} test - * @param {Error} err - */ -Runner.prototype.fail = function(test, err) { - if (test.isPending()) { - return; - } - - ++this.failures; - test.state = STATE_FAILED; - - if (!isError(err)) { - err = thrown2Error(err); - } - - try { - err.stack = - this.fullStackTrace || !err.stack ? err.stack : stackFilter(err.stack); - } catch (ignore) { - // some environments do not take kindly to monkeying with the stack - } - - this.emit(constants.EVENT_TEST_FAIL, test, err); -}; - -/** - * Fail the given `hook` with `err`. - * - * Hook failures work in the following pattern: - * - If bail, run corresponding `after each` and `after` hooks, - * then exit - * - Failed `before` hook skips all tests in a suite and subsuites, - * but jumps to corresponding `after` hook - * - Failed `before each` hook skips remaining tests in a - * suite and jumps to corresponding `after each` hook, - * which is run only once - * - Failed `after` hook does not alter execution order - * - Failed `after each` hook skips remaining tests in a - * suite and subsuites, but executes other `after each` - * hooks - * - * @private - * @param {Hook} hook - * @param {Error} err - */ -Runner.prototype.failHook = function(hook, err) { - hook.originalTitle = hook.originalTitle || hook.title; - if (hook.ctx && hook.ctx.currentTest) { - hook.title = - hook.originalTitle + ' for ' + dQuote(hook.ctx.currentTest.title); - } else { - var parentTitle; - if (hook.parent.title) { - parentTitle = hook.parent.title; - } else { - parentTitle = hook.parent.root ? '{root}' : ''; - } - hook.title = hook.originalTitle + ' in ' + dQuote(parentTitle); - } - - this.fail(hook, err); -}; - -/** - * Run hook `name` callbacks and then invoke `fn()`. - * - * @private - * @param {string} name - * @param {Function} fn - */ - -Runner.prototype.hook = function(name, fn) { - var suite = this.suite; - var hooks = suite.getHooks(name); - var self = this; - - function next(i) { - var hook = hooks[i]; - if (!hook) { - return fn(); - } - self.currentRunnable = hook; - - if (name === HOOK_TYPE_BEFORE_ALL) { - hook.ctx.currentTest = hook.parent.tests[0]; - } else if (name === HOOK_TYPE_AFTER_ALL) { - hook.ctx.currentTest = hook.parent.tests[hook.parent.tests.length - 1]; - } else { - hook.ctx.currentTest = self.test; - } - - hook.allowUncaught = self.allowUncaught; - - self.emit(constants.EVENT_HOOK_BEGIN, hook); - - if (!hook.listeners('error').length) { - hook.on('error', function(err) { - self.failHook(hook, err); - }); - } - - hook.run(function(err) { - var testError = hook.error(); - if (testError) { - self.fail(self.test, testError); - } - // conditional skip - if (hook.pending) { - if (name === HOOK_TYPE_AFTER_EACH) { - // TODO define and implement use case - if (self.test) { - self.test.pending = true; - } - } else if (name === HOOK_TYPE_BEFORE_EACH) { - if (self.test) { - self.test.pending = true; - } - self.emit(constants.EVENT_HOOK_END, hook); - hook.pending = false; // activates hook for next test - return fn(new Error('abort hookDown')); - } else if (name === HOOK_TYPE_BEFORE_ALL) { - suite.tests.forEach(function(test) { - test.pending = true; - }); - suite.suites.forEach(function(suite) { - suite.pending = true; - }); - } else { - hook.pending = false; - var errForbid = createUnsupportedError('`this.skip` forbidden'); - self.failHook(hook, errForbid); - return fn(errForbid); - } - } else if (err) { - self.failHook(hook, err); - // stop executing hooks, notify callee of hook err - return fn(err); - } - self.emit(constants.EVENT_HOOK_END, hook); - delete hook.ctx.currentTest; - next(++i); - }); - } - - Runner.immediately(function() { - next(0); - }); -}; - -/** - * Run hook `name` for the given array of `suites` - * in order, and callback `fn(err, errSuite)`. - * - * @private - * @param {string} name - * @param {Array} suites - * @param {Function} fn - */ -Runner.prototype.hooks = function(name, suites, fn) { - var self = this; - var orig = this.suite; - - function next(suite) { - self.suite = suite; - - if (!suite) { - self.suite = orig; - return fn(); - } - - self.hook(name, function(err) { - if (err) { - var errSuite = self.suite; - self.suite = orig; - return fn(err, errSuite); - } - - next(suites.pop()); - }); - } - - next(suites.pop()); -}; - -/** - * Run hooks from the top level down. - * - * @param {String} name - * @param {Function} fn - * @private - */ -Runner.prototype.hookUp = function(name, fn) { - var suites = [this.suite].concat(this.parents()).reverse(); - this.hooks(name, suites, fn); -}; - -/** - * Run hooks from the bottom up. - * - * @param {String} name - * @param {Function} fn - * @private - */ -Runner.prototype.hookDown = function(name, fn) { - var suites = [this.suite].concat(this.parents()); - this.hooks(name, suites, fn); -}; - -/** - * Return an array of parent Suites from - * closest to furthest. - * - * @return {Array} - * @private - */ -Runner.prototype.parents = function() { - var suite = this.suite; - var suites = []; - while (suite.parent) { - suite = suite.parent; - suites.push(suite); - } - return suites; -}; - -/** - * Run the current test and callback `fn(err)`. - * - * @param {Function} fn - * @private - */ -Runner.prototype.runTest = function(fn) { - var self = this; - var test = this.test; - - if (!test) { - return; - } - - var suite = this.parents().reverse()[0] || this.suite; - if (this.forbidOnly && suite.hasOnly()) { - fn(new Error('`.only` forbidden')); - return; - } - if (this.asyncOnly) { - test.asyncOnly = true; - } - test.on('error', function(err) { - if (err instanceof Pending) { - return; - } - self.fail(test, err); - }); - if (this.allowUncaught) { - test.allowUncaught = true; - return test.run(fn); - } - try { - test.run(fn); - } catch (err) { - fn(err); - } -}; - -/** - * Run tests in the given `suite` and invoke the callback `fn()` when complete. - * - * @private - * @param {Suite} suite - * @param {Function} fn - */ -Runner.prototype.runTests = function(suite, fn) { - var self = this; - var tests = suite.tests.slice(); - var test; - - function hookErr(_, errSuite, after) { - // before/after Each hook for errSuite failed: - var orig = self.suite; - - // for failed 'after each' hook start from errSuite parent, - // otherwise start from errSuite itself - self.suite = after ? errSuite.parent : errSuite; - - if (self.suite) { - // call hookUp afterEach - self.hookUp(HOOK_TYPE_AFTER_EACH, function(err2, errSuite2) { - self.suite = orig; - // some hooks may fail even now - if (err2) { - return hookErr(err2, errSuite2, true); - } - // report error suite - fn(errSuite); - }); - } else { - // there is no need calling other 'after each' hooks - self.suite = orig; - fn(errSuite); - } - } - - function next(err, errSuite) { - // if we bail after first err - if (self.failures && suite._bail) { - tests = []; - } - - if (self._abort) { - return fn(); - } - - if (err) { - return hookErr(err, errSuite, true); - } - - // next test - test = tests.shift(); - - // all done - if (!test) { - return fn(); - } - - // grep - var match = self._grep.test(test.fullTitle()); - if (self._invert) { - match = !match; - } - if (!match) { - // Run immediately only if we have defined a grep. When we - // define a grep — It can cause maximum callstack error if - // the grep is doing a large recursive loop by neglecting - // all tests. The run immediately function also comes with - // a performance cost. So we don't want to run immediately - // if we run the whole test suite, because running the whole - // test suite don't do any immediate recursive loops. Thus, - // allowing a JS runtime to breathe. - if (self._grep !== self._defaultGrep) { - Runner.immediately(next); - } else { - next(); - } - return; - } - - // static skip, no hooks are executed - if (test.isPending()) { - if (self.forbidPending) { - test.isPending = alwaysFalse; - self.fail(test, new Error('Pending test forbidden')); - delete test.isPending; - } else { - self.emit(constants.EVENT_TEST_PENDING, test); - } - self.emit(constants.EVENT_TEST_END, test); - return next(); - } - - // execute test and hook(s) - self.emit(constants.EVENT_TEST_BEGIN, (self.test = test)); - self.hookDown(HOOK_TYPE_BEFORE_EACH, function(err, errSuite) { - // conditional skip within beforeEach - if (test.isPending()) { - if (self.forbidPending) { - test.isPending = alwaysFalse; - self.fail(test, new Error('Pending test forbidden')); - delete test.isPending; - } else { - self.emit(constants.EVENT_TEST_PENDING, test); - } - self.emit(constants.EVENT_TEST_END, test); - // skip inner afterEach hooks below errSuite level - var origSuite = self.suite; - self.suite = errSuite || self.suite; - return self.hookUp(HOOK_TYPE_AFTER_EACH, function(e, eSuite) { - self.suite = origSuite; - next(e, eSuite); - }); - } - if (err) { - return hookErr(err, errSuite, false); - } - self.currentRunnable = self.test; - self.runTest(function(err) { - test = self.test; - // conditional skip within it - if (test.pending) { - if (self.forbidPending) { - test.isPending = alwaysFalse; - self.fail(test, new Error('Pending test forbidden')); - delete test.isPending; - } else { - self.emit(constants.EVENT_TEST_PENDING, test); - } - self.emit(constants.EVENT_TEST_END, test); - return self.hookUp(HOOK_TYPE_AFTER_EACH, next); - } else if (err) { - var retry = test.currentRetry(); - if (retry < test.retries()) { - var clonedTest = test.clone(); - clonedTest.currentRetry(retry + 1); - tests.unshift(clonedTest); - - self.emit(constants.EVENT_TEST_RETRY, test, err); - - // Early return + hook trigger so that it doesn't - // increment the count wrong - return self.hookUp(HOOK_TYPE_AFTER_EACH, next); - } else { - self.fail(test, err); - } - self.emit(constants.EVENT_TEST_END, test); - return self.hookUp(HOOK_TYPE_AFTER_EACH, next); - } - - test.state = STATE_PASSED; - self.emit(constants.EVENT_TEST_PASS, test); - self.emit(constants.EVENT_TEST_END, test); - self.hookUp(HOOK_TYPE_AFTER_EACH, next); - }); - }); - } - - this.next = next; - this.hookErr = hookErr; - next(); -}; - -function alwaysFalse() { - return false; -} - -/** - * Run the given `suite` and invoke the callback `fn()` when complete. - * - * @private - * @param {Suite} suite - * @param {Function} fn - */ -Runner.prototype.runSuite = function(suite, fn) { - var i = 0; - var self = this; - var total = this.grepTotal(suite); - - debug('run suite %s', suite.fullTitle()); - - if (!total || (self.failures && suite._bail)) { - return fn(); - } - - this.emit(constants.EVENT_SUITE_BEGIN, (this.suite = suite)); - - function next(errSuite) { - if (errSuite) { - // current suite failed on a hook from errSuite - if (errSuite === suite) { - // if errSuite is current suite - // continue to the next sibling suite - return done(); - } - // errSuite is among the parents of current suite - // stop execution of errSuite and all sub-suites - return done(errSuite); - } - - if (self._abort) { - return done(); - } - - var curr = suite.suites[i++]; - if (!curr) { - return done(); - } - - // Avoid grep neglecting large number of tests causing a - // huge recursive loop and thus a maximum call stack error. - // See comment in `this.runTests()` for more information. - if (self._grep !== self._defaultGrep) { - Runner.immediately(function() { - self.runSuite(curr, next); - }); - } else { - self.runSuite(curr, next); - } - } - - function done(errSuite) { - self.suite = suite; - self.nextSuite = next; - - // remove reference to test - delete self.test; - - self.hook(HOOK_TYPE_AFTER_ALL, function() { - self.emit(constants.EVENT_SUITE_END, suite); - fn(errSuite); - }); - } - - this.nextSuite = next; - - this.hook(HOOK_TYPE_BEFORE_ALL, function(err) { - if (err) { - return done(); - } - self.runTests(suite, next); - }); -}; - -/** - * Handle uncaught exceptions within runner. - * - * @param {Error} err - * @private - */ -Runner.prototype.uncaught = function(err) { - if (err instanceof Pending) { - return; - } - // browser does not exit script when throwing in global.onerror() - if (this.allowUncaught && !process.browser) { - throw err; - } - - if (err) { - debug('uncaught exception %O', err); - } else { - debug('uncaught undefined/falsy exception'); - err = createInvalidExceptionError( - 'Caught falsy/undefined exception which would otherwise be uncaught. No stack trace found; try a debugger', - err - ); - } - - if (!isError(err)) { - err = thrown2Error(err); - } - err.uncaught = true; - - var runnable = this.currentRunnable; - - if (!runnable) { - runnable = new Runnable('Uncaught error outside test suite'); - runnable.parent = this.suite; - - if (this.started) { - this.fail(runnable, err); - } else { - // Can't recover from this failure - this.emit(constants.EVENT_RUN_BEGIN); - this.fail(runnable, err); - this.emit(constants.EVENT_RUN_END); - } - - return; - } - - runnable.clearTimeout(); - - if (runnable.isFailed()) { - // Ignore error if already failed - return; - } else if (runnable.isPending()) { - // report 'pending test' retrospectively as failed - runnable.isPending = alwaysFalse; - this.fail(runnable, err); - delete runnable.isPending; - return; - } - - // we cannot recover gracefully if a Runnable has already passed - // then fails asynchronously - if (runnable.isPassed()) { - this.fail(runnable, err); - this.abort(); - } else { - debug(runnable); - return runnable.callback(err); - } -}; - -/** - * Handle uncaught exceptions after runner's end event. - * - * @param {Error} err - * @private - */ -Runner.prototype.uncaughtEnd = function uncaughtEnd(err) { - if (err instanceof Pending) return; - throw err; -}; - -/** - * Run the root suite and invoke `fn(failures)` - * on completion. - * - * @public - * @memberof Runner - * @param {Function} fn - * @return {Runner} Runner instance. - */ -Runner.prototype.run = function(fn) { - var self = this; - var rootSuite = this.suite; - - fn = fn || function() {}; - - function uncaught(err) { - self.uncaught(err); - } - - function start() { - // If there is an `only` filter - if (rootSuite.hasOnly()) { - rootSuite.filterOnly(); - } - self.started = true; - if (self._delay) { - self.emit(constants.EVENT_DELAY_END); - } - self.emit(constants.EVENT_RUN_BEGIN); - - self.runSuite(rootSuite, function() { - debug('finished running'); - self.emit(constants.EVENT_RUN_END); - }); - } - - debug(constants.EVENT_RUN_BEGIN); - - // references cleanup to avoid memory leaks - this.on(constants.EVENT_SUITE_END, function(suite) { - suite.cleanReferences(); - }); - - // callback - this.on(constants.EVENT_RUN_END, function() { - debug(constants.EVENT_RUN_END); - process.removeListener('uncaughtException', uncaught); - process.on('uncaughtException', self.uncaughtEnd); - fn(self.failures); - }); - - // uncaught exception - process.removeListener('uncaughtException', self.uncaughtEnd); - process.on('uncaughtException', uncaught); - - if (this._delay) { - // for reporters, I guess. - // might be nice to debounce some dots while we wait. - this.emit(constants.EVENT_DELAY_BEGIN, rootSuite); - rootSuite.once(EVENT_ROOT_SUITE_RUN, start); - } else { - Runner.immediately(function() { - start(); - }); - } - - return this; -}; - -/** - * Cleanly abort execution. - * - * @memberof Runner - * @public - * @return {Runner} Runner instance. - */ -Runner.prototype.abort = function() { - debug('aborting'); - this._abort = true; - - return this; -}; - -/** - * Filter leaks with the given globals flagged as `ok`. - * - * @private - * @param {Array} ok - * @param {Array} globals - * @return {Array} - */ -function filterLeaks(ok, globals) { - return globals.filter(function(key) { - // Firefox and Chrome exposes iframes as index inside the window object - if (/^\d+/.test(key)) { - return false; - } - - // in firefox - // if runner runs in an iframe, this iframe's window.getInterface method - // not init at first it is assigned in some seconds - if (global.navigator && /^getInterface/.test(key)) { - return false; - } - - // an iframe could be approached by window[iframeIndex] - // in ie6,7,8 and opera, iframeIndex is enumerable, this could cause leak - if (global.navigator && /^\d+/.test(key)) { - return false; - } - - // Opera and IE expose global variables for HTML element IDs (issue #243) - if (/^mocha-/.test(key)) { - return false; - } - - var matched = ok.filter(function(ok) { - if (~ok.indexOf('*')) { - return key.indexOf(ok.split('*')[0]) === 0; - } - return key === ok; - }); - return !matched.length && (!global.navigator || key !== 'onerror'); - }); -} - -/** - * Check if argument is an instance of Error object or a duck-typed equivalent. - * - * @private - * @param {Object} err - object to check - * @param {string} err.message - error message - * @returns {boolean} - */ -function isError(err) { - return err instanceof Error || (err && typeof err.message === 'string'); -} - -/** - * - * Converts thrown non-extensible type into proper Error. - * - * @private - * @param {*} thrown - Non-extensible type thrown by code - * @return {Error} - */ -function thrown2Error(err) { - return new Error( - 'the ' + type(err) + ' ' + stringify(err) + ' was thrown, throw an Error :)' - ); -} - -Runner.constants = constants; - -/** - * Node.js' `EventEmitter` - * @external EventEmitter - * @see {@link https://nodejs.org/api/events.html#events_class_eventemitter} - */ - -}).call(this,require('_process'),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"./errors":6,"./pending":16,"./runnable":33,"./suite":36,"./utils":38,"_process":69,"debug":45,"events":50,"util":89}],35:[function(require,module,exports){ -(function (global){ -'use strict'; - -/** - * Provides a factory function for a {@link StatsCollector} object. - * @module - */ - -var constants = require('./runner').constants; -var EVENT_TEST_PASS = constants.EVENT_TEST_PASS; -var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL; -var EVENT_SUITE_BEGIN = constants.EVENT_SUITE_BEGIN; -var EVENT_RUN_BEGIN = constants.EVENT_RUN_BEGIN; -var EVENT_TEST_PENDING = constants.EVENT_TEST_PENDING; -var EVENT_RUN_END = constants.EVENT_RUN_END; -var EVENT_TEST_END = constants.EVENT_TEST_END; - -/** - * Test statistics collector. - * - * @public - * @typedef {Object} StatsCollector - * @property {number} suites - integer count of suites run. - * @property {number} tests - integer count of tests run. - * @property {number} passes - integer count of passing tests. - * @property {number} pending - integer count of pending tests. - * @property {number} failures - integer count of failed tests. - * @property {Date} start - time when testing began. - * @property {Date} end - time when testing concluded. - * @property {number} duration - number of msecs that testing took. - */ - -var Date = global.Date; - -/** - * Provides stats such as test duration, number of tests passed / failed etc., by listening for events emitted by `runner`. - * - * @private - * @param {Runner} runner - Runner instance - * @throws {TypeError} If falsy `runner` - */ -function createStatsCollector(runner) { - /** - * @type StatsCollector - */ - var stats = { - suites: 0, - tests: 0, - passes: 0, - pending: 0, - failures: 0 - }; - - if (!runner) { - throw new TypeError('Missing runner argument'); - } - - runner.stats = stats; - - runner.once(EVENT_RUN_BEGIN, function() { - stats.start = new Date(); - }); - runner.on(EVENT_SUITE_BEGIN, function(suite) { - suite.root || stats.suites++; - }); - runner.on(EVENT_TEST_PASS, function() { - stats.passes++; - }); - runner.on(EVENT_TEST_FAIL, function() { - stats.failures++; - }); - runner.on(EVENT_TEST_PENDING, function() { - stats.pending++; - }); - runner.on(EVENT_TEST_END, function() { - stats.tests++; - }); - runner.once(EVENT_RUN_END, function() { - stats.end = new Date(); - stats.duration = stats.end - stats.start; - }); -} - -module.exports = createStatsCollector; - -}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"./runner":34}],36:[function(require,module,exports){ -'use strict'; - -/** - * Module dependencies. - */ -var EventEmitter = require('events').EventEmitter; -var Hook = require('./hook'); -var utils = require('./utils'); -var inherits = utils.inherits; -var debug = require('debug')('mocha:suite'); -var milliseconds = require('ms'); -var errors = require('./errors'); -var createInvalidArgumentTypeError = errors.createInvalidArgumentTypeError; - -/** - * Expose `Suite`. - */ - -exports = module.exports = Suite; - -/** - * Create a new `Suite` with the given `title` and parent `Suite`. - * - * @public - * @param {Suite} parent - Parent suite (required!) - * @param {string} title - Title - * @return {Suite} - */ -Suite.create = function(parent, title) { - var suite = new Suite(title, parent.ctx); - suite.parent = parent; - title = suite.fullTitle(); - parent.addSuite(suite); - return suite; -}; - -/** - * Constructs a new `Suite` instance with the given `title`, `ctx`, and `isRoot`. - * - * @public - * @class - * @extends EventEmitter - * @see {@link https://nodejs.org/api/events.html#events_class_eventemitter|EventEmitter} - * @param {string} title - Suite title. - * @param {Context} parentContext - Parent context instance. - * @param {boolean} [isRoot=false] - Whether this is the root suite. - */ -function Suite(title, parentContext, isRoot) { - if (!utils.isString(title)) { - throw createInvalidArgumentTypeError( - 'Suite argument "title" must be a string. Received type "' + - typeof title + - '"', - 'title', - 'string' - ); - } - this.title = title; - function Context() {} - Context.prototype = parentContext; - this.ctx = new Context(); - this.suites = []; - this.tests = []; - this.pending = false; - this._beforeEach = []; - this._beforeAll = []; - this._afterEach = []; - this._afterAll = []; - this.root = isRoot === true; - this._timeout = 2000; - this._enableTimeouts = true; - this._slow = 75; - this._bail = false; - this._retries = -1; - this._onlyTests = []; - this._onlySuites = []; - this.delayed = false; - - this.on('newListener', function(event) { - if (deprecatedEvents[event]) { - utils.deprecate( - 'Event "' + - event + - '" is deprecated. Please let the Mocha team know about your use case: https://git.io/v6Lwm' - ); - } - }); -} - -/** - * Inherit from `EventEmitter.prototype`. - */ -inherits(Suite, EventEmitter); - -/** - * Return a clone of this `Suite`. - * - * @private - * @return {Suite} - */ -Suite.prototype.clone = function() { - var suite = new Suite(this.title); - debug('clone'); - suite.ctx = this.ctx; - suite.root = this.root; - suite.timeout(this.timeout()); - suite.retries(this.retries()); - suite.enableTimeouts(this.enableTimeouts()); - suite.slow(this.slow()); - suite.bail(this.bail()); - return suite; -}; - -/** - * Set or get timeout `ms` or short-hand such as "2s". - * - * @private - * @todo Do not attempt to set value if `ms` is undefined - * @param {number|string} ms - * @return {Suite|number} for chaining - */ -Suite.prototype.timeout = function(ms) { - if (!arguments.length) { - return this._timeout; - } - if (ms.toString() === '0') { - this._enableTimeouts = false; - } - if (typeof ms === 'string') { - ms = milliseconds(ms); - } - debug('timeout %d', ms); - this._timeout = parseInt(ms, 10); - return this; -}; - -/** - * Set or get number of times to retry a failed test. - * - * @private - * @param {number|string} n - * @return {Suite|number} for chaining - */ -Suite.prototype.retries = function(n) { - if (!arguments.length) { - return this._retries; - } - debug('retries %d', n); - this._retries = parseInt(n, 10) || 0; - return this; -}; - -/** - * Set or get timeout to `enabled`. - * - * @private - * @param {boolean} enabled - * @return {Suite|boolean} self or enabled - */ -Suite.prototype.enableTimeouts = function(enabled) { - if (!arguments.length) { - return this._enableTimeouts; - } - debug('enableTimeouts %s', enabled); - this._enableTimeouts = enabled; - return this; -}; - -/** - * Set or get slow `ms` or short-hand such as "2s". - * - * @private - * @param {number|string} ms - * @return {Suite|number} for chaining - */ -Suite.prototype.slow = function(ms) { - if (!arguments.length) { - return this._slow; - } - if (typeof ms === 'string') { - ms = milliseconds(ms); - } - debug('slow %d', ms); - this._slow = ms; - return this; -}; - -/** - * Set or get whether to bail after first error. - * - * @private - * @param {boolean} bail - * @return {Suite|number} for chaining - */ -Suite.prototype.bail = function(bail) { - if (!arguments.length) { - return this._bail; - } - debug('bail %s', bail); - this._bail = bail; - return this; -}; - -/** - * Check if this suite or its parent suite is marked as pending. - * - * @private - */ -Suite.prototype.isPending = function() { - return this.pending || (this.parent && this.parent.isPending()); -}; - -/** - * Generic hook-creator. - * @private - * @param {string} title - Title of hook - * @param {Function} fn - Hook callback - * @returns {Hook} A new hook - */ -Suite.prototype._createHook = function(title, fn) { - var hook = new Hook(title, fn); - hook.parent = this; - hook.timeout(this.timeout()); - hook.retries(this.retries()); - hook.enableTimeouts(this.enableTimeouts()); - hook.slow(this.slow()); - hook.ctx = this.ctx; - hook.file = this.file; - return hook; -}; - -/** - * Run `fn(test[, done])` before running tests. - * - * @private - * @param {string} title - * @param {Function} fn - * @return {Suite} for chaining - */ -Suite.prototype.beforeAll = function(title, fn) { - if (this.isPending()) { - return this; - } - if (typeof title === 'function') { - fn = title; - title = fn.name; - } - title = '"before all" hook' + (title ? ': ' + title : ''); - - var hook = this._createHook(title, fn); - this._beforeAll.push(hook); - this.emit(constants.EVENT_SUITE_ADD_HOOK_BEFORE_ALL, hook); - return this; -}; - -/** - * Run `fn(test[, done])` after running tests. - * - * @private - * @param {string} title - * @param {Function} fn - * @return {Suite} for chaining - */ -Suite.prototype.afterAll = function(title, fn) { - if (this.isPending()) { - return this; - } - if (typeof title === 'function') { - fn = title; - title = fn.name; - } - title = '"after all" hook' + (title ? ': ' + title : ''); - - var hook = this._createHook(title, fn); - this._afterAll.push(hook); - this.emit(constants.EVENT_SUITE_ADD_HOOK_AFTER_ALL, hook); - return this; -}; - -/** - * Run `fn(test[, done])` before each test case. - * - * @private - * @param {string} title - * @param {Function} fn - * @return {Suite} for chaining - */ -Suite.prototype.beforeEach = function(title, fn) { - if (this.isPending()) { - return this; - } - if (typeof title === 'function') { - fn = title; - title = fn.name; - } - title = '"before each" hook' + (title ? ': ' + title : ''); - - var hook = this._createHook(title, fn); - this._beforeEach.push(hook); - this.emit(constants.EVENT_SUITE_ADD_HOOK_BEFORE_EACH, hook); - return this; -}; - -/** - * Run `fn(test[, done])` after each test case. - * - * @private - * @param {string} title - * @param {Function} fn - * @return {Suite} for chaining - */ -Suite.prototype.afterEach = function(title, fn) { - if (this.isPending()) { - return this; - } - if (typeof title === 'function') { - fn = title; - title = fn.name; - } - title = '"after each" hook' + (title ? ': ' + title : ''); - - var hook = this._createHook(title, fn); - this._afterEach.push(hook); - this.emit(constants.EVENT_SUITE_ADD_HOOK_AFTER_EACH, hook); - return this; -}; - -/** - * Add a test `suite`. - * - * @private - * @param {Suite} suite - * @return {Suite} for chaining - */ -Suite.prototype.addSuite = function(suite) { - suite.parent = this; - suite.root = false; - suite.timeout(this.timeout()); - suite.retries(this.retries()); - suite.enableTimeouts(this.enableTimeouts()); - suite.slow(this.slow()); - suite.bail(this.bail()); - this.suites.push(suite); - this.emit(constants.EVENT_SUITE_ADD_SUITE, suite); - return this; -}; - -/** - * Add a `test` to this suite. - * - * @private - * @param {Test} test - * @return {Suite} for chaining - */ -Suite.prototype.addTest = function(test) { - test.parent = this; - test.timeout(this.timeout()); - test.retries(this.retries()); - test.enableTimeouts(this.enableTimeouts()); - test.slow(this.slow()); - test.ctx = this.ctx; - this.tests.push(test); - this.emit(constants.EVENT_SUITE_ADD_TEST, test); - return this; -}; - -/** - * Return the full title generated by recursively concatenating the parent's - * full title. - * - * @memberof Suite - * @public - * @return {string} - */ -Suite.prototype.fullTitle = function() { - return this.titlePath().join(' '); -}; - -/** - * Return the title path generated by recursively concatenating the parent's - * title path. - * - * @memberof Suite - * @public - * @return {string} - */ -Suite.prototype.titlePath = function() { - var result = []; - if (this.parent) { - result = result.concat(this.parent.titlePath()); - } - if (!this.root) { - result.push(this.title); - } - return result; -}; - -/** - * Return the total number of tests. - * - * @memberof Suite - * @public - * @return {number} - */ -Suite.prototype.total = function() { - return ( - this.suites.reduce(function(sum, suite) { - return sum + suite.total(); - }, 0) + this.tests.length - ); -}; - -/** - * Iterates through each suite recursively to find all tests. Applies a - * function in the format `fn(test)`. - * - * @private - * @param {Function} fn - * @return {Suite} - */ -Suite.prototype.eachTest = function(fn) { - this.tests.forEach(fn); - this.suites.forEach(function(suite) { - suite.eachTest(fn); - }); - return this; -}; - -/** - * This will run the root suite if we happen to be running in delayed mode. - * @private - */ -Suite.prototype.run = function run() { - if (this.root) { - this.emit(constants.EVENT_ROOT_SUITE_RUN); - } -}; - -/** - * Determines whether a suite has an `only` test or suite as a descendant. - * - * @private - * @returns {Boolean} - */ -Suite.prototype.hasOnly = function hasOnly() { - return ( - this._onlyTests.length > 0 || - this._onlySuites.length > 0 || - this.suites.some(function(suite) { - return suite.hasOnly(); - }) - ); -}; - -/** - * Filter suites based on `isOnly` logic. - * - * @private - * @returns {Boolean} - */ -Suite.prototype.filterOnly = function filterOnly() { - if (this._onlyTests.length) { - // If the suite contains `only` tests, run those and ignore any nested suites. - this.tests = this._onlyTests; - this.suites = []; - } else { - // Otherwise, do not run any of the tests in this suite. - this.tests = []; - this._onlySuites.forEach(function(onlySuite) { - // If there are other `only` tests/suites nested in the current `only` suite, then filter that `only` suite. - // Otherwise, all of the tests on this `only` suite should be run, so don't filter it. - if (onlySuite.hasOnly()) { - onlySuite.filterOnly(); - } - }); - // Run the `only` suites, as well as any other suites that have `only` tests/suites as descendants. - var onlySuites = this._onlySuites; - this.suites = this.suites.filter(function(childSuite) { - return onlySuites.indexOf(childSuite) !== -1 || childSuite.filterOnly(); - }); - } - // Keep the suite only if there is something to run - return this.tests.length > 0 || this.suites.length > 0; -}; - -/** - * Adds a suite to the list of subsuites marked `only`. - * - * @private - * @param {Suite} suite - */ -Suite.prototype.appendOnlySuite = function(suite) { - this._onlySuites.push(suite); -}; - -/** - * Adds a test to the list of tests marked `only`. - * - * @private - * @param {Test} test - */ -Suite.prototype.appendOnlyTest = function(test) { - this._onlyTests.push(test); -}; - -/** - * Returns the array of hooks by hook name; see `HOOK_TYPE_*` constants. - * @private - */ -Suite.prototype.getHooks = function getHooks(name) { - return this['_' + name]; -}; - -/** - * Cleans up the references to all the deferred functions - * (before/after/beforeEach/afterEach) and tests of a Suite. - * These must be deleted otherwise a memory leak can happen, - * as those functions may reference variables from closures, - * thus those variables can never be garbage collected as long - * as the deferred functions exist. - * - * @private - */ -Suite.prototype.cleanReferences = function cleanReferences() { - function cleanArrReferences(arr) { - for (var i = 0; i < arr.length; i++) { - delete arr[i].fn; - } - } - - if (Array.isArray(this._beforeAll)) { - cleanArrReferences(this._beforeAll); - } - - if (Array.isArray(this._beforeEach)) { - cleanArrReferences(this._beforeEach); - } - - if (Array.isArray(this._afterAll)) { - cleanArrReferences(this._afterAll); - } - - if (Array.isArray(this._afterEach)) { - cleanArrReferences(this._afterEach); - } - - for (var i = 0; i < this.tests.length; i++) { - delete this.tests[i].fn; - } -}; - -var constants = utils.defineConstants( - /** - * {@link Suite}-related constants. - * @public - * @memberof Suite - * @alias constants - * @readonly - * @static - * @enum {string} - */ - { - /** - * Event emitted after a test file has been loaded Not emitted in browser. - */ - EVENT_FILE_POST_REQUIRE: 'post-require', - /** - * Event emitted before a test file has been loaded. In browser, this is emitted once an interface has been selected. - */ - EVENT_FILE_PRE_REQUIRE: 'pre-require', - /** - * Event emitted immediately after a test file has been loaded. Not emitted in browser. - */ - EVENT_FILE_REQUIRE: 'require', - /** - * Event emitted when `global.run()` is called (use with `delay` option) - */ - EVENT_ROOT_SUITE_RUN: 'run', - - /** - * Namespace for collection of a `Suite`'s "after all" hooks - */ - HOOK_TYPE_AFTER_ALL: 'afterAll', - /** - * Namespace for collection of a `Suite`'s "after each" hooks - */ - HOOK_TYPE_AFTER_EACH: 'afterEach', - /** - * Namespace for collection of a `Suite`'s "before all" hooks - */ - HOOK_TYPE_BEFORE_ALL: 'beforeAll', - /** - * Namespace for collection of a `Suite`'s "before all" hooks - */ - HOOK_TYPE_BEFORE_EACH: 'beforeEach', - - // the following events are all deprecated - - /** - * Emitted after an "after all" `Hook` has been added to a `Suite`. Deprecated - */ - EVENT_SUITE_ADD_HOOK_AFTER_ALL: 'afterAll', - /** - * Emitted after an "after each" `Hook` has been added to a `Suite` Deprecated - */ - EVENT_SUITE_ADD_HOOK_AFTER_EACH: 'afterEach', - /** - * Emitted after an "before all" `Hook` has been added to a `Suite` Deprecated - */ - EVENT_SUITE_ADD_HOOK_BEFORE_ALL: 'beforeAll', - /** - * Emitted after an "before each" `Hook` has been added to a `Suite` Deprecated - */ - EVENT_SUITE_ADD_HOOK_BEFORE_EACH: 'beforeEach', - /** - * Emitted after a child `Suite` has been added to a `Suite`. Deprecated - */ - EVENT_SUITE_ADD_SUITE: 'suite', - /** - * Emitted after a `Test` has been added to a `Suite`. Deprecated - */ - EVENT_SUITE_ADD_TEST: 'test' - } -); - -/** - * @summary There are no known use cases for these events. - * @desc This is a `Set`-like object having all keys being the constant's string value and the value being `true`. - * @todo Remove eventually - * @type {Object} - * @ignore - */ -var deprecatedEvents = Object.keys(constants) - .filter(function(constant) { - return constant.substring(0, 15) === 'EVENT_SUITE_ADD'; - }) - .reduce(function(acc, constant) { - acc[constants[constant]] = true; - return acc; - }, utils.createMap()); - -Suite.constants = constants; - -},{"./errors":6,"./hook":7,"./utils":38,"debug":45,"events":50,"ms":60}],37:[function(require,module,exports){ -'use strict'; -var Runnable = require('./runnable'); -var utils = require('./utils'); -var errors = require('./errors'); -var createInvalidArgumentTypeError = errors.createInvalidArgumentTypeError; -var isString = utils.isString; - -module.exports = Test; - -/** - * Initialize a new `Test` with the given `title` and callback `fn`. - * - * @public - * @class - * @extends Runnable - * @param {String} title - Test title (required) - * @param {Function} [fn] - Test callback. If omitted, the Test is considered "pending" - */ -function Test(title, fn) { - if (!isString(title)) { - throw createInvalidArgumentTypeError( - 'Test argument "title" should be a string. Received type "' + - typeof title + - '"', - 'title', - 'string' - ); - } - Runnable.call(this, title, fn); - this.pending = !fn; - this.type = 'test'; -} - -/** - * Inherit from `Runnable.prototype`. - */ -utils.inherits(Test, Runnable); - -/** - * Set or get retried test - * - * @private - */ -Test.prototype.retriedTest = function(n) { - if (!arguments.length) { - return this._retriedTest; - } - this._retriedTest = n; -}; - -Test.prototype.clone = function() { - var test = new Test(this.title, this.fn); - test.timeout(this.timeout()); - test.slow(this.slow()); - test.enableTimeouts(this.enableTimeouts()); - test.retries(this.retries()); - test.currentRetry(this.currentRetry()); - test.retriedTest(this.retriedTest() || this); - test.globals(this.globals()); - test.parent = this.parent; - test.file = this.file; - test.ctx = this.ctx; - return test; -}; - -},{"./errors":6,"./runnable":33,"./utils":38}],38:[function(require,module,exports){ -(function (process,Buffer){ -'use strict'; - -/** - * Various utility functions used throughout Mocha's codebase. - * @module utils - */ - -/** - * Module dependencies. - */ - -var fs = require('fs'); -var path = require('path'); -var util = require('util'); -var glob = require('glob'); -var he = require('he'); -var errors = require('./errors'); -var createNoFilesMatchPatternError = errors.createNoFilesMatchPatternError; -var createMissingArgumentError = errors.createMissingArgumentError; - -var assign = (exports.assign = require('object.assign').getPolyfill()); - -/** - * Inherit the prototype methods from one constructor into another. - * - * @param {function} ctor - Constructor function which needs to inherit the - * prototype. - * @param {function} superCtor - Constructor function to inherit prototype from. - * @throws {TypeError} if either constructor is null, or if super constructor - * lacks a prototype. - */ -exports.inherits = util.inherits; - -/** - * Escape special characters in the given string of html. - * - * @private - * @param {string} html - * @return {string} - */ -exports.escape = function(html) { - return he.encode(String(html), {useNamedReferences: false}); -}; - -/** - * Test if the given obj is type of string. - * - * @private - * @param {Object} obj - * @return {boolean} - */ -exports.isString = function(obj) { - return typeof obj === 'string'; -}; - -/** - * Compute a slug from the given `str`. - * - * @private - * @param {string} str - * @return {string} - */ -exports.slug = function(str) { - return str - .toLowerCase() - .replace(/ +/g, '-') - .replace(/[^-\w]/g, ''); -}; - -/** - * Strip the function definition from `str`, and re-indent for pre whitespace. - * - * @param {string} str - * @return {string} - */ -exports.clean = function(str) { - str = str - .replace(/\r\n?|[\n\u2028\u2029]/g, '\n') - .replace(/^\uFEFF/, '') - // (traditional)-> space/name parameters body (lambda)-> parameters body multi-statement/single keep body content - .replace( - /^function(?:\s*|\s+[^(]*)\([^)]*\)\s*\{((?:.|\n)*?)\s*\}$|^\([^)]*\)\s*=>\s*(?:\{((?:.|\n)*?)\s*\}|((?:.|\n)*))$/, - '$1$2$3' - ); - - var spaces = str.match(/^\n?( *)/)[1].length; - var tabs = str.match(/^\n?(\t*)/)[1].length; - var re = new RegExp( - '^\n?' + (tabs ? '\t' : ' ') + '{' + (tabs || spaces) + '}', - 'gm' - ); - - str = str.replace(re, ''); - - return str.trim(); -}; - -/** - * Parse the given `qs`. - * - * @private - * @param {string} qs - * @return {Object} - */ -exports.parseQuery = function(qs) { - return qs - .replace('?', '') - .split('&') - .reduce(function(obj, pair) { - var i = pair.indexOf('='); - var key = pair.slice(0, i); - var val = pair.slice(++i); - - // Due to how the URLSearchParams API treats spaces - obj[key] = decodeURIComponent(val.replace(/\+/g, '%20')); - - return obj; - }, {}); -}; - -/** - * Highlight the given string of `js`. - * - * @private - * @param {string} js - * @return {string} - */ -function highlight(js) { - return js - .replace(//g, '>') - .replace(/\/\/(.*)/gm, '//$1') - .replace(/('.*?')/gm, '$1') - .replace(/(\d+\.\d+)/gm, '$1') - .replace(/(\d+)/gm, '$1') - .replace( - /\bnew[ \t]+(\w+)/gm, - 'new $1' - ) - .replace( - /\b(function|new|throw|return|var|if|else)\b/gm, - '$1' - ); -} - -/** - * Highlight the contents of tag `name`. - * - * @private - * @param {string} name - */ -exports.highlightTags = function(name) { - var code = document.getElementById('mocha').getElementsByTagName(name); - for (var i = 0, len = code.length; i < len; ++i) { - code[i].innerHTML = highlight(code[i].innerHTML); - } -}; - -/** - * If a value could have properties, and has none, this function is called, - * which returns a string representation of the empty value. - * - * Functions w/ no properties return `'[Function]'` - * Arrays w/ length === 0 return `'[]'` - * Objects w/ no properties return `'{}'` - * All else: return result of `value.toString()` - * - * @private - * @param {*} value The value to inspect. - * @param {string} typeHint The type of the value - * @returns {string} - */ -function emptyRepresentation(value, typeHint) { - switch (typeHint) { - case 'function': - return '[Function]'; - case 'object': - return '{}'; - case 'array': - return '[]'; - default: - return value.toString(); - } -} - -/** - * Takes some variable and asks `Object.prototype.toString()` what it thinks it - * is. - * - * @private - * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString - * @param {*} value The value to test. - * @returns {string} Computed type - * @example - * type({}) // 'object' - * type([]) // 'array' - * type(1) // 'number' - * type(false) // 'boolean' - * type(Infinity) // 'number' - * type(null) // 'null' - * type(new Date()) // 'date' - * type(/foo/) // 'regexp' - * type('type') // 'string' - * type(global) // 'global' - * type(new String('foo') // 'object' - */ -var type = (exports.type = function type(value) { - if (value === undefined) { - return 'undefined'; - } else if (value === null) { - return 'null'; - } else if (Buffer.isBuffer(value)) { - return 'buffer'; - } - return Object.prototype.toString - .call(value) - .replace(/^\[.+\s(.+?)]$/, '$1') - .toLowerCase(); -}); - -/** - * Stringify `value`. Different behavior depending on type of value: - * - * - If `value` is undefined or null, return `'[undefined]'` or `'[null]'`, respectively. - * - If `value` is not an object, function or array, return result of `value.toString()` wrapped in double-quotes. - * - If `value` is an *empty* object, function, or array, return result of function - * {@link emptyRepresentation}. - * - If `value` has properties, call {@link exports.canonicalize} on it, then return result of - * JSON.stringify(). - * - * @private - * @see exports.type - * @param {*} value - * @return {string} - */ -exports.stringify = function(value) { - var typeHint = type(value); - - if (!~['object', 'array', 'function'].indexOf(typeHint)) { - if (typeHint === 'buffer') { - var json = Buffer.prototype.toJSON.call(value); - // Based on the toJSON result - return jsonStringify( - json.data && json.type ? json.data : json, - 2 - ).replace(/,(\n|$)/g, '$1'); - } - - // IE7/IE8 has a bizarre String constructor; needs to be coerced - // into an array and back to obj. - if (typeHint === 'string' && typeof value === 'object') { - value = value.split('').reduce(function(acc, char, idx) { - acc[idx] = char; - return acc; - }, {}); - typeHint = 'object'; - } else { - return jsonStringify(value); - } - } - - for (var prop in value) { - if (Object.prototype.hasOwnProperty.call(value, prop)) { - return jsonStringify( - exports.canonicalize(value, null, typeHint), - 2 - ).replace(/,(\n|$)/g, '$1'); - } - } - - return emptyRepresentation(value, typeHint); -}; - -/** - * like JSON.stringify but more sense. - * - * @private - * @param {Object} object - * @param {number=} spaces - * @param {number=} depth - * @returns {*} - */ -function jsonStringify(object, spaces, depth) { - if (typeof spaces === 'undefined') { - // primitive types - return _stringify(object); - } - - depth = depth || 1; - var space = spaces * depth; - var str = Array.isArray(object) ? '[' : '{'; - var end = Array.isArray(object) ? ']' : '}'; - var length = - typeof object.length === 'number' - ? object.length - : Object.keys(object).length; - // `.repeat()` polyfill - function repeat(s, n) { - return new Array(n).join(s); - } - - function _stringify(val) { - switch (type(val)) { - case 'null': - case 'undefined': - val = '[' + val + ']'; - break; - case 'array': - case 'object': - val = jsonStringify(val, spaces, depth + 1); - break; - case 'boolean': - case 'regexp': - case 'symbol': - case 'number': - val = - val === 0 && 1 / val === -Infinity // `-0` - ? '-0' - : val.toString(); - break; - case 'date': - var sDate = isNaN(val.getTime()) ? val.toString() : val.toISOString(); - val = '[Date: ' + sDate + ']'; - break; - case 'buffer': - var json = val.toJSON(); - // Based on the toJSON result - json = json.data && json.type ? json.data : json; - val = '[Buffer: ' + jsonStringify(json, 2, depth + 1) + ']'; - break; - default: - val = - val === '[Function]' || val === '[Circular]' - ? val - : JSON.stringify(val); // string - } - return val; - } - - for (var i in object) { - if (!Object.prototype.hasOwnProperty.call(object, i)) { - continue; // not my business - } - --length; - str += - '\n ' + - repeat(' ', space) + - (Array.isArray(object) ? '' : '"' + i + '": ') + // key - _stringify(object[i]) + // value - (length ? ',' : ''); // comma - } - - return ( - str + - // [], {} - (str.length !== 1 ? '\n' + repeat(' ', --space) + end : end) - ); -} - -/** - * Return a new Thing that has the keys in sorted order. Recursive. - * - * If the Thing... - * - has already been seen, return string `'[Circular]'` - * - is `undefined`, return string `'[undefined]'` - * - is `null`, return value `null` - * - is some other primitive, return the value - * - is not a primitive or an `Array`, `Object`, or `Function`, return the value of the Thing's `toString()` method - * - is a non-empty `Array`, `Object`, or `Function`, return the result of calling this function again. - * - is an empty `Array`, `Object`, or `Function`, return the result of calling `emptyRepresentation()` - * - * @private - * @see {@link exports.stringify} - * @param {*} value Thing to inspect. May or may not have properties. - * @param {Array} [stack=[]] Stack of seen values - * @param {string} [typeHint] Type hint - * @return {(Object|Array|Function|string|undefined)} - */ -exports.canonicalize = function canonicalize(value, stack, typeHint) { - var canonicalizedObj; - /* eslint-disable no-unused-vars */ - var prop; - /* eslint-enable no-unused-vars */ - typeHint = typeHint || type(value); - function withStack(value, fn) { - stack.push(value); - fn(); - stack.pop(); - } - - stack = stack || []; - - if (stack.indexOf(value) !== -1) { - return '[Circular]'; - } - - switch (typeHint) { - case 'undefined': - case 'buffer': - case 'null': - canonicalizedObj = value; - break; - case 'array': - withStack(value, function() { - canonicalizedObj = value.map(function(item) { - return exports.canonicalize(item, stack); - }); - }); - break; - case 'function': - /* eslint-disable guard-for-in */ - for (prop in value) { - canonicalizedObj = {}; - break; - } - /* eslint-enable guard-for-in */ - if (!canonicalizedObj) { - canonicalizedObj = emptyRepresentation(value, typeHint); - break; - } - /* falls through */ - case 'object': - canonicalizedObj = canonicalizedObj || {}; - withStack(value, function() { - Object.keys(value) - .sort() - .forEach(function(key) { - canonicalizedObj[key] = exports.canonicalize(value[key], stack); - }); - }); - break; - case 'date': - case 'number': - case 'regexp': - case 'boolean': - case 'symbol': - canonicalizedObj = value; - break; - default: - canonicalizedObj = value + ''; - } - - return canonicalizedObj; -}; - -/** - * Determines if pathname has a matching file extension. - * - * @private - * @param {string} pathname - Pathname to check for match. - * @param {string[]} exts - List of file extensions (sans period). - * @return {boolean} whether file extension matches. - * @example - * hasMatchingExtname('foo.html', ['js', 'css']); // => false - */ -function hasMatchingExtname(pathname, exts) { - var suffix = path.extname(pathname).slice(1); - return exts.some(function(element) { - return suffix === element; - }); -} - -/** - * Determines if pathname would be a "hidden" file (or directory) on UN*X. - * - * @description - * On UN*X, pathnames beginning with a full stop (aka dot) are hidden during - * typical usage. Dotfiles, plain-text configuration files, are prime examples. - * - * @see {@link http://xahlee.info/UnixResource_dir/writ/unix_origin_of_dot_filename.html|Origin of Dot File Names} - * - * @private - * @param {string} pathname - Pathname to check for match. - * @return {boolean} whether pathname would be considered a hidden file. - * @example - * isHiddenOnUnix('.profile'); // => true - */ -function isHiddenOnUnix(pathname) { - return path.basename(pathname)[0] === '.'; -} - -/** - * Lookup file names at the given `path`. - * - * @description - * Filenames are returned in _traversal_ order by the OS/filesystem. - * **Make no assumption that the names will be sorted in any fashion.** - * - * @public - * @memberof Mocha.utils - * @param {string} filepath - Base path to start searching from. - * @param {string[]} [extensions=[]] - File extensions to look for. - * @param {boolean} [recursive=false] - Whether to recurse into subdirectories. - * @return {string[]} An array of paths. - * @throws {Error} if no files match pattern. - * @throws {TypeError} if `filepath` is directory and `extensions` not provided. - */ -exports.lookupFiles = function lookupFiles(filepath, extensions, recursive) { - extensions = extensions || []; - recursive = recursive || false; - var files = []; - var stat; - - if (!fs.existsSync(filepath)) { - var pattern; - if (glob.hasMagic(filepath)) { - // Handle glob as is without extensions - pattern = filepath; - } else { - // glob pattern e.g. 'filepath+(.js|.ts)' - var strExtensions = extensions - .map(function(v) { - return '.' + v; - }) - .join('|'); - pattern = filepath + '+(' + strExtensions + ')'; - } - files = glob.sync(pattern, {nodir: true}); - if (!files.length) { - throw createNoFilesMatchPatternError( - 'Cannot find any files matching pattern ' + exports.dQuote(filepath), - filepath - ); - } - return files; - } - - // Handle file - try { - stat = fs.statSync(filepath); - if (stat.isFile()) { - return filepath; - } - } catch (err) { - // ignore error - return; - } - - // Handle directory - fs.readdirSync(filepath).forEach(function(dirent) { - var pathname = path.join(filepath, dirent); - var stat; - - try { - stat = fs.statSync(pathname); - if (stat.isDirectory()) { - if (recursive) { - files = files.concat(lookupFiles(pathname, extensions, recursive)); - } - return; - } - } catch (err) { - // ignore error - return; - } - if (!extensions.length) { - throw createMissingArgumentError( - util.format( - 'Argument %s required when argument %s is a directory', - exports.sQuote('extensions'), - exports.sQuote('filepath') - ), - 'extensions', - 'array' - ); - } - - if ( - !stat.isFile() || - !hasMatchingExtname(pathname, extensions) || - isHiddenOnUnix(pathname) - ) { - return; - } - files.push(pathname); - }); - - return files; -}; - -/** - * process.emitWarning or a polyfill - * @see https://nodejs.org/api/process.html#process_process_emitwarning_warning_options - * @ignore - */ -function emitWarning(msg, type) { - if (process.emitWarning) { - process.emitWarning(msg, type); - } else { - process.nextTick(function() { - console.warn(type + ': ' + msg); - }); - } -} - -/** - * Show a deprecation warning. Each distinct message is only displayed once. - * Ignores empty messages. - * - * @param {string} [msg] - Warning to print - * @private - */ -exports.deprecate = function deprecate(msg) { - msg = String(msg); - if (msg && !deprecate.cache[msg]) { - deprecate.cache[msg] = true; - emitWarning(msg, 'DeprecationWarning'); - } -}; -exports.deprecate.cache = {}; - -/** - * Show a generic warning. - * Ignores empty messages. - * - * @param {string} [msg] - Warning to print - * @private - */ -exports.warn = function warn(msg) { - if (msg) { - emitWarning(msg); - } -}; - -/** - * @summary - * This Filter based on `mocha-clean` module.(see: `github.com/rstacruz/mocha-clean`) - * @description - * When invoking this function you get a filter function that get the Error.stack as an input, - * and return a prettify output. - * (i.e: strip Mocha and internal node functions from stack trace). - * @returns {Function} - */ -exports.stackTraceFilter = function() { - // TODO: Replace with `process.browser` - var is = typeof document === 'undefined' ? {node: true} : {browser: true}; - var slash = path.sep; - var cwd; - if (is.node) { - cwd = process.cwd() + slash; - } else { - cwd = (typeof location === 'undefined' - ? window.location - : location - ).href.replace(/\/[^/]*$/, '/'); - slash = '/'; - } - - function isMochaInternal(line) { - return ( - ~line.indexOf('node_modules' + slash + 'mocha' + slash) || - ~line.indexOf(slash + 'mocha.js') || - ~line.indexOf(slash + 'mocha.min.js') - ); - } - - function isNodeInternal(line) { - return ( - ~line.indexOf('(timers.js:') || - ~line.indexOf('(events.js:') || - ~line.indexOf('(node.js:') || - ~line.indexOf('(module.js:') || - ~line.indexOf('GeneratorFunctionPrototype.next (native)') || - false - ); - } - - return function(stack) { - stack = stack.split('\n'); - - stack = stack.reduce(function(list, line) { - if (isMochaInternal(line)) { - return list; - } - - if (is.node && isNodeInternal(line)) { - return list; - } - - // Clean up cwd(absolute) - if (/:\d+:\d+\)?$/.test(line)) { - line = line.replace('(' + cwd, '('); - } - - list.push(line); - return list; - }, []); - - return stack.join('\n'); - }; -}; - -/** - * Crude, but effective. - * @public - * @param {*} value - * @returns {boolean} Whether or not `value` is a Promise - */ -exports.isPromise = function isPromise(value) { - return ( - typeof value === 'object' && - value !== null && - typeof value.then === 'function' - ); -}; - -/** - * Clamps a numeric value to an inclusive range. - * - * @param {number} value - Value to be clamped. - * @param {numer[]} range - Two element array specifying [min, max] range. - * @returns {number} clamped value - */ -exports.clamp = function clamp(value, range) { - return Math.min(Math.max(value, range[0]), range[1]); -}; - -/** - * Single quote text by combining with undirectional ASCII quotation marks. - * - * @description - * Provides a simple means of markup for quoting text to be used in output. - * Use this to quote names of variables, methods, and packages. - * - * package 'foo' cannot be found - * - * @private - * @param {string} str - Value to be quoted. - * @returns {string} quoted value - * @example - * sQuote('n') // => 'n' - */ -exports.sQuote = function(str) { - return "'" + str + "'"; -}; - -/** - * Double quote text by combining with undirectional ASCII quotation marks. - * - * @description - * Provides a simple means of markup for quoting text to be used in output. - * Use this to quote names of datatypes, classes, pathnames, and strings. - * - * argument 'value' must be "string" or "number" - * - * @private - * @param {string} str - Value to be quoted. - * @returns {string} quoted value - * @example - * dQuote('number') // => "number" - */ -exports.dQuote = function(str) { - return '"' + str + '"'; -}; - -/** - * It's a noop. - * @public - */ -exports.noop = function() {}; - -/** - * Creates a map-like object. - * - * @description - * A "map" is an object with no prototype, for our purposes. In some cases - * this would be more appropriate than a `Map`, especially if your environment - * doesn't support it. Recommended for use in Mocha's public APIs. - * - * @public - * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map|MDN:Map} - * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create#Custom_and_Null_objects|MDN:Object.create - Custom objects} - * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign|MDN:Object.assign} - * @param {...*} [obj] - Arguments to `Object.assign()`. - * @returns {Object} An object with no prototype, having `...obj` properties - */ -exports.createMap = function(obj) { - return assign.apply( - null, - [Object.create(null)].concat(Array.prototype.slice.call(arguments)) - ); -}; - -/** - * Creates a read-only map-like object. - * - * @description - * This differs from {@link module:utils.createMap createMap} only in that - * the argument must be non-empty, because the result is frozen. - * - * @see {@link module:utils.createMap createMap} - * @param {...*} [obj] - Arguments to `Object.assign()`. - * @returns {Object} A frozen object with no prototype, having `...obj` properties - * @throws {TypeError} if argument is not a non-empty object. - */ -exports.defineConstants = function(obj) { - if (type(obj) !== 'object' || !Object.keys(obj).length) { - throw new TypeError('Invalid argument; expected a non-empty object'); - } - return Object.freeze(exports.createMap(obj)); -}; - -/** - * Whether current version of Node support ES modules - * - * @description - * Versions prior to 10 did not support ES Modules, and version 10 has an old incompatibile version of ESM. - * This function returns whether Node.JS has ES Module supports that is compatible with Mocha's needs, - * which is version >=12.11. - * - * @returns {Boolean} whether the current version of Node.JS supports ES Modules in a way that is compatible with Mocha - */ -exports.supportsEsModules = function() { - if (!process.browser && process.versions && process.versions.node) { - var versionFields = process.versions.node.split('.'); - var major = +versionFields[0]; - var minor = +versionFields[1]; - - if (major >= 13 || (major === 12 && minor >= 11)) { - return true; - } - } -}; - -}).call(this,require('_process'),require("buffer").Buffer) -},{"./errors":6,"_process":69,"buffer":43,"fs":42,"glob":42,"he":54,"object.assign":65,"path":42,"util":89}],39:[function(require,module,exports){ -'use strict' - -exports.byteLength = byteLength -exports.toByteArray = toByteArray -exports.fromByteArray = fromByteArray - -var lookup = [] -var revLookup = [] -var Arr = typeof Uint8Array !== 'undefined' ? Uint8Array : Array - -var code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' -for (var i = 0, len = code.length; i < len; ++i) { - lookup[i] = code[i] - revLookup[code.charCodeAt(i)] = i -} - -// Support decoding URL-safe base64 strings, as Node.js does. -// See: https://en.wikipedia.org/wiki/Base64#URL_applications -revLookup['-'.charCodeAt(0)] = 62 -revLookup['_'.charCodeAt(0)] = 63 - -function getLens (b64) { - var len = b64.length - - if (len % 4 > 0) { - throw new Error('Invalid string. Length must be a multiple of 4') - } - - // Trim off extra bytes after placeholder bytes are found - // See: https://github.com/beatgammit/base64-js/issues/42 - var validLen = b64.indexOf('=') - if (validLen === -1) validLen = len - - var placeHoldersLen = validLen === len - ? 0 - : 4 - (validLen % 4) - - return [validLen, placeHoldersLen] -} - -// base64 is 4/3 + up to two characters of the original data -function byteLength (b64) { - var lens = getLens(b64) - var validLen = lens[0] - var placeHoldersLen = lens[1] - return ((validLen + placeHoldersLen) * 3 / 4) - placeHoldersLen -} - -function _byteLength (b64, validLen, placeHoldersLen) { - return ((validLen + placeHoldersLen) * 3 / 4) - placeHoldersLen -} - -function toByteArray (b64) { - var tmp - var lens = getLens(b64) - var validLen = lens[0] - var placeHoldersLen = lens[1] - - var arr = new Arr(_byteLength(b64, validLen, placeHoldersLen)) - - var curByte = 0 - - // if there are placeholders, only get up to the last complete 4 chars - var len = placeHoldersLen > 0 - ? validLen - 4 - : validLen - - for (var i = 0; i < len; i += 4) { - tmp = - (revLookup[b64.charCodeAt(i)] << 18) | - (revLookup[b64.charCodeAt(i + 1)] << 12) | - (revLookup[b64.charCodeAt(i + 2)] << 6) | - revLookup[b64.charCodeAt(i + 3)] - arr[curByte++] = (tmp >> 16) & 0xFF - arr[curByte++] = (tmp >> 8) & 0xFF - arr[curByte++] = tmp & 0xFF - } - - if (placeHoldersLen === 2) { - tmp = - (revLookup[b64.charCodeAt(i)] << 2) | - (revLookup[b64.charCodeAt(i + 1)] >> 4) - arr[curByte++] = tmp & 0xFF - } - - if (placeHoldersLen === 1) { - tmp = - (revLookup[b64.charCodeAt(i)] << 10) | - (revLookup[b64.charCodeAt(i + 1)] << 4) | - (revLookup[b64.charCodeAt(i + 2)] >> 2) - arr[curByte++] = (tmp >> 8) & 0xFF - arr[curByte++] = tmp & 0xFF - } - - return arr -} - -function tripletToBase64 (num) { - return lookup[num >> 18 & 0x3F] + - lookup[num >> 12 & 0x3F] + - lookup[num >> 6 & 0x3F] + - lookup[num & 0x3F] -} - -function encodeChunk (uint8, start, end) { - var tmp - var output = [] - for (var i = start; i < end; i += 3) { - tmp = - ((uint8[i] << 16) & 0xFF0000) + - ((uint8[i + 1] << 8) & 0xFF00) + - (uint8[i + 2] & 0xFF) - output.push(tripletToBase64(tmp)) - } - return output.join('') -} - -function fromByteArray (uint8) { - var tmp - var len = uint8.length - var extraBytes = len % 3 // if we have 1 byte left, pad 2 bytes - var parts = [] - var maxChunkLength = 16383 // must be multiple of 3 - - // go through the array every three bytes, we'll deal with trailing stuff later - for (var i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) { - parts.push(encodeChunk( - uint8, i, (i + maxChunkLength) > len2 ? len2 : (i + maxChunkLength) - )) - } - - // pad the end with zeros, but make sure to not forget the extra bytes - if (extraBytes === 1) { - tmp = uint8[len - 1] - parts.push( - lookup[tmp >> 2] + - lookup[(tmp << 4) & 0x3F] + - '==' - ) - } else if (extraBytes === 2) { - tmp = (uint8[len - 2] << 8) + uint8[len - 1] - parts.push( - lookup[tmp >> 10] + - lookup[(tmp >> 4) & 0x3F] + - lookup[(tmp << 2) & 0x3F] + - '=' - ) - } - - return parts.join('') -} - -},{}],40:[function(require,module,exports){ - -},{}],41:[function(require,module,exports){ -(function (process){ -var WritableStream = require('stream').Writable -var inherits = require('util').inherits - -module.exports = BrowserStdout - - -inherits(BrowserStdout, WritableStream) - -function BrowserStdout(opts) { - if (!(this instanceof BrowserStdout)) return new BrowserStdout(opts) - - opts = opts || {} - WritableStream.call(this, opts) - this.label = (opts.label !== undefined) ? opts.label : 'stdout' -} - -BrowserStdout.prototype._write = function(chunks, encoding, cb) { - var output = chunks.toString ? chunks.toString() : chunks - if (this.label === false) { - console.log(output) - } else { - console.log(this.label+':', output) - } - process.nextTick(cb) -} - -}).call(this,require('_process')) -},{"_process":69,"stream":84,"util":89}],42:[function(require,module,exports){ -arguments[4][40][0].apply(exports,arguments) -},{"dup":40}],43:[function(require,module,exports){ -(function (Buffer){ -/*! - * The buffer module from node.js, for the browser. - * - * @author Feross Aboukhadijeh - * @license MIT - */ -/* eslint-disable no-proto */ - -'use strict' - -var base64 = require('base64-js') -var ieee754 = require('ieee754') - -exports.Buffer = Buffer -exports.SlowBuffer = SlowBuffer -exports.INSPECT_MAX_BYTES = 50 - -var K_MAX_LENGTH = 0x7fffffff -exports.kMaxLength = K_MAX_LENGTH - -/** - * If `Buffer.TYPED_ARRAY_SUPPORT`: - * === true Use Uint8Array implementation (fastest) - * === false Print warning and recommend using `buffer` v4.x which has an Object - * implementation (most compatible, even IE6) - * - * Browsers that support typed arrays are IE 10+, Firefox 4+, Chrome 7+, Safari 5.1+, - * Opera 11.6+, iOS 4.2+. - * - * We report that the browser does not support typed arrays if the are not subclassable - * using __proto__. Firefox 4-29 lacks support for adding new properties to `Uint8Array` - * (See: https://bugzilla.mozilla.org/show_bug.cgi?id=695438). IE 10 lacks support - * for __proto__ and has a buggy typed array implementation. - */ -Buffer.TYPED_ARRAY_SUPPORT = typedArraySupport() - -if (!Buffer.TYPED_ARRAY_SUPPORT && typeof console !== 'undefined' && - typeof console.error === 'function') { - console.error( - 'This browser lacks typed array (Uint8Array) support which is required by ' + - '`buffer` v5.x. Use `buffer` v4.x if you require old browser support.' - ) -} - -function typedArraySupport () { - // Can typed array instances can be augmented? - try { - var arr = new Uint8Array(1) - arr.__proto__ = { __proto__: Uint8Array.prototype, foo: function () { return 42 } } - return arr.foo() === 42 - } catch (e) { - return false - } -} - -Object.defineProperty(Buffer.prototype, 'parent', { - enumerable: true, - get: function () { - if (!Buffer.isBuffer(this)) return undefined - return this.buffer - } -}) - -Object.defineProperty(Buffer.prototype, 'offset', { - enumerable: true, - get: function () { - if (!Buffer.isBuffer(this)) return undefined - return this.byteOffset - } -}) - -function createBuffer (length) { - if (length > K_MAX_LENGTH) { - throw new RangeError('The value "' + length + '" is invalid for option "size"') - } - // Return an augmented `Uint8Array` instance - var buf = new Uint8Array(length) - buf.__proto__ = Buffer.prototype - return buf -} - -/** - * The Buffer constructor returns instances of `Uint8Array` that have their - * prototype changed to `Buffer.prototype`. Furthermore, `Buffer` is a subclass of - * `Uint8Array`, so the returned instances will have all the node `Buffer` methods - * and the `Uint8Array` methods. Square bracket notation works as expected -- it - * returns a single octet. - * - * The `Uint8Array` prototype remains unmodified. - */ - -function Buffer (arg, encodingOrOffset, length) { - // Common case. - if (typeof arg === 'number') { - if (typeof encodingOrOffset === 'string') { - throw new TypeError( - 'The "string" argument must be of type string. Received type number' - ) - } - return allocUnsafe(arg) - } - return from(arg, encodingOrOffset, length) -} - -// Fix subarray() in ES2016. See: https://github.com/feross/buffer/pull/97 -if (typeof Symbol !== 'undefined' && Symbol.species != null && - Buffer[Symbol.species] === Buffer) { - Object.defineProperty(Buffer, Symbol.species, { - value: null, - configurable: true, - enumerable: false, - writable: false - }) -} - -Buffer.poolSize = 8192 // not used by this implementation - -function from (value, encodingOrOffset, length) { - if (typeof value === 'string') { - return fromString(value, encodingOrOffset) - } - - if (ArrayBuffer.isView(value)) { - return fromArrayLike(value) - } - - if (value == null) { - throw TypeError( - 'The first argument must be one of type string, Buffer, ArrayBuffer, Array, ' + - 'or Array-like Object. Received type ' + (typeof value) - ) - } - - if (isInstance(value, ArrayBuffer) || - (value && isInstance(value.buffer, ArrayBuffer))) { - return fromArrayBuffer(value, encodingOrOffset, length) - } - - if (typeof value === 'number') { - throw new TypeError( - 'The "value" argument must not be of type number. Received type number' - ) - } - - var valueOf = value.valueOf && value.valueOf() - if (valueOf != null && valueOf !== value) { - return Buffer.from(valueOf, encodingOrOffset, length) - } - - var b = fromObject(value) - if (b) return b - - if (typeof Symbol !== 'undefined' && Symbol.toPrimitive != null && - typeof value[Symbol.toPrimitive] === 'function') { - return Buffer.from( - value[Symbol.toPrimitive]('string'), encodingOrOffset, length - ) - } - - throw new TypeError( - 'The first argument must be one of type string, Buffer, ArrayBuffer, Array, ' + - 'or Array-like Object. Received type ' + (typeof value) - ) -} - -/** - * Functionally equivalent to Buffer(arg, encoding) but throws a TypeError - * if value is a number. - * Buffer.from(str[, encoding]) - * Buffer.from(array) - * Buffer.from(buffer) - * Buffer.from(arrayBuffer[, byteOffset[, length]]) - **/ -Buffer.from = function (value, encodingOrOffset, length) { - return from(value, encodingOrOffset, length) -} - -// Note: Change prototype *after* Buffer.from is defined to workaround Chrome bug: -// https://github.com/feross/buffer/pull/148 -Buffer.prototype.__proto__ = Uint8Array.prototype -Buffer.__proto__ = Uint8Array - -function assertSize (size) { - if (typeof size !== 'number') { - throw new TypeError('"size" argument must be of type number') - } else if (size < 0) { - throw new RangeError('The value "' + size + '" is invalid for option "size"') - } -} - -function alloc (size, fill, encoding) { - assertSize(size) - if (size <= 0) { - return createBuffer(size) - } - if (fill !== undefined) { - // Only pay attention to encoding if it's a string. This - // prevents accidentally sending in a number that would - // be interpretted as a start offset. - return typeof encoding === 'string' - ? createBuffer(size).fill(fill, encoding) - : createBuffer(size).fill(fill) - } - return createBuffer(size) -} - -/** - * Creates a new filled Buffer instance. - * alloc(size[, fill[, encoding]]) - **/ -Buffer.alloc = function (size, fill, encoding) { - return alloc(size, fill, encoding) -} - -function allocUnsafe (size) { - assertSize(size) - return createBuffer(size < 0 ? 0 : checked(size) | 0) -} - -/** - * Equivalent to Buffer(num), by default creates a non-zero-filled Buffer instance. - * */ -Buffer.allocUnsafe = function (size) { - return allocUnsafe(size) -} -/** - * Equivalent to SlowBuffer(num), by default creates a non-zero-filled Buffer instance. - */ -Buffer.allocUnsafeSlow = function (size) { - return allocUnsafe(size) -} - -function fromString (string, encoding) { - if (typeof encoding !== 'string' || encoding === '') { - encoding = 'utf8' - } - - if (!Buffer.isEncoding(encoding)) { - throw new TypeError('Unknown encoding: ' + encoding) - } - - var length = byteLength(string, encoding) | 0 - var buf = createBuffer(length) - - var actual = buf.write(string, encoding) - - if (actual !== length) { - // Writing a hex string, for example, that contains invalid characters will - // cause everything after the first invalid character to be ignored. (e.g. - // 'abxxcd' will be treated as 'ab') - buf = buf.slice(0, actual) - } - - return buf -} - -function fromArrayLike (array) { - var length = array.length < 0 ? 0 : checked(array.length) | 0 - var buf = createBuffer(length) - for (var i = 0; i < length; i += 1) { - buf[i] = array[i] & 255 - } - return buf -} - -function fromArrayBuffer (array, byteOffset, length) { - if (byteOffset < 0 || array.byteLength < byteOffset) { - throw new RangeError('"offset" is outside of buffer bounds') - } - - if (array.byteLength < byteOffset + (length || 0)) { - throw new RangeError('"length" is outside of buffer bounds') - } - - var buf - if (byteOffset === undefined && length === undefined) { - buf = new Uint8Array(array) - } else if (length === undefined) { - buf = new Uint8Array(array, byteOffset) - } else { - buf = new Uint8Array(array, byteOffset, length) - } - - // Return an augmented `Uint8Array` instance - buf.__proto__ = Buffer.prototype - return buf -} - -function fromObject (obj) { - if (Buffer.isBuffer(obj)) { - var len = checked(obj.length) | 0 - var buf = createBuffer(len) - - if (buf.length === 0) { - return buf - } - - obj.copy(buf, 0, 0, len) - return buf - } - - if (obj.length !== undefined) { - if (typeof obj.length !== 'number' || numberIsNaN(obj.length)) { - return createBuffer(0) - } - return fromArrayLike(obj) - } - - if (obj.type === 'Buffer' && Array.isArray(obj.data)) { - return fromArrayLike(obj.data) - } -} - -function checked (length) { - // Note: cannot use `length < K_MAX_LENGTH` here because that fails when - // length is NaN (which is otherwise coerced to zero.) - if (length >= K_MAX_LENGTH) { - throw new RangeError('Attempt to allocate Buffer larger than maximum ' + - 'size: 0x' + K_MAX_LENGTH.toString(16) + ' bytes') - } - return length | 0 -} - -function SlowBuffer (length) { - if (+length != length) { // eslint-disable-line eqeqeq - length = 0 - } - return Buffer.alloc(+length) -} - -Buffer.isBuffer = function isBuffer (b) { - return b != null && b._isBuffer === true && - b !== Buffer.prototype // so Buffer.isBuffer(Buffer.prototype) will be false -} - -Buffer.compare = function compare (a, b) { - if (isInstance(a, Uint8Array)) a = Buffer.from(a, a.offset, a.byteLength) - if (isInstance(b, Uint8Array)) b = Buffer.from(b, b.offset, b.byteLength) - if (!Buffer.isBuffer(a) || !Buffer.isBuffer(b)) { - throw new TypeError( - 'The "buf1", "buf2" arguments must be one of type Buffer or Uint8Array' - ) - } - - if (a === b) return 0 - - var x = a.length - var y = b.length - - for (var i = 0, len = Math.min(x, y); i < len; ++i) { - if (a[i] !== b[i]) { - x = a[i] - y = b[i] - break - } - } - - if (x < y) return -1 - if (y < x) return 1 - return 0 -} - -Buffer.isEncoding = function isEncoding (encoding) { - switch (String(encoding).toLowerCase()) { - case 'hex': - case 'utf8': - case 'utf-8': - case 'ascii': - case 'latin1': - case 'binary': - case 'base64': - case 'ucs2': - case 'ucs-2': - case 'utf16le': - case 'utf-16le': - return true - default: - return false - } -} - -Buffer.concat = function concat (list, length) { - if (!Array.isArray(list)) { - throw new TypeError('"list" argument must be an Array of Buffers') - } - - if (list.length === 0) { - return Buffer.alloc(0) - } - - var i - if (length === undefined) { - length = 0 - for (i = 0; i < list.length; ++i) { - length += list[i].length - } - } - - var buffer = Buffer.allocUnsafe(length) - var pos = 0 - for (i = 0; i < list.length; ++i) { - var buf = list[i] - if (isInstance(buf, Uint8Array)) { - buf = Buffer.from(buf) - } - if (!Buffer.isBuffer(buf)) { - throw new TypeError('"list" argument must be an Array of Buffers') - } - buf.copy(buffer, pos) - pos += buf.length - } - return buffer -} - -function byteLength (string, encoding) { - if (Buffer.isBuffer(string)) { - return string.length - } - if (ArrayBuffer.isView(string) || isInstance(string, ArrayBuffer)) { - return string.byteLength - } - if (typeof string !== 'string') { - throw new TypeError( - 'The "string" argument must be one of type string, Buffer, or ArrayBuffer. ' + - 'Received type ' + typeof string - ) - } - - var len = string.length - var mustMatch = (arguments.length > 2 && arguments[2] === true) - if (!mustMatch && len === 0) return 0 - - // Use a for loop to avoid recursion - var loweredCase = false - for (;;) { - switch (encoding) { - case 'ascii': - case 'latin1': - case 'binary': - return len - case 'utf8': - case 'utf-8': - return utf8ToBytes(string).length - case 'ucs2': - case 'ucs-2': - case 'utf16le': - case 'utf-16le': - return len * 2 - case 'hex': - return len >>> 1 - case 'base64': - return base64ToBytes(string).length - default: - if (loweredCase) { - return mustMatch ? -1 : utf8ToBytes(string).length // assume utf8 - } - encoding = ('' + encoding).toLowerCase() - loweredCase = true - } - } -} -Buffer.byteLength = byteLength - -function slowToString (encoding, start, end) { - var loweredCase = false - - // No need to verify that "this.length <= MAX_UINT32" since it's a read-only - // property of a typed array. - - // This behaves neither like String nor Uint8Array in that we set start/end - // to their upper/lower bounds if the value passed is out of range. - // undefined is handled specially as per ECMA-262 6th Edition, - // Section 13.3.3.7 Runtime Semantics: KeyedBindingInitialization. - if (start === undefined || start < 0) { - start = 0 - } - // Return early if start > this.length. Done here to prevent potential uint32 - // coercion fail below. - if (start > this.length) { - return '' - } - - if (end === undefined || end > this.length) { - end = this.length - } - - if (end <= 0) { - return '' - } - - // Force coersion to uint32. This will also coerce falsey/NaN values to 0. - end >>>= 0 - start >>>= 0 - - if (end <= start) { - return '' - } - - if (!encoding) encoding = 'utf8' - - while (true) { - switch (encoding) { - case 'hex': - return hexSlice(this, start, end) - - case 'utf8': - case 'utf-8': - return utf8Slice(this, start, end) - - case 'ascii': - return asciiSlice(this, start, end) - - case 'latin1': - case 'binary': - return latin1Slice(this, start, end) - - case 'base64': - return base64Slice(this, start, end) - - case 'ucs2': - case 'ucs-2': - case 'utf16le': - case 'utf-16le': - return utf16leSlice(this, start, end) - - default: - if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding) - encoding = (encoding + '').toLowerCase() - loweredCase = true - } - } -} - -// This property is used by `Buffer.isBuffer` (and the `is-buffer` npm package) -// to detect a Buffer instance. It's not possible to use `instanceof Buffer` -// reliably in a browserify context because there could be multiple different -// copies of the 'buffer' package in use. This method works even for Buffer -// instances that were created from another copy of the `buffer` package. -// See: https://github.com/feross/buffer/issues/154 -Buffer.prototype._isBuffer = true - -function swap (b, n, m) { - var i = b[n] - b[n] = b[m] - b[m] = i -} - -Buffer.prototype.swap16 = function swap16 () { - var len = this.length - if (len % 2 !== 0) { - throw new RangeError('Buffer size must be a multiple of 16-bits') - } - for (var i = 0; i < len; i += 2) { - swap(this, i, i + 1) - } - return this -} - -Buffer.prototype.swap32 = function swap32 () { - var len = this.length - if (len % 4 !== 0) { - throw new RangeError('Buffer size must be a multiple of 32-bits') - } - for (var i = 0; i < len; i += 4) { - swap(this, i, i + 3) - swap(this, i + 1, i + 2) - } - return this -} - -Buffer.prototype.swap64 = function swap64 () { - var len = this.length - if (len % 8 !== 0) { - throw new RangeError('Buffer size must be a multiple of 64-bits') - } - for (var i = 0; i < len; i += 8) { - swap(this, i, i + 7) - swap(this, i + 1, i + 6) - swap(this, i + 2, i + 5) - swap(this, i + 3, i + 4) - } - return this -} - -Buffer.prototype.toString = function toString () { - var length = this.length - if (length === 0) return '' - if (arguments.length === 0) return utf8Slice(this, 0, length) - return slowToString.apply(this, arguments) -} - -Buffer.prototype.toLocaleString = Buffer.prototype.toString - -Buffer.prototype.equals = function equals (b) { - if (!Buffer.isBuffer(b)) throw new TypeError('Argument must be a Buffer') - if (this === b) return true - return Buffer.compare(this, b) === 0 -} - -Buffer.prototype.inspect = function inspect () { - var str = '' - var max = exports.INSPECT_MAX_BYTES - str = this.toString('hex', 0, max).replace(/(.{2})/g, '$1 ').trim() - if (this.length > max) str += ' ... ' - return '' -} - -Buffer.prototype.compare = function compare (target, start, end, thisStart, thisEnd) { - if (isInstance(target, Uint8Array)) { - target = Buffer.from(target, target.offset, target.byteLength) - } - if (!Buffer.isBuffer(target)) { - throw new TypeError( - 'The "target" argument must be one of type Buffer or Uint8Array. ' + - 'Received type ' + (typeof target) - ) - } - - if (start === undefined) { - start = 0 - } - if (end === undefined) { - end = target ? target.length : 0 - } - if (thisStart === undefined) { - thisStart = 0 - } - if (thisEnd === undefined) { - thisEnd = this.length - } - - if (start < 0 || end > target.length || thisStart < 0 || thisEnd > this.length) { - throw new RangeError('out of range index') - } - - if (thisStart >= thisEnd && start >= end) { - return 0 - } - if (thisStart >= thisEnd) { - return -1 - } - if (start >= end) { - return 1 - } - - start >>>= 0 - end >>>= 0 - thisStart >>>= 0 - thisEnd >>>= 0 - - if (this === target) return 0 - - var x = thisEnd - thisStart - var y = end - start - var len = Math.min(x, y) - - var thisCopy = this.slice(thisStart, thisEnd) - var targetCopy = target.slice(start, end) - - for (var i = 0; i < len; ++i) { - if (thisCopy[i] !== targetCopy[i]) { - x = thisCopy[i] - y = targetCopy[i] - break - } - } - - if (x < y) return -1 - if (y < x) return 1 - return 0 -} - -// Finds either the first index of `val` in `buffer` at offset >= `byteOffset`, -// OR the last index of `val` in `buffer` at offset <= `byteOffset`. -// -// Arguments: -// - buffer - a Buffer to search -// - val - a string, Buffer, or number -// - byteOffset - an index into `buffer`; will be clamped to an int32 -// - encoding - an optional encoding, relevant is val is a string -// - dir - true for indexOf, false for lastIndexOf -function bidirectionalIndexOf (buffer, val, byteOffset, encoding, dir) { - // Empty buffer means no match - if (buffer.length === 0) return -1 - - // Normalize byteOffset - if (typeof byteOffset === 'string') { - encoding = byteOffset - byteOffset = 0 - } else if (byteOffset > 0x7fffffff) { - byteOffset = 0x7fffffff - } else if (byteOffset < -0x80000000) { - byteOffset = -0x80000000 - } - byteOffset = +byteOffset // Coerce to Number. - if (numberIsNaN(byteOffset)) { - // byteOffset: it it's undefined, null, NaN, "foo", etc, search whole buffer - byteOffset = dir ? 0 : (buffer.length - 1) - } - - // Normalize byteOffset: negative offsets start from the end of the buffer - if (byteOffset < 0) byteOffset = buffer.length + byteOffset - if (byteOffset >= buffer.length) { - if (dir) return -1 - else byteOffset = buffer.length - 1 - } else if (byteOffset < 0) { - if (dir) byteOffset = 0 - else return -1 - } - - // Normalize val - if (typeof val === 'string') { - val = Buffer.from(val, encoding) - } - - // Finally, search either indexOf (if dir is true) or lastIndexOf - if (Buffer.isBuffer(val)) { - // Special case: looking for empty string/buffer always fails - if (val.length === 0) { - return -1 - } - return arrayIndexOf(buffer, val, byteOffset, encoding, dir) - } else if (typeof val === 'number') { - val = val & 0xFF // Search for a byte value [0-255] - if (typeof Uint8Array.prototype.indexOf === 'function') { - if (dir) { - return Uint8Array.prototype.indexOf.call(buffer, val, byteOffset) - } else { - return Uint8Array.prototype.lastIndexOf.call(buffer, val, byteOffset) - } - } - return arrayIndexOf(buffer, [ val ], byteOffset, encoding, dir) - } - - throw new TypeError('val must be string, number or Buffer') -} - -function arrayIndexOf (arr, val, byteOffset, encoding, dir) { - var indexSize = 1 - var arrLength = arr.length - var valLength = val.length - - if (encoding !== undefined) { - encoding = String(encoding).toLowerCase() - if (encoding === 'ucs2' || encoding === 'ucs-2' || - encoding === 'utf16le' || encoding === 'utf-16le') { - if (arr.length < 2 || val.length < 2) { - return -1 - } - indexSize = 2 - arrLength /= 2 - valLength /= 2 - byteOffset /= 2 - } - } - - function read (buf, i) { - if (indexSize === 1) { - return buf[i] - } else { - return buf.readUInt16BE(i * indexSize) - } - } - - var i - if (dir) { - var foundIndex = -1 - for (i = byteOffset; i < arrLength; i++) { - if (read(arr, i) === read(val, foundIndex === -1 ? 0 : i - foundIndex)) { - if (foundIndex === -1) foundIndex = i - if (i - foundIndex + 1 === valLength) return foundIndex * indexSize - } else { - if (foundIndex !== -1) i -= i - foundIndex - foundIndex = -1 - } - } - } else { - if (byteOffset + valLength > arrLength) byteOffset = arrLength - valLength - for (i = byteOffset; i >= 0; i--) { - var found = true - for (var j = 0; j < valLength; j++) { - if (read(arr, i + j) !== read(val, j)) { - found = false - break - } - } - if (found) return i - } - } - - return -1 -} - -Buffer.prototype.includes = function includes (val, byteOffset, encoding) { - return this.indexOf(val, byteOffset, encoding) !== -1 -} - -Buffer.prototype.indexOf = function indexOf (val, byteOffset, encoding) { - return bidirectionalIndexOf(this, val, byteOffset, encoding, true) -} - -Buffer.prototype.lastIndexOf = function lastIndexOf (val, byteOffset, encoding) { - return bidirectionalIndexOf(this, val, byteOffset, encoding, false) -} - -function hexWrite (buf, string, offset, length) { - offset = Number(offset) || 0 - var remaining = buf.length - offset - if (!length) { - length = remaining - } else { - length = Number(length) - if (length > remaining) { - length = remaining - } - } - - var strLen = string.length - - if (length > strLen / 2) { - length = strLen / 2 - } - for (var i = 0; i < length; ++i) { - var parsed = parseInt(string.substr(i * 2, 2), 16) - if (numberIsNaN(parsed)) return i - buf[offset + i] = parsed - } - return i -} - -function utf8Write (buf, string, offset, length) { - return blitBuffer(utf8ToBytes(string, buf.length - offset), buf, offset, length) -} - -function asciiWrite (buf, string, offset, length) { - return blitBuffer(asciiToBytes(string), buf, offset, length) -} - -function latin1Write (buf, string, offset, length) { - return asciiWrite(buf, string, offset, length) -} - -function base64Write (buf, string, offset, length) { - return blitBuffer(base64ToBytes(string), buf, offset, length) -} - -function ucs2Write (buf, string, offset, length) { - return blitBuffer(utf16leToBytes(string, buf.length - offset), buf, offset, length) -} - -Buffer.prototype.write = function write (string, offset, length, encoding) { - // Buffer#write(string) - if (offset === undefined) { - encoding = 'utf8' - length = this.length - offset = 0 - // Buffer#write(string, encoding) - } else if (length === undefined && typeof offset === 'string') { - encoding = offset - length = this.length - offset = 0 - // Buffer#write(string, offset[, length][, encoding]) - } else if (isFinite(offset)) { - offset = offset >>> 0 - if (isFinite(length)) { - length = length >>> 0 - if (encoding === undefined) encoding = 'utf8' - } else { - encoding = length - length = undefined - } - } else { - throw new Error( - 'Buffer.write(string, encoding, offset[, length]) is no longer supported' - ) - } - - var remaining = this.length - offset - if (length === undefined || length > remaining) length = remaining - - if ((string.length > 0 && (length < 0 || offset < 0)) || offset > this.length) { - throw new RangeError('Attempt to write outside buffer bounds') - } - - if (!encoding) encoding = 'utf8' - - var loweredCase = false - for (;;) { - switch (encoding) { - case 'hex': - return hexWrite(this, string, offset, length) - - case 'utf8': - case 'utf-8': - return utf8Write(this, string, offset, length) - - case 'ascii': - return asciiWrite(this, string, offset, length) - - case 'latin1': - case 'binary': - return latin1Write(this, string, offset, length) - - case 'base64': - // Warning: maxLength not taken into account in base64Write - return base64Write(this, string, offset, length) - - case 'ucs2': - case 'ucs-2': - case 'utf16le': - case 'utf-16le': - return ucs2Write(this, string, offset, length) - - default: - if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding) - encoding = ('' + encoding).toLowerCase() - loweredCase = true - } - } -} - -Buffer.prototype.toJSON = function toJSON () { - return { - type: 'Buffer', - data: Array.prototype.slice.call(this._arr || this, 0) - } -} - -function base64Slice (buf, start, end) { - if (start === 0 && end === buf.length) { - return base64.fromByteArray(buf) - } else { - return base64.fromByteArray(buf.slice(start, end)) - } -} - -function utf8Slice (buf, start, end) { - end = Math.min(buf.length, end) - var res = [] - - var i = start - while (i < end) { - var firstByte = buf[i] - var codePoint = null - var bytesPerSequence = (firstByte > 0xEF) ? 4 - : (firstByte > 0xDF) ? 3 - : (firstByte > 0xBF) ? 2 - : 1 - - if (i + bytesPerSequence <= end) { - var secondByte, thirdByte, fourthByte, tempCodePoint - - switch (bytesPerSequence) { - case 1: - if (firstByte < 0x80) { - codePoint = firstByte - } - break - case 2: - secondByte = buf[i + 1] - if ((secondByte & 0xC0) === 0x80) { - tempCodePoint = (firstByte & 0x1F) << 0x6 | (secondByte & 0x3F) - if (tempCodePoint > 0x7F) { - codePoint = tempCodePoint - } - } - break - case 3: - secondByte = buf[i + 1] - thirdByte = buf[i + 2] - if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80) { - tempCodePoint = (firstByte & 0xF) << 0xC | (secondByte & 0x3F) << 0x6 | (thirdByte & 0x3F) - if (tempCodePoint > 0x7FF && (tempCodePoint < 0xD800 || tempCodePoint > 0xDFFF)) { - codePoint = tempCodePoint - } - } - break - case 4: - secondByte = buf[i + 1] - thirdByte = buf[i + 2] - fourthByte = buf[i + 3] - if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80 && (fourthByte & 0xC0) === 0x80) { - tempCodePoint = (firstByte & 0xF) << 0x12 | (secondByte & 0x3F) << 0xC | (thirdByte & 0x3F) << 0x6 | (fourthByte & 0x3F) - if (tempCodePoint > 0xFFFF && tempCodePoint < 0x110000) { - codePoint = tempCodePoint - } - } - } - } - - if (codePoint === null) { - // we did not generate a valid codePoint so insert a - // replacement char (U+FFFD) and advance only 1 byte - codePoint = 0xFFFD - bytesPerSequence = 1 - } else if (codePoint > 0xFFFF) { - // encode to utf16 (surrogate pair dance) - codePoint -= 0x10000 - res.push(codePoint >>> 10 & 0x3FF | 0xD800) - codePoint = 0xDC00 | codePoint & 0x3FF - } - - res.push(codePoint) - i += bytesPerSequence - } - - return decodeCodePointsArray(res) -} - -// Based on http://stackoverflow.com/a/22747272/680742, the browser with -// the lowest limit is Chrome, with 0x10000 args. -// We go 1 magnitude less, for safety -var MAX_ARGUMENTS_LENGTH = 0x1000 - -function decodeCodePointsArray (codePoints) { - var len = codePoints.length - if (len <= MAX_ARGUMENTS_LENGTH) { - return String.fromCharCode.apply(String, codePoints) // avoid extra slice() - } - - // Decode in chunks to avoid "call stack size exceeded". - var res = '' - var i = 0 - while (i < len) { - res += String.fromCharCode.apply( - String, - codePoints.slice(i, i += MAX_ARGUMENTS_LENGTH) - ) - } - return res -} - -function asciiSlice (buf, start, end) { - var ret = '' - end = Math.min(buf.length, end) - - for (var i = start; i < end; ++i) { - ret += String.fromCharCode(buf[i] & 0x7F) - } - return ret -} - -function latin1Slice (buf, start, end) { - var ret = '' - end = Math.min(buf.length, end) - - for (var i = start; i < end; ++i) { - ret += String.fromCharCode(buf[i]) - } - return ret -} - -function hexSlice (buf, start, end) { - var len = buf.length - - if (!start || start < 0) start = 0 - if (!end || end < 0 || end > len) end = len - - var out = '' - for (var i = start; i < end; ++i) { - out += toHex(buf[i]) - } - return out -} - -function utf16leSlice (buf, start, end) { - var bytes = buf.slice(start, end) - var res = '' - for (var i = 0; i < bytes.length; i += 2) { - res += String.fromCharCode(bytes[i] + (bytes[i + 1] * 256)) - } - return res -} - -Buffer.prototype.slice = function slice (start, end) { - var len = this.length - start = ~~start - end = end === undefined ? len : ~~end - - if (start < 0) { - start += len - if (start < 0) start = 0 - } else if (start > len) { - start = len - } - - if (end < 0) { - end += len - if (end < 0) end = 0 - } else if (end > len) { - end = len - } - - if (end < start) end = start - - var newBuf = this.subarray(start, end) - // Return an augmented `Uint8Array` instance - newBuf.__proto__ = Buffer.prototype - return newBuf -} - -/* - * Need to make sure that buffer isn't trying to write out of bounds. - */ -function checkOffset (offset, ext, length) { - if ((offset % 1) !== 0 || offset < 0) throw new RangeError('offset is not uint') - if (offset + ext > length) throw new RangeError('Trying to access beyond buffer length') -} - -Buffer.prototype.readUIntLE = function readUIntLE (offset, byteLength, noAssert) { - offset = offset >>> 0 - byteLength = byteLength >>> 0 - if (!noAssert) checkOffset(offset, byteLength, this.length) - - var val = this[offset] - var mul = 1 - var i = 0 - while (++i < byteLength && (mul *= 0x100)) { - val += this[offset + i] * mul - } - - return val -} - -Buffer.prototype.readUIntBE = function readUIntBE (offset, byteLength, noAssert) { - offset = offset >>> 0 - byteLength = byteLength >>> 0 - if (!noAssert) { - checkOffset(offset, byteLength, this.length) - } - - var val = this[offset + --byteLength] - var mul = 1 - while (byteLength > 0 && (mul *= 0x100)) { - val += this[offset + --byteLength] * mul - } - - return val -} - -Buffer.prototype.readUInt8 = function readUInt8 (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 1, this.length) - return this[offset] -} - -Buffer.prototype.readUInt16LE = function readUInt16LE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 2, this.length) - return this[offset] | (this[offset + 1] << 8) -} - -Buffer.prototype.readUInt16BE = function readUInt16BE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 2, this.length) - return (this[offset] << 8) | this[offset + 1] -} - -Buffer.prototype.readUInt32LE = function readUInt32LE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 4, this.length) - - return ((this[offset]) | - (this[offset + 1] << 8) | - (this[offset + 2] << 16)) + - (this[offset + 3] * 0x1000000) -} - -Buffer.prototype.readUInt32BE = function readUInt32BE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 4, this.length) - - return (this[offset] * 0x1000000) + - ((this[offset + 1] << 16) | - (this[offset + 2] << 8) | - this[offset + 3]) -} - -Buffer.prototype.readIntLE = function readIntLE (offset, byteLength, noAssert) { - offset = offset >>> 0 - byteLength = byteLength >>> 0 - if (!noAssert) checkOffset(offset, byteLength, this.length) - - var val = this[offset] - var mul = 1 - var i = 0 - while (++i < byteLength && (mul *= 0x100)) { - val += this[offset + i] * mul - } - mul *= 0x80 - - if (val >= mul) val -= Math.pow(2, 8 * byteLength) - - return val -} - -Buffer.prototype.readIntBE = function readIntBE (offset, byteLength, noAssert) { - offset = offset >>> 0 - byteLength = byteLength >>> 0 - if (!noAssert) checkOffset(offset, byteLength, this.length) - - var i = byteLength - var mul = 1 - var val = this[offset + --i] - while (i > 0 && (mul *= 0x100)) { - val += this[offset + --i] * mul - } - mul *= 0x80 - - if (val >= mul) val -= Math.pow(2, 8 * byteLength) - - return val -} - -Buffer.prototype.readInt8 = function readInt8 (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 1, this.length) - if (!(this[offset] & 0x80)) return (this[offset]) - return ((0xff - this[offset] + 1) * -1) -} - -Buffer.prototype.readInt16LE = function readInt16LE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 2, this.length) - var val = this[offset] | (this[offset + 1] << 8) - return (val & 0x8000) ? val | 0xFFFF0000 : val -} - -Buffer.prototype.readInt16BE = function readInt16BE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 2, this.length) - var val = this[offset + 1] | (this[offset] << 8) - return (val & 0x8000) ? val | 0xFFFF0000 : val -} - -Buffer.prototype.readInt32LE = function readInt32LE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 4, this.length) - - return (this[offset]) | - (this[offset + 1] << 8) | - (this[offset + 2] << 16) | - (this[offset + 3] << 24) -} - -Buffer.prototype.readInt32BE = function readInt32BE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 4, this.length) - - return (this[offset] << 24) | - (this[offset + 1] << 16) | - (this[offset + 2] << 8) | - (this[offset + 3]) -} - -Buffer.prototype.readFloatLE = function readFloatLE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 4, this.length) - return ieee754.read(this, offset, true, 23, 4) -} - -Buffer.prototype.readFloatBE = function readFloatBE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 4, this.length) - return ieee754.read(this, offset, false, 23, 4) -} - -Buffer.prototype.readDoubleLE = function readDoubleLE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 8, this.length) - return ieee754.read(this, offset, true, 52, 8) -} - -Buffer.prototype.readDoubleBE = function readDoubleBE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 8, this.length) - return ieee754.read(this, offset, false, 52, 8) -} - -function checkInt (buf, value, offset, ext, max, min) { - if (!Buffer.isBuffer(buf)) throw new TypeError('"buffer" argument must be a Buffer instance') - if (value > max || value < min) throw new RangeError('"value" argument is out of bounds') - if (offset + ext > buf.length) throw new RangeError('Index out of range') -} - -Buffer.prototype.writeUIntLE = function writeUIntLE (value, offset, byteLength, noAssert) { - value = +value - offset = offset >>> 0 - byteLength = byteLength >>> 0 - if (!noAssert) { - var maxBytes = Math.pow(2, 8 * byteLength) - 1 - checkInt(this, value, offset, byteLength, maxBytes, 0) - } - - var mul = 1 - var i = 0 - this[offset] = value & 0xFF - while (++i < byteLength && (mul *= 0x100)) { - this[offset + i] = (value / mul) & 0xFF - } - - return offset + byteLength -} - -Buffer.prototype.writeUIntBE = function writeUIntBE (value, offset, byteLength, noAssert) { - value = +value - offset = offset >>> 0 - byteLength = byteLength >>> 0 - if (!noAssert) { - var maxBytes = Math.pow(2, 8 * byteLength) - 1 - checkInt(this, value, offset, byteLength, maxBytes, 0) - } - - var i = byteLength - 1 - var mul = 1 - this[offset + i] = value & 0xFF - while (--i >= 0 && (mul *= 0x100)) { - this[offset + i] = (value / mul) & 0xFF - } - - return offset + byteLength -} - -Buffer.prototype.writeUInt8 = function writeUInt8 (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 1, 0xff, 0) - this[offset] = (value & 0xff) - return offset + 1 -} - -Buffer.prototype.writeUInt16LE = function writeUInt16LE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0) - this[offset] = (value & 0xff) - this[offset + 1] = (value >>> 8) - return offset + 2 -} - -Buffer.prototype.writeUInt16BE = function writeUInt16BE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0) - this[offset] = (value >>> 8) - this[offset + 1] = (value & 0xff) - return offset + 2 -} - -Buffer.prototype.writeUInt32LE = function writeUInt32LE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0) - this[offset + 3] = (value >>> 24) - this[offset + 2] = (value >>> 16) - this[offset + 1] = (value >>> 8) - this[offset] = (value & 0xff) - return offset + 4 -} - -Buffer.prototype.writeUInt32BE = function writeUInt32BE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0) - this[offset] = (value >>> 24) - this[offset + 1] = (value >>> 16) - this[offset + 2] = (value >>> 8) - this[offset + 3] = (value & 0xff) - return offset + 4 -} - -Buffer.prototype.writeIntLE = function writeIntLE (value, offset, byteLength, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) { - var limit = Math.pow(2, (8 * byteLength) - 1) - - checkInt(this, value, offset, byteLength, limit - 1, -limit) - } - - var i = 0 - var mul = 1 - var sub = 0 - this[offset] = value & 0xFF - while (++i < byteLength && (mul *= 0x100)) { - if (value < 0 && sub === 0 && this[offset + i - 1] !== 0) { - sub = 1 - } - this[offset + i] = ((value / mul) >> 0) - sub & 0xFF - } - - return offset + byteLength -} - -Buffer.prototype.writeIntBE = function writeIntBE (value, offset, byteLength, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) { - var limit = Math.pow(2, (8 * byteLength) - 1) - - checkInt(this, value, offset, byteLength, limit - 1, -limit) - } - - var i = byteLength - 1 - var mul = 1 - var sub = 0 - this[offset + i] = value & 0xFF - while (--i >= 0 && (mul *= 0x100)) { - if (value < 0 && sub === 0 && this[offset + i + 1] !== 0) { - sub = 1 - } - this[offset + i] = ((value / mul) >> 0) - sub & 0xFF - } - - return offset + byteLength -} - -Buffer.prototype.writeInt8 = function writeInt8 (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 1, 0x7f, -0x80) - if (value < 0) value = 0xff + value + 1 - this[offset] = (value & 0xff) - return offset + 1 -} - -Buffer.prototype.writeInt16LE = function writeInt16LE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000) - this[offset] = (value & 0xff) - this[offset + 1] = (value >>> 8) - return offset + 2 -} - -Buffer.prototype.writeInt16BE = function writeInt16BE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000) - this[offset] = (value >>> 8) - this[offset + 1] = (value & 0xff) - return offset + 2 -} - -Buffer.prototype.writeInt32LE = function writeInt32LE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000) - this[offset] = (value & 0xff) - this[offset + 1] = (value >>> 8) - this[offset + 2] = (value >>> 16) - this[offset + 3] = (value >>> 24) - return offset + 4 -} - -Buffer.prototype.writeInt32BE = function writeInt32BE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000) - if (value < 0) value = 0xffffffff + value + 1 - this[offset] = (value >>> 24) - this[offset + 1] = (value >>> 16) - this[offset + 2] = (value >>> 8) - this[offset + 3] = (value & 0xff) - return offset + 4 -} - -function checkIEEE754 (buf, value, offset, ext, max, min) { - if (offset + ext > buf.length) throw new RangeError('Index out of range') - if (offset < 0) throw new RangeError('Index out of range') -} - -function writeFloat (buf, value, offset, littleEndian, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) { - checkIEEE754(buf, value, offset, 4, 3.4028234663852886e+38, -3.4028234663852886e+38) - } - ieee754.write(buf, value, offset, littleEndian, 23, 4) - return offset + 4 -} - -Buffer.prototype.writeFloatLE = function writeFloatLE (value, offset, noAssert) { - return writeFloat(this, value, offset, true, noAssert) -} - -Buffer.prototype.writeFloatBE = function writeFloatBE (value, offset, noAssert) { - return writeFloat(this, value, offset, false, noAssert) -} - -function writeDouble (buf, value, offset, littleEndian, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) { - checkIEEE754(buf, value, offset, 8, 1.7976931348623157E+308, -1.7976931348623157E+308) - } - ieee754.write(buf, value, offset, littleEndian, 52, 8) - return offset + 8 -} - -Buffer.prototype.writeDoubleLE = function writeDoubleLE (value, offset, noAssert) { - return writeDouble(this, value, offset, true, noAssert) -} - -Buffer.prototype.writeDoubleBE = function writeDoubleBE (value, offset, noAssert) { - return writeDouble(this, value, offset, false, noAssert) -} - -// copy(targetBuffer, targetStart=0, sourceStart=0, sourceEnd=buffer.length) -Buffer.prototype.copy = function copy (target, targetStart, start, end) { - if (!Buffer.isBuffer(target)) throw new TypeError('argument should be a Buffer') - if (!start) start = 0 - if (!end && end !== 0) end = this.length - if (targetStart >= target.length) targetStart = target.length - if (!targetStart) targetStart = 0 - if (end > 0 && end < start) end = start - - // Copy 0 bytes; we're done - if (end === start) return 0 - if (target.length === 0 || this.length === 0) return 0 - - // Fatal error conditions - if (targetStart < 0) { - throw new RangeError('targetStart out of bounds') - } - if (start < 0 || start >= this.length) throw new RangeError('Index out of range') - if (end < 0) throw new RangeError('sourceEnd out of bounds') - - // Are we oob? - if (end > this.length) end = this.length - if (target.length - targetStart < end - start) { - end = target.length - targetStart + start - } - - var len = end - start - - if (this === target && typeof Uint8Array.prototype.copyWithin === 'function') { - // Use built-in when available, missing from IE11 - this.copyWithin(targetStart, start, end) - } else if (this === target && start < targetStart && targetStart < end) { - // descending copy from end - for (var i = len - 1; i >= 0; --i) { - target[i + targetStart] = this[i + start] - } - } else { - Uint8Array.prototype.set.call( - target, - this.subarray(start, end), - targetStart - ) - } - - return len -} - -// Usage: -// buffer.fill(number[, offset[, end]]) -// buffer.fill(buffer[, offset[, end]]) -// buffer.fill(string[, offset[, end]][, encoding]) -Buffer.prototype.fill = function fill (val, start, end, encoding) { - // Handle string cases: - if (typeof val === 'string') { - if (typeof start === 'string') { - encoding = start - start = 0 - end = this.length - } else if (typeof end === 'string') { - encoding = end - end = this.length - } - if (encoding !== undefined && typeof encoding !== 'string') { - throw new TypeError('encoding must be a string') - } - if (typeof encoding === 'string' && !Buffer.isEncoding(encoding)) { - throw new TypeError('Unknown encoding: ' + encoding) - } - if (val.length === 1) { - var code = val.charCodeAt(0) - if ((encoding === 'utf8' && code < 128) || - encoding === 'latin1') { - // Fast path: If `val` fits into a single byte, use that numeric value. - val = code - } - } - } else if (typeof val === 'number') { - val = val & 255 - } - - // Invalid ranges are not set to a default, so can range check early. - if (start < 0 || this.length < start || this.length < end) { - throw new RangeError('Out of range index') - } - - if (end <= start) { - return this - } - - start = start >>> 0 - end = end === undefined ? this.length : end >>> 0 - - if (!val) val = 0 - - var i - if (typeof val === 'number') { - for (i = start; i < end; ++i) { - this[i] = val - } - } else { - var bytes = Buffer.isBuffer(val) - ? val - : Buffer.from(val, encoding) - var len = bytes.length - if (len === 0) { - throw new TypeError('The value "' + val + - '" is invalid for argument "value"') - } - for (i = 0; i < end - start; ++i) { - this[i + start] = bytes[i % len] - } - } - - return this -} - -// HELPER FUNCTIONS -// ================ - -var INVALID_BASE64_RE = /[^+/0-9A-Za-z-_]/g - -function base64clean (str) { - // Node takes equal signs as end of the Base64 encoding - str = str.split('=')[0] - // Node strips out invalid characters like \n and \t from the string, base64-js does not - str = str.trim().replace(INVALID_BASE64_RE, '') - // Node converts strings with length < 2 to '' - if (str.length < 2) return '' - // Node allows for non-padded base64 strings (missing trailing ===), base64-js does not - while (str.length % 4 !== 0) { - str = str + '=' - } - return str -} - -function toHex (n) { - if (n < 16) return '0' + n.toString(16) - return n.toString(16) -} - -function utf8ToBytes (string, units) { - units = units || Infinity - var codePoint - var length = string.length - var leadSurrogate = null - var bytes = [] - - for (var i = 0; i < length; ++i) { - codePoint = string.charCodeAt(i) - - // is surrogate component - if (codePoint > 0xD7FF && codePoint < 0xE000) { - // last char was a lead - if (!leadSurrogate) { - // no lead yet - if (codePoint > 0xDBFF) { - // unexpected trail - if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) - continue - } else if (i + 1 === length) { - // unpaired lead - if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) - continue - } - - // valid lead - leadSurrogate = codePoint - - continue - } - - // 2 leads in a row - if (codePoint < 0xDC00) { - if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) - leadSurrogate = codePoint - continue - } - - // valid surrogate pair - codePoint = (leadSurrogate - 0xD800 << 10 | codePoint - 0xDC00) + 0x10000 - } else if (leadSurrogate) { - // valid bmp char, but last char was a lead - if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) - } - - leadSurrogate = null - - // encode utf8 - if (codePoint < 0x80) { - if ((units -= 1) < 0) break - bytes.push(codePoint) - } else if (codePoint < 0x800) { - if ((units -= 2) < 0) break - bytes.push( - codePoint >> 0x6 | 0xC0, - codePoint & 0x3F | 0x80 - ) - } else if (codePoint < 0x10000) { - if ((units -= 3) < 0) break - bytes.push( - codePoint >> 0xC | 0xE0, - codePoint >> 0x6 & 0x3F | 0x80, - codePoint & 0x3F | 0x80 - ) - } else if (codePoint < 0x110000) { - if ((units -= 4) < 0) break - bytes.push( - codePoint >> 0x12 | 0xF0, - codePoint >> 0xC & 0x3F | 0x80, - codePoint >> 0x6 & 0x3F | 0x80, - codePoint & 0x3F | 0x80 - ) - } else { - throw new Error('Invalid code point') - } - } - - return bytes -} - -function asciiToBytes (str) { - var byteArray = [] - for (var i = 0; i < str.length; ++i) { - // Node's code seems to be doing this and not & 0x7F.. - byteArray.push(str.charCodeAt(i) & 0xFF) - } - return byteArray -} - -function utf16leToBytes (str, units) { - var c, hi, lo - var byteArray = [] - for (var i = 0; i < str.length; ++i) { - if ((units -= 2) < 0) break - - c = str.charCodeAt(i) - hi = c >> 8 - lo = c % 256 - byteArray.push(lo) - byteArray.push(hi) - } - - return byteArray -} - -function base64ToBytes (str) { - return base64.toByteArray(base64clean(str)) -} - -function blitBuffer (src, dst, offset, length) { - for (var i = 0; i < length; ++i) { - if ((i + offset >= dst.length) || (i >= src.length)) break - dst[i + offset] = src[i] - } - return i -} - -// ArrayBuffer or Uint8Array objects from other contexts (i.e. iframes) do not pass -// the `instanceof` check but they should be treated as of that type. -// See: https://github.com/feross/buffer/issues/166 -function isInstance (obj, type) { - return obj instanceof type || - (obj != null && obj.constructor != null && obj.constructor.name != null && - obj.constructor.name === type.name) -} -function numberIsNaN (obj) { - // For IE11 support - return obj !== obj // eslint-disable-line no-self-compare -} - -}).call(this,require("buffer").Buffer) -},{"base64-js":39,"buffer":43,"ieee754":55}],44:[function(require,module,exports){ -(function (Buffer){ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -// NOTE: These type checking functions intentionally don't use `instanceof` -// because it is fragile and can be easily faked with `Object.create()`. - -function isArray(arg) { - if (Array.isArray) { - return Array.isArray(arg); - } - return objectToString(arg) === '[object Array]'; -} -exports.isArray = isArray; - -function isBoolean(arg) { - return typeof arg === 'boolean'; -} -exports.isBoolean = isBoolean; - -function isNull(arg) { - return arg === null; -} -exports.isNull = isNull; - -function isNullOrUndefined(arg) { - return arg == null; -} -exports.isNullOrUndefined = isNullOrUndefined; - -function isNumber(arg) { - return typeof arg === 'number'; -} -exports.isNumber = isNumber; - -function isString(arg) { - return typeof arg === 'string'; -} -exports.isString = isString; - -function isSymbol(arg) { - return typeof arg === 'symbol'; -} -exports.isSymbol = isSymbol; - -function isUndefined(arg) { - return arg === void 0; -} -exports.isUndefined = isUndefined; - -function isRegExp(re) { - return objectToString(re) === '[object RegExp]'; -} -exports.isRegExp = isRegExp; - -function isObject(arg) { - return typeof arg === 'object' && arg !== null; -} -exports.isObject = isObject; - -function isDate(d) { - return objectToString(d) === '[object Date]'; -} -exports.isDate = isDate; - -function isError(e) { - return (objectToString(e) === '[object Error]' || e instanceof Error); -} -exports.isError = isError; - -function isFunction(arg) { - return typeof arg === 'function'; -} -exports.isFunction = isFunction; - -function isPrimitive(arg) { - return arg === null || - typeof arg === 'boolean' || - typeof arg === 'number' || - typeof arg === 'string' || - typeof arg === 'symbol' || // ES6 symbol - typeof arg === 'undefined'; -} -exports.isPrimitive = isPrimitive; - -exports.isBuffer = Buffer.isBuffer; - -function objectToString(o) { - return Object.prototype.toString.call(o); -} - -}).call(this,{"isBuffer":require("../../is-buffer/index.js")}) -},{"../../is-buffer/index.js":57}],45:[function(require,module,exports){ -(function (process){ -"use strict"; - -function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } - -/* eslint-env browser */ - -/** - * This is the web browser implementation of `debug()`. - */ -exports.log = log; -exports.formatArgs = formatArgs; -exports.save = save; -exports.load = load; -exports.useColors = useColors; -exports.storage = localstorage(); -/** - * Colors. - */ - -exports.colors = ['#0000CC', '#0000FF', '#0033CC', '#0033FF', '#0066CC', '#0066FF', '#0099CC', '#0099FF', '#00CC00', '#00CC33', '#00CC66', '#00CC99', '#00CCCC', '#00CCFF', '#3300CC', '#3300FF', '#3333CC', '#3333FF', '#3366CC', '#3366FF', '#3399CC', '#3399FF', '#33CC00', '#33CC33', '#33CC66', '#33CC99', '#33CCCC', '#33CCFF', '#6600CC', '#6600FF', '#6633CC', '#6633FF', '#66CC00', '#66CC33', '#9900CC', '#9900FF', '#9933CC', '#9933FF', '#99CC00', '#99CC33', '#CC0000', '#CC0033', '#CC0066', '#CC0099', '#CC00CC', '#CC00FF', '#CC3300', '#CC3333', '#CC3366', '#CC3399', '#CC33CC', '#CC33FF', '#CC6600', '#CC6633', '#CC9900', '#CC9933', '#CCCC00', '#CCCC33', '#FF0000', '#FF0033', '#FF0066', '#FF0099', '#FF00CC', '#FF00FF', '#FF3300', '#FF3333', '#FF3366', '#FF3399', '#FF33CC', '#FF33FF', '#FF6600', '#FF6633', '#FF9900', '#FF9933', '#FFCC00', '#FFCC33']; -/** - * Currently only WebKit-based Web Inspectors, Firefox >= v31, - * and the Firebug extension (any Firefox version) are known - * to support "%c" CSS customizations. - * - * TODO: add a `localStorage` variable to explicitly enable/disable colors - */ -// eslint-disable-next-line complexity - -function useColors() { - // NB: In an Electron preload script, document will be defined but not fully - // initialized. Since we know we're in Chrome, we'll just detect this case - // explicitly - if (typeof window !== 'undefined' && window.process && (window.process.type === 'renderer' || window.process.__nwjs)) { - return true; - } // Internet Explorer and Edge do not support colors. - - - if (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/)) { - return false; - } // Is webkit? http://stackoverflow.com/a/16459606/376773 - // document is undefined in react-native: https://github.com/facebook/react-native/pull/1632 - - - return typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance || // Is firebug? http://stackoverflow.com/a/398120/376773 - typeof window !== 'undefined' && window.console && (window.console.firebug || window.console.exception && window.console.table) || // Is firefox >= v31? - // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages - typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31 || // Double check webkit in userAgent just in case we are in a worker - typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/); -} -/** - * Colorize log arguments if enabled. - * - * @api public - */ - - -function formatArgs(args) { - args[0] = (this.useColors ? '%c' : '') + this.namespace + (this.useColors ? ' %c' : ' ') + args[0] + (this.useColors ? '%c ' : ' ') + '+' + module.exports.humanize(this.diff); - - if (!this.useColors) { - return; - } - - var c = 'color: ' + this.color; - args.splice(1, 0, c, 'color: inherit'); // The final "%c" is somewhat tricky, because there could be other - // arguments passed either before or after the %c, so we need to - // figure out the correct index to insert the CSS into - - var index = 0; - var lastC = 0; - args[0].replace(/%[a-zA-Z%]/g, function (match) { - if (match === '%%') { - return; - } - - index++; - - if (match === '%c') { - // We only are interested in the *last* %c - // (the user may have provided their own) - lastC = index; - } - }); - args.splice(lastC, 0, c); -} -/** - * Invokes `console.log()` when available. - * No-op when `console.log` is not a "function". - * - * @api public - */ - - -function log() { - var _console; - - // This hackery is required for IE8/9, where - // the `console.log` function doesn't have 'apply' - return (typeof console === "undefined" ? "undefined" : _typeof(console)) === 'object' && console.log && (_console = console).log.apply(_console, arguments); -} -/** - * Save `namespaces`. - * - * @param {String} namespaces - * @api private - */ - - -function save(namespaces) { - try { - if (namespaces) { - exports.storage.setItem('debug', namespaces); - } else { - exports.storage.removeItem('debug'); - } - } catch (error) {// Swallow - // XXX (@Qix-) should we be logging these? - } -} -/** - * Load `namespaces`. - * - * @return {String} returns the previously persisted debug modes - * @api private - */ - - -function load() { - var r; - - try { - r = exports.storage.getItem('debug'); - } catch (error) {} // Swallow - // XXX (@Qix-) should we be logging these? - // If debug isn't set in LS, and we're in Electron, try to load $DEBUG - - - if (!r && typeof process !== 'undefined' && 'env' in process) { - r = process.env.DEBUG; - } - - return r; -} -/** - * Localstorage attempts to return the localstorage. - * - * This is necessary because safari throws - * when a user disables cookies/localstorage - * and you attempt to access it. - * - * @return {LocalStorage} - * @api private - */ - - -function localstorage() { - try { - // TVMLKit (Apple TV JS Runtime) does not have a window object, just localStorage in the global context - // The Browser also has localStorage in the global context. - return localStorage; - } catch (error) {// Swallow - // XXX (@Qix-) should we be logging these? - } -} - -module.exports = require('./common')(exports); -var formatters = module.exports.formatters; -/** - * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. - */ - -formatters.j = function (v) { - try { - return JSON.stringify(v); - } catch (error) { - return '[UnexpectedJSONParseError]: ' + error.message; - } -}; - - -}).call(this,require('_process')) -},{"./common":46,"_process":69}],46:[function(require,module,exports){ -"use strict"; - -/** - * This is the common logic for both the Node.js and web browser - * implementations of `debug()`. - */ -function setup(env) { - createDebug.debug = createDebug; - createDebug.default = createDebug; - createDebug.coerce = coerce; - createDebug.disable = disable; - createDebug.enable = enable; - createDebug.enabled = enabled; - createDebug.humanize = require('ms'); - Object.keys(env).forEach(function (key) { - createDebug[key] = env[key]; - }); - /** - * Active `debug` instances. - */ - - createDebug.instances = []; - /** - * The currently active debug mode names, and names to skip. - */ - - createDebug.names = []; - createDebug.skips = []; - /** - * Map of special "%n" handling functions, for the debug "format" argument. - * - * Valid key names are a single, lower or upper-case letter, i.e. "n" and "N". - */ - - createDebug.formatters = {}; - /** - * Selects a color for a debug namespace - * @param {String} namespace The namespace string for the for the debug instance to be colored - * @return {Number|String} An ANSI color code for the given namespace - * @api private - */ - - function selectColor(namespace) { - var hash = 0; - - for (var i = 0; i < namespace.length; i++) { - hash = (hash << 5) - hash + namespace.charCodeAt(i); - hash |= 0; // Convert to 32bit integer - } - - return createDebug.colors[Math.abs(hash) % createDebug.colors.length]; - } - - createDebug.selectColor = selectColor; - /** - * Create a debugger with the given `namespace`. - * - * @param {String} namespace - * @return {Function} - * @api public - */ - - function createDebug(namespace) { - var prevTime; - - function debug() { - // Disabled? - if (!debug.enabled) { - return; - } - - for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { - args[_key] = arguments[_key]; - } - - var self = debug; // Set `diff` timestamp - - var curr = Number(new Date()); - var ms = curr - (prevTime || curr); - self.diff = ms; - self.prev = prevTime; - self.curr = curr; - prevTime = curr; - args[0] = createDebug.coerce(args[0]); - - if (typeof args[0] !== 'string') { - // Anything else let's inspect with %O - args.unshift('%O'); - } // Apply any `formatters` transformations - - - var index = 0; - args[0] = args[0].replace(/%([a-zA-Z%])/g, function (match, format) { - // If we encounter an escaped % then don't increase the array index - if (match === '%%') { - return match; - } - - index++; - var formatter = createDebug.formatters[format]; - - if (typeof formatter === 'function') { - var val = args[index]; - match = formatter.call(self, val); // Now we need to remove `args[index]` since it's inlined in the `format` - - args.splice(index, 1); - index--; - } - - return match; - }); // Apply env-specific formatting (colors, etc.) - - createDebug.formatArgs.call(self, args); - var logFn = self.log || createDebug.log; - logFn.apply(self, args); - } - - debug.namespace = namespace; - debug.enabled = createDebug.enabled(namespace); - debug.useColors = createDebug.useColors(); - debug.color = selectColor(namespace); - debug.destroy = destroy; - debug.extend = extend; // Debug.formatArgs = formatArgs; - // debug.rawLog = rawLog; - // env-specific initialization logic for debug instances - - if (typeof createDebug.init === 'function') { - createDebug.init(debug); - } - - createDebug.instances.push(debug); - return debug; - } - - function destroy() { - var index = createDebug.instances.indexOf(this); - - if (index !== -1) { - createDebug.instances.splice(index, 1); - return true; - } - - return false; - } - - function extend(namespace, delimiter) { - return createDebug(this.namespace + (typeof delimiter === 'undefined' ? ':' : delimiter) + namespace); - } - /** - * Enables a debug mode by namespaces. This can include modes - * separated by a colon and wildcards. - * - * @param {String} namespaces - * @api public - */ - - - function enable(namespaces) { - createDebug.save(namespaces); - createDebug.names = []; - createDebug.skips = []; - var i; - var split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/); - var len = split.length; - - for (i = 0; i < len; i++) { - if (!split[i]) { - // ignore empty strings - continue; - } - - namespaces = split[i].replace(/\*/g, '.*?'); - - if (namespaces[0] === '-') { - createDebug.skips.push(new RegExp('^' + namespaces.substr(1) + '$')); - } else { - createDebug.names.push(new RegExp('^' + namespaces + '$')); - } - } - - for (i = 0; i < createDebug.instances.length; i++) { - var instance = createDebug.instances[i]; - instance.enabled = createDebug.enabled(instance.namespace); - } - } - /** - * Disable debug output. - * - * @api public - */ - - - function disable() { - createDebug.enable(''); - } - /** - * Returns true if the given mode name is enabled, false otherwise. - * - * @param {String} name - * @return {Boolean} - * @api public - */ - - - function enabled(name) { - if (name[name.length - 1] === '*') { - return true; - } - - var i; - var len; - - for (i = 0, len = createDebug.skips.length; i < len; i++) { - if (createDebug.skips[i].test(name)) { - return false; - } - } - - for (i = 0, len = createDebug.names.length; i < len; i++) { - if (createDebug.names[i].test(name)) { - return true; - } - } - - return false; - } - /** - * Coerce `val`. - * - * @param {Mixed} val - * @return {Mixed} - * @api private - */ - - - function coerce(val) { - if (val instanceof Error) { - return val.stack || val.message; - } - - return val; - } - - createDebug.enable(createDebug.load()); - return createDebug; -} - -module.exports = setup; - - -},{"ms":60}],47:[function(require,module,exports){ -'use strict'; - -var keys = require('object-keys'); -var hasSymbols = typeof Symbol === 'function' && typeof Symbol('foo') === 'symbol'; - -var toStr = Object.prototype.toString; -var concat = Array.prototype.concat; -var origDefineProperty = Object.defineProperty; - -var isFunction = function (fn) { - return typeof fn === 'function' && toStr.call(fn) === '[object Function]'; -}; - -var arePropertyDescriptorsSupported = function () { - var obj = {}; - try { - origDefineProperty(obj, 'x', { enumerable: false, value: obj }); - // eslint-disable-next-line no-unused-vars, no-restricted-syntax - for (var _ in obj) { // jscs:ignore disallowUnusedVariables - return false; - } - return obj.x === obj; - } catch (e) { /* this is IE 8. */ - return false; - } -}; -var supportsDescriptors = origDefineProperty && arePropertyDescriptorsSupported(); - -var defineProperty = function (object, name, value, predicate) { - if (name in object && (!isFunction(predicate) || !predicate())) { - return; - } - if (supportsDescriptors) { - origDefineProperty(object, name, { - configurable: true, - enumerable: false, - value: value, - writable: true - }); - } else { - object[name] = value; - } -}; - -var defineProperties = function (object, map) { - var predicates = arguments.length > 2 ? arguments[2] : {}; - var props = keys(map); - if (hasSymbols) { - props = concat.call(props, Object.getOwnPropertySymbols(map)); - } - for (var i = 0; i < props.length; i += 1) { - defineProperty(object, props[i], map[props[i]], predicates[props[i]]); - } -}; - -defineProperties.supportsDescriptors = !!supportsDescriptors; - -module.exports = defineProperties; - -},{"object-keys":62}],48:[function(require,module,exports){ -/*! - - diff v3.5.0 - -Software License Agreement (BSD License) - -Copyright (c) 2009-2015, Kevin Decker - -All rights reserved. - -Redistribution and use of this software in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above - copyright notice, this list of conditions and the - following disclaimer. - -* Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the - following disclaimer in the documentation and/or other - materials provided with the distribution. - -* Neither the name of Kevin Decker nor the names of its - contributors may be used to endorse or promote products - derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER -IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT -OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -@license -*/ -(function webpackUniversalModuleDefinition(root, factory) { - if(typeof exports === 'object' && typeof module === 'object') - module.exports = factory(); - else if(false) - define([], factory); - else if(typeof exports === 'object') - exports["JsDiff"] = factory(); - else - root["JsDiff"] = factory(); -})(this, function() { -return /******/ (function(modules) { // webpackBootstrap -/******/ // The module cache -/******/ var installedModules = {}; - -/******/ // The require function -/******/ function __webpack_require__(moduleId) { - -/******/ // Check if module is in cache -/******/ if(installedModules[moduleId]) -/******/ return installedModules[moduleId].exports; - -/******/ // Create a new module (and put it into the cache) -/******/ var module = installedModules[moduleId] = { -/******/ exports: {}, -/******/ id: moduleId, -/******/ loaded: false -/******/ }; - -/******/ // Execute the module function -/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); - -/******/ // Flag the module as loaded -/******/ module.loaded = true; - -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } - - -/******/ // expose the modules object (__webpack_modules__) -/******/ __webpack_require__.m = modules; - -/******/ // expose the module cache -/******/ __webpack_require__.c = installedModules; - -/******/ // __webpack_public_path__ -/******/ __webpack_require__.p = ""; - -/******/ // Load entry module and return exports -/******/ return __webpack_require__(0); -/******/ }) -/************************************************************************/ -/******/ ([ -/* 0 */ -/***/ (function(module, exports, __webpack_require__) { - - /*istanbul ignore start*/'use strict'; - - exports.__esModule = true; - exports.canonicalize = exports.convertChangesToXML = exports.convertChangesToDMP = exports.merge = exports.parsePatch = exports.applyPatches = exports.applyPatch = exports.createPatch = exports.createTwoFilesPatch = exports.structuredPatch = exports.diffArrays = exports.diffJson = exports.diffCss = exports.diffSentences = exports.diffTrimmedLines = exports.diffLines = exports.diffWordsWithSpace = exports.diffWords = exports.diffChars = exports.Diff = undefined; - - /*istanbul ignore end*/var /*istanbul ignore start*/_base = __webpack_require__(1) /*istanbul ignore end*/; - - /*istanbul ignore start*/var _base2 = _interopRequireDefault(_base); - - /*istanbul ignore end*/var /*istanbul ignore start*/_character = __webpack_require__(2) /*istanbul ignore end*/; - - var /*istanbul ignore start*/_word = __webpack_require__(3) /*istanbul ignore end*/; - - var /*istanbul ignore start*/_line = __webpack_require__(5) /*istanbul ignore end*/; - - var /*istanbul ignore start*/_sentence = __webpack_require__(6) /*istanbul ignore end*/; - - var /*istanbul ignore start*/_css = __webpack_require__(7) /*istanbul ignore end*/; - - var /*istanbul ignore start*/_json = __webpack_require__(8) /*istanbul ignore end*/; - - var /*istanbul ignore start*/_array = __webpack_require__(9) /*istanbul ignore end*/; - - var /*istanbul ignore start*/_apply = __webpack_require__(10) /*istanbul ignore end*/; - - var /*istanbul ignore start*/_parse = __webpack_require__(11) /*istanbul ignore end*/; - - var /*istanbul ignore start*/_merge = __webpack_require__(13) /*istanbul ignore end*/; - - var /*istanbul ignore start*/_create = __webpack_require__(14) /*istanbul ignore end*/; - - var /*istanbul ignore start*/_dmp = __webpack_require__(16) /*istanbul ignore end*/; - - var /*istanbul ignore start*/_xml = __webpack_require__(17) /*istanbul ignore end*/; - - /*istanbul ignore start*/function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - - /* See LICENSE file for terms of use */ - - /* - * Text diff implementation. - * - * This library supports the following APIS: - * JsDiff.diffChars: Character by character diff - * JsDiff.diffWords: Word (as defined by \b regex) diff which ignores whitespace - * JsDiff.diffLines: Line based diff - * - * JsDiff.diffCss: Diff targeted at CSS content - * - * These methods are based on the implementation proposed in - * "An O(ND) Difference Algorithm and its Variations" (Myers, 1986). - * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.4.6927 - */ - exports. /*istanbul ignore end*/Diff = _base2['default']; - /*istanbul ignore start*/exports. /*istanbul ignore end*/diffChars = _character.diffChars; - /*istanbul ignore start*/exports. /*istanbul ignore end*/diffWords = _word.diffWords; - /*istanbul ignore start*/exports. /*istanbul ignore end*/diffWordsWithSpace = _word.diffWordsWithSpace; - /*istanbul ignore start*/exports. /*istanbul ignore end*/diffLines = _line.diffLines; - /*istanbul ignore start*/exports. /*istanbul ignore end*/diffTrimmedLines = _line.diffTrimmedLines; - /*istanbul ignore start*/exports. /*istanbul ignore end*/diffSentences = _sentence.diffSentences; - /*istanbul ignore start*/exports. /*istanbul ignore end*/diffCss = _css.diffCss; - /*istanbul ignore start*/exports. /*istanbul ignore end*/diffJson = _json.diffJson; - /*istanbul ignore start*/exports. /*istanbul ignore end*/diffArrays = _array.diffArrays; - /*istanbul ignore start*/exports. /*istanbul ignore end*/structuredPatch = _create.structuredPatch; - /*istanbul ignore start*/exports. /*istanbul ignore end*/createTwoFilesPatch = _create.createTwoFilesPatch; - /*istanbul ignore start*/exports. /*istanbul ignore end*/createPatch = _create.createPatch; - /*istanbul ignore start*/exports. /*istanbul ignore end*/applyPatch = _apply.applyPatch; - /*istanbul ignore start*/exports. /*istanbul ignore end*/applyPatches = _apply.applyPatches; - /*istanbul ignore start*/exports. /*istanbul ignore end*/parsePatch = _parse.parsePatch; - /*istanbul ignore start*/exports. /*istanbul ignore end*/merge = _merge.merge; - /*istanbul ignore start*/exports. /*istanbul ignore end*/convertChangesToDMP = _dmp.convertChangesToDMP; - /*istanbul ignore start*/exports. /*istanbul ignore end*/convertChangesToXML = _xml.convertChangesToXML; - /*istanbul ignore start*/exports. /*istanbul ignore end*/canonicalize = _json.canonicalize; - - - -/***/ }), -/* 1 */ -/***/ (function(module, exports) { - - /*istanbul ignore start*/'use strict'; - - exports.__esModule = true; - exports['default'] = /*istanbul ignore end*/Diff; - function Diff() {} - - Diff.prototype = { - /*istanbul ignore start*/ /*istanbul ignore end*/diff: function diff(oldString, newString) { - /*istanbul ignore start*/var /*istanbul ignore end*/options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; - - var callback = options.callback; - if (typeof options === 'function') { - callback = options; - options = {}; - } - this.options = options; - - var self = this; - - function done(value) { - if (callback) { - setTimeout(function () { - callback(undefined, value); - }, 0); - return true; - } else { - return value; - } - } - - // Allow subclasses to massage the input prior to running - oldString = this.castInput(oldString); - newString = this.castInput(newString); - - oldString = this.removeEmpty(this.tokenize(oldString)); - newString = this.removeEmpty(this.tokenize(newString)); - - var newLen = newString.length, - oldLen = oldString.length; - var editLength = 1; - var maxEditLength = newLen + oldLen; - var bestPath = [{ newPos: -1, components: [] }]; - - // Seed editLength = 0, i.e. the content starts with the same values - var oldPos = this.extractCommon(bestPath[0], newString, oldString, 0); - if (bestPath[0].newPos + 1 >= newLen && oldPos + 1 >= oldLen) { - // Identity per the equality and tokenizer - return done([{ value: this.join(newString), count: newString.length }]); - } - - // Main worker method. checks all permutations of a given edit length for acceptance. - function execEditLength() { - for (var diagonalPath = -1 * editLength; diagonalPath <= editLength; diagonalPath += 2) { - var basePath = /*istanbul ignore start*/void 0 /*istanbul ignore end*/; - var addPath = bestPath[diagonalPath - 1], - removePath = bestPath[diagonalPath + 1], - _oldPos = (removePath ? removePath.newPos : 0) - diagonalPath; - if (addPath) { - // No one else is going to attempt to use this value, clear it - bestPath[diagonalPath - 1] = undefined; - } - - var canAdd = addPath && addPath.newPos + 1 < newLen, - canRemove = removePath && 0 <= _oldPos && _oldPos < oldLen; - if (!canAdd && !canRemove) { - // If this path is a terminal then prune - bestPath[diagonalPath] = undefined; - continue; - } - - // Select the diagonal that we want to branch from. We select the prior - // path whose position in the new string is the farthest from the origin - // and does not pass the bounds of the diff graph - if (!canAdd || canRemove && addPath.newPos < removePath.newPos) { - basePath = clonePath(removePath); - self.pushComponent(basePath.components, undefined, true); - } else { - basePath = addPath; // No need to clone, we've pulled it from the list - basePath.newPos++; - self.pushComponent(basePath.components, true, undefined); - } - - _oldPos = self.extractCommon(basePath, newString, oldString, diagonalPath); - - // If we have hit the end of both strings, then we are done - if (basePath.newPos + 1 >= newLen && _oldPos + 1 >= oldLen) { - return done(buildValues(self, basePath.components, newString, oldString, self.useLongestToken)); - } else { - // Otherwise track this path as a potential candidate and continue. - bestPath[diagonalPath] = basePath; - } - } - - editLength++; - } - - // Performs the length of edit iteration. Is a bit fugly as this has to support the - // sync and async mode which is never fun. Loops over execEditLength until a value - // is produced. - if (callback) { - (function exec() { - setTimeout(function () { - // This should not happen, but we want to be safe. - /* istanbul ignore next */ - if (editLength > maxEditLength) { - return callback(); - } - - if (!execEditLength()) { - exec(); - } - }, 0); - })(); - } else { - while (editLength <= maxEditLength) { - var ret = execEditLength(); - if (ret) { - return ret; - } - } - } - }, - /*istanbul ignore start*/ /*istanbul ignore end*/pushComponent: function pushComponent(components, added, removed) { - var last = components[components.length - 1]; - if (last && last.added === added && last.removed === removed) { - // We need to clone here as the component clone operation is just - // as shallow array clone - components[components.length - 1] = { count: last.count + 1, added: added, removed: removed }; - } else { - components.push({ count: 1, added: added, removed: removed }); - } - }, - /*istanbul ignore start*/ /*istanbul ignore end*/extractCommon: function extractCommon(basePath, newString, oldString, diagonalPath) { - var newLen = newString.length, - oldLen = oldString.length, - newPos = basePath.newPos, - oldPos = newPos - diagonalPath, - commonCount = 0; - while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(newString[newPos + 1], oldString[oldPos + 1])) { - newPos++; - oldPos++; - commonCount++; - } - - if (commonCount) { - basePath.components.push({ count: commonCount }); - } - - basePath.newPos = newPos; - return oldPos; - }, - /*istanbul ignore start*/ /*istanbul ignore end*/equals: function equals(left, right) { - if (this.options.comparator) { - return this.options.comparator(left, right); - } else { - return left === right || this.options.ignoreCase && left.toLowerCase() === right.toLowerCase(); - } - }, - /*istanbul ignore start*/ /*istanbul ignore end*/removeEmpty: function removeEmpty(array) { - var ret = []; - for (var i = 0; i < array.length; i++) { - if (array[i]) { - ret.push(array[i]); - } - } - return ret; - }, - /*istanbul ignore start*/ /*istanbul ignore end*/castInput: function castInput(value) { - return value; - }, - /*istanbul ignore start*/ /*istanbul ignore end*/tokenize: function tokenize(value) { - return value.split(''); - }, - /*istanbul ignore start*/ /*istanbul ignore end*/join: function join(chars) { - return chars.join(''); - } - }; - - function buildValues(diff, components, newString, oldString, useLongestToken) { - var componentPos = 0, - componentLen = components.length, - newPos = 0, - oldPos = 0; - - for (; componentPos < componentLen; componentPos++) { - var component = components[componentPos]; - if (!component.removed) { - if (!component.added && useLongestToken) { - var value = newString.slice(newPos, newPos + component.count); - value = value.map(function (value, i) { - var oldValue = oldString[oldPos + i]; - return oldValue.length > value.length ? oldValue : value; - }); - - component.value = diff.join(value); - } else { - component.value = diff.join(newString.slice(newPos, newPos + component.count)); - } - newPos += component.count; - - // Common case - if (!component.added) { - oldPos += component.count; - } - } else { - component.value = diff.join(oldString.slice(oldPos, oldPos + component.count)); - oldPos += component.count; - - // Reverse add and remove so removes are output first to match common convention - // The diffing algorithm is tied to add then remove output and this is the simplest - // route to get the desired output with minimal overhead. - if (componentPos && components[componentPos - 1].added) { - var tmp = components[componentPos - 1]; - components[componentPos - 1] = components[componentPos]; - components[componentPos] = tmp; - } - } - } - - // Special case handle for when one terminal is ignored (i.e. whitespace). - // For this case we merge the terminal into the prior string and drop the change. - // This is only available for string mode. - var lastComponent = components[componentLen - 1]; - if (componentLen > 1 && typeof lastComponent.value === 'string' && (lastComponent.added || lastComponent.removed) && diff.equals('', lastComponent.value)) { - components[componentLen - 2].value += lastComponent.value; - components.pop(); - } - - return components; - } - - function clonePath(path) { - return { newPos: path.newPos, components: path.components.slice(0) }; - } - - - -/***/ }), -/* 2 */ -/***/ (function(module, exports, __webpack_require__) { - - /*istanbul ignore start*/'use strict'; - - exports.__esModule = true; - exports.characterDiff = undefined; - exports. /*istanbul ignore end*/diffChars = diffChars; - - var /*istanbul ignore start*/_base = __webpack_require__(1) /*istanbul ignore end*/; - - /*istanbul ignore start*/var _base2 = _interopRequireDefault(_base); - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - - /*istanbul ignore end*/var characterDiff = /*istanbul ignore start*/exports. /*istanbul ignore end*/characterDiff = new /*istanbul ignore start*/_base2['default'] /*istanbul ignore end*/(); - function diffChars(oldStr, newStr, options) { - return characterDiff.diff(oldStr, newStr, options); - } - - - -/***/ }), -/* 3 */ -/***/ (function(module, exports, __webpack_require__) { - - /*istanbul ignore start*/'use strict'; - - exports.__esModule = true; - exports.wordDiff = undefined; - exports. /*istanbul ignore end*/diffWords = diffWords; - /*istanbul ignore start*/exports. /*istanbul ignore end*/diffWordsWithSpace = diffWordsWithSpace; - - var /*istanbul ignore start*/_base = __webpack_require__(1) /*istanbul ignore end*/; - - /*istanbul ignore start*/var _base2 = _interopRequireDefault(_base); - - /*istanbul ignore end*/var /*istanbul ignore start*/_params = __webpack_require__(4) /*istanbul ignore end*/; - - /*istanbul ignore start*/function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - - /*istanbul ignore end*/ // Based on https://en.wikipedia.org/wiki/Latin_script_in_Unicode - // - // Ranges and exceptions: - // Latin-1 Supplement, 0080–00FF - // - U+00D7 × Multiplication sign - // - U+00F7 ÷ Division sign - // Latin Extended-A, 0100–017F - // Latin Extended-B, 0180–024F - // IPA Extensions, 0250–02AF - // Spacing Modifier Letters, 02B0–02FF - // - U+02C7 ˇ ˇ Caron - // - U+02D8 ˘ ˘ Breve - // - U+02D9 ˙ ˙ Dot Above - // - U+02DA ˚ ˚ Ring Above - // - U+02DB ˛ ˛ Ogonek - // - U+02DC ˜ ˜ Small Tilde - // - U+02DD ˝ ˝ Double Acute Accent - // Latin Extended Additional, 1E00–1EFF - var extendedWordChars = /^[A-Za-z\xC0-\u02C6\u02C8-\u02D7\u02DE-\u02FF\u1E00-\u1EFF]+$/; - - var reWhitespace = /\S/; - - var wordDiff = /*istanbul ignore start*/exports. /*istanbul ignore end*/wordDiff = new /*istanbul ignore start*/_base2['default'] /*istanbul ignore end*/(); - wordDiff.equals = function (left, right) { - if (this.options.ignoreCase) { - left = left.toLowerCase(); - right = right.toLowerCase(); - } - return left === right || this.options.ignoreWhitespace && !reWhitespace.test(left) && !reWhitespace.test(right); - }; - wordDiff.tokenize = function (value) { - var tokens = value.split(/(\s+|\b)/); - - // Join the boundary splits that we do not consider to be boundaries. This is primarily the extended Latin character set. - for (var i = 0; i < tokens.length - 1; i++) { - // If we have an empty string in the next field and we have only word chars before and after, merge - if (!tokens[i + 1] && tokens[i + 2] && extendedWordChars.test(tokens[i]) && extendedWordChars.test(tokens[i + 2])) { - tokens[i] += tokens[i + 2]; - tokens.splice(i + 1, 2); - i--; - } - } - - return tokens; - }; - - function diffWords(oldStr, newStr, options) { - options = /*istanbul ignore start*/(0, _params.generateOptions) /*istanbul ignore end*/(options, { ignoreWhitespace: true }); - return wordDiff.diff(oldStr, newStr, options); - } - - function diffWordsWithSpace(oldStr, newStr, options) { - return wordDiff.diff(oldStr, newStr, options); - } - - - -/***/ }), -/* 4 */ -/***/ (function(module, exports) { - - /*istanbul ignore start*/'use strict'; - - exports.__esModule = true; - exports. /*istanbul ignore end*/generateOptions = generateOptions; - function generateOptions(options, defaults) { - if (typeof options === 'function') { - defaults.callback = options; - } else if (options) { - for (var name in options) { - /* istanbul ignore else */ - if (options.hasOwnProperty(name)) { - defaults[name] = options[name]; - } - } - } - return defaults; - } - - - -/***/ }), -/* 5 */ -/***/ (function(module, exports, __webpack_require__) { - - /*istanbul ignore start*/'use strict'; - - exports.__esModule = true; - exports.lineDiff = undefined; - exports. /*istanbul ignore end*/diffLines = diffLines; - /*istanbul ignore start*/exports. /*istanbul ignore end*/diffTrimmedLines = diffTrimmedLines; - - var /*istanbul ignore start*/_base = __webpack_require__(1) /*istanbul ignore end*/; - - /*istanbul ignore start*/var _base2 = _interopRequireDefault(_base); - - /*istanbul ignore end*/var /*istanbul ignore start*/_params = __webpack_require__(4) /*istanbul ignore end*/; - - /*istanbul ignore start*/function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - - /*istanbul ignore end*/var lineDiff = /*istanbul ignore start*/exports. /*istanbul ignore end*/lineDiff = new /*istanbul ignore start*/_base2['default'] /*istanbul ignore end*/(); - lineDiff.tokenize = function (value) { - var retLines = [], - linesAndNewlines = value.split(/(\n|\r\n)/); - - // Ignore the final empty token that occurs if the string ends with a new line - if (!linesAndNewlines[linesAndNewlines.length - 1]) { - linesAndNewlines.pop(); - } - - // Merge the content and line separators into single tokens - for (var i = 0; i < linesAndNewlines.length; i++) { - var line = linesAndNewlines[i]; - - if (i % 2 && !this.options.newlineIsToken) { - retLines[retLines.length - 1] += line; - } else { - if (this.options.ignoreWhitespace) { - line = line.trim(); - } - retLines.push(line); - } - } - - return retLines; - }; - - function diffLines(oldStr, newStr, callback) { - return lineDiff.diff(oldStr, newStr, callback); - } - function diffTrimmedLines(oldStr, newStr, callback) { - var options = /*istanbul ignore start*/(0, _params.generateOptions) /*istanbul ignore end*/(callback, { ignoreWhitespace: true }); - return lineDiff.diff(oldStr, newStr, options); - } - - - -/***/ }), -/* 6 */ -/***/ (function(module, exports, __webpack_require__) { - - /*istanbul ignore start*/'use strict'; - - exports.__esModule = true; - exports.sentenceDiff = undefined; - exports. /*istanbul ignore end*/diffSentences = diffSentences; - - var /*istanbul ignore start*/_base = __webpack_require__(1) /*istanbul ignore end*/; - - /*istanbul ignore start*/var _base2 = _interopRequireDefault(_base); - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - - /*istanbul ignore end*/var sentenceDiff = /*istanbul ignore start*/exports. /*istanbul ignore end*/sentenceDiff = new /*istanbul ignore start*/_base2['default'] /*istanbul ignore end*/(); - sentenceDiff.tokenize = function (value) { - return value.split(/(\S.+?[.!?])(?=\s+|$)/); - }; - - function diffSentences(oldStr, newStr, callback) { - return sentenceDiff.diff(oldStr, newStr, callback); - } - - - -/***/ }), -/* 7 */ -/***/ (function(module, exports, __webpack_require__) { - - /*istanbul ignore start*/'use strict'; - - exports.__esModule = true; - exports.cssDiff = undefined; - exports. /*istanbul ignore end*/diffCss = diffCss; - - var /*istanbul ignore start*/_base = __webpack_require__(1) /*istanbul ignore end*/; - - /*istanbul ignore start*/var _base2 = _interopRequireDefault(_base); - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - - /*istanbul ignore end*/var cssDiff = /*istanbul ignore start*/exports. /*istanbul ignore end*/cssDiff = new /*istanbul ignore start*/_base2['default'] /*istanbul ignore end*/(); - cssDiff.tokenize = function (value) { - return value.split(/([{}:;,]|\s+)/); - }; - - function diffCss(oldStr, newStr, callback) { - return cssDiff.diff(oldStr, newStr, callback); - } - - - -/***/ }), -/* 8 */ -/***/ (function(module, exports, __webpack_require__) { - - /*istanbul ignore start*/'use strict'; - - exports.__esModule = true; - exports.jsonDiff = undefined; - - var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - - exports. /*istanbul ignore end*/diffJson = diffJson; - /*istanbul ignore start*/exports. /*istanbul ignore end*/canonicalize = canonicalize; - - var /*istanbul ignore start*/_base = __webpack_require__(1) /*istanbul ignore end*/; - - /*istanbul ignore start*/var _base2 = _interopRequireDefault(_base); - - /*istanbul ignore end*/var /*istanbul ignore start*/_line = __webpack_require__(5) /*istanbul ignore end*/; - - /*istanbul ignore start*/function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - - /*istanbul ignore end*/var objectPrototypeToString = Object.prototype.toString; - - var jsonDiff = /*istanbul ignore start*/exports. /*istanbul ignore end*/jsonDiff = new /*istanbul ignore start*/_base2['default'] /*istanbul ignore end*/(); - // Discriminate between two lines of pretty-printed, serialized JSON where one of them has a - // dangling comma and the other doesn't. Turns out including the dangling comma yields the nicest output: - jsonDiff.useLongestToken = true; - - jsonDiff.tokenize = /*istanbul ignore start*/_line.lineDiff /*istanbul ignore end*/.tokenize; - jsonDiff.castInput = function (value) { - /*istanbul ignore start*/var _options = /*istanbul ignore end*/this.options, - undefinedReplacement = _options.undefinedReplacement, - _options$stringifyRep = _options.stringifyReplacer, - stringifyReplacer = _options$stringifyRep === undefined ? function (k, v) /*istanbul ignore start*/{ - return (/*istanbul ignore end*/typeof v === 'undefined' ? undefinedReplacement : v - ); - } : _options$stringifyRep; - - - return typeof value === 'string' ? value : JSON.stringify(canonicalize(value, null, null, stringifyReplacer), stringifyReplacer, ' '); - }; - jsonDiff.equals = function (left, right) { - return (/*istanbul ignore start*/_base2['default'] /*istanbul ignore end*/.prototype.equals.call(jsonDiff, left.replace(/,([\r\n])/g, '$1'), right.replace(/,([\r\n])/g, '$1')) - ); - }; - - function diffJson(oldObj, newObj, options) { - return jsonDiff.diff(oldObj, newObj, options); - } - - // This function handles the presence of circular references by bailing out when encountering an - // object that is already on the "stack" of items being processed. Accepts an optional replacer - function canonicalize(obj, stack, replacementStack, replacer, key) { - stack = stack || []; - replacementStack = replacementStack || []; - - if (replacer) { - obj = replacer(key, obj); - } - - var i = /*istanbul ignore start*/void 0 /*istanbul ignore end*/; - - for (i = 0; i < stack.length; i += 1) { - if (stack[i] === obj) { - return replacementStack[i]; - } - } - - var canonicalizedObj = /*istanbul ignore start*/void 0 /*istanbul ignore end*/; - - if ('[object Array]' === objectPrototypeToString.call(obj)) { - stack.push(obj); - canonicalizedObj = new Array(obj.length); - replacementStack.push(canonicalizedObj); - for (i = 0; i < obj.length; i += 1) { - canonicalizedObj[i] = canonicalize(obj[i], stack, replacementStack, replacer, key); - } - stack.pop(); - replacementStack.pop(); - return canonicalizedObj; - } - - if (obj && obj.toJSON) { - obj = obj.toJSON(); - } - - if ( /*istanbul ignore start*/(typeof /*istanbul ignore end*/obj === 'undefined' ? 'undefined' : _typeof(obj)) === 'object' && obj !== null) { - stack.push(obj); - canonicalizedObj = {}; - replacementStack.push(canonicalizedObj); - var sortedKeys = [], - _key = /*istanbul ignore start*/void 0 /*istanbul ignore end*/; - for (_key in obj) { - /* istanbul ignore else */ - if (obj.hasOwnProperty(_key)) { - sortedKeys.push(_key); - } - } - sortedKeys.sort(); - for (i = 0; i < sortedKeys.length; i += 1) { - _key = sortedKeys[i]; - canonicalizedObj[_key] = canonicalize(obj[_key], stack, replacementStack, replacer, _key); - } - stack.pop(); - replacementStack.pop(); - } else { - canonicalizedObj = obj; - } - return canonicalizedObj; - } - - - -/***/ }), -/* 9 */ -/***/ (function(module, exports, __webpack_require__) { - - /*istanbul ignore start*/'use strict'; - - exports.__esModule = true; - exports.arrayDiff = undefined; - exports. /*istanbul ignore end*/diffArrays = diffArrays; - - var /*istanbul ignore start*/_base = __webpack_require__(1) /*istanbul ignore end*/; - - /*istanbul ignore start*/var _base2 = _interopRequireDefault(_base); - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - - /*istanbul ignore end*/var arrayDiff = /*istanbul ignore start*/exports. /*istanbul ignore end*/arrayDiff = new /*istanbul ignore start*/_base2['default'] /*istanbul ignore end*/(); - arrayDiff.tokenize = function (value) { - return value.slice(); - }; - arrayDiff.join = arrayDiff.removeEmpty = function (value) { - return value; - }; - - function diffArrays(oldArr, newArr, callback) { - return arrayDiff.diff(oldArr, newArr, callback); - } - - - -/***/ }), -/* 10 */ -/***/ (function(module, exports, __webpack_require__) { - - /*istanbul ignore start*/'use strict'; - - exports.__esModule = true; - exports. /*istanbul ignore end*/applyPatch = applyPatch; - /*istanbul ignore start*/exports. /*istanbul ignore end*/applyPatches = applyPatches; - - var /*istanbul ignore start*/_parse = __webpack_require__(11) /*istanbul ignore end*/; - - var /*istanbul ignore start*/_distanceIterator = __webpack_require__(12) /*istanbul ignore end*/; - - /*istanbul ignore start*/var _distanceIterator2 = _interopRequireDefault(_distanceIterator); - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - - /*istanbul ignore end*/function applyPatch(source, uniDiff) { - /*istanbul ignore start*/var /*istanbul ignore end*/options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; - - if (typeof uniDiff === 'string') { - uniDiff = /*istanbul ignore start*/(0, _parse.parsePatch) /*istanbul ignore end*/(uniDiff); - } - - if (Array.isArray(uniDiff)) { - if (uniDiff.length > 1) { - throw new Error('applyPatch only works with a single input.'); - } - - uniDiff = uniDiff[0]; - } - - // Apply the diff to the input - var lines = source.split(/\r\n|[\n\v\f\r\x85]/), - delimiters = source.match(/\r\n|[\n\v\f\r\x85]/g) || [], - hunks = uniDiff.hunks, - compareLine = options.compareLine || function (lineNumber, line, operation, patchContent) /*istanbul ignore start*/{ - return (/*istanbul ignore end*/line === patchContent - ); - }, - errorCount = 0, - fuzzFactor = options.fuzzFactor || 0, - minLine = 0, - offset = 0, - removeEOFNL = /*istanbul ignore start*/void 0 /*istanbul ignore end*/, - addEOFNL = /*istanbul ignore start*/void 0 /*istanbul ignore end*/; - - /** - * Checks if the hunk exactly fits on the provided location - */ - function hunkFits(hunk, toPos) { - for (var j = 0; j < hunk.lines.length; j++) { - var line = hunk.lines[j], - operation = line.length > 0 ? line[0] : ' ', - content = line.length > 0 ? line.substr(1) : line; - - if (operation === ' ' || operation === '-') { - // Context sanity check - if (!compareLine(toPos + 1, lines[toPos], operation, content)) { - errorCount++; - - if (errorCount > fuzzFactor) { - return false; - } - } - toPos++; - } - } - - return true; - } - - // Search best fit offsets for each hunk based on the previous ones - for (var i = 0; i < hunks.length; i++) { - var hunk = hunks[i], - maxLine = lines.length - hunk.oldLines, - localOffset = 0, - toPos = offset + hunk.oldStart - 1; - - var iterator = /*istanbul ignore start*/(0, _distanceIterator2['default']) /*istanbul ignore end*/(toPos, minLine, maxLine); - - for (; localOffset !== undefined; localOffset = iterator()) { - if (hunkFits(hunk, toPos + localOffset)) { - hunk.offset = offset += localOffset; - break; - } - } - - if (localOffset === undefined) { - return false; - } - - // Set lower text limit to end of the current hunk, so next ones don't try - // to fit over already patched text - minLine = hunk.offset + hunk.oldStart + hunk.oldLines; - } - - // Apply patch hunks - var diffOffset = 0; - for (var _i = 0; _i < hunks.length; _i++) { - var _hunk = hunks[_i], - _toPos = _hunk.oldStart + _hunk.offset + diffOffset - 1; - diffOffset += _hunk.newLines - _hunk.oldLines; - - if (_toPos < 0) { - // Creating a new file - _toPos = 0; - } - - for (var j = 0; j < _hunk.lines.length; j++) { - var line = _hunk.lines[j], - operation = line.length > 0 ? line[0] : ' ', - content = line.length > 0 ? line.substr(1) : line, - delimiter = _hunk.linedelimiters[j]; - - if (operation === ' ') { - _toPos++; - } else if (operation === '-') { - lines.splice(_toPos, 1); - delimiters.splice(_toPos, 1); - /* istanbul ignore else */ - } else if (operation === '+') { - lines.splice(_toPos, 0, content); - delimiters.splice(_toPos, 0, delimiter); - _toPos++; - } else if (operation === '\\') { - var previousOperation = _hunk.lines[j - 1] ? _hunk.lines[j - 1][0] : null; - if (previousOperation === '+') { - removeEOFNL = true; - } else if (previousOperation === '-') { - addEOFNL = true; - } - } - } - } - - // Handle EOFNL insertion/removal - if (removeEOFNL) { - while (!lines[lines.length - 1]) { - lines.pop(); - delimiters.pop(); - } - } else if (addEOFNL) { - lines.push(''); - delimiters.push('\n'); - } - for (var _k = 0; _k < lines.length - 1; _k++) { - lines[_k] = lines[_k] + delimiters[_k]; - } - return lines.join(''); - } - - // Wrapper that supports multiple file patches via callbacks. - function applyPatches(uniDiff, options) { - if (typeof uniDiff === 'string') { - uniDiff = /*istanbul ignore start*/(0, _parse.parsePatch) /*istanbul ignore end*/(uniDiff); - } - - var currentIndex = 0; - function processIndex() { - var index = uniDiff[currentIndex++]; - if (!index) { - return options.complete(); - } - - options.loadFile(index, function (err, data) { - if (err) { - return options.complete(err); - } - - var updatedContent = applyPatch(data, index, options); - options.patched(index, updatedContent, function (err) { - if (err) { - return options.complete(err); - } - - processIndex(); - }); - }); - } - processIndex(); - } - - - -/***/ }), -/* 11 */ -/***/ (function(module, exports) { - - /*istanbul ignore start*/'use strict'; - - exports.__esModule = true; - exports. /*istanbul ignore end*/parsePatch = parsePatch; - function parsePatch(uniDiff) { - /*istanbul ignore start*/var /*istanbul ignore end*/options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - - var diffstr = uniDiff.split(/\r\n|[\n\v\f\r\x85]/), - delimiters = uniDiff.match(/\r\n|[\n\v\f\r\x85]/g) || [], - list = [], - i = 0; - - function parseIndex() { - var index = {}; - list.push(index); - - // Parse diff metadata - while (i < diffstr.length) { - var line = diffstr[i]; - - // File header found, end parsing diff metadata - if (/^(\-\-\-|\+\+\+|@@)\s/.test(line)) { - break; - } - - // Diff index - var header = /^(?:Index:|diff(?: -r \w+)+)\s+(.+?)\s*$/.exec(line); - if (header) { - index.index = header[1]; - } - - i++; - } - - // Parse file headers if they are defined. Unified diff requires them, but - // there's no technical issues to have an isolated hunk without file header - parseFileHeader(index); - parseFileHeader(index); - - // Parse hunks - index.hunks = []; - - while (i < diffstr.length) { - var _line = diffstr[i]; - - if (/^(Index:|diff|\-\-\-|\+\+\+)\s/.test(_line)) { - break; - } else if (/^@@/.test(_line)) { - index.hunks.push(parseHunk()); - } else if (_line && options.strict) { - // Ignore unexpected content unless in strict mode - throw new Error('Unknown line ' + (i + 1) + ' ' + JSON.stringify(_line)); - } else { - i++; - } - } - } - - // Parses the --- and +++ headers, if none are found, no lines - // are consumed. - function parseFileHeader(index) { - var fileHeader = /^(---|\+\+\+)\s+(.*)$/.exec(diffstr[i]); - if (fileHeader) { - var keyPrefix = fileHeader[1] === '---' ? 'old' : 'new'; - var data = fileHeader[2].split('\t', 2); - var fileName = data[0].replace(/\\\\/g, '\\'); - if (/^".*"$/.test(fileName)) { - fileName = fileName.substr(1, fileName.length - 2); - } - index[keyPrefix + 'FileName'] = fileName; - index[keyPrefix + 'Header'] = (data[1] || '').trim(); - - i++; - } - } - - // Parses a hunk - // This assumes that we are at the start of a hunk. - function parseHunk() { - var chunkHeaderIndex = i, - chunkHeaderLine = diffstr[i++], - chunkHeader = chunkHeaderLine.split(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/); - - var hunk = { - oldStart: +chunkHeader[1], - oldLines: +chunkHeader[2] || 1, - newStart: +chunkHeader[3], - newLines: +chunkHeader[4] || 1, - lines: [], - linedelimiters: [] - }; - - var addCount = 0, - removeCount = 0; - for (; i < diffstr.length; i++) { - // Lines starting with '---' could be mistaken for the "remove line" operation - // But they could be the header for the next file. Therefore prune such cases out. - if (diffstr[i].indexOf('--- ') === 0 && i + 2 < diffstr.length && diffstr[i + 1].indexOf('+++ ') === 0 && diffstr[i + 2].indexOf('@@') === 0) { - break; - } - var operation = diffstr[i].length == 0 && i != diffstr.length - 1 ? ' ' : diffstr[i][0]; - - if (operation === '+' || operation === '-' || operation === ' ' || operation === '\\') { - hunk.lines.push(diffstr[i]); - hunk.linedelimiters.push(delimiters[i] || '\n'); - - if (operation === '+') { - addCount++; - } else if (operation === '-') { - removeCount++; - } else if (operation === ' ') { - addCount++; - removeCount++; - } - } else { - break; - } - } - - // Handle the empty block count case - if (!addCount && hunk.newLines === 1) { - hunk.newLines = 0; - } - if (!removeCount && hunk.oldLines === 1) { - hunk.oldLines = 0; - } - - // Perform optional sanity checking - if (options.strict) { - if (addCount !== hunk.newLines) { - throw new Error('Added line count did not match for hunk at line ' + (chunkHeaderIndex + 1)); - } - if (removeCount !== hunk.oldLines) { - throw new Error('Removed line count did not match for hunk at line ' + (chunkHeaderIndex + 1)); - } - } - - return hunk; - } - - while (i < diffstr.length) { - parseIndex(); - } - - return list; - } - - - -/***/ }), -/* 12 */ -/***/ (function(module, exports) { - - /*istanbul ignore start*/"use strict"; - - exports.__esModule = true; - - exports["default"] = /*istanbul ignore end*/function (start, minLine, maxLine) { - var wantForward = true, - backwardExhausted = false, - forwardExhausted = false, - localOffset = 1; - - return function iterator() { - if (wantForward && !forwardExhausted) { - if (backwardExhausted) { - localOffset++; - } else { - wantForward = false; - } - - // Check if trying to fit beyond text length, and if not, check it fits - // after offset location (or desired location on first iteration) - if (start + localOffset <= maxLine) { - return localOffset; - } - - forwardExhausted = true; - } - - if (!backwardExhausted) { - if (!forwardExhausted) { - wantForward = true; - } - - // Check if trying to fit before text beginning, and if not, check it fits - // before offset location - if (minLine <= start - localOffset) { - return -localOffset++; - } - - backwardExhausted = true; - return iterator(); - } - - // We tried to fit hunk before text beginning and beyond text length, then - // hunk can't fit on the text. Return undefined - }; - }; - - - -/***/ }), -/* 13 */ -/***/ (function(module, exports, __webpack_require__) { - - /*istanbul ignore start*/'use strict'; - - exports.__esModule = true; - exports. /*istanbul ignore end*/calcLineCount = calcLineCount; - /*istanbul ignore start*/exports. /*istanbul ignore end*/merge = merge; - - var /*istanbul ignore start*/_create = __webpack_require__(14) /*istanbul ignore end*/; - - var /*istanbul ignore start*/_parse = __webpack_require__(11) /*istanbul ignore end*/; - - var /*istanbul ignore start*/_array = __webpack_require__(15) /*istanbul ignore end*/; - - /*istanbul ignore start*/function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } - - /*istanbul ignore end*/function calcLineCount(hunk) { - /*istanbul ignore start*/var _calcOldNewLineCount = /*istanbul ignore end*/calcOldNewLineCount(hunk.lines), - oldLines = _calcOldNewLineCount.oldLines, - newLines = _calcOldNewLineCount.newLines; - - if (oldLines !== undefined) { - hunk.oldLines = oldLines; - } else { - delete hunk.oldLines; - } - - if (newLines !== undefined) { - hunk.newLines = newLines; - } else { - delete hunk.newLines; - } - } - - function merge(mine, theirs, base) { - mine = loadPatch(mine, base); - theirs = loadPatch(theirs, base); - - var ret = {}; - - // For index we just let it pass through as it doesn't have any necessary meaning. - // Leaving sanity checks on this to the API consumer that may know more about the - // meaning in their own context. - if (mine.index || theirs.index) { - ret.index = mine.index || theirs.index; - } - - if (mine.newFileName || theirs.newFileName) { - if (!fileNameChanged(mine)) { - // No header or no change in ours, use theirs (and ours if theirs does not exist) - ret.oldFileName = theirs.oldFileName || mine.oldFileName; - ret.newFileName = theirs.newFileName || mine.newFileName; - ret.oldHeader = theirs.oldHeader || mine.oldHeader; - ret.newHeader = theirs.newHeader || mine.newHeader; - } else if (!fileNameChanged(theirs)) { - // No header or no change in theirs, use ours - ret.oldFileName = mine.oldFileName; - ret.newFileName = mine.newFileName; - ret.oldHeader = mine.oldHeader; - ret.newHeader = mine.newHeader; - } else { - // Both changed... figure it out - ret.oldFileName = selectField(ret, mine.oldFileName, theirs.oldFileName); - ret.newFileName = selectField(ret, mine.newFileName, theirs.newFileName); - ret.oldHeader = selectField(ret, mine.oldHeader, theirs.oldHeader); - ret.newHeader = selectField(ret, mine.newHeader, theirs.newHeader); - } - } - - ret.hunks = []; - - var mineIndex = 0, - theirsIndex = 0, - mineOffset = 0, - theirsOffset = 0; - - while (mineIndex < mine.hunks.length || theirsIndex < theirs.hunks.length) { - var mineCurrent = mine.hunks[mineIndex] || { oldStart: Infinity }, - theirsCurrent = theirs.hunks[theirsIndex] || { oldStart: Infinity }; - - if (hunkBefore(mineCurrent, theirsCurrent)) { - // This patch does not overlap with any of the others, yay. - ret.hunks.push(cloneHunk(mineCurrent, mineOffset)); - mineIndex++; - theirsOffset += mineCurrent.newLines - mineCurrent.oldLines; - } else if (hunkBefore(theirsCurrent, mineCurrent)) { - // This patch does not overlap with any of the others, yay. - ret.hunks.push(cloneHunk(theirsCurrent, theirsOffset)); - theirsIndex++; - mineOffset += theirsCurrent.newLines - theirsCurrent.oldLines; - } else { - // Overlap, merge as best we can - var mergedHunk = { - oldStart: Math.min(mineCurrent.oldStart, theirsCurrent.oldStart), - oldLines: 0, - newStart: Math.min(mineCurrent.newStart + mineOffset, theirsCurrent.oldStart + theirsOffset), - newLines: 0, - lines: [] - }; - mergeLines(mergedHunk, mineCurrent.oldStart, mineCurrent.lines, theirsCurrent.oldStart, theirsCurrent.lines); - theirsIndex++; - mineIndex++; - - ret.hunks.push(mergedHunk); - } - } - - return ret; - } - - function loadPatch(param, base) { - if (typeof param === 'string') { - if (/^@@/m.test(param) || /^Index:/m.test(param)) { - return (/*istanbul ignore start*/(0, _parse.parsePatch) /*istanbul ignore end*/(param)[0] - ); - } - - if (!base) { - throw new Error('Must provide a base reference or pass in a patch'); - } - return (/*istanbul ignore start*/(0, _create.structuredPatch) /*istanbul ignore end*/(undefined, undefined, base, param) - ); - } - - return param; - } - - function fileNameChanged(patch) { - return patch.newFileName && patch.newFileName !== patch.oldFileName; - } - - function selectField(index, mine, theirs) { - if (mine === theirs) { - return mine; - } else { - index.conflict = true; - return { mine: mine, theirs: theirs }; - } - } - - function hunkBefore(test, check) { - return test.oldStart < check.oldStart && test.oldStart + test.oldLines < check.oldStart; - } - - function cloneHunk(hunk, offset) { - return { - oldStart: hunk.oldStart, oldLines: hunk.oldLines, - newStart: hunk.newStart + offset, newLines: hunk.newLines, - lines: hunk.lines - }; - } - - function mergeLines(hunk, mineOffset, mineLines, theirOffset, theirLines) { - // This will generally result in a conflicted hunk, but there are cases where the context - // is the only overlap where we can successfully merge the content here. - var mine = { offset: mineOffset, lines: mineLines, index: 0 }, - their = { offset: theirOffset, lines: theirLines, index: 0 }; - - // Handle any leading content - insertLeading(hunk, mine, their); - insertLeading(hunk, their, mine); - - // Now in the overlap content. Scan through and select the best changes from each. - while (mine.index < mine.lines.length && their.index < their.lines.length) { - var mineCurrent = mine.lines[mine.index], - theirCurrent = their.lines[their.index]; - - if ((mineCurrent[0] === '-' || mineCurrent[0] === '+') && (theirCurrent[0] === '-' || theirCurrent[0] === '+')) { - // Both modified ... - mutualChange(hunk, mine, their); - } else if (mineCurrent[0] === '+' && theirCurrent[0] === ' ') { - /*istanbul ignore start*/var _hunk$lines; - - /*istanbul ignore end*/ // Mine inserted - /*istanbul ignore start*/(_hunk$lines = /*istanbul ignore end*/hunk.lines).push. /*istanbul ignore start*/apply /*istanbul ignore end*/( /*istanbul ignore start*/_hunk$lines /*istanbul ignore end*/, /*istanbul ignore start*/_toConsumableArray( /*istanbul ignore end*/collectChange(mine))); - } else if (theirCurrent[0] === '+' && mineCurrent[0] === ' ') { - /*istanbul ignore start*/var _hunk$lines2; - - /*istanbul ignore end*/ // Theirs inserted - /*istanbul ignore start*/(_hunk$lines2 = /*istanbul ignore end*/hunk.lines).push. /*istanbul ignore start*/apply /*istanbul ignore end*/( /*istanbul ignore start*/_hunk$lines2 /*istanbul ignore end*/, /*istanbul ignore start*/_toConsumableArray( /*istanbul ignore end*/collectChange(their))); - } else if (mineCurrent[0] === '-' && theirCurrent[0] === ' ') { - // Mine removed or edited - removal(hunk, mine, their); - } else if (theirCurrent[0] === '-' && mineCurrent[0] === ' ') { - // Their removed or edited - removal(hunk, their, mine, true); - } else if (mineCurrent === theirCurrent) { - // Context identity - hunk.lines.push(mineCurrent); - mine.index++; - their.index++; - } else { - // Context mismatch - conflict(hunk, collectChange(mine), collectChange(their)); - } - } - - // Now push anything that may be remaining - insertTrailing(hunk, mine); - insertTrailing(hunk, their); - - calcLineCount(hunk); - } - - function mutualChange(hunk, mine, their) { - var myChanges = collectChange(mine), - theirChanges = collectChange(their); - - if (allRemoves(myChanges) && allRemoves(theirChanges)) { - // Special case for remove changes that are supersets of one another - if ( /*istanbul ignore start*/(0, _array.arrayStartsWith) /*istanbul ignore end*/(myChanges, theirChanges) && skipRemoveSuperset(their, myChanges, myChanges.length - theirChanges.length)) { - /*istanbul ignore start*/var _hunk$lines3; - - /*istanbul ignore end*/ /*istanbul ignore start*/(_hunk$lines3 = /*istanbul ignore end*/hunk.lines).push. /*istanbul ignore start*/apply /*istanbul ignore end*/( /*istanbul ignore start*/_hunk$lines3 /*istanbul ignore end*/, /*istanbul ignore start*/_toConsumableArray( /*istanbul ignore end*/myChanges)); - return; - } else if ( /*istanbul ignore start*/(0, _array.arrayStartsWith) /*istanbul ignore end*/(theirChanges, myChanges) && skipRemoveSuperset(mine, theirChanges, theirChanges.length - myChanges.length)) { - /*istanbul ignore start*/var _hunk$lines4; - - /*istanbul ignore end*/ /*istanbul ignore start*/(_hunk$lines4 = /*istanbul ignore end*/hunk.lines).push. /*istanbul ignore start*/apply /*istanbul ignore end*/( /*istanbul ignore start*/_hunk$lines4 /*istanbul ignore end*/, /*istanbul ignore start*/_toConsumableArray( /*istanbul ignore end*/theirChanges)); - return; - } - } else if ( /*istanbul ignore start*/(0, _array.arrayEqual) /*istanbul ignore end*/(myChanges, theirChanges)) { - /*istanbul ignore start*/var _hunk$lines5; - - /*istanbul ignore end*/ /*istanbul ignore start*/(_hunk$lines5 = /*istanbul ignore end*/hunk.lines).push. /*istanbul ignore start*/apply /*istanbul ignore end*/( /*istanbul ignore start*/_hunk$lines5 /*istanbul ignore end*/, /*istanbul ignore start*/_toConsumableArray( /*istanbul ignore end*/myChanges)); - return; - } - - conflict(hunk, myChanges, theirChanges); - } - - function removal(hunk, mine, their, swap) { - var myChanges = collectChange(mine), - theirChanges = collectContext(their, myChanges); - if (theirChanges.merged) { - /*istanbul ignore start*/var _hunk$lines6; - - /*istanbul ignore end*/ /*istanbul ignore start*/(_hunk$lines6 = /*istanbul ignore end*/hunk.lines).push. /*istanbul ignore start*/apply /*istanbul ignore end*/( /*istanbul ignore start*/_hunk$lines6 /*istanbul ignore end*/, /*istanbul ignore start*/_toConsumableArray( /*istanbul ignore end*/theirChanges.merged)); - } else { - conflict(hunk, swap ? theirChanges : myChanges, swap ? myChanges : theirChanges); - } - } - - function conflict(hunk, mine, their) { - hunk.conflict = true; - hunk.lines.push({ - conflict: true, - mine: mine, - theirs: their - }); - } - - function insertLeading(hunk, insert, their) { - while (insert.offset < their.offset && insert.index < insert.lines.length) { - var line = insert.lines[insert.index++]; - hunk.lines.push(line); - insert.offset++; - } - } - function insertTrailing(hunk, insert) { - while (insert.index < insert.lines.length) { - var line = insert.lines[insert.index++]; - hunk.lines.push(line); - } - } - - function collectChange(state) { - var ret = [], - operation = state.lines[state.index][0]; - while (state.index < state.lines.length) { - var line = state.lines[state.index]; - - // Group additions that are immediately after subtractions and treat them as one "atomic" modify change. - if (operation === '-' && line[0] === '+') { - operation = '+'; - } - - if (operation === line[0]) { - ret.push(line); - state.index++; - } else { - break; - } - } - - return ret; - } - function collectContext(state, matchChanges) { - var changes = [], - merged = [], - matchIndex = 0, - contextChanges = false, - conflicted = false; - while (matchIndex < matchChanges.length && state.index < state.lines.length) { - var change = state.lines[state.index], - match = matchChanges[matchIndex]; - - // Once we've hit our add, then we are done - if (match[0] === '+') { - break; - } - - contextChanges = contextChanges || change[0] !== ' '; - - merged.push(match); - matchIndex++; - - // Consume any additions in the other block as a conflict to attempt - // to pull in the remaining context after this - if (change[0] === '+') { - conflicted = true; - - while (change[0] === '+') { - changes.push(change); - change = state.lines[++state.index]; - } - } - - if (match.substr(1) === change.substr(1)) { - changes.push(change); - state.index++; - } else { - conflicted = true; - } - } - - if ((matchChanges[matchIndex] || '')[0] === '+' && contextChanges) { - conflicted = true; - } - - if (conflicted) { - return changes; - } - - while (matchIndex < matchChanges.length) { - merged.push(matchChanges[matchIndex++]); - } - - return { - merged: merged, - changes: changes - }; - } - - function allRemoves(changes) { - return changes.reduce(function (prev, change) { - return prev && change[0] === '-'; - }, true); - } - function skipRemoveSuperset(state, removeChanges, delta) { - for (var i = 0; i < delta; i++) { - var changeContent = removeChanges[removeChanges.length - delta + i].substr(1); - if (state.lines[state.index + i] !== ' ' + changeContent) { - return false; - } - } - - state.index += delta; - return true; - } - - function calcOldNewLineCount(lines) { - var oldLines = 0; - var newLines = 0; - - lines.forEach(function (line) { - if (typeof line !== 'string') { - var myCount = calcOldNewLineCount(line.mine); - var theirCount = calcOldNewLineCount(line.theirs); - - if (oldLines !== undefined) { - if (myCount.oldLines === theirCount.oldLines) { - oldLines += myCount.oldLines; - } else { - oldLines = undefined; - } - } - - if (newLines !== undefined) { - if (myCount.newLines === theirCount.newLines) { - newLines += myCount.newLines; - } else { - newLines = undefined; - } - } - } else { - if (newLines !== undefined && (line[0] === '+' || line[0] === ' ')) { - newLines++; - } - if (oldLines !== undefined && (line[0] === '-' || line[0] === ' ')) { - oldLines++; - } - } - }); - - return { oldLines: oldLines, newLines: newLines }; - } - - - -/***/ }), -/* 14 */ -/***/ (function(module, exports, __webpack_require__) { - - /*istanbul ignore start*/'use strict'; - - exports.__esModule = true; - exports. /*istanbul ignore end*/structuredPatch = structuredPatch; - /*istanbul ignore start*/exports. /*istanbul ignore end*/createTwoFilesPatch = createTwoFilesPatch; - /*istanbul ignore start*/exports. /*istanbul ignore end*/createPatch = createPatch; - - var /*istanbul ignore start*/_line = __webpack_require__(5) /*istanbul ignore end*/; - - /*istanbul ignore start*/function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } - - /*istanbul ignore end*/function structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) { - if (!options) { - options = {}; - } - if (typeof options.context === 'undefined') { - options.context = 4; - } - - var diff = /*istanbul ignore start*/(0, _line.diffLines) /*istanbul ignore end*/(oldStr, newStr, options); - diff.push({ value: '', lines: [] }); // Append an empty value to make cleanup easier - - function contextLines(lines) { - return lines.map(function (entry) { - return ' ' + entry; - }); - } - - var hunks = []; - var oldRangeStart = 0, - newRangeStart = 0, - curRange = [], - oldLine = 1, - newLine = 1; - - /*istanbul ignore start*/var _loop = function _loop( /*istanbul ignore end*/i) { - var current = diff[i], - lines = current.lines || current.value.replace(/\n$/, '').split('\n'); - current.lines = lines; - - if (current.added || current.removed) { - /*istanbul ignore start*/var _curRange; - - /*istanbul ignore end*/ // If we have previous context, start with that - if (!oldRangeStart) { - var prev = diff[i - 1]; - oldRangeStart = oldLine; - newRangeStart = newLine; - - if (prev) { - curRange = options.context > 0 ? contextLines(prev.lines.slice(-options.context)) : []; - oldRangeStart -= curRange.length; - newRangeStart -= curRange.length; - } - } - - // Output our changes - /*istanbul ignore start*/(_curRange = /*istanbul ignore end*/curRange).push. /*istanbul ignore start*/apply /*istanbul ignore end*/( /*istanbul ignore start*/_curRange /*istanbul ignore end*/, /*istanbul ignore start*/_toConsumableArray( /*istanbul ignore end*/lines.map(function (entry) { - return (current.added ? '+' : '-') + entry; - }))); - - // Track the updated file position - if (current.added) { - newLine += lines.length; - } else { - oldLine += lines.length; - } - } else { - // Identical context lines. Track line changes - if (oldRangeStart) { - // Close out any changes that have been output (or join overlapping) - if (lines.length <= options.context * 2 && i < diff.length - 2) { - /*istanbul ignore start*/var _curRange2; - - /*istanbul ignore end*/ // Overlapping - /*istanbul ignore start*/(_curRange2 = /*istanbul ignore end*/curRange).push. /*istanbul ignore start*/apply /*istanbul ignore end*/( /*istanbul ignore start*/_curRange2 /*istanbul ignore end*/, /*istanbul ignore start*/_toConsumableArray( /*istanbul ignore end*/contextLines(lines))); - } else { - /*istanbul ignore start*/var _curRange3; - - /*istanbul ignore end*/ // end the range and output - var contextSize = Math.min(lines.length, options.context); - /*istanbul ignore start*/(_curRange3 = /*istanbul ignore end*/curRange).push. /*istanbul ignore start*/apply /*istanbul ignore end*/( /*istanbul ignore start*/_curRange3 /*istanbul ignore end*/, /*istanbul ignore start*/_toConsumableArray( /*istanbul ignore end*/contextLines(lines.slice(0, contextSize)))); - - var hunk = { - oldStart: oldRangeStart, - oldLines: oldLine - oldRangeStart + contextSize, - newStart: newRangeStart, - newLines: newLine - newRangeStart + contextSize, - lines: curRange - }; - if (i >= diff.length - 2 && lines.length <= options.context) { - // EOF is inside this hunk - var oldEOFNewline = /\n$/.test(oldStr); - var newEOFNewline = /\n$/.test(newStr); - if (lines.length == 0 && !oldEOFNewline) { - // special case: old has no eol and no trailing context; no-nl can end up before adds - curRange.splice(hunk.oldLines, 0, '\\ No newline at end of file'); - } else if (!oldEOFNewline || !newEOFNewline) { - curRange.push('\\ No newline at end of file'); - } - } - hunks.push(hunk); - - oldRangeStart = 0; - newRangeStart = 0; - curRange = []; - } - } - oldLine += lines.length; - newLine += lines.length; - } - }; - - for (var i = 0; i < diff.length; i++) { - /*istanbul ignore start*/_loop( /*istanbul ignore end*/i); - } - - return { - oldFileName: oldFileName, newFileName: newFileName, - oldHeader: oldHeader, newHeader: newHeader, - hunks: hunks - }; - } - - function createTwoFilesPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) { - var diff = structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options); - - var ret = []; - if (oldFileName == newFileName) { - ret.push('Index: ' + oldFileName); - } - ret.push('==================================================================='); - ret.push('--- ' + diff.oldFileName + (typeof diff.oldHeader === 'undefined' ? '' : '\t' + diff.oldHeader)); - ret.push('+++ ' + diff.newFileName + (typeof diff.newHeader === 'undefined' ? '' : '\t' + diff.newHeader)); - - for (var i = 0; i < diff.hunks.length; i++) { - var hunk = diff.hunks[i]; - ret.push('@@ -' + hunk.oldStart + ',' + hunk.oldLines + ' +' + hunk.newStart + ',' + hunk.newLines + ' @@'); - ret.push.apply(ret, hunk.lines); - } - - return ret.join('\n') + '\n'; - } - - function createPatch(fileName, oldStr, newStr, oldHeader, newHeader, options) { - return createTwoFilesPatch(fileName, fileName, oldStr, newStr, oldHeader, newHeader, options); - } - - - -/***/ }), -/* 15 */ -/***/ (function(module, exports) { - - /*istanbul ignore start*/"use strict"; - - exports.__esModule = true; - exports. /*istanbul ignore end*/arrayEqual = arrayEqual; - /*istanbul ignore start*/exports. /*istanbul ignore end*/arrayStartsWith = arrayStartsWith; - function arrayEqual(a, b) { - if (a.length !== b.length) { - return false; - } - - return arrayStartsWith(a, b); - } - - function arrayStartsWith(array, start) { - if (start.length > array.length) { - return false; - } - - for (var i = 0; i < start.length; i++) { - if (start[i] !== array[i]) { - return false; - } - } - - return true; - } - - - -/***/ }), -/* 16 */ -/***/ (function(module, exports) { - - /*istanbul ignore start*/"use strict"; - - exports.__esModule = true; - exports. /*istanbul ignore end*/convertChangesToDMP = convertChangesToDMP; - // See: http://code.google.com/p/google-diff-match-patch/wiki/API - function convertChangesToDMP(changes) { - var ret = [], - change = /*istanbul ignore start*/void 0 /*istanbul ignore end*/, - operation = /*istanbul ignore start*/void 0 /*istanbul ignore end*/; - for (var i = 0; i < changes.length; i++) { - change = changes[i]; - if (change.added) { - operation = 1; - } else if (change.removed) { - operation = -1; - } else { - operation = 0; - } - - ret.push([operation, change.value]); - } - return ret; - } - - - -/***/ }), -/* 17 */ -/***/ (function(module, exports) { - - /*istanbul ignore start*/'use strict'; - - exports.__esModule = true; - exports. /*istanbul ignore end*/convertChangesToXML = convertChangesToXML; - function convertChangesToXML(changes) { - var ret = []; - for (var i = 0; i < changes.length; i++) { - var change = changes[i]; - if (change.added) { - ret.push(''); - } else if (change.removed) { - ret.push(''); - } - - ret.push(escapeHTML(change.value)); - - if (change.added) { - ret.push(''); - } else if (change.removed) { - ret.push(''); - } - } - return ret.join(''); - } - - function escapeHTML(s) { - var n = s; - n = n.replace(/&/g, '&'); - n = n.replace(//g, '>'); - n = n.replace(/"/g, '"'); - - return n; - } - - - -/***/ }) -/******/ ]) -}); -; -},{}],49:[function(require,module,exports){ -'use strict'; - -var matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g; - -module.exports = function (str) { - if (typeof str !== 'string') { - throw new TypeError('Expected a string'); - } - - return str.replace(matchOperatorsRe, '\\$&'); -}; - -},{}],50:[function(require,module,exports){ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -var objectCreate = Object.create || objectCreatePolyfill -var objectKeys = Object.keys || objectKeysPolyfill -var bind = Function.prototype.bind || functionBindPolyfill - -function EventEmitter() { - if (!this._events || !Object.prototype.hasOwnProperty.call(this, '_events')) { - this._events = objectCreate(null); - this._eventsCount = 0; - } - - this._maxListeners = this._maxListeners || undefined; -} -module.exports = EventEmitter; - -// Backwards-compat with node 0.10.x -EventEmitter.EventEmitter = EventEmitter; - -EventEmitter.prototype._events = undefined; -EventEmitter.prototype._maxListeners = undefined; - -// By default EventEmitters will print a warning if more than 10 listeners are -// added to it. This is a useful default which helps finding memory leaks. -var defaultMaxListeners = 10; - -var hasDefineProperty; -try { - var o = {}; - if (Object.defineProperty) Object.defineProperty(o, 'x', { value: 0 }); - hasDefineProperty = o.x === 0; -} catch (err) { hasDefineProperty = false } -if (hasDefineProperty) { - Object.defineProperty(EventEmitter, 'defaultMaxListeners', { - enumerable: true, - get: function() { - return defaultMaxListeners; - }, - set: function(arg) { - // check whether the input is a positive number (whose value is zero or - // greater and not a NaN). - if (typeof arg !== 'number' || arg < 0 || arg !== arg) - throw new TypeError('"defaultMaxListeners" must be a positive number'); - defaultMaxListeners = arg; - } - }); -} else { - EventEmitter.defaultMaxListeners = defaultMaxListeners; -} - -// Obviously not all Emitters should be limited to 10. This function allows -// that to be increased. Set to zero for unlimited. -EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { - if (typeof n !== 'number' || n < 0 || isNaN(n)) - throw new TypeError('"n" argument must be a positive number'); - this._maxListeners = n; - return this; -}; - -function $getMaxListeners(that) { - if (that._maxListeners === undefined) - return EventEmitter.defaultMaxListeners; - return that._maxListeners; -} - -EventEmitter.prototype.getMaxListeners = function getMaxListeners() { - return $getMaxListeners(this); -}; - -// These standalone emit* functions are used to optimize calling of event -// handlers for fast cases because emit() itself often has a variable number of -// arguments and can be deoptimized because of that. These functions always have -// the same number of arguments and thus do not get deoptimized, so the code -// inside them can execute faster. -function emitNone(handler, isFn, self) { - if (isFn) - handler.call(self); - else { - var len = handler.length; - var listeners = arrayClone(handler, len); - for (var i = 0; i < len; ++i) - listeners[i].call(self); - } -} -function emitOne(handler, isFn, self, arg1) { - if (isFn) - handler.call(self, arg1); - else { - var len = handler.length; - var listeners = arrayClone(handler, len); - for (var i = 0; i < len; ++i) - listeners[i].call(self, arg1); - } -} -function emitTwo(handler, isFn, self, arg1, arg2) { - if (isFn) - handler.call(self, arg1, arg2); - else { - var len = handler.length; - var listeners = arrayClone(handler, len); - for (var i = 0; i < len; ++i) - listeners[i].call(self, arg1, arg2); - } -} -function emitThree(handler, isFn, self, arg1, arg2, arg3) { - if (isFn) - handler.call(self, arg1, arg2, arg3); - else { - var len = handler.length; - var listeners = arrayClone(handler, len); - for (var i = 0; i < len; ++i) - listeners[i].call(self, arg1, arg2, arg3); - } -} - -function emitMany(handler, isFn, self, args) { - if (isFn) - handler.apply(self, args); - else { - var len = handler.length; - var listeners = arrayClone(handler, len); - for (var i = 0; i < len; ++i) - listeners[i].apply(self, args); - } -} - -EventEmitter.prototype.emit = function emit(type) { - var er, handler, len, args, i, events; - var doError = (type === 'error'); - - events = this._events; - if (events) - doError = (doError && events.error == null); - else if (!doError) - return false; - - // If there is no 'error' event listener then throw. - if (doError) { - if (arguments.length > 1) - er = arguments[1]; - if (er instanceof Error) { - throw er; // Unhandled 'error' event - } else { - // At least give some kind of context to the user - var err = new Error('Unhandled "error" event. (' + er + ')'); - err.context = er; - throw err; - } - return false; - } - - handler = events[type]; - - if (!handler) - return false; - - var isFn = typeof handler === 'function'; - len = arguments.length; - switch (len) { - // fast cases - case 1: - emitNone(handler, isFn, this); - break; - case 2: - emitOne(handler, isFn, this, arguments[1]); - break; - case 3: - emitTwo(handler, isFn, this, arguments[1], arguments[2]); - break; - case 4: - emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]); - break; - // slower - default: - args = new Array(len - 1); - for (i = 1; i < len; i++) - args[i - 1] = arguments[i]; - emitMany(handler, isFn, this, args); - } - - return true; -}; - -function _addListener(target, type, listener, prepend) { - var m; - var events; - var existing; - - if (typeof listener !== 'function') - throw new TypeError('"listener" argument must be a function'); - - events = target._events; - if (!events) { - events = target._events = objectCreate(null); - target._eventsCount = 0; - } else { - // To avoid recursion in the case that type === "newListener"! Before - // adding it to the listeners, first emit "newListener". - if (events.newListener) { - target.emit('newListener', type, - listener.listener ? listener.listener : listener); - - // Re-assign `events` because a newListener handler could have caused the - // this._events to be assigned to a new object - events = target._events; - } - existing = events[type]; - } - - if (!existing) { - // Optimize the case of one listener. Don't need the extra array object. - existing = events[type] = listener; - ++target._eventsCount; - } else { - if (typeof existing === 'function') { - // Adding the second element, need to change to array. - existing = events[type] = - prepend ? [listener, existing] : [existing, listener]; - } else { - // If we've already got an array, just append. - if (prepend) { - existing.unshift(listener); - } else { - existing.push(listener); - } - } - - // Check for listener leak - if (!existing.warned) { - m = $getMaxListeners(target); - if (m && m > 0 && existing.length > m) { - existing.warned = true; - var w = new Error('Possible EventEmitter memory leak detected. ' + - existing.length + ' "' + String(type) + '" listeners ' + - 'added. Use emitter.setMaxListeners() to ' + - 'increase limit.'); - w.name = 'MaxListenersExceededWarning'; - w.emitter = target; - w.type = type; - w.count = existing.length; - if (typeof console === 'object' && console.warn) { - console.warn('%s: %s', w.name, w.message); - } - } - } - } - - return target; -} - -EventEmitter.prototype.addListener = function addListener(type, listener) { - return _addListener(this, type, listener, false); -}; - -EventEmitter.prototype.on = EventEmitter.prototype.addListener; - -EventEmitter.prototype.prependListener = - function prependListener(type, listener) { - return _addListener(this, type, listener, true); - }; - -function onceWrapper() { - if (!this.fired) { - this.target.removeListener(this.type, this.wrapFn); - this.fired = true; - switch (arguments.length) { - case 0: - return this.listener.call(this.target); - case 1: - return this.listener.call(this.target, arguments[0]); - case 2: - return this.listener.call(this.target, arguments[0], arguments[1]); - case 3: - return this.listener.call(this.target, arguments[0], arguments[1], - arguments[2]); - default: - var args = new Array(arguments.length); - for (var i = 0; i < args.length; ++i) - args[i] = arguments[i]; - this.listener.apply(this.target, args); - } - } -} - -function _onceWrap(target, type, listener) { - var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener }; - var wrapped = bind.call(onceWrapper, state); - wrapped.listener = listener; - state.wrapFn = wrapped; - return wrapped; -} - -EventEmitter.prototype.once = function once(type, listener) { - if (typeof listener !== 'function') - throw new TypeError('"listener" argument must be a function'); - this.on(type, _onceWrap(this, type, listener)); - return this; -}; - -EventEmitter.prototype.prependOnceListener = - function prependOnceListener(type, listener) { - if (typeof listener !== 'function') - throw new TypeError('"listener" argument must be a function'); - this.prependListener(type, _onceWrap(this, type, listener)); - return this; - }; - -// Emits a 'removeListener' event if and only if the listener was removed. -EventEmitter.prototype.removeListener = - function removeListener(type, listener) { - var list, events, position, i, originalListener; - - if (typeof listener !== 'function') - throw new TypeError('"listener" argument must be a function'); - - events = this._events; - if (!events) - return this; - - list = events[type]; - if (!list) - return this; - - if (list === listener || list.listener === listener) { - if (--this._eventsCount === 0) - this._events = objectCreate(null); - else { - delete events[type]; - if (events.removeListener) - this.emit('removeListener', type, list.listener || listener); - } - } else if (typeof list !== 'function') { - position = -1; - - for (i = list.length - 1; i >= 0; i--) { - if (list[i] === listener || list[i].listener === listener) { - originalListener = list[i].listener; - position = i; - break; - } - } - - if (position < 0) - return this; - - if (position === 0) - list.shift(); - else - spliceOne(list, position); - - if (list.length === 1) - events[type] = list[0]; - - if (events.removeListener) - this.emit('removeListener', type, originalListener || listener); - } - - return this; - }; - -EventEmitter.prototype.removeAllListeners = - function removeAllListeners(type) { - var listeners, events, i; - - events = this._events; - if (!events) - return this; - - // not listening for removeListener, no need to emit - if (!events.removeListener) { - if (arguments.length === 0) { - this._events = objectCreate(null); - this._eventsCount = 0; - } else if (events[type]) { - if (--this._eventsCount === 0) - this._events = objectCreate(null); - else - delete events[type]; - } - return this; - } - - // emit removeListener for all listeners on all events - if (arguments.length === 0) { - var keys = objectKeys(events); - var key; - for (i = 0; i < keys.length; ++i) { - key = keys[i]; - if (key === 'removeListener') continue; - this.removeAllListeners(key); - } - this.removeAllListeners('removeListener'); - this._events = objectCreate(null); - this._eventsCount = 0; - return this; - } - - listeners = events[type]; - - if (typeof listeners === 'function') { - this.removeListener(type, listeners); - } else if (listeners) { - // LIFO order - for (i = listeners.length - 1; i >= 0; i--) { - this.removeListener(type, listeners[i]); - } - } - - return this; - }; - -function _listeners(target, type, unwrap) { - var events = target._events; - - if (!events) - return []; - - var evlistener = events[type]; - if (!evlistener) - return []; - - if (typeof evlistener === 'function') - return unwrap ? [evlistener.listener || evlistener] : [evlistener]; - - return unwrap ? unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length); -} - -EventEmitter.prototype.listeners = function listeners(type) { - return _listeners(this, type, true); -}; - -EventEmitter.prototype.rawListeners = function rawListeners(type) { - return _listeners(this, type, false); -}; - -EventEmitter.listenerCount = function(emitter, type) { - if (typeof emitter.listenerCount === 'function') { - return emitter.listenerCount(type); - } else { - return listenerCount.call(emitter, type); - } -}; - -EventEmitter.prototype.listenerCount = listenerCount; -function listenerCount(type) { - var events = this._events; - - if (events) { - var evlistener = events[type]; - - if (typeof evlistener === 'function') { - return 1; - } else if (evlistener) { - return evlistener.length; - } - } - - return 0; -} - -EventEmitter.prototype.eventNames = function eventNames() { - return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : []; -}; - -// About 1.5x faster than the two-arg version of Array#splice(). -function spliceOne(list, index) { - for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) - list[i] = list[k]; - list.pop(); -} - -function arrayClone(arr, n) { - var copy = new Array(n); - for (var i = 0; i < n; ++i) - copy[i] = arr[i]; - return copy; -} - -function unwrapListeners(arr) { - var ret = new Array(arr.length); - for (var i = 0; i < ret.length; ++i) { - ret[i] = arr[i].listener || arr[i]; - } - return ret; -} - -function objectCreatePolyfill(proto) { - var F = function() {}; - F.prototype = proto; - return new F; -} -function objectKeysPolyfill(obj) { - var keys = []; - for (var k in obj) if (Object.prototype.hasOwnProperty.call(obj, k)) { - keys.push(k); - } - return k; -} -function functionBindPolyfill(context) { - var fn = this; - return function () { - return fn.apply(context, arguments); - }; -} - -},{}],51:[function(require,module,exports){ -'use strict'; - -/* eslint no-invalid-this: 1 */ - -var ERROR_MESSAGE = 'Function.prototype.bind called on incompatible '; -var slice = Array.prototype.slice; -var toStr = Object.prototype.toString; -var funcType = '[object Function]'; - -module.exports = function bind(that) { - var target = this; - if (typeof target !== 'function' || toStr.call(target) !== funcType) { - throw new TypeError(ERROR_MESSAGE + target); - } - var args = slice.call(arguments, 1); - - var bound; - var binder = function () { - if (this instanceof bound) { - var result = target.apply( - this, - args.concat(slice.call(arguments)) - ); - if (Object(result) === result) { - return result; - } - return this; - } else { - return target.apply( - that, - args.concat(slice.call(arguments)) - ); - } - }; - - var boundLength = Math.max(0, target.length - args.length); - var boundArgs = []; - for (var i = 0; i < boundLength; i++) { - boundArgs.push('$' + i); - } - - bound = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this,arguments); }')(binder); - - if (target.prototype) { - var Empty = function Empty() {}; - Empty.prototype = target.prototype; - bound.prototype = new Empty(); - Empty.prototype = null; - } - - return bound; -}; - -},{}],52:[function(require,module,exports){ -'use strict'; - -var implementation = require('./implementation'); - -module.exports = Function.prototype.bind || implementation; - -},{"./implementation":51}],53:[function(require,module,exports){ -'use strict'; - -/* eslint complexity: [2, 17], max-statements: [2, 33] */ -module.exports = function hasSymbols() { - if (typeof Symbol !== 'function' || typeof Object.getOwnPropertySymbols !== 'function') { return false; } - if (typeof Symbol.iterator === 'symbol') { return true; } - - var obj = {}; - var sym = Symbol('test'); - var symObj = Object(sym); - if (typeof sym === 'string') { return false; } - - if (Object.prototype.toString.call(sym) !== '[object Symbol]') { return false; } - if (Object.prototype.toString.call(symObj) !== '[object Symbol]') { return false; } - - // temp disabled per https://github.com/ljharb/object.assign/issues/17 - // if (sym instanceof Symbol) { return false; } - // temp disabled per https://github.com/WebReflection/get-own-property-symbols/issues/4 - // if (!(symObj instanceof Symbol)) { return false; } - - // if (typeof Symbol.prototype.toString !== 'function') { return false; } - // if (String(sym) !== Symbol.prototype.toString.call(sym)) { return false; } - - var symVal = 42; - obj[sym] = symVal; - for (sym in obj) { return false; } // eslint-disable-line no-restricted-syntax - if (typeof Object.keys === 'function' && Object.keys(obj).length !== 0) { return false; } - - if (typeof Object.getOwnPropertyNames === 'function' && Object.getOwnPropertyNames(obj).length !== 0) { return false; } - - var syms = Object.getOwnPropertySymbols(obj); - if (syms.length !== 1 || syms[0] !== sym) { return false; } - - if (!Object.prototype.propertyIsEnumerable.call(obj, sym)) { return false; } - - if (typeof Object.getOwnPropertyDescriptor === 'function') { - var descriptor = Object.getOwnPropertyDescriptor(obj, sym); - if (descriptor.value !== symVal || descriptor.enumerable !== true) { return false; } - } - - return true; -}; - -},{}],54:[function(require,module,exports){ -(function (global){ -/*! https://mths.be/he v1.2.0 by @mathias | MIT license */ -;(function(root) { - - // Detect free variables `exports`. - var freeExports = typeof exports == 'object' && exports; - - // Detect free variable `module`. - var freeModule = typeof module == 'object' && module && - module.exports == freeExports && module; - - // Detect free variable `global`, from Node.js or Browserified code, - // and use it as `root`. - var freeGlobal = typeof global == 'object' && global; - if (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal) { - root = freeGlobal; - } - - /*--------------------------------------------------------------------------*/ - - // All astral symbols. - var regexAstralSymbols = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g; - // All ASCII symbols (not just printable ASCII) except those listed in the - // first column of the overrides table. - // https://html.spec.whatwg.org/multipage/syntax.html#table-charref-overrides - var regexAsciiWhitelist = /[\x01-\x7F]/g; - // All BMP symbols that are not ASCII newlines, printable ASCII symbols, or - // code points listed in the first column of the overrides table on - // https://html.spec.whatwg.org/multipage/syntax.html#table-charref-overrides. - var regexBmpWhitelist = /[\x01-\t\x0B\f\x0E-\x1F\x7F\x81\x8D\x8F\x90\x9D\xA0-\uFFFF]/g; - - var regexEncodeNonAscii = /<\u20D2|=\u20E5|>\u20D2|\u205F\u200A|\u219D\u0338|\u2202\u0338|\u2220\u20D2|\u2229\uFE00|\u222A\uFE00|\u223C\u20D2|\u223D\u0331|\u223E\u0333|\u2242\u0338|\u224B\u0338|\u224D\u20D2|\u224E\u0338|\u224F\u0338|\u2250\u0338|\u2261\u20E5|\u2264\u20D2|\u2265\u20D2|\u2266\u0338|\u2267\u0338|\u2268\uFE00|\u2269\uFE00|\u226A\u0338|\u226A\u20D2|\u226B\u0338|\u226B\u20D2|\u227F\u0338|\u2282\u20D2|\u2283\u20D2|\u228A\uFE00|\u228B\uFE00|\u228F\u0338|\u2290\u0338|\u2293\uFE00|\u2294\uFE00|\u22B4\u20D2|\u22B5\u20D2|\u22D8\u0338|\u22D9\u0338|\u22DA\uFE00|\u22DB\uFE00|\u22F5\u0338|\u22F9\u0338|\u2933\u0338|\u29CF\u0338|\u29D0\u0338|\u2A6D\u0338|\u2A70\u0338|\u2A7D\u0338|\u2A7E\u0338|\u2AA1\u0338|\u2AA2\u0338|\u2AAC\uFE00|\u2AAD\uFE00|\u2AAF\u0338|\u2AB0\u0338|\u2AC5\u0338|\u2AC6\u0338|\u2ACB\uFE00|\u2ACC\uFE00|\u2AFD\u20E5|[\xA0-\u0113\u0116-\u0122\u0124-\u012B\u012E-\u014D\u0150-\u017E\u0192\u01B5\u01F5\u0237\u02C6\u02C7\u02D8-\u02DD\u0311\u0391-\u03A1\u03A3-\u03A9\u03B1-\u03C9\u03D1\u03D2\u03D5\u03D6\u03DC\u03DD\u03F0\u03F1\u03F5\u03F6\u0401-\u040C\u040E-\u044F\u0451-\u045C\u045E\u045F\u2002-\u2005\u2007-\u2010\u2013-\u2016\u2018-\u201A\u201C-\u201E\u2020-\u2022\u2025\u2026\u2030-\u2035\u2039\u203A\u203E\u2041\u2043\u2044\u204F\u2057\u205F-\u2063\u20AC\u20DB\u20DC\u2102\u2105\u210A-\u2113\u2115-\u211E\u2122\u2124\u2127-\u2129\u212C\u212D\u212F-\u2131\u2133-\u2138\u2145-\u2148\u2153-\u215E\u2190-\u219B\u219D-\u21A7\u21A9-\u21AE\u21B0-\u21B3\u21B5-\u21B7\u21BA-\u21DB\u21DD\u21E4\u21E5\u21F5\u21FD-\u2205\u2207-\u2209\u220B\u220C\u220F-\u2214\u2216-\u2218\u221A\u221D-\u2238\u223A-\u2257\u2259\u225A\u225C\u225F-\u2262\u2264-\u228B\u228D-\u229B\u229D-\u22A5\u22A7-\u22B0\u22B2-\u22BB\u22BD-\u22DB\u22DE-\u22E3\u22E6-\u22F7\u22F9-\u22FE\u2305\u2306\u2308-\u2310\u2312\u2313\u2315\u2316\u231C-\u231F\u2322\u2323\u232D\u232E\u2336\u233D\u233F\u237C\u23B0\u23B1\u23B4-\u23B6\u23DC-\u23DF\u23E2\u23E7\u2423\u24C8\u2500\u2502\u250C\u2510\u2514\u2518\u251C\u2524\u252C\u2534\u253C\u2550-\u256C\u2580\u2584\u2588\u2591-\u2593\u25A1\u25AA\u25AB\u25AD\u25AE\u25B1\u25B3-\u25B5\u25B8\u25B9\u25BD-\u25BF\u25C2\u25C3\u25CA\u25CB\u25EC\u25EF\u25F8-\u25FC\u2605\u2606\u260E\u2640\u2642\u2660\u2663\u2665\u2666\u266A\u266D-\u266F\u2713\u2717\u2720\u2736\u2758\u2772\u2773\u27C8\u27C9\u27E6-\u27ED\u27F5-\u27FA\u27FC\u27FF\u2902-\u2905\u290C-\u2913\u2916\u2919-\u2920\u2923-\u292A\u2933\u2935-\u2939\u293C\u293D\u2945\u2948-\u294B\u294E-\u2976\u2978\u2979\u297B-\u297F\u2985\u2986\u298B-\u2996\u299A\u299C\u299D\u29A4-\u29B7\u29B9\u29BB\u29BC\u29BE-\u29C5\u29C9\u29CD-\u29D0\u29DC-\u29DE\u29E3-\u29E5\u29EB\u29F4\u29F6\u2A00-\u2A02\u2A04\u2A06\u2A0C\u2A0D\u2A10-\u2A17\u2A22-\u2A27\u2A29\u2A2A\u2A2D-\u2A31\u2A33-\u2A3C\u2A3F\u2A40\u2A42-\u2A4D\u2A50\u2A53-\u2A58\u2A5A-\u2A5D\u2A5F\u2A66\u2A6A\u2A6D-\u2A75\u2A77-\u2A9A\u2A9D-\u2AA2\u2AA4-\u2AB0\u2AB3-\u2AC8\u2ACB\u2ACC\u2ACF-\u2ADB\u2AE4\u2AE6-\u2AE9\u2AEB-\u2AF3\u2AFD\uFB00-\uFB04]|\uD835[\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDCCF\uDD04\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDD6B]/g; - var encodeMap = {'\xAD':'shy','\u200C':'zwnj','\u200D':'zwj','\u200E':'lrm','\u2063':'ic','\u2062':'it','\u2061':'af','\u200F':'rlm','\u200B':'ZeroWidthSpace','\u2060':'NoBreak','\u0311':'DownBreve','\u20DB':'tdot','\u20DC':'DotDot','\t':'Tab','\n':'NewLine','\u2008':'puncsp','\u205F':'MediumSpace','\u2009':'thinsp','\u200A':'hairsp','\u2004':'emsp13','\u2002':'ensp','\u2005':'emsp14','\u2003':'emsp','\u2007':'numsp','\xA0':'nbsp','\u205F\u200A':'ThickSpace','\u203E':'oline','_':'lowbar','\u2010':'dash','\u2013':'ndash','\u2014':'mdash','\u2015':'horbar',',':'comma',';':'semi','\u204F':'bsemi',':':'colon','\u2A74':'Colone','!':'excl','\xA1':'iexcl','?':'quest','\xBF':'iquest','.':'period','\u2025':'nldr','\u2026':'mldr','\xB7':'middot','\'':'apos','\u2018':'lsquo','\u2019':'rsquo','\u201A':'sbquo','\u2039':'lsaquo','\u203A':'rsaquo','"':'quot','\u201C':'ldquo','\u201D':'rdquo','\u201E':'bdquo','\xAB':'laquo','\xBB':'raquo','(':'lpar',')':'rpar','[':'lsqb',']':'rsqb','{':'lcub','}':'rcub','\u2308':'lceil','\u2309':'rceil','\u230A':'lfloor','\u230B':'rfloor','\u2985':'lopar','\u2986':'ropar','\u298B':'lbrke','\u298C':'rbrke','\u298D':'lbrkslu','\u298E':'rbrksld','\u298F':'lbrksld','\u2990':'rbrkslu','\u2991':'langd','\u2992':'rangd','\u2993':'lparlt','\u2994':'rpargt','\u2995':'gtlPar','\u2996':'ltrPar','\u27E6':'lobrk','\u27E7':'robrk','\u27E8':'lang','\u27E9':'rang','\u27EA':'Lang','\u27EB':'Rang','\u27EC':'loang','\u27ED':'roang','\u2772':'lbbrk','\u2773':'rbbrk','\u2016':'Vert','\xA7':'sect','\xB6':'para','@':'commat','*':'ast','/':'sol','undefined':null,'&':'amp','#':'num','%':'percnt','\u2030':'permil','\u2031':'pertenk','\u2020':'dagger','\u2021':'Dagger','\u2022':'bull','\u2043':'hybull','\u2032':'prime','\u2033':'Prime','\u2034':'tprime','\u2057':'qprime','\u2035':'bprime','\u2041':'caret','`':'grave','\xB4':'acute','\u02DC':'tilde','^':'Hat','\xAF':'macr','\u02D8':'breve','\u02D9':'dot','\xA8':'die','\u02DA':'ring','\u02DD':'dblac','\xB8':'cedil','\u02DB':'ogon','\u02C6':'circ','\u02C7':'caron','\xB0':'deg','\xA9':'copy','\xAE':'reg','\u2117':'copysr','\u2118':'wp','\u211E':'rx','\u2127':'mho','\u2129':'iiota','\u2190':'larr','\u219A':'nlarr','\u2192':'rarr','\u219B':'nrarr','\u2191':'uarr','\u2193':'darr','\u2194':'harr','\u21AE':'nharr','\u2195':'varr','\u2196':'nwarr','\u2197':'nearr','\u2198':'searr','\u2199':'swarr','\u219D':'rarrw','\u219D\u0338':'nrarrw','\u219E':'Larr','\u219F':'Uarr','\u21A0':'Rarr','\u21A1':'Darr','\u21A2':'larrtl','\u21A3':'rarrtl','\u21A4':'mapstoleft','\u21A5':'mapstoup','\u21A6':'map','\u21A7':'mapstodown','\u21A9':'larrhk','\u21AA':'rarrhk','\u21AB':'larrlp','\u21AC':'rarrlp','\u21AD':'harrw','\u21B0':'lsh','\u21B1':'rsh','\u21B2':'ldsh','\u21B3':'rdsh','\u21B5':'crarr','\u21B6':'cularr','\u21B7':'curarr','\u21BA':'olarr','\u21BB':'orarr','\u21BC':'lharu','\u21BD':'lhard','\u21BE':'uharr','\u21BF':'uharl','\u21C0':'rharu','\u21C1':'rhard','\u21C2':'dharr','\u21C3':'dharl','\u21C4':'rlarr','\u21C5':'udarr','\u21C6':'lrarr','\u21C7':'llarr','\u21C8':'uuarr','\u21C9':'rrarr','\u21CA':'ddarr','\u21CB':'lrhar','\u21CC':'rlhar','\u21D0':'lArr','\u21CD':'nlArr','\u21D1':'uArr','\u21D2':'rArr','\u21CF':'nrArr','\u21D3':'dArr','\u21D4':'iff','\u21CE':'nhArr','\u21D5':'vArr','\u21D6':'nwArr','\u21D7':'neArr','\u21D8':'seArr','\u21D9':'swArr','\u21DA':'lAarr','\u21DB':'rAarr','\u21DD':'zigrarr','\u21E4':'larrb','\u21E5':'rarrb','\u21F5':'duarr','\u21FD':'loarr','\u21FE':'roarr','\u21FF':'hoarr','\u2200':'forall','\u2201':'comp','\u2202':'part','\u2202\u0338':'npart','\u2203':'exist','\u2204':'nexist','\u2205':'empty','\u2207':'Del','\u2208':'in','\u2209':'notin','\u220B':'ni','\u220C':'notni','\u03F6':'bepsi','\u220F':'prod','\u2210':'coprod','\u2211':'sum','+':'plus','\xB1':'pm','\xF7':'div','\xD7':'times','<':'lt','\u226E':'nlt','<\u20D2':'nvlt','=':'equals','\u2260':'ne','=\u20E5':'bne','\u2A75':'Equal','>':'gt','\u226F':'ngt','>\u20D2':'nvgt','\xAC':'not','|':'vert','\xA6':'brvbar','\u2212':'minus','\u2213':'mp','\u2214':'plusdo','\u2044':'frasl','\u2216':'setmn','\u2217':'lowast','\u2218':'compfn','\u221A':'Sqrt','\u221D':'prop','\u221E':'infin','\u221F':'angrt','\u2220':'ang','\u2220\u20D2':'nang','\u2221':'angmsd','\u2222':'angsph','\u2223':'mid','\u2224':'nmid','\u2225':'par','\u2226':'npar','\u2227':'and','\u2228':'or','\u2229':'cap','\u2229\uFE00':'caps','\u222A':'cup','\u222A\uFE00':'cups','\u222B':'int','\u222C':'Int','\u222D':'tint','\u2A0C':'qint','\u222E':'oint','\u222F':'Conint','\u2230':'Cconint','\u2231':'cwint','\u2232':'cwconint','\u2233':'awconint','\u2234':'there4','\u2235':'becaus','\u2236':'ratio','\u2237':'Colon','\u2238':'minusd','\u223A':'mDDot','\u223B':'homtht','\u223C':'sim','\u2241':'nsim','\u223C\u20D2':'nvsim','\u223D':'bsim','\u223D\u0331':'race','\u223E':'ac','\u223E\u0333':'acE','\u223F':'acd','\u2240':'wr','\u2242':'esim','\u2242\u0338':'nesim','\u2243':'sime','\u2244':'nsime','\u2245':'cong','\u2247':'ncong','\u2246':'simne','\u2248':'ap','\u2249':'nap','\u224A':'ape','\u224B':'apid','\u224B\u0338':'napid','\u224C':'bcong','\u224D':'CupCap','\u226D':'NotCupCap','\u224D\u20D2':'nvap','\u224E':'bump','\u224E\u0338':'nbump','\u224F':'bumpe','\u224F\u0338':'nbumpe','\u2250':'doteq','\u2250\u0338':'nedot','\u2251':'eDot','\u2252':'efDot','\u2253':'erDot','\u2254':'colone','\u2255':'ecolon','\u2256':'ecir','\u2257':'cire','\u2259':'wedgeq','\u225A':'veeeq','\u225C':'trie','\u225F':'equest','\u2261':'equiv','\u2262':'nequiv','\u2261\u20E5':'bnequiv','\u2264':'le','\u2270':'nle','\u2264\u20D2':'nvle','\u2265':'ge','\u2271':'nge','\u2265\u20D2':'nvge','\u2266':'lE','\u2266\u0338':'nlE','\u2267':'gE','\u2267\u0338':'ngE','\u2268\uFE00':'lvnE','\u2268':'lnE','\u2269':'gnE','\u2269\uFE00':'gvnE','\u226A':'ll','\u226A\u0338':'nLtv','\u226A\u20D2':'nLt','\u226B':'gg','\u226B\u0338':'nGtv','\u226B\u20D2':'nGt','\u226C':'twixt','\u2272':'lsim','\u2274':'nlsim','\u2273':'gsim','\u2275':'ngsim','\u2276':'lg','\u2278':'ntlg','\u2277':'gl','\u2279':'ntgl','\u227A':'pr','\u2280':'npr','\u227B':'sc','\u2281':'nsc','\u227C':'prcue','\u22E0':'nprcue','\u227D':'sccue','\u22E1':'nsccue','\u227E':'prsim','\u227F':'scsim','\u227F\u0338':'NotSucceedsTilde','\u2282':'sub','\u2284':'nsub','\u2282\u20D2':'vnsub','\u2283':'sup','\u2285':'nsup','\u2283\u20D2':'vnsup','\u2286':'sube','\u2288':'nsube','\u2287':'supe','\u2289':'nsupe','\u228A\uFE00':'vsubne','\u228A':'subne','\u228B\uFE00':'vsupne','\u228B':'supne','\u228D':'cupdot','\u228E':'uplus','\u228F':'sqsub','\u228F\u0338':'NotSquareSubset','\u2290':'sqsup','\u2290\u0338':'NotSquareSuperset','\u2291':'sqsube','\u22E2':'nsqsube','\u2292':'sqsupe','\u22E3':'nsqsupe','\u2293':'sqcap','\u2293\uFE00':'sqcaps','\u2294':'sqcup','\u2294\uFE00':'sqcups','\u2295':'oplus','\u2296':'ominus','\u2297':'otimes','\u2298':'osol','\u2299':'odot','\u229A':'ocir','\u229B':'oast','\u229D':'odash','\u229E':'plusb','\u229F':'minusb','\u22A0':'timesb','\u22A1':'sdotb','\u22A2':'vdash','\u22AC':'nvdash','\u22A3':'dashv','\u22A4':'top','\u22A5':'bot','\u22A7':'models','\u22A8':'vDash','\u22AD':'nvDash','\u22A9':'Vdash','\u22AE':'nVdash','\u22AA':'Vvdash','\u22AB':'VDash','\u22AF':'nVDash','\u22B0':'prurel','\u22B2':'vltri','\u22EA':'nltri','\u22B3':'vrtri','\u22EB':'nrtri','\u22B4':'ltrie','\u22EC':'nltrie','\u22B4\u20D2':'nvltrie','\u22B5':'rtrie','\u22ED':'nrtrie','\u22B5\u20D2':'nvrtrie','\u22B6':'origof','\u22B7':'imof','\u22B8':'mumap','\u22B9':'hercon','\u22BA':'intcal','\u22BB':'veebar','\u22BD':'barvee','\u22BE':'angrtvb','\u22BF':'lrtri','\u22C0':'Wedge','\u22C1':'Vee','\u22C2':'xcap','\u22C3':'xcup','\u22C4':'diam','\u22C5':'sdot','\u22C6':'Star','\u22C7':'divonx','\u22C8':'bowtie','\u22C9':'ltimes','\u22CA':'rtimes','\u22CB':'lthree','\u22CC':'rthree','\u22CD':'bsime','\u22CE':'cuvee','\u22CF':'cuwed','\u22D0':'Sub','\u22D1':'Sup','\u22D2':'Cap','\u22D3':'Cup','\u22D4':'fork','\u22D5':'epar','\u22D6':'ltdot','\u22D7':'gtdot','\u22D8':'Ll','\u22D8\u0338':'nLl','\u22D9':'Gg','\u22D9\u0338':'nGg','\u22DA\uFE00':'lesg','\u22DA':'leg','\u22DB':'gel','\u22DB\uFE00':'gesl','\u22DE':'cuepr','\u22DF':'cuesc','\u22E6':'lnsim','\u22E7':'gnsim','\u22E8':'prnsim','\u22E9':'scnsim','\u22EE':'vellip','\u22EF':'ctdot','\u22F0':'utdot','\u22F1':'dtdot','\u22F2':'disin','\u22F3':'isinsv','\u22F4':'isins','\u22F5':'isindot','\u22F5\u0338':'notindot','\u22F6':'notinvc','\u22F7':'notinvb','\u22F9':'isinE','\u22F9\u0338':'notinE','\u22FA':'nisd','\u22FB':'xnis','\u22FC':'nis','\u22FD':'notnivc','\u22FE':'notnivb','\u2305':'barwed','\u2306':'Barwed','\u230C':'drcrop','\u230D':'dlcrop','\u230E':'urcrop','\u230F':'ulcrop','\u2310':'bnot','\u2312':'profline','\u2313':'profsurf','\u2315':'telrec','\u2316':'target','\u231C':'ulcorn','\u231D':'urcorn','\u231E':'dlcorn','\u231F':'drcorn','\u2322':'frown','\u2323':'smile','\u232D':'cylcty','\u232E':'profalar','\u2336':'topbot','\u233D':'ovbar','\u233F':'solbar','\u237C':'angzarr','\u23B0':'lmoust','\u23B1':'rmoust','\u23B4':'tbrk','\u23B5':'bbrk','\u23B6':'bbrktbrk','\u23DC':'OverParenthesis','\u23DD':'UnderParenthesis','\u23DE':'OverBrace','\u23DF':'UnderBrace','\u23E2':'trpezium','\u23E7':'elinters','\u2423':'blank','\u2500':'boxh','\u2502':'boxv','\u250C':'boxdr','\u2510':'boxdl','\u2514':'boxur','\u2518':'boxul','\u251C':'boxvr','\u2524':'boxvl','\u252C':'boxhd','\u2534':'boxhu','\u253C':'boxvh','\u2550':'boxH','\u2551':'boxV','\u2552':'boxdR','\u2553':'boxDr','\u2554':'boxDR','\u2555':'boxdL','\u2556':'boxDl','\u2557':'boxDL','\u2558':'boxuR','\u2559':'boxUr','\u255A':'boxUR','\u255B':'boxuL','\u255C':'boxUl','\u255D':'boxUL','\u255E':'boxvR','\u255F':'boxVr','\u2560':'boxVR','\u2561':'boxvL','\u2562':'boxVl','\u2563':'boxVL','\u2564':'boxHd','\u2565':'boxhD','\u2566':'boxHD','\u2567':'boxHu','\u2568':'boxhU','\u2569':'boxHU','\u256A':'boxvH','\u256B':'boxVh','\u256C':'boxVH','\u2580':'uhblk','\u2584':'lhblk','\u2588':'block','\u2591':'blk14','\u2592':'blk12','\u2593':'blk34','\u25A1':'squ','\u25AA':'squf','\u25AB':'EmptyVerySmallSquare','\u25AD':'rect','\u25AE':'marker','\u25B1':'fltns','\u25B3':'xutri','\u25B4':'utrif','\u25B5':'utri','\u25B8':'rtrif','\u25B9':'rtri','\u25BD':'xdtri','\u25BE':'dtrif','\u25BF':'dtri','\u25C2':'ltrif','\u25C3':'ltri','\u25CA':'loz','\u25CB':'cir','\u25EC':'tridot','\u25EF':'xcirc','\u25F8':'ultri','\u25F9':'urtri','\u25FA':'lltri','\u25FB':'EmptySmallSquare','\u25FC':'FilledSmallSquare','\u2605':'starf','\u2606':'star','\u260E':'phone','\u2640':'female','\u2642':'male','\u2660':'spades','\u2663':'clubs','\u2665':'hearts','\u2666':'diams','\u266A':'sung','\u2713':'check','\u2717':'cross','\u2720':'malt','\u2736':'sext','\u2758':'VerticalSeparator','\u27C8':'bsolhsub','\u27C9':'suphsol','\u27F5':'xlarr','\u27F6':'xrarr','\u27F7':'xharr','\u27F8':'xlArr','\u27F9':'xrArr','\u27FA':'xhArr','\u27FC':'xmap','\u27FF':'dzigrarr','\u2902':'nvlArr','\u2903':'nvrArr','\u2904':'nvHarr','\u2905':'Map','\u290C':'lbarr','\u290D':'rbarr','\u290E':'lBarr','\u290F':'rBarr','\u2910':'RBarr','\u2911':'DDotrahd','\u2912':'UpArrowBar','\u2913':'DownArrowBar','\u2916':'Rarrtl','\u2919':'latail','\u291A':'ratail','\u291B':'lAtail','\u291C':'rAtail','\u291D':'larrfs','\u291E':'rarrfs','\u291F':'larrbfs','\u2920':'rarrbfs','\u2923':'nwarhk','\u2924':'nearhk','\u2925':'searhk','\u2926':'swarhk','\u2927':'nwnear','\u2928':'toea','\u2929':'tosa','\u292A':'swnwar','\u2933':'rarrc','\u2933\u0338':'nrarrc','\u2935':'cudarrr','\u2936':'ldca','\u2937':'rdca','\u2938':'cudarrl','\u2939':'larrpl','\u293C':'curarrm','\u293D':'cularrp','\u2945':'rarrpl','\u2948':'harrcir','\u2949':'Uarrocir','\u294A':'lurdshar','\u294B':'ldrushar','\u294E':'LeftRightVector','\u294F':'RightUpDownVector','\u2950':'DownLeftRightVector','\u2951':'LeftUpDownVector','\u2952':'LeftVectorBar','\u2953':'RightVectorBar','\u2954':'RightUpVectorBar','\u2955':'RightDownVectorBar','\u2956':'DownLeftVectorBar','\u2957':'DownRightVectorBar','\u2958':'LeftUpVectorBar','\u2959':'LeftDownVectorBar','\u295A':'LeftTeeVector','\u295B':'RightTeeVector','\u295C':'RightUpTeeVector','\u295D':'RightDownTeeVector','\u295E':'DownLeftTeeVector','\u295F':'DownRightTeeVector','\u2960':'LeftUpTeeVector','\u2961':'LeftDownTeeVector','\u2962':'lHar','\u2963':'uHar','\u2964':'rHar','\u2965':'dHar','\u2966':'luruhar','\u2967':'ldrdhar','\u2968':'ruluhar','\u2969':'rdldhar','\u296A':'lharul','\u296B':'llhard','\u296C':'rharul','\u296D':'lrhard','\u296E':'udhar','\u296F':'duhar','\u2970':'RoundImplies','\u2971':'erarr','\u2972':'simrarr','\u2973':'larrsim','\u2974':'rarrsim','\u2975':'rarrap','\u2976':'ltlarr','\u2978':'gtrarr','\u2979':'subrarr','\u297B':'suplarr','\u297C':'lfisht','\u297D':'rfisht','\u297E':'ufisht','\u297F':'dfisht','\u299A':'vzigzag','\u299C':'vangrt','\u299D':'angrtvbd','\u29A4':'ange','\u29A5':'range','\u29A6':'dwangle','\u29A7':'uwangle','\u29A8':'angmsdaa','\u29A9':'angmsdab','\u29AA':'angmsdac','\u29AB':'angmsdad','\u29AC':'angmsdae','\u29AD':'angmsdaf','\u29AE':'angmsdag','\u29AF':'angmsdah','\u29B0':'bemptyv','\u29B1':'demptyv','\u29B2':'cemptyv','\u29B3':'raemptyv','\u29B4':'laemptyv','\u29B5':'ohbar','\u29B6':'omid','\u29B7':'opar','\u29B9':'operp','\u29BB':'olcross','\u29BC':'odsold','\u29BE':'olcir','\u29BF':'ofcir','\u29C0':'olt','\u29C1':'ogt','\u29C2':'cirscir','\u29C3':'cirE','\u29C4':'solb','\u29C5':'bsolb','\u29C9':'boxbox','\u29CD':'trisb','\u29CE':'rtriltri','\u29CF':'LeftTriangleBar','\u29CF\u0338':'NotLeftTriangleBar','\u29D0':'RightTriangleBar','\u29D0\u0338':'NotRightTriangleBar','\u29DC':'iinfin','\u29DD':'infintie','\u29DE':'nvinfin','\u29E3':'eparsl','\u29E4':'smeparsl','\u29E5':'eqvparsl','\u29EB':'lozf','\u29F4':'RuleDelayed','\u29F6':'dsol','\u2A00':'xodot','\u2A01':'xoplus','\u2A02':'xotime','\u2A04':'xuplus','\u2A06':'xsqcup','\u2A0D':'fpartint','\u2A10':'cirfnint','\u2A11':'awint','\u2A12':'rppolint','\u2A13':'scpolint','\u2A14':'npolint','\u2A15':'pointint','\u2A16':'quatint','\u2A17':'intlarhk','\u2A22':'pluscir','\u2A23':'plusacir','\u2A24':'simplus','\u2A25':'plusdu','\u2A26':'plussim','\u2A27':'plustwo','\u2A29':'mcomma','\u2A2A':'minusdu','\u2A2D':'loplus','\u2A2E':'roplus','\u2A2F':'Cross','\u2A30':'timesd','\u2A31':'timesbar','\u2A33':'smashp','\u2A34':'lotimes','\u2A35':'rotimes','\u2A36':'otimesas','\u2A37':'Otimes','\u2A38':'odiv','\u2A39':'triplus','\u2A3A':'triminus','\u2A3B':'tritime','\u2A3C':'iprod','\u2A3F':'amalg','\u2A40':'capdot','\u2A42':'ncup','\u2A43':'ncap','\u2A44':'capand','\u2A45':'cupor','\u2A46':'cupcap','\u2A47':'capcup','\u2A48':'cupbrcap','\u2A49':'capbrcup','\u2A4A':'cupcup','\u2A4B':'capcap','\u2A4C':'ccups','\u2A4D':'ccaps','\u2A50':'ccupssm','\u2A53':'And','\u2A54':'Or','\u2A55':'andand','\u2A56':'oror','\u2A57':'orslope','\u2A58':'andslope','\u2A5A':'andv','\u2A5B':'orv','\u2A5C':'andd','\u2A5D':'ord','\u2A5F':'wedbar','\u2A66':'sdote','\u2A6A':'simdot','\u2A6D':'congdot','\u2A6D\u0338':'ncongdot','\u2A6E':'easter','\u2A6F':'apacir','\u2A70':'apE','\u2A70\u0338':'napE','\u2A71':'eplus','\u2A72':'pluse','\u2A73':'Esim','\u2A77':'eDDot','\u2A78':'equivDD','\u2A79':'ltcir','\u2A7A':'gtcir','\u2A7B':'ltquest','\u2A7C':'gtquest','\u2A7D':'les','\u2A7D\u0338':'nles','\u2A7E':'ges','\u2A7E\u0338':'nges','\u2A7F':'lesdot','\u2A80':'gesdot','\u2A81':'lesdoto','\u2A82':'gesdoto','\u2A83':'lesdotor','\u2A84':'gesdotol','\u2A85':'lap','\u2A86':'gap','\u2A87':'lne','\u2A88':'gne','\u2A89':'lnap','\u2A8A':'gnap','\u2A8B':'lEg','\u2A8C':'gEl','\u2A8D':'lsime','\u2A8E':'gsime','\u2A8F':'lsimg','\u2A90':'gsiml','\u2A91':'lgE','\u2A92':'glE','\u2A93':'lesges','\u2A94':'gesles','\u2A95':'els','\u2A96':'egs','\u2A97':'elsdot','\u2A98':'egsdot','\u2A99':'el','\u2A9A':'eg','\u2A9D':'siml','\u2A9E':'simg','\u2A9F':'simlE','\u2AA0':'simgE','\u2AA1':'LessLess','\u2AA1\u0338':'NotNestedLessLess','\u2AA2':'GreaterGreater','\u2AA2\u0338':'NotNestedGreaterGreater','\u2AA4':'glj','\u2AA5':'gla','\u2AA6':'ltcc','\u2AA7':'gtcc','\u2AA8':'lescc','\u2AA9':'gescc','\u2AAA':'smt','\u2AAB':'lat','\u2AAC':'smte','\u2AAC\uFE00':'smtes','\u2AAD':'late','\u2AAD\uFE00':'lates','\u2AAE':'bumpE','\u2AAF':'pre','\u2AAF\u0338':'npre','\u2AB0':'sce','\u2AB0\u0338':'nsce','\u2AB3':'prE','\u2AB4':'scE','\u2AB5':'prnE','\u2AB6':'scnE','\u2AB7':'prap','\u2AB8':'scap','\u2AB9':'prnap','\u2ABA':'scnap','\u2ABB':'Pr','\u2ABC':'Sc','\u2ABD':'subdot','\u2ABE':'supdot','\u2ABF':'subplus','\u2AC0':'supplus','\u2AC1':'submult','\u2AC2':'supmult','\u2AC3':'subedot','\u2AC4':'supedot','\u2AC5':'subE','\u2AC5\u0338':'nsubE','\u2AC6':'supE','\u2AC6\u0338':'nsupE','\u2AC7':'subsim','\u2AC8':'supsim','\u2ACB\uFE00':'vsubnE','\u2ACB':'subnE','\u2ACC\uFE00':'vsupnE','\u2ACC':'supnE','\u2ACF':'csub','\u2AD0':'csup','\u2AD1':'csube','\u2AD2':'csupe','\u2AD3':'subsup','\u2AD4':'supsub','\u2AD5':'subsub','\u2AD6':'supsup','\u2AD7':'suphsub','\u2AD8':'supdsub','\u2AD9':'forkv','\u2ADA':'topfork','\u2ADB':'mlcp','\u2AE4':'Dashv','\u2AE6':'Vdashl','\u2AE7':'Barv','\u2AE8':'vBar','\u2AE9':'vBarv','\u2AEB':'Vbar','\u2AEC':'Not','\u2AED':'bNot','\u2AEE':'rnmid','\u2AEF':'cirmid','\u2AF0':'midcir','\u2AF1':'topcir','\u2AF2':'nhpar','\u2AF3':'parsim','\u2AFD':'parsl','\u2AFD\u20E5':'nparsl','\u266D':'flat','\u266E':'natur','\u266F':'sharp','\xA4':'curren','\xA2':'cent','$':'dollar','\xA3':'pound','\xA5':'yen','\u20AC':'euro','\xB9':'sup1','\xBD':'half','\u2153':'frac13','\xBC':'frac14','\u2155':'frac15','\u2159':'frac16','\u215B':'frac18','\xB2':'sup2','\u2154':'frac23','\u2156':'frac25','\xB3':'sup3','\xBE':'frac34','\u2157':'frac35','\u215C':'frac38','\u2158':'frac45','\u215A':'frac56','\u215D':'frac58','\u215E':'frac78','\uD835\uDCB6':'ascr','\uD835\uDD52':'aopf','\uD835\uDD1E':'afr','\uD835\uDD38':'Aopf','\uD835\uDD04':'Afr','\uD835\uDC9C':'Ascr','\xAA':'ordf','\xE1':'aacute','\xC1':'Aacute','\xE0':'agrave','\xC0':'Agrave','\u0103':'abreve','\u0102':'Abreve','\xE2':'acirc','\xC2':'Acirc','\xE5':'aring','\xC5':'angst','\xE4':'auml','\xC4':'Auml','\xE3':'atilde','\xC3':'Atilde','\u0105':'aogon','\u0104':'Aogon','\u0101':'amacr','\u0100':'Amacr','\xE6':'aelig','\xC6':'AElig','\uD835\uDCB7':'bscr','\uD835\uDD53':'bopf','\uD835\uDD1F':'bfr','\uD835\uDD39':'Bopf','\u212C':'Bscr','\uD835\uDD05':'Bfr','\uD835\uDD20':'cfr','\uD835\uDCB8':'cscr','\uD835\uDD54':'copf','\u212D':'Cfr','\uD835\uDC9E':'Cscr','\u2102':'Copf','\u0107':'cacute','\u0106':'Cacute','\u0109':'ccirc','\u0108':'Ccirc','\u010D':'ccaron','\u010C':'Ccaron','\u010B':'cdot','\u010A':'Cdot','\xE7':'ccedil','\xC7':'Ccedil','\u2105':'incare','\uD835\uDD21':'dfr','\u2146':'dd','\uD835\uDD55':'dopf','\uD835\uDCB9':'dscr','\uD835\uDC9F':'Dscr','\uD835\uDD07':'Dfr','\u2145':'DD','\uD835\uDD3B':'Dopf','\u010F':'dcaron','\u010E':'Dcaron','\u0111':'dstrok','\u0110':'Dstrok','\xF0':'eth','\xD0':'ETH','\u2147':'ee','\u212F':'escr','\uD835\uDD22':'efr','\uD835\uDD56':'eopf','\u2130':'Escr','\uD835\uDD08':'Efr','\uD835\uDD3C':'Eopf','\xE9':'eacute','\xC9':'Eacute','\xE8':'egrave','\xC8':'Egrave','\xEA':'ecirc','\xCA':'Ecirc','\u011B':'ecaron','\u011A':'Ecaron','\xEB':'euml','\xCB':'Euml','\u0117':'edot','\u0116':'Edot','\u0119':'eogon','\u0118':'Eogon','\u0113':'emacr','\u0112':'Emacr','\uD835\uDD23':'ffr','\uD835\uDD57':'fopf','\uD835\uDCBB':'fscr','\uD835\uDD09':'Ffr','\uD835\uDD3D':'Fopf','\u2131':'Fscr','\uFB00':'fflig','\uFB03':'ffilig','\uFB04':'ffllig','\uFB01':'filig','fj':'fjlig','\uFB02':'fllig','\u0192':'fnof','\u210A':'gscr','\uD835\uDD58':'gopf','\uD835\uDD24':'gfr','\uD835\uDCA2':'Gscr','\uD835\uDD3E':'Gopf','\uD835\uDD0A':'Gfr','\u01F5':'gacute','\u011F':'gbreve','\u011E':'Gbreve','\u011D':'gcirc','\u011C':'Gcirc','\u0121':'gdot','\u0120':'Gdot','\u0122':'Gcedil','\uD835\uDD25':'hfr','\u210E':'planckh','\uD835\uDCBD':'hscr','\uD835\uDD59':'hopf','\u210B':'Hscr','\u210C':'Hfr','\u210D':'Hopf','\u0125':'hcirc','\u0124':'Hcirc','\u210F':'hbar','\u0127':'hstrok','\u0126':'Hstrok','\uD835\uDD5A':'iopf','\uD835\uDD26':'ifr','\uD835\uDCBE':'iscr','\u2148':'ii','\uD835\uDD40':'Iopf','\u2110':'Iscr','\u2111':'Im','\xED':'iacute','\xCD':'Iacute','\xEC':'igrave','\xCC':'Igrave','\xEE':'icirc','\xCE':'Icirc','\xEF':'iuml','\xCF':'Iuml','\u0129':'itilde','\u0128':'Itilde','\u0130':'Idot','\u012F':'iogon','\u012E':'Iogon','\u012B':'imacr','\u012A':'Imacr','\u0133':'ijlig','\u0132':'IJlig','\u0131':'imath','\uD835\uDCBF':'jscr','\uD835\uDD5B':'jopf','\uD835\uDD27':'jfr','\uD835\uDCA5':'Jscr','\uD835\uDD0D':'Jfr','\uD835\uDD41':'Jopf','\u0135':'jcirc','\u0134':'Jcirc','\u0237':'jmath','\uD835\uDD5C':'kopf','\uD835\uDCC0':'kscr','\uD835\uDD28':'kfr','\uD835\uDCA6':'Kscr','\uD835\uDD42':'Kopf','\uD835\uDD0E':'Kfr','\u0137':'kcedil','\u0136':'Kcedil','\uD835\uDD29':'lfr','\uD835\uDCC1':'lscr','\u2113':'ell','\uD835\uDD5D':'lopf','\u2112':'Lscr','\uD835\uDD0F':'Lfr','\uD835\uDD43':'Lopf','\u013A':'lacute','\u0139':'Lacute','\u013E':'lcaron','\u013D':'Lcaron','\u013C':'lcedil','\u013B':'Lcedil','\u0142':'lstrok','\u0141':'Lstrok','\u0140':'lmidot','\u013F':'Lmidot','\uD835\uDD2A':'mfr','\uD835\uDD5E':'mopf','\uD835\uDCC2':'mscr','\uD835\uDD10':'Mfr','\uD835\uDD44':'Mopf','\u2133':'Mscr','\uD835\uDD2B':'nfr','\uD835\uDD5F':'nopf','\uD835\uDCC3':'nscr','\u2115':'Nopf','\uD835\uDCA9':'Nscr','\uD835\uDD11':'Nfr','\u0144':'nacute','\u0143':'Nacute','\u0148':'ncaron','\u0147':'Ncaron','\xF1':'ntilde','\xD1':'Ntilde','\u0146':'ncedil','\u0145':'Ncedil','\u2116':'numero','\u014B':'eng','\u014A':'ENG','\uD835\uDD60':'oopf','\uD835\uDD2C':'ofr','\u2134':'oscr','\uD835\uDCAA':'Oscr','\uD835\uDD12':'Ofr','\uD835\uDD46':'Oopf','\xBA':'ordm','\xF3':'oacute','\xD3':'Oacute','\xF2':'ograve','\xD2':'Ograve','\xF4':'ocirc','\xD4':'Ocirc','\xF6':'ouml','\xD6':'Ouml','\u0151':'odblac','\u0150':'Odblac','\xF5':'otilde','\xD5':'Otilde','\xF8':'oslash','\xD8':'Oslash','\u014D':'omacr','\u014C':'Omacr','\u0153':'oelig','\u0152':'OElig','\uD835\uDD2D':'pfr','\uD835\uDCC5':'pscr','\uD835\uDD61':'popf','\u2119':'Popf','\uD835\uDD13':'Pfr','\uD835\uDCAB':'Pscr','\uD835\uDD62':'qopf','\uD835\uDD2E':'qfr','\uD835\uDCC6':'qscr','\uD835\uDCAC':'Qscr','\uD835\uDD14':'Qfr','\u211A':'Qopf','\u0138':'kgreen','\uD835\uDD2F':'rfr','\uD835\uDD63':'ropf','\uD835\uDCC7':'rscr','\u211B':'Rscr','\u211C':'Re','\u211D':'Ropf','\u0155':'racute','\u0154':'Racute','\u0159':'rcaron','\u0158':'Rcaron','\u0157':'rcedil','\u0156':'Rcedil','\uD835\uDD64':'sopf','\uD835\uDCC8':'sscr','\uD835\uDD30':'sfr','\uD835\uDD4A':'Sopf','\uD835\uDD16':'Sfr','\uD835\uDCAE':'Sscr','\u24C8':'oS','\u015B':'sacute','\u015A':'Sacute','\u015D':'scirc','\u015C':'Scirc','\u0161':'scaron','\u0160':'Scaron','\u015F':'scedil','\u015E':'Scedil','\xDF':'szlig','\uD835\uDD31':'tfr','\uD835\uDCC9':'tscr','\uD835\uDD65':'topf','\uD835\uDCAF':'Tscr','\uD835\uDD17':'Tfr','\uD835\uDD4B':'Topf','\u0165':'tcaron','\u0164':'Tcaron','\u0163':'tcedil','\u0162':'Tcedil','\u2122':'trade','\u0167':'tstrok','\u0166':'Tstrok','\uD835\uDCCA':'uscr','\uD835\uDD66':'uopf','\uD835\uDD32':'ufr','\uD835\uDD4C':'Uopf','\uD835\uDD18':'Ufr','\uD835\uDCB0':'Uscr','\xFA':'uacute','\xDA':'Uacute','\xF9':'ugrave','\xD9':'Ugrave','\u016D':'ubreve','\u016C':'Ubreve','\xFB':'ucirc','\xDB':'Ucirc','\u016F':'uring','\u016E':'Uring','\xFC':'uuml','\xDC':'Uuml','\u0171':'udblac','\u0170':'Udblac','\u0169':'utilde','\u0168':'Utilde','\u0173':'uogon','\u0172':'Uogon','\u016B':'umacr','\u016A':'Umacr','\uD835\uDD33':'vfr','\uD835\uDD67':'vopf','\uD835\uDCCB':'vscr','\uD835\uDD19':'Vfr','\uD835\uDD4D':'Vopf','\uD835\uDCB1':'Vscr','\uD835\uDD68':'wopf','\uD835\uDCCC':'wscr','\uD835\uDD34':'wfr','\uD835\uDCB2':'Wscr','\uD835\uDD4E':'Wopf','\uD835\uDD1A':'Wfr','\u0175':'wcirc','\u0174':'Wcirc','\uD835\uDD35':'xfr','\uD835\uDCCD':'xscr','\uD835\uDD69':'xopf','\uD835\uDD4F':'Xopf','\uD835\uDD1B':'Xfr','\uD835\uDCB3':'Xscr','\uD835\uDD36':'yfr','\uD835\uDCCE':'yscr','\uD835\uDD6A':'yopf','\uD835\uDCB4':'Yscr','\uD835\uDD1C':'Yfr','\uD835\uDD50':'Yopf','\xFD':'yacute','\xDD':'Yacute','\u0177':'ycirc','\u0176':'Ycirc','\xFF':'yuml','\u0178':'Yuml','\uD835\uDCCF':'zscr','\uD835\uDD37':'zfr','\uD835\uDD6B':'zopf','\u2128':'Zfr','\u2124':'Zopf','\uD835\uDCB5':'Zscr','\u017A':'zacute','\u0179':'Zacute','\u017E':'zcaron','\u017D':'Zcaron','\u017C':'zdot','\u017B':'Zdot','\u01B5':'imped','\xFE':'thorn','\xDE':'THORN','\u0149':'napos','\u03B1':'alpha','\u0391':'Alpha','\u03B2':'beta','\u0392':'Beta','\u03B3':'gamma','\u0393':'Gamma','\u03B4':'delta','\u0394':'Delta','\u03B5':'epsi','\u03F5':'epsiv','\u0395':'Epsilon','\u03DD':'gammad','\u03DC':'Gammad','\u03B6':'zeta','\u0396':'Zeta','\u03B7':'eta','\u0397':'Eta','\u03B8':'theta','\u03D1':'thetav','\u0398':'Theta','\u03B9':'iota','\u0399':'Iota','\u03BA':'kappa','\u03F0':'kappav','\u039A':'Kappa','\u03BB':'lambda','\u039B':'Lambda','\u03BC':'mu','\xB5':'micro','\u039C':'Mu','\u03BD':'nu','\u039D':'Nu','\u03BE':'xi','\u039E':'Xi','\u03BF':'omicron','\u039F':'Omicron','\u03C0':'pi','\u03D6':'piv','\u03A0':'Pi','\u03C1':'rho','\u03F1':'rhov','\u03A1':'Rho','\u03C3':'sigma','\u03A3':'Sigma','\u03C2':'sigmaf','\u03C4':'tau','\u03A4':'Tau','\u03C5':'upsi','\u03A5':'Upsilon','\u03D2':'Upsi','\u03C6':'phi','\u03D5':'phiv','\u03A6':'Phi','\u03C7':'chi','\u03A7':'Chi','\u03C8':'psi','\u03A8':'Psi','\u03C9':'omega','\u03A9':'ohm','\u0430':'acy','\u0410':'Acy','\u0431':'bcy','\u0411':'Bcy','\u0432':'vcy','\u0412':'Vcy','\u0433':'gcy','\u0413':'Gcy','\u0453':'gjcy','\u0403':'GJcy','\u0434':'dcy','\u0414':'Dcy','\u0452':'djcy','\u0402':'DJcy','\u0435':'iecy','\u0415':'IEcy','\u0451':'iocy','\u0401':'IOcy','\u0454':'jukcy','\u0404':'Jukcy','\u0436':'zhcy','\u0416':'ZHcy','\u0437':'zcy','\u0417':'Zcy','\u0455':'dscy','\u0405':'DScy','\u0438':'icy','\u0418':'Icy','\u0456':'iukcy','\u0406':'Iukcy','\u0457':'yicy','\u0407':'YIcy','\u0439':'jcy','\u0419':'Jcy','\u0458':'jsercy','\u0408':'Jsercy','\u043A':'kcy','\u041A':'Kcy','\u045C':'kjcy','\u040C':'KJcy','\u043B':'lcy','\u041B':'Lcy','\u0459':'ljcy','\u0409':'LJcy','\u043C':'mcy','\u041C':'Mcy','\u043D':'ncy','\u041D':'Ncy','\u045A':'njcy','\u040A':'NJcy','\u043E':'ocy','\u041E':'Ocy','\u043F':'pcy','\u041F':'Pcy','\u0440':'rcy','\u0420':'Rcy','\u0441':'scy','\u0421':'Scy','\u0442':'tcy','\u0422':'Tcy','\u045B':'tshcy','\u040B':'TSHcy','\u0443':'ucy','\u0423':'Ucy','\u045E':'ubrcy','\u040E':'Ubrcy','\u0444':'fcy','\u0424':'Fcy','\u0445':'khcy','\u0425':'KHcy','\u0446':'tscy','\u0426':'TScy','\u0447':'chcy','\u0427':'CHcy','\u045F':'dzcy','\u040F':'DZcy','\u0448':'shcy','\u0428':'SHcy','\u0449':'shchcy','\u0429':'SHCHcy','\u044A':'hardcy','\u042A':'HARDcy','\u044B':'ycy','\u042B':'Ycy','\u044C':'softcy','\u042C':'SOFTcy','\u044D':'ecy','\u042D':'Ecy','\u044E':'yucy','\u042E':'YUcy','\u044F':'yacy','\u042F':'YAcy','\u2135':'aleph','\u2136':'beth','\u2137':'gimel','\u2138':'daleth'}; - - var regexEscape = /["&'<>`]/g; - var escapeMap = { - '"': '"', - '&': '&', - '\'': ''', - '<': '<', - // See https://mathiasbynens.be/notes/ambiguous-ampersands: in HTML, the - // following is not strictly necessary unless it’s part of a tag or an - // unquoted attribute value. We’re only escaping it to support those - // situations, and for XML support. - '>': '>', - // In Internet Explorer ≤ 8, the backtick character can be used - // to break out of (un)quoted attribute values or HTML comments. - // See http://html5sec.org/#102, http://html5sec.org/#108, and - // http://html5sec.org/#133. - '`': '`' - }; - - var regexInvalidEntity = /&#(?:[xX][^a-fA-F0-9]|[^0-9xX])/; - var regexInvalidRawCodePoint = /[\0-\x08\x0B\x0E-\x1F\x7F-\x9F\uFDD0-\uFDEF\uFFFE\uFFFF]|[\uD83F\uD87F\uD8BF\uD8FF\uD93F\uD97F\uD9BF\uD9FF\uDA3F\uDA7F\uDABF\uDAFF\uDB3F\uDB7F\uDBBF\uDBFF][\uDFFE\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/; - var regexDecode = /&(CounterClockwiseContourIntegral|DoubleLongLeftRightArrow|ClockwiseContourIntegral|NotNestedGreaterGreater|NotSquareSupersetEqual|DiacriticalDoubleAcute|NotRightTriangleEqual|NotSucceedsSlantEqual|NotPrecedesSlantEqual|CloseCurlyDoubleQuote|NegativeVeryThinSpace|DoubleContourIntegral|FilledVerySmallSquare|CapitalDifferentialD|OpenCurlyDoubleQuote|EmptyVerySmallSquare|NestedGreaterGreater|DoubleLongRightArrow|NotLeftTriangleEqual|NotGreaterSlantEqual|ReverseUpEquilibrium|DoubleLeftRightArrow|NotSquareSubsetEqual|NotDoubleVerticalBar|RightArrowLeftArrow|NotGreaterFullEqual|NotRightTriangleBar|SquareSupersetEqual|DownLeftRightVector|DoubleLongLeftArrow|leftrightsquigarrow|LeftArrowRightArrow|NegativeMediumSpace|blacktriangleright|RightDownVectorBar|PrecedesSlantEqual|RightDoubleBracket|SucceedsSlantEqual|NotLeftTriangleBar|RightTriangleEqual|SquareIntersection|RightDownTeeVector|ReverseEquilibrium|NegativeThickSpace|longleftrightarrow|Longleftrightarrow|LongLeftRightArrow|DownRightTeeVector|DownRightVectorBar|GreaterSlantEqual|SquareSubsetEqual|LeftDownVectorBar|LeftDoubleBracket|VerticalSeparator|rightleftharpoons|NotGreaterGreater|NotSquareSuperset|blacktriangleleft|blacktriangledown|NegativeThinSpace|LeftDownTeeVector|NotLessSlantEqual|leftrightharpoons|DoubleUpDownArrow|DoubleVerticalBar|LeftTriangleEqual|FilledSmallSquare|twoheadrightarrow|NotNestedLessLess|DownLeftTeeVector|DownLeftVectorBar|RightAngleBracket|NotTildeFullEqual|NotReverseElement|RightUpDownVector|DiacriticalTilde|NotSucceedsTilde|circlearrowright|NotPrecedesEqual|rightharpoondown|DoubleRightArrow|NotSucceedsEqual|NonBreakingSpace|NotRightTriangle|LessEqualGreater|RightUpTeeVector|LeftAngleBracket|GreaterFullEqual|DownArrowUpArrow|RightUpVectorBar|twoheadleftarrow|GreaterEqualLess|downharpoonright|RightTriangleBar|ntrianglerighteq|NotSupersetEqual|LeftUpDownVector|DiacriticalAcute|rightrightarrows|vartriangleright|UpArrowDownArrow|DiacriticalGrave|UnderParenthesis|EmptySmallSquare|LeftUpVectorBar|leftrightarrows|DownRightVector|downharpoonleft|trianglerighteq|ShortRightArrow|OverParenthesis|DoubleLeftArrow|DoubleDownArrow|NotSquareSubset|bigtriangledown|ntrianglelefteq|UpperRightArrow|curvearrowright|vartriangleleft|NotLeftTriangle|nleftrightarrow|LowerRightArrow|NotHumpDownHump|NotGreaterTilde|rightthreetimes|LeftUpTeeVector|NotGreaterEqual|straightepsilon|LeftTriangleBar|rightsquigarrow|ContourIntegral|rightleftarrows|CloseCurlyQuote|RightDownVector|LeftRightVector|nLeftrightarrow|leftharpoondown|circlearrowleft|SquareSuperset|OpenCurlyQuote|hookrightarrow|HorizontalLine|DiacriticalDot|NotLessGreater|ntriangleright|DoubleRightTee|InvisibleComma|InvisibleTimes|LowerLeftArrow|DownLeftVector|NotSubsetEqual|curvearrowleft|trianglelefteq|NotVerticalBar|TildeFullEqual|downdownarrows|NotGreaterLess|RightTeeVector|ZeroWidthSpace|looparrowright|LongRightArrow|doublebarwedge|ShortLeftArrow|ShortDownArrow|RightVectorBar|GreaterGreater|ReverseElement|rightharpoonup|LessSlantEqual|leftthreetimes|upharpoonright|rightarrowtail|LeftDownVector|Longrightarrow|NestedLessLess|UpperLeftArrow|nshortparallel|leftleftarrows|leftrightarrow|Leftrightarrow|LeftRightArrow|longrightarrow|upharpoonleft|RightArrowBar|ApplyFunction|LeftTeeVector|leftarrowtail|NotEqualTilde|varsubsetneqq|varsupsetneqq|RightTeeArrow|SucceedsEqual|SucceedsTilde|LeftVectorBar|SupersetEqual|hookleftarrow|DifferentialD|VerticalTilde|VeryThinSpace|blacktriangle|bigtriangleup|LessFullEqual|divideontimes|leftharpoonup|UpEquilibrium|ntriangleleft|RightTriangle|measuredangle|shortparallel|longleftarrow|Longleftarrow|LongLeftArrow|DoubleLeftTee|Poincareplane|PrecedesEqual|triangleright|DoubleUpArrow|RightUpVector|fallingdotseq|looparrowleft|PrecedesTilde|NotTildeEqual|NotTildeTilde|smallsetminus|Proportional|triangleleft|triangledown|UnderBracket|NotHumpEqual|exponentiale|ExponentialE|NotLessTilde|HilbertSpace|RightCeiling|blacklozenge|varsupsetneq|HumpDownHump|GreaterEqual|VerticalLine|LeftTeeArrow|NotLessEqual|DownTeeArrow|LeftTriangle|varsubsetneq|Intersection|NotCongruent|DownArrowBar|LeftUpVector|LeftArrowBar|risingdotseq|GreaterTilde|RoundImplies|SquareSubset|ShortUpArrow|NotSuperset|quaternions|precnapprox|backepsilon|preccurlyeq|OverBracket|blacksquare|MediumSpace|VerticalBar|circledcirc|circleddash|CircleMinus|CircleTimes|LessGreater|curlyeqprec|curlyeqsucc|diamondsuit|UpDownArrow|Updownarrow|RuleDelayed|Rrightarrow|updownarrow|RightVector|nRightarrow|nrightarrow|eqslantless|LeftCeiling|Equilibrium|SmallCircle|expectation|NotSucceeds|thickapprox|GreaterLess|SquareUnion|NotPrecedes|NotLessLess|straightphi|succnapprox|succcurlyeq|SubsetEqual|sqsupseteq|Proportion|Laplacetrf|ImaginaryI|supsetneqq|NotGreater|gtreqqless|NotElement|ThickSpace|TildeEqual|TildeTilde|Fouriertrf|rmoustache|EqualTilde|eqslantgtr|UnderBrace|LeftVector|UpArrowBar|nLeftarrow|nsubseteqq|subsetneqq|nsupseteqq|nleftarrow|succapprox|lessapprox|UpTeeArrow|upuparrows|curlywedge|lesseqqgtr|varepsilon|varnothing|RightFloor|complement|CirclePlus|sqsubseteq|Lleftarrow|circledast|RightArrow|Rightarrow|rightarrow|lmoustache|Bernoullis|precapprox|mapstoleft|mapstodown|longmapsto|dotsquare|downarrow|DoubleDot|nsubseteq|supsetneq|leftarrow|nsupseteq|subsetneq|ThinSpace|ngeqslant|subseteqq|HumpEqual|NotSubset|triangleq|NotCupCap|lesseqgtr|heartsuit|TripleDot|Leftarrow|Coproduct|Congruent|varpropto|complexes|gvertneqq|LeftArrow|LessTilde|supseteqq|MinusPlus|CircleDot|nleqslant|NotExists|gtreqless|nparallel|UnionPlus|LeftFloor|checkmark|CenterDot|centerdot|Mellintrf|gtrapprox|bigotimes|OverBrace|spadesuit|therefore|pitchfork|rationals|PlusMinus|Backslash|Therefore|DownBreve|backsimeq|backprime|DownArrow|nshortmid|Downarrow|lvertneqq|eqvparsl|imagline|imagpart|infintie|integers|Integral|intercal|LessLess|Uarrocir|intlarhk|sqsupset|angmsdaf|sqsubset|llcorner|vartheta|cupbrcap|lnapprox|Superset|SuchThat|succnsim|succneqq|angmsdag|biguplus|curlyvee|trpezium|Succeeds|NotTilde|bigwedge|angmsdah|angrtvbd|triminus|cwconint|fpartint|lrcorner|smeparsl|subseteq|urcorner|lurdshar|laemptyv|DDotrahd|approxeq|ldrushar|awconint|mapstoup|backcong|shortmid|triangle|geqslant|gesdotol|timesbar|circledR|circledS|setminus|multimap|naturals|scpolint|ncongdot|RightTee|boxminus|gnapprox|boxtimes|andslope|thicksim|angmsdaa|varsigma|cirfnint|rtriltri|angmsdab|rppolint|angmsdac|barwedge|drbkarow|clubsuit|thetasym|bsolhsub|capbrcup|dzigrarr|doteqdot|DotEqual|dotminus|UnderBar|NotEqual|realpart|otimesas|ulcorner|hksearow|hkswarow|parallel|PartialD|elinters|emptyset|plusacir|bbrktbrk|angmsdad|pointint|bigoplus|angmsdae|Precedes|bigsqcup|varkappa|notindot|supseteq|precneqq|precnsim|profalar|profline|profsurf|leqslant|lesdotor|raemptyv|subplus|notnivb|notnivc|subrarr|zigrarr|vzigzag|submult|subedot|Element|between|cirscir|larrbfs|larrsim|lotimes|lbrksld|lbrkslu|lozenge|ldrdhar|dbkarow|bigcirc|epsilon|simrarr|simplus|ltquest|Epsilon|luruhar|gtquest|maltese|npolint|eqcolon|npreceq|bigodot|ddagger|gtrless|bnequiv|harrcir|ddotseq|equivDD|backsim|demptyv|nsqsube|nsqsupe|Upsilon|nsubset|upsilon|minusdu|nsucceq|swarrow|nsupset|coloneq|searrow|boxplus|napprox|natural|asympeq|alefsym|congdot|nearrow|bigstar|diamond|supplus|tritime|LeftTee|nvinfin|triplus|NewLine|nvltrie|nvrtrie|nwarrow|nexists|Diamond|ruluhar|Implies|supmult|angzarr|suplarr|suphsub|questeq|because|digamma|Because|olcross|bemptyv|omicron|Omicron|rotimes|NoBreak|intprod|angrtvb|orderof|uwangle|suphsol|lesdoto|orslope|DownTee|realine|cudarrl|rdldhar|OverBar|supedot|lessdot|supdsub|topfork|succsim|rbrkslu|rbrksld|pertenk|cudarrr|isindot|planckh|lessgtr|pluscir|gesdoto|plussim|plustwo|lesssim|cularrp|rarrsim|Cayleys|notinva|notinvb|notinvc|UpArrow|Uparrow|uparrow|NotLess|dwangle|precsim|Product|curarrm|Cconint|dotplus|rarrbfs|ccupssm|Cedilla|cemptyv|notniva|quatint|frac35|frac38|frac45|frac56|frac58|frac78|tridot|xoplus|gacute|gammad|Gammad|lfisht|lfloor|bigcup|sqsupe|gbreve|Gbreve|lharul|sqsube|sqcups|Gcedil|apacir|llhard|lmidot|Lmidot|lmoust|andand|sqcaps|approx|Abreve|spades|circeq|tprime|divide|topcir|Assign|topbot|gesdot|divonx|xuplus|timesd|gesles|atilde|solbar|SOFTcy|loplus|timesb|lowast|lowbar|dlcorn|dlcrop|softcy|dollar|lparlt|thksim|lrhard|Atilde|lsaquo|smashp|bigvee|thinsp|wreath|bkarow|lsquor|lstrok|Lstrok|lthree|ltimes|ltlarr|DotDot|simdot|ltrPar|weierp|xsqcup|angmsd|sigmav|sigmaf|zeetrf|Zcaron|zcaron|mapsto|vsupne|thetav|cirmid|marker|mcomma|Zacute|vsubnE|there4|gtlPar|vsubne|bottom|gtrarr|SHCHcy|shchcy|midast|midcir|middot|minusb|minusd|gtrdot|bowtie|sfrown|mnplus|models|colone|seswar|Colone|mstpos|searhk|gtrsim|nacute|Nacute|boxbox|telrec|hairsp|Tcedil|nbumpe|scnsim|ncaron|Ncaron|ncedil|Ncedil|hamilt|Scedil|nearhk|hardcy|HARDcy|tcedil|Tcaron|commat|nequiv|nesear|tcaron|target|hearts|nexist|varrho|scedil|Scaron|scaron|hellip|Sacute|sacute|hercon|swnwar|compfn|rtimes|rthree|rsquor|rsaquo|zacute|wedgeq|homtht|barvee|barwed|Barwed|rpargt|horbar|conint|swarhk|roplus|nltrie|hslash|hstrok|Hstrok|rmoust|Conint|bprime|hybull|hyphen|iacute|Iacute|supsup|supsub|supsim|varphi|coprod|brvbar|agrave|Supset|supset|igrave|Igrave|notinE|Agrave|iiiint|iinfin|copysr|wedbar|Verbar|vangrt|becaus|incare|verbar|inodot|bullet|drcorn|intcal|drcrop|cularr|vellip|Utilde|bumpeq|cupcap|dstrok|Dstrok|CupCap|cupcup|cupdot|eacute|Eacute|supdot|iquest|easter|ecaron|Ecaron|ecolon|isinsv|utilde|itilde|Itilde|curarr|succeq|Bumpeq|cacute|ulcrop|nparsl|Cacute|nprcue|egrave|Egrave|nrarrc|nrarrw|subsup|subsub|nrtrie|jsercy|nsccue|Jsercy|kappav|kcedil|Kcedil|subsim|ulcorn|nsimeq|egsdot|veebar|kgreen|capand|elsdot|Subset|subset|curren|aacute|lacute|Lacute|emptyv|ntilde|Ntilde|lagran|lambda|Lambda|capcap|Ugrave|langle|subdot|emsp13|numero|emsp14|nvdash|nvDash|nVdash|nVDash|ugrave|ufisht|nvHarr|larrfs|nvlArr|larrhk|larrlp|larrpl|nvrArr|Udblac|nwarhk|larrtl|nwnear|oacute|Oacute|latail|lAtail|sstarf|lbrace|odblac|Odblac|lbrack|udblac|odsold|eparsl|lcaron|Lcaron|ograve|Ograve|lcedil|Lcedil|Aacute|ssmile|ssetmn|squarf|ldquor|capcup|ominus|cylcty|rharul|eqcirc|dagger|rfloor|rfisht|Dagger|daleth|equals|origof|capdot|equest|dcaron|Dcaron|rdquor|oslash|Oslash|otilde|Otilde|otimes|Otimes|urcrop|Ubreve|ubreve|Yacute|Uacute|uacute|Rcedil|rcedil|urcorn|parsim|Rcaron|Vdashl|rcaron|Tstrok|percnt|period|permil|Exists|yacute|rbrack|rbrace|phmmat|ccaron|Ccaron|planck|ccedil|plankv|tstrok|female|plusdo|plusdu|ffilig|plusmn|ffllig|Ccedil|rAtail|dfisht|bernou|ratail|Rarrtl|rarrtl|angsph|rarrpl|rarrlp|rarrhk|xwedge|xotime|forall|ForAll|Vvdash|vsupnE|preceq|bigcap|frac12|frac13|frac14|primes|rarrfs|prnsim|frac15|Square|frac16|square|lesdot|frac18|frac23|propto|prurel|rarrap|rangle|puncsp|frac25|Racute|qprime|racute|lesges|frac34|abreve|AElig|eqsim|utdot|setmn|urtri|Equal|Uring|seArr|uring|searr|dashv|Dashv|mumap|nabla|iogon|Iogon|sdote|sdotb|scsim|napid|napos|equiv|natur|Acirc|dblac|erarr|nbump|iprod|erDot|ucirc|awint|esdot|angrt|ncong|isinE|scnap|Scirc|scirc|ndash|isins|Ubrcy|nearr|neArr|isinv|nedot|ubrcy|acute|Ycirc|iukcy|Iukcy|xutri|nesim|caret|jcirc|Jcirc|caron|twixt|ddarr|sccue|exist|jmath|sbquo|ngeqq|angst|ccaps|lceil|ngsim|UpTee|delta|Delta|rtrif|nharr|nhArr|nhpar|rtrie|jukcy|Jukcy|kappa|rsquo|Kappa|nlarr|nlArr|TSHcy|rrarr|aogon|Aogon|fflig|xrarr|tshcy|ccirc|nleqq|filig|upsih|nless|dharl|nlsim|fjlig|ropar|nltri|dharr|robrk|roarr|fllig|fltns|roang|rnmid|subnE|subne|lAarr|trisb|Ccirc|acirc|ccups|blank|VDash|forkv|Vdash|langd|cedil|blk12|blk14|laquo|strns|diams|notin|vDash|larrb|blk34|block|disin|uplus|vdash|vBarv|aelig|starf|Wedge|check|xrArr|lates|lbarr|lBarr|notni|lbbrk|bcong|frasl|lbrke|frown|vrtri|vprop|vnsup|gamma|Gamma|wedge|xodot|bdquo|srarr|doteq|ldquo|boxdl|boxdL|gcirc|Gcirc|boxDl|boxDL|boxdr|boxdR|boxDr|TRADE|trade|rlhar|boxDR|vnsub|npart|vltri|rlarr|boxhd|boxhD|nprec|gescc|nrarr|nrArr|boxHd|boxHD|boxhu|boxhU|nrtri|boxHu|clubs|boxHU|times|colon|Colon|gimel|xlArr|Tilde|nsime|tilde|nsmid|nspar|THORN|thorn|xlarr|nsube|nsubE|thkap|xhArr|comma|nsucc|boxul|boxuL|nsupe|nsupE|gneqq|gnsim|boxUl|boxUL|grave|boxur|boxuR|boxUr|boxUR|lescc|angle|bepsi|boxvh|varpi|boxvH|numsp|Theta|gsime|gsiml|theta|boxVh|boxVH|boxvl|gtcir|gtdot|boxvL|boxVl|boxVL|crarr|cross|Cross|nvsim|boxvr|nwarr|nwArr|sqsup|dtdot|Uogon|lhard|lharu|dtrif|ocirc|Ocirc|lhblk|duarr|odash|sqsub|Hacek|sqcup|llarr|duhar|oelig|OElig|ofcir|boxvR|uogon|lltri|boxVr|csube|uuarr|ohbar|csupe|ctdot|olarr|olcir|harrw|oline|sqcap|omacr|Omacr|omega|Omega|boxVR|aleph|lneqq|lnsim|loang|loarr|rharu|lobrk|hcirc|operp|oplus|rhard|Hcirc|orarr|Union|order|ecirc|Ecirc|cuepr|szlig|cuesc|breve|reals|eDDot|Breve|hoarr|lopar|utrif|rdquo|Umacr|umacr|efDot|swArr|ultri|alpha|rceil|ovbar|swarr|Wcirc|wcirc|smtes|smile|bsemi|lrarr|aring|parsl|lrhar|bsime|uhblk|lrtri|cupor|Aring|uharr|uharl|slarr|rbrke|bsolb|lsime|rbbrk|RBarr|lsimg|phone|rBarr|rbarr|icirc|lsquo|Icirc|emacr|Emacr|ratio|simne|plusb|simlE|simgE|simeq|pluse|ltcir|ltdot|empty|xharr|xdtri|iexcl|Alpha|ltrie|rarrw|pound|ltrif|xcirc|bumpe|prcue|bumpE|asymp|amacr|cuvee|Sigma|sigma|iiint|udhar|iiota|ijlig|IJlig|supnE|imacr|Imacr|prime|Prime|image|prnap|eogon|Eogon|rarrc|mdash|mDDot|cuwed|imath|supne|imped|Amacr|udarr|prsim|micro|rarrb|cwint|raquo|infin|eplus|range|rangd|Ucirc|radic|minus|amalg|veeeq|rAarr|epsiv|ycirc|quest|sharp|quot|zwnj|Qscr|race|qscr|Qopf|qopf|qint|rang|Rang|Zscr|zscr|Zopf|zopf|rarr|rArr|Rarr|Pscr|pscr|prop|prod|prnE|prec|ZHcy|zhcy|prap|Zeta|zeta|Popf|popf|Zdot|plus|zdot|Yuml|yuml|phiv|YUcy|yucy|Yscr|yscr|perp|Yopf|yopf|part|para|YIcy|Ouml|rcub|yicy|YAcy|rdca|ouml|osol|Oscr|rdsh|yacy|real|oscr|xvee|andd|rect|andv|Xscr|oror|ordm|ordf|xscr|ange|aopf|Aopf|rHar|Xopf|opar|Oopf|xopf|xnis|rhov|oopf|omid|xmap|oint|apid|apos|ogon|ascr|Ascr|odot|odiv|xcup|xcap|ocir|oast|nvlt|nvle|nvgt|nvge|nvap|Wscr|wscr|auml|ntlg|ntgl|nsup|nsub|nsim|Nscr|nscr|nsce|Wopf|ring|npre|wopf|npar|Auml|Barv|bbrk|Nopf|nopf|nmid|nLtv|beta|ropf|Ropf|Beta|beth|nles|rpar|nleq|bnot|bNot|nldr|NJcy|rscr|Rscr|Vscr|vscr|rsqb|njcy|bopf|nisd|Bopf|rtri|Vopf|nGtv|ngtr|vopf|boxh|boxH|boxv|nges|ngeq|boxV|bscr|scap|Bscr|bsim|Vert|vert|bsol|bull|bump|caps|cdot|ncup|scnE|ncap|nbsp|napE|Cdot|cent|sdot|Vbar|nang|vBar|chcy|Mscr|mscr|sect|semi|CHcy|Mopf|mopf|sext|circ|cire|mldr|mlcp|cirE|comp|shcy|SHcy|vArr|varr|cong|copf|Copf|copy|COPY|malt|male|macr|lvnE|cscr|ltri|sime|ltcc|simg|Cscr|siml|csub|Uuml|lsqb|lsim|uuml|csup|Lscr|lscr|utri|smid|lpar|cups|smte|lozf|darr|Lopf|Uscr|solb|lopf|sopf|Sopf|lneq|uscr|spar|dArr|lnap|Darr|dash|Sqrt|LJcy|ljcy|lHar|dHar|Upsi|upsi|diam|lesg|djcy|DJcy|leqq|dopf|Dopf|dscr|Dscr|dscy|ldsh|ldca|squf|DScy|sscr|Sscr|dsol|lcub|late|star|Star|Uopf|Larr|lArr|larr|uopf|dtri|dzcy|sube|subE|Lang|lang|Kscr|kscr|Kopf|kopf|KJcy|kjcy|KHcy|khcy|DZcy|ecir|edot|eDot|Jscr|jscr|succ|Jopf|jopf|Edot|uHar|emsp|ensp|Iuml|iuml|eopf|isin|Iscr|iscr|Eopf|epar|sung|epsi|escr|sup1|sup2|sup3|Iota|iota|supe|supE|Iopf|iopf|IOcy|iocy|Escr|esim|Esim|imof|Uarr|QUOT|uArr|uarr|euml|IEcy|iecy|Idot|Euml|euro|excl|Hscr|hscr|Hopf|hopf|TScy|tscy|Tscr|hbar|tscr|flat|tbrk|fnof|hArr|harr|half|fopf|Fopf|tdot|gvnE|fork|trie|gtcc|fscr|Fscr|gdot|gsim|Gscr|gscr|Gopf|gopf|gneq|Gdot|tosa|gnap|Topf|topf|geqq|toea|GJcy|gjcy|tint|gesl|mid|Sfr|ggg|top|ges|gla|glE|glj|geq|gne|gEl|gel|gnE|Gcy|gcy|gap|Tfr|tfr|Tcy|tcy|Hat|Tau|Ffr|tau|Tab|hfr|Hfr|ffr|Fcy|fcy|icy|Icy|iff|ETH|eth|ifr|Ifr|Eta|eta|int|Int|Sup|sup|ucy|Ucy|Sum|sum|jcy|ENG|ufr|Ufr|eng|Jcy|jfr|els|ell|egs|Efr|efr|Jfr|uml|kcy|Kcy|Ecy|ecy|kfr|Kfr|lap|Sub|sub|lat|lcy|Lcy|leg|Dot|dot|lEg|leq|les|squ|div|die|lfr|Lfr|lgE|Dfr|dfr|Del|deg|Dcy|dcy|lne|lnE|sol|loz|smt|Cup|lrm|cup|lsh|Lsh|sim|shy|map|Map|mcy|Mcy|mfr|Mfr|mho|gfr|Gfr|sfr|cir|Chi|chi|nap|Cfr|vcy|Vcy|cfr|Scy|scy|ncy|Ncy|vee|Vee|Cap|cap|nfr|scE|sce|Nfr|nge|ngE|nGg|vfr|Vfr|ngt|bot|nGt|nis|niv|Rsh|rsh|nle|nlE|bne|Bfr|bfr|nLl|nlt|nLt|Bcy|bcy|not|Not|rlm|wfr|Wfr|npr|nsc|num|ocy|ast|Ocy|ofr|xfr|Xfr|Ofr|ogt|ohm|apE|olt|Rho|ape|rho|Rfr|rfr|ord|REG|ang|reg|orv|And|and|AMP|Rcy|amp|Afr|ycy|Ycy|yen|yfr|Yfr|rcy|par|pcy|Pcy|pfr|Pfr|phi|Phi|afr|Acy|acy|zcy|Zcy|piv|acE|acd|zfr|Zfr|pre|prE|psi|Psi|qfr|Qfr|zwj|Or|ge|Gg|gt|gg|el|oS|lt|Lt|LT|Re|lg|gl|eg|ne|Im|it|le|DD|wp|wr|nu|Nu|dd|lE|Sc|sc|pi|Pi|ee|af|ll|Ll|rx|gE|xi|pm|Xi|ic|pr|Pr|in|ni|mp|mu|ac|Mu|or|ap|Gt|GT|ii);|&(Aacute|Agrave|Atilde|Ccedil|Eacute|Egrave|Iacute|Igrave|Ntilde|Oacute|Ograve|Oslash|Otilde|Uacute|Ugrave|Yacute|aacute|agrave|atilde|brvbar|ccedil|curren|divide|eacute|egrave|frac12|frac14|frac34|iacute|igrave|iquest|middot|ntilde|oacute|ograve|oslash|otilde|plusmn|uacute|ugrave|yacute|AElig|Acirc|Aring|Ecirc|Icirc|Ocirc|THORN|Ucirc|acirc|acute|aelig|aring|cedil|ecirc|icirc|iexcl|laquo|micro|ocirc|pound|raquo|szlig|thorn|times|ucirc|Auml|COPY|Euml|Iuml|Ouml|QUOT|Uuml|auml|cent|copy|euml|iuml|macr|nbsp|ordf|ordm|ouml|para|quot|sect|sup1|sup2|sup3|uuml|yuml|AMP|ETH|REG|amp|deg|eth|not|reg|shy|uml|yen|GT|LT|gt|lt)(?!;)([=a-zA-Z0-9]?)|&#([0-9]+)(;?)|&#[xX]([a-fA-F0-9]+)(;?)|&([0-9a-zA-Z]+)/g; - var decodeMap = {'aacute':'\xE1','Aacute':'\xC1','abreve':'\u0103','Abreve':'\u0102','ac':'\u223E','acd':'\u223F','acE':'\u223E\u0333','acirc':'\xE2','Acirc':'\xC2','acute':'\xB4','acy':'\u0430','Acy':'\u0410','aelig':'\xE6','AElig':'\xC6','af':'\u2061','afr':'\uD835\uDD1E','Afr':'\uD835\uDD04','agrave':'\xE0','Agrave':'\xC0','alefsym':'\u2135','aleph':'\u2135','alpha':'\u03B1','Alpha':'\u0391','amacr':'\u0101','Amacr':'\u0100','amalg':'\u2A3F','amp':'&','AMP':'&','and':'\u2227','And':'\u2A53','andand':'\u2A55','andd':'\u2A5C','andslope':'\u2A58','andv':'\u2A5A','ang':'\u2220','ange':'\u29A4','angle':'\u2220','angmsd':'\u2221','angmsdaa':'\u29A8','angmsdab':'\u29A9','angmsdac':'\u29AA','angmsdad':'\u29AB','angmsdae':'\u29AC','angmsdaf':'\u29AD','angmsdag':'\u29AE','angmsdah':'\u29AF','angrt':'\u221F','angrtvb':'\u22BE','angrtvbd':'\u299D','angsph':'\u2222','angst':'\xC5','angzarr':'\u237C','aogon':'\u0105','Aogon':'\u0104','aopf':'\uD835\uDD52','Aopf':'\uD835\uDD38','ap':'\u2248','apacir':'\u2A6F','ape':'\u224A','apE':'\u2A70','apid':'\u224B','apos':'\'','ApplyFunction':'\u2061','approx':'\u2248','approxeq':'\u224A','aring':'\xE5','Aring':'\xC5','ascr':'\uD835\uDCB6','Ascr':'\uD835\uDC9C','Assign':'\u2254','ast':'*','asymp':'\u2248','asympeq':'\u224D','atilde':'\xE3','Atilde':'\xC3','auml':'\xE4','Auml':'\xC4','awconint':'\u2233','awint':'\u2A11','backcong':'\u224C','backepsilon':'\u03F6','backprime':'\u2035','backsim':'\u223D','backsimeq':'\u22CD','Backslash':'\u2216','Barv':'\u2AE7','barvee':'\u22BD','barwed':'\u2305','Barwed':'\u2306','barwedge':'\u2305','bbrk':'\u23B5','bbrktbrk':'\u23B6','bcong':'\u224C','bcy':'\u0431','Bcy':'\u0411','bdquo':'\u201E','becaus':'\u2235','because':'\u2235','Because':'\u2235','bemptyv':'\u29B0','bepsi':'\u03F6','bernou':'\u212C','Bernoullis':'\u212C','beta':'\u03B2','Beta':'\u0392','beth':'\u2136','between':'\u226C','bfr':'\uD835\uDD1F','Bfr':'\uD835\uDD05','bigcap':'\u22C2','bigcirc':'\u25EF','bigcup':'\u22C3','bigodot':'\u2A00','bigoplus':'\u2A01','bigotimes':'\u2A02','bigsqcup':'\u2A06','bigstar':'\u2605','bigtriangledown':'\u25BD','bigtriangleup':'\u25B3','biguplus':'\u2A04','bigvee':'\u22C1','bigwedge':'\u22C0','bkarow':'\u290D','blacklozenge':'\u29EB','blacksquare':'\u25AA','blacktriangle':'\u25B4','blacktriangledown':'\u25BE','blacktriangleleft':'\u25C2','blacktriangleright':'\u25B8','blank':'\u2423','blk12':'\u2592','blk14':'\u2591','blk34':'\u2593','block':'\u2588','bne':'=\u20E5','bnequiv':'\u2261\u20E5','bnot':'\u2310','bNot':'\u2AED','bopf':'\uD835\uDD53','Bopf':'\uD835\uDD39','bot':'\u22A5','bottom':'\u22A5','bowtie':'\u22C8','boxbox':'\u29C9','boxdl':'\u2510','boxdL':'\u2555','boxDl':'\u2556','boxDL':'\u2557','boxdr':'\u250C','boxdR':'\u2552','boxDr':'\u2553','boxDR':'\u2554','boxh':'\u2500','boxH':'\u2550','boxhd':'\u252C','boxhD':'\u2565','boxHd':'\u2564','boxHD':'\u2566','boxhu':'\u2534','boxhU':'\u2568','boxHu':'\u2567','boxHU':'\u2569','boxminus':'\u229F','boxplus':'\u229E','boxtimes':'\u22A0','boxul':'\u2518','boxuL':'\u255B','boxUl':'\u255C','boxUL':'\u255D','boxur':'\u2514','boxuR':'\u2558','boxUr':'\u2559','boxUR':'\u255A','boxv':'\u2502','boxV':'\u2551','boxvh':'\u253C','boxvH':'\u256A','boxVh':'\u256B','boxVH':'\u256C','boxvl':'\u2524','boxvL':'\u2561','boxVl':'\u2562','boxVL':'\u2563','boxvr':'\u251C','boxvR':'\u255E','boxVr':'\u255F','boxVR':'\u2560','bprime':'\u2035','breve':'\u02D8','Breve':'\u02D8','brvbar':'\xA6','bscr':'\uD835\uDCB7','Bscr':'\u212C','bsemi':'\u204F','bsim':'\u223D','bsime':'\u22CD','bsol':'\\','bsolb':'\u29C5','bsolhsub':'\u27C8','bull':'\u2022','bullet':'\u2022','bump':'\u224E','bumpe':'\u224F','bumpE':'\u2AAE','bumpeq':'\u224F','Bumpeq':'\u224E','cacute':'\u0107','Cacute':'\u0106','cap':'\u2229','Cap':'\u22D2','capand':'\u2A44','capbrcup':'\u2A49','capcap':'\u2A4B','capcup':'\u2A47','capdot':'\u2A40','CapitalDifferentialD':'\u2145','caps':'\u2229\uFE00','caret':'\u2041','caron':'\u02C7','Cayleys':'\u212D','ccaps':'\u2A4D','ccaron':'\u010D','Ccaron':'\u010C','ccedil':'\xE7','Ccedil':'\xC7','ccirc':'\u0109','Ccirc':'\u0108','Cconint':'\u2230','ccups':'\u2A4C','ccupssm':'\u2A50','cdot':'\u010B','Cdot':'\u010A','cedil':'\xB8','Cedilla':'\xB8','cemptyv':'\u29B2','cent':'\xA2','centerdot':'\xB7','CenterDot':'\xB7','cfr':'\uD835\uDD20','Cfr':'\u212D','chcy':'\u0447','CHcy':'\u0427','check':'\u2713','checkmark':'\u2713','chi':'\u03C7','Chi':'\u03A7','cir':'\u25CB','circ':'\u02C6','circeq':'\u2257','circlearrowleft':'\u21BA','circlearrowright':'\u21BB','circledast':'\u229B','circledcirc':'\u229A','circleddash':'\u229D','CircleDot':'\u2299','circledR':'\xAE','circledS':'\u24C8','CircleMinus':'\u2296','CirclePlus':'\u2295','CircleTimes':'\u2297','cire':'\u2257','cirE':'\u29C3','cirfnint':'\u2A10','cirmid':'\u2AEF','cirscir':'\u29C2','ClockwiseContourIntegral':'\u2232','CloseCurlyDoubleQuote':'\u201D','CloseCurlyQuote':'\u2019','clubs':'\u2663','clubsuit':'\u2663','colon':':','Colon':'\u2237','colone':'\u2254','Colone':'\u2A74','coloneq':'\u2254','comma':',','commat':'@','comp':'\u2201','compfn':'\u2218','complement':'\u2201','complexes':'\u2102','cong':'\u2245','congdot':'\u2A6D','Congruent':'\u2261','conint':'\u222E','Conint':'\u222F','ContourIntegral':'\u222E','copf':'\uD835\uDD54','Copf':'\u2102','coprod':'\u2210','Coproduct':'\u2210','copy':'\xA9','COPY':'\xA9','copysr':'\u2117','CounterClockwiseContourIntegral':'\u2233','crarr':'\u21B5','cross':'\u2717','Cross':'\u2A2F','cscr':'\uD835\uDCB8','Cscr':'\uD835\uDC9E','csub':'\u2ACF','csube':'\u2AD1','csup':'\u2AD0','csupe':'\u2AD2','ctdot':'\u22EF','cudarrl':'\u2938','cudarrr':'\u2935','cuepr':'\u22DE','cuesc':'\u22DF','cularr':'\u21B6','cularrp':'\u293D','cup':'\u222A','Cup':'\u22D3','cupbrcap':'\u2A48','cupcap':'\u2A46','CupCap':'\u224D','cupcup':'\u2A4A','cupdot':'\u228D','cupor':'\u2A45','cups':'\u222A\uFE00','curarr':'\u21B7','curarrm':'\u293C','curlyeqprec':'\u22DE','curlyeqsucc':'\u22DF','curlyvee':'\u22CE','curlywedge':'\u22CF','curren':'\xA4','curvearrowleft':'\u21B6','curvearrowright':'\u21B7','cuvee':'\u22CE','cuwed':'\u22CF','cwconint':'\u2232','cwint':'\u2231','cylcty':'\u232D','dagger':'\u2020','Dagger':'\u2021','daleth':'\u2138','darr':'\u2193','dArr':'\u21D3','Darr':'\u21A1','dash':'\u2010','dashv':'\u22A3','Dashv':'\u2AE4','dbkarow':'\u290F','dblac':'\u02DD','dcaron':'\u010F','Dcaron':'\u010E','dcy':'\u0434','Dcy':'\u0414','dd':'\u2146','DD':'\u2145','ddagger':'\u2021','ddarr':'\u21CA','DDotrahd':'\u2911','ddotseq':'\u2A77','deg':'\xB0','Del':'\u2207','delta':'\u03B4','Delta':'\u0394','demptyv':'\u29B1','dfisht':'\u297F','dfr':'\uD835\uDD21','Dfr':'\uD835\uDD07','dHar':'\u2965','dharl':'\u21C3','dharr':'\u21C2','DiacriticalAcute':'\xB4','DiacriticalDot':'\u02D9','DiacriticalDoubleAcute':'\u02DD','DiacriticalGrave':'`','DiacriticalTilde':'\u02DC','diam':'\u22C4','diamond':'\u22C4','Diamond':'\u22C4','diamondsuit':'\u2666','diams':'\u2666','die':'\xA8','DifferentialD':'\u2146','digamma':'\u03DD','disin':'\u22F2','div':'\xF7','divide':'\xF7','divideontimes':'\u22C7','divonx':'\u22C7','djcy':'\u0452','DJcy':'\u0402','dlcorn':'\u231E','dlcrop':'\u230D','dollar':'$','dopf':'\uD835\uDD55','Dopf':'\uD835\uDD3B','dot':'\u02D9','Dot':'\xA8','DotDot':'\u20DC','doteq':'\u2250','doteqdot':'\u2251','DotEqual':'\u2250','dotminus':'\u2238','dotplus':'\u2214','dotsquare':'\u22A1','doublebarwedge':'\u2306','DoubleContourIntegral':'\u222F','DoubleDot':'\xA8','DoubleDownArrow':'\u21D3','DoubleLeftArrow':'\u21D0','DoubleLeftRightArrow':'\u21D4','DoubleLeftTee':'\u2AE4','DoubleLongLeftArrow':'\u27F8','DoubleLongLeftRightArrow':'\u27FA','DoubleLongRightArrow':'\u27F9','DoubleRightArrow':'\u21D2','DoubleRightTee':'\u22A8','DoubleUpArrow':'\u21D1','DoubleUpDownArrow':'\u21D5','DoubleVerticalBar':'\u2225','downarrow':'\u2193','Downarrow':'\u21D3','DownArrow':'\u2193','DownArrowBar':'\u2913','DownArrowUpArrow':'\u21F5','DownBreve':'\u0311','downdownarrows':'\u21CA','downharpoonleft':'\u21C3','downharpoonright':'\u21C2','DownLeftRightVector':'\u2950','DownLeftTeeVector':'\u295E','DownLeftVector':'\u21BD','DownLeftVectorBar':'\u2956','DownRightTeeVector':'\u295F','DownRightVector':'\u21C1','DownRightVectorBar':'\u2957','DownTee':'\u22A4','DownTeeArrow':'\u21A7','drbkarow':'\u2910','drcorn':'\u231F','drcrop':'\u230C','dscr':'\uD835\uDCB9','Dscr':'\uD835\uDC9F','dscy':'\u0455','DScy':'\u0405','dsol':'\u29F6','dstrok':'\u0111','Dstrok':'\u0110','dtdot':'\u22F1','dtri':'\u25BF','dtrif':'\u25BE','duarr':'\u21F5','duhar':'\u296F','dwangle':'\u29A6','dzcy':'\u045F','DZcy':'\u040F','dzigrarr':'\u27FF','eacute':'\xE9','Eacute':'\xC9','easter':'\u2A6E','ecaron':'\u011B','Ecaron':'\u011A','ecir':'\u2256','ecirc':'\xEA','Ecirc':'\xCA','ecolon':'\u2255','ecy':'\u044D','Ecy':'\u042D','eDDot':'\u2A77','edot':'\u0117','eDot':'\u2251','Edot':'\u0116','ee':'\u2147','efDot':'\u2252','efr':'\uD835\uDD22','Efr':'\uD835\uDD08','eg':'\u2A9A','egrave':'\xE8','Egrave':'\xC8','egs':'\u2A96','egsdot':'\u2A98','el':'\u2A99','Element':'\u2208','elinters':'\u23E7','ell':'\u2113','els':'\u2A95','elsdot':'\u2A97','emacr':'\u0113','Emacr':'\u0112','empty':'\u2205','emptyset':'\u2205','EmptySmallSquare':'\u25FB','emptyv':'\u2205','EmptyVerySmallSquare':'\u25AB','emsp':'\u2003','emsp13':'\u2004','emsp14':'\u2005','eng':'\u014B','ENG':'\u014A','ensp':'\u2002','eogon':'\u0119','Eogon':'\u0118','eopf':'\uD835\uDD56','Eopf':'\uD835\uDD3C','epar':'\u22D5','eparsl':'\u29E3','eplus':'\u2A71','epsi':'\u03B5','epsilon':'\u03B5','Epsilon':'\u0395','epsiv':'\u03F5','eqcirc':'\u2256','eqcolon':'\u2255','eqsim':'\u2242','eqslantgtr':'\u2A96','eqslantless':'\u2A95','Equal':'\u2A75','equals':'=','EqualTilde':'\u2242','equest':'\u225F','Equilibrium':'\u21CC','equiv':'\u2261','equivDD':'\u2A78','eqvparsl':'\u29E5','erarr':'\u2971','erDot':'\u2253','escr':'\u212F','Escr':'\u2130','esdot':'\u2250','esim':'\u2242','Esim':'\u2A73','eta':'\u03B7','Eta':'\u0397','eth':'\xF0','ETH':'\xD0','euml':'\xEB','Euml':'\xCB','euro':'\u20AC','excl':'!','exist':'\u2203','Exists':'\u2203','expectation':'\u2130','exponentiale':'\u2147','ExponentialE':'\u2147','fallingdotseq':'\u2252','fcy':'\u0444','Fcy':'\u0424','female':'\u2640','ffilig':'\uFB03','fflig':'\uFB00','ffllig':'\uFB04','ffr':'\uD835\uDD23','Ffr':'\uD835\uDD09','filig':'\uFB01','FilledSmallSquare':'\u25FC','FilledVerySmallSquare':'\u25AA','fjlig':'fj','flat':'\u266D','fllig':'\uFB02','fltns':'\u25B1','fnof':'\u0192','fopf':'\uD835\uDD57','Fopf':'\uD835\uDD3D','forall':'\u2200','ForAll':'\u2200','fork':'\u22D4','forkv':'\u2AD9','Fouriertrf':'\u2131','fpartint':'\u2A0D','frac12':'\xBD','frac13':'\u2153','frac14':'\xBC','frac15':'\u2155','frac16':'\u2159','frac18':'\u215B','frac23':'\u2154','frac25':'\u2156','frac34':'\xBE','frac35':'\u2157','frac38':'\u215C','frac45':'\u2158','frac56':'\u215A','frac58':'\u215D','frac78':'\u215E','frasl':'\u2044','frown':'\u2322','fscr':'\uD835\uDCBB','Fscr':'\u2131','gacute':'\u01F5','gamma':'\u03B3','Gamma':'\u0393','gammad':'\u03DD','Gammad':'\u03DC','gap':'\u2A86','gbreve':'\u011F','Gbreve':'\u011E','Gcedil':'\u0122','gcirc':'\u011D','Gcirc':'\u011C','gcy':'\u0433','Gcy':'\u0413','gdot':'\u0121','Gdot':'\u0120','ge':'\u2265','gE':'\u2267','gel':'\u22DB','gEl':'\u2A8C','geq':'\u2265','geqq':'\u2267','geqslant':'\u2A7E','ges':'\u2A7E','gescc':'\u2AA9','gesdot':'\u2A80','gesdoto':'\u2A82','gesdotol':'\u2A84','gesl':'\u22DB\uFE00','gesles':'\u2A94','gfr':'\uD835\uDD24','Gfr':'\uD835\uDD0A','gg':'\u226B','Gg':'\u22D9','ggg':'\u22D9','gimel':'\u2137','gjcy':'\u0453','GJcy':'\u0403','gl':'\u2277','gla':'\u2AA5','glE':'\u2A92','glj':'\u2AA4','gnap':'\u2A8A','gnapprox':'\u2A8A','gne':'\u2A88','gnE':'\u2269','gneq':'\u2A88','gneqq':'\u2269','gnsim':'\u22E7','gopf':'\uD835\uDD58','Gopf':'\uD835\uDD3E','grave':'`','GreaterEqual':'\u2265','GreaterEqualLess':'\u22DB','GreaterFullEqual':'\u2267','GreaterGreater':'\u2AA2','GreaterLess':'\u2277','GreaterSlantEqual':'\u2A7E','GreaterTilde':'\u2273','gscr':'\u210A','Gscr':'\uD835\uDCA2','gsim':'\u2273','gsime':'\u2A8E','gsiml':'\u2A90','gt':'>','Gt':'\u226B','GT':'>','gtcc':'\u2AA7','gtcir':'\u2A7A','gtdot':'\u22D7','gtlPar':'\u2995','gtquest':'\u2A7C','gtrapprox':'\u2A86','gtrarr':'\u2978','gtrdot':'\u22D7','gtreqless':'\u22DB','gtreqqless':'\u2A8C','gtrless':'\u2277','gtrsim':'\u2273','gvertneqq':'\u2269\uFE00','gvnE':'\u2269\uFE00','Hacek':'\u02C7','hairsp':'\u200A','half':'\xBD','hamilt':'\u210B','hardcy':'\u044A','HARDcy':'\u042A','harr':'\u2194','hArr':'\u21D4','harrcir':'\u2948','harrw':'\u21AD','Hat':'^','hbar':'\u210F','hcirc':'\u0125','Hcirc':'\u0124','hearts':'\u2665','heartsuit':'\u2665','hellip':'\u2026','hercon':'\u22B9','hfr':'\uD835\uDD25','Hfr':'\u210C','HilbertSpace':'\u210B','hksearow':'\u2925','hkswarow':'\u2926','hoarr':'\u21FF','homtht':'\u223B','hookleftarrow':'\u21A9','hookrightarrow':'\u21AA','hopf':'\uD835\uDD59','Hopf':'\u210D','horbar':'\u2015','HorizontalLine':'\u2500','hscr':'\uD835\uDCBD','Hscr':'\u210B','hslash':'\u210F','hstrok':'\u0127','Hstrok':'\u0126','HumpDownHump':'\u224E','HumpEqual':'\u224F','hybull':'\u2043','hyphen':'\u2010','iacute':'\xED','Iacute':'\xCD','ic':'\u2063','icirc':'\xEE','Icirc':'\xCE','icy':'\u0438','Icy':'\u0418','Idot':'\u0130','iecy':'\u0435','IEcy':'\u0415','iexcl':'\xA1','iff':'\u21D4','ifr':'\uD835\uDD26','Ifr':'\u2111','igrave':'\xEC','Igrave':'\xCC','ii':'\u2148','iiiint':'\u2A0C','iiint':'\u222D','iinfin':'\u29DC','iiota':'\u2129','ijlig':'\u0133','IJlig':'\u0132','Im':'\u2111','imacr':'\u012B','Imacr':'\u012A','image':'\u2111','ImaginaryI':'\u2148','imagline':'\u2110','imagpart':'\u2111','imath':'\u0131','imof':'\u22B7','imped':'\u01B5','Implies':'\u21D2','in':'\u2208','incare':'\u2105','infin':'\u221E','infintie':'\u29DD','inodot':'\u0131','int':'\u222B','Int':'\u222C','intcal':'\u22BA','integers':'\u2124','Integral':'\u222B','intercal':'\u22BA','Intersection':'\u22C2','intlarhk':'\u2A17','intprod':'\u2A3C','InvisibleComma':'\u2063','InvisibleTimes':'\u2062','iocy':'\u0451','IOcy':'\u0401','iogon':'\u012F','Iogon':'\u012E','iopf':'\uD835\uDD5A','Iopf':'\uD835\uDD40','iota':'\u03B9','Iota':'\u0399','iprod':'\u2A3C','iquest':'\xBF','iscr':'\uD835\uDCBE','Iscr':'\u2110','isin':'\u2208','isindot':'\u22F5','isinE':'\u22F9','isins':'\u22F4','isinsv':'\u22F3','isinv':'\u2208','it':'\u2062','itilde':'\u0129','Itilde':'\u0128','iukcy':'\u0456','Iukcy':'\u0406','iuml':'\xEF','Iuml':'\xCF','jcirc':'\u0135','Jcirc':'\u0134','jcy':'\u0439','Jcy':'\u0419','jfr':'\uD835\uDD27','Jfr':'\uD835\uDD0D','jmath':'\u0237','jopf':'\uD835\uDD5B','Jopf':'\uD835\uDD41','jscr':'\uD835\uDCBF','Jscr':'\uD835\uDCA5','jsercy':'\u0458','Jsercy':'\u0408','jukcy':'\u0454','Jukcy':'\u0404','kappa':'\u03BA','Kappa':'\u039A','kappav':'\u03F0','kcedil':'\u0137','Kcedil':'\u0136','kcy':'\u043A','Kcy':'\u041A','kfr':'\uD835\uDD28','Kfr':'\uD835\uDD0E','kgreen':'\u0138','khcy':'\u0445','KHcy':'\u0425','kjcy':'\u045C','KJcy':'\u040C','kopf':'\uD835\uDD5C','Kopf':'\uD835\uDD42','kscr':'\uD835\uDCC0','Kscr':'\uD835\uDCA6','lAarr':'\u21DA','lacute':'\u013A','Lacute':'\u0139','laemptyv':'\u29B4','lagran':'\u2112','lambda':'\u03BB','Lambda':'\u039B','lang':'\u27E8','Lang':'\u27EA','langd':'\u2991','langle':'\u27E8','lap':'\u2A85','Laplacetrf':'\u2112','laquo':'\xAB','larr':'\u2190','lArr':'\u21D0','Larr':'\u219E','larrb':'\u21E4','larrbfs':'\u291F','larrfs':'\u291D','larrhk':'\u21A9','larrlp':'\u21AB','larrpl':'\u2939','larrsim':'\u2973','larrtl':'\u21A2','lat':'\u2AAB','latail':'\u2919','lAtail':'\u291B','late':'\u2AAD','lates':'\u2AAD\uFE00','lbarr':'\u290C','lBarr':'\u290E','lbbrk':'\u2772','lbrace':'{','lbrack':'[','lbrke':'\u298B','lbrksld':'\u298F','lbrkslu':'\u298D','lcaron':'\u013E','Lcaron':'\u013D','lcedil':'\u013C','Lcedil':'\u013B','lceil':'\u2308','lcub':'{','lcy':'\u043B','Lcy':'\u041B','ldca':'\u2936','ldquo':'\u201C','ldquor':'\u201E','ldrdhar':'\u2967','ldrushar':'\u294B','ldsh':'\u21B2','le':'\u2264','lE':'\u2266','LeftAngleBracket':'\u27E8','leftarrow':'\u2190','Leftarrow':'\u21D0','LeftArrow':'\u2190','LeftArrowBar':'\u21E4','LeftArrowRightArrow':'\u21C6','leftarrowtail':'\u21A2','LeftCeiling':'\u2308','LeftDoubleBracket':'\u27E6','LeftDownTeeVector':'\u2961','LeftDownVector':'\u21C3','LeftDownVectorBar':'\u2959','LeftFloor':'\u230A','leftharpoondown':'\u21BD','leftharpoonup':'\u21BC','leftleftarrows':'\u21C7','leftrightarrow':'\u2194','Leftrightarrow':'\u21D4','LeftRightArrow':'\u2194','leftrightarrows':'\u21C6','leftrightharpoons':'\u21CB','leftrightsquigarrow':'\u21AD','LeftRightVector':'\u294E','LeftTee':'\u22A3','LeftTeeArrow':'\u21A4','LeftTeeVector':'\u295A','leftthreetimes':'\u22CB','LeftTriangle':'\u22B2','LeftTriangleBar':'\u29CF','LeftTriangleEqual':'\u22B4','LeftUpDownVector':'\u2951','LeftUpTeeVector':'\u2960','LeftUpVector':'\u21BF','LeftUpVectorBar':'\u2958','LeftVector':'\u21BC','LeftVectorBar':'\u2952','leg':'\u22DA','lEg':'\u2A8B','leq':'\u2264','leqq':'\u2266','leqslant':'\u2A7D','les':'\u2A7D','lescc':'\u2AA8','lesdot':'\u2A7F','lesdoto':'\u2A81','lesdotor':'\u2A83','lesg':'\u22DA\uFE00','lesges':'\u2A93','lessapprox':'\u2A85','lessdot':'\u22D6','lesseqgtr':'\u22DA','lesseqqgtr':'\u2A8B','LessEqualGreater':'\u22DA','LessFullEqual':'\u2266','LessGreater':'\u2276','lessgtr':'\u2276','LessLess':'\u2AA1','lesssim':'\u2272','LessSlantEqual':'\u2A7D','LessTilde':'\u2272','lfisht':'\u297C','lfloor':'\u230A','lfr':'\uD835\uDD29','Lfr':'\uD835\uDD0F','lg':'\u2276','lgE':'\u2A91','lHar':'\u2962','lhard':'\u21BD','lharu':'\u21BC','lharul':'\u296A','lhblk':'\u2584','ljcy':'\u0459','LJcy':'\u0409','ll':'\u226A','Ll':'\u22D8','llarr':'\u21C7','llcorner':'\u231E','Lleftarrow':'\u21DA','llhard':'\u296B','lltri':'\u25FA','lmidot':'\u0140','Lmidot':'\u013F','lmoust':'\u23B0','lmoustache':'\u23B0','lnap':'\u2A89','lnapprox':'\u2A89','lne':'\u2A87','lnE':'\u2268','lneq':'\u2A87','lneqq':'\u2268','lnsim':'\u22E6','loang':'\u27EC','loarr':'\u21FD','lobrk':'\u27E6','longleftarrow':'\u27F5','Longleftarrow':'\u27F8','LongLeftArrow':'\u27F5','longleftrightarrow':'\u27F7','Longleftrightarrow':'\u27FA','LongLeftRightArrow':'\u27F7','longmapsto':'\u27FC','longrightarrow':'\u27F6','Longrightarrow':'\u27F9','LongRightArrow':'\u27F6','looparrowleft':'\u21AB','looparrowright':'\u21AC','lopar':'\u2985','lopf':'\uD835\uDD5D','Lopf':'\uD835\uDD43','loplus':'\u2A2D','lotimes':'\u2A34','lowast':'\u2217','lowbar':'_','LowerLeftArrow':'\u2199','LowerRightArrow':'\u2198','loz':'\u25CA','lozenge':'\u25CA','lozf':'\u29EB','lpar':'(','lparlt':'\u2993','lrarr':'\u21C6','lrcorner':'\u231F','lrhar':'\u21CB','lrhard':'\u296D','lrm':'\u200E','lrtri':'\u22BF','lsaquo':'\u2039','lscr':'\uD835\uDCC1','Lscr':'\u2112','lsh':'\u21B0','Lsh':'\u21B0','lsim':'\u2272','lsime':'\u2A8D','lsimg':'\u2A8F','lsqb':'[','lsquo':'\u2018','lsquor':'\u201A','lstrok':'\u0142','Lstrok':'\u0141','lt':'<','Lt':'\u226A','LT':'<','ltcc':'\u2AA6','ltcir':'\u2A79','ltdot':'\u22D6','lthree':'\u22CB','ltimes':'\u22C9','ltlarr':'\u2976','ltquest':'\u2A7B','ltri':'\u25C3','ltrie':'\u22B4','ltrif':'\u25C2','ltrPar':'\u2996','lurdshar':'\u294A','luruhar':'\u2966','lvertneqq':'\u2268\uFE00','lvnE':'\u2268\uFE00','macr':'\xAF','male':'\u2642','malt':'\u2720','maltese':'\u2720','map':'\u21A6','Map':'\u2905','mapsto':'\u21A6','mapstodown':'\u21A7','mapstoleft':'\u21A4','mapstoup':'\u21A5','marker':'\u25AE','mcomma':'\u2A29','mcy':'\u043C','Mcy':'\u041C','mdash':'\u2014','mDDot':'\u223A','measuredangle':'\u2221','MediumSpace':'\u205F','Mellintrf':'\u2133','mfr':'\uD835\uDD2A','Mfr':'\uD835\uDD10','mho':'\u2127','micro':'\xB5','mid':'\u2223','midast':'*','midcir':'\u2AF0','middot':'\xB7','minus':'\u2212','minusb':'\u229F','minusd':'\u2238','minusdu':'\u2A2A','MinusPlus':'\u2213','mlcp':'\u2ADB','mldr':'\u2026','mnplus':'\u2213','models':'\u22A7','mopf':'\uD835\uDD5E','Mopf':'\uD835\uDD44','mp':'\u2213','mscr':'\uD835\uDCC2','Mscr':'\u2133','mstpos':'\u223E','mu':'\u03BC','Mu':'\u039C','multimap':'\u22B8','mumap':'\u22B8','nabla':'\u2207','nacute':'\u0144','Nacute':'\u0143','nang':'\u2220\u20D2','nap':'\u2249','napE':'\u2A70\u0338','napid':'\u224B\u0338','napos':'\u0149','napprox':'\u2249','natur':'\u266E','natural':'\u266E','naturals':'\u2115','nbsp':'\xA0','nbump':'\u224E\u0338','nbumpe':'\u224F\u0338','ncap':'\u2A43','ncaron':'\u0148','Ncaron':'\u0147','ncedil':'\u0146','Ncedil':'\u0145','ncong':'\u2247','ncongdot':'\u2A6D\u0338','ncup':'\u2A42','ncy':'\u043D','Ncy':'\u041D','ndash':'\u2013','ne':'\u2260','nearhk':'\u2924','nearr':'\u2197','neArr':'\u21D7','nearrow':'\u2197','nedot':'\u2250\u0338','NegativeMediumSpace':'\u200B','NegativeThickSpace':'\u200B','NegativeThinSpace':'\u200B','NegativeVeryThinSpace':'\u200B','nequiv':'\u2262','nesear':'\u2928','nesim':'\u2242\u0338','NestedGreaterGreater':'\u226B','NestedLessLess':'\u226A','NewLine':'\n','nexist':'\u2204','nexists':'\u2204','nfr':'\uD835\uDD2B','Nfr':'\uD835\uDD11','nge':'\u2271','ngE':'\u2267\u0338','ngeq':'\u2271','ngeqq':'\u2267\u0338','ngeqslant':'\u2A7E\u0338','nges':'\u2A7E\u0338','nGg':'\u22D9\u0338','ngsim':'\u2275','ngt':'\u226F','nGt':'\u226B\u20D2','ngtr':'\u226F','nGtv':'\u226B\u0338','nharr':'\u21AE','nhArr':'\u21CE','nhpar':'\u2AF2','ni':'\u220B','nis':'\u22FC','nisd':'\u22FA','niv':'\u220B','njcy':'\u045A','NJcy':'\u040A','nlarr':'\u219A','nlArr':'\u21CD','nldr':'\u2025','nle':'\u2270','nlE':'\u2266\u0338','nleftarrow':'\u219A','nLeftarrow':'\u21CD','nleftrightarrow':'\u21AE','nLeftrightarrow':'\u21CE','nleq':'\u2270','nleqq':'\u2266\u0338','nleqslant':'\u2A7D\u0338','nles':'\u2A7D\u0338','nless':'\u226E','nLl':'\u22D8\u0338','nlsim':'\u2274','nlt':'\u226E','nLt':'\u226A\u20D2','nltri':'\u22EA','nltrie':'\u22EC','nLtv':'\u226A\u0338','nmid':'\u2224','NoBreak':'\u2060','NonBreakingSpace':'\xA0','nopf':'\uD835\uDD5F','Nopf':'\u2115','not':'\xAC','Not':'\u2AEC','NotCongruent':'\u2262','NotCupCap':'\u226D','NotDoubleVerticalBar':'\u2226','NotElement':'\u2209','NotEqual':'\u2260','NotEqualTilde':'\u2242\u0338','NotExists':'\u2204','NotGreater':'\u226F','NotGreaterEqual':'\u2271','NotGreaterFullEqual':'\u2267\u0338','NotGreaterGreater':'\u226B\u0338','NotGreaterLess':'\u2279','NotGreaterSlantEqual':'\u2A7E\u0338','NotGreaterTilde':'\u2275','NotHumpDownHump':'\u224E\u0338','NotHumpEqual':'\u224F\u0338','notin':'\u2209','notindot':'\u22F5\u0338','notinE':'\u22F9\u0338','notinva':'\u2209','notinvb':'\u22F7','notinvc':'\u22F6','NotLeftTriangle':'\u22EA','NotLeftTriangleBar':'\u29CF\u0338','NotLeftTriangleEqual':'\u22EC','NotLess':'\u226E','NotLessEqual':'\u2270','NotLessGreater':'\u2278','NotLessLess':'\u226A\u0338','NotLessSlantEqual':'\u2A7D\u0338','NotLessTilde':'\u2274','NotNestedGreaterGreater':'\u2AA2\u0338','NotNestedLessLess':'\u2AA1\u0338','notni':'\u220C','notniva':'\u220C','notnivb':'\u22FE','notnivc':'\u22FD','NotPrecedes':'\u2280','NotPrecedesEqual':'\u2AAF\u0338','NotPrecedesSlantEqual':'\u22E0','NotReverseElement':'\u220C','NotRightTriangle':'\u22EB','NotRightTriangleBar':'\u29D0\u0338','NotRightTriangleEqual':'\u22ED','NotSquareSubset':'\u228F\u0338','NotSquareSubsetEqual':'\u22E2','NotSquareSuperset':'\u2290\u0338','NotSquareSupersetEqual':'\u22E3','NotSubset':'\u2282\u20D2','NotSubsetEqual':'\u2288','NotSucceeds':'\u2281','NotSucceedsEqual':'\u2AB0\u0338','NotSucceedsSlantEqual':'\u22E1','NotSucceedsTilde':'\u227F\u0338','NotSuperset':'\u2283\u20D2','NotSupersetEqual':'\u2289','NotTilde':'\u2241','NotTildeEqual':'\u2244','NotTildeFullEqual':'\u2247','NotTildeTilde':'\u2249','NotVerticalBar':'\u2224','npar':'\u2226','nparallel':'\u2226','nparsl':'\u2AFD\u20E5','npart':'\u2202\u0338','npolint':'\u2A14','npr':'\u2280','nprcue':'\u22E0','npre':'\u2AAF\u0338','nprec':'\u2280','npreceq':'\u2AAF\u0338','nrarr':'\u219B','nrArr':'\u21CF','nrarrc':'\u2933\u0338','nrarrw':'\u219D\u0338','nrightarrow':'\u219B','nRightarrow':'\u21CF','nrtri':'\u22EB','nrtrie':'\u22ED','nsc':'\u2281','nsccue':'\u22E1','nsce':'\u2AB0\u0338','nscr':'\uD835\uDCC3','Nscr':'\uD835\uDCA9','nshortmid':'\u2224','nshortparallel':'\u2226','nsim':'\u2241','nsime':'\u2244','nsimeq':'\u2244','nsmid':'\u2224','nspar':'\u2226','nsqsube':'\u22E2','nsqsupe':'\u22E3','nsub':'\u2284','nsube':'\u2288','nsubE':'\u2AC5\u0338','nsubset':'\u2282\u20D2','nsubseteq':'\u2288','nsubseteqq':'\u2AC5\u0338','nsucc':'\u2281','nsucceq':'\u2AB0\u0338','nsup':'\u2285','nsupe':'\u2289','nsupE':'\u2AC6\u0338','nsupset':'\u2283\u20D2','nsupseteq':'\u2289','nsupseteqq':'\u2AC6\u0338','ntgl':'\u2279','ntilde':'\xF1','Ntilde':'\xD1','ntlg':'\u2278','ntriangleleft':'\u22EA','ntrianglelefteq':'\u22EC','ntriangleright':'\u22EB','ntrianglerighteq':'\u22ED','nu':'\u03BD','Nu':'\u039D','num':'#','numero':'\u2116','numsp':'\u2007','nvap':'\u224D\u20D2','nvdash':'\u22AC','nvDash':'\u22AD','nVdash':'\u22AE','nVDash':'\u22AF','nvge':'\u2265\u20D2','nvgt':'>\u20D2','nvHarr':'\u2904','nvinfin':'\u29DE','nvlArr':'\u2902','nvle':'\u2264\u20D2','nvlt':'<\u20D2','nvltrie':'\u22B4\u20D2','nvrArr':'\u2903','nvrtrie':'\u22B5\u20D2','nvsim':'\u223C\u20D2','nwarhk':'\u2923','nwarr':'\u2196','nwArr':'\u21D6','nwarrow':'\u2196','nwnear':'\u2927','oacute':'\xF3','Oacute':'\xD3','oast':'\u229B','ocir':'\u229A','ocirc':'\xF4','Ocirc':'\xD4','ocy':'\u043E','Ocy':'\u041E','odash':'\u229D','odblac':'\u0151','Odblac':'\u0150','odiv':'\u2A38','odot':'\u2299','odsold':'\u29BC','oelig':'\u0153','OElig':'\u0152','ofcir':'\u29BF','ofr':'\uD835\uDD2C','Ofr':'\uD835\uDD12','ogon':'\u02DB','ograve':'\xF2','Ograve':'\xD2','ogt':'\u29C1','ohbar':'\u29B5','ohm':'\u03A9','oint':'\u222E','olarr':'\u21BA','olcir':'\u29BE','olcross':'\u29BB','oline':'\u203E','olt':'\u29C0','omacr':'\u014D','Omacr':'\u014C','omega':'\u03C9','Omega':'\u03A9','omicron':'\u03BF','Omicron':'\u039F','omid':'\u29B6','ominus':'\u2296','oopf':'\uD835\uDD60','Oopf':'\uD835\uDD46','opar':'\u29B7','OpenCurlyDoubleQuote':'\u201C','OpenCurlyQuote':'\u2018','operp':'\u29B9','oplus':'\u2295','or':'\u2228','Or':'\u2A54','orarr':'\u21BB','ord':'\u2A5D','order':'\u2134','orderof':'\u2134','ordf':'\xAA','ordm':'\xBA','origof':'\u22B6','oror':'\u2A56','orslope':'\u2A57','orv':'\u2A5B','oS':'\u24C8','oscr':'\u2134','Oscr':'\uD835\uDCAA','oslash':'\xF8','Oslash':'\xD8','osol':'\u2298','otilde':'\xF5','Otilde':'\xD5','otimes':'\u2297','Otimes':'\u2A37','otimesas':'\u2A36','ouml':'\xF6','Ouml':'\xD6','ovbar':'\u233D','OverBar':'\u203E','OverBrace':'\u23DE','OverBracket':'\u23B4','OverParenthesis':'\u23DC','par':'\u2225','para':'\xB6','parallel':'\u2225','parsim':'\u2AF3','parsl':'\u2AFD','part':'\u2202','PartialD':'\u2202','pcy':'\u043F','Pcy':'\u041F','percnt':'%','period':'.','permil':'\u2030','perp':'\u22A5','pertenk':'\u2031','pfr':'\uD835\uDD2D','Pfr':'\uD835\uDD13','phi':'\u03C6','Phi':'\u03A6','phiv':'\u03D5','phmmat':'\u2133','phone':'\u260E','pi':'\u03C0','Pi':'\u03A0','pitchfork':'\u22D4','piv':'\u03D6','planck':'\u210F','planckh':'\u210E','plankv':'\u210F','plus':'+','plusacir':'\u2A23','plusb':'\u229E','pluscir':'\u2A22','plusdo':'\u2214','plusdu':'\u2A25','pluse':'\u2A72','PlusMinus':'\xB1','plusmn':'\xB1','plussim':'\u2A26','plustwo':'\u2A27','pm':'\xB1','Poincareplane':'\u210C','pointint':'\u2A15','popf':'\uD835\uDD61','Popf':'\u2119','pound':'\xA3','pr':'\u227A','Pr':'\u2ABB','prap':'\u2AB7','prcue':'\u227C','pre':'\u2AAF','prE':'\u2AB3','prec':'\u227A','precapprox':'\u2AB7','preccurlyeq':'\u227C','Precedes':'\u227A','PrecedesEqual':'\u2AAF','PrecedesSlantEqual':'\u227C','PrecedesTilde':'\u227E','preceq':'\u2AAF','precnapprox':'\u2AB9','precneqq':'\u2AB5','precnsim':'\u22E8','precsim':'\u227E','prime':'\u2032','Prime':'\u2033','primes':'\u2119','prnap':'\u2AB9','prnE':'\u2AB5','prnsim':'\u22E8','prod':'\u220F','Product':'\u220F','profalar':'\u232E','profline':'\u2312','profsurf':'\u2313','prop':'\u221D','Proportion':'\u2237','Proportional':'\u221D','propto':'\u221D','prsim':'\u227E','prurel':'\u22B0','pscr':'\uD835\uDCC5','Pscr':'\uD835\uDCAB','psi':'\u03C8','Psi':'\u03A8','puncsp':'\u2008','qfr':'\uD835\uDD2E','Qfr':'\uD835\uDD14','qint':'\u2A0C','qopf':'\uD835\uDD62','Qopf':'\u211A','qprime':'\u2057','qscr':'\uD835\uDCC6','Qscr':'\uD835\uDCAC','quaternions':'\u210D','quatint':'\u2A16','quest':'?','questeq':'\u225F','quot':'"','QUOT':'"','rAarr':'\u21DB','race':'\u223D\u0331','racute':'\u0155','Racute':'\u0154','radic':'\u221A','raemptyv':'\u29B3','rang':'\u27E9','Rang':'\u27EB','rangd':'\u2992','range':'\u29A5','rangle':'\u27E9','raquo':'\xBB','rarr':'\u2192','rArr':'\u21D2','Rarr':'\u21A0','rarrap':'\u2975','rarrb':'\u21E5','rarrbfs':'\u2920','rarrc':'\u2933','rarrfs':'\u291E','rarrhk':'\u21AA','rarrlp':'\u21AC','rarrpl':'\u2945','rarrsim':'\u2974','rarrtl':'\u21A3','Rarrtl':'\u2916','rarrw':'\u219D','ratail':'\u291A','rAtail':'\u291C','ratio':'\u2236','rationals':'\u211A','rbarr':'\u290D','rBarr':'\u290F','RBarr':'\u2910','rbbrk':'\u2773','rbrace':'}','rbrack':']','rbrke':'\u298C','rbrksld':'\u298E','rbrkslu':'\u2990','rcaron':'\u0159','Rcaron':'\u0158','rcedil':'\u0157','Rcedil':'\u0156','rceil':'\u2309','rcub':'}','rcy':'\u0440','Rcy':'\u0420','rdca':'\u2937','rdldhar':'\u2969','rdquo':'\u201D','rdquor':'\u201D','rdsh':'\u21B3','Re':'\u211C','real':'\u211C','realine':'\u211B','realpart':'\u211C','reals':'\u211D','rect':'\u25AD','reg':'\xAE','REG':'\xAE','ReverseElement':'\u220B','ReverseEquilibrium':'\u21CB','ReverseUpEquilibrium':'\u296F','rfisht':'\u297D','rfloor':'\u230B','rfr':'\uD835\uDD2F','Rfr':'\u211C','rHar':'\u2964','rhard':'\u21C1','rharu':'\u21C0','rharul':'\u296C','rho':'\u03C1','Rho':'\u03A1','rhov':'\u03F1','RightAngleBracket':'\u27E9','rightarrow':'\u2192','Rightarrow':'\u21D2','RightArrow':'\u2192','RightArrowBar':'\u21E5','RightArrowLeftArrow':'\u21C4','rightarrowtail':'\u21A3','RightCeiling':'\u2309','RightDoubleBracket':'\u27E7','RightDownTeeVector':'\u295D','RightDownVector':'\u21C2','RightDownVectorBar':'\u2955','RightFloor':'\u230B','rightharpoondown':'\u21C1','rightharpoonup':'\u21C0','rightleftarrows':'\u21C4','rightleftharpoons':'\u21CC','rightrightarrows':'\u21C9','rightsquigarrow':'\u219D','RightTee':'\u22A2','RightTeeArrow':'\u21A6','RightTeeVector':'\u295B','rightthreetimes':'\u22CC','RightTriangle':'\u22B3','RightTriangleBar':'\u29D0','RightTriangleEqual':'\u22B5','RightUpDownVector':'\u294F','RightUpTeeVector':'\u295C','RightUpVector':'\u21BE','RightUpVectorBar':'\u2954','RightVector':'\u21C0','RightVectorBar':'\u2953','ring':'\u02DA','risingdotseq':'\u2253','rlarr':'\u21C4','rlhar':'\u21CC','rlm':'\u200F','rmoust':'\u23B1','rmoustache':'\u23B1','rnmid':'\u2AEE','roang':'\u27ED','roarr':'\u21FE','robrk':'\u27E7','ropar':'\u2986','ropf':'\uD835\uDD63','Ropf':'\u211D','roplus':'\u2A2E','rotimes':'\u2A35','RoundImplies':'\u2970','rpar':')','rpargt':'\u2994','rppolint':'\u2A12','rrarr':'\u21C9','Rrightarrow':'\u21DB','rsaquo':'\u203A','rscr':'\uD835\uDCC7','Rscr':'\u211B','rsh':'\u21B1','Rsh':'\u21B1','rsqb':']','rsquo':'\u2019','rsquor':'\u2019','rthree':'\u22CC','rtimes':'\u22CA','rtri':'\u25B9','rtrie':'\u22B5','rtrif':'\u25B8','rtriltri':'\u29CE','RuleDelayed':'\u29F4','ruluhar':'\u2968','rx':'\u211E','sacute':'\u015B','Sacute':'\u015A','sbquo':'\u201A','sc':'\u227B','Sc':'\u2ABC','scap':'\u2AB8','scaron':'\u0161','Scaron':'\u0160','sccue':'\u227D','sce':'\u2AB0','scE':'\u2AB4','scedil':'\u015F','Scedil':'\u015E','scirc':'\u015D','Scirc':'\u015C','scnap':'\u2ABA','scnE':'\u2AB6','scnsim':'\u22E9','scpolint':'\u2A13','scsim':'\u227F','scy':'\u0441','Scy':'\u0421','sdot':'\u22C5','sdotb':'\u22A1','sdote':'\u2A66','searhk':'\u2925','searr':'\u2198','seArr':'\u21D8','searrow':'\u2198','sect':'\xA7','semi':';','seswar':'\u2929','setminus':'\u2216','setmn':'\u2216','sext':'\u2736','sfr':'\uD835\uDD30','Sfr':'\uD835\uDD16','sfrown':'\u2322','sharp':'\u266F','shchcy':'\u0449','SHCHcy':'\u0429','shcy':'\u0448','SHcy':'\u0428','ShortDownArrow':'\u2193','ShortLeftArrow':'\u2190','shortmid':'\u2223','shortparallel':'\u2225','ShortRightArrow':'\u2192','ShortUpArrow':'\u2191','shy':'\xAD','sigma':'\u03C3','Sigma':'\u03A3','sigmaf':'\u03C2','sigmav':'\u03C2','sim':'\u223C','simdot':'\u2A6A','sime':'\u2243','simeq':'\u2243','simg':'\u2A9E','simgE':'\u2AA0','siml':'\u2A9D','simlE':'\u2A9F','simne':'\u2246','simplus':'\u2A24','simrarr':'\u2972','slarr':'\u2190','SmallCircle':'\u2218','smallsetminus':'\u2216','smashp':'\u2A33','smeparsl':'\u29E4','smid':'\u2223','smile':'\u2323','smt':'\u2AAA','smte':'\u2AAC','smtes':'\u2AAC\uFE00','softcy':'\u044C','SOFTcy':'\u042C','sol':'/','solb':'\u29C4','solbar':'\u233F','sopf':'\uD835\uDD64','Sopf':'\uD835\uDD4A','spades':'\u2660','spadesuit':'\u2660','spar':'\u2225','sqcap':'\u2293','sqcaps':'\u2293\uFE00','sqcup':'\u2294','sqcups':'\u2294\uFE00','Sqrt':'\u221A','sqsub':'\u228F','sqsube':'\u2291','sqsubset':'\u228F','sqsubseteq':'\u2291','sqsup':'\u2290','sqsupe':'\u2292','sqsupset':'\u2290','sqsupseteq':'\u2292','squ':'\u25A1','square':'\u25A1','Square':'\u25A1','SquareIntersection':'\u2293','SquareSubset':'\u228F','SquareSubsetEqual':'\u2291','SquareSuperset':'\u2290','SquareSupersetEqual':'\u2292','SquareUnion':'\u2294','squarf':'\u25AA','squf':'\u25AA','srarr':'\u2192','sscr':'\uD835\uDCC8','Sscr':'\uD835\uDCAE','ssetmn':'\u2216','ssmile':'\u2323','sstarf':'\u22C6','star':'\u2606','Star':'\u22C6','starf':'\u2605','straightepsilon':'\u03F5','straightphi':'\u03D5','strns':'\xAF','sub':'\u2282','Sub':'\u22D0','subdot':'\u2ABD','sube':'\u2286','subE':'\u2AC5','subedot':'\u2AC3','submult':'\u2AC1','subne':'\u228A','subnE':'\u2ACB','subplus':'\u2ABF','subrarr':'\u2979','subset':'\u2282','Subset':'\u22D0','subseteq':'\u2286','subseteqq':'\u2AC5','SubsetEqual':'\u2286','subsetneq':'\u228A','subsetneqq':'\u2ACB','subsim':'\u2AC7','subsub':'\u2AD5','subsup':'\u2AD3','succ':'\u227B','succapprox':'\u2AB8','succcurlyeq':'\u227D','Succeeds':'\u227B','SucceedsEqual':'\u2AB0','SucceedsSlantEqual':'\u227D','SucceedsTilde':'\u227F','succeq':'\u2AB0','succnapprox':'\u2ABA','succneqq':'\u2AB6','succnsim':'\u22E9','succsim':'\u227F','SuchThat':'\u220B','sum':'\u2211','Sum':'\u2211','sung':'\u266A','sup':'\u2283','Sup':'\u22D1','sup1':'\xB9','sup2':'\xB2','sup3':'\xB3','supdot':'\u2ABE','supdsub':'\u2AD8','supe':'\u2287','supE':'\u2AC6','supedot':'\u2AC4','Superset':'\u2283','SupersetEqual':'\u2287','suphsol':'\u27C9','suphsub':'\u2AD7','suplarr':'\u297B','supmult':'\u2AC2','supne':'\u228B','supnE':'\u2ACC','supplus':'\u2AC0','supset':'\u2283','Supset':'\u22D1','supseteq':'\u2287','supseteqq':'\u2AC6','supsetneq':'\u228B','supsetneqq':'\u2ACC','supsim':'\u2AC8','supsub':'\u2AD4','supsup':'\u2AD6','swarhk':'\u2926','swarr':'\u2199','swArr':'\u21D9','swarrow':'\u2199','swnwar':'\u292A','szlig':'\xDF','Tab':'\t','target':'\u2316','tau':'\u03C4','Tau':'\u03A4','tbrk':'\u23B4','tcaron':'\u0165','Tcaron':'\u0164','tcedil':'\u0163','Tcedil':'\u0162','tcy':'\u0442','Tcy':'\u0422','tdot':'\u20DB','telrec':'\u2315','tfr':'\uD835\uDD31','Tfr':'\uD835\uDD17','there4':'\u2234','therefore':'\u2234','Therefore':'\u2234','theta':'\u03B8','Theta':'\u0398','thetasym':'\u03D1','thetav':'\u03D1','thickapprox':'\u2248','thicksim':'\u223C','ThickSpace':'\u205F\u200A','thinsp':'\u2009','ThinSpace':'\u2009','thkap':'\u2248','thksim':'\u223C','thorn':'\xFE','THORN':'\xDE','tilde':'\u02DC','Tilde':'\u223C','TildeEqual':'\u2243','TildeFullEqual':'\u2245','TildeTilde':'\u2248','times':'\xD7','timesb':'\u22A0','timesbar':'\u2A31','timesd':'\u2A30','tint':'\u222D','toea':'\u2928','top':'\u22A4','topbot':'\u2336','topcir':'\u2AF1','topf':'\uD835\uDD65','Topf':'\uD835\uDD4B','topfork':'\u2ADA','tosa':'\u2929','tprime':'\u2034','trade':'\u2122','TRADE':'\u2122','triangle':'\u25B5','triangledown':'\u25BF','triangleleft':'\u25C3','trianglelefteq':'\u22B4','triangleq':'\u225C','triangleright':'\u25B9','trianglerighteq':'\u22B5','tridot':'\u25EC','trie':'\u225C','triminus':'\u2A3A','TripleDot':'\u20DB','triplus':'\u2A39','trisb':'\u29CD','tritime':'\u2A3B','trpezium':'\u23E2','tscr':'\uD835\uDCC9','Tscr':'\uD835\uDCAF','tscy':'\u0446','TScy':'\u0426','tshcy':'\u045B','TSHcy':'\u040B','tstrok':'\u0167','Tstrok':'\u0166','twixt':'\u226C','twoheadleftarrow':'\u219E','twoheadrightarrow':'\u21A0','uacute':'\xFA','Uacute':'\xDA','uarr':'\u2191','uArr':'\u21D1','Uarr':'\u219F','Uarrocir':'\u2949','ubrcy':'\u045E','Ubrcy':'\u040E','ubreve':'\u016D','Ubreve':'\u016C','ucirc':'\xFB','Ucirc':'\xDB','ucy':'\u0443','Ucy':'\u0423','udarr':'\u21C5','udblac':'\u0171','Udblac':'\u0170','udhar':'\u296E','ufisht':'\u297E','ufr':'\uD835\uDD32','Ufr':'\uD835\uDD18','ugrave':'\xF9','Ugrave':'\xD9','uHar':'\u2963','uharl':'\u21BF','uharr':'\u21BE','uhblk':'\u2580','ulcorn':'\u231C','ulcorner':'\u231C','ulcrop':'\u230F','ultri':'\u25F8','umacr':'\u016B','Umacr':'\u016A','uml':'\xA8','UnderBar':'_','UnderBrace':'\u23DF','UnderBracket':'\u23B5','UnderParenthesis':'\u23DD','Union':'\u22C3','UnionPlus':'\u228E','uogon':'\u0173','Uogon':'\u0172','uopf':'\uD835\uDD66','Uopf':'\uD835\uDD4C','uparrow':'\u2191','Uparrow':'\u21D1','UpArrow':'\u2191','UpArrowBar':'\u2912','UpArrowDownArrow':'\u21C5','updownarrow':'\u2195','Updownarrow':'\u21D5','UpDownArrow':'\u2195','UpEquilibrium':'\u296E','upharpoonleft':'\u21BF','upharpoonright':'\u21BE','uplus':'\u228E','UpperLeftArrow':'\u2196','UpperRightArrow':'\u2197','upsi':'\u03C5','Upsi':'\u03D2','upsih':'\u03D2','upsilon':'\u03C5','Upsilon':'\u03A5','UpTee':'\u22A5','UpTeeArrow':'\u21A5','upuparrows':'\u21C8','urcorn':'\u231D','urcorner':'\u231D','urcrop':'\u230E','uring':'\u016F','Uring':'\u016E','urtri':'\u25F9','uscr':'\uD835\uDCCA','Uscr':'\uD835\uDCB0','utdot':'\u22F0','utilde':'\u0169','Utilde':'\u0168','utri':'\u25B5','utrif':'\u25B4','uuarr':'\u21C8','uuml':'\xFC','Uuml':'\xDC','uwangle':'\u29A7','vangrt':'\u299C','varepsilon':'\u03F5','varkappa':'\u03F0','varnothing':'\u2205','varphi':'\u03D5','varpi':'\u03D6','varpropto':'\u221D','varr':'\u2195','vArr':'\u21D5','varrho':'\u03F1','varsigma':'\u03C2','varsubsetneq':'\u228A\uFE00','varsubsetneqq':'\u2ACB\uFE00','varsupsetneq':'\u228B\uFE00','varsupsetneqq':'\u2ACC\uFE00','vartheta':'\u03D1','vartriangleleft':'\u22B2','vartriangleright':'\u22B3','vBar':'\u2AE8','Vbar':'\u2AEB','vBarv':'\u2AE9','vcy':'\u0432','Vcy':'\u0412','vdash':'\u22A2','vDash':'\u22A8','Vdash':'\u22A9','VDash':'\u22AB','Vdashl':'\u2AE6','vee':'\u2228','Vee':'\u22C1','veebar':'\u22BB','veeeq':'\u225A','vellip':'\u22EE','verbar':'|','Verbar':'\u2016','vert':'|','Vert':'\u2016','VerticalBar':'\u2223','VerticalLine':'|','VerticalSeparator':'\u2758','VerticalTilde':'\u2240','VeryThinSpace':'\u200A','vfr':'\uD835\uDD33','Vfr':'\uD835\uDD19','vltri':'\u22B2','vnsub':'\u2282\u20D2','vnsup':'\u2283\u20D2','vopf':'\uD835\uDD67','Vopf':'\uD835\uDD4D','vprop':'\u221D','vrtri':'\u22B3','vscr':'\uD835\uDCCB','Vscr':'\uD835\uDCB1','vsubne':'\u228A\uFE00','vsubnE':'\u2ACB\uFE00','vsupne':'\u228B\uFE00','vsupnE':'\u2ACC\uFE00','Vvdash':'\u22AA','vzigzag':'\u299A','wcirc':'\u0175','Wcirc':'\u0174','wedbar':'\u2A5F','wedge':'\u2227','Wedge':'\u22C0','wedgeq':'\u2259','weierp':'\u2118','wfr':'\uD835\uDD34','Wfr':'\uD835\uDD1A','wopf':'\uD835\uDD68','Wopf':'\uD835\uDD4E','wp':'\u2118','wr':'\u2240','wreath':'\u2240','wscr':'\uD835\uDCCC','Wscr':'\uD835\uDCB2','xcap':'\u22C2','xcirc':'\u25EF','xcup':'\u22C3','xdtri':'\u25BD','xfr':'\uD835\uDD35','Xfr':'\uD835\uDD1B','xharr':'\u27F7','xhArr':'\u27FA','xi':'\u03BE','Xi':'\u039E','xlarr':'\u27F5','xlArr':'\u27F8','xmap':'\u27FC','xnis':'\u22FB','xodot':'\u2A00','xopf':'\uD835\uDD69','Xopf':'\uD835\uDD4F','xoplus':'\u2A01','xotime':'\u2A02','xrarr':'\u27F6','xrArr':'\u27F9','xscr':'\uD835\uDCCD','Xscr':'\uD835\uDCB3','xsqcup':'\u2A06','xuplus':'\u2A04','xutri':'\u25B3','xvee':'\u22C1','xwedge':'\u22C0','yacute':'\xFD','Yacute':'\xDD','yacy':'\u044F','YAcy':'\u042F','ycirc':'\u0177','Ycirc':'\u0176','ycy':'\u044B','Ycy':'\u042B','yen':'\xA5','yfr':'\uD835\uDD36','Yfr':'\uD835\uDD1C','yicy':'\u0457','YIcy':'\u0407','yopf':'\uD835\uDD6A','Yopf':'\uD835\uDD50','yscr':'\uD835\uDCCE','Yscr':'\uD835\uDCB4','yucy':'\u044E','YUcy':'\u042E','yuml':'\xFF','Yuml':'\u0178','zacute':'\u017A','Zacute':'\u0179','zcaron':'\u017E','Zcaron':'\u017D','zcy':'\u0437','Zcy':'\u0417','zdot':'\u017C','Zdot':'\u017B','zeetrf':'\u2128','ZeroWidthSpace':'\u200B','zeta':'\u03B6','Zeta':'\u0396','zfr':'\uD835\uDD37','Zfr':'\u2128','zhcy':'\u0436','ZHcy':'\u0416','zigrarr':'\u21DD','zopf':'\uD835\uDD6B','Zopf':'\u2124','zscr':'\uD835\uDCCF','Zscr':'\uD835\uDCB5','zwj':'\u200D','zwnj':'\u200C'}; - var decodeMapLegacy = {'aacute':'\xE1','Aacute':'\xC1','acirc':'\xE2','Acirc':'\xC2','acute':'\xB4','aelig':'\xE6','AElig':'\xC6','agrave':'\xE0','Agrave':'\xC0','amp':'&','AMP':'&','aring':'\xE5','Aring':'\xC5','atilde':'\xE3','Atilde':'\xC3','auml':'\xE4','Auml':'\xC4','brvbar':'\xA6','ccedil':'\xE7','Ccedil':'\xC7','cedil':'\xB8','cent':'\xA2','copy':'\xA9','COPY':'\xA9','curren':'\xA4','deg':'\xB0','divide':'\xF7','eacute':'\xE9','Eacute':'\xC9','ecirc':'\xEA','Ecirc':'\xCA','egrave':'\xE8','Egrave':'\xC8','eth':'\xF0','ETH':'\xD0','euml':'\xEB','Euml':'\xCB','frac12':'\xBD','frac14':'\xBC','frac34':'\xBE','gt':'>','GT':'>','iacute':'\xED','Iacute':'\xCD','icirc':'\xEE','Icirc':'\xCE','iexcl':'\xA1','igrave':'\xEC','Igrave':'\xCC','iquest':'\xBF','iuml':'\xEF','Iuml':'\xCF','laquo':'\xAB','lt':'<','LT':'<','macr':'\xAF','micro':'\xB5','middot':'\xB7','nbsp':'\xA0','not':'\xAC','ntilde':'\xF1','Ntilde':'\xD1','oacute':'\xF3','Oacute':'\xD3','ocirc':'\xF4','Ocirc':'\xD4','ograve':'\xF2','Ograve':'\xD2','ordf':'\xAA','ordm':'\xBA','oslash':'\xF8','Oslash':'\xD8','otilde':'\xF5','Otilde':'\xD5','ouml':'\xF6','Ouml':'\xD6','para':'\xB6','plusmn':'\xB1','pound':'\xA3','quot':'"','QUOT':'"','raquo':'\xBB','reg':'\xAE','REG':'\xAE','sect':'\xA7','shy':'\xAD','sup1':'\xB9','sup2':'\xB2','sup3':'\xB3','szlig':'\xDF','thorn':'\xFE','THORN':'\xDE','times':'\xD7','uacute':'\xFA','Uacute':'\xDA','ucirc':'\xFB','Ucirc':'\xDB','ugrave':'\xF9','Ugrave':'\xD9','uml':'\xA8','uuml':'\xFC','Uuml':'\xDC','yacute':'\xFD','Yacute':'\xDD','yen':'\xA5','yuml':'\xFF'}; - var decodeMapNumeric = {'0':'\uFFFD','128':'\u20AC','130':'\u201A','131':'\u0192','132':'\u201E','133':'\u2026','134':'\u2020','135':'\u2021','136':'\u02C6','137':'\u2030','138':'\u0160','139':'\u2039','140':'\u0152','142':'\u017D','145':'\u2018','146':'\u2019','147':'\u201C','148':'\u201D','149':'\u2022','150':'\u2013','151':'\u2014','152':'\u02DC','153':'\u2122','154':'\u0161','155':'\u203A','156':'\u0153','158':'\u017E','159':'\u0178'}; - var invalidReferenceCodePoints = [1,2,3,4,5,6,7,8,11,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,64976,64977,64978,64979,64980,64981,64982,64983,64984,64985,64986,64987,64988,64989,64990,64991,64992,64993,64994,64995,64996,64997,64998,64999,65000,65001,65002,65003,65004,65005,65006,65007,65534,65535,131070,131071,196606,196607,262142,262143,327678,327679,393214,393215,458750,458751,524286,524287,589822,589823,655358,655359,720894,720895,786430,786431,851966,851967,917502,917503,983038,983039,1048574,1048575,1114110,1114111]; - - /*--------------------------------------------------------------------------*/ - - var stringFromCharCode = String.fromCharCode; - - var object = {}; - var hasOwnProperty = object.hasOwnProperty; - var has = function(object, propertyName) { - return hasOwnProperty.call(object, propertyName); - }; - - var contains = function(array, value) { - var index = -1; - var length = array.length; - while (++index < length) { - if (array[index] == value) { - return true; - } - } - return false; - }; - - var merge = function(options, defaults) { - if (!options) { - return defaults; - } - var result = {}; - var key; - for (key in defaults) { - // A `hasOwnProperty` check is not needed here, since only recognized - // option names are used anyway. Any others are ignored. - result[key] = has(options, key) ? options[key] : defaults[key]; - } - return result; - }; - - // Modified version of `ucs2encode`; see https://mths.be/punycode. - var codePointToSymbol = function(codePoint, strict) { - var output = ''; - if ((codePoint >= 0xD800 && codePoint <= 0xDFFF) || codePoint > 0x10FFFF) { - // See issue #4: - // “Otherwise, if the number is in the range 0xD800 to 0xDFFF or is - // greater than 0x10FFFF, then this is a parse error. Return a U+FFFD - // REPLACEMENT CHARACTER.” - if (strict) { - parseError('character reference outside the permissible Unicode range'); - } - return '\uFFFD'; - } - if (has(decodeMapNumeric, codePoint)) { - if (strict) { - parseError('disallowed character reference'); - } - return decodeMapNumeric[codePoint]; - } - if (strict && contains(invalidReferenceCodePoints, codePoint)) { - parseError('disallowed character reference'); - } - if (codePoint > 0xFFFF) { - codePoint -= 0x10000; - output += stringFromCharCode(codePoint >>> 10 & 0x3FF | 0xD800); - codePoint = 0xDC00 | codePoint & 0x3FF; - } - output += stringFromCharCode(codePoint); - return output; - }; - - var hexEscape = function(codePoint) { - return '&#x' + codePoint.toString(16).toUpperCase() + ';'; - }; - - var decEscape = function(codePoint) { - return '&#' + codePoint + ';'; - }; - - var parseError = function(message) { - throw Error('Parse error: ' + message); - }; - - /*--------------------------------------------------------------------------*/ - - var encode = function(string, options) { - options = merge(options, encode.options); - var strict = options.strict; - if (strict && regexInvalidRawCodePoint.test(string)) { - parseError('forbidden code point'); - } - var encodeEverything = options.encodeEverything; - var useNamedReferences = options.useNamedReferences; - var allowUnsafeSymbols = options.allowUnsafeSymbols; - var escapeCodePoint = options.decimal ? decEscape : hexEscape; - - var escapeBmpSymbol = function(symbol) { - return escapeCodePoint(symbol.charCodeAt(0)); - }; - - if (encodeEverything) { - // Encode ASCII symbols. - string = string.replace(regexAsciiWhitelist, function(symbol) { - // Use named references if requested & possible. - if (useNamedReferences && has(encodeMap, symbol)) { - return '&' + encodeMap[symbol] + ';'; - } - return escapeBmpSymbol(symbol); - }); - // Shorten a few escapes that represent two symbols, of which at least one - // is within the ASCII range. - if (useNamedReferences) { - string = string - .replace(/>\u20D2/g, '>⃒') - .replace(/<\u20D2/g, '<⃒') - .replace(/fj/g, 'fj'); - } - // Encode non-ASCII symbols. - if (useNamedReferences) { - // Encode non-ASCII symbols that can be replaced with a named reference. - string = string.replace(regexEncodeNonAscii, function(string) { - // Note: there is no need to check `has(encodeMap, string)` here. - return '&' + encodeMap[string] + ';'; - }); - } - // Note: any remaining non-ASCII symbols are handled outside of the `if`. - } else if (useNamedReferences) { - // Apply named character references. - // Encode `<>"'&` using named character references. - if (!allowUnsafeSymbols) { - string = string.replace(regexEscape, function(string) { - return '&' + encodeMap[string] + ';'; // no need to check `has()` here - }); - } - // Shorten escapes that represent two symbols, of which at least one is - // `<>"'&`. - string = string - .replace(/>\u20D2/g, '>⃒') - .replace(/<\u20D2/g, '<⃒'); - // Encode non-ASCII symbols that can be replaced with a named reference. - string = string.replace(regexEncodeNonAscii, function(string) { - // Note: there is no need to check `has(encodeMap, string)` here. - return '&' + encodeMap[string] + ';'; - }); - } else if (!allowUnsafeSymbols) { - // Encode `<>"'&` using hexadecimal escapes, now that they’re not handled - // using named character references. - string = string.replace(regexEscape, escapeBmpSymbol); - } - return string - // Encode astral symbols. - .replace(regexAstralSymbols, function($0) { - // https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae - var high = $0.charCodeAt(0); - var low = $0.charCodeAt(1); - var codePoint = (high - 0xD800) * 0x400 + low - 0xDC00 + 0x10000; - return escapeCodePoint(codePoint); - }) - // Encode any remaining BMP symbols that are not printable ASCII symbols - // using a hexadecimal escape. - .replace(regexBmpWhitelist, escapeBmpSymbol); - }; - // Expose default options (so they can be overridden globally). - encode.options = { - 'allowUnsafeSymbols': false, - 'encodeEverything': false, - 'strict': false, - 'useNamedReferences': false, - 'decimal' : false - }; - - var decode = function(html, options) { - options = merge(options, decode.options); - var strict = options.strict; - if (strict && regexInvalidEntity.test(html)) { - parseError('malformed character reference'); - } - return html.replace(regexDecode, function($0, $1, $2, $3, $4, $5, $6, $7, $8) { - var codePoint; - var semicolon; - var decDigits; - var hexDigits; - var reference; - var next; - - if ($1) { - reference = $1; - // Note: there is no need to check `has(decodeMap, reference)`. - return decodeMap[reference]; - } - - if ($2) { - // Decode named character references without trailing `;`, e.g. `&`. - // This is only a parse error if it gets converted to `&`, or if it is - // followed by `=` in an attribute context. - reference = $2; - next = $3; - if (next && options.isAttributeValue) { - if (strict && next == '=') { - parseError('`&` did not start a character reference'); - } - return $0; - } else { - if (strict) { - parseError( - 'named character reference was not terminated by a semicolon' - ); - } - // Note: there is no need to check `has(decodeMapLegacy, reference)`. - return decodeMapLegacy[reference] + (next || ''); - } - } - - if ($4) { - // Decode decimal escapes, e.g. `𝌆`. - decDigits = $4; - semicolon = $5; - if (strict && !semicolon) { - parseError('character reference was not terminated by a semicolon'); - } - codePoint = parseInt(decDigits, 10); - return codePointToSymbol(codePoint, strict); - } - - if ($6) { - // Decode hexadecimal escapes, e.g. `𝌆`. - hexDigits = $6; - semicolon = $7; - if (strict && !semicolon) { - parseError('character reference was not terminated by a semicolon'); - } - codePoint = parseInt(hexDigits, 16); - return codePointToSymbol(codePoint, strict); - } - - // If we’re still here, `if ($7)` is implied; it’s an ambiguous - // ampersand for sure. https://mths.be/notes/ambiguous-ampersands - if (strict) { - parseError( - 'named character reference was not terminated by a semicolon' - ); - } - return $0; - }); - }; - // Expose default options (so they can be overridden globally). - decode.options = { - 'isAttributeValue': false, - 'strict': false - }; - - var escape = function(string) { - return string.replace(regexEscape, function($0) { - // Note: there is no need to check `has(escapeMap, $0)` here. - return escapeMap[$0]; - }); - }; - - /*--------------------------------------------------------------------------*/ - - var he = { - 'version': '1.2.0', - 'encode': encode, - 'decode': decode, - 'escape': escape, - 'unescape': decode - }; - - // Some AMD build optimizers, like r.js, check for specific condition patterns - // like the following: - if ( - false - ) { - define(function() { - return he; - }); - } else if (freeExports && !freeExports.nodeType) { - if (freeModule) { // in Node.js, io.js, or RingoJS v0.8.0+ - freeModule.exports = he; - } else { // in Narwhal or RingoJS v0.7.0- - for (var key in he) { - has(he, key) && (freeExports[key] = he[key]); - } - } - } else { // in Rhino or a web browser - root.he = he; - } - -}(this)); - -}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{}],55:[function(require,module,exports){ -exports.read = function (buffer, offset, isLE, mLen, nBytes) { - var e, m - var eLen = (nBytes * 8) - mLen - 1 - var eMax = (1 << eLen) - 1 - var eBias = eMax >> 1 - var nBits = -7 - var i = isLE ? (nBytes - 1) : 0 - var d = isLE ? -1 : 1 - var s = buffer[offset + i] - - i += d - - e = s & ((1 << (-nBits)) - 1) - s >>= (-nBits) - nBits += eLen - for (; nBits > 0; e = (e * 256) + buffer[offset + i], i += d, nBits -= 8) {} - - m = e & ((1 << (-nBits)) - 1) - e >>= (-nBits) - nBits += mLen - for (; nBits > 0; m = (m * 256) + buffer[offset + i], i += d, nBits -= 8) {} - - if (e === 0) { - e = 1 - eBias - } else if (e === eMax) { - return m ? NaN : ((s ? -1 : 1) * Infinity) - } else { - m = m + Math.pow(2, mLen) - e = e - eBias - } - return (s ? -1 : 1) * m * Math.pow(2, e - mLen) -} - -exports.write = function (buffer, value, offset, isLE, mLen, nBytes) { - var e, m, c - var eLen = (nBytes * 8) - mLen - 1 - var eMax = (1 << eLen) - 1 - var eBias = eMax >> 1 - var rt = (mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0) - var i = isLE ? 0 : (nBytes - 1) - var d = isLE ? 1 : -1 - var s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0 - - value = Math.abs(value) - - if (isNaN(value) || value === Infinity) { - m = isNaN(value) ? 1 : 0 - e = eMax - } else { - e = Math.floor(Math.log(value) / Math.LN2) - if (value * (c = Math.pow(2, -e)) < 1) { - e-- - c *= 2 - } - if (e + eBias >= 1) { - value += rt / c - } else { - value += rt * Math.pow(2, 1 - eBias) - } - if (value * c >= 2) { - e++ - c /= 2 - } - - if (e + eBias >= eMax) { - m = 0 - e = eMax - } else if (e + eBias >= 1) { - m = ((value * c) - 1) * Math.pow(2, mLen) - e = e + eBias - } else { - m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen) - e = 0 - } - } - - for (; mLen >= 8; buffer[offset + i] = m & 0xff, i += d, m /= 256, mLen -= 8) {} - - e = (e << mLen) | m - eLen += mLen - for (; eLen > 0; buffer[offset + i] = e & 0xff, i += d, e /= 256, eLen -= 8) {} - - buffer[offset + i - d] |= s * 128 -} - -},{}],56:[function(require,module,exports){ -if (typeof Object.create === 'function') { - // implementation from standard node.js 'util' module - module.exports = function inherits(ctor, superCtor) { - ctor.super_ = superCtor - ctor.prototype = Object.create(superCtor.prototype, { - constructor: { - value: ctor, - enumerable: false, - writable: true, - configurable: true - } - }); - }; -} else { - // old school shim for old browsers - module.exports = function inherits(ctor, superCtor) { - ctor.super_ = superCtor - var TempCtor = function () {} - TempCtor.prototype = superCtor.prototype - ctor.prototype = new TempCtor() - ctor.prototype.constructor = ctor - } -} - -},{}],57:[function(require,module,exports){ -/*! - * Determine if an object is a Buffer - * - * @author Feross Aboukhadijeh - * @license MIT - */ - -// The _isBuffer check is for Safari 5-7 support, because it's missing -// Object.prototype.constructor. Remove this eventually -module.exports = function (obj) { - return obj != null && (isBuffer(obj) || isSlowBuffer(obj) || !!obj._isBuffer) -} - -function isBuffer (obj) { - return !!obj.constructor && typeof obj.constructor.isBuffer === 'function' && obj.constructor.isBuffer(obj) -} - -// For Node v0.10 support. Remove this eventually. -function isSlowBuffer (obj) { - return typeof obj.readFloatLE === 'function' && typeof obj.slice === 'function' && isBuffer(obj.slice(0, 0)) -} - -},{}],58:[function(require,module,exports){ -var toString = {}.toString; - -module.exports = Array.isArray || function (arr) { - return toString.call(arr) == '[object Array]'; -}; - -},{}],59:[function(require,module,exports){ -var path = require('path'); -var fs = require('fs'); -var _0777 = parseInt('0777', 8); - -module.exports = mkdirP.mkdirp = mkdirP.mkdirP = mkdirP; - -function mkdirP (p, opts, f, made) { - if (typeof opts === 'function') { - f = opts; - opts = {}; - } - else if (!opts || typeof opts !== 'object') { - opts = { mode: opts }; - } - - var mode = opts.mode; - var xfs = opts.fs || fs; - - if (mode === undefined) { - mode = _0777 - } - if (!made) made = null; - - var cb = f || function () {}; - p = path.resolve(p); - - xfs.mkdir(p, mode, function (er) { - if (!er) { - made = made || p; - return cb(null, made); - } - switch (er.code) { - case 'ENOENT': - if (path.dirname(p) === p) return cb(er); - mkdirP(path.dirname(p), opts, function (er, made) { - if (er) cb(er, made); - else mkdirP(p, opts, cb, made); - }); - break; - - // In the case of any other error, just see if there's a dir - // there already. If so, then hooray! If not, then something - // is borked. - default: - xfs.stat(p, function (er2, stat) { - // if the stat fails, then that's super weird. - // let the original error be the failure reason. - if (er2 || !stat.isDirectory()) cb(er, made) - else cb(null, made); - }); - break; - } - }); -} - -mkdirP.sync = function sync (p, opts, made) { - if (!opts || typeof opts !== 'object') { - opts = { mode: opts }; - } - - var mode = opts.mode; - var xfs = opts.fs || fs; - - if (mode === undefined) { - mode = _0777 - } - if (!made) made = null; - - p = path.resolve(p); - - try { - xfs.mkdirSync(p, mode); - made = made || p; - } - catch (err0) { - switch (err0.code) { - case 'ENOENT' : - made = sync(path.dirname(p), opts, made); - sync(p, opts, made); - break; - - // In the case of any other error, just see if there's a dir - // there already. If so, then hooray! If not, then something - // is borked. - default: - var stat; - try { - stat = xfs.statSync(p); - } - catch (err1) { - throw err0; - } - if (!stat.isDirectory()) throw err0; - break; - } - } - - return made; -}; - -},{"fs":42,"path":42}],60:[function(require,module,exports){ -/** - * Helpers. - */ - -var s = 1000; -var m = s * 60; -var h = m * 60; -var d = h * 24; -var w = d * 7; -var y = d * 365.25; - -/** - * Parse or format the given `val`. - * - * Options: - * - * - `long` verbose formatting [false] - * - * @param {String|Number} val - * @param {Object} [options] - * @throws {Error} throw an error if val is not a non-empty string or a number - * @return {String|Number} - * @api public - */ - -module.exports = function(val, options) { - options = options || {}; - var type = typeof val; - if (type === 'string' && val.length > 0) { - return parse(val); - } else if (type === 'number' && isNaN(val) === false) { - return options.long ? fmtLong(val) : fmtShort(val); - } - throw new Error( - 'val is not a non-empty string or a valid number. val=' + - JSON.stringify(val) - ); -}; - -/** - * Parse the given `str` and return milliseconds. - * - * @param {String} str - * @return {Number} - * @api private - */ - -function parse(str) { - str = String(str); - if (str.length > 100) { - return; - } - var match = /^((?:\d+)?\-?\d?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec( - str - ); - if (!match) { - return; - } - var n = parseFloat(match[1]); - var type = (match[2] || 'ms').toLowerCase(); - switch (type) { - case 'years': - case 'year': - case 'yrs': - case 'yr': - case 'y': - return n * y; - case 'weeks': - case 'week': - case 'w': - return n * w; - case 'days': - case 'day': - case 'd': - return n * d; - case 'hours': - case 'hour': - case 'hrs': - case 'hr': - case 'h': - return n * h; - case 'minutes': - case 'minute': - case 'mins': - case 'min': - case 'm': - return n * m; - case 'seconds': - case 'second': - case 'secs': - case 'sec': - case 's': - return n * s; - case 'milliseconds': - case 'millisecond': - case 'msecs': - case 'msec': - case 'ms': - return n; - default: - return undefined; - } -} - -/** - * Short format for `ms`. - * - * @param {Number} ms - * @return {String} - * @api private - */ - -function fmtShort(ms) { - var msAbs = Math.abs(ms); - if (msAbs >= d) { - return Math.round(ms / d) + 'd'; - } - if (msAbs >= h) { - return Math.round(ms / h) + 'h'; - } - if (msAbs >= m) { - return Math.round(ms / m) + 'm'; - } - if (msAbs >= s) { - return Math.round(ms / s) + 's'; - } - return ms + 'ms'; -} - -/** - * Long format for `ms`. - * - * @param {Number} ms - * @return {String} - * @api private - */ - -function fmtLong(ms) { - var msAbs = Math.abs(ms); - if (msAbs >= d) { - return plural(ms, msAbs, d, 'day'); - } - if (msAbs >= h) { - return plural(ms, msAbs, h, 'hour'); - } - if (msAbs >= m) { - return plural(ms, msAbs, m, 'minute'); - } - if (msAbs >= s) { - return plural(ms, msAbs, s, 'second'); - } - return ms + ' ms'; -} - -/** - * Pluralization helper. - */ - -function plural(ms, msAbs, n, name) { - var isPlural = msAbs >= n * 1.5; - return Math.round(ms / n) + ' ' + name + (isPlural ? 's' : ''); -} - -},{}],61:[function(require,module,exports){ -'use strict'; - -var keysShim; -if (!Object.keys) { - // modified from https://github.com/es-shims/es5-shim - var has = Object.prototype.hasOwnProperty; - var toStr = Object.prototype.toString; - var isArgs = require('./isArguments'); // eslint-disable-line global-require - var isEnumerable = Object.prototype.propertyIsEnumerable; - var hasDontEnumBug = !isEnumerable.call({ toString: null }, 'toString'); - var hasProtoEnumBug = isEnumerable.call(function () {}, 'prototype'); - var dontEnums = [ - 'toString', - 'toLocaleString', - 'valueOf', - 'hasOwnProperty', - 'isPrototypeOf', - 'propertyIsEnumerable', - 'constructor' - ]; - var equalsConstructorPrototype = function (o) { - var ctor = o.constructor; - return ctor && ctor.prototype === o; - }; - var excludedKeys = { - $applicationCache: true, - $console: true, - $external: true, - $frame: true, - $frameElement: true, - $frames: true, - $innerHeight: true, - $innerWidth: true, - $outerHeight: true, - $outerWidth: true, - $pageXOffset: true, - $pageYOffset: true, - $parent: true, - $scrollLeft: true, - $scrollTop: true, - $scrollX: true, - $scrollY: true, - $self: true, - $webkitIndexedDB: true, - $webkitStorageInfo: true, - $window: true - }; - var hasAutomationEqualityBug = (function () { - /* global window */ - if (typeof window === 'undefined') { return false; } - for (var k in window) { - try { - if (!excludedKeys['$' + k] && has.call(window, k) && window[k] !== null && typeof window[k] === 'object') { - try { - equalsConstructorPrototype(window[k]); - } catch (e) { - return true; - } - } - } catch (e) { - return true; - } - } - return false; - }()); - var equalsConstructorPrototypeIfNotBuggy = function (o) { - /* global window */ - if (typeof window === 'undefined' || !hasAutomationEqualityBug) { - return equalsConstructorPrototype(o); - } - try { - return equalsConstructorPrototype(o); - } catch (e) { - return false; - } - }; - - keysShim = function keys(object) { - var isObject = object !== null && typeof object === 'object'; - var isFunction = toStr.call(object) === '[object Function]'; - var isArguments = isArgs(object); - var isString = isObject && toStr.call(object) === '[object String]'; - var theKeys = []; - - if (!isObject && !isFunction && !isArguments) { - throw new TypeError('Object.keys called on a non-object'); - } - - var skipProto = hasProtoEnumBug && isFunction; - if (isString && object.length > 0 && !has.call(object, 0)) { - for (var i = 0; i < object.length; ++i) { - theKeys.push(String(i)); - } - } - - if (isArguments && object.length > 0) { - for (var j = 0; j < object.length; ++j) { - theKeys.push(String(j)); - } - } else { - for (var name in object) { - if (!(skipProto && name === 'prototype') && has.call(object, name)) { - theKeys.push(String(name)); - } - } - } - - if (hasDontEnumBug) { - var skipConstructor = equalsConstructorPrototypeIfNotBuggy(object); - - for (var k = 0; k < dontEnums.length; ++k) { - if (!(skipConstructor && dontEnums[k] === 'constructor') && has.call(object, dontEnums[k])) { - theKeys.push(dontEnums[k]); - } - } - } - return theKeys; - }; -} -module.exports = keysShim; - -},{"./isArguments":63}],62:[function(require,module,exports){ -'use strict'; - -var slice = Array.prototype.slice; -var isArgs = require('./isArguments'); - -var origKeys = Object.keys; -var keysShim = origKeys ? function keys(o) { return origKeys(o); } : require('./implementation'); - -var originalKeys = Object.keys; - -keysShim.shim = function shimObjectKeys() { - if (Object.keys) { - var keysWorksWithArguments = (function () { - // Safari 5.0 bug - var args = Object.keys(arguments); - return args && args.length === arguments.length; - }(1, 2)); - if (!keysWorksWithArguments) { - Object.keys = function keys(object) { // eslint-disable-line func-name-matching - if (isArgs(object)) { - return originalKeys(slice.call(object)); - } - return originalKeys(object); - }; - } - } else { - Object.keys = keysShim; - } - return Object.keys || keysShim; -}; - -module.exports = keysShim; - -},{"./implementation":61,"./isArguments":63}],63:[function(require,module,exports){ -'use strict'; - -var toStr = Object.prototype.toString; - -module.exports = function isArguments(value) { - var str = toStr.call(value); - var isArgs = str === '[object Arguments]'; - if (!isArgs) { - isArgs = str !== '[object Array]' && - value !== null && - typeof value === 'object' && - typeof value.length === 'number' && - value.length >= 0 && - toStr.call(value.callee) === '[object Function]'; - } - return isArgs; -}; - -},{}],64:[function(require,module,exports){ -'use strict'; - -// modified from https://github.com/es-shims/es6-shim -var keys = require('object-keys'); -var bind = require('function-bind'); -var canBeObject = function (obj) { - return typeof obj !== 'undefined' && obj !== null; -}; -var hasSymbols = require('has-symbols/shams')(); -var toObject = Object; -var push = bind.call(Function.call, Array.prototype.push); -var propIsEnumerable = bind.call(Function.call, Object.prototype.propertyIsEnumerable); -var originalGetSymbols = hasSymbols ? Object.getOwnPropertySymbols : null; - -module.exports = function assign(target, source1) { - if (!canBeObject(target)) { throw new TypeError('target must be an object'); } - var objTarget = toObject(target); - var s, source, i, props, syms, value, key; - for (s = 1; s < arguments.length; ++s) { - source = toObject(arguments[s]); - props = keys(source); - var getSymbols = hasSymbols && (Object.getOwnPropertySymbols || originalGetSymbols); - if (getSymbols) { - syms = getSymbols(source); - for (i = 0; i < syms.length; ++i) { - key = syms[i]; - if (propIsEnumerable(source, key)) { - push(props, key); - } - } - } - for (i = 0; i < props.length; ++i) { - key = props[i]; - value = source[key]; - if (propIsEnumerable(source, key)) { - objTarget[key] = value; - } - } - } - return objTarget; -}; - -},{"function-bind":52,"has-symbols/shams":53,"object-keys":62}],65:[function(require,module,exports){ -'use strict'; - -var defineProperties = require('define-properties'); - -var implementation = require('./implementation'); -var getPolyfill = require('./polyfill'); -var shim = require('./shim'); - -var polyfill = getPolyfill(); - -defineProperties(polyfill, { - getPolyfill: getPolyfill, - implementation: implementation, - shim: shim -}); - -module.exports = polyfill; - -},{"./implementation":64,"./polyfill":66,"./shim":67,"define-properties":47}],66:[function(require,module,exports){ -'use strict'; - -var implementation = require('./implementation'); - -var lacksProperEnumerationOrder = function () { - if (!Object.assign) { - return false; - } - // v8, specifically in node 4.x, has a bug with incorrect property enumeration order - // note: this does not detect the bug unless there's 20 characters - var str = 'abcdefghijklmnopqrst'; - var letters = str.split(''); - var map = {}; - for (var i = 0; i < letters.length; ++i) { - map[letters[i]] = letters[i]; - } - var obj = Object.assign({}, map); - var actual = ''; - for (var k in obj) { - actual += k; - } - return str !== actual; -}; - -var assignHasPendingExceptions = function () { - if (!Object.assign || !Object.preventExtensions) { - return false; - } - // Firefox 37 still has "pending exception" logic in its Object.assign implementation, - // which is 72% slower than our shim, and Firefox 40's native implementation. - var thrower = Object.preventExtensions({ 1: 2 }); - try { - Object.assign(thrower, 'xy'); - } catch (e) { - return thrower[1] === 'y'; - } - return false; -}; - -module.exports = function getPolyfill() { - if (!Object.assign) { - return implementation; - } - if (lacksProperEnumerationOrder()) { - return implementation; - } - if (assignHasPendingExceptions()) { - return implementation; - } - return Object.assign; -}; - -},{"./implementation":64}],67:[function(require,module,exports){ -'use strict'; - -var define = require('define-properties'); -var getPolyfill = require('./polyfill'); - -module.exports = function shimAssign() { - var polyfill = getPolyfill(); - define( - Object, - { assign: polyfill }, - { assign: function () { return Object.assign !== polyfill; } } - ); - return polyfill; -}; - -},{"./polyfill":66,"define-properties":47}],68:[function(require,module,exports){ -(function (process){ -'use strict'; - -if (!process.version || - process.version.indexOf('v0.') === 0 || - process.version.indexOf('v1.') === 0 && process.version.indexOf('v1.8.') !== 0) { - module.exports = { nextTick: nextTick }; -} else { - module.exports = process -} - -function nextTick(fn, arg1, arg2, arg3) { - if (typeof fn !== 'function') { - throw new TypeError('"callback" argument must be a function'); - } - var len = arguments.length; - var args, i; - switch (len) { - case 0: - case 1: - return process.nextTick(fn); - case 2: - return process.nextTick(function afterTickOne() { - fn.call(null, arg1); - }); - case 3: - return process.nextTick(function afterTickTwo() { - fn.call(null, arg1, arg2); - }); - case 4: - return process.nextTick(function afterTickThree() { - fn.call(null, arg1, arg2, arg3); - }); - default: - args = new Array(len - 1); - i = 0; - while (i < args.length) { - args[i++] = arguments[i]; - } - return process.nextTick(function afterTick() { - fn.apply(null, args); - }); - } -} - - -}).call(this,require('_process')) -},{"_process":69}],69:[function(require,module,exports){ -// shim for using process in browser -var process = module.exports = {}; - -// cached from whatever global is present so that test runners that stub it -// don't break things. But we need to wrap it in a try catch in case it is -// wrapped in strict mode code which doesn't define any globals. It's inside a -// function because try/catches deoptimize in certain engines. - -var cachedSetTimeout; -var cachedClearTimeout; - -function defaultSetTimout() { - throw new Error('setTimeout has not been defined'); -} -function defaultClearTimeout () { - throw new Error('clearTimeout has not been defined'); -} -(function () { - try { - if (typeof setTimeout === 'function') { - cachedSetTimeout = setTimeout; - } else { - cachedSetTimeout = defaultSetTimout; - } - } catch (e) { - cachedSetTimeout = defaultSetTimout; - } - try { - if (typeof clearTimeout === 'function') { - cachedClearTimeout = clearTimeout; - } else { - cachedClearTimeout = defaultClearTimeout; - } - } catch (e) { - cachedClearTimeout = defaultClearTimeout; - } -} ()) -function runTimeout(fun) { - if (cachedSetTimeout === setTimeout) { - //normal enviroments in sane situations - return setTimeout(fun, 0); - } - // if setTimeout wasn't available but was latter defined - if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) { - cachedSetTimeout = setTimeout; - return setTimeout(fun, 0); - } - try { - // when when somebody has screwed with setTimeout but no I.E. maddness - return cachedSetTimeout(fun, 0); - } catch(e){ - try { - // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally - return cachedSetTimeout.call(null, fun, 0); - } catch(e){ - // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error - return cachedSetTimeout.call(this, fun, 0); - } - } - - -} -function runClearTimeout(marker) { - if (cachedClearTimeout === clearTimeout) { - //normal enviroments in sane situations - return clearTimeout(marker); - } - // if clearTimeout wasn't available but was latter defined - if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) { - cachedClearTimeout = clearTimeout; - return clearTimeout(marker); - } - try { - // when when somebody has screwed with setTimeout but no I.E. maddness - return cachedClearTimeout(marker); - } catch (e){ - try { - // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally - return cachedClearTimeout.call(null, marker); - } catch (e){ - // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error. - // Some versions of I.E. have different rules for clearTimeout vs setTimeout - return cachedClearTimeout.call(this, marker); - } - } - - - -} -var queue = []; -var draining = false; -var currentQueue; -var queueIndex = -1; - -function cleanUpNextTick() { - if (!draining || !currentQueue) { - return; - } - draining = false; - if (currentQueue.length) { - queue = currentQueue.concat(queue); - } else { - queueIndex = -1; - } - if (queue.length) { - drainQueue(); - } -} - -function drainQueue() { - if (draining) { - return; - } - var timeout = runTimeout(cleanUpNextTick); - draining = true; - - var len = queue.length; - while(len) { - currentQueue = queue; - queue = []; - while (++queueIndex < len) { - if (currentQueue) { - currentQueue[queueIndex].run(); - } - } - queueIndex = -1; - len = queue.length; - } - currentQueue = null; - draining = false; - runClearTimeout(timeout); -} - -process.nextTick = function (fun) { - var args = new Array(arguments.length - 1); - if (arguments.length > 1) { - for (var i = 1; i < arguments.length; i++) { - args[i - 1] = arguments[i]; - } - } - queue.push(new Item(fun, args)); - if (queue.length === 1 && !draining) { - runTimeout(drainQueue); - } -}; - -// v8 likes predictible objects -function Item(fun, array) { - this.fun = fun; - this.array = array; -} -Item.prototype.run = function () { - this.fun.apply(null, this.array); -}; -process.title = 'browser'; -process.browser = true; -process.env = {}; -process.argv = []; -process.version = ''; // empty string to avoid regexp issues -process.versions = {}; - -function noop() {} - -process.on = noop; -process.addListener = noop; -process.once = noop; -process.off = noop; -process.removeListener = noop; -process.removeAllListeners = noop; -process.emit = noop; -process.prependListener = noop; -process.prependOnceListener = noop; - -process.listeners = function (name) { return [] } - -process.binding = function (name) { - throw new Error('process.binding is not supported'); -}; - -process.cwd = function () { return '/' }; -process.chdir = function (dir) { - throw new Error('process.chdir is not supported'); -}; -process.umask = function() { return 0; }; - -},{}],70:[function(require,module,exports){ -module.exports = require('./lib/_stream_duplex.js'); - -},{"./lib/_stream_duplex.js":71}],71:[function(require,module,exports){ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -// a duplex stream is just a stream that is both readable and writable. -// Since JS doesn't have multiple prototypal inheritance, this class -// prototypally inherits from Readable, and then parasitically from -// Writable. - -'use strict'; - -/**/ - -var pna = require('process-nextick-args'); -/**/ - -/**/ -var objectKeys = Object.keys || function (obj) { - var keys = []; - for (var key in obj) { - keys.push(key); - }return keys; -}; -/**/ - -module.exports = Duplex; - -/**/ -var util = require('core-util-is'); -util.inherits = require('inherits'); -/**/ - -var Readable = require('./_stream_readable'); -var Writable = require('./_stream_writable'); - -util.inherits(Duplex, Readable); - -{ - // avoid scope creep, the keys array can then be collected - var keys = objectKeys(Writable.prototype); - for (var v = 0; v < keys.length; v++) { - var method = keys[v]; - if (!Duplex.prototype[method]) Duplex.prototype[method] = Writable.prototype[method]; - } -} - -function Duplex(options) { - if (!(this instanceof Duplex)) return new Duplex(options); - - Readable.call(this, options); - Writable.call(this, options); - - if (options && options.readable === false) this.readable = false; - - if (options && options.writable === false) this.writable = false; - - this.allowHalfOpen = true; - if (options && options.allowHalfOpen === false) this.allowHalfOpen = false; - - this.once('end', onend); -} - -Object.defineProperty(Duplex.prototype, 'writableHighWaterMark', { - // making it explicit this property is not enumerable - // because otherwise some prototype manipulation in - // userland will fail - enumerable: false, - get: function () { - return this._writableState.highWaterMark; - } -}); - -// the no-half-open enforcer -function onend() { - // if we allow half-open state, or if the writable side ended, - // then we're ok. - if (this.allowHalfOpen || this._writableState.ended) return; - - // no more data can be written. - // But allow more writes to happen in this tick. - pna.nextTick(onEndNT, this); -} - -function onEndNT(self) { - self.end(); -} - -Object.defineProperty(Duplex.prototype, 'destroyed', { - get: function () { - if (this._readableState === undefined || this._writableState === undefined) { - return false; - } - return this._readableState.destroyed && this._writableState.destroyed; - }, - set: function (value) { - // we ignore the value if the stream - // has not been initialized yet - if (this._readableState === undefined || this._writableState === undefined) { - return; - } - - // backward compatibility, the user is explicitly - // managing destroyed - this._readableState.destroyed = value; - this._writableState.destroyed = value; - } -}); - -Duplex.prototype._destroy = function (err, cb) { - this.push(null); - this.end(); - - pna.nextTick(cb, err); -}; -},{"./_stream_readable":73,"./_stream_writable":75,"core-util-is":44,"inherits":56,"process-nextick-args":68}],72:[function(require,module,exports){ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -// a passthrough stream. -// basically just the most minimal sort of Transform stream. -// Every written chunk gets output as-is. - -'use strict'; - -module.exports = PassThrough; - -var Transform = require('./_stream_transform'); - -/**/ -var util = require('core-util-is'); -util.inherits = require('inherits'); -/**/ - -util.inherits(PassThrough, Transform); - -function PassThrough(options) { - if (!(this instanceof PassThrough)) return new PassThrough(options); - - Transform.call(this, options); -} - -PassThrough.prototype._transform = function (chunk, encoding, cb) { - cb(null, chunk); -}; -},{"./_stream_transform":74,"core-util-is":44,"inherits":56}],73:[function(require,module,exports){ -(function (process,global){ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -'use strict'; - -/**/ - -var pna = require('process-nextick-args'); -/**/ - -module.exports = Readable; - -/**/ -var isArray = require('isarray'); -/**/ - -/**/ -var Duplex; -/**/ - -Readable.ReadableState = ReadableState; - -/**/ -var EE = require('events').EventEmitter; - -var EElistenerCount = function (emitter, type) { - return emitter.listeners(type).length; -}; -/**/ - -/**/ -var Stream = require('./internal/streams/stream'); -/**/ - -/**/ - -var Buffer = require('safe-buffer').Buffer; -var OurUint8Array = global.Uint8Array || function () {}; -function _uint8ArrayToBuffer(chunk) { - return Buffer.from(chunk); -} -function _isUint8Array(obj) { - return Buffer.isBuffer(obj) || obj instanceof OurUint8Array; -} - -/**/ - -/**/ -var util = require('core-util-is'); -util.inherits = require('inherits'); -/**/ - -/**/ -var debugUtil = require('util'); -var debug = void 0; -if (debugUtil && debugUtil.debuglog) { - debug = debugUtil.debuglog('stream'); -} else { - debug = function () {}; -} -/**/ - -var BufferList = require('./internal/streams/BufferList'); -var destroyImpl = require('./internal/streams/destroy'); -var StringDecoder; - -util.inherits(Readable, Stream); - -var kProxyEvents = ['error', 'close', 'destroy', 'pause', 'resume']; - -function prependListener(emitter, event, fn) { - // Sadly this is not cacheable as some libraries bundle their own - // event emitter implementation with them. - if (typeof emitter.prependListener === 'function') return emitter.prependListener(event, fn); - - // This is a hack to make sure that our error handler is attached before any - // userland ones. NEVER DO THIS. This is here only because this code needs - // to continue to work with older versions of Node.js that do not include - // the prependListener() method. The goal is to eventually remove this hack. - if (!emitter._events || !emitter._events[event]) emitter.on(event, fn);else if (isArray(emitter._events[event])) emitter._events[event].unshift(fn);else emitter._events[event] = [fn, emitter._events[event]]; -} - -function ReadableState(options, stream) { - Duplex = Duplex || require('./_stream_duplex'); - - options = options || {}; - - // Duplex streams are both readable and writable, but share - // the same options object. - // However, some cases require setting options to different - // values for the readable and the writable sides of the duplex stream. - // These options can be provided separately as readableXXX and writableXXX. - var isDuplex = stream instanceof Duplex; - - // object stream flag. Used to make read(n) ignore n and to - // make all the buffer merging and length checks go away - this.objectMode = !!options.objectMode; - - if (isDuplex) this.objectMode = this.objectMode || !!options.readableObjectMode; - - // the point at which it stops calling _read() to fill the buffer - // Note: 0 is a valid value, means "don't call _read preemptively ever" - var hwm = options.highWaterMark; - var readableHwm = options.readableHighWaterMark; - var defaultHwm = this.objectMode ? 16 : 16 * 1024; - - if (hwm || hwm === 0) this.highWaterMark = hwm;else if (isDuplex && (readableHwm || readableHwm === 0)) this.highWaterMark = readableHwm;else this.highWaterMark = defaultHwm; - - // cast to ints. - this.highWaterMark = Math.floor(this.highWaterMark); - - // A linked list is used to store data chunks instead of an array because the - // linked list can remove elements from the beginning faster than - // array.shift() - this.buffer = new BufferList(); - this.length = 0; - this.pipes = null; - this.pipesCount = 0; - this.flowing = null; - this.ended = false; - this.endEmitted = false; - this.reading = false; - - // a flag to be able to tell if the event 'readable'/'data' is emitted - // immediately, or on a later tick. We set this to true at first, because - // any actions that shouldn't happen until "later" should generally also - // not happen before the first read call. - this.sync = true; - - // whenever we return null, then we set a flag to say - // that we're awaiting a 'readable' event emission. - this.needReadable = false; - this.emittedReadable = false; - this.readableListening = false; - this.resumeScheduled = false; - - // has it been destroyed - this.destroyed = false; - - // Crypto is kind of old and crusty. Historically, its default string - // encoding is 'binary' so we have to make this configurable. - // Everything else in the universe uses 'utf8', though. - this.defaultEncoding = options.defaultEncoding || 'utf8'; - - // the number of writers that are awaiting a drain event in .pipe()s - this.awaitDrain = 0; - - // if true, a maybeReadMore has been scheduled - this.readingMore = false; - - this.decoder = null; - this.encoding = null; - if (options.encoding) { - if (!StringDecoder) StringDecoder = require('string_decoder/').StringDecoder; - this.decoder = new StringDecoder(options.encoding); - this.encoding = options.encoding; - } -} - -function Readable(options) { - Duplex = Duplex || require('./_stream_duplex'); - - if (!(this instanceof Readable)) return new Readable(options); - - this._readableState = new ReadableState(options, this); - - // legacy - this.readable = true; - - if (options) { - if (typeof options.read === 'function') this._read = options.read; - - if (typeof options.destroy === 'function') this._destroy = options.destroy; - } - - Stream.call(this); -} - -Object.defineProperty(Readable.prototype, 'destroyed', { - get: function () { - if (this._readableState === undefined) { - return false; - } - return this._readableState.destroyed; - }, - set: function (value) { - // we ignore the value if the stream - // has not been initialized yet - if (!this._readableState) { - return; - } - - // backward compatibility, the user is explicitly - // managing destroyed - this._readableState.destroyed = value; - } -}); - -Readable.prototype.destroy = destroyImpl.destroy; -Readable.prototype._undestroy = destroyImpl.undestroy; -Readable.prototype._destroy = function (err, cb) { - this.push(null); - cb(err); -}; - -// Manually shove something into the read() buffer. -// This returns true if the highWaterMark has not been hit yet, -// similar to how Writable.write() returns true if you should -// write() some more. -Readable.prototype.push = function (chunk, encoding) { - var state = this._readableState; - var skipChunkCheck; - - if (!state.objectMode) { - if (typeof chunk === 'string') { - encoding = encoding || state.defaultEncoding; - if (encoding !== state.encoding) { - chunk = Buffer.from(chunk, encoding); - encoding = ''; - } - skipChunkCheck = true; - } - } else { - skipChunkCheck = true; - } - - return readableAddChunk(this, chunk, encoding, false, skipChunkCheck); -}; - -// Unshift should *always* be something directly out of read() -Readable.prototype.unshift = function (chunk) { - return readableAddChunk(this, chunk, null, true, false); -}; - -function readableAddChunk(stream, chunk, encoding, addToFront, skipChunkCheck) { - var state = stream._readableState; - if (chunk === null) { - state.reading = false; - onEofChunk(stream, state); - } else { - var er; - if (!skipChunkCheck) er = chunkInvalid(state, chunk); - if (er) { - stream.emit('error', er); - } else if (state.objectMode || chunk && chunk.length > 0) { - if (typeof chunk !== 'string' && !state.objectMode && Object.getPrototypeOf(chunk) !== Buffer.prototype) { - chunk = _uint8ArrayToBuffer(chunk); - } - - if (addToFront) { - if (state.endEmitted) stream.emit('error', new Error('stream.unshift() after end event'));else addChunk(stream, state, chunk, true); - } else if (state.ended) { - stream.emit('error', new Error('stream.push() after EOF')); - } else { - state.reading = false; - if (state.decoder && !encoding) { - chunk = state.decoder.write(chunk); - if (state.objectMode || chunk.length !== 0) addChunk(stream, state, chunk, false);else maybeReadMore(stream, state); - } else { - addChunk(stream, state, chunk, false); - } - } - } else if (!addToFront) { - state.reading = false; - } - } - - return needMoreData(state); -} - -function addChunk(stream, state, chunk, addToFront) { - if (state.flowing && state.length === 0 && !state.sync) { - stream.emit('data', chunk); - stream.read(0); - } else { - // update the buffer info. - state.length += state.objectMode ? 1 : chunk.length; - if (addToFront) state.buffer.unshift(chunk);else state.buffer.push(chunk); - - if (state.needReadable) emitReadable(stream); - } - maybeReadMore(stream, state); -} - -function chunkInvalid(state, chunk) { - var er; - if (!_isUint8Array(chunk) && typeof chunk !== 'string' && chunk !== undefined && !state.objectMode) { - er = new TypeError('Invalid non-string/buffer chunk'); - } - return er; -} - -// if it's past the high water mark, we can push in some more. -// Also, if we have no data yet, we can stand some -// more bytes. This is to work around cases where hwm=0, -// such as the repl. Also, if the push() triggered a -// readable event, and the user called read(largeNumber) such that -// needReadable was set, then we ought to push more, so that another -// 'readable' event will be triggered. -function needMoreData(state) { - return !state.ended && (state.needReadable || state.length < state.highWaterMark || state.length === 0); -} - -Readable.prototype.isPaused = function () { - return this._readableState.flowing === false; -}; - -// backwards compatibility. -Readable.prototype.setEncoding = function (enc) { - if (!StringDecoder) StringDecoder = require('string_decoder/').StringDecoder; - this._readableState.decoder = new StringDecoder(enc); - this._readableState.encoding = enc; - return this; -}; - -// Don't raise the hwm > 8MB -var MAX_HWM = 0x800000; -function computeNewHighWaterMark(n) { - if (n >= MAX_HWM) { - n = MAX_HWM; - } else { - // Get the next highest power of 2 to prevent increasing hwm excessively in - // tiny amounts - n--; - n |= n >>> 1; - n |= n >>> 2; - n |= n >>> 4; - n |= n >>> 8; - n |= n >>> 16; - n++; - } - return n; -} - -// This function is designed to be inlinable, so please take care when making -// changes to the function body. -function howMuchToRead(n, state) { - if (n <= 0 || state.length === 0 && state.ended) return 0; - if (state.objectMode) return 1; - if (n !== n) { - // Only flow one buffer at a time - if (state.flowing && state.length) return state.buffer.head.data.length;else return state.length; - } - // If we're asking for more than the current hwm, then raise the hwm. - if (n > state.highWaterMark) state.highWaterMark = computeNewHighWaterMark(n); - if (n <= state.length) return n; - // Don't have enough - if (!state.ended) { - state.needReadable = true; - return 0; - } - return state.length; -} - -// you can override either this method, or the async _read(n) below. -Readable.prototype.read = function (n) { - debug('read', n); - n = parseInt(n, 10); - var state = this._readableState; - var nOrig = n; - - if (n !== 0) state.emittedReadable = false; - - // if we're doing read(0) to trigger a readable event, but we - // already have a bunch of data in the buffer, then just trigger - // the 'readable' event and move on. - if (n === 0 && state.needReadable && (state.length >= state.highWaterMark || state.ended)) { - debug('read: emitReadable', state.length, state.ended); - if (state.length === 0 && state.ended) endReadable(this);else emitReadable(this); - return null; - } - - n = howMuchToRead(n, state); - - // if we've ended, and we're now clear, then finish it up. - if (n === 0 && state.ended) { - if (state.length === 0) endReadable(this); - return null; - } - - // All the actual chunk generation logic needs to be - // *below* the call to _read. The reason is that in certain - // synthetic stream cases, such as passthrough streams, _read - // may be a completely synchronous operation which may change - // the state of the read buffer, providing enough data when - // before there was *not* enough. - // - // So, the steps are: - // 1. Figure out what the state of things will be after we do - // a read from the buffer. - // - // 2. If that resulting state will trigger a _read, then call _read. - // Note that this may be asynchronous, or synchronous. Yes, it is - // deeply ugly to write APIs this way, but that still doesn't mean - // that the Readable class should behave improperly, as streams are - // designed to be sync/async agnostic. - // Take note if the _read call is sync or async (ie, if the read call - // has returned yet), so that we know whether or not it's safe to emit - // 'readable' etc. - // - // 3. Actually pull the requested chunks out of the buffer and return. - - // if we need a readable event, then we need to do some reading. - var doRead = state.needReadable; - debug('need readable', doRead); - - // if we currently have less than the highWaterMark, then also read some - if (state.length === 0 || state.length - n < state.highWaterMark) { - doRead = true; - debug('length less than watermark', doRead); - } - - // however, if we've ended, then there's no point, and if we're already - // reading, then it's unnecessary. - if (state.ended || state.reading) { - doRead = false; - debug('reading or ended', doRead); - } else if (doRead) { - debug('do read'); - state.reading = true; - state.sync = true; - // if the length is currently zero, then we *need* a readable event. - if (state.length === 0) state.needReadable = true; - // call internal read method - this._read(state.highWaterMark); - state.sync = false; - // If _read pushed data synchronously, then `reading` will be false, - // and we need to re-evaluate how much data we can return to the user. - if (!state.reading) n = howMuchToRead(nOrig, state); - } - - var ret; - if (n > 0) ret = fromList(n, state);else ret = null; - - if (ret === null) { - state.needReadable = true; - n = 0; - } else { - state.length -= n; - } - - if (state.length === 0) { - // If we have nothing in the buffer, then we want to know - // as soon as we *do* get something into the buffer. - if (!state.ended) state.needReadable = true; - - // If we tried to read() past the EOF, then emit end on the next tick. - if (nOrig !== n && state.ended) endReadable(this); - } - - if (ret !== null) this.emit('data', ret); - - return ret; -}; - -function onEofChunk(stream, state) { - if (state.ended) return; - if (state.decoder) { - var chunk = state.decoder.end(); - if (chunk && chunk.length) { - state.buffer.push(chunk); - state.length += state.objectMode ? 1 : chunk.length; - } - } - state.ended = true; - - // emit 'readable' now to make sure it gets picked up. - emitReadable(stream); -} - -// Don't emit readable right away in sync mode, because this can trigger -// another read() call => stack overflow. This way, it might trigger -// a nextTick recursion warning, but that's not so bad. -function emitReadable(stream) { - var state = stream._readableState; - state.needReadable = false; - if (!state.emittedReadable) { - debug('emitReadable', state.flowing); - state.emittedReadable = true; - if (state.sync) pna.nextTick(emitReadable_, stream);else emitReadable_(stream); - } -} - -function emitReadable_(stream) { - debug('emit readable'); - stream.emit('readable'); - flow(stream); -} - -// at this point, the user has presumably seen the 'readable' event, -// and called read() to consume some data. that may have triggered -// in turn another _read(n) call, in which case reading = true if -// it's in progress. -// However, if we're not ended, or reading, and the length < hwm, -// then go ahead and try to read some more preemptively. -function maybeReadMore(stream, state) { - if (!state.readingMore) { - state.readingMore = true; - pna.nextTick(maybeReadMore_, stream, state); - } -} - -function maybeReadMore_(stream, state) { - var len = state.length; - while (!state.reading && !state.flowing && !state.ended && state.length < state.highWaterMark) { - debug('maybeReadMore read 0'); - stream.read(0); - if (len === state.length) - // didn't get any data, stop spinning. - break;else len = state.length; - } - state.readingMore = false; -} - -// abstract method. to be overridden in specific implementation classes. -// call cb(er, data) where data is <= n in length. -// for virtual (non-string, non-buffer) streams, "length" is somewhat -// arbitrary, and perhaps not very meaningful. -Readable.prototype._read = function (n) { - this.emit('error', new Error('_read() is not implemented')); -}; - -Readable.prototype.pipe = function (dest, pipeOpts) { - var src = this; - var state = this._readableState; - - switch (state.pipesCount) { - case 0: - state.pipes = dest; - break; - case 1: - state.pipes = [state.pipes, dest]; - break; - default: - state.pipes.push(dest); - break; - } - state.pipesCount += 1; - debug('pipe count=%d opts=%j', state.pipesCount, pipeOpts); - - var doEnd = (!pipeOpts || pipeOpts.end !== false) && dest !== process.stdout && dest !== process.stderr; - - var endFn = doEnd ? onend : unpipe; - if (state.endEmitted) pna.nextTick(endFn);else src.once('end', endFn); - - dest.on('unpipe', onunpipe); - function onunpipe(readable, unpipeInfo) { - debug('onunpipe'); - if (readable === src) { - if (unpipeInfo && unpipeInfo.hasUnpiped === false) { - unpipeInfo.hasUnpiped = true; - cleanup(); - } - } - } - - function onend() { - debug('onend'); - dest.end(); - } - - // when the dest drains, it reduces the awaitDrain counter - // on the source. This would be more elegant with a .once() - // handler in flow(), but adding and removing repeatedly is - // too slow. - var ondrain = pipeOnDrain(src); - dest.on('drain', ondrain); - - var cleanedUp = false; - function cleanup() { - debug('cleanup'); - // cleanup event handlers once the pipe is broken - dest.removeListener('close', onclose); - dest.removeListener('finish', onfinish); - dest.removeListener('drain', ondrain); - dest.removeListener('error', onerror); - dest.removeListener('unpipe', onunpipe); - src.removeListener('end', onend); - src.removeListener('end', unpipe); - src.removeListener('data', ondata); - - cleanedUp = true; - - // if the reader is waiting for a drain event from this - // specific writer, then it would cause it to never start - // flowing again. - // So, if this is awaiting a drain, then we just call it now. - // If we don't know, then assume that we are waiting for one. - if (state.awaitDrain && (!dest._writableState || dest._writableState.needDrain)) ondrain(); - } - - // If the user pushes more data while we're writing to dest then we'll end up - // in ondata again. However, we only want to increase awaitDrain once because - // dest will only emit one 'drain' event for the multiple writes. - // => Introduce a guard on increasing awaitDrain. - var increasedAwaitDrain = false; - src.on('data', ondata); - function ondata(chunk) { - debug('ondata'); - increasedAwaitDrain = false; - var ret = dest.write(chunk); - if (false === ret && !increasedAwaitDrain) { - // If the user unpiped during `dest.write()`, it is possible - // to get stuck in a permanently paused state if that write - // also returned false. - // => Check whether `dest` is still a piping destination. - if ((state.pipesCount === 1 && state.pipes === dest || state.pipesCount > 1 && indexOf(state.pipes, dest) !== -1) && !cleanedUp) { - debug('false write response, pause', src._readableState.awaitDrain); - src._readableState.awaitDrain++; - increasedAwaitDrain = true; - } - src.pause(); - } - } - - // if the dest has an error, then stop piping into it. - // however, don't suppress the throwing behavior for this. - function onerror(er) { - debug('onerror', er); - unpipe(); - dest.removeListener('error', onerror); - if (EElistenerCount(dest, 'error') === 0) dest.emit('error', er); - } - - // Make sure our error handler is attached before userland ones. - prependListener(dest, 'error', onerror); - - // Both close and finish should trigger unpipe, but only once. - function onclose() { - dest.removeListener('finish', onfinish); - unpipe(); - } - dest.once('close', onclose); - function onfinish() { - debug('onfinish'); - dest.removeListener('close', onclose); - unpipe(); - } - dest.once('finish', onfinish); - - function unpipe() { - debug('unpipe'); - src.unpipe(dest); - } - - // tell the dest that it's being piped to - dest.emit('pipe', src); - - // start the flow if it hasn't been started already. - if (!state.flowing) { - debug('pipe resume'); - src.resume(); - } - - return dest; -}; - -function pipeOnDrain(src) { - return function () { - var state = src._readableState; - debug('pipeOnDrain', state.awaitDrain); - if (state.awaitDrain) state.awaitDrain--; - if (state.awaitDrain === 0 && EElistenerCount(src, 'data')) { - state.flowing = true; - flow(src); - } - }; -} - -Readable.prototype.unpipe = function (dest) { - var state = this._readableState; - var unpipeInfo = { hasUnpiped: false }; - - // if we're not piping anywhere, then do nothing. - if (state.pipesCount === 0) return this; - - // just one destination. most common case. - if (state.pipesCount === 1) { - // passed in one, but it's not the right one. - if (dest && dest !== state.pipes) return this; - - if (!dest) dest = state.pipes; - - // got a match. - state.pipes = null; - state.pipesCount = 0; - state.flowing = false; - if (dest) dest.emit('unpipe', this, unpipeInfo); - return this; - } - - // slow case. multiple pipe destinations. - - if (!dest) { - // remove all. - var dests = state.pipes; - var len = state.pipesCount; - state.pipes = null; - state.pipesCount = 0; - state.flowing = false; - - for (var i = 0; i < len; i++) { - dests[i].emit('unpipe', this, unpipeInfo); - }return this; - } - - // try to find the right one. - var index = indexOf(state.pipes, dest); - if (index === -1) return this; - - state.pipes.splice(index, 1); - state.pipesCount -= 1; - if (state.pipesCount === 1) state.pipes = state.pipes[0]; - - dest.emit('unpipe', this, unpipeInfo); - - return this; -}; - -// set up data events if they are asked for -// Ensure readable listeners eventually get something -Readable.prototype.on = function (ev, fn) { - var res = Stream.prototype.on.call(this, ev, fn); - - if (ev === 'data') { - // Start flowing on next tick if stream isn't explicitly paused - if (this._readableState.flowing !== false) this.resume(); - } else if (ev === 'readable') { - var state = this._readableState; - if (!state.endEmitted && !state.readableListening) { - state.readableListening = state.needReadable = true; - state.emittedReadable = false; - if (!state.reading) { - pna.nextTick(nReadingNextTick, this); - } else if (state.length) { - emitReadable(this); - } - } - } - - return res; -}; -Readable.prototype.addListener = Readable.prototype.on; - -function nReadingNextTick(self) { - debug('readable nexttick read 0'); - self.read(0); -} - -// pause() and resume() are remnants of the legacy readable stream API -// If the user uses them, then switch into old mode. -Readable.prototype.resume = function () { - var state = this._readableState; - if (!state.flowing) { - debug('resume'); - state.flowing = true; - resume(this, state); - } - return this; -}; - -function resume(stream, state) { - if (!state.resumeScheduled) { - state.resumeScheduled = true; - pna.nextTick(resume_, stream, state); - } -} - -function resume_(stream, state) { - if (!state.reading) { - debug('resume read 0'); - stream.read(0); - } - - state.resumeScheduled = false; - state.awaitDrain = 0; - stream.emit('resume'); - flow(stream); - if (state.flowing && !state.reading) stream.read(0); -} - -Readable.prototype.pause = function () { - debug('call pause flowing=%j', this._readableState.flowing); - if (false !== this._readableState.flowing) { - debug('pause'); - this._readableState.flowing = false; - this.emit('pause'); - } - return this; -}; - -function flow(stream) { - var state = stream._readableState; - debug('flow', state.flowing); - while (state.flowing && stream.read() !== null) {} -} - -// wrap an old-style stream as the async data source. -// This is *not* part of the readable stream interface. -// It is an ugly unfortunate mess of history. -Readable.prototype.wrap = function (stream) { - var _this = this; - - var state = this._readableState; - var paused = false; - - stream.on('end', function () { - debug('wrapped end'); - if (state.decoder && !state.ended) { - var chunk = state.decoder.end(); - if (chunk && chunk.length) _this.push(chunk); - } - - _this.push(null); - }); - - stream.on('data', function (chunk) { - debug('wrapped data'); - if (state.decoder) chunk = state.decoder.write(chunk); - - // don't skip over falsy values in objectMode - if (state.objectMode && (chunk === null || chunk === undefined)) return;else if (!state.objectMode && (!chunk || !chunk.length)) return; - - var ret = _this.push(chunk); - if (!ret) { - paused = true; - stream.pause(); - } - }); - - // proxy all the other methods. - // important when wrapping filters and duplexes. - for (var i in stream) { - if (this[i] === undefined && typeof stream[i] === 'function') { - this[i] = function (method) { - return function () { - return stream[method].apply(stream, arguments); - }; - }(i); - } - } - - // proxy certain important events. - for (var n = 0; n < kProxyEvents.length; n++) { - stream.on(kProxyEvents[n], this.emit.bind(this, kProxyEvents[n])); - } - - // when we try to consume some more bytes, simply unpause the - // underlying stream. - this._read = function (n) { - debug('wrapped _read', n); - if (paused) { - paused = false; - stream.resume(); - } - }; - - return this; -}; - -Object.defineProperty(Readable.prototype, 'readableHighWaterMark', { - // making it explicit this property is not enumerable - // because otherwise some prototype manipulation in - // userland will fail - enumerable: false, - get: function () { - return this._readableState.highWaterMark; - } -}); - -// exposed for testing purposes only. -Readable._fromList = fromList; - -// Pluck off n bytes from an array of buffers. -// Length is the combined lengths of all the buffers in the list. -// This function is designed to be inlinable, so please take care when making -// changes to the function body. -function fromList(n, state) { - // nothing buffered - if (state.length === 0) return null; - - var ret; - if (state.objectMode) ret = state.buffer.shift();else if (!n || n >= state.length) { - // read it all, truncate the list - if (state.decoder) ret = state.buffer.join('');else if (state.buffer.length === 1) ret = state.buffer.head.data;else ret = state.buffer.concat(state.length); - state.buffer.clear(); - } else { - // read part of list - ret = fromListPartial(n, state.buffer, state.decoder); - } - - return ret; -} - -// Extracts only enough buffered data to satisfy the amount requested. -// This function is designed to be inlinable, so please take care when making -// changes to the function body. -function fromListPartial(n, list, hasStrings) { - var ret; - if (n < list.head.data.length) { - // slice is the same for buffers and strings - ret = list.head.data.slice(0, n); - list.head.data = list.head.data.slice(n); - } else if (n === list.head.data.length) { - // first chunk is a perfect match - ret = list.shift(); - } else { - // result spans more than one buffer - ret = hasStrings ? copyFromBufferString(n, list) : copyFromBuffer(n, list); - } - return ret; -} - -// Copies a specified amount of characters from the list of buffered data -// chunks. -// This function is designed to be inlinable, so please take care when making -// changes to the function body. -function copyFromBufferString(n, list) { - var p = list.head; - var c = 1; - var ret = p.data; - n -= ret.length; - while (p = p.next) { - var str = p.data; - var nb = n > str.length ? str.length : n; - if (nb === str.length) ret += str;else ret += str.slice(0, n); - n -= nb; - if (n === 0) { - if (nb === str.length) { - ++c; - if (p.next) list.head = p.next;else list.head = list.tail = null; - } else { - list.head = p; - p.data = str.slice(nb); - } - break; - } - ++c; - } - list.length -= c; - return ret; -} - -// Copies a specified amount of bytes from the list of buffered data chunks. -// This function is designed to be inlinable, so please take care when making -// changes to the function body. -function copyFromBuffer(n, list) { - var ret = Buffer.allocUnsafe(n); - var p = list.head; - var c = 1; - p.data.copy(ret); - n -= p.data.length; - while (p = p.next) { - var buf = p.data; - var nb = n > buf.length ? buf.length : n; - buf.copy(ret, ret.length - n, 0, nb); - n -= nb; - if (n === 0) { - if (nb === buf.length) { - ++c; - if (p.next) list.head = p.next;else list.head = list.tail = null; - } else { - list.head = p; - p.data = buf.slice(nb); - } - break; - } - ++c; - } - list.length -= c; - return ret; -} - -function endReadable(stream) { - var state = stream._readableState; - - // If we get here before consuming all the bytes, then that is a - // bug in node. Should never happen. - if (state.length > 0) throw new Error('"endReadable()" called on non-empty stream'); - - if (!state.endEmitted) { - state.ended = true; - pna.nextTick(endReadableNT, state, stream); - } -} - -function endReadableNT(state, stream) { - // Check that we didn't get one last unshift. - if (!state.endEmitted && state.length === 0) { - state.endEmitted = true; - stream.readable = false; - stream.emit('end'); - } -} - -function indexOf(xs, x) { - for (var i = 0, l = xs.length; i < l; i++) { - if (xs[i] === x) return i; - } - return -1; -} -}).call(this,require('_process'),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"./_stream_duplex":71,"./internal/streams/BufferList":76,"./internal/streams/destroy":77,"./internal/streams/stream":78,"_process":69,"core-util-is":44,"events":50,"inherits":56,"isarray":58,"process-nextick-args":68,"safe-buffer":83,"string_decoder/":85,"util":40}],74:[function(require,module,exports){ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -// a transform stream is a readable/writable stream where you do -// something with the data. Sometimes it's called a "filter", -// but that's not a great name for it, since that implies a thing where -// some bits pass through, and others are simply ignored. (That would -// be a valid example of a transform, of course.) -// -// While the output is causally related to the input, it's not a -// necessarily symmetric or synchronous transformation. For example, -// a zlib stream might take multiple plain-text writes(), and then -// emit a single compressed chunk some time in the future. -// -// Here's how this works: -// -// The Transform stream has all the aspects of the readable and writable -// stream classes. When you write(chunk), that calls _write(chunk,cb) -// internally, and returns false if there's a lot of pending writes -// buffered up. When you call read(), that calls _read(n) until -// there's enough pending readable data buffered up. -// -// In a transform stream, the written data is placed in a buffer. When -// _read(n) is called, it transforms the queued up data, calling the -// buffered _write cb's as it consumes chunks. If consuming a single -// written chunk would result in multiple output chunks, then the first -// outputted bit calls the readcb, and subsequent chunks just go into -// the read buffer, and will cause it to emit 'readable' if necessary. -// -// This way, back-pressure is actually determined by the reading side, -// since _read has to be called to start processing a new chunk. However, -// a pathological inflate type of transform can cause excessive buffering -// here. For example, imagine a stream where every byte of input is -// interpreted as an integer from 0-255, and then results in that many -// bytes of output. Writing the 4 bytes {ff,ff,ff,ff} would result in -// 1kb of data being output. In this case, you could write a very small -// amount of input, and end up with a very large amount of output. In -// such a pathological inflating mechanism, there'd be no way to tell -// the system to stop doing the transform. A single 4MB write could -// cause the system to run out of memory. -// -// However, even in such a pathological case, only a single written chunk -// would be consumed, and then the rest would wait (un-transformed) until -// the results of the previous transformed chunk were consumed. - -'use strict'; - -module.exports = Transform; - -var Duplex = require('./_stream_duplex'); - -/**/ -var util = require('core-util-is'); -util.inherits = require('inherits'); -/**/ - -util.inherits(Transform, Duplex); - -function afterTransform(er, data) { - var ts = this._transformState; - ts.transforming = false; - - var cb = ts.writecb; - - if (!cb) { - return this.emit('error', new Error('write callback called multiple times')); - } - - ts.writechunk = null; - ts.writecb = null; - - if (data != null) // single equals check for both `null` and `undefined` - this.push(data); - - cb(er); - - var rs = this._readableState; - rs.reading = false; - if (rs.needReadable || rs.length < rs.highWaterMark) { - this._read(rs.highWaterMark); - } -} - -function Transform(options) { - if (!(this instanceof Transform)) return new Transform(options); - - Duplex.call(this, options); - - this._transformState = { - afterTransform: afterTransform.bind(this), - needTransform: false, - transforming: false, - writecb: null, - writechunk: null, - writeencoding: null - }; - - // start out asking for a readable event once data is transformed. - this._readableState.needReadable = true; - - // we have implemented the _read method, and done the other things - // that Readable wants before the first _read call, so unset the - // sync guard flag. - this._readableState.sync = false; - - if (options) { - if (typeof options.transform === 'function') this._transform = options.transform; - - if (typeof options.flush === 'function') this._flush = options.flush; - } - - // When the writable side finishes, then flush out anything remaining. - this.on('prefinish', prefinish); -} - -function prefinish() { - var _this = this; - - if (typeof this._flush === 'function') { - this._flush(function (er, data) { - done(_this, er, data); - }); - } else { - done(this, null, null); - } -} - -Transform.prototype.push = function (chunk, encoding) { - this._transformState.needTransform = false; - return Duplex.prototype.push.call(this, chunk, encoding); -}; - -// This is the part where you do stuff! -// override this function in implementation classes. -// 'chunk' is an input chunk. -// -// Call `push(newChunk)` to pass along transformed output -// to the readable side. You may call 'push' zero or more times. -// -// Call `cb(err)` when you are done with this chunk. If you pass -// an error, then that'll put the hurt on the whole operation. If you -// never call cb(), then you'll never get another chunk. -Transform.prototype._transform = function (chunk, encoding, cb) { - throw new Error('_transform() is not implemented'); -}; - -Transform.prototype._write = function (chunk, encoding, cb) { - var ts = this._transformState; - ts.writecb = cb; - ts.writechunk = chunk; - ts.writeencoding = encoding; - if (!ts.transforming) { - var rs = this._readableState; - if (ts.needTransform || rs.needReadable || rs.length < rs.highWaterMark) this._read(rs.highWaterMark); - } -}; - -// Doesn't matter what the args are here. -// _transform does all the work. -// That we got here means that the readable side wants more data. -Transform.prototype._read = function (n) { - var ts = this._transformState; - - if (ts.writechunk !== null && ts.writecb && !ts.transforming) { - ts.transforming = true; - this._transform(ts.writechunk, ts.writeencoding, ts.afterTransform); - } else { - // mark that we need a transform, so that any data that comes in - // will get processed, now that we've asked for it. - ts.needTransform = true; - } -}; - -Transform.prototype._destroy = function (err, cb) { - var _this2 = this; - - Duplex.prototype._destroy.call(this, err, function (err2) { - cb(err2); - _this2.emit('close'); - }); -}; - -function done(stream, er, data) { - if (er) return stream.emit('error', er); - - if (data != null) // single equals check for both `null` and `undefined` - stream.push(data); - - // if there's nothing in the write buffer, then that means - // that nothing more will ever be provided - if (stream._writableState.length) throw new Error('Calling transform done when ws.length != 0'); - - if (stream._transformState.transforming) throw new Error('Calling transform done when still transforming'); - - return stream.push(null); -} -},{"./_stream_duplex":71,"core-util-is":44,"inherits":56}],75:[function(require,module,exports){ -(function (process,global,setImmediate){ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -// A bit simpler than readable streams. -// Implement an async ._write(chunk, encoding, cb), and it'll handle all -// the drain event emission and buffering. - -'use strict'; - -/**/ - -var pna = require('process-nextick-args'); -/**/ - -module.exports = Writable; - -/* */ -function WriteReq(chunk, encoding, cb) { - this.chunk = chunk; - this.encoding = encoding; - this.callback = cb; - this.next = null; -} - -// It seems a linked list but it is not -// there will be only 2 of these for each stream -function CorkedRequest(state) { - var _this = this; - - this.next = null; - this.entry = null; - this.finish = function () { - onCorkedFinish(_this, state); - }; -} -/* */ - -/**/ -var asyncWrite = !process.browser && ['v0.10', 'v0.9.'].indexOf(process.version.slice(0, 5)) > -1 ? setImmediate : pna.nextTick; -/**/ - -/**/ -var Duplex; -/**/ - -Writable.WritableState = WritableState; - -/**/ -var util = require('core-util-is'); -util.inherits = require('inherits'); -/**/ - -/**/ -var internalUtil = { - deprecate: require('util-deprecate') -}; -/**/ - -/**/ -var Stream = require('./internal/streams/stream'); -/**/ - -/**/ - -var Buffer = require('safe-buffer').Buffer; -var OurUint8Array = global.Uint8Array || function () {}; -function _uint8ArrayToBuffer(chunk) { - return Buffer.from(chunk); -} -function _isUint8Array(obj) { - return Buffer.isBuffer(obj) || obj instanceof OurUint8Array; -} - -/**/ - -var destroyImpl = require('./internal/streams/destroy'); - -util.inherits(Writable, Stream); - -function nop() {} - -function WritableState(options, stream) { - Duplex = Duplex || require('./_stream_duplex'); - - options = options || {}; - - // Duplex streams are both readable and writable, but share - // the same options object. - // However, some cases require setting options to different - // values for the readable and the writable sides of the duplex stream. - // These options can be provided separately as readableXXX and writableXXX. - var isDuplex = stream instanceof Duplex; - - // object stream flag to indicate whether or not this stream - // contains buffers or objects. - this.objectMode = !!options.objectMode; - - if (isDuplex) this.objectMode = this.objectMode || !!options.writableObjectMode; - - // the point at which write() starts returning false - // Note: 0 is a valid value, means that we always return false if - // the entire buffer is not flushed immediately on write() - var hwm = options.highWaterMark; - var writableHwm = options.writableHighWaterMark; - var defaultHwm = this.objectMode ? 16 : 16 * 1024; - - if (hwm || hwm === 0) this.highWaterMark = hwm;else if (isDuplex && (writableHwm || writableHwm === 0)) this.highWaterMark = writableHwm;else this.highWaterMark = defaultHwm; - - // cast to ints. - this.highWaterMark = Math.floor(this.highWaterMark); - - // if _final has been called - this.finalCalled = false; - - // drain event flag. - this.needDrain = false; - // at the start of calling end() - this.ending = false; - // when end() has been called, and returned - this.ended = false; - // when 'finish' is emitted - this.finished = false; - - // has it been destroyed - this.destroyed = false; - - // should we decode strings into buffers before passing to _write? - // this is here so that some node-core streams can optimize string - // handling at a lower level. - var noDecode = options.decodeStrings === false; - this.decodeStrings = !noDecode; - - // Crypto is kind of old and crusty. Historically, its default string - // encoding is 'binary' so we have to make this configurable. - // Everything else in the universe uses 'utf8', though. - this.defaultEncoding = options.defaultEncoding || 'utf8'; - - // not an actual buffer we keep track of, but a measurement - // of how much we're waiting to get pushed to some underlying - // socket or file. - this.length = 0; - - // a flag to see when we're in the middle of a write. - this.writing = false; - - // when true all writes will be buffered until .uncork() call - this.corked = 0; - - // a flag to be able to tell if the onwrite cb is called immediately, - // or on a later tick. We set this to true at first, because any - // actions that shouldn't happen until "later" should generally also - // not happen before the first write call. - this.sync = true; - - // a flag to know if we're processing previously buffered items, which - // may call the _write() callback in the same tick, so that we don't - // end up in an overlapped onwrite situation. - this.bufferProcessing = false; - - // the callback that's passed to _write(chunk,cb) - this.onwrite = function (er) { - onwrite(stream, er); - }; - - // the callback that the user supplies to write(chunk,encoding,cb) - this.writecb = null; - - // the amount that is being written when _write is called. - this.writelen = 0; - - this.bufferedRequest = null; - this.lastBufferedRequest = null; - - // number of pending user-supplied write callbacks - // this must be 0 before 'finish' can be emitted - this.pendingcb = 0; - - // emit prefinish if the only thing we're waiting for is _write cbs - // This is relevant for synchronous Transform streams - this.prefinished = false; - - // True if the error was already emitted and should not be thrown again - this.errorEmitted = false; - - // count buffered requests - this.bufferedRequestCount = 0; - - // allocate the first CorkedRequest, there is always - // one allocated and free to use, and we maintain at most two - this.corkedRequestsFree = new CorkedRequest(this); -} - -WritableState.prototype.getBuffer = function getBuffer() { - var current = this.bufferedRequest; - var out = []; - while (current) { - out.push(current); - current = current.next; - } - return out; -}; - -(function () { - try { - Object.defineProperty(WritableState.prototype, 'buffer', { - get: internalUtil.deprecate(function () { - return this.getBuffer(); - }, '_writableState.buffer is deprecated. Use _writableState.getBuffer ' + 'instead.', 'DEP0003') - }); - } catch (_) {} -})(); - -// Test _writableState for inheritance to account for Duplex streams, -// whose prototype chain only points to Readable. -var realHasInstance; -if (typeof Symbol === 'function' && Symbol.hasInstance && typeof Function.prototype[Symbol.hasInstance] === 'function') { - realHasInstance = Function.prototype[Symbol.hasInstance]; - Object.defineProperty(Writable, Symbol.hasInstance, { - value: function (object) { - if (realHasInstance.call(this, object)) return true; - if (this !== Writable) return false; - - return object && object._writableState instanceof WritableState; - } - }); -} else { - realHasInstance = function (object) { - return object instanceof this; - }; -} - -function Writable(options) { - Duplex = Duplex || require('./_stream_duplex'); - - // Writable ctor is applied to Duplexes, too. - // `realHasInstance` is necessary because using plain `instanceof` - // would return false, as no `_writableState` property is attached. - - // Trying to use the custom `instanceof` for Writable here will also break the - // Node.js LazyTransform implementation, which has a non-trivial getter for - // `_writableState` that would lead to infinite recursion. - if (!realHasInstance.call(Writable, this) && !(this instanceof Duplex)) { - return new Writable(options); - } - - this._writableState = new WritableState(options, this); - - // legacy. - this.writable = true; - - if (options) { - if (typeof options.write === 'function') this._write = options.write; - - if (typeof options.writev === 'function') this._writev = options.writev; - - if (typeof options.destroy === 'function') this._destroy = options.destroy; - - if (typeof options.final === 'function') this._final = options.final; - } - - Stream.call(this); -} - -// Otherwise people can pipe Writable streams, which is just wrong. -Writable.prototype.pipe = function () { - this.emit('error', new Error('Cannot pipe, not readable')); -}; - -function writeAfterEnd(stream, cb) { - var er = new Error('write after end'); - // TODO: defer error events consistently everywhere, not just the cb - stream.emit('error', er); - pna.nextTick(cb, er); -} - -// Checks that a user-supplied chunk is valid, especially for the particular -// mode the stream is in. Currently this means that `null` is never accepted -// and undefined/non-string values are only allowed in object mode. -function validChunk(stream, state, chunk, cb) { - var valid = true; - var er = false; - - if (chunk === null) { - er = new TypeError('May not write null values to stream'); - } else if (typeof chunk !== 'string' && chunk !== undefined && !state.objectMode) { - er = new TypeError('Invalid non-string/buffer chunk'); - } - if (er) { - stream.emit('error', er); - pna.nextTick(cb, er); - valid = false; - } - return valid; -} - -Writable.prototype.write = function (chunk, encoding, cb) { - var state = this._writableState; - var ret = false; - var isBuf = !state.objectMode && _isUint8Array(chunk); - - if (isBuf && !Buffer.isBuffer(chunk)) { - chunk = _uint8ArrayToBuffer(chunk); - } - - if (typeof encoding === 'function') { - cb = encoding; - encoding = null; - } - - if (isBuf) encoding = 'buffer';else if (!encoding) encoding = state.defaultEncoding; - - if (typeof cb !== 'function') cb = nop; - - if (state.ended) writeAfterEnd(this, cb);else if (isBuf || validChunk(this, state, chunk, cb)) { - state.pendingcb++; - ret = writeOrBuffer(this, state, isBuf, chunk, encoding, cb); - } - - return ret; -}; - -Writable.prototype.cork = function () { - var state = this._writableState; - - state.corked++; -}; - -Writable.prototype.uncork = function () { - var state = this._writableState; - - if (state.corked) { - state.corked--; - - if (!state.writing && !state.corked && !state.finished && !state.bufferProcessing && state.bufferedRequest) clearBuffer(this, state); - } -}; - -Writable.prototype.setDefaultEncoding = function setDefaultEncoding(encoding) { - // node::ParseEncoding() requires lower case. - if (typeof encoding === 'string') encoding = encoding.toLowerCase(); - if (!(['hex', 'utf8', 'utf-8', 'ascii', 'binary', 'base64', 'ucs2', 'ucs-2', 'utf16le', 'utf-16le', 'raw'].indexOf((encoding + '').toLowerCase()) > -1)) throw new TypeError('Unknown encoding: ' + encoding); - this._writableState.defaultEncoding = encoding; - return this; -}; - -function decodeChunk(state, chunk, encoding) { - if (!state.objectMode && state.decodeStrings !== false && typeof chunk === 'string') { - chunk = Buffer.from(chunk, encoding); - } - return chunk; -} - -Object.defineProperty(Writable.prototype, 'writableHighWaterMark', { - // making it explicit this property is not enumerable - // because otherwise some prototype manipulation in - // userland will fail - enumerable: false, - get: function () { - return this._writableState.highWaterMark; - } -}); - -// if we're already writing something, then just put this -// in the queue, and wait our turn. Otherwise, call _write -// If we return false, then we need a drain event, so set that flag. -function writeOrBuffer(stream, state, isBuf, chunk, encoding, cb) { - if (!isBuf) { - var newChunk = decodeChunk(state, chunk, encoding); - if (chunk !== newChunk) { - isBuf = true; - encoding = 'buffer'; - chunk = newChunk; - } - } - var len = state.objectMode ? 1 : chunk.length; - - state.length += len; - - var ret = state.length < state.highWaterMark; - // we must ensure that previous needDrain will not be reset to false. - if (!ret) state.needDrain = true; - - if (state.writing || state.corked) { - var last = state.lastBufferedRequest; - state.lastBufferedRequest = { - chunk: chunk, - encoding: encoding, - isBuf: isBuf, - callback: cb, - next: null - }; - if (last) { - last.next = state.lastBufferedRequest; - } else { - state.bufferedRequest = state.lastBufferedRequest; - } - state.bufferedRequestCount += 1; - } else { - doWrite(stream, state, false, len, chunk, encoding, cb); - } - - return ret; -} - -function doWrite(stream, state, writev, len, chunk, encoding, cb) { - state.writelen = len; - state.writecb = cb; - state.writing = true; - state.sync = true; - if (writev) stream._writev(chunk, state.onwrite);else stream._write(chunk, encoding, state.onwrite); - state.sync = false; -} - -function onwriteError(stream, state, sync, er, cb) { - --state.pendingcb; - - if (sync) { - // defer the callback if we are being called synchronously - // to avoid piling up things on the stack - pna.nextTick(cb, er); - // this can emit finish, and it will always happen - // after error - pna.nextTick(finishMaybe, stream, state); - stream._writableState.errorEmitted = true; - stream.emit('error', er); - } else { - // the caller expect this to happen before if - // it is async - cb(er); - stream._writableState.errorEmitted = true; - stream.emit('error', er); - // this can emit finish, but finish must - // always follow error - finishMaybe(stream, state); - } -} - -function onwriteStateUpdate(state) { - state.writing = false; - state.writecb = null; - state.length -= state.writelen; - state.writelen = 0; -} - -function onwrite(stream, er) { - var state = stream._writableState; - var sync = state.sync; - var cb = state.writecb; - - onwriteStateUpdate(state); - - if (er) onwriteError(stream, state, sync, er, cb);else { - // Check if we're actually ready to finish, but don't emit yet - var finished = needFinish(state); - - if (!finished && !state.corked && !state.bufferProcessing && state.bufferedRequest) { - clearBuffer(stream, state); - } - - if (sync) { - /**/ - asyncWrite(afterWrite, stream, state, finished, cb); - /**/ - } else { - afterWrite(stream, state, finished, cb); - } - } -} - -function afterWrite(stream, state, finished, cb) { - if (!finished) onwriteDrain(stream, state); - state.pendingcb--; - cb(); - finishMaybe(stream, state); -} - -// Must force callback to be called on nextTick, so that we don't -// emit 'drain' before the write() consumer gets the 'false' return -// value, and has a chance to attach a 'drain' listener. -function onwriteDrain(stream, state) { - if (state.length === 0 && state.needDrain) { - state.needDrain = false; - stream.emit('drain'); - } -} - -// if there's something in the buffer waiting, then process it -function clearBuffer(stream, state) { - state.bufferProcessing = true; - var entry = state.bufferedRequest; - - if (stream._writev && entry && entry.next) { - // Fast case, write everything using _writev() - var l = state.bufferedRequestCount; - var buffer = new Array(l); - var holder = state.corkedRequestsFree; - holder.entry = entry; - - var count = 0; - var allBuffers = true; - while (entry) { - buffer[count] = entry; - if (!entry.isBuf) allBuffers = false; - entry = entry.next; - count += 1; - } - buffer.allBuffers = allBuffers; - - doWrite(stream, state, true, state.length, buffer, '', holder.finish); - - // doWrite is almost always async, defer these to save a bit of time - // as the hot path ends with doWrite - state.pendingcb++; - state.lastBufferedRequest = null; - if (holder.next) { - state.corkedRequestsFree = holder.next; - holder.next = null; - } else { - state.corkedRequestsFree = new CorkedRequest(state); - } - state.bufferedRequestCount = 0; - } else { - // Slow case, write chunks one-by-one - while (entry) { - var chunk = entry.chunk; - var encoding = entry.encoding; - var cb = entry.callback; - var len = state.objectMode ? 1 : chunk.length; - - doWrite(stream, state, false, len, chunk, encoding, cb); - entry = entry.next; - state.bufferedRequestCount--; - // if we didn't call the onwrite immediately, then - // it means that we need to wait until it does. - // also, that means that the chunk and cb are currently - // being processed, so move the buffer counter past them. - if (state.writing) { - break; - } - } - - if (entry === null) state.lastBufferedRequest = null; - } - - state.bufferedRequest = entry; - state.bufferProcessing = false; -} - -Writable.prototype._write = function (chunk, encoding, cb) { - cb(new Error('_write() is not implemented')); -}; - -Writable.prototype._writev = null; - -Writable.prototype.end = function (chunk, encoding, cb) { - var state = this._writableState; - - if (typeof chunk === 'function') { - cb = chunk; - chunk = null; - encoding = null; - } else if (typeof encoding === 'function') { - cb = encoding; - encoding = null; - } - - if (chunk !== null && chunk !== undefined) this.write(chunk, encoding); - - // .end() fully uncorks - if (state.corked) { - state.corked = 1; - this.uncork(); - } - - // ignore unnecessary end() calls. - if (!state.ending && !state.finished) endWritable(this, state, cb); -}; - -function needFinish(state) { - return state.ending && state.length === 0 && state.bufferedRequest === null && !state.finished && !state.writing; -} -function callFinal(stream, state) { - stream._final(function (err) { - state.pendingcb--; - if (err) { - stream.emit('error', err); - } - state.prefinished = true; - stream.emit('prefinish'); - finishMaybe(stream, state); - }); -} -function prefinish(stream, state) { - if (!state.prefinished && !state.finalCalled) { - if (typeof stream._final === 'function') { - state.pendingcb++; - state.finalCalled = true; - pna.nextTick(callFinal, stream, state); - } else { - state.prefinished = true; - stream.emit('prefinish'); - } - } -} - -function finishMaybe(stream, state) { - var need = needFinish(state); - if (need) { - prefinish(stream, state); - if (state.pendingcb === 0) { - state.finished = true; - stream.emit('finish'); - } - } - return need; -} - -function endWritable(stream, state, cb) { - state.ending = true; - finishMaybe(stream, state); - if (cb) { - if (state.finished) pna.nextTick(cb);else stream.once('finish', cb); - } - state.ended = true; - stream.writable = false; -} - -function onCorkedFinish(corkReq, state, err) { - var entry = corkReq.entry; - corkReq.entry = null; - while (entry) { - var cb = entry.callback; - state.pendingcb--; - cb(err); - entry = entry.next; - } - if (state.corkedRequestsFree) { - state.corkedRequestsFree.next = corkReq; - } else { - state.corkedRequestsFree = corkReq; - } -} - -Object.defineProperty(Writable.prototype, 'destroyed', { - get: function () { - if (this._writableState === undefined) { - return false; - } - return this._writableState.destroyed; - }, - set: function (value) { - // we ignore the value if the stream - // has not been initialized yet - if (!this._writableState) { - return; - } - - // backward compatibility, the user is explicitly - // managing destroyed - this._writableState.destroyed = value; - } -}); - -Writable.prototype.destroy = destroyImpl.destroy; -Writable.prototype._undestroy = destroyImpl.undestroy; -Writable.prototype._destroy = function (err, cb) { - this.end(); - cb(err); -}; -}).call(this,require('_process'),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {},require("timers").setImmediate) -},{"./_stream_duplex":71,"./internal/streams/destroy":77,"./internal/streams/stream":78,"_process":69,"core-util-is":44,"inherits":56,"process-nextick-args":68,"safe-buffer":83,"timers":86,"util-deprecate":87}],76:[function(require,module,exports){ -'use strict'; - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var Buffer = require('safe-buffer').Buffer; -var util = require('util'); - -function copyBuffer(src, target, offset) { - src.copy(target, offset); -} - -module.exports = function () { - function BufferList() { - _classCallCheck(this, BufferList); - - this.head = null; - this.tail = null; - this.length = 0; - } - - BufferList.prototype.push = function push(v) { - var entry = { data: v, next: null }; - if (this.length > 0) this.tail.next = entry;else this.head = entry; - this.tail = entry; - ++this.length; - }; - - BufferList.prototype.unshift = function unshift(v) { - var entry = { data: v, next: this.head }; - if (this.length === 0) this.tail = entry; - this.head = entry; - ++this.length; - }; - - BufferList.prototype.shift = function shift() { - if (this.length === 0) return; - var ret = this.head.data; - if (this.length === 1) this.head = this.tail = null;else this.head = this.head.next; - --this.length; - return ret; - }; - - BufferList.prototype.clear = function clear() { - this.head = this.tail = null; - this.length = 0; - }; - - BufferList.prototype.join = function join(s) { - if (this.length === 0) return ''; - var p = this.head; - var ret = '' + p.data; - while (p = p.next) { - ret += s + p.data; - }return ret; - }; - - BufferList.prototype.concat = function concat(n) { - if (this.length === 0) return Buffer.alloc(0); - if (this.length === 1) return this.head.data; - var ret = Buffer.allocUnsafe(n >>> 0); - var p = this.head; - var i = 0; - while (p) { - copyBuffer(p.data, ret, i); - i += p.data.length; - p = p.next; - } - return ret; - }; - - return BufferList; -}(); - -if (util && util.inspect && util.inspect.custom) { - module.exports.prototype[util.inspect.custom] = function () { - var obj = util.inspect({ length: this.length }); - return this.constructor.name + ' ' + obj; - }; -} -},{"safe-buffer":83,"util":40}],77:[function(require,module,exports){ -'use strict'; - -/**/ - -var pna = require('process-nextick-args'); -/**/ - -// undocumented cb() API, needed for core, not for public API -function destroy(err, cb) { - var _this = this; - - var readableDestroyed = this._readableState && this._readableState.destroyed; - var writableDestroyed = this._writableState && this._writableState.destroyed; - - if (readableDestroyed || writableDestroyed) { - if (cb) { - cb(err); - } else if (err && (!this._writableState || !this._writableState.errorEmitted)) { - pna.nextTick(emitErrorNT, this, err); - } - return this; - } - - // we set destroyed to true before firing error callbacks in order - // to make it re-entrance safe in case destroy() is called within callbacks - - if (this._readableState) { - this._readableState.destroyed = true; - } - - // if this is a duplex stream mark the writable part as destroyed as well - if (this._writableState) { - this._writableState.destroyed = true; - } - - this._destroy(err || null, function (err) { - if (!cb && err) { - pna.nextTick(emitErrorNT, _this, err); - if (_this._writableState) { - _this._writableState.errorEmitted = true; - } - } else if (cb) { - cb(err); - } - }); - - return this; -} - -function undestroy() { - if (this._readableState) { - this._readableState.destroyed = false; - this._readableState.reading = false; - this._readableState.ended = false; - this._readableState.endEmitted = false; - } - - if (this._writableState) { - this._writableState.destroyed = false; - this._writableState.ended = false; - this._writableState.ending = false; - this._writableState.finished = false; - this._writableState.errorEmitted = false; - } -} - -function emitErrorNT(self, err) { - self.emit('error', err); -} - -module.exports = { - destroy: destroy, - undestroy: undestroy -}; -},{"process-nextick-args":68}],78:[function(require,module,exports){ -module.exports = require('events').EventEmitter; - -},{"events":50}],79:[function(require,module,exports){ -module.exports = require('./readable').PassThrough - -},{"./readable":80}],80:[function(require,module,exports){ -exports = module.exports = require('./lib/_stream_readable.js'); -exports.Stream = exports; -exports.Readable = exports; -exports.Writable = require('./lib/_stream_writable.js'); -exports.Duplex = require('./lib/_stream_duplex.js'); -exports.Transform = require('./lib/_stream_transform.js'); -exports.PassThrough = require('./lib/_stream_passthrough.js'); - -},{"./lib/_stream_duplex.js":71,"./lib/_stream_passthrough.js":72,"./lib/_stream_readable.js":73,"./lib/_stream_transform.js":74,"./lib/_stream_writable.js":75}],81:[function(require,module,exports){ -module.exports = require('./readable').Transform - -},{"./readable":80}],82:[function(require,module,exports){ -module.exports = require('./lib/_stream_writable.js'); - -},{"./lib/_stream_writable.js":75}],83:[function(require,module,exports){ -/* eslint-disable node/no-deprecated-api */ -var buffer = require('buffer') -var Buffer = buffer.Buffer - -// alternative to using Object.keys for old browsers -function copyProps (src, dst) { - for (var key in src) { - dst[key] = src[key] - } -} -if (Buffer.from && Buffer.alloc && Buffer.allocUnsafe && Buffer.allocUnsafeSlow) { - module.exports = buffer -} else { - // Copy properties from require('buffer') - copyProps(buffer, exports) - exports.Buffer = SafeBuffer -} - -function SafeBuffer (arg, encodingOrOffset, length) { - return Buffer(arg, encodingOrOffset, length) -} - -// Copy static methods from Buffer -copyProps(Buffer, SafeBuffer) - -SafeBuffer.from = function (arg, encodingOrOffset, length) { - if (typeof arg === 'number') { - throw new TypeError('Argument must not be a number') - } - return Buffer(arg, encodingOrOffset, length) -} - -SafeBuffer.alloc = function (size, fill, encoding) { - if (typeof size !== 'number') { - throw new TypeError('Argument must be a number') - } - var buf = Buffer(size) - if (fill !== undefined) { - if (typeof encoding === 'string') { - buf.fill(fill, encoding) - } else { - buf.fill(fill) - } - } else { - buf.fill(0) - } - return buf -} - -SafeBuffer.allocUnsafe = function (size) { - if (typeof size !== 'number') { - throw new TypeError('Argument must be a number') - } - return Buffer(size) -} - -SafeBuffer.allocUnsafeSlow = function (size) { - if (typeof size !== 'number') { - throw new TypeError('Argument must be a number') - } - return buffer.SlowBuffer(size) -} - -},{"buffer":43}],84:[function(require,module,exports){ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -module.exports = Stream; - -var EE = require('events').EventEmitter; -var inherits = require('inherits'); - -inherits(Stream, EE); -Stream.Readable = require('readable-stream/readable.js'); -Stream.Writable = require('readable-stream/writable.js'); -Stream.Duplex = require('readable-stream/duplex.js'); -Stream.Transform = require('readable-stream/transform.js'); -Stream.PassThrough = require('readable-stream/passthrough.js'); - -// Backwards-compat with node 0.4.x -Stream.Stream = Stream; - - - -// old-style streams. Note that the pipe method (the only relevant -// part of this class) is overridden in the Readable class. - -function Stream() { - EE.call(this); -} - -Stream.prototype.pipe = function(dest, options) { - var source = this; - - function ondata(chunk) { - if (dest.writable) { - if (false === dest.write(chunk) && source.pause) { - source.pause(); - } - } - } - - source.on('data', ondata); - - function ondrain() { - if (source.readable && source.resume) { - source.resume(); - } - } - - dest.on('drain', ondrain); - - // If the 'end' option is not supplied, dest.end() will be called when - // source gets the 'end' or 'close' events. Only dest.end() once. - if (!dest._isStdio && (!options || options.end !== false)) { - source.on('end', onend); - source.on('close', onclose); - } - - var didOnEnd = false; - function onend() { - if (didOnEnd) return; - didOnEnd = true; - - dest.end(); - } - - - function onclose() { - if (didOnEnd) return; - didOnEnd = true; - - if (typeof dest.destroy === 'function') dest.destroy(); - } - - // don't leave dangling pipes when there are errors. - function onerror(er) { - cleanup(); - if (EE.listenerCount(this, 'error') === 0) { - throw er; // Unhandled stream error in pipe. - } - } - - source.on('error', onerror); - dest.on('error', onerror); - - // remove all the event listeners that were added. - function cleanup() { - source.removeListener('data', ondata); - dest.removeListener('drain', ondrain); - - source.removeListener('end', onend); - source.removeListener('close', onclose); - - source.removeListener('error', onerror); - dest.removeListener('error', onerror); - - source.removeListener('end', cleanup); - source.removeListener('close', cleanup); - - dest.removeListener('close', cleanup); - } - - source.on('end', cleanup); - source.on('close', cleanup); - - dest.on('close', cleanup); - - dest.emit('pipe', source); - - // Allow for unix-like usage: A.pipe(B).pipe(C) - return dest; -}; - -},{"events":50,"inherits":56,"readable-stream/duplex.js":70,"readable-stream/passthrough.js":79,"readable-stream/readable.js":80,"readable-stream/transform.js":81,"readable-stream/writable.js":82}],85:[function(require,module,exports){ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -'use strict'; - -/**/ - -var Buffer = require('safe-buffer').Buffer; -/**/ - -var isEncoding = Buffer.isEncoding || function (encoding) { - encoding = '' + encoding; - switch (encoding && encoding.toLowerCase()) { - case 'hex':case 'utf8':case 'utf-8':case 'ascii':case 'binary':case 'base64':case 'ucs2':case 'ucs-2':case 'utf16le':case 'utf-16le':case 'raw': - return true; - default: - return false; - } -}; - -function _normalizeEncoding(enc) { - if (!enc) return 'utf8'; - var retried; - while (true) { - switch (enc) { - case 'utf8': - case 'utf-8': - return 'utf8'; - case 'ucs2': - case 'ucs-2': - case 'utf16le': - case 'utf-16le': - return 'utf16le'; - case 'latin1': - case 'binary': - return 'latin1'; - case 'base64': - case 'ascii': - case 'hex': - return enc; - default: - if (retried) return; // undefined - enc = ('' + enc).toLowerCase(); - retried = true; - } - } -}; - -// Do not cache `Buffer.isEncoding` when checking encoding names as some -// modules monkey-patch it to support additional encodings -function normalizeEncoding(enc) { - var nenc = _normalizeEncoding(enc); - if (typeof nenc !== 'string' && (Buffer.isEncoding === isEncoding || !isEncoding(enc))) throw new Error('Unknown encoding: ' + enc); - return nenc || enc; -} - -// StringDecoder provides an interface for efficiently splitting a series of -// buffers into a series of JS strings without breaking apart multi-byte -// characters. -exports.StringDecoder = StringDecoder; -function StringDecoder(encoding) { - this.encoding = normalizeEncoding(encoding); - var nb; - switch (this.encoding) { - case 'utf16le': - this.text = utf16Text; - this.end = utf16End; - nb = 4; - break; - case 'utf8': - this.fillLast = utf8FillLast; - nb = 4; - break; - case 'base64': - this.text = base64Text; - this.end = base64End; - nb = 3; - break; - default: - this.write = simpleWrite; - this.end = simpleEnd; - return; - } - this.lastNeed = 0; - this.lastTotal = 0; - this.lastChar = Buffer.allocUnsafe(nb); -} - -StringDecoder.prototype.write = function (buf) { - if (buf.length === 0) return ''; - var r; - var i; - if (this.lastNeed) { - r = this.fillLast(buf); - if (r === undefined) return ''; - i = this.lastNeed; - this.lastNeed = 0; - } else { - i = 0; - } - if (i < buf.length) return r ? r + this.text(buf, i) : this.text(buf, i); - return r || ''; -}; - -StringDecoder.prototype.end = utf8End; - -// Returns only complete characters in a Buffer -StringDecoder.prototype.text = utf8Text; - -// Attempts to complete a partial non-UTF-8 character using bytes from a Buffer -StringDecoder.prototype.fillLast = function (buf) { - if (this.lastNeed <= buf.length) { - buf.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, this.lastNeed); - return this.lastChar.toString(this.encoding, 0, this.lastTotal); - } - buf.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, buf.length); - this.lastNeed -= buf.length; -}; - -// Checks the type of a UTF-8 byte, whether it's ASCII, a leading byte, or a -// continuation byte. If an invalid byte is detected, -2 is returned. -function utf8CheckByte(byte) { - if (byte <= 0x7F) return 0;else if (byte >> 5 === 0x06) return 2;else if (byte >> 4 === 0x0E) return 3;else if (byte >> 3 === 0x1E) return 4; - return byte >> 6 === 0x02 ? -1 : -2; -} - -// Checks at most 3 bytes at the end of a Buffer in order to detect an -// incomplete multi-byte UTF-8 character. The total number of bytes (2, 3, or 4) -// needed to complete the UTF-8 character (if applicable) are returned. -function utf8CheckIncomplete(self, buf, i) { - var j = buf.length - 1; - if (j < i) return 0; - var nb = utf8CheckByte(buf[j]); - if (nb >= 0) { - if (nb > 0) self.lastNeed = nb - 1; - return nb; - } - if (--j < i || nb === -2) return 0; - nb = utf8CheckByte(buf[j]); - if (nb >= 0) { - if (nb > 0) self.lastNeed = nb - 2; - return nb; - } - if (--j < i || nb === -2) return 0; - nb = utf8CheckByte(buf[j]); - if (nb >= 0) { - if (nb > 0) { - if (nb === 2) nb = 0;else self.lastNeed = nb - 3; - } - return nb; - } - return 0; -} - -// Validates as many continuation bytes for a multi-byte UTF-8 character as -// needed or are available. If we see a non-continuation byte where we expect -// one, we "replace" the validated continuation bytes we've seen so far with -// a single UTF-8 replacement character ('\ufffd'), to match v8's UTF-8 decoding -// behavior. The continuation byte check is included three times in the case -// where all of the continuation bytes for a character exist in the same buffer. -// It is also done this way as a slight performance increase instead of using a -// loop. -function utf8CheckExtraBytes(self, buf, p) { - if ((buf[0] & 0xC0) !== 0x80) { - self.lastNeed = 0; - return '\ufffd'; - } - if (self.lastNeed > 1 && buf.length > 1) { - if ((buf[1] & 0xC0) !== 0x80) { - self.lastNeed = 1; - return '\ufffd'; - } - if (self.lastNeed > 2 && buf.length > 2) { - if ((buf[2] & 0xC0) !== 0x80) { - self.lastNeed = 2; - return '\ufffd'; - } - } - } -} - -// Attempts to complete a multi-byte UTF-8 character using bytes from a Buffer. -function utf8FillLast(buf) { - var p = this.lastTotal - this.lastNeed; - var r = utf8CheckExtraBytes(this, buf, p); - if (r !== undefined) return r; - if (this.lastNeed <= buf.length) { - buf.copy(this.lastChar, p, 0, this.lastNeed); - return this.lastChar.toString(this.encoding, 0, this.lastTotal); - } - buf.copy(this.lastChar, p, 0, buf.length); - this.lastNeed -= buf.length; -} - -// Returns all complete UTF-8 characters in a Buffer. If the Buffer ended on a -// partial character, the character's bytes are buffered until the required -// number of bytes are available. -function utf8Text(buf, i) { - var total = utf8CheckIncomplete(this, buf, i); - if (!this.lastNeed) return buf.toString('utf8', i); - this.lastTotal = total; - var end = buf.length - (total - this.lastNeed); - buf.copy(this.lastChar, 0, end); - return buf.toString('utf8', i, end); -} - -// For UTF-8, a replacement character is added when ending on a partial -// character. -function utf8End(buf) { - var r = buf && buf.length ? this.write(buf) : ''; - if (this.lastNeed) return r + '\ufffd'; - return r; -} - -// UTF-16LE typically needs two bytes per character, but even if we have an even -// number of bytes available, we need to check if we end on a leading/high -// surrogate. In that case, we need to wait for the next two bytes in order to -// decode the last character properly. -function utf16Text(buf, i) { - if ((buf.length - i) % 2 === 0) { - var r = buf.toString('utf16le', i); - if (r) { - var c = r.charCodeAt(r.length - 1); - if (c >= 0xD800 && c <= 0xDBFF) { - this.lastNeed = 2; - this.lastTotal = 4; - this.lastChar[0] = buf[buf.length - 2]; - this.lastChar[1] = buf[buf.length - 1]; - return r.slice(0, -1); - } - } - return r; - } - this.lastNeed = 1; - this.lastTotal = 2; - this.lastChar[0] = buf[buf.length - 1]; - return buf.toString('utf16le', i, buf.length - 1); -} - -// For UTF-16LE we do not explicitly append special replacement characters if we -// end on a partial character, we simply let v8 handle that. -function utf16End(buf) { - var r = buf && buf.length ? this.write(buf) : ''; - if (this.lastNeed) { - var end = this.lastTotal - this.lastNeed; - return r + this.lastChar.toString('utf16le', 0, end); - } - return r; -} - -function base64Text(buf, i) { - var n = (buf.length - i) % 3; - if (n === 0) return buf.toString('base64', i); - this.lastNeed = 3 - n; - this.lastTotal = 3; - if (n === 1) { - this.lastChar[0] = buf[buf.length - 1]; - } else { - this.lastChar[0] = buf[buf.length - 2]; - this.lastChar[1] = buf[buf.length - 1]; - } - return buf.toString('base64', i, buf.length - n); -} - -function base64End(buf) { - var r = buf && buf.length ? this.write(buf) : ''; - if (this.lastNeed) return r + this.lastChar.toString('base64', 0, 3 - this.lastNeed); - return r; -} - -// Pass bytes on through for single-byte encodings (e.g. ascii, latin1, hex) -function simpleWrite(buf) { - return buf.toString(this.encoding); -} - -function simpleEnd(buf) { - return buf && buf.length ? this.write(buf) : ''; -} -},{"safe-buffer":83}],86:[function(require,module,exports){ -(function (setImmediate,clearImmediate){ -var nextTick = require('process/browser.js').nextTick; -var apply = Function.prototype.apply; -var slice = Array.prototype.slice; -var immediateIds = {}; -var nextImmediateId = 0; - -// DOM APIs, for completeness - -exports.setTimeout = function() { - return new Timeout(apply.call(setTimeout, window, arguments), clearTimeout); -}; -exports.setInterval = function() { - return new Timeout(apply.call(setInterval, window, arguments), clearInterval); -}; -exports.clearTimeout = -exports.clearInterval = function(timeout) { timeout.close(); }; - -function Timeout(id, clearFn) { - this._id = id; - this._clearFn = clearFn; -} -Timeout.prototype.unref = Timeout.prototype.ref = function() {}; -Timeout.prototype.close = function() { - this._clearFn.call(window, this._id); -}; - -// Does not start the time, just sets up the members needed. -exports.enroll = function(item, msecs) { - clearTimeout(item._idleTimeoutId); - item._idleTimeout = msecs; -}; - -exports.unenroll = function(item) { - clearTimeout(item._idleTimeoutId); - item._idleTimeout = -1; -}; - -exports._unrefActive = exports.active = function(item) { - clearTimeout(item._idleTimeoutId); - - var msecs = item._idleTimeout; - if (msecs >= 0) { - item._idleTimeoutId = setTimeout(function onTimeout() { - if (item._onTimeout) - item._onTimeout(); - }, msecs); - } -}; - -// That's not how node.js implements it but the exposed api is the same. -exports.setImmediate = typeof setImmediate === "function" ? setImmediate : function(fn) { - var id = nextImmediateId++; - var args = arguments.length < 2 ? false : slice.call(arguments, 1); - - immediateIds[id] = true; - - nextTick(function onNextTick() { - if (immediateIds[id]) { - // fn.call() is faster so we optimize for the common use-case - // @see http://jsperf.com/call-apply-segu - if (args) { - fn.apply(null, args); - } else { - fn.call(null); - } - // Prevent ids from leaking - exports.clearImmediate(id); - } - }); - - return id; -}; - -exports.clearImmediate = typeof clearImmediate === "function" ? clearImmediate : function(id) { - delete immediateIds[id]; -}; -}).call(this,require("timers").setImmediate,require("timers").clearImmediate) -},{"process/browser.js":69,"timers":86}],87:[function(require,module,exports){ -(function (global){ - -/** - * Module exports. - */ - -module.exports = deprecate; - -/** - * Mark that a method should not be used. - * Returns a modified function which warns once by default. - * - * If `localStorage.noDeprecation = true` is set, then it is a no-op. - * - * If `localStorage.throwDeprecation = true` is set, then deprecated functions - * will throw an Error when invoked. - * - * If `localStorage.traceDeprecation = true` is set, then deprecated functions - * will invoke `console.trace()` instead of `console.error()`. - * - * @param {Function} fn - the function to deprecate - * @param {String} msg - the string to print to the console when `fn` is invoked - * @returns {Function} a new "deprecated" version of `fn` - * @api public - */ - -function deprecate (fn, msg) { - if (config('noDeprecation')) { - return fn; - } - - var warned = false; - function deprecated() { - if (!warned) { - if (config('throwDeprecation')) { - throw new Error(msg); - } else if (config('traceDeprecation')) { - console.trace(msg); - } else { - console.warn(msg); - } - warned = true; - } - return fn.apply(this, arguments); - } - - return deprecated; -} - -/** - * Checks `localStorage` for boolean values for the given `name`. - * - * @param {String} name - * @returns {Boolean} - * @api private - */ - -function config (name) { - // accessing global.localStorage can trigger a DOMException in sandboxed iframes - try { - if (!global.localStorage) return false; - } catch (_) { - return false; - } - var val = global.localStorage[name]; - if (null == val) return false; - return String(val).toLowerCase() === 'true'; -} - -}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{}],88:[function(require,module,exports){ -module.exports = function isBuffer(arg) { - return arg && typeof arg === 'object' - && typeof arg.copy === 'function' - && typeof arg.fill === 'function' - && typeof arg.readUInt8 === 'function'; -} -},{}],89:[function(require,module,exports){ -(function (process,global){ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -var formatRegExp = /%[sdj%]/g; -exports.format = function(f) { - if (!isString(f)) { - var objects = []; - for (var i = 0; i < arguments.length; i++) { - objects.push(inspect(arguments[i])); - } - return objects.join(' '); - } - - var i = 1; - var args = arguments; - var len = args.length; - var str = String(f).replace(formatRegExp, function(x) { - if (x === '%%') return '%'; - if (i >= len) return x; - switch (x) { - case '%s': return String(args[i++]); - case '%d': return Number(args[i++]); - case '%j': - try { - return JSON.stringify(args[i++]); - } catch (_) { - return '[Circular]'; - } - default: - return x; - } - }); - for (var x = args[i]; i < len; x = args[++i]) { - if (isNull(x) || !isObject(x)) { - str += ' ' + x; - } else { - str += ' ' + inspect(x); - } - } - return str; -}; - - -// Mark that a method should not be used. -// Returns a modified function which warns once by default. -// If --no-deprecation is set, then it is a no-op. -exports.deprecate = function(fn, msg) { - // Allow for deprecating things in the process of starting up. - if (isUndefined(global.process)) { - return function() { - return exports.deprecate(fn, msg).apply(this, arguments); - }; - } - - if (process.noDeprecation === true) { - return fn; - } - - var warned = false; - function deprecated() { - if (!warned) { - if (process.throwDeprecation) { - throw new Error(msg); - } else if (process.traceDeprecation) { - console.trace(msg); - } else { - console.error(msg); - } - warned = true; - } - return fn.apply(this, arguments); - } - - return deprecated; -}; - - -var debugs = {}; -var debugEnviron; -exports.debuglog = function(set) { - if (isUndefined(debugEnviron)) - debugEnviron = process.env.NODE_DEBUG || ''; - set = set.toUpperCase(); - if (!debugs[set]) { - if (new RegExp('\\b' + set + '\\b', 'i').test(debugEnviron)) { - var pid = process.pid; - debugs[set] = function() { - var msg = exports.format.apply(exports, arguments); - console.error('%s %d: %s', set, pid, msg); - }; - } else { - debugs[set] = function() {}; - } - } - return debugs[set]; -}; - - -/** - * Echos the value of a value. Trys to print the value out - * in the best way possible given the different types. - * - * @param {Object} obj The object to print out. - * @param {Object} opts Optional options object that alters the output. - */ -/* legacy: obj, showHidden, depth, colors*/ -function inspect(obj, opts) { - // default options - var ctx = { - seen: [], - stylize: stylizeNoColor - }; - // legacy... - if (arguments.length >= 3) ctx.depth = arguments[2]; - if (arguments.length >= 4) ctx.colors = arguments[3]; - if (isBoolean(opts)) { - // legacy... - ctx.showHidden = opts; - } else if (opts) { - // got an "options" object - exports._extend(ctx, opts); - } - // set default options - if (isUndefined(ctx.showHidden)) ctx.showHidden = false; - if (isUndefined(ctx.depth)) ctx.depth = 2; - if (isUndefined(ctx.colors)) ctx.colors = false; - if (isUndefined(ctx.customInspect)) ctx.customInspect = true; - if (ctx.colors) ctx.stylize = stylizeWithColor; - return formatValue(ctx, obj, ctx.depth); -} -exports.inspect = inspect; - - -// http://en.wikipedia.org/wiki/ANSI_escape_code#graphics -inspect.colors = { - 'bold' : [1, 22], - 'italic' : [3, 23], - 'underline' : [4, 24], - 'inverse' : [7, 27], - 'white' : [37, 39], - 'grey' : [90, 39], - 'black' : [30, 39], - 'blue' : [34, 39], - 'cyan' : [36, 39], - 'green' : [32, 39], - 'magenta' : [35, 39], - 'red' : [31, 39], - 'yellow' : [33, 39] -}; - -// Don't use 'blue' not visible on cmd.exe -inspect.styles = { - 'special': 'cyan', - 'number': 'yellow', - 'boolean': 'yellow', - 'undefined': 'grey', - 'null': 'bold', - 'string': 'green', - 'date': 'magenta', - // "name": intentionally not styling - 'regexp': 'red' -}; - - -function stylizeWithColor(str, styleType) { - var style = inspect.styles[styleType]; - - if (style) { - return '\u001b[' + inspect.colors[style][0] + 'm' + str + - '\u001b[' + inspect.colors[style][1] + 'm'; - } else { - return str; - } -} - - -function stylizeNoColor(str, styleType) { - return str; -} - - -function arrayToHash(array) { - var hash = {}; - - array.forEach(function(val, idx) { - hash[val] = true; - }); - - return hash; -} - - -function formatValue(ctx, value, recurseTimes) { - // Provide a hook for user-specified inspect functions. - // Check that value is an object with an inspect function on it - if (ctx.customInspect && - value && - isFunction(value.inspect) && - // Filter out the util module, it's inspect function is special - value.inspect !== exports.inspect && - // Also filter out any prototype objects using the circular check. - !(value.constructor && value.constructor.prototype === value)) { - var ret = value.inspect(recurseTimes, ctx); - if (!isString(ret)) { - ret = formatValue(ctx, ret, recurseTimes); - } - return ret; - } - - // Primitive types cannot have properties - var primitive = formatPrimitive(ctx, value); - if (primitive) { - return primitive; - } - - // Look up the keys of the object. - var keys = Object.keys(value); - var visibleKeys = arrayToHash(keys); - - if (ctx.showHidden) { - keys = Object.getOwnPropertyNames(value); - } - - // IE doesn't make error fields non-enumerable - // http://msdn.microsoft.com/en-us/library/ie/dww52sbt(v=vs.94).aspx - if (isError(value) - && (keys.indexOf('message') >= 0 || keys.indexOf('description') >= 0)) { - return formatError(value); - } - - // Some type of object without properties can be shortcutted. - if (keys.length === 0) { - if (isFunction(value)) { - var name = value.name ? ': ' + value.name : ''; - return ctx.stylize('[Function' + name + ']', 'special'); - } - if (isRegExp(value)) { - return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp'); - } - if (isDate(value)) { - return ctx.stylize(Date.prototype.toString.call(value), 'date'); - } - if (isError(value)) { - return formatError(value); - } - } - - var base = '', array = false, braces = ['{', '}']; - - // Make Array say that they are Array - if (isArray(value)) { - array = true; - braces = ['[', ']']; - } - - // Make functions say that they are functions - if (isFunction(value)) { - var n = value.name ? ': ' + value.name : ''; - base = ' [Function' + n + ']'; - } - - // Make RegExps say that they are RegExps - if (isRegExp(value)) { - base = ' ' + RegExp.prototype.toString.call(value); - } - - // Make dates with properties first say the date - if (isDate(value)) { - base = ' ' + Date.prototype.toUTCString.call(value); - } - - // Make error with message first say the error - if (isError(value)) { - base = ' ' + formatError(value); - } - - if (keys.length === 0 && (!array || value.length == 0)) { - return braces[0] + base + braces[1]; - } - - if (recurseTimes < 0) { - if (isRegExp(value)) { - return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp'); - } else { - return ctx.stylize('[Object]', 'special'); - } - } - - ctx.seen.push(value); - - var output; - if (array) { - output = formatArray(ctx, value, recurseTimes, visibleKeys, keys); - } else { - output = keys.map(function(key) { - return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array); - }); - } - - ctx.seen.pop(); - - return reduceToSingleString(output, base, braces); -} - - -function formatPrimitive(ctx, value) { - if (isUndefined(value)) - return ctx.stylize('undefined', 'undefined'); - if (isString(value)) { - var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '') - .replace(/'/g, "\\'") - .replace(/\\"/g, '"') + '\''; - return ctx.stylize(simple, 'string'); - } - if (isNumber(value)) - return ctx.stylize('' + value, 'number'); - if (isBoolean(value)) - return ctx.stylize('' + value, 'boolean'); - // For some reason typeof null is "object", so special case here. - if (isNull(value)) - return ctx.stylize('null', 'null'); -} - - -function formatError(value) { - return '[' + Error.prototype.toString.call(value) + ']'; -} - - -function formatArray(ctx, value, recurseTimes, visibleKeys, keys) { - var output = []; - for (var i = 0, l = value.length; i < l; ++i) { - if (hasOwnProperty(value, String(i))) { - output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, - String(i), true)); - } else { - output.push(''); - } - } - keys.forEach(function(key) { - if (!key.match(/^\d+$/)) { - output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, - key, true)); - } - }); - return output; -} - - -function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) { - var name, str, desc; - desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] }; - if (desc.get) { - if (desc.set) { - str = ctx.stylize('[Getter/Setter]', 'special'); - } else { - str = ctx.stylize('[Getter]', 'special'); - } - } else { - if (desc.set) { - str = ctx.stylize('[Setter]', 'special'); - } - } - if (!hasOwnProperty(visibleKeys, key)) { - name = '[' + key + ']'; - } - if (!str) { - if (ctx.seen.indexOf(desc.value) < 0) { - if (isNull(recurseTimes)) { - str = formatValue(ctx, desc.value, null); - } else { - str = formatValue(ctx, desc.value, recurseTimes - 1); - } - if (str.indexOf('\n') > -1) { - if (array) { - str = str.split('\n').map(function(line) { - return ' ' + line; - }).join('\n').substr(2); - } else { - str = '\n' + str.split('\n').map(function(line) { - return ' ' + line; - }).join('\n'); - } - } - } else { - str = ctx.stylize('[Circular]', 'special'); - } - } - if (isUndefined(name)) { - if (array && key.match(/^\d+$/)) { - return str; - } - name = JSON.stringify('' + key); - if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { - name = name.substr(1, name.length - 2); - name = ctx.stylize(name, 'name'); - } else { - name = name.replace(/'/g, "\\'") - .replace(/\\"/g, '"') - .replace(/(^"|"$)/g, "'"); - name = ctx.stylize(name, 'string'); - } - } - - return name + ': ' + str; -} - - -function reduceToSingleString(output, base, braces) { - var numLinesEst = 0; - var length = output.reduce(function(prev, cur) { - numLinesEst++; - if (cur.indexOf('\n') >= 0) numLinesEst++; - return prev + cur.replace(/\u001b\[\d\d?m/g, '').length + 1; - }, 0); - - if (length > 60) { - return braces[0] + - (base === '' ? '' : base + '\n ') + - ' ' + - output.join(',\n ') + - ' ' + - braces[1]; - } - - return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; -} - - -// NOTE: These type checking functions intentionally don't use `instanceof` -// because it is fragile and can be easily faked with `Object.create()`. -function isArray(ar) { - return Array.isArray(ar); -} -exports.isArray = isArray; - -function isBoolean(arg) { - return typeof arg === 'boolean'; -} -exports.isBoolean = isBoolean; - -function isNull(arg) { - return arg === null; -} -exports.isNull = isNull; - -function isNullOrUndefined(arg) { - return arg == null; -} -exports.isNullOrUndefined = isNullOrUndefined; - -function isNumber(arg) { - return typeof arg === 'number'; -} -exports.isNumber = isNumber; - -function isString(arg) { - return typeof arg === 'string'; -} -exports.isString = isString; - -function isSymbol(arg) { - return typeof arg === 'symbol'; -} -exports.isSymbol = isSymbol; - -function isUndefined(arg) { - return arg === void 0; -} -exports.isUndefined = isUndefined; - -function isRegExp(re) { - return isObject(re) && objectToString(re) === '[object RegExp]'; -} -exports.isRegExp = isRegExp; - -function isObject(arg) { - return typeof arg === 'object' && arg !== null; -} -exports.isObject = isObject; - -function isDate(d) { - return isObject(d) && objectToString(d) === '[object Date]'; -} -exports.isDate = isDate; - -function isError(e) { - return isObject(e) && - (objectToString(e) === '[object Error]' || e instanceof Error); -} -exports.isError = isError; - -function isFunction(arg) { - return typeof arg === 'function'; -} -exports.isFunction = isFunction; - -function isPrimitive(arg) { - return arg === null || - typeof arg === 'boolean' || - typeof arg === 'number' || - typeof arg === 'string' || - typeof arg === 'symbol' || // ES6 symbol - typeof arg === 'undefined'; -} -exports.isPrimitive = isPrimitive; - -exports.isBuffer = require('./support/isBuffer'); - -function objectToString(o) { - return Object.prototype.toString.call(o); -} - - -function pad(n) { - return n < 10 ? '0' + n.toString(10) : n.toString(10); -} - - -var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', - 'Oct', 'Nov', 'Dec']; - -// 26 Feb 16:19:34 -function timestamp() { - var d = new Date(); - var time = [pad(d.getHours()), - pad(d.getMinutes()), - pad(d.getSeconds())].join(':'); - return [d.getDate(), months[d.getMonth()], time].join(' '); -} - - -// log is just a thin wrapper to console.log that prepends a timestamp -exports.log = function() { - console.log('%s - %s', timestamp(), exports.format.apply(exports, arguments)); -}; - - -/** - * Inherit the prototype methods from one constructor into another. - * - * The Function.prototype.inherits from lang.js rewritten as a standalone - * function (not on Function.prototype). NOTE: If this file is to be loaded - * during bootstrapping this function needs to be rewritten using some native - * functions as prototype setup using normal JavaScript does not work as - * expected during bootstrapping (see mirror.js in r114903). - * - * @param {function} ctor Constructor function which needs to inherit the - * prototype. - * @param {function} superCtor Constructor function to inherit prototype from. - */ -exports.inherits = require('inherits'); - -exports._extend = function(origin, add) { - // Don't do anything if add isn't an object - if (!add || !isObject(add)) return origin; - - var keys = Object.keys(add); - var i = keys.length; - while (i--) { - origin[keys[i]] = add[keys[i]]; - } - return origin; -}; - -function hasOwnProperty(obj, prop) { - return Object.prototype.hasOwnProperty.call(obj, prop); -} - -}).call(this,require('_process'),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"./support/isBuffer":88,"_process":69,"inherits":56}],90:[function(require,module,exports){ -module.exports={ - "name": "mocha", - "version": "7.1.2", - "homepage": "https://mochajs.org/", - "notifyLogo": "https://ibin.co/4QuRuGjXvl36.png" -} -},{}]},{},[1]); diff --git a/src/tests/frontend/lib/sendkeys.js b/src/tests/frontend/lib/sendkeys.js deleted file mode 100644 index 91433f964..000000000 --- a/src/tests/frontend/lib/sendkeys.js +++ /dev/null @@ -1,467 +0,0 @@ -// Cross-broswer implementation of text ranges and selections -// documentation: http://bililite.com/blog/2011/01/11/cross-browser-.and-selections/ -// Version: 1.1 -// Copyright (c) 2010 Daniel Wachsstock -// MIT license: -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: - -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. - -(function($){ - -bililiteRange = function(el, debug){ - var ret; - if (debug){ - ret = new NothingRange(); // Easier to force it to use the no-selection type than to try to find an old browser - }else if (document.selection && !document.addEventListener){ - // Internet Explorer 8 and lower - ret = new IERange(); - }else if (window.getSelection && el.setSelectionRange){ - // Standards. Element is an input or textarea - ret = new InputRange(); - }else if (window.getSelection){ - // Standards, with any other kind of element - ret = new W3CRange() - }else{ - // doesn't support selection - ret = new NothingRange(); - } - ret._el = el; - ret._doc = el.ownerDocument; - ret._win = 'defaultView' in ret._doc ? ret._doc.defaultView : ret._doc.parentWindow; - ret._textProp = textProp(el); - ret._bounds = [0, ret.length()]; - return ret; -} - -function textProp(el){ - // returns the property that contains the text of the element - if (typeof el.value != 'undefined') return 'value'; - if (typeof el.text != 'undefined') return 'text'; - if (typeof el.textContent != 'undefined') return 'textContent'; - return 'innerText'; -} - -// base class -function Range(){} -Range.prototype = { - length: function() { - return this._el[this._textProp].replace(/\r/g, '').length; // need to correct for IE's CrLf weirdness - }, - bounds: function(s){ - if (s === 'all'){ - this._bounds = [0, this.length()]; - }else if (s === 'start'){ - this._bounds = [0, 0]; - }else if (s === 'end'){ - this._bounds = [this.length(), this.length()]; - }else if (s === 'selection'){ - this.bounds ('all'); // first select the whole thing for constraining - this._bounds = this._nativeSelection(); - }else if (s){ - this._bounds = s; // don't error check now; the element may change at any moment, so constrain it when we need it. - }else{ - var b = [ - Math.max(0, Math.min (this.length(), this._bounds[0])), - Math.max(0, Math.min (this.length(), this._bounds[1])) - ]; - return b; // need to constrain it to fit - } - return this; // allow for chaining - }, - select: function(){ - this._nativeSelect(this._nativeRange(this.bounds())); - return this; // allow for chaining - }, - text: function(text, select){ - if (arguments.length){ - this._nativeSetText(text, this._nativeRange(this.bounds())); - if (select == 'start'){ - this.bounds ([this._bounds[0], this._bounds[0]]); - this.select(); - }else if (select == 'end'){ - this.bounds ([this._bounds[0]+text.length, this._bounds[0]+text.length]); - this.select(); - }else if (select == 'all'){ - this.bounds ([this._bounds[0], this._bounds[0]+text.length]); - this.select(); - } - return this; // allow for chaining - }else{ - return this._nativeGetText(this._nativeRange(this.bounds())); - } - }, - insertEOL: function (){ - this._nativeEOL(); - this._bounds = [this._bounds[0]+1, this._bounds[0]+1]; // move past the EOL marker - return this; - } -}; - - -function IERange(){} -IERange.prototype = new Range(); -IERange.prototype._nativeRange = function (bounds){ - var rng; - if (this._el.tagName == 'INPUT'){ - // IE 8 is very inconsistent; textareas have createTextRange but it doesn't work - rng = this._el.createTextRange(); - }else{ - rng = this._doc.body.createTextRange (); - rng.moveToElementText(this._el); - } - if (bounds){ - if (bounds[1] < 0) bounds[1] = 0; // IE tends to run elements out of bounds - if (bounds[0] > this.length()) bounds[0] = this.length(); - if (bounds[1] < rng.text.replace(/\r/g, '').length){ // correct for IE's CrLf wierdness - // block-display elements have an invisible, uncounted end of element marker, so we move an extra one and use the current length of the range - rng.moveEnd ('character', -1); - rng.moveEnd ('character', bounds[1]-rng.text.replace(/\r/g, '').length); - } - if (bounds[0] > 0) rng.moveStart('character', bounds[0]); - } - return rng; -}; -IERange.prototype._nativeSelect = function (rng){ - rng.select(); -}; -IERange.prototype._nativeSelection = function (){ - // returns [start, end] for the selection constrained to be in element - var rng = this._nativeRange(); // range of the element to constrain to - var len = this.length(); - if (this._doc.selection.type != 'Text') return [0,0]; // append to the end - var sel = this._doc.selection.createRange(); - try{ - return [ - iestart(sel, rng), - ieend (sel, rng) - ]; - }catch (e){ - // IE gets upset sometimes about comparing text to input elements, but the selections cannot overlap, so make a best guess - return (sel.parentElement().sourceIndex < this._el.sourceIndex) ? [0,0] : [len, len]; - } -}; -IERange.prototype._nativeGetText = function (rng){ - return rng.text.replace(/\r/g, ''); // correct for IE's CrLf weirdness -}; -IERange.prototype._nativeSetText = function (text, rng){ - rng.text = text; -}; -IERange.prototype._nativeEOL = function(){ - if (typeof this._el.value != 'undefined'){ - this.text('\n'); // for input and textarea, insert it straight - }else{ - this._nativeRange(this.bounds()).pasteHTML('
    '); - } -}; -// IE internals -function iestart(rng, constraint){ - // returns the position (in character) of the start of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after - var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf wierdness - if (rng.compareEndPoints ('StartToStart', constraint) <= 0) return 0; // at or before the beginning - if (rng.compareEndPoints ('StartToEnd', constraint) >= 0) return len; - for (var i = 0; rng.compareEndPoints ('StartToStart', constraint) > 0; ++i, rng.moveStart('character', -1)); - return i; -} -function ieend (rng, constraint){ - // returns the position (in character) of the end of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after - var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf wierdness - if (rng.compareEndPoints ('EndToEnd', constraint) >= 0) return len; // at or after the end - if (rng.compareEndPoints ('EndToStart', constraint) <= 0) return 0; - for (var i = 0; rng.compareEndPoints ('EndToStart', constraint) > 0; ++i, rng.moveEnd('character', -1)); - return i; -} - -// an input element in a standards document. "Native Range" is just the bounds array -function InputRange(){} -InputRange.prototype = new Range(); -InputRange.prototype._nativeRange = function(bounds) { - return bounds || [0, this.length()]; -}; -InputRange.prototype._nativeSelect = function (rng){ - this._el.setSelectionRange(rng[0], rng[1]); -}; -InputRange.prototype._nativeSelection = function(){ - return [this._el.selectionStart, this._el.selectionEnd]; -}; -InputRange.prototype._nativeGetText = function(rng){ - return this._el.value.substring(rng[0], rng[1]); -}; -InputRange.prototype._nativeSetText = function(text, rng){ - var val = this._el.value; - this._el.value = val.substring(0, rng[0]) + text + val.substring(rng[1]); -}; -InputRange.prototype._nativeEOL = function(){ - this.text('\n'); -}; - -function W3CRange(){} -W3CRange.prototype = new Range(); -W3CRange.prototype._nativeRange = function (bounds){ - var rng = this._doc.createRange(); - rng.selectNodeContents(this._el); - if (bounds){ - w3cmoveBoundary (rng, bounds[0], true, this._el); - rng.collapse (true); - w3cmoveBoundary (rng, bounds[1]-bounds[0], false, this._el); - } - return rng; -}; -W3CRange.prototype._nativeSelect = function (rng){ - this._win.getSelection().removeAllRanges(); - this._win.getSelection().addRange (rng); -}; -W3CRange.prototype._nativeSelection = function (){ - // returns [start, end] for the selection constrained to be in element - var rng = this._nativeRange(); // range of the element to constrain to - if (this._win.getSelection().rangeCount == 0) return [this.length(), this.length()]; // append to the end - var sel = this._win.getSelection().getRangeAt(0); - return [ - w3cstart(sel, rng), - w3cend (sel, rng) - ]; - } -W3CRange.prototype._nativeGetText = function (rng){ - return rng.toString(); -}; -W3CRange.prototype._nativeSetText = function (text, rng){ - rng.deleteContents(); - rng.insertNode (this._doc.createTextNode(text)); - this._el.normalize(); // merge the text with the surrounding text -}; -W3CRange.prototype._nativeEOL = function(){ - var rng = this._nativeRange(this.bounds()); - rng.deleteContents(); - var br = this._doc.createElement('br'); - br.setAttribute ('_moz_dirty', ''); // for Firefox - rng.insertNode (br); - rng.insertNode (this._doc.createTextNode('\n')); - rng.collapse (false); -}; -// W3C internals -function nextnode (node, root){ - // in-order traversal - // we've already visited node, so get kids then siblings - if (node.firstChild) return node.firstChild; - if (node.nextSibling) return node.nextSibling; - if (node===root) return null; - while (node.parentNode){ - // get uncles - node = node.parentNode; - if (node == root) return null; - if (node.nextSibling) return node.nextSibling; - } - return null; -} -function w3cmoveBoundary (rng, n, bStart, el){ - // move the boundary (bStart == true ? start : end) n characters forward, up to the end of element el. Forward only! - // if the start is moved after the end, then an exception is raised - if (n <= 0) return; - var node = rng[bStart ? 'startContainer' : 'endContainer']; - if (node.nodeType == 3){ - // we may be starting somewhere into the text - n += rng[bStart ? 'startOffset' : 'endOffset']; - } - while (node){ - if (node.nodeType == 3){ - if (n <= node.nodeValue.length){ - rng[bStart ? 'setStart' : 'setEnd'](node, n); - // special case: if we end next to a
    , include that node. - if (n == node.nodeValue.length){ - // skip past zero-length text nodes - for (var next = nextnode (node, el); next && next.nodeType==3 && next.nodeValue.length == 0; next = nextnode(next, el)){ - rng[bStart ? 'setStartAfter' : 'setEndAfter'](next); - } - if (next && next.nodeType == 1 && next.nodeName == "BR") rng[bStart ? 'setStartAfter' : 'setEndAfter'](next); - } - return; - }else{ - rng[bStart ? 'setStartAfter' : 'setEndAfter'](node); // skip past this one - n -= node.nodeValue.length; // and eat these characters - } - } - node = nextnode (node, el); - } -} -var START_TO_START = 0; // from the w3c definitions -var START_TO_END = 1; -var END_TO_END = 2; -var END_TO_START = 3; -// from the Mozilla documentation, for range.compareBoundaryPoints(how, sourceRange) -// -1, 0, or 1, indicating whether the corresponding boundary-point of range is respectively before, equal to, or after the corresponding boundary-point of sourceRange. - // * Range.END_TO_END compares the end boundary-point of sourceRange to the end boundary-point of range. - // * Range.END_TO_START compares the end boundary-point of sourceRange to the start boundary-point of range. - // * Range.START_TO_END compares the start boundary-point of sourceRange to the end boundary-point of range. - // * Range.START_TO_START compares the start boundary-point of sourceRange to the start boundary-point of range. -function w3cstart(rng, constraint){ - if (rng.compareBoundaryPoints (START_TO_START, constraint) <= 0) return 0; // at or before the beginning - if (rng.compareBoundaryPoints (END_TO_START, constraint) >= 0) return constraint.toString().length; - rng = rng.cloneRange(); // don't change the original - rng.setEnd (constraint.endContainer, constraint.endOffset); // they now end at the same place - return constraint.toString().length - rng.toString().length; -} -function w3cend (rng, constraint){ - if (rng.compareBoundaryPoints (END_TO_END, constraint) >= 0) return constraint.toString().length; // at or after the end - if (rng.compareBoundaryPoints (START_TO_END, constraint) <= 0) return 0; - rng = rng.cloneRange(); // don't change the original - rng.setStart (constraint.startContainer, constraint.startOffset); // they now start at the same place - return rng.toString().length; -} - -function NothingRange(){} -NothingRange.prototype = new Range(); -NothingRange.prototype._nativeRange = function(bounds) { - return bounds || [0,this.length()]; -}; -NothingRange.prototype._nativeSelect = function (rng){ // do nothing -}; -NothingRange.prototype._nativeSelection = function(){ - return [0,0]; -}; -NothingRange.prototype._nativeGetText = function (rng){ - return this._el[this._textProp].substring(rng[0], rng[1]); -}; -NothingRange.prototype._nativeSetText = function (text, rng){ - var val = this._el[this._textProp]; - this._el[this._textProp] = val.substring(0, rng[0]) + text + val.substring(rng[1]); -}; -NothingRange.prototype._nativeEOL = function(){ - this.text('\n'); -}; - -})(jQuery); - -// insert characters in a textarea or text input field -// special characters are enclosed in {}; use {{} for the { character itself -// documentation: http://bililite.com/blog/2008/08/20/the-fnsendkeys-plugin/ -// Version: 2.0 -// Copyright (c) 2010 Daniel Wachsstock -// MIT license: -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: - -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. - -(function($){ - -$.fn.sendkeys = function (x, opts){ - return this.each( function(){ - var localkeys = $.extend({}, opts, $(this).data('sendkeys')); // allow for element-specific key functions - // most elements to not keep track of their selection when they lose focus, so we have to do it for them - var rng = $.data (this, 'sendkeys.selection'); - if (!rng){ - rng = bililiteRange(this).bounds('selection'); - $.data(this, 'sendkeys.selection', rng); - $(this).bind('mouseup.sendkeys', function(){ - // we have to update the saved range. The routines here update the bounds with each press, but actual keypresses and mouseclicks do not - $.data(this, 'sendkeys.selection').bounds('selection'); - }).bind('keyup.sendkeys', function(evt){ - // restore the selection if we got here with a tab (a click should select what was clicked on) - if (evt.which == 9){ - // there's a flash of selection when we restore the focus, but I don't know how to avoid that - $.data(this, 'sendkeys.selection').select(); - }else{ - $.data(this, 'sendkeys.selection').bounds('selection'); - } - }); - } - this.focus(); - if (typeof x === 'undefined') return; // no string, so we just set up the event handlers - $.data(this, 'sendkeys.originalText', rng.text()); - x.replace(/\n/g, '{enter}'). // turn line feeds into explicit break insertions - replace(/{[^}]*}|[^{]+/g, function(s){ - (localkeys[s] || $.fn.sendkeys.defaults[s] || $.fn.sendkeys.defaults.simplechar)(rng, s); - }); - $(this).trigger({type: 'sendkeys', which: x}); - }); -}; // sendkeys - - -// add the functions publicly so they can be overridden -$.fn.sendkeys.defaults = { - simplechar: function (rng, s){ - rng.text(s, 'end'); - for (var i =0; i < s.length; ++i){ - var x = s.charCodeAt(i); - // a bit of cheating: rng._el is the element associated with rng. - $(rng._el).trigger({type: 'keypress', keyCode: x, which: x, charCode: x}); - } - }, - '{{}': function (rng){ - $.fn.sendkeys.defaults.simplechar (rng, '{') - }, - '{enter}': function (rng){ - rng.insertEOL(); - rng.select(); - $(rng._el).trigger( - {type: 'keypress', keyCode: 13, which: 13, charCode: 13, code: 'Enter', key: 'Enter'}); - }, - '{backspace}': function (rng){ - var b = rng.bounds(); - if (b[0] == b[1]) rng.bounds([b[0]-1, b[0]]); // no characters selected; it's just an insertion point. Remove the previous character - rng.text('', 'end'); // delete the characters and update the selection - }, - '{del}': function (rng){ - var b = rng.bounds(); - if (b[0] == b[1]) rng.bounds([b[0], b[0]+1]); // no characters selected; it's just an insertion point. Remove the next character - rng.text('', 'end'); // delete the characters and update the selection - }, - '{rightarrow}': function (rng){ - var b = rng.bounds(); - if (b[0] == b[1]) ++b[1]; // no characters selected; it's just an insertion point. Move to the right - rng.bounds([b[1], b[1]]).select(); - }, - '{leftarrow}': function (rng){ - var b = rng.bounds(); - if (b[0] == b[1]) --b[0]; // no characters selected; it's just an insertion point. Move to the left - rng.bounds([b[0], b[0]]).select(); - }, - '{selectall}' : function (rng){ - rng.bounds('all').select(); - }, - '{selection}': function (rng){ - $.fn.sendkeys.defaults.simplechar(rng, $.data(rng._el, 'sendkeys.originalText')); - }, - '{mark}' : function (rng){ - var bounds = rng.bounds(); - $(rng._el).one('sendkeys', function(){ - // set up the event listener to change the selection after the sendkeys is done - rng.bounds(bounds).select(); - }); - } -}; - -})(jQuery) diff --git a/src/tests/frontend/lib/underscore.js b/src/tests/frontend/lib/underscore.js deleted file mode 100644 index 1ebe2671b..000000000 --- a/src/tests/frontend/lib/underscore.js +++ /dev/null @@ -1,1200 +0,0 @@ -// Underscore.js 1.4.2 -// http://underscorejs.org -// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. -// Underscore may be freely distributed under the MIT license. - -(function() { - - // Baseline setup - // -------------- - - // Establish the root object, `window` in the browser, or `global` on the server. - var root = this; - - // Save the previous value of the `_` variable. - var previousUnderscore = root._; - - // Establish the object that gets returned to break out of a loop iteration. - var breaker = {}; - - // Save bytes in the minified (but not gzipped) version: - var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; - - // Create quick reference variables for speed access to core prototypes. - var push = ArrayProto.push, - slice = ArrayProto.slice, - concat = ArrayProto.concat, - unshift = ArrayProto.unshift, - toString = ObjProto.toString, - hasOwnProperty = ObjProto.hasOwnProperty; - - // All **ECMAScript 5** native function implementations that we hope to use - // are declared here. - var - nativeForEach = ArrayProto.forEach, - nativeMap = ArrayProto.map, - nativeReduce = ArrayProto.reduce, - nativeReduceRight = ArrayProto.reduceRight, - nativeFilter = ArrayProto.filter, - nativeEvery = ArrayProto.every, - nativeSome = ArrayProto.some, - nativeIndexOf = ArrayProto.indexOf, - nativeLastIndexOf = ArrayProto.lastIndexOf, - nativeIsArray = Array.isArray, - nativeKeys = Object.keys, - nativeBind = FuncProto.bind; - - // Create a safe reference to the Underscore object for use below. - var _ = function(obj) { - if (obj instanceof _) return obj; - if (!(this instanceof _)) return new _(obj); - this._wrapped = obj; - }; - - // Export the Underscore object for **Node.js**, with - // backwards-compatibility for the old `require()` API. If we're in - // the browser, add `_` as a global object via a string identifier, - // for Closure Compiler "advanced" mode. - if (typeof exports !== 'undefined') { - if (typeof module !== 'undefined' && module.exports) { - exports = module.exports = _; - } - exports._ = _; - } else { - root['_'] = _; - } - - // Current version. - _.VERSION = '1.4.2'; - - // Collection Functions - // -------------------- - - // The cornerstone, an `each` implementation, aka `forEach`. - // Handles objects with the built-in `forEach`, arrays, and raw objects. - // Delegates to **ECMAScript 5**'s native `forEach` if available. - var each = _.each = _.forEach = function(obj, iterator, context) { - if (obj == null) return; - if (nativeForEach && obj.forEach === nativeForEach) { - obj.forEach(iterator, context); - } else if (obj.length === +obj.length) { - for (var i = 0, l = obj.length; i < l; i++) { - if (iterator.call(context, obj[i], i, obj) === breaker) return; - } - } else { - for (var key in obj) { - if (_.has(obj, key)) { - if (iterator.call(context, obj[key], key, obj) === breaker) return; - } - } - } - }; - - // Return the results of applying the iterator to each element. - // Delegates to **ECMAScript 5**'s native `map` if available. - _.map = _.collect = function(obj, iterator, context) { - var results = []; - if (obj == null) return results; - if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); - each(obj, function(value, index, list) { - results[results.length] = iterator.call(context, value, index, list); - }); - return results; - }; - - // **Reduce** builds up a single result from a list of values, aka `inject`, - // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. - _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { - var initial = arguments.length > 2; - if (obj == null) obj = []; - if (nativeReduce && obj.reduce === nativeReduce) { - if (context) iterator = _.bind(iterator, context); - return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); - } - each(obj, function(value, index, list) { - if (!initial) { - memo = value; - initial = true; - } else { - memo = iterator.call(context, memo, value, index, list); - } - }); - if (!initial) throw new TypeError('Reduce of empty array with no initial value'); - return memo; - }; - - // The right-associative version of reduce, also known as `foldr`. - // Delegates to **ECMAScript 5**'s native `reduceRight` if available. - _.reduceRight = _.foldr = function(obj, iterator, memo, context) { - var initial = arguments.length > 2; - if (obj == null) obj = []; - if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { - if (context) iterator = _.bind(iterator, context); - return arguments.length > 2 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); - } - var length = obj.length; - if (length !== +length) { - var keys = _.keys(obj); - length = keys.length; - } - each(obj, function(value, index, list) { - index = keys ? keys[--length] : --length; - if (!initial) { - memo = obj[index]; - initial = true; - } else { - memo = iterator.call(context, memo, obj[index], index, list); - } - }); - if (!initial) throw new TypeError('Reduce of empty array with no initial value'); - return memo; - }; - - // Return the first value which passes a truth test. Aliased as `detect`. - _.find = _.detect = function(obj, iterator, context) { - var result; - any(obj, function(value, index, list) { - if (iterator.call(context, value, index, list)) { - result = value; - return true; - } - }); - return result; - }; - - // Return all the elements that pass a truth test. - // Delegates to **ECMAScript 5**'s native `filter` if available. - // Aliased as `select`. - _.filter = _.select = function(obj, iterator, context) { - var results = []; - if (obj == null) return results; - if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); - each(obj, function(value, index, list) { - if (iterator.call(context, value, index, list)) results[results.length] = value; - }); - return results; - }; - - // Return all the elements for which a truth test fails. - _.reject = function(obj, iterator, context) { - var results = []; - if (obj == null) return results; - each(obj, function(value, index, list) { - if (!iterator.call(context, value, index, list)) results[results.length] = value; - }); - return results; - }; - - // Determine whether all of the elements match a truth test. - // Delegates to **ECMAScript 5**'s native `every` if available. - // Aliased as `all`. - _.every = _.all = function(obj, iterator, context) { - iterator || (iterator = _.identity); - var result = true; - if (obj == null) return result; - if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); - each(obj, function(value, index, list) { - if (!(result = result && iterator.call(context, value, index, list))) return breaker; - }); - return !!result; - }; - - // Determine if at least one element in the object matches a truth test. - // Delegates to **ECMAScript 5**'s native `some` if available. - // Aliased as `any`. - var any = _.some = _.any = function(obj, iterator, context) { - iterator || (iterator = _.identity); - var result = false; - if (obj == null) return result; - if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); - each(obj, function(value, index, list) { - if (result || (result = iterator.call(context, value, index, list))) return breaker; - }); - return !!result; - }; - - // Determine if the array or object contains a given value (using `===`). - // Aliased as `include`. - _.contains = _.include = function(obj, target) { - var found = false; - if (obj == null) return found; - if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; - found = any(obj, function(value) { - return value === target; - }); - return found; - }; - - // Invoke a method (with arguments) on every item in a collection. - _.invoke = function(obj, method) { - var args = slice.call(arguments, 2); - return _.map(obj, function(value) { - return (_.isFunction(method) ? method : value[method]).apply(value, args); - }); - }; - - // Convenience version of a common use case of `map`: fetching a property. - _.pluck = function(obj, key) { - return _.map(obj, function(value){ return value[key]; }); - }; - - // Convenience version of a common use case of `filter`: selecting only objects - // with specific `key:value` pairs. - _.where = function(obj, attrs) { - if (_.isEmpty(attrs)) return []; - return _.filter(obj, function(value) { - for (var key in attrs) { - if (attrs[key] !== value[key]) return false; - } - return true; - }); - }; - - // Return the maximum element or (element-based computation). - // Can't optimize arrays of integers longer than 65,535 elements. - // See: https://bugs.webkit.org/show_bug.cgi?id=80797 - _.max = function(obj, iterator, context) { - if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { - return Math.max.apply(Math, obj); - } - if (!iterator && _.isEmpty(obj)) return -Infinity; - var result = {computed : -Infinity}; - each(obj, function(value, index, list) { - var computed = iterator ? iterator.call(context, value, index, list) : value; - computed >= result.computed && (result = {value : value, computed : computed}); - }); - return result.value; - }; - - // Return the minimum element (or element-based computation). - _.min = function(obj, iterator, context) { - if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { - return Math.min.apply(Math, obj); - } - if (!iterator && _.isEmpty(obj)) return Infinity; - var result = {computed : Infinity}; - each(obj, function(value, index, list) { - var computed = iterator ? iterator.call(context, value, index, list) : value; - computed < result.computed && (result = {value : value, computed : computed}); - }); - return result.value; - }; - - // Shuffle an array. - _.shuffle = function(obj) { - var rand; - var index = 0; - var shuffled = []; - each(obj, function(value) { - rand = _.random(index++); - shuffled[index - 1] = shuffled[rand]; - shuffled[rand] = value; - }); - return shuffled; - }; - - // An internal function to generate lookup iterators. - var lookupIterator = function(value) { - return _.isFunction(value) ? value : function(obj){ return obj[value]; }; - }; - - // Sort the object's values by a criterion produced by an iterator. - _.sortBy = function(obj, value, context) { - var iterator = lookupIterator(value); - return _.pluck(_.map(obj, function(value, index, list) { - return { - value : value, - index : index, - criteria : iterator.call(context, value, index, list) - }; - }).sort(function(left, right) { - var a = left.criteria; - var b = right.criteria; - if (a !== b) { - if (a > b || a === void 0) return 1; - if (a < b || b === void 0) return -1; - } - return left.index < right.index ? -1 : 1; - }), 'value'); - }; - - // An internal function used for aggregate "group by" operations. - var group = function(obj, value, context, behavior) { - var result = {}; - var iterator = lookupIterator(value); - each(obj, function(value, index) { - var key = iterator.call(context, value, index, obj); - behavior(result, key, value); - }); - return result; - }; - - // Groups the object's values by a criterion. Pass either a string attribute - // to group by, or a function that returns the criterion. - _.groupBy = function(obj, value, context) { - return group(obj, value, context, function(result, key, value) { - (_.has(result, key) ? result[key] : (result[key] = [])).push(value); - }); - }; - - // Counts instances of an object that group by a certain criterion. Pass - // either a string attribute to count by, or a function that returns the - // criterion. - _.countBy = function(obj, value, context) { - return group(obj, value, context, function(result, key, value) { - if (!_.has(result, key)) result[key] = 0; - result[key]++; - }); - }; - - // Use a comparator function to figure out the smallest index at which - // an object should be inserted so as to maintain order. Uses binary search. - _.sortedIndex = function(array, obj, iterator, context) { - iterator = iterator == null ? _.identity : lookupIterator(iterator); - var value = iterator.call(context, obj); - var low = 0, high = array.length; - while (low < high) { - var mid = (low + high) >>> 1; - iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; - } - return low; - }; - - // Safely convert anything iterable into a real, live array. - _.toArray = function(obj) { - if (!obj) return []; - if (obj.length === +obj.length) return slice.call(obj); - return _.values(obj); - }; - - // Return the number of elements in an object. - _.size = function(obj) { - return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; - }; - - // Array Functions - // --------------- - - // Get the first element of an array. Passing **n** will return the first N - // values in the array. Aliased as `head` and `take`. The **guard** check - // allows it to work with `_.map`. - _.first = _.head = _.take = function(array, n, guard) { - return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; - }; - - // Returns everything but the last entry of the array. Especially useful on - // the arguments object. Passing **n** will return all the values in - // the array, excluding the last N. The **guard** check allows it to work with - // `_.map`. - _.initial = function(array, n, guard) { - return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); - }; - - // Get the last element of an array. Passing **n** will return the last N - // values in the array. The **guard** check allows it to work with `_.map`. - _.last = function(array, n, guard) { - if ((n != null) && !guard) { - return slice.call(array, Math.max(array.length - n, 0)); - } else { - return array[array.length - 1]; - } - }; - - // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. - // Especially useful on the arguments object. Passing an **n** will return - // the rest N values in the array. The **guard** - // check allows it to work with `_.map`. - _.rest = _.tail = _.drop = function(array, n, guard) { - return slice.call(array, (n == null) || guard ? 1 : n); - }; - - // Trim out all falsy values from an array. - _.compact = function(array) { - return _.filter(array, function(value){ return !!value; }); - }; - - // Internal implementation of a recursive `flatten` function. - var flatten = function(input, shallow, output) { - each(input, function(value) { - if (_.isArray(value)) { - shallow ? push.apply(output, value) : flatten(value, shallow, output); - } else { - output.push(value); - } - }); - return output; - }; - - // Return a completely flattened version of an array. - _.flatten = function(array, shallow) { - return flatten(array, shallow, []); - }; - - // Return a version of the array that does not contain the specified value(s). - _.without = function(array) { - return _.difference(array, slice.call(arguments, 1)); - }; - - // Produce a duplicate-free version of the array. If the array has already - // been sorted, you have the option of using a faster algorithm. - // Aliased as `unique`. - _.uniq = _.unique = function(array, isSorted, iterator, context) { - var initial = iterator ? _.map(array, iterator, context) : array; - var results = []; - var seen = []; - each(initial, function(value, index) { - if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { - seen.push(value); - results.push(array[index]); - } - }); - return results; - }; - - // Produce an array that contains the union: each distinct element from all of - // the passed-in arrays. - _.union = function() { - return _.uniq(concat.apply(ArrayProto, arguments)); - }; - - // Produce an array that contains every item shared between all the - // passed-in arrays. - _.intersection = function(array) { - var rest = slice.call(arguments, 1); - return _.filter(_.uniq(array), function(item) { - return _.every(rest, function(other) { - return _.indexOf(other, item) >= 0; - }); - }); - }; - - // Take the difference between one array and a number of other arrays. - // Only the elements present in just the first array will remain. - _.difference = function(array) { - var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); - return _.filter(array, function(value){ return !_.contains(rest, value); }); - }; - - // Zip together multiple lists into a single array -- elements that share - // an index go together. - _.zip = function() { - var args = slice.call(arguments); - var length = _.max(_.pluck(args, 'length')); - var results = new Array(length); - for (var i = 0; i < length; i++) { - results[i] = _.pluck(args, "" + i); - } - return results; - }; - - // Converts lists into objects. Pass either a single array of `[key, value]` - // pairs, or two parallel arrays of the same length -- one of keys, and one of - // the corresponding values. - _.object = function(list, values) { - var result = {}; - for (var i = 0, l = list.length; i < l; i++) { - if (values) { - result[list[i]] = values[i]; - } else { - result[list[i][0]] = list[i][1]; - } - } - return result; - }; - - // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), - // we need this function. Return the position of the first occurrence of an - // item in an array, or -1 if the item is not included in the array. - // Delegates to **ECMAScript 5**'s native `indexOf` if available. - // If the array is large and already in sort order, pass `true` - // for **isSorted** to use binary search. - _.indexOf = function(array, item, isSorted) { - if (array == null) return -1; - var i = 0, l = array.length; - if (isSorted) { - if (typeof isSorted == 'number') { - i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted); - } else { - i = _.sortedIndex(array, item); - return array[i] === item ? i : -1; - } - } - if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); - for (; i < l; i++) if (array[i] === item) return i; - return -1; - }; - - // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. - _.lastIndexOf = function(array, item, from) { - if (array == null) return -1; - var hasIndex = from != null; - if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { - return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); - } - var i = (hasIndex ? from : array.length); - while (i--) if (array[i] === item) return i; - return -1; - }; - - // Generate an integer Array containing an arithmetic progression. A port of - // the native Python `range()` function. See - // [the Python documentation](http://docs.python.org/library/functions.html#range). - _.range = function(start, stop, step) { - if (arguments.length <= 1) { - stop = start || 0; - start = 0; - } - step = arguments[2] || 1; - - var len = Math.max(Math.ceil((stop - start) / step), 0); - var idx = 0; - var range = new Array(len); - - while(idx < len) { - range[idx++] = start; - start += step; - } - - return range; - }; - - // Function (ahem) Functions - // ------------------ - - // Reusable constructor function for prototype setting. - var ctor = function(){}; - - // Create a function bound to a given object (assigning `this`, and arguments, - // optionally). Binding with arguments is also known as `curry`. - // Delegates to **ECMAScript 5**'s native `Function.bind` if available. - // We check for `func.bind` first, to fail fast when `func` is undefined. - _.bind = function bind(func, context) { - var bound, args; - if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); - if (!_.isFunction(func)) throw new TypeError; - args = slice.call(arguments, 2); - return bound = function() { - if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); - ctor.prototype = func.prototype; - var self = new ctor; - var result = func.apply(self, args.concat(slice.call(arguments))); - if (Object(result) === result) return result; - return self; - }; - }; - - // Bind all of an object's methods to that object. Useful for ensuring that - // all callbacks defined on an object belong to it. - _.bindAll = function(obj) { - var funcs = slice.call(arguments, 1); - if (funcs.length == 0) funcs = _.functions(obj); - each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); - return obj; - }; - - // Memoize an expensive function by storing its results. - _.memoize = function(func, hasher) { - var memo = {}; - hasher || (hasher = _.identity); - return function() { - var key = hasher.apply(this, arguments); - return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); - }; - }; - - // Delays a function for the given number of milliseconds, and then calls - // it with the arguments supplied. - _.delay = function(func, wait) { - var args = slice.call(arguments, 2); - return setTimeout(function(){ return func.apply(null, args); }, wait); - }; - - // Defers a function, scheduling it to run after the current call stack has - // cleared. - _.defer = function(func) { - return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); - }; - - // Returns a function, that, when invoked, will only be triggered at most once - // during a given window of time. - _.throttle = function(func, wait) { - var context, args, timeout, throttling, more, result; - var whenDone = _.debounce(function(){ more = throttling = false; }, wait); - return function() { - context = this; args = arguments; - var later = function() { - timeout = null; - if (more) { - result = func.apply(context, args); - } - whenDone(); - }; - if (!timeout) timeout = setTimeout(later, wait); - if (throttling) { - more = true; - } else { - throttling = true; - result = func.apply(context, args); - } - whenDone(); - return result; - }; - }; - - // Returns a function, that, as long as it continues to be invoked, will not - // be triggered. The function will be called after it stops being called for - // N milliseconds. If `immediate` is passed, trigger the function on the - // leading edge, instead of the trailing. - _.debounce = function(func, wait, immediate) { - var timeout, result; - return function() { - var context = this, args = arguments; - var later = function() { - timeout = null; - if (!immediate) result = func.apply(context, args); - }; - var callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) result = func.apply(context, args); - return result; - }; - }; - - // Returns a function that will be executed at most one time, no matter how - // often you call it. Useful for lazy initialization. - _.once = function(func) { - var ran = false, memo; - return function() { - if (ran) return memo; - ran = true; - memo = func.apply(this, arguments); - func = null; - return memo; - }; - }; - - // Returns the first function passed as an argument to the second, - // allowing you to adjust arguments, run code before and after, and - // conditionally execute the original function. - _.wrap = function(func, wrapper) { - return function() { - var args = [func]; - push.apply(args, arguments); - return wrapper.apply(this, args); - }; - }; - - // Returns a function that is the composition of a list of functions, each - // consuming the return value of the function that follows. - _.compose = function() { - var funcs = arguments; - return function() { - var args = arguments; - for (var i = funcs.length - 1; i >= 0; i--) { - args = [funcs[i].apply(this, args)]; - } - return args[0]; - }; - }; - - // Returns a function that will only be executed after being called N times. - _.after = function(times, func) { - if (times <= 0) return func(); - return function() { - if (--times < 1) { - return func.apply(this, arguments); - } - }; - }; - - // Object Functions - // ---------------- - - // Retrieve the names of an object's properties. - // Delegates to **ECMAScript 5**'s native `Object.keys` - _.keys = nativeKeys || function(obj) { - if (obj !== Object(obj)) throw new TypeError('Invalid object'); - var keys = []; - for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; - return keys; - }; - - // Retrieve the values of an object's properties. - _.values = function(obj) { - var values = []; - for (var key in obj) if (_.has(obj, key)) values.push(obj[key]); - return values; - }; - - // Convert an object into a list of `[key, value]` pairs. - _.pairs = function(obj) { - var pairs = []; - for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]); - return pairs; - }; - - // Invert the keys and values of an object. The values must be serializable. - _.invert = function(obj) { - var result = {}; - for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key; - return result; - }; - - // Return a sorted list of the function names available on the object. - // Aliased as `methods` - _.functions = _.methods = function(obj) { - var names = []; - for (var key in obj) { - if (_.isFunction(obj[key])) names.push(key); - } - return names.sort(); - }; - - // Extend a given object with all the properties in passed-in object(s). - _.extend = function(obj) { - each(slice.call(arguments, 1), function(source) { - for (var prop in source) { - obj[prop] = source[prop]; - } - }); - return obj; - }; - - // Return a copy of the object only containing the whitelisted properties. - _.pick = function(obj) { - var copy = {}; - var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); - each(keys, function(key) { - if (key in obj) copy[key] = obj[key]; - }); - return copy; - }; - - // Return a copy of the object without the blacklisted properties. - _.omit = function(obj) { - var copy = {}; - var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); - for (var key in obj) { - if (!_.contains(keys, key)) copy[key] = obj[key]; - } - return copy; - }; - - // Fill in a given object with default properties. - _.defaults = function(obj) { - each(slice.call(arguments, 1), function(source) { - for (var prop in source) { - if (obj[prop] == null) obj[prop] = source[prop]; - } - }); - return obj; - }; - - // Create a (shallow-cloned) duplicate of an object. - _.clone = function(obj) { - if (!_.isObject(obj)) return obj; - return _.isArray(obj) ? obj.slice() : _.extend({}, obj); - }; - - // Invokes interceptor with the obj, and then returns obj. - // The primary purpose of this method is to "tap into" a method chain, in - // order to perform operations on intermediate results within the chain. - _.tap = function(obj, interceptor) { - interceptor(obj); - return obj; - }; - - // Internal recursive comparison function for `isEqual`. - var eq = function(a, b, aStack, bStack) { - // Identical objects are equal. `0 === -0`, but they aren't identical. - // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. - if (a === b) return a !== 0 || 1 / a == 1 / b; - // A strict comparison is necessary because `null == undefined`. - if (a == null || b == null) return a === b; - // Unwrap any wrapped objects. - if (a instanceof _) a = a._wrapped; - if (b instanceof _) b = b._wrapped; - // Compare `[[Class]]` names. - var className = toString.call(a); - if (className != toString.call(b)) return false; - switch (className) { - // Strings, numbers, dates, and booleans are compared by value. - case '[object String]': - // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is - // equivalent to `new String("5")`. - return a == String(b); - case '[object Number]': - // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for - // other numeric values. - return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); - case '[object Date]': - case '[object Boolean]': - // Coerce dates and booleans to numeric primitive values. Dates are compared by their - // millisecond representations. Note that invalid dates with millisecond representations - // of `NaN` are not equivalent. - return +a == +b; - // RegExps are compared by their source patterns and flags. - case '[object RegExp]': - return a.source == b.source && - a.global == b.global && - a.multiline == b.multiline && - a.ignoreCase == b.ignoreCase; - } - if (typeof a != 'object' || typeof b != 'object') return false; - // Assume equality for cyclic structures. The algorithm for detecting cyclic - // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. - var length = aStack.length; - while (length--) { - // Linear search. Performance is inversely proportional to the number of - // unique nested structures. - if (aStack[length] == a) return bStack[length] == b; - } - // Add the first object to the stack of traversed objects. - aStack.push(a); - bStack.push(b); - var size = 0, result = true; - // Recursively compare objects and arrays. - if (className == '[object Array]') { - // Compare array lengths to determine if a deep comparison is necessary. - size = a.length; - result = size == b.length; - if (result) { - // Deep compare the contents, ignoring non-numeric properties. - while (size--) { - if (!(result = eq(a[size], b[size], aStack, bStack))) break; - } - } - } else { - // Objects with different constructors are not equivalent, but `Object`s - // from different frames are. - var aCtor = a.constructor, bCtor = b.constructor; - if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && - _.isFunction(bCtor) && (bCtor instanceof bCtor))) { - return false; - } - // Deep compare objects. - for (var key in a) { - if (_.has(a, key)) { - // Count the expected number of properties. - size++; - // Deep compare each member. - if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; - } - } - // Ensure that both objects contain the same number of properties. - if (result) { - for (key in b) { - if (_.has(b, key) && !(size--)) break; - } - result = !size; - } - } - // Remove the first object from the stack of traversed objects. - aStack.pop(); - bStack.pop(); - return result; - }; - - // Perform a deep comparison to check if two objects are equal. - _.isEqual = function(a, b) { - return eq(a, b, [], []); - }; - - // Is a given array, string, or object empty? - // An "empty" object has no enumerable own-properties. - _.isEmpty = function(obj) { - if (obj == null) return true; - if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; - for (var key in obj) if (_.has(obj, key)) return false; - return true; - }; - - // Is a given value a DOM element? - _.isElement = function(obj) { - return !!(obj && obj.nodeType === 1); - }; - - // Is a given value an array? - // Delegates to ECMA5's native Array.isArray - _.isArray = nativeIsArray || function(obj) { - return toString.call(obj) == '[object Array]'; - }; - - // Is a given variable an object? - _.isObject = function(obj) { - return obj === Object(obj); - }; - - // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. - each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { - _['is' + name] = function(obj) { - return toString.call(obj) == '[object ' + name + ']'; - }; - }); - - // Define a fallback version of the method in browsers (ahem, IE), where - // there isn't any inspectable "Arguments" type. - if (!_.isArguments(arguments)) { - _.isArguments = function(obj) { - return !!(obj && _.has(obj, 'callee')); - }; - } - - // Optimize `isFunction` if appropriate. - if (typeof (/./) !== 'function') { - _.isFunction = function(obj) { - return typeof obj === 'function'; - }; - } - - // Is a given object a finite number? - _.isFinite = function(obj) { - return _.isNumber(obj) && isFinite(obj); - }; - - // Is the given value `NaN`? (NaN is the only number which does not equal itself). - _.isNaN = function(obj) { - return _.isNumber(obj) && obj != +obj; - }; - - // Is a given value a boolean? - _.isBoolean = function(obj) { - return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; - }; - - // Is a given value equal to null? - _.isNull = function(obj) { - return obj === null; - }; - - // Is a given variable undefined? - _.isUndefined = function(obj) { - return obj === void 0; - }; - - // Shortcut function for checking if an object has a given property directly - // on itself (in other words, not on a prototype). - _.has = function(obj, key) { - return hasOwnProperty.call(obj, key); - }; - - // Utility Functions - // ----------------- - - // Run Underscore.js in *noConflict* mode, returning the `_` variable to its - // previous owner. Returns a reference to the Underscore object. - _.noConflict = function() { - root._ = previousUnderscore; - return this; - }; - - // Keep the identity function around for default iterators. - _.identity = function(value) { - return value; - }; - - // Run a function **n** times. - _.times = function(n, iterator, context) { - for (var i = 0; i < n; i++) iterator.call(context, i); - }; - - // Return a random integer between min and max (inclusive). - _.random = function(min, max) { - if (max == null) { - max = min; - min = 0; - } - return min + (0 | Math.random() * (max - min + 1)); - }; - - // List of HTML entities for escaping. - var entityMap = { - escape: { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/' - } - }; - entityMap.unescape = _.invert(entityMap.escape); - - // Regexes containing the keys and values listed immediately above. - var entityRegexes = { - escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), - unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') - }; - - // Functions for escaping and unescaping strings to/from HTML interpolation. - _.each(['escape', 'unescape'], function(method) { - _[method] = function(string) { - if (string == null) return ''; - return ('' + string).replace(entityRegexes[method], function(match) { - return entityMap[method][match]; - }); - }; - }); - - // If the value of the named property is a function then invoke it; - // otherwise, return it. - _.result = function(object, property) { - if (object == null) return null; - var value = object[property]; - return _.isFunction(value) ? value.call(object) : value; - }; - - // Add your own custom functions to the Underscore object. - _.mixin = function(obj) { - each(_.functions(obj), function(name){ - var func = _[name] = obj[name]; - _.prototype[name] = function() { - var args = [this._wrapped]; - push.apply(args, arguments); - return result.call(this, func.apply(_, args)); - }; - }); - }; - - // Generate a unique integer id (unique within the entire client session). - // Useful for temporary DOM ids. - var idCounter = 0; - _.uniqueId = function(prefix) { - var id = idCounter++; - return prefix ? prefix + id : id; - }; - - // By default, Underscore uses ERB-style template delimiters, change the - // following template settings to use alternative delimiters. - _.templateSettings = { - evaluate : /<%([\s\S]+?)%>/g, - interpolate : /<%=([\s\S]+?)%>/g, - escape : /<%-([\s\S]+?)%>/g - }; - - // When customizing `templateSettings`, if you don't want to define an - // interpolation, evaluation or escaping regex, we need one that is - // guaranteed not to match. - var noMatch = /(.)^/; - - // Certain characters need to be escaped so that they can be put into a - // string literal. - var escapes = { - "'": "'", - '\\': '\\', - '\r': 'r', - '\n': 'n', - '\t': 't', - '\u2028': 'u2028', - '\u2029': 'u2029' - }; - - var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; - - // JavaScript micro-templating, similar to John Resig's implementation. - // Underscore templating handles arbitrary delimiters, preserves whitespace, - // and correctly escapes quotes within interpolated code. - _.template = function(text, data, settings) { - settings = _.defaults({}, settings, _.templateSettings); - - // Combine delimiters into one regular expression via alternation. - var matcher = new RegExp([ - (settings.escape || noMatch).source, - (settings.interpolate || noMatch).source, - (settings.evaluate || noMatch).source - ].join('|') + '|$', 'g'); - - // Compile the template source, escaping string literals appropriately. - var index = 0; - var source = "__p+='"; - text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { - source += text.slice(index, offset) - .replace(escaper, function(match) { return '\\' + escapes[match]; }); - source += - escape ? "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'" : - interpolate ? "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'" : - evaluate ? "';\n" + evaluate + "\n__p+='" : ''; - index = offset + match.length; - }); - source += "';\n"; - - // If a variable is not specified, place data values in local scope. - if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; - - source = "var __t,__p='',__j=Array.prototype.join," + - "print=function(){__p+=__j.call(arguments,'');};\n" + - source + "return __p;\n"; - - try { - var render = new Function(settings.variable || 'obj', '_', source); - } catch (e) { - e.source = source; - throw e; - } - - if (data) return render(data, _); - var template = function(data) { - return render.call(this, data, _); - }; - - // Provide the compiled function source as a convenience for precompilation. - template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; - - return template; - }; - - // Add a "chain" function, which will delegate to the wrapper. - _.chain = function(obj) { - return _(obj).chain(); - }; - - // OOP - // --------------- - // If Underscore is called as a function, it returns a wrapped object that - // can be used OO-style. This wrapper holds altered versions of all the - // underscore functions. Wrapped objects may be chained. - - // Helper function to continue chaining intermediate results. - var result = function(obj) { - return this._chain ? _(obj).chain() : obj; - }; - - // Add all of the Underscore functions to the wrapper object. - _.mixin(_); - - // Add all mutator Array functions to the wrapper. - each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { - var method = ArrayProto[name]; - _.prototype[name] = function() { - var obj = this._wrapped; - method.apply(obj, arguments); - if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; - return result.call(this, obj); - }; - }); - - // Add all accessor Array functions to the wrapper. - each(['concat', 'join', 'slice'], function(name) { - var method = ArrayProto[name]; - _.prototype[name] = function() { - return result.call(this, method.apply(this._wrapped, arguments)); - }; - }); - - _.extend(_.prototype, { - - // Start chaining a wrapped Underscore object. - chain: function() { - this._chain = true; - return this; - }, - - // Extracts the result from a wrapped and chained object. - value: function() { - return this._wrapped; - } - - }); - -}).call(this); diff --git a/src/tests/frontend/runner.js b/src/tests/frontend/runner.js index 3e512d4d1..b2c76d00a 100644 --- a/src/tests/frontend/runner.js +++ b/src/tests/frontend/runner.js @@ -187,7 +187,6 @@ $(() => (async () => { // mutates the module definition function to temporarily replace Mocha's functions with // placeholders. The placeholders make it possible to defer the actual Mocha function calls until // after the modules are all loaded in parallel. require.setGlobalKeyPath() is used to coax - // require-kernel into using the wrapper define() method instead of require.define(). // Per-module log of attempted Mocha function calls. Key is module path, value is an array of // [functionName, argsArray] arrays. diff --git a/src/tests/frontend/specs/adminsettings.js b/src/tests/frontend/specs/adminsettings.js deleted file mode 100644 index a2ee02046..000000000 --- a/src/tests/frontend/specs/adminsettings.js +++ /dev/null @@ -1,90 +0,0 @@ -'use strict'; - -describe('Admin > Settings', function () { - this.timeout(480000); - - before(async function () { - let success = false; - $.ajax({ - url: `${location.protocol}//admin:changeme@${location.hostname}:${location.port}/admin/`, - type: 'GET', - success: () => success = true, - }); - await helper.waitForPromise(() => success === true); - }); - - beforeEach(async function () { - helper.newAdmin('settings'); - // needed, because the load event is fired to early - await helper.waitForPromise( - () => helper.admin$ && helper.admin$('.settings').val().length > 0, 5000); - }); - - it('Are Settings visible, populated, does save work', async function () { - const save = async () => { - const p = new Promise((resolve) => { - const observer = new MutationObserver(() => { resolve(); observer.disconnect(); }); - observer.observe( - helper.admin$('#response')[0], {attributes: true, childList: false, subtree: false}); - }); - helper.admin$('#saveSettings').click(); - await p; - }; - - // save old value - const settings = helper.admin$('.settings').val(); - const settingsLength = settings.length; - - // set new value - helper.admin$('.settings').val((_, text) => `/* test */\n${text}`); - await helper.waitForPromise( - () => settingsLength + 11 === helper.admin$('.settings').val().length, 5000); - await save(); - - // new value for settings.json should now be saved - // reset it to the old value - helper.newAdmin('settings'); - await helper.waitForPromise( - () => helper.admin$ && - helper.admin$('.settings').val().length === settingsLength + 11, 20000); - - // replace the test value with a line break - helper.admin$('.settings').val((_, text) => text.replace('/* test */\n', '')); - await helper.waitForPromise(() => settingsLength === helper.admin$('.settings').val().length); - await save(); - - // settings should have the old value - helper.newAdmin('settings'); - await helper.waitForPromise( - () => helper.admin$ && helper.admin$('.settings').val().length === settingsLength && - settings === helper.admin$('.settings').val(), 20000); - }); - - it('restart works', async function () { - const getStartTime = async () => { - try { - const {httpStartTime} = await $.ajax({ - url: new URL('/stats', window.location.href), - method: 'GET', - dataType: 'json', - timeout: 450, // Slightly less than the waitForPromise() interval. - }); - return httpStartTime; - } catch (err) { - document.getElementById('console').append( - `an error occurred: ${err.message} of type ${err.name}\n`); - return null; - } - }; - let oldStartTime; - await helper.waitForPromise(async () => { - oldStartTime = await getStartTime(); - return oldStartTime != null && oldStartTime > 0; - }, 1000, 500); - helper.admin$('#restartEtherpad').click(); - await helper.waitForPromise(async () => { - const startTime = await getStartTime(); - return startTime != null && startTime > oldStartTime; - }, 60000, 500); - }); -}); diff --git a/src/tests/frontend/specs/admintroubleshooting.js b/src/tests/frontend/specs/admintroubleshooting.js deleted file mode 100755 index 6e428d3b1..000000000 --- a/src/tests/frontend/specs/admintroubleshooting.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; - -describe('Admin Troupbleshooting page', function () { - before(async function () { - let success = false; - $.ajax({ - url: `${location.protocol}//admin:changeme@${location.hostname}:${location.port}/admin`, - type: 'GET', - success: () => success = true, - }); - await helper.waitForPromise(() => success === true); - }); - - // create a new pad before each test run - beforeEach(async function () { - helper.newAdmin('plugins/info'); - await helper.waitForPromise( - () => helper.admin$ && helper.admin$('.menu').find('li').length >= 3); - }); - - it('Shows Troubleshooting page Manager', async function () { - helper.admin$('a[data-l10n-id="admin_plugins_info"]')[0].click(); - }); - - it('Shows a version number', async function () { - const content = helper.admin$('span[data-l10n-id="admin_plugins_info.version_number"]') - .parent().text(); - const version = content.split(': ')[1].split('.'); - if (version.length !== 3) { - throw new Error('Not displaying a semver version number'); - } - }); - - it('Lists installed parts', async function () { - const parts = helper.admin$('pre')[1]; - if (parts.textContent.indexOf('ep_etherpad-lite/adminsettings') === -1) { - throw new Error('No admin setting part being displayed...'); - } - }); - - it('Lists installed hooks', async function () { - const parts = helper.admin$('dt'); - if (parts.length <= 20) { - throw new Error('Not enough hooks being displayed...'); - } - }); -}); diff --git a/src/tests/frontend/specs/adminupdateplugins.js b/src/tests/frontend/specs/adminupdateplugins.js deleted file mode 100755 index 7ab472858..000000000 --- a/src/tests/frontend/specs/adminupdateplugins.js +++ /dev/null @@ -1,115 +0,0 @@ -'use strict'; - -describe('Plugins page', function () { - function timeout(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); - } - - before(async function () { - let success = false; - $.ajax({ - url: `${location.protocol}//admin:changeme@${location.hostname}:${location.port}/admin`, - type: 'GET', - success: () => success = true, - }); - await helper.waitForPromise(() => success === true); - }); - - // create a new pad before each test run - beforeEach(async function () { - helper.newAdmin('plugins'); - await helper.waitForPromise( - () => helper.admin$ && helper.admin$('.menu').find('li').length >= 3, 30000); - }); - - it('Lists some plugins', async function () { - await helper.waitForPromise(() => helper.admin$('.results').children().length > 50, 20000); - }); - - it('Searches for plugin', async function () { - helper.admin$('#search-query').val('ep_font_color'); - await helper.waitForPromise(() => helper.admin$('.results').children().length > 0, 10000); - await helper.waitForPromise(() => helper.admin$('.results').children().length < 300, 10000); - }); - - it('Attempt to Update a plugin', async function () { - this.timeout(280000); - - await helper.waitForPromise(() => helper.admin$('.results').children().length > 50, 20000); - - if (helper.admin$('.ep_align').length === 0) this.skip(); - - await helper.waitForPromise( - () => helper.admin$('.ep_align .version').text().split('.').length >= 2); - - const minorVersionBefore = - parseInt(helper.admin$('.ep_align .version').text().split('.')[1]); - - if (!minorVersionBefore) { - throw new Error('Unable to get minor number of plugin, is the plugin installed?'); - } - - if (minorVersionBefore !== 2) this.skip(); - - helper.waitForPromise( - () => helper.admin$('.ep_align .do-update').length === 1); - - await timeout(500); // HACK! Please submit better fix.. - const $doUpdateButton = helper.admin$('.ep_align .do-update'); - $doUpdateButton.click(); - - // ensure its showing as Updating - await helper.waitForPromise( - () => helper.admin$('.ep_align .message').text() === 'Updating'); - - // Ensure it's a higher minor version IE 0.3.x as 0.2.x was installed - // Coverage for https://github.com/ether/etherpad-lite/issues/4536 - await helper.waitForPromise(() => parseInt(helper.admin$( - '.ep_align .version' - ) - .text() - .split('.')[1]) > minorVersionBefore, 60000, 1000); - // allow 50 seconds, check every 1 second. - }); - it('Attempt to Install a plugin', async function () { - this.timeout(280000); - - helper.admin$('#search-query').val('ep_headings2'); - await helper.waitForPromise(() => helper.admin$('.results').children().length < 300, 6000); - await helper.waitForPromise(() => helper.admin$('.results').children().length > 0, 6000); - - // skip if we already have ep_headings2 installed.. - if (helper.admin$('.ep_headings2 .do-install').is(':visible') === false) this.skip(); - - helper.admin$('.ep_headings2 .do-install').click(); - // ensure install has attempted to be started - await helper.waitForPromise( - () => helper.admin$('.ep_headings2 .do-install').length !== 0, 120000); - // ensure its not showing installing any more - await helper.waitForPromise( - () => helper.admin$('.ep_headings2 .message').text() === '', 180000); - // ensure uninstall button is visible - await helper.waitForPromise( - () => helper.admin$('.ep_headings2 .do-uninstall').length !== 0, 120000); - }); - - it('Attempt to Uninstall a plugin', async function () { - this.timeout(360000); - await helper.waitForPromise( - () => helper.admin$('.ep_headings2 .do-uninstall').length !== 0, 120000); - - helper.admin$('.ep_headings2 .do-uninstall').click(); - - // ensure its showing uninstalling - await helper.waitForPromise( - () => helper.admin$('.ep_headings2 .message') - .text() === 'Uninstalling', 120000); - // ensure its gone - await helper.waitForPromise( - () => helper.admin$('.ep_headings2').length === 0, 240000); - - helper.admin$('#search-query').val('ep_font'); - await helper.waitForPromise(() => helper.admin$('.results').children().length < 300, 240000); - await helper.waitForPromise(() => helper.admin$('.results').children().length > 0, 1000); - }); -}); diff --git a/src/tests/frontend/specs/alphabet.js b/src/tests/frontend/specs/alphabet.js deleted file mode 100644 index 999cfdf3a..000000000 --- a/src/tests/frontend/specs/alphabet.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -describe('All the alphabet works n stuff', function () { - const expectedString = 'abcdefghijklmnopqrstuvwxyz'; - - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('when you enter any char it appears right', function (done) { - const inner$ = helper.padInner$; - - // get the first text element out of the inner iframe - const firstTextElement = inner$('div').first(); - - // simulate key presses to delete content - firstTextElement.sendkeys('{selectall}'); // select all - firstTextElement.sendkeys('{del}'); // clear the first line - firstTextElement.sendkeys(expectedString); // insert the string - - helper.waitFor(() => inner$('div').first().text() === expectedString, 2000).done(done); - }); -}); diff --git a/src/tests/frontend/specs/authorship_of_editions.js b/src/tests/frontend/specs/authorship_of_editions.js index 9fd90ffb7..066c8d86b 100644 --- a/src/tests/frontend/specs/authorship_of_editions.js +++ b/src/tests/frontend/specs/authorship_of_editions.js @@ -25,7 +25,7 @@ describe('author of pad edition', function () { $lineWithUnorderedList.sendkeys('{selectall}'); const $insertUnorderedListButton = helper.padChrome$('.buttonicon-insertunorderedlist'); - $insertUnorderedListButton.click(); + $insertUnorderedListButton.trigger('click'); await helper.waitForPromise(() => ( getLine(LINE_WITH_UNORDERED_LIST).find('ul li').length === 1 && @@ -36,7 +36,7 @@ describe('author of pad edition', function () { $lineWithOrderedList.sendkeys('{selectall}'); const $insertOrderedListButton = helper.padChrome$('.buttonicon-insertorderedlist'); - $insertOrderedListButton.click(); + $insertOrderedListButton.trigger('click'); await helper.waitForPromise(() => ( getLine(LINE_WITH_ORDERED_LIST).find('ol li').length === 1 && diff --git a/src/tests/frontend/specs/bold.js b/src/tests/frontend/specs/bold.js deleted file mode 100644 index 51655588d..000000000 --- a/src/tests/frontend/specs/bold.js +++ /dev/null @@ -1,64 +0,0 @@ -'use strict'; - -describe('bold button', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('makes text bold on click', function (done) { - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - // get the first text element out of the inner iframe - const $firstTextElement = inner$('div').first(); - - // select this text element - $firstTextElement.sendkeys('{selectall}'); - - // get the bold button and click it - const $boldButton = chrome$('.buttonicon-bold'); - $boldButton.click(); - - const $newFirstTextElement = inner$('div').first(); - - // is there a element now? - const isBold = $newFirstTextElement.find('b').length === 1; - - // expect it to be bold - expect(isBold).to.be(true); - - // make sure the text hasn't changed - expect($newFirstTextElement.text()).to.eql($firstTextElement.text()); - - done(); - }); - - it('makes text bold on keypress', function (done) { - const inner$ = helper.padInner$; - - // get the first text element out of the inner iframe - const $firstTextElement = inner$('div').first(); - - // select this text element - $firstTextElement.sendkeys('{selectall}'); - - const e = new inner$.Event(helper.evtType); - e.ctrlKey = true; // Control key - e.which = 66; // b - inner$('#innerdocbody').trigger(e); - - const $newFirstTextElement = inner$('div').first(); - - // is there a element now? - const isBold = $newFirstTextElement.find('b').length === 1; - - // expect it to be bold - expect(isBold).to.be(true); - - // make sure the text hasn't changed - expect($newFirstTextElement.text()).to.eql($firstTextElement.text()); - - done(); - }); -}); diff --git a/src/tests/frontend/specs/change_user_color.js b/src/tests/frontend/specs/change_user_color.js deleted file mode 100644 index e39dc52e6..000000000 --- a/src/tests/frontend/specs/change_user_color.js +++ /dev/null @@ -1,103 +0,0 @@ -'use strict'; - -describe('change user color', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('Color picker matches original color and remembers the user color' + - ' after a refresh', async function () { - this.timeout(10000); - let chrome$ = helper.padChrome$; - - // click on the settings button to make settings visible - let $userButton = chrome$('.buttonicon-showusers'); - $userButton.click(); - - let $userSwatch = chrome$('#myswatch'); - $userSwatch.click(); - - const fb = chrome$.farbtastic('#colorpicker'); - const $colorPickerSave = chrome$('#mycolorpickersave'); - let $colorPickerPreview = chrome$('#mycolorpickerpreview'); - - // Same color represented in two different ways - const testColorHash = '#abcdef'; - const testColorRGB = 'rgb(171, 205, 239)'; - - // Check that the color picker matches the automatically assigned random color on the swatch. - // NOTE: This has a tiny chance of creating a false positive for passing in the - // off-chance the randomly assigned color is the same as the test color. - expect($colorPickerPreview.css('background-color')).to.be($userSwatch.css('background-color')); - - // The swatch updates as the test color is picked. - fb.setColor(testColorHash); - expect($colorPickerPreview.css('background-color')).to.be(testColorRGB); - $colorPickerSave.click(); - expect($userSwatch.css('background-color')).to.be(testColorRGB); - - // give it a second to save the color on the server side - await new Promise((resolve) => setTimeout(resolve, 1000)); - - // get a new pad, but don't clear the cookies - await helper.aNewPad({clearCookies: false}); - - chrome$ = helper.padChrome$; - - // click on the settings button to make settings visible - $userButton = chrome$('.buttonicon-showusers'); - $userButton.click(); - - $userSwatch = chrome$('#myswatch'); - $userSwatch.click(); - - $colorPickerPreview = chrome$('#mycolorpickerpreview'); - - expect($colorPickerPreview.css('background-color')).to.be(testColorRGB); - expect($userSwatch.css('background-color')).to.be(testColorRGB); - }); - - it('Own user color is shown when you enter a chat', function (done) { - this.timeout(1000); - const chrome$ = helper.padChrome$; - - const $colorOption = helper.padChrome$('#options-colorscheck'); - if (!$colorOption.is(':checked')) { - $colorOption.click(); - } - - // click on the settings button to make settings visible - const $userButton = chrome$('.buttonicon-showusers'); - $userButton.click(); - - const $userSwatch = chrome$('#myswatch'); - $userSwatch.click(); - - const fb = chrome$.farbtastic('#colorpicker'); - const $colorPickerSave = chrome$('#mycolorpickersave'); - - // Same color represented in two different ways - const testColorHash = '#abcdef'; - const testColorRGB = 'rgb(171, 205, 239)'; - - fb.setColor(testColorHash); - $colorPickerSave.click(); - - // click on the chat button to make chat visible - const $chatButton = chrome$('#chaticon'); - $chatButton.click(); - const $chatInput = chrome$('#chatinput'); - $chatInput.sendkeys('O hi'); // simulate a keypress of typing user - $chatInput.sendkeys('{enter}'); - - // wait until the chat message shows up - helper.waitFor(() => chrome$('#chattext').children('p').length !== 0 - ).done(() => { - const $firstChatMessage = chrome$('#chattext').children('p'); - // expect the first chat message to be of the user's color - expect($firstChatMessage.css('background-color')).to.be(testColorRGB); - done(); - }); - }); -}); diff --git a/src/tests/frontend/specs/change_user_name.js b/src/tests/frontend/specs/change_user_name.js deleted file mode 100644 index b146a1281..000000000 --- a/src/tests/frontend/specs/change_user_name.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -describe('change username value', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('Remembers the user name after a refresh', async function () { - this.timeout(10000); - await helper.toggleUserList(); - await helper.setUserName('😃'); - // Give the server an opportunity to write the new name. - await new Promise((resolve) => setTimeout(resolve, 1000)); - // get a new pad, but don't clear the cookies - await helper.aNewPad({clearCookies: false}); - await helper.toggleUserList(); - await helper.waitForPromise(() => helper.usernameField().val() === '😃'); - }); - - it('Own user name is shown when you enter a chat', async function () { - this.timeout(10000); - await helper.toggleUserList(); - await helper.setUserName('😃'); - - await helper.showChat(); - await helper.sendChatMessage('O hi{enter}'); - - await helper.waitForPromise(() => { - // username:hours:minutes text - const chatText = helper.chatTextParagraphs().text(); - return chatText.indexOf('😃') === 0; - }); - }); -}); diff --git a/src/tests/frontend/specs/chat.js b/src/tests/frontend/specs/chat.js deleted file mode 100644 index 82527f372..000000000 --- a/src/tests/frontend/specs/chat.js +++ /dev/null @@ -1,116 +0,0 @@ -'use strict'; - -describe('Chat messages and UI', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('opens chat, sends a message, makes sure it exists ' + - 'on the page and hides chat', async function () { - this.timeout(3000); - const chatValue = 'JohnMcLear'; - - await helper.showChat(); - await helper.sendChatMessage(`${chatValue}{enter}`); - - expect(helper.chatTextParagraphs().length).to.be(1); - - //

    - // unnamed: - // 12:38 - // JohnMcLear - //

    - const username = helper.chatTextParagraphs().children('b').text(); - const time = helper.chatTextParagraphs().children('.time').text(); - - // TODO: The '\n' is an artifact of $.sendkeys('{enter}'). Figure out how to get rid of it - // without breaking the other tests that use $.sendkeys(). - expect(helper.chatTextParagraphs().text()).to.be(`${username}${time} ${chatValue}\n`); - - await helper.hideChat(); - }); - - it("makes sure that an empty message can't be sent", async function () { - const chatValue = 'mluto'; - - await helper.showChat(); - - // simulate a keypress of typing enter, mluto and enter (to send 'mluto') - await helper.sendChatMessage(`{enter}${chatValue}{enter}`); - - const chat = helper.chatTextParagraphs(); - - expect(chat.length).to.be(1); - - // check that the received message is not the empty one - const username = chat.children('b').text(); - const time = chat.children('.time').text(); - - // TODO: Each '\n' is an artifact of $.sendkeys('{enter}'). Figure out how to get rid of them - // without breaking the other tests that use $.sendkeys(). - expect(chat.text()).to.be(`${username}${time} \n${chatValue}\n`); - }); - - it('makes chat stick to right side of the screen via settings, ' + - 'remove sticky via settings, close it', async function () { - this.timeout(5000); - await helper.showSettings(); - - await helper.enableStickyChatviaSettings(); - expect(helper.isChatboxShown()).to.be(true); - expect(helper.isChatboxSticky()).to.be(true); - - await helper.disableStickyChatviaSettings(); - expect(helper.isChatboxSticky()).to.be(false); - expect(helper.isChatboxShown()).to.be(true); - - await helper.hideChat(); - expect(helper.isChatboxSticky()).to.be(false); - expect(helper.isChatboxShown()).to.be(false); - }); - - it('makes chat stick to right side of the screen via icon on the top' + - ' right, remove sticky via icon, close it', async function () { - this.timeout(5000); - await helper.showChat(); - - await helper.enableStickyChatviaIcon(); - expect(helper.isChatboxShown()).to.be(true); - expect(helper.isChatboxSticky()).to.be(true); - - await helper.disableStickyChatviaIcon(); - expect(helper.isChatboxShown()).to.be(true); - expect(helper.isChatboxSticky()).to.be(false); - - await helper.hideChat(); - expect(helper.isChatboxSticky()).to.be(false); - expect(helper.isChatboxShown()).to.be(false); - }); - - xit('Checks showChat=false URL Parameter hides chat then' + - ' when removed it shows chat', async function () { - // give it a second to save the username on the server side - await new Promise((resolve) => setTimeout(resolve, 3000)); - - // get a new pad, but don't clear the cookies - await helper.aNewPad({clearCookies: false, params: {showChat: 'false'}}); - - let chrome$ = helper.padChrome$; - let chaticon = chrome$('#chaticon'); - // chat should be hidden. - expect(chaticon.is(':visible')).to.be(false); - - // give it a second to save the username on the server side - await new Promise((resolve) => setTimeout(resolve, 1000)); - - // get a new pad, but don't clear the cookies - await helper.aNewPad({clearCookies: false}); - - chrome$ = helper.padChrome$; - chaticon = chrome$('#chaticon'); - // chat should be visible. - expect(chaticon.is(':visible')).to.be(true); - }); -}); diff --git a/src/tests/frontend/specs/chat_load_messages.js b/src/tests/frontend/specs/chat_load_messages.js index d720fbba1..86bd2b3c7 100644 --- a/src/tests/frontend/specs/chat_load_messages.js +++ b/src/tests/frontend/specs/chat_load_messages.js @@ -10,7 +10,7 @@ describe('chat-load-messages', function () { it('adds a lot of messages', async function () { const chrome$ = helper.padChrome$; const chatButton = chrome$('#chaticon'); - chatButton.click(); + chatButton.trigger('click'); const chatInput = chrome$('#chatinput'); const chatText = chrome$('#chattext'); @@ -33,7 +33,7 @@ describe('chat-load-messages', function () { const chrome$ = helper.padChrome$; helper.waitFor(() => { const chatButton = chrome$('#chaticon'); - chatButton.click(); + chatButton.trigger('click'); chatText = chrome$('#chattext'); return chatText.children('p').length === expectedCount; }).always(() => { @@ -47,11 +47,11 @@ describe('chat-load-messages', function () { const expectedCount = 122; const chrome$ = helper.padChrome$; const chatButton = chrome$('#chaticon'); - chatButton.click(); + chatButton.trigger('click'); const chatText = chrome$('#chattext'); const loadMsgBtn = chrome$('#chatloadmessagesbutton'); - loadMsgBtn.click(); + loadMsgBtn.trigger('click'); helper.waitFor(() => chatText.children('p').length === expectedCount).always(() => { expect(chatText.children('p').length).to.be(expectedCount); done(); @@ -63,11 +63,11 @@ describe('chat-load-messages', function () { const expectedDisplay = 'none'; const chrome$ = helper.padChrome$; const chatButton = chrome$('#chaticon'); - chatButton.click(); + chatButton.trigger('click'); const loadMsgBtn = chrome$('#chatloadmessagesbutton'); const loadMsgBall = chrome$('#chatloadmessagesball'); - loadMsgBtn.click(); + loadMsgBtn.trigger('click'); helper.waitFor(() => loadMsgBtn.css('display') === expectedDisplay && loadMsgBall.css('display') === expectedDisplay).always(() => { expect(loadMsgBtn.css('display')).to.be(expectedDisplay); diff --git a/src/tests/frontend/specs/clear_authorship_colors.js b/src/tests/frontend/specs/clear_authorship_colors.js deleted file mode 100644 index 20c358b49..000000000 --- a/src/tests/frontend/specs/clear_authorship_colors.js +++ /dev/null @@ -1,125 +0,0 @@ -'use strict'; - -describe('clear authorship colors button', function () { - let padId; - - // create a new pad before each test run - beforeEach(async function () { - padId = await helper.aNewPad(); - }); - - it('makes text clear authorship colors', async function () { - this.timeout(2500); - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - // override the confirm dialogue functioon - helper.padChrome$.window.confirm = () => true; - - // get the first text element out of the inner iframe - const $firstTextElement = inner$('div').first(); - - // Set some new text - const sentText = 'Hello'; - - // select this text element - $firstTextElement.sendkeys('{selectall}'); - $firstTextElement.sendkeys(sentText); - $firstTextElement.sendkeys('{rightarrow}'); - - // wait until we have the full value available - await helper.waitForPromise( - () => inner$('div span').first().attr('class').indexOf('author') !== -1); - - // IE hates you if you don't give focus to the inner frame bevore you do a clearAuthorship - inner$('div').first().focus(); - - // get the clear authorship colors button and click it - const $clearauthorshipcolorsButton = chrome$('.buttonicon-clearauthorship'); - $clearauthorshipcolorsButton.click(); - - // does the first div include an author class? - const hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1; - expect(hasAuthorClass).to.be(false); - - await helper.waitForPromise( - () => chrome$('div.disconnected').attr('class').indexOf('visible') === -1); - }); - - it("makes text clear authorship colors and checks it can't be undone", async function () { - this.timeout(1500); - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - // override the confirm dialogue functioon - helper.padChrome$.window.confirm = () => true; - - // get the first text element out of the inner iframe - const $firstTextElement = inner$('div').first(); - - // Set some new text - const sentText = 'Hello'; - - // select this text element - $firstTextElement.sendkeys('{selectall}'); - $firstTextElement.sendkeys(sentText); - $firstTextElement.sendkeys('{rightarrow}'); - - // wait until we have the full value available - await helper.waitForPromise( - () => inner$('div span').first().attr('class').indexOf('author') !== -1); - - // IE hates you if you don't give focus to the inner frame bevore you do a clearAuthorship - inner$('div').first().focus(); - - // get the clear authorship colors button and click it - const $clearauthorshipcolorsButton = chrome$('.buttonicon-clearauthorship'); - $clearauthorshipcolorsButton.click(); - - // does the first div include an author class? - let hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1; - expect(hasAuthorClass).to.be(false); - - const e = new inner$.Event(helper.evtType); - e.ctrlKey = true; // Control key - e.which = 90; // z - inner$('#innerdocbody').trigger(e); // shouldn't od anything - - // does the first div include an author class? - hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1; - expect(hasAuthorClass).to.be(false); - - // get undo and redo buttons - const $undoButton = chrome$('.buttonicon-undo'); - - // click the button - $undoButton.click(); // shouldn't do anything - hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1; - expect(hasAuthorClass).to.be(false); - - await helper.waitForPromise( - () => chrome$('div.disconnected').attr('class').indexOf('visible') === -1); - }); - - // Test for https://github.com/ether/etherpad-lite/issues/5128 - it('clears authorship when first line has line attributes', async function () { - // override the confirm dialogue function - helper.padChrome$.window.confirm = () => true; - - // Make sure there is text with author info. The first line must have a line attribute. - await helper.clearPad(); - await helper.edit('Hello'); - helper.padChrome$('.buttonicon-insertunorderedlist').click(); - await helper.waitForPromise(() => helper.padInner$('[class*="author-"]').length > 0); - - const nCommits = helper.commits.length; - helper.padChrome$('.buttonicon-clearauthorship').click(); - await helper.waitForPromise(() => helper.padInner$('[class*="author-"]').length === 0); - - // Make sure the change was actually accepted by reloading the pad and looking for authorship. - // Before the pad can be reloaded the server might need some time to accept the change. - await helper.waitForPromise(() => helper.commits.length > nCommits); - await helper.aNewPad({id: padId}); - expect(helper.padInner$('[class*="author-"]').length).to.be(0); - }); -}); diff --git a/src/tests/frontend/specs/collab_client.js b/src/tests/frontend/specs/collab_client.js deleted file mode 100644 index 307e37908..000000000 --- a/src/tests/frontend/specs/collab_client.js +++ /dev/null @@ -1,102 +0,0 @@ -'use strict'; - -describe('Messages in the COLLABROOM', function () { - const user1Text = 'text created by user 1'; - const user2Text = 'text created by user 2'; - - const triggerEvent = (eventName) => { - const event = new helper.padInner$.Event(eventName); - helper.padInner$('#innerdocbody').trigger(event); - }; - - const replaceLineText = async (lineNumber, newText) => { - const inner$ = helper.padInner$; - - // get the line element - const $line = inner$('div').eq(lineNumber); - - // simulate key presses to delete content - $line.sendkeys('{selectall}'); // select all - $line.sendkeys('{del}'); // clear the first line - $line.sendkeys(newText); // insert the string - - await helper.waitForPromise(() => inner$('div').eq(lineNumber).text() === newText); - }; - - before(async function () { - this.timeout(10000); - await helper.aNewPad(); - await helper.multipleUsers.init(); - }); - - it('bug #4978 regression test', async function () { - // The bug was triggered by receiving a change from another user while simultaneously composing - // a character and waiting for an acknowledgement of a previously sent change. - - // User 1 starts sending a change to the server. - let sendStarted; - const finishSend = (() => { - const socketJsonObj = helper.padChrome$.window.pad.socket.json; - const sendBackup = socketJsonObj.send; - let startSend; - sendStarted = new Promise((resolve) => { startSend = resolve; }); - let finishSend; - const sendP = new Promise((resolve) => { finishSend = resolve; }); - socketJsonObj.send = (...args) => { - startSend(); - sendP.then(() => { - socketJsonObj.send = sendBackup; - socketJsonObj.send(...args); - }); - }; - return finishSend; - })(); - await replaceLineText(0, user1Text); - await sendStarted; - - // User 1 starts a character composition. - triggerEvent('compositionstart'); - - // User 1 receives a change from user 2. (User 1 will not incorporate the change until the - // composition is completed.) - const user2ChangeArrivedAtUser1 = new Promise((resolve) => { - const cc = helper.padChrome$.window.pad.collabClient; - const origHM = cc.handleMessageFromServer; - cc.handleMessageFromServer = (evt) => { - if (evt.type === 'COLLABROOM' && evt.data.type === 'NEW_CHANGES') { - cc.handleMessageFromServer = origHM; - resolve(); - } - return origHM.call(cc, evt); - }; - }); - await helper.multipleUsers.performAsOtherUser(async () => await replaceLineText(1, user2Text)); - await user2ChangeArrivedAtUser1; - - // User 1 finishes sending the change to the server. User 2 should see the changes right away. - finishSend(); - await helper.multipleUsers.performAsOtherUser(async () => await helper.waitForPromise( - () => helper.padInner$('div').eq(0).text() === user1Text)); - - // User 1 finishes the character composition. User 2's change should then become visible. - triggerEvent('compositionend'); - await helper.waitForPromise(() => helper.padInner$('div').eq(1).text() === user2Text); - - // Users 1 and 2 make some more changes. - await helper.multipleUsers.performAsOtherUser(async () => await replaceLineText(3, user2Text)); - await replaceLineText(2, user1Text); - - // All changes should appear in both views. - const assertContent = async () => await helper.waitForPromise(() => { - const expectedLines = [ - user1Text, - user2Text, - user1Text, - user2Text, - ]; - return expectedLines.every((txt, i) => helper.padInner$('div').eq(i).text() === txt); - }); - await assertContent(); - await helper.multipleUsers.performAsOtherUser(assertContent); - }); -}); diff --git a/src/tests/frontend/specs/delete.js b/src/tests/frontend/specs/delete.js deleted file mode 100644 index 05164280b..000000000 --- a/src/tests/frontend/specs/delete.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -describe('delete keystroke', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('makes text delete', async function () { - const inner$ = helper.padInner$; - - // get the first text element out of the inner iframe - const $firstTextElement = inner$('div').first(); - - // get the original length of this element - const elementLength = $firstTextElement.text().length; - - // simulate key presses to delete content - $firstTextElement.sendkeys('{leftarrow}'); // simulate a keypress of the left arrow key - $firstTextElement.sendkeys('{del}'); // simulate a keypress of delete - - const $newFirstTextElement = inner$('div').first(); - - // get the new length of this element - const newElementLength = $newFirstTextElement.text().length; - - // expect it to be one char less in length - expect(newElementLength).to.be((elementLength - 1)); - }); -}); diff --git a/src/tests/frontend/specs/drag_and_drop.js b/src/tests/frontend/specs/drag_and_drop.js index 6d3f5a363..2aeb22f24 100644 --- a/src/tests/frontend/specs/drag_and_drop.js +++ b/src/tests/frontend/specs/drag_and_drop.js @@ -24,7 +24,7 @@ describe('drag and drop', function () { before(async function () { const originalHTML = helper.padInner$('body').html(); const $undoButton = helper.padChrome$('.buttonicon-undo'); - $undoButton.click(); + $undoButton.trigger('click'); await helper.waitForPromise(() => helper.padInner$('body').html() !== originalHTML); }); @@ -59,7 +59,7 @@ describe('drag and drop', function () { before(async function () { const originalHTML = helper.padInner$('body').html(); const $undoButton = helper.padChrome$('.buttonicon-undo'); - $undoButton.click(); + $undoButton.trigger('click'); await helper.waitForPromise(() => helper.padInner$('body').html() !== originalHTML); }); diff --git a/src/tests/frontend/specs/easysync-follow.js b/src/tests/frontend/specs/easysync-follow.js new file mode 100644 index 000000000..9ec5a7e83 --- /dev/null +++ b/src/tests/frontend/specs/easysync-follow.js @@ -0,0 +1,82 @@ +'use strict'; + +const Changeset = require('../../../static/js/Changeset'); +const AttributePool = require('../../../static/js/AttributePool'); +const {randomMultiline, randomTestChangeset} = require('../easysync-helper.js'); + +describe('easysync-follow', function () { + describe('follow & compose', function () { + const testFollow = (randomSeed) => { + it(`testFollow#${randomSeed}`, async function () { + const p = new AttributePool(); + + const startText = `${randomMultiline(10, 20)}\n`; + + const cs1 = randomTestChangeset(startText)[0]; + const cs2 = randomTestChangeset(startText)[0]; + + const afb = Changeset.checkRep(Changeset.follow(cs1, cs2, false, p)); + const bfa = Changeset.checkRep(Changeset.follow(cs2, cs1, true, p)); + + const merge1 = Changeset.checkRep(Changeset.compose(cs1, afb)); + const merge2 = Changeset.checkRep(Changeset.compose(cs2, bfa)); + + expect(merge2).to.equal(merge1); + }); + }; + + for (let i = 0; i < 30; i++) testFollow(i); + }); + + describe('followAttributes & composeAttributes', function () { + const p = new AttributePool(); + p.putAttrib(['x', '']); + p.putAttrib(['x', 'abc']); + p.putAttrib(['x', 'def']); + p.putAttrib(['y', '']); + p.putAttrib(['y', 'abc']); + p.putAttrib(['y', 'def']); + let n = 0; + + const testFollow = (a, b, afb, bfa, merge) => { + it(`manual #${++n}`, async function () { + expect(Changeset.exportedForTestingOnly.followAttributes(a, b, p)).to.equal(afb); + expect(Changeset.exportedForTestingOnly.followAttributes(b, a, p)).to.equal(bfa); + expect(Changeset.composeAttributes(a, afb, true, p)).to.equal(merge); + expect(Changeset.composeAttributes(b, bfa, true, p)).to.equal(merge); + }); + }; + + testFollow('', '', '', '', ''); + testFollow('*0', '', '', '*0', '*0'); + testFollow('*0', '*0', '', '', '*0'); + testFollow('*0', '*1', '', '*0', '*0'); + testFollow('*1', '*2', '', '*1', '*1'); + testFollow('*0*1', '', '', '*0*1', '*0*1'); + testFollow('*0*4', '*2*3', '*3', '*0', '*0*3'); + testFollow('*0*4', '*2', '', '*0*4', '*0*4'); + }); + + describe('chracterRangeFollow', function () { + const testCharacterRangeFollow = (testId, cs, oldRange, insertionsAfter, correctNewRange) => { + it(`testCharacterRangeFollow#${testId}`, async function () { + cs = Changeset.checkRep(cs); + expect(Changeset.characterRangeFollow(cs, oldRange[0], oldRange[1], insertionsAfter)) + .to.eql(correctNewRange); + }); + }; + + testCharacterRangeFollow(1, 'Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk', + [7, 10], false, [14, 15]); + testCharacterRangeFollow(2, 'Z:bc<6|x=b4|2-6$', [400, 407], false, [400, 401]); + testCharacterRangeFollow(3, 'Z:4>0-3+3$abc', [0, 3], false, [3, 3]); + testCharacterRangeFollow(4, 'Z:4>0-3+3$abc', [0, 3], true, [0, 0]); + testCharacterRangeFollow(5, 'Z:5>1+1=1-3+3$abcd', [1, 4], false, [5, 5]); + testCharacterRangeFollow(6, 'Z:5>1+1=1-3+3$abcd', [1, 4], true, [2, 2]); + testCharacterRangeFollow(7, 'Z:5>1+1=1-3+3$abcd', [0, 6], false, [1, 7]); + testCharacterRangeFollow(8, 'Z:5>1+1=1-3+3$abcd', [0, 3], false, [1, 2]); + testCharacterRangeFollow(9, 'Z:5>1+1=1-3+3$abcd', [2, 5], false, [5, 6]); + testCharacterRangeFollow(10, 'Z:2>1+1$a', [0, 0], false, [1, 1]); + testCharacterRangeFollow(11, 'Z:2>1+1$a', [0, 0], true, [0, 0]); + }); +}); diff --git a/src/tests/frontend/specs/easysync.js b/src/tests/frontend/specs/easysync.js deleted file mode 100644 index 121218407..000000000 --- a/src/tests/frontend/specs/easysync.js +++ /dev/null @@ -1,924 +0,0 @@ -'use strict'; -/** - * I found this tests in the old Etherpad and used it to test if the Changeset library can be run on - * node.js. It has no use for ep-lite, but I thought I keep it cause it may help someone to - * understand the Changeset library - * https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2_tests.js - */ - -/* - * Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - -const Changeset = require('../../../static/js/Changeset'); -const AttributePool = require('../../../static/js/AttributePool'); - -const randInt = (maxValue) => Math.floor(Math.random() * maxValue); - -describe('easysync', function () { - it('throughIterator', async function () { - const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1'; - const iter = Changeset.opIterator(x); - const assem = Changeset.opAssembler(); - while (iter.hasNext()) assem.append(iter.next()); - expect(assem.toString()).to.equal(x); - }); - - it('throughSmartAssembler', async function () { - const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1'; - const iter = Changeset.opIterator(x); - const assem = Changeset.smartOpAssembler(); - while (iter.hasNext()) assem.append(iter.next()); - assem.endDocument(); - expect(assem.toString()).to.equal(x); - }); - - const applyMutations = (mu, arrayOfArrays) => { - arrayOfArrays.forEach((a) => { - const result = mu[a[0]](...a.slice(1)); - if (a[0] === 'remove' && a[3]) { - expect(result).to.equal(a[3]); - } - }); - }; - - const mutationsToChangeset = (oldLen, arrayOfArrays) => { - const assem = Changeset.smartOpAssembler(); - const op = Changeset.newOp(); - const bank = Changeset.stringAssembler(); - let oldPos = 0; - let newLen = 0; - arrayOfArrays.forEach((a) => { - if (a[0] === 'skip') { - op.opcode = '='; - op.chars = a[1]; - op.lines = (a[2] || 0); - assem.append(op); - oldPos += op.chars; - newLen += op.chars; - } else if (a[0] === 'remove') { - op.opcode = '-'; - op.chars = a[1]; - op.lines = (a[2] || 0); - assem.append(op); - oldPos += op.chars; - } else if (a[0] === 'insert') { - op.opcode = '+'; - bank.append(a[1]); - op.chars = a[1].length; - op.lines = (a[2] || 0); - assem.append(op); - newLen += op.chars; - } - }); - newLen += oldLen - oldPos; - assem.endDocument(); - return Changeset.pack(oldLen, newLen, assem.toString(), bank.toString()); - }; - - const runMutationTest = (testId, origLines, muts, correct) => { - it(`runMutationTest#${testId}`, async function () { - let lines = origLines.slice(); - const mu = Changeset.exportedForTestingOnly.textLinesMutator(lines); - applyMutations(mu, muts); - mu.close(); - expect(lines).to.eql(correct); - - const inText = origLines.join(''); - const cs = mutationsToChangeset(inText.length, muts); - lines = origLines.slice(); - Changeset.mutateTextLines(cs, lines); - expect(lines).to.eql(correct); - - const correctText = correct.join(''); - const outText = Changeset.applyToText(cs, inText); - expect(outText).to.equal(correctText); - }); - }; - - runMutationTest(1, ['apple\n', 'banana\n', 'cabbage\n', 'duffle\n', 'eggplant\n'], [ - ['remove', 1, 0, 'a'], - ['insert', 'tu'], - ['remove', 1, 0, 'p'], - ['skip', 4, 1], - ['skip', 7, 1], - ['insert', 'cream\npie\n', 2], - ['skip', 2], - ['insert', 'bot'], - ['insert', '\n', 1], - ['insert', 'bu'], - ['skip', 3], - ['remove', 3, 1, 'ge\n'], - ['remove', 6, 0, 'duffle'], - ], ['tuple\n', 'banana\n', 'cream\n', 'pie\n', 'cabot\n', 'bubba\n', 'eggplant\n']); - - runMutationTest(2, ['apple\n', 'banana\n', 'cabbage\n', 'duffle\n', 'eggplant\n'], [ - ['remove', 1, 0, 'a'], - ['remove', 1, 0, 'p'], - ['insert', 'tu'], - ['skip', 11, 2], - ['insert', 'cream\npie\n', 2], - ['skip', 2], - ['insert', 'bot'], - ['insert', '\n', 1], - ['insert', 'bu'], - ['skip', 3], - ['remove', 3, 1, 'ge\n'], - ['remove', 6, 0, 'duffle'], - ], ['tuple\n', 'banana\n', 'cream\n', 'pie\n', 'cabot\n', 'bubba\n', 'eggplant\n']); - - runMutationTest(3, ['apple\n', 'banana\n', 'cabbage\n', 'duffle\n', 'eggplant\n'], [ - ['remove', 6, 1, 'apple\n'], - ['skip', 15, 2], - ['skip', 6], - ['remove', 1, 1, '\n'], - ['remove', 8, 0, 'eggplant'], - ['skip', 1, 1], - ], ['banana\n', 'cabbage\n', 'duffle\n']); - - runMutationTest(4, ['15\n'], [ - ['skip', 1], - ['insert', '\n2\n3\n4\n', 4], - ['skip', 2, 1], - ], ['1\n', '2\n', '3\n', '4\n', '5\n']); - - runMutationTest(5, ['1\n', '2\n', '3\n', '4\n', '5\n'], [ - ['skip', 1], - ['remove', 7, 4, '\n2\n3\n4\n'], - ['skip', 2, 1], - ], ['15\n']); - - runMutationTest(6, ['123\n', 'abc\n', 'def\n', 'ghi\n', 'xyz\n'], [ - ['insert', '0'], - ['skip', 4, 1], - ['skip', 4, 1], - ['remove', 8, 2, 'def\nghi\n'], - ['skip', 4, 1], - ], ['0123\n', 'abc\n', 'xyz\n']); - - runMutationTest(7, ['apple\n', 'banana\n', 'cabbage\n', 'duffle\n', 'eggplant\n'], [ - ['remove', 6, 1, 'apple\n'], - ['skip', 15, 2, true], - ['skip', 6, 0, true], - ['remove', 1, 1, '\n'], - ['remove', 8, 0, 'eggplant'], - ['skip', 1, 1, true], - ], ['banana\n', 'cabbage\n', 'duffle\n']); - - const poolOrArray = (attribs) => { - if (attribs.getAttrib) { - return attribs; // it's already an attrib pool - } else { - // assume it's an array of attrib strings to be split and added - const p = new AttributePool(); - attribs.forEach((kv) => { - p.putAttrib(kv.split(',')); - }); - return p; - } - }; - - const runApplyToAttributionTest = (testId, attribs, cs, inAttr, outCorrect) => { - it(`applyToAttribution#${testId}`, async function () { - const p = poolOrArray(attribs); - const result = Changeset.applyToAttribution(Changeset.checkRep(cs), inAttr, p); - expect(result).to.equal(outCorrect); - }); - }; - - // turn cactus\n into actusabcd\n - runApplyToAttributionTest(1, - ['bold,', 'bold,true'], 'Z:7>3-1*0=1*1=1=3+4$abcd', '+1*1+1|1+5', '+1*1+1|1+8'); - - // turn "david\ngreenspan\n" into "david\ngreen\n" - runApplyToAttributionTest(2, - ['bold,', 'bold,true'], 'Z:g<4*1|1=6*1=5-4$', '|2+g', '*1|1+6*1+5|1+1'); - - it('mutatorHasMore', async function () { - const lines = ['1\n', '2\n', '3\n', '4\n']; - let mu; - - mu = Changeset.exportedForTestingOnly.textLinesMutator(lines); - expect(mu.hasMore()).to.be(true); - mu.skip(8, 4); - expect(mu.hasMore()).to.be(false); - mu.close(); - expect(mu.hasMore()).to.be(false); - - // still 1,2,3,4 - mu = Changeset.exportedForTestingOnly.textLinesMutator(lines); - expect(mu.hasMore()).to.be(true); - mu.remove(2, 1); - expect(mu.hasMore()).to.be(true); - mu.skip(2, 1); - expect(mu.hasMore()).to.be(true); - mu.skip(2, 1); - expect(mu.hasMore()).to.be(true); - mu.skip(2, 1); - expect(mu.hasMore()).to.be(false); - mu.insert('5\n', 1); - expect(mu.hasMore()).to.be(false); - mu.close(); - expect(mu.hasMore()).to.be(false); - - // 2,3,4,5 now - mu = Changeset.exportedForTestingOnly.textLinesMutator(lines); - expect(mu.hasMore()).to.be(true); - mu.remove(6, 3); - expect(mu.hasMore()).to.be(true); - mu.remove(2, 1); - expect(mu.hasMore()).to.be(false); - mu.insert('hello\n', 1); - expect(mu.hasMore()).to.be(false); - mu.close(); - expect(mu.hasMore()).to.be(false); - }); - - const runMutateAttributionTest = (testId, attribs, cs, alines, outCorrect) => { - it(`runMutateAttributionTest#${testId}`, async function () { - const p = poolOrArray(attribs); - const alines2 = Array.prototype.slice.call(alines); - Changeset.mutateAttributionLines(Changeset.checkRep(cs), alines2, p); - expect(alines2).to.eql(outCorrect); - - const removeQuestionMarks = (a) => a.replace(/\?/g, ''); - const inMerged = Changeset.joinAttributionLines(alines.map(removeQuestionMarks)); - const correctMerged = Changeset.joinAttributionLines(outCorrect.map(removeQuestionMarks)); - const mergedResult = Changeset.applyToAttribution(cs, inMerged, p); - expect(mergedResult).to.equal(correctMerged); - }); - }; - - // turn 123\n 456\n 789\n into 123\n 456\n 789\n - runMutateAttributionTest(1, - ['bold,true'], 'Z:c>0|1=4=1*0=1$', ['|1+4', '|1+4', '|1+4'], ['|1+4', '+1*0+1|1+2', '|1+4']); - - // make a document bold - runMutateAttributionTest(2, - ['bold,true'], 'Z:c>0*0|3=c$', ['|1+4', '|1+4', '|1+4'], ['*0|1+4', '*0|1+4', '*0|1+4']); - - // clear bold on document - runMutateAttributionTest(3, - ['bold,', 'bold,true'], 'Z:c>0*0|3=c$', - ['*1+1+1*1+1|1+1', '+1*1+1|1+2', '*1+1+1*1+1|1+1'], ['|1+4', '|1+4', '|1+4']); - - // add a character on line 3 of a document with 5 blank lines, and make sure - // the optimization that skips purely-kept lines is working; if any attribution string - // with a '?' is parsed it will cause an error. - runMutateAttributionTest(4, - ['foo,bar', 'line,1', 'line,2', 'line,3', 'line,4', 'line,5'], - 'Z:5>1|2=2+1$x', ['?*1|1+1', '?*2|1+1', '*3|1+1', '?*4|1+1', '?*5|1+1'], - ['?*1|1+1', '?*2|1+1', '+1*3|1+1', '?*4|1+1', '?*5|1+1']); - - const testPoolWithChars = (() => { - const p = new AttributePool(); - p.putAttrib(['char', 'newline']); - for (let i = 1; i < 36; i++) { - p.putAttrib(['char', Changeset.numToString(i)]); - } - p.putAttrib(['char', '']); - return p; - })(); - - // based on runMutationTest#1 - runMutateAttributionTest(5, testPoolWithChars, - 'Z:11>7-2*t+1*u+1|2=b|2+a=2*b+1*o+1*t+1*0|1+1*b+1*u+1=3|1-3-6$tucream\npie\nbot\nbu', - [ - '*a+1*p+2*l+1*e+1*0|1+1', - '*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1', - '*c+1*a+1*b+2*a+1*g+1*e+1*0|1+1', - '*d+1*u+1*f+2*l+1*e+1*0|1+1', - '*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1', - ], - [ - '*t+1*u+1*p+1*l+1*e+1*0|1+1', - '*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1', - '|1+6', - '|1+4', - '*c+1*a+1*b+1*o+1*t+1*0|1+1', - '*b+1*u+1*b+2*a+1*0|1+1', - '*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1', - ]); - - // based on runMutationTest#3 - runMutateAttributionTest(6, testPoolWithChars, - 'Z:117=1|4+7$\n2\n3\n4\n', - ['*1+1*5|1+2'], ['*1+1|1+1', '|1+2', '|1+2', '|1+2', '*5|1+2']); - - // based on runMutationTest#5 - runMutateAttributionTest(8, testPoolWithChars, 'Z:a<7=1|4-7$', - ['*1|1+2', '*2|1+2', '*3|1+2', '*4|1+2', '*5|1+2'], ['*1+1*5|1+2']); - - // based on runMutationTest#6 - runMutateAttributionTest(9, testPoolWithChars, 'Z:k<7*0+1*10|2=8|2-8$0', - [ - '*1+1*2+1*3+1|1+1', - '*a+1*b+1*c+1|1+1', - '*d+1*e+1*f+1|1+1', - '*g+1*h+1*i+1|1+1', - '?*x+1*y+1*z+1|1+1', - ], - ['*0+1|1+4', '|1+4', '?*x+1*y+1*z+1|1+1']); - - runMutateAttributionTest(10, testPoolWithChars, 'Z:6>4=1+1=1+1|1=1+1=1*0+1$abcd', - ['|1+3', '|1+3'], ['|1+5', '+2*0+1|1+2']); - - - runMutateAttributionTest(11, testPoolWithChars, 'Z:s>1|1=4=6|1+1$\n', - ['*0|1+4', '*0|1+8', '*0+5|1+1', '*0|1+1', '*0|1+5', '*0|1+1', '*0|1+1', '*0|1+1', '|1+1'], - [ - '*0|1+4', - '*0+6|1+1', - '*0|1+2', - '*0+5|1+1', - '*0|1+1', - '*0|1+5', - '*0|1+1', - '*0|1+1', - '*0|1+1', - '|1+1', - ]); - - const randomInlineString = (len) => { - const assem = Changeset.stringAssembler(); - for (let i = 0; i < len; i++) { - assem.append(String.fromCharCode(randInt(26) + 97)); - } - return assem.toString(); - }; - - const randomMultiline = (approxMaxLines, approxMaxCols) => { - const numParts = randInt(approxMaxLines * 2) + 1; - const txt = Changeset.stringAssembler(); - txt.append(randInt(2) ? '\n' : ''); - for (let i = 0; i < numParts; i++) { - if ((i % 2) === 0) { - if (randInt(10)) { - txt.append(randomInlineString(randInt(approxMaxCols) + 1)); - } else { - txt.append('\n'); - } - } else { - txt.append('\n'); - } - } - return txt.toString(); - }; - - const randomStringOperation = (numCharsLeft) => { - let result; - switch (randInt(9)) { - case 0: - { - // insert char - result = { - insert: randomInlineString(1), - }; - break; - } - case 1: - { - // delete char - result = { - remove: 1, - }; - break; - } - case 2: - { - // skip char - result = { - skip: 1, - }; - break; - } - case 3: - { - // insert small - result = { - insert: randomInlineString(randInt(4) + 1), - }; - break; - } - case 4: - { - // delete small - result = { - remove: randInt(4) + 1, - }; - break; - } - case 5: - { - // skip small - result = { - skip: randInt(4) + 1, - }; - break; - } - case 6: - { - // insert multiline; - result = { - insert: randomMultiline(5, 20), - }; - break; - } - case 7: - { - // delete multiline - result = { - remove: Math.round(numCharsLeft * Math.random() * Math.random()), - }; - break; - } - case 8: - { - // skip multiline - result = { - skip: Math.round(numCharsLeft * Math.random() * Math.random()), - }; - break; - } - case 9: - { - // delete to end - result = { - remove: numCharsLeft, - }; - break; - } - case 10: - { - // skip to end - result = { - skip: numCharsLeft, - }; - break; - } - } - const maxOrig = numCharsLeft - 1; - if ('remove' in result) { - result.remove = Math.min(result.remove, maxOrig); - } else if ('skip' in result) { - result.skip = Math.min(result.skip, maxOrig); - } - return result; - }; - - const randomTwoPropAttribs = (opcode) => { - // assumes attrib pool like ['apple,','apple,true','banana,','banana,true'] - if (opcode === '-' || randInt(3)) { - return ''; - } else if (randInt(3)) { // eslint-disable-line no-dupe-else-if - if (opcode === '+' || randInt(2)) { - return `*${Changeset.numToString(randInt(2) * 2 + 1)}`; - } else { - return `*${Changeset.numToString(randInt(2) * 2)}`; - } - } else if (opcode === '+' || randInt(4) === 0) { - return '*1*3'; - } else { - return ['*0*2', '*0*3', '*1*2'][randInt(3)]; - } - }; - - const randomTestChangeset = (origText, withAttribs) => { - const charBank = Changeset.stringAssembler(); - let textLeft = origText; // always keep final newline - const outTextAssem = Changeset.stringAssembler(); - const opAssem = Changeset.smartOpAssembler(); - const oldLen = origText.length; - - const nextOp = Changeset.newOp(); - - const appendMultilineOp = (opcode, txt) => { - nextOp.opcode = opcode; - if (withAttribs) { - nextOp.attribs = randomTwoPropAttribs(opcode); - } - txt.replace(/\n|[^\n]+/g, (t) => { - if (t === '\n') { - nextOp.chars = 1; - nextOp.lines = 1; - opAssem.append(nextOp); - } else { - nextOp.chars = t.length; - nextOp.lines = 0; - opAssem.append(nextOp); - } - return ''; - }); - }; - - const doOp = () => { - const o = randomStringOperation(textLeft.length); - if (o.insert) { - const txt = o.insert; - charBank.append(txt); - outTextAssem.append(txt); - appendMultilineOp('+', txt); - } else if (o.skip) { - const txt = textLeft.substring(0, o.skip); - textLeft = textLeft.substring(o.skip); - outTextAssem.append(txt); - appendMultilineOp('=', txt); - } else if (o.remove) { - const txt = textLeft.substring(0, o.remove); - textLeft = textLeft.substring(o.remove); - appendMultilineOp('-', txt); - } - }; - - while (textLeft.length > 1) doOp(); - for (let i = 0; i < 5; i++) doOp(); // do some more (only insertions will happen) - const outText = `${outTextAssem.toString()}\n`; - opAssem.endDocument(); - const cs = Changeset.pack(oldLen, outText.length, opAssem.toString(), charBank.toString()); - Changeset.checkRep(cs); - return [cs, outText]; - }; - - const testCompose = (randomSeed) => { - it(`testCompose#${randomSeed}`, async function () { - const p = new AttributePool(); - - const startText = `${randomMultiline(10, 20)}\n`; - - const x1 = randomTestChangeset(startText); - const change1 = x1[0]; - const text1 = x1[1]; - - const x2 = randomTestChangeset(text1); - const change2 = x2[0]; - const text2 = x2[1]; - - const x3 = randomTestChangeset(text2); - const change3 = x3[0]; - const text3 = x3[1]; - - const change12 = Changeset.checkRep(Changeset.compose(change1, change2, p)); - const change23 = Changeset.checkRep(Changeset.compose(change2, change3, p)); - const change123 = Changeset.checkRep(Changeset.compose(change12, change3, p)); - const change123a = Changeset.checkRep(Changeset.compose(change1, change23, p)); - expect(change123a).to.equal(change123); - - expect(Changeset.applyToText(change12, startText)).to.equal(text2); - expect(Changeset.applyToText(change23, text1)).to.equal(text3); - expect(Changeset.applyToText(change123, startText)).to.equal(text3); - }); - }; - - for (let i = 0; i < 30; i++) testCompose(i); - - it('simpleComposeAttributesTest', async function () { - const p = new AttributePool(); - p.putAttrib(['bold', '']); - p.putAttrib(['bold', 'true']); - const cs1 = Changeset.checkRep('Z:2>1*1+1*1=1$x'); - const cs2 = Changeset.checkRep('Z:3>0*0|1=3$'); - const cs12 = Changeset.checkRep(Changeset.compose(cs1, cs2, p)); - expect(cs12).to.equal('Z:2>1+1*0|1=2$x'); - }); - - (() => { - const p = new AttributePool(); - p.putAttrib(['x', '']); - p.putAttrib(['x', 'abc']); - p.putAttrib(['x', 'def']); - p.putAttrib(['y', '']); - p.putAttrib(['y', 'abc']); - p.putAttrib(['y', 'def']); - let n = 0; - - const testFollow = (a, b, afb, bfa, merge) => { - it(`testFollow manual #${++n}`, async function () { - expect(Changeset.exportedForTestingOnly.followAttributes(a, b, p)).to.equal(afb); - expect(Changeset.exportedForTestingOnly.followAttributes(b, a, p)).to.equal(bfa); - expect(Changeset.composeAttributes(a, afb, true, p)).to.equal(merge); - expect(Changeset.composeAttributes(b, bfa, true, p)).to.equal(merge); - }); - }; - - testFollow('', '', '', '', ''); - testFollow('*0', '', '', '*0', '*0'); - testFollow('*0', '*0', '', '', '*0'); - testFollow('*0', '*1', '', '*0', '*0'); - testFollow('*1', '*2', '', '*1', '*1'); - testFollow('*0*1', '', '', '*0*1', '*0*1'); - testFollow('*0*4', '*2*3', '*3', '*0', '*0*3'); - testFollow('*0*4', '*2', '', '*0*4', '*0*4'); - })(); - - const testFollow = (randomSeed) => { - it(`testFollow#${randomSeed}`, async function () { - const p = new AttributePool(); - - const startText = `${randomMultiline(10, 20)}\n`; - - const cs1 = randomTestChangeset(startText)[0]; - const cs2 = randomTestChangeset(startText)[0]; - - const afb = Changeset.checkRep(Changeset.follow(cs1, cs2, false, p)); - const bfa = Changeset.checkRep(Changeset.follow(cs2, cs1, true, p)); - - const merge1 = Changeset.checkRep(Changeset.compose(cs1, afb)); - const merge2 = Changeset.checkRep(Changeset.compose(cs2, bfa)); - - expect(merge2).to.equal(merge1); - }); - }; - - for (let i = 0; i < 30; i++) testFollow(i); - - const testSplitJoinAttributionLines = (randomSeed) => { - const stringToOps = (str) => { - const assem = Changeset.mergingOpAssembler(); - const o = Changeset.newOp('+'); - o.chars = 1; - for (let i = 0; i < str.length; i++) { - const c = str.charAt(i); - o.lines = (c === '\n' ? 1 : 0); - o.attribs = (c === 'a' || c === 'b' ? `*${c}` : ''); - assem.append(o); - } - return assem.toString(); - }; - - it(`testSplitJoinAttributionLines#${randomSeed}`, async function () { - const doc = `${randomMultiline(10, 20)}\n`; - - const theJoined = stringToOps(doc); - const theSplit = doc.match(/[^\n]*\n/g).map(stringToOps); - - expect(Changeset.splitAttributionLines(theJoined, doc)).to.eql(theSplit); - expect(Changeset.joinAttributionLines(theSplit)).to.equal(theJoined); - }); - }; - - for (let i = 0; i < 10; i++) testSplitJoinAttributionLines(i); - - it('testMoveOpsToNewPool', async function () { - const pool1 = new AttributePool(); - const pool2 = new AttributePool(); - - pool1.putAttrib(['baz', 'qux']); - pool1.putAttrib(['foo', 'bar']); - - pool2.putAttrib(['foo', 'bar']); - - expect(Changeset.moveOpsToNewPool('Z:1>2*1+1*0+1$ab', pool1, pool2)) - .to.equal('Z:1>2*0+1*1+1$ab'); - expect(Changeset.moveOpsToNewPool('*1+1*0+1', pool1, pool2)).to.equal('*0+1*1+1'); - }); - - it('testMakeSplice', async function () { - const t = 'a\nb\nc\n'; - const t2 = Changeset.applyToText(Changeset.makeSplice(t, 5, 0, 'def'), t); - expect(t2).to.equal('a\nb\ncdef\n'); - }); - - it('testToSplices', async function () { - const cs = Changeset.checkRep('Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk'); - const correctSplices = [ - [5, 8, '123456789'], - [9, 17, 'abcdefghijk'], - ]; - expect(Changeset.exportedForTestingOnly.toSplices(cs)).to.eql(correctSplices); - }); - - const testCharacterRangeFollow = (testId, cs, oldRange, insertionsAfter, correctNewRange) => { - it(`testCharacterRangeFollow#${testId}`, async function () { - cs = Changeset.checkRep(cs); - expect(Changeset.characterRangeFollow(cs, oldRange[0], oldRange[1], insertionsAfter)) - .to.eql(correctNewRange); - }); - }; - - testCharacterRangeFollow(1, 'Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk', - [7, 10], false, [14, 15]); - testCharacterRangeFollow(2, 'Z:bc<6|x=b4|2-6$', [400, 407], false, [400, 401]); - testCharacterRangeFollow(3, 'Z:4>0-3+3$abc', [0, 3], false, [3, 3]); - testCharacterRangeFollow(4, 'Z:4>0-3+3$abc', [0, 3], true, [0, 0]); - testCharacterRangeFollow(5, 'Z:5>1+1=1-3+3$abcd', [1, 4], false, [5, 5]); - testCharacterRangeFollow(6, 'Z:5>1+1=1-3+3$abcd', [1, 4], true, [2, 2]); - testCharacterRangeFollow(7, 'Z:5>1+1=1-3+3$abcd', [0, 6], false, [1, 7]); - testCharacterRangeFollow(8, 'Z:5>1+1=1-3+3$abcd', [0, 3], false, [1, 2]); - testCharacterRangeFollow(9, 'Z:5>1+1=1-3+3$abcd', [2, 5], false, [5, 6]); - testCharacterRangeFollow(10, 'Z:2>1+1$a', [0, 0], false, [1, 1]); - testCharacterRangeFollow(11, 'Z:2>1+1$a', [0, 0], true, [0, 0]); - - it('testOpAttributeValue', async function () { - const p = new AttributePool(); - p.putAttrib(['name', 'david']); - p.putAttrib(['color', 'green']); - - const stringOp = (str) => Changeset.opIterator(str).next(); - - expect(Changeset.opAttributeValue(stringOp('*0*1+1'), 'name', p)).to.equal('david'); - expect(Changeset.opAttributeValue(stringOp('*0+1'), 'name', p)).to.equal('david'); - expect(Changeset.opAttributeValue(stringOp('*1+1'), 'name', p)).to.equal(''); - expect(Changeset.opAttributeValue(stringOp('+1'), 'name', p)).to.equal(''); - expect(Changeset.opAttributeValue(stringOp('*0*1+1'), 'color', p)).to.equal('green'); - expect(Changeset.opAttributeValue(stringOp('*1+1'), 'color', p)).to.equal('green'); - expect(Changeset.opAttributeValue(stringOp('*0+1'), 'color', p)).to.equal(''); - expect(Changeset.opAttributeValue(stringOp('+1'), 'color', p)).to.equal(''); - }); - - const testAppendATextToAssembler = (testId, atext, correctOps) => { - it(`testAppendATextToAssembler#${testId}`, async function () { - const assem = Changeset.smartOpAssembler(); - Changeset.appendATextToAssembler(atext, assem); - expect(assem.toString()).to.equal(correctOps); - }); - }; - - testAppendATextToAssembler(1, { - text: '\n', - attribs: '|1+1', - }, ''); - testAppendATextToAssembler(2, { - text: '\n\n', - attribs: '|2+2', - }, '|1+1'); - testAppendATextToAssembler(3, { - text: '\n\n', - attribs: '*x|2+2', - }, '*x|1+1'); - testAppendATextToAssembler(4, { - text: '\n\n', - attribs: '*x|1+1|1+1', - }, '*x|1+1'); - testAppendATextToAssembler(5, { - text: 'foo\n', - attribs: '|1+4', - }, '+3'); - testAppendATextToAssembler(6, { - text: '\nfoo\n', - attribs: '|2+5', - }, '|1+1+3'); - testAppendATextToAssembler(7, { - text: '\nfoo\n', - attribs: '*x|2+5', - }, '*x|1+1*x+3'); - testAppendATextToAssembler(8, { - text: '\n\n\nfoo\n', - attribs: '|2+2*x|2+5', - }, '|2+2*x|1+1*x+3'); - - const testMakeAttribsString = (testId, pool, opcode, attribs, correctString) => { - it(`testMakeAttribsString#${testId}`, async function () { - const p = poolOrArray(pool); - const str = Changeset.makeAttribsString(opcode, attribs, p); - expect(str).to.equal(correctString); - }); - }; - - testMakeAttribsString(1, ['bold,'], '+', [ - ['bold', ''], - ], ''); - testMakeAttribsString(2, ['abc,def', 'bold,'], '=', [ - ['bold', ''], - ], '*1'); - testMakeAttribsString(3, ['abc,def', 'bold,true'], '+', [ - ['abc', 'def'], - ['bold', 'true'], - ], '*0*1'); - testMakeAttribsString(4, ['abc,def', 'bold,true'], '+', [ - ['bold', 'true'], - ['abc', 'def'], - ], '*0*1'); - - const testSubattribution = (testId, astr, start, end, correctOutput) => { - it(`testSubattribution#${testId}`, async function () { - const str = Changeset.subattribution(astr, start, end); - expect(str).to.equal(correctOutput); - }); - }; - - testSubattribution(1, '+1', 0, 0, ''); - testSubattribution(2, '+1', 0, 1, '+1'); - testSubattribution(3, '+1', 0, undefined, '+1'); - testSubattribution(4, '|1+1', 0, 0, ''); - testSubattribution(5, '|1+1', 0, 1, '|1+1'); - testSubattribution(6, '|1+1', 0, undefined, '|1+1'); - testSubattribution(7, '*0+1', 0, 0, ''); - testSubattribution(8, '*0+1', 0, 1, '*0+1'); - testSubattribution(9, '*0+1', 0, undefined, '*0+1'); - testSubattribution(10, '*0|1+1', 0, 0, ''); - testSubattribution(11, '*0|1+1', 0, 1, '*0|1+1'); - testSubattribution(12, '*0|1+1', 0, undefined, '*0|1+1'); - testSubattribution(13, '*0+2+1*1+3', 0, 1, '*0+1'); - testSubattribution(14, '*0+2+1*1+3', 0, 2, '*0+2'); - testSubattribution(15, '*0+2+1*1+3', 0, 3, '*0+2+1'); - testSubattribution(16, '*0+2+1*1+3', 0, 4, '*0+2+1*1+1'); - testSubattribution(17, '*0+2+1*1+3', 0, 5, '*0+2+1*1+2'); - testSubattribution(18, '*0+2+1*1+3', 0, 6, '*0+2+1*1+3'); - testSubattribution(19, '*0+2+1*1+3', 0, 7, '*0+2+1*1+3'); - testSubattribution(20, '*0+2+1*1+3', 0, undefined, '*0+2+1*1+3'); - testSubattribution(21, '*0+2+1*1+3', 1, undefined, '*0+1+1*1+3'); - testSubattribution(22, '*0+2+1*1+3', 2, undefined, '+1*1+3'); - testSubattribution(23, '*0+2+1*1+3', 3, undefined, '*1+3'); - testSubattribution(24, '*0+2+1*1+3', 4, undefined, '*1+2'); - testSubattribution(25, '*0+2+1*1+3', 5, undefined, '*1+1'); - testSubattribution(26, '*0+2+1*1+3', 6, undefined, ''); - testSubattribution(27, '*0+2+1*1|1+3', 0, 1, '*0+1'); - testSubattribution(28, '*0+2+1*1|1+3', 0, 2, '*0+2'); - testSubattribution(29, '*0+2+1*1|1+3', 0, 3, '*0+2+1'); - testSubattribution(30, '*0+2+1*1|1+3', 0, 4, '*0+2+1*1+1'); - testSubattribution(31, '*0+2+1*1|1+3', 0, 5, '*0+2+1*1+2'); - testSubattribution(32, '*0+2+1*1|1+3', 0, 6, '*0+2+1*1|1+3'); - testSubattribution(33, '*0+2+1*1|1+3', 0, 7, '*0+2+1*1|1+3'); - testSubattribution(34, '*0+2+1*1|1+3', 0, undefined, '*0+2+1*1|1+3'); - testSubattribution(35, '*0+2+1*1|1+3', 1, undefined, '*0+1+1*1|1+3'); - testSubattribution(36, '*0+2+1*1|1+3', 2, undefined, '+1*1|1+3'); - testSubattribution(37, '*0+2+1*1|1+3', 3, undefined, '*1|1+3'); - testSubattribution(38, '*0+2+1*1|1+3', 4, undefined, '*1|1+2'); - testSubattribution(39, '*0+2+1*1|1+3', 5, undefined, '*1|1+1'); - testSubattribution(40, '*0+2+1*1|1+3', 1, 5, '*0+1+1*1+2'); - testSubattribution(41, '*0+2+1*1|1+3', 2, 6, '+1*1|1+3'); - testSubattribution(42, '*0+2+1*1+3', 2, 6, '+1*1+3'); - - const testFilterAttribNumbers = (testId, cs, filter, correctOutput) => { - it(`testFilterAttribNumbers#${testId}`, async function () { - const str = Changeset.filterAttribNumbers(cs, filter); - expect(str).to.equal(correctOutput); - }); - }; - - testFilterAttribNumbers(1, '*0*1+1+2+3*1+4*2+5*0*2*1*b*c+6', - (n) => (n % 2) === 0, '*0+1+2+3+4*2+5*0*2*c+6'); - testFilterAttribNumbers(2, '*0*1+1+2+3*1+4*2+5*0*2*1*b*c+6', - (n) => (n % 2) === 1, '*1+1+2+3*1+4+5*1*b+6'); - - const testInverse = (testId, cs, lines, alines, pool, correctOutput) => { - it(`testInverse#${testId}`, async function () { - pool = poolOrArray(pool); - const str = Changeset.inverse(Changeset.checkRep(cs), lines, alines, pool); - expect(str).to.equal(correctOutput); - }); - }; - - // take "FFFFTTTTT" and apply "-FT--FFTT", the inverse of which is "--F--TT--" - testInverse(1, 'Z:9>0=1*0=1*1=1=2*0=2*1|1=2$', null, - ['+4*1+5'], ['bold,', 'bold,true'], 'Z:9>0=2*0=1=2*1=2$'); - - const testMutateTextLines = (testId, cs, lines, correctLines) => { - it(`testMutateTextLines#${testId}`, async function () { - const a = lines.slice(); - Changeset.mutateTextLines(cs, a); - expect(a).to.eql(correctLines); - }); - }; - - testMutateTextLines(1, 'Z:4<1|1-2-1|1+1+1$\nc', ['a\n', 'b\n'], ['\n', 'c\n']); - testMutateTextLines(2, 'Z:4>0|1-2-1|2+3$\nc\n', ['a\n', 'b\n'], ['\n', 'c\n', '\n']); - - const testInverseRandom = (randomSeed) => { - it(`testInverseRandom#${randomSeed}`, async function () { - const p = poolOrArray(['apple,', 'apple,true', 'banana,', 'banana,true']); - - const startText = `${randomMultiline(10, 20)}\n`; - const alines = - Changeset.splitAttributionLines(Changeset.makeAttribution(startText), startText); - const lines = startText.slice(0, -1).split('\n').map((s) => `${s}\n`); - - const stylifier = randomTestChangeset(startText, true)[0]; - - Changeset.mutateAttributionLines(stylifier, alines, p); - Changeset.mutateTextLines(stylifier, lines); - - const changeset = randomTestChangeset(lines.join(''), true)[0]; - const inverseChangeset = Changeset.inverse(changeset, lines, alines, p); - - const origLines = lines.slice(); - const origALines = alines.slice(); - - Changeset.mutateTextLines(changeset, lines); - Changeset.mutateAttributionLines(changeset, alines, p); - Changeset.mutateTextLines(inverseChangeset, lines); - Changeset.mutateAttributionLines(inverseChangeset, alines, p); - expect(lines).to.eql(origLines); - expect(alines).to.eql(origALines); - }); - }; - - for (let i = 0; i < 30; i++) testInverseRandom(i); -}); diff --git a/src/tests/frontend/specs/embed_value.js b/src/tests/frontend/specs/embed_value.js deleted file mode 100644 index e92d070a7..000000000 --- a/src/tests/frontend/specs/embed_value.js +++ /dev/null @@ -1,125 +0,0 @@ -'use strict'; - -describe('embed links', function () { - const objectify = function (str) { - const hash = {}; - const parts = str.split('&'); - for (let i = 0; i < parts.length; i++) { - const keyValue = parts[i].split('='); - hash[keyValue[0]] = keyValue[1]; - } - return hash; - }; - - const checkiFrameCode = function (embedCode, readonly) { - // turn the code into an html element - const $embediFrame = $(embedCode); - - // read and check the frame attributes - const width = $embediFrame.attr('width'); - const height = $embediFrame.attr('height'); - const name = $embediFrame.attr('name'); - expect(width).to.be('100%'); - expect(height).to.be('600'); - expect(name).to.be(readonly ? 'embed_readonly' : 'embed_readwrite'); - - // parse the url - const src = $embediFrame.attr('src'); - const questionMark = src.indexOf('?'); - const url = src.substr(0, questionMark); - const paramsStr = src.substr(questionMark + 1); - const params = objectify(paramsStr); - - const expectedParams = { - showControls: 'true', - showChat: 'true', - showLineNumbers: 'true', - useMonospaceFont: 'false', - }; - - // check the url - if (readonly) { - expect(url.indexOf('r.') > 0).to.be(true); - } else { - expect(url).to.be(helper.padChrome$.window.location.href); - } - - // check if all parts of the url are like expected - expect(params).to.eql(expectedParams); - }; - - describe('read and write', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - describe('the share link', function () { - it('is the actual pad url', async function () { - const chrome$ = helper.padChrome$; - - // open share dropdown - chrome$('.buttonicon-embed').click(); - - // get the link of the share field + the actual pad url and compare them - const shareLink = chrome$('#linkinput').val(); - const padURL = chrome$.window.location.href; - expect(shareLink).to.be(padURL); - }); - }); - - describe('the embed as iframe code', function () { - it('is an iframe with the the correct url parameters and correct size', async function () { - const chrome$ = helper.padChrome$; - - // open share dropdown - chrome$('.buttonicon-embed').click(); - - // get the link of the share field + the actual pad url and compare them - const embedCode = chrome$('#embedinput').val(); - - checkiFrameCode(embedCode, false); - }); - }); - }); - - describe('when read only option is set', function () { - beforeEach(async function () { - await helper.aNewPad(); - }); - - describe('the share link', function () { - it('shows a read only url', async function () { - const chrome$ = helper.padChrome$; - - // open share dropdown - chrome$('.buttonicon-embed').click(); - chrome$('#readonlyinput').click(); - chrome$('#readonlyinput:checkbox:not(:checked)').attr('checked', 'checked'); - - // get the link of the share field + the actual pad url and compare them - const shareLink = chrome$('#linkinput').val(); - const containsReadOnlyLink = shareLink.indexOf('r.') > 0; - expect(containsReadOnlyLink).to.be(true); - }); - }); - - describe('the embed as iframe code', function () { - it('is an iframe with the the correct url parameters and correct size', async function () { - const chrome$ = helper.padChrome$; - - // open share dropdown - chrome$('.buttonicon-embed').click(); - // check read only checkbox, a bit hacky - chrome$('#readonlyinput').click(); - chrome$('#readonlyinput:checkbox:not(:checked)').attr('checked', 'checked'); - - - // get the link of the share field + the actual pad url and compare them - const embedCode = chrome$('#embedinput').val(); - - checkiFrameCode(embedCode, true); - }); - }); - }); -}); diff --git a/src/tests/frontend/specs/enter.js b/src/tests/frontend/specs/enter.js deleted file mode 100644 index a32a90c6e..000000000 --- a/src/tests/frontend/specs/enter.js +++ /dev/null @@ -1,61 +0,0 @@ -'use strict'; - -describe('enter keystroke', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('creates a new line & puts cursor onto a new line', async function () { - this.timeout(2000); - const inner$ = helper.padInner$; - - // get the first text element out of the inner iframe - const $firstTextElement = inner$('div').first(); - - // get the original string value minus the last char - const originalTextValue = $firstTextElement.text(); - - // simulate key presses to enter content - $firstTextElement.sendkeys('{enter}'); - - await helper.waitForPromise(() => inner$('div').first().text() === ''); - - const $newSecondLine = inner$('div').first().next(); - const newFirstTextElementValue = inner$('div').first().text(); - expect(newFirstTextElementValue).to.be(''); // expect the first line to be blank - // expect the second line to be the same as the original first line. - expect($newSecondLine.text()).to.be(originalTextValue); - }); - - it('enter is always visible after event', async function () { - const originalLength = helper.padInner$('div').length; - let $lastLine = helper.padInner$('div').last(); - - // simulate key presses to enter content - let i = 0; - const numberOfLines = 15; - let previousLineLength = originalLength; - while (i < numberOfLines) { - $lastLine = helper.padInner$('div').last(); - $lastLine.sendkeys('{enter}'); - await helper.waitForPromise(() => helper.padInner$('div').length > previousLineLength); - previousLineLength = helper.padInner$('div').length; - // check we can see the caret.. - - i++; - } - await helper.waitForPromise( - () => helper.padInner$('div').length === numberOfLines + originalLength); - - // is edited line fully visible? - const lastLine = helper.padInner$('div').last(); - const bottomOfLastLine = lastLine.offset().top + lastLine.height(); - const scrolledWindow = helper.padChrome$('iframe')[0]; - await helper.waitForPromise(() => { - const scrolledAmount = - scrolledWindow.contentWindow.pageYOffset + scrolledWindow.contentWindow.innerHeight; - return scrolledAmount >= bottomOfLastLine; - }); - }); -}); diff --git a/src/tests/frontend/specs/font_type.js b/src/tests/frontend/specs/font_type.js deleted file mode 100644 index fbebcdfd7..000000000 --- a/src/tests/frontend/specs/font_type.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -describe('font select', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('makes text RobotoMono', async function () { - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - // click on the settings button to make settings visible - const $settingsButton = chrome$('.buttonicon-settings'); - $settingsButton.click(); - - // get the font menu and RobotoMono option - const $viewfontmenu = chrome$('#viewfontmenu'); - - // select RobotoMono and fire change event - // $RobotoMonooption.attr('selected','selected'); - // commenting out above will break safari test - $viewfontmenu.val('RobotoMono'); - $viewfontmenu.change(); - - // check if font changed to RobotoMono - const fontFamily = inner$('body').css('font-family').toLowerCase(); - const containsStr = fontFamily.indexOf('robotomono'); - expect(containsStr).to.not.be(-1); - }); -}); diff --git a/src/tests/frontend/specs/helper.js b/src/tests/frontend/specs/helper.js index 0d876b336..908f98442 100644 --- a/src/tests/frontend/specs/helper.js +++ b/src/tests/frontend/specs/helper.js @@ -7,7 +7,7 @@ describe('the test helper', function () { for (let i = 0; i < 10; ++i) await helper.aNewPad(); }); - it('gives me 3 jquery instances of chrome, outer and inner', async function () { + xit('gives me 3 jquery instances of chrome, outer and inner', async function () { this.timeout(10000); await helper.aNewPad(); // check if the jquery selectors have the desired elements @@ -27,7 +27,7 @@ describe('the test helper', function () { // However this doesn't seem to always be easily replicated, so this // timeout may or may end up in the code. None the less, we test here // to catch it if the bug comes up again. - it('clears cookies', async function () { + xit('clears cookies', async function () { // set cookies far into the future to make sure they're not expired yet window.Cookies.set('token', 'foo', {expires: 7 /* days */}); window.Cookies.set('language', 'bar', {expires: 7 /* days */}); @@ -47,13 +47,13 @@ describe('the test helper', function () { // click on the settings button to make settings visible let $userButton = chrome$('.buttonicon-showusers'); - $userButton.click(); + $userButton.trigger('click'); let $usernameInput = chrome$('#myusernameedit'); - $usernameInput.click(); + $usernameInput.trigger('click'); $usernameInput.val('John McLear'); - $usernameInput.blur(); + $usernameInput.trigger('blur'); // Before refreshing, make sure the name is there expect($usernameInput.val()).to.be('John McLear'); @@ -85,7 +85,7 @@ describe('the test helper', function () { // click on the settings button to make settings visible $userButton = chrome$('.buttonicon-showusers'); - $userButton.click(); + $userButton.trigger('click'); // confirm that the session was actually cleared $usernameInput = chrome$('#myusernameedit'); @@ -167,7 +167,7 @@ describe('the test helper', function () { expect(Date.now() - before).to.be.lessThan(800); }); - it('polls exactly once if timeout < interval', async function () { + xit('polls exactly once if timeout < interval', async function () { let calls = 0; await helper.waitFor(() => { calls++; }, 1, 1000) .fail(() => {}) // Suppress the redundant uncatchable exception. @@ -249,7 +249,7 @@ describe('the test helper', function () { }); }); - it('changes editor selection to be between startOffset of $startLine ' + + xit('changes editor selection to be between startOffset of $startLine ' + 'and endOffset of $endLine', function (done) { const inner$ = helper.padInner$; @@ -410,13 +410,13 @@ describe('the test helper', function () { }); }); - it('.edit() defaults to send an edit to the first line', async function () { + xit('.edit() defaults to send an edit to the first line', async function () { const firstLine = helper.textLines()[0]; await helper.edit('line'); expect(helper.textLines()[0]).to.be(`line${firstLine}`); }); - it('.edit() to the line specified with parameter lineNo', async function () { + xit('.edit() to the line specified with parameter lineNo', async function () { const firstLine = helper.textLines()[0]; await helper.edit('second line', 2); @@ -425,7 +425,7 @@ describe('the test helper', function () { expect(text[1]).to.equal('second line'); }); - it('.edit() supports sendkeys syntax ({selectall},{del},{enter})', async function () { + xit('.edit() supports sendkeys syntax ({selectall},{del},{enter})', async function () { expect(helper.textLines()[0]).to.not.equal(''); // select first line diff --git a/src/tests/frontend/specs/importexport.js b/src/tests/frontend/specs/importexport.js index 46254bedd..e04f5db56 100644 --- a/src/tests/frontend/specs/importexport.js +++ b/src/tests/frontend/specs/importexport.js @@ -527,7 +527,7 @@ describe('importexport.js', function () { const isVisible = () => popup.hasClass('popup-show'); if (isVisible()) return; const button = helper.padChrome$('button[data-l10n-id="pad.toolbar.import_export.title"]'); - button.click(); + button.trigger('click'); await helper.waitForPromise(isVisible); }); @@ -558,7 +558,7 @@ describe('importexport.js', function () { dt.items.add(new File([contents], `file.${ext}`, {type: 'text/plain'})); const form = helper.padChrome$('#importform'); form.find('input[type=file]')[0].files = dt.files; - form.find('#importsubmitinput').submit(); + form.find('#importsubmitinput').trigger('submit'); try { await helper.waitForPromise(() => { const got = helper.linesDiv(); diff --git a/src/tests/frontend/specs/indentation.js b/src/tests/frontend/specs/indentation.js deleted file mode 100644 index 80a0f9140..000000000 --- a/src/tests/frontend/specs/indentation.js +++ /dev/null @@ -1,310 +0,0 @@ -'use strict'; - -describe('indentation button', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('indent text with keypress', async function () { - const inner$ = helper.padInner$; - - // get the first text element out of the inner iframe - const $firstTextElement = inner$('div').first(); - - // select this text element - $firstTextElement.sendkeys('{selectall}'); - - const e = new inner$.Event(helper.evtType); - e.keyCode = 9; // tab :| - inner$('#innerdocbody').trigger(e); - - await helper.waitForPromise(() => inner$('div').first().find('ul li').length === 1); - }); - - it('indent text with button', async function () { - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - const $indentButton = chrome$('.buttonicon-indent'); - $indentButton.click(); - - await helper.waitForPromise(() => inner$('div').first().find('ul li').length === 1); - }); - - it('keeps the indent on enter for the new line', async function () { - this.timeout(1200); - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - const $indentButton = chrome$('.buttonicon-indent'); - $indentButton.click(); - - // type a bit, make a line break and type again - const $firstTextElement = inner$('div span').first(); - $firstTextElement.sendkeys('line 1'); - $firstTextElement.sendkeys('{enter}'); - $firstTextElement.sendkeys('line 2'); - $firstTextElement.sendkeys('{enter}'); - - await helper.waitFor(() => inner$('div span').first().text().indexOf('line 2') === -1); - - const $newSecondLine = inner$('div').first().next(); - const hasULElement = $newSecondLine.find('ul li').length === 1; - - expect(hasULElement).to.be(true); - expect($newSecondLine.text()).to.be('line 2'); - }); - - it('indents text with spaces on enter if previous line ends ' + - "with ':', '[', '(', or '{'", async function () { - this.timeout(1200); - const inner$ = helper.padInner$; - - // type a bit, make a line break and type again - const $firstTextElement = inner$('div').first(); - $firstTextElement.sendkeys("line with ':'{enter}"); - $firstTextElement.sendkeys("line with '['{enter}"); - $firstTextElement.sendkeys("line with '('{enter}"); - $firstTextElement.sendkeys("line with '{{}'{enter}"); - - await helper.waitForPromise(() => { - // wait for Etherpad to split four lines into separated divs - const $fourthLine = inner$('div').first().next().next().next(); - return $fourthLine.text().indexOf("line with '{'") === 0; - }); - - // we validate bottom to top for easier implementation - - // curly braces - const $lineWithCurlyBraces = inner$('div').first().next().next().next(); - $lineWithCurlyBraces.sendkeys('{{}'); - // cannot use sendkeys('{enter}') here, browser does not read the command properly - pressEnter(); - const $lineAfterCurlyBraces = inner$('div').first().next().next().next().next(); - expect($lineAfterCurlyBraces.text()).to.match(/\s{4}/); // tab === 4 spaces - - // parenthesis - const $lineWithParenthesis = inner$('div').first().next().next(); - $lineWithParenthesis.sendkeys('('); - pressEnter(); - const $lineAfterParenthesis = inner$('div').first().next().next().next(); - expect($lineAfterParenthesis.text()).to.match(/\s{4}/); - - // bracket - const $lineWithBracket = inner$('div').first().next(); - $lineWithBracket.sendkeys('['); - pressEnter(); - const $lineAfterBracket = inner$('div').first().next().next(); - expect($lineAfterBracket.text()).to.match(/\s{4}/); - - // colon - const $lineWithColon = inner$('div').first(); - $lineWithColon.sendkeys(':'); - pressEnter(); - const $lineAfterColon = inner$('div').first().next(); - expect($lineAfterColon.text()).to.match(/\s{4}/); - }); - - it('appends indentation to the indent of previous line if previous line ends ' + - "with ':', '[', '(', or '{'", async function () { - this.timeout(1200); - const inner$ = helper.padInner$; - - // type a bit, make a line break and type again - const $firstTextElement = inner$('div').first(); - $firstTextElement.sendkeys(" line with some indentation and ':'{enter}"); - $firstTextElement.sendkeys('line 2{enter}'); - - await helper.waitForPromise(() => { - // wait for Etherpad to split two lines into separated divs - const $secondLine = inner$('div').first().next(); - return $secondLine.text().indexOf('line 2') === 0; - }); - - const $lineWithColon = inner$('div').first(); - $lineWithColon.sendkeys(':'); - pressEnter(); - const $lineAfterColon = inner$('div').first().next(); - // previous line indentation + regular tab (4 spaces) - expect($lineAfterColon.text()).to.match(/\s{6}/); - }); - - it("issue #2772 shows '*' when multiple indented lines " + - ' receive a style and are outdented', async function () { - this.timeout(1200); - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - // make sure pad has more than one line - inner$('div').first().sendkeys('First{enter}Second{enter}'); - await helper.waitForPromise(() => inner$('div').first().text().trim() === 'First'); - - // indent first 2 lines - const $lines = inner$('div'); - const $firstLine = $lines.first(); - let $secondLine = $lines.slice(1, 2); - helper.selectLines($firstLine, $secondLine); - - const $indentButton = chrome$('.buttonicon-indent'); - $indentButton.click(); - - await helper.waitForPromise(() => inner$('div').first().find('ul li').length === 1); - - // apply bold - const $boldButton = chrome$('.buttonicon-bold'); - $boldButton.click(); - - await helper.waitForPromise(() => inner$('div').first().find('b').length === 1); - - // outdent first 2 lines - const $outdentButton = chrome$('.buttonicon-outdent'); - $outdentButton.click(); - await helper.waitForPromise(() => inner$('div').first().find('ul li').length === 0); - - // check if '*' is displayed - $secondLine = inner$('div').slice(1, 2); - expect($secondLine.text().trim()).to.be('Second'); - }); - - xit('makes text indented and outdented', async function () { - // get the inner iframe - const $inner = helper.$getPadInner(); - - // get the first text element out of the inner iframe - let firstTextElement = $inner.find('div').first(); - - // select this text element - helper.selectText(firstTextElement[0], $inner); - - // get the indentation button and click it - const $indentButton = helper.$getPadChrome().find('.buttonicon-indent'); - $indentButton.click(); - - let newFirstTextElement = $inner.find('div').first(); - - // is there a list-indent class element now? - let firstChild = newFirstTextElement.children(':first'); - let isUL = firstChild.is('ul'); - - // expect it to be the beginning of a list - expect(isUL).to.be(true); - - let secondChild = firstChild.children(':first'); - let isLI = secondChild.is('li'); - // expect it to be part of a list - expect(isLI).to.be(true); - - // indent again - $indentButton.click(); - - newFirstTextElement = $inner.find('div').first(); - - // is there a list-indent class element now? - firstChild = newFirstTextElement.children(':first'); - const hasListIndent2 = firstChild.hasClass('list-indent2'); - - // expect it to be part of a list - expect(hasListIndent2).to.be(true); - - // make sure the text hasn't changed - expect(newFirstTextElement.text()).to.eql(firstTextElement.text()); - - - // test outdent - - // get the unindentation button and click it twice - const $outdentButton = helper.$getPadChrome().find('.buttonicon-outdent'); - $outdentButton.click(); - $outdentButton.click(); - - newFirstTextElement = $inner.find('div').first(); - - // is there a list-indent class element now? - firstChild = newFirstTextElement.children(':first'); - isUL = firstChild.is('ul'); - - // expect it not to be the beginning of a list - expect(isUL).to.be(false); - - secondChild = firstChild.children(':first'); - isLI = secondChild.is('li'); - // expect it to not be part of a list - expect(isLI).to.be(false); - - // make sure the text hasn't changed - expect(newFirstTextElement.text()).to.eql(firstTextElement.text()); - - - // Next test tests multiple line indentation - - // select this text element - helper.selectText(firstTextElement[0], $inner); - - // indent twice - $indentButton.click(); - $indentButton.click(); - - // get the first text element out of the inner iframe - firstTextElement = $inner.find('div').first(); - - // select this text element - helper.selectText(firstTextElement[0], $inner); - - /* this test creates the below content, both should have double indentation - line1 - line2 - */ - - firstTextElement.sendkeys('{rightarrow}'); // simulate a keypress of enter - firstTextElement.sendkeys('{enter}'); // simulate a keypress of enter - firstTextElement.sendkeys('line 1'); // simulate writing the first line - firstTextElement.sendkeys('{enter}'); // simulate a keypress of enter - firstTextElement.sendkeys('line 2'); // simulate writing the second line - - // get the second text element out of the inner iframe - await new Promise((resolve) => setTimeout(resolve, 1000)); // THIS IS REALLY BAD - - const secondTextElement = $('iframe').contents() - .find('iframe').contents() - .find('iframe').contents().find('body > div').get(1); // THIS IS UGLY - - // is there a list-indent class element now? - firstChild = secondTextElement.children(':first'); - isUL = firstChild.is('ul'); - - // expect it to be the beginning of a list - expect(isUL).to.be(true); - - secondChild = secondChild.children(':first'); - isLI = secondChild.is('li'); - // expect it to be part of a list - expect(isLI).to.be(true); - - // get the first text element out of the inner iframe - const thirdTextElement = $('iframe').contents() - .find('iframe').contents() - .find('iframe').contents() - .find('body > div').get(2); // THIS IS UGLY TOO - - // is there a list-indent class element now? - firstChild = thirdTextElement.children(':first'); - isUL = firstChild.is('ul'); - - // expect it to be the beginning of a list - expect(isUL).to.be(true); - - secondChild = firstChild.children(':first'); - isLI = secondChild.is('li'); - - // expect it to be part of a list - expect(isLI).to.be(true); - }); -}); - -const pressEnter = () => { - const inner$ = helper.padInner$; - const e = new inner$.Event(helper.evtType); - e.keyCode = 13; // enter :| - inner$('#innerdocbody').trigger(e); -}; diff --git a/src/tests/frontend/specs/inner_height.js b/src/tests/frontend/specs/inner_height.js deleted file mode 100644 index d1a6b118b..000000000 --- a/src/tests/frontend/specs/inner_height.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -describe('height regression after ace.js refactoring', function () { - before(async function () { - await helper.aNewPad(); - }); - - // everything fits inside the viewport - it('clientHeight should equal scrollHeight with few lines', async function () { - await helper.clearPad(); - const outerHtml = helper.padChrome$('iframe')[0].contentDocument.documentElement; - // Give some time for the heights to settle. - await new Promise((resolve) => setTimeout(resolve, 100)); - expect(outerHtml.clientHeight).to.be(outerHtml.scrollHeight); - }); - - it('client height should be less than scrollHeight with many lines', async function () { - await helper.clearPad(); - await helper.edit('Test line\n' + - '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' + - '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' + - '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' + - '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' + - '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' + - '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' + - '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'); - const outerHtml = helper.padChrome$('iframe')[0].contentDocument.documentElement; - // Need to poll because the heights take some time to settle. - await helper.waitForPromise(() => outerHtml.clientHeight < outerHtml.scrollHeight); - }); -}); diff --git a/src/tests/frontend/specs/italic.js b/src/tests/frontend/specs/italic.js deleted file mode 100644 index 9b7b00b9f..000000000 --- a/src/tests/frontend/specs/italic.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -describe('italic some text', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('makes text italic using button', async function () { - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - // get the first text element out of the inner iframe - const $firstTextElement = inner$('div').first(); - - // select this text element - $firstTextElement.sendkeys('{selectall}'); - - // get the bold button and click it - const $boldButton = chrome$('.buttonicon-italic'); - $boldButton.click(); - - // ace creates a new dom element when you press a button, just get the first text element again - const $newFirstTextElement = inner$('div').first(); - - // is there a element now? - const isItalic = $newFirstTextElement.find('i').length === 1; - - // expect it to be bold - expect(isItalic).to.be(true); - - // make sure the text hasn't changed - expect($newFirstTextElement.text()).to.eql($firstTextElement.text()); - }); - - it('makes text italic using keypress', async function () { - const inner$ = helper.padInner$; - - // get the first text element out of the inner iframe - const $firstTextElement = inner$('div').first(); - - // select this text element - $firstTextElement.sendkeys('{selectall}'); - - const e = new inner$.Event(helper.evtType); - e.ctrlKey = true; // Control key - e.which = 105; // i - inner$('#innerdocbody').trigger(e); - - // ace creates a new dom element when you press a button, just get the first text element again - const $newFirstTextElement = inner$('div').first(); - - // is there a element now? - const isItalic = $newFirstTextElement.find('i').length === 1; - - // expect it to be bold - expect(isItalic).to.be(true); - - // make sure the text hasn't changed - expect($newFirstTextElement.text()).to.eql($firstTextElement.text()); - }); -}); diff --git a/src/tests/frontend/specs/language.js b/src/tests/frontend/specs/language.js deleted file mode 100644 index 0f0e3f345..000000000 --- a/src/tests/frontend/specs/language.js +++ /dev/null @@ -1,116 +0,0 @@ -'use strict'; - -describe('Language select and change', function () { - // Destroy language cookies - window.Cookies.remove('language'); - - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - // Destroy language cookies - it('makes text german', async function () { - this.timeout(1000); - const chrome$ = helper.padChrome$; - - // click on the settings button to make settings visible - const $settingsButton = chrome$('.buttonicon-settings'); - $settingsButton.click(); - - // click the language button - const $language = chrome$('#languagemenu'); - const $languageoption = $language.find('[value=de]'); - - // select german - $languageoption.attr('selected', 'selected'); - $language.change(); - - await helper.waitForPromise( - () => chrome$('.buttonicon-bold').parent()[0].title === 'Fett (Strg-B)'); - - // get the value of the bold button - const $boldButton = chrome$('.buttonicon-bold').parent(); - - // get the title of the bold button - const boldButtonTitle = $boldButton[0].title; - - // check if the language is now german - expect(boldButtonTitle).to.be('Fett (Strg-B)'); - }); - - it('makes text English', async function () { - this.timeout(1000); - const chrome$ = helper.padChrome$; - - // click on the settings button to make settings visible - const $settingsButton = chrome$('.buttonicon-settings'); - $settingsButton.click(); - - // click the language button - const $language = chrome$('#languagemenu'); - // select english - $language.val('en'); - $language.change(); - - // get the value of the bold button - let $boldButton = chrome$('.buttonicon-bold').parent(); - - await helper.waitForPromise(() => $boldButton[0].title !== 'Fett (Strg+B)'); - - // get the value of the bold button - $boldButton = chrome$('.buttonicon-bold').parent(); - - // get the title of the bold button - const boldButtonTitle = $boldButton[0].title; - - // check if the language is now English - expect(boldButtonTitle).to.be('Bold (Ctrl+B)'); - }); - - it('changes direction when picking an rtl lang', async function () { - this.timeout(1000); - const chrome$ = helper.padChrome$; - - // click on the settings button to make settings visible - const $settingsButton = chrome$('.buttonicon-settings'); - $settingsButton.click(); - - // click the language button - const $language = chrome$('#languagemenu'); - const $languageoption = $language.find('[value=ar]'); - - // select arabic - // $languageoption.attr('selected','selected'); // Breaks the test.. - $language.val('ar'); - $languageoption.change(); - - await helper.waitForPromise(() => chrome$('html')[0].dir !== 'ltr'); - - // check if the document's direction was changed - expect(chrome$('html')[0].dir).to.be('rtl'); - }); - - it('changes direction when picking an ltr lang', async function () { - const chrome$ = helper.padChrome$; - - // click on the settings button to make settings visible - const $settingsButton = chrome$('.buttonicon-settings'); - $settingsButton.click(); - - // click the language button - const $language = chrome$('#languagemenu'); - const $languageoption = $language.find('[value=en]'); - - // select english - // select arabic - $languageoption.attr('selected', 'selected'); - $language.val('en'); - $languageoption.change(); - - await helper.waitForPromise(() => chrome$('html')[0].dir !== 'rtl'); - - // check if the document's direction was changed - expect(chrome$('html')[0].dir).to.be('ltr'); - }); -}); diff --git a/src/tests/frontend/specs/multiple_authors_clear_authorship_colors.js b/src/tests/frontend/specs/multiple_authors_clear_authorship_colors.js index e0c86fa43..1276f839c 100755 --- a/src/tests/frontend/specs/multiple_authors_clear_authorship_colors.js +++ b/src/tests/frontend/specs/multiple_authors_clear_authorship_colors.js @@ -33,7 +33,7 @@ describe('author of pad edition', function () { // get the clear authorship colors button and click it const $clearauthorshipcolorsButton = chrome$('.buttonicon-clearauthorship'); - $clearauthorshipcolorsButton.click(); + $clearauthorshipcolorsButton.trigger('click'); // does the first divs span include an author class? const hasAuthorClass = inner$('div span').first().attr('class').indexOf('author') !== -1; diff --git a/src/tests/frontend/specs/ordered_list.js b/src/tests/frontend/specs/ordered_list.js deleted file mode 100644 index 21331596f..000000000 --- a/src/tests/frontend/specs/ordered_list.js +++ /dev/null @@ -1,233 +0,0 @@ -'use strict'; - -describe('ordered_list.js', function () { - describe('assign ordered list', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('inserts ordered list text', async function () { - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist'); - $insertorderedlistButton.click(); - - await helper.waitForPromise(() => inner$('div').first().find('ol li').length === 1); - }); - - context('when user presses Ctrl+Shift+N', function () { - context('and pad shortcut is enabled', function () { - beforeEach(async function () { - const originalHTML = helper.padInner$('body').html(); - makeSureShortcutIsEnabled('cmdShiftN'); - triggerCtrlShiftShortcut('N'); - await helper.waitForPromise(() => helper.padInner$('body').html() !== originalHTML); - }); - - it('inserts unordered list', async function () { - await helper.waitForPromise( - () => helper.padInner$('div').first().find('ol li').length === 1); - }); - }); - - context('and pad shortcut is disabled', function () { - beforeEach(async function () { - const originalHTML = helper.padInner$('body').html(); - makeSureShortcutIsDisabled('cmdShiftN'); - triggerCtrlShiftShortcut('N'); - try { - // The HTML should not change. Briefly wait for it to change and fail if it does change. - await helper.waitForPromise( - () => helper.padInner$('body').html() !== originalHTML, 500); - } catch (err) { - // We want the test to pass if the above wait timed out. (If it timed out that - // means the HTML never changed, which is a good thing.) - // TODO: Re-throw non-"condition never became true" errors to avoid false positives. - } - // This will fail if the above `waitForPromise()` succeeded. - expect(helper.padInner$('body').html()).to.be(originalHTML); - }); - - it('does not insert unordered list', async function () { - this.timeout(3000); - try { - await helper.waitForPromise( - () => helper.padInner$('div').first().find('ol li').length === 1); - } catch (err) { - return; - } - expect().fail('Unordered list inserted, should ignore shortcut'); - }); - }); - }); - - context('when user presses Ctrl+Shift+1', function () { - context('and pad shortcut is enabled', function () { - beforeEach(async function () { - const originalHTML = helper.padInner$('body').html(); - makeSureShortcutIsEnabled('cmdShift1'); - triggerCtrlShiftShortcut('1'); - await helper.waitForPromise(() => helper.padInner$('body').html() !== originalHTML); - }); - - it('inserts unordered list', async function () { - helper.waitForPromise(() => helper.padInner$('div').first().find('ol li').length === 1); - }); - }); - - context('and pad shortcut is disabled', function () { - beforeEach(async function () { - const originalHTML = helper.padInner$('body').html(); - makeSureShortcutIsDisabled('cmdShift1'); - triggerCtrlShiftShortcut('1'); - try { - // The HTML should not change. Briefly wait for it to change and fail if it does change. - await helper.waitForPromise( - () => helper.padInner$('body').html() !== originalHTML, 500); - } catch (err) { - // We want the test to pass if the above wait timed out. (If it timed out that - // means the HTML never changed, which is a good thing.) - // TODO: Re-throw non-"condition never became true" errors to avoid false positives. - } - // This will fail if the above `waitForPromise()` succeeded. - expect(helper.padInner$('body').html()).to.be(originalHTML); - }); - - it('does not insert unordered list', async function () { - this.timeout(3000); - try { - await helper.waitForPromise( - () => helper.padInner$('div').first().find('ol li').length === 1); - } catch (err) { - return; - } - expect().fail('Unordered list inserted, should ignore shortcut'); - }); - }); - }); - - it('issue #4748 keeps numbers increment on OL', async function () { - this.timeout(5000); - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist'); - const $firstLine = inner$('div').first(); - $firstLine.sendkeys('{selectall}'); - $insertorderedlistButton.click(); - const $secondLine = inner$('div').first().next(); - $secondLine.sendkeys('{selectall}'); - $insertorderedlistButton.click(); - expect($secondLine.find('ol').attr('start') === 2); - }); - - xit('issue #1125 keeps the numbered list on enter for the new line', async function () { - // EMULATES PASTING INTO A PAD - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist'); - $insertorderedlistButton.click(); - - // type a bit, make a line break and type again - const $firstTextElement = inner$('div span').first(); - $firstTextElement.sendkeys('line 1'); - $firstTextElement.sendkeys('{enter}'); - $firstTextElement.sendkeys('line 2'); - $firstTextElement.sendkeys('{enter}'); - - await helper.waitForPromise(() => inner$('div span').first().text().indexOf('line 2') === -1); - - const $newSecondLine = inner$('div').first().next(); - const hasOLElement = $newSecondLine.find('ol li').length === 1; - expect(hasOLElement).to.be(true); - expect($newSecondLine.text()).to.be('line 2'); - const hasLineNumber = $newSecondLine.find('ol').attr('start') === 2; - // This doesn't work because pasting in content doesn't work - expect(hasLineNumber).to.be(true); - }); - - const triggerCtrlShiftShortcut = (shortcutChar) => { - const inner$ = helper.padInner$; - const e = new inner$.Event(helper.evtType); - e.ctrlKey = true; - e.shiftKey = true; - e.which = shortcutChar.toString().charCodeAt(0); - inner$('#innerdocbody').trigger(e); - }; - - const makeSureShortcutIsDisabled = (shortcut) => { - helper.padChrome$.window.clientVars.padShortcutEnabled[shortcut] = false; - }; - const makeSureShortcutIsEnabled = (shortcut) => { - helper.padChrome$.window.clientVars.padShortcutEnabled[shortcut] = true; - }; - }); - - describe('Pressing Tab in an OL increases and decreases indentation', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('indent and de-indent list item with keypress', async function () { - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - // get the first text element out of the inner iframe - const $firstTextElement = inner$('div').first(); - - // select this text element - $firstTextElement.sendkeys('{selectall}'); - - const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist'); - $insertorderedlistButton.click(); - - const e = new inner$.Event(helper.evtType); - e.keyCode = 9; // tab - inner$('#innerdocbody').trigger(e); - - expect(inner$('div').first().find('.list-number2').length === 1).to.be(true); - e.shiftKey = true; // shift - e.keyCode = 9; // tab - inner$('#innerdocbody').trigger(e); - - await helper.waitForPromise(() => inner$('div').first().find('.list-number1').length === 1); - }); - }); - - - describe('Pressing indent/outdent button in an OL increases and ' + - 'decreases indentation and bullet / ol formatting', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('indent and de-indent list item with indent button', async function () { - this.timeout(1000); - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - // get the first text element out of the inner iframe - const $firstTextElement = inner$('div').first(); - - // select this text element - $firstTextElement.sendkeys('{selectall}'); - - const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist'); - $insertorderedlistButton.click(); - - const $indentButton = chrome$('.buttonicon-indent'); - $indentButton.click(); // make it indented twice - - expect(inner$('div').first().find('.list-number2').length === 1).to.be(true); - - const $outdentButton = chrome$('.buttonicon-outdent'); - $outdentButton.click(); // make it deindented to 1 - - await helper.waitForPromise(() => inner$('div').first().find('.list-number1').length === 1); - }); - }); -}); diff --git a/src/tests/frontend/specs/pad_modal.js b/src/tests/frontend/specs/pad_modal.js index 735abe8c0..d2a53b7bc 100644 --- a/src/tests/frontend/specs/pad_modal.js +++ b/src/tests/frontend/specs/pad_modal.js @@ -71,16 +71,16 @@ describe('Pad modal', function () { const clickOnPadInner = () => { const $editor = helper.padInner$('#innerdocbody'); - $editor.click(); + $editor.trigger('click'); }; const clickOnPadOuter = () => { const $lineNumbersColumn = helper.padOuter$('#sidedivinner'); - $lineNumbersColumn.click(); + $lineNumbersColumn.trigger('click'); }; const openSettingsAndWaitForModalToBeVisible = async () => { - helper.padChrome$('.buttonicon-settings').click(); + helper.padChrome$('.buttonicon-settings').trigger('click'); // wait for modal to be displayed const modalSelector = '#settings'; diff --git a/src/tests/frontend/specs/redo.js b/src/tests/frontend/specs/redo.js deleted file mode 100644 index bbe85a7ea..000000000 --- a/src/tests/frontend/specs/redo.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict'; - -describe('undo button then redo button', function () { - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('redo some typing with button', async function () { - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - // get the first text element inside the editable space - const $firstTextElement = inner$('div span').first(); - const originalValue = $firstTextElement.text(); // get the original value - const newString = 'Foo'; - - $firstTextElement.sendkeys(newString); // send line 1 to the pad - const modifiedValue = $firstTextElement.text(); // get the modified value - expect(modifiedValue).not.to.be(originalValue); // expect the value to change - - // get undo and redo buttons - const $undoButton = chrome$('.buttonicon-undo'); - const $redoButton = chrome$('.buttonicon-redo'); - // click the buttons - $undoButton.click(); // removes foo - $redoButton.click(); // resends foo - - await helper.waitForPromise(() => inner$('div span').first().text() === newString); - const finalValue = inner$('div').first().text(); - expect(finalValue).to.be(modifiedValue); // expect the value to change - }); - - it('redo some typing with keypress', async function () { - const inner$ = helper.padInner$; - - // get the first text element inside the editable space - const $firstTextElement = inner$('div span').first(); - const originalValue = $firstTextElement.text(); // get the original value - const newString = 'Foo'; - - $firstTextElement.sendkeys(newString); // send line 1 to the pad - const modifiedValue = $firstTextElement.text(); // get the modified value - expect(modifiedValue).not.to.be(originalValue); // expect the value to change - - let e = new inner$.Event(helper.evtType); - e.ctrlKey = true; // Control key - e.which = 90; // z - inner$('#innerdocbody').trigger(e); - - e = new inner$.Event(helper.evtType); - e.ctrlKey = true; // Control key - e.which = 121; // y - inner$('#innerdocbody').trigger(e); - - await helper.waitForPromise(() => inner$('div span').first().text() === newString); - const finalValue = inner$('div').first().text(); - expect(finalValue).to.be(modifiedValue); // expect the value to change - }); -}); diff --git a/src/tests/frontend/specs/select_formatting_buttons.js b/src/tests/frontend/specs/select_formatting_buttons.js index 28921357d..67c93d636 100644 --- a/src/tests/frontend/specs/select_formatting_buttons.js +++ b/src/tests/frontend/specs/select_formatting_buttons.js @@ -13,7 +13,7 @@ describe('select formatting buttons when selection has style applied', function const chrome$ = helper.padChrome$; selectLine(line); const $formattingButton = chrome$(`.buttonicon-${style}`); - $formattingButton.click(); + $formattingButton.trigger('click'); }; const isButtonSelected = function (style) { @@ -37,7 +37,7 @@ describe('select formatting buttons when selection has style applied', function const undo = async function () { const originalHTML = helper.padInner$('body').html(); const $undoButton = helper.padChrome$('.buttonicon-undo'); - $undoButton.click(); + $undoButton.trigger('click'); await helper.waitForPromise(() => helper.padInner$('body').html() !== originalHTML); }; diff --git a/src/tests/frontend/specs/skiplist.js b/src/tests/frontend/specs/skiplist.js deleted file mode 100644 index 16b985615..000000000 --- a/src/tests/frontend/specs/skiplist.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; - -const SkipList = require('ep_etherpad-lite/static/js/skiplist'); - -describe('skiplist.js', function () { - it('rejects null keys', async function () { - const skiplist = new SkipList(); - for (const key of [undefined, null]) { - expect(() => skiplist.push({key})).to.throwError(); - } - }); - - it('rejects duplicate keys', async function () { - const skiplist = new SkipList(); - skiplist.push({key: 'foo'}); - expect(() => skiplist.push({key: 'foo'})).to.throwError(); - }); - - it('atOffset() returns last entry that touches offset', async function () { - const skiplist = new SkipList(); - const entries = []; - let nextId = 0; - const makeEntry = (width) => { - const entry = {key: `id${nextId++}`, width}; - entries.push(entry); - return entry; - }; - - skiplist.push(makeEntry(5)); - expect(skiplist.atOffset(4)).to.be(entries[0]); - expect(skiplist.atOffset(5)).to.be(entries[0]); - expect(() => skiplist.atOffset(6)).to.throwError(); - - skiplist.push(makeEntry(0)); - expect(skiplist.atOffset(4)).to.be(entries[0]); - expect(skiplist.atOffset(5)).to.be(entries[1]); - expect(() => skiplist.atOffset(6)).to.throwError(); - - skiplist.push(makeEntry(0)); - expect(skiplist.atOffset(4)).to.be(entries[0]); - expect(skiplist.atOffset(5)).to.be(entries[2]); - expect(() => skiplist.atOffset(6)).to.throwError(); - - skiplist.splice(2, 0, [makeEntry(0)]); - expect(skiplist.atOffset(4)).to.be(entries[0]); - expect(skiplist.atOffset(5)).to.be(entries[2]); - expect(() => skiplist.atOffset(6)).to.throwError(); - - skiplist.push(makeEntry(3)); - expect(skiplist.atOffset(4)).to.be(entries[0]); - expect(skiplist.atOffset(5)).to.be(entries[4]); - expect(skiplist.atOffset(6)).to.be(entries[4]); - }); -}); diff --git a/src/tests/frontend/specs/strikethrough.js b/src/tests/frontend/specs/strikethrough.js deleted file mode 100644 index 4c33244cd..000000000 --- a/src/tests/frontend/specs/strikethrough.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -describe('strikethrough button', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('makes text strikethrough', async function () { - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - // get the first text element out of the inner iframe - const $firstTextElement = inner$('div').first(); - - // select this text element - $firstTextElement.sendkeys('{selectall}'); - - // get the strikethrough button and click it - const $strikethroughButton = chrome$('.buttonicon-strikethrough'); - $strikethroughButton.click(); - - // ace creates a new dom element when you press a button, just get the first text element again - const $newFirstTextElement = inner$('div').first(); - - // is there a element now? - const isstrikethrough = $newFirstTextElement.find('s').length === 1; - - // expect it to be strikethrough - expect(isstrikethrough).to.be(true); - - // make sure the text hasn't changed - expect($newFirstTextElement.text()).to.eql($firstTextElement.text()); - }); -}); diff --git a/src/tests/frontend/specs/timeslider.js b/src/tests/frontend/specs/timeslider.js deleted file mode 100644 index 350965720..000000000 --- a/src/tests/frontend/specs/timeslider.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -// deactivated, we need a nice way to get the timeslider, this is ugly -xdescribe('timeslider button takes you to the timeslider of a pad', function () { - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('timeslider contained in URL', async function () { - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - // get the first text element inside the editable space - const $firstTextElement = inner$('div span').first(); - const originalValue = $firstTextElement.text(); // get the original value - $firstTextElement.sendkeys('Testing'); // send line 1 to the pad - - const modifiedValue = $firstTextElement.text(); // get the modified value - expect(modifiedValue).not.to.be(originalValue); // expect the value to change - - // The value has changed so we can.. - await helper.waitForPromise(() => modifiedValue !== originalValue); - - const $timesliderButton = chrome$('#timesliderlink'); - $timesliderButton.click(); // So click the timeslider link - - await helper.waitForPromise(() => { - const iFrameURL = chrome$.window.location.href; - if (iFrameURL) { - return iFrameURL.indexOf('timeslider') !== -1; - } else { - return false; // the URL hasnt been set yet - } - }); - - // click the buttons - const iFrameURL = chrome$.window.location.href; // get the url - const inTimeslider = iFrameURL.indexOf('timeslider') !== -1; - expect(inTimeslider).to.be(true); // expect the value to change - }); -}); diff --git a/src/tests/frontend/specs/timeslider_follow.js b/src/tests/frontend/specs/timeslider_follow.js deleted file mode 100644 index ffda2d286..000000000 --- a/src/tests/frontend/specs/timeslider_follow.js +++ /dev/null @@ -1,101 +0,0 @@ -'use strict'; - -describe('timeslider follow', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - // TODO needs test if content is also followed, when user a makes edits - // while user b is in the timeslider - it("content as it's added to timeslider", async function () { - this.timeout(20000); - // send 6 revisions - const revs = 6; - const message = 'a\n\n\n\n\n\n\n\n\n\n'; - const newLines = message.split('\n').length; - for (let i = 0; i < revs; i++) { - await helper.edit(message, newLines * i + 1); - } - - await helper.gotoTimeslider(0); - await helper.waitForPromise(() => helper.contentWindow().location.hash === '#0'); - - const originalTop = helper.contentWindow().$('#innerdocbody').offset(); - - // set to follow contents as it arrives - helper.contentWindow().$('#options-followContents').prop('checked', true); - helper.contentWindow().$('#playpause_button_icon').click(); - - let newTop; - await helper.waitForPromise(() => { - newTop = helper.contentWindow().$('#innerdocbody').offset(); - return newTop.top < originalTop.top; - }); - }); - - /** - * Tests for bug described in #4389 - * The goal is to scroll to the first line that contains a change right before - * the change is applied. - */ - it('only to lines that exist in the pad view, regression test for #4389', async function () { - await helper.clearPad(); - await helper.edit('Test line\n' + - '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' + - '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' + - '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'); - await helper.edit('Another test line', 40); - - - await helper.gotoTimeslider(); - - // set to follow contents as it arrives - helper.contentWindow().$('#options-followContents').prop('checked', true); - - const oldYPosition = helper.contentWindow().$('#editorcontainerbox')[0].scrollTop; - expect(oldYPosition).to.be(0); - - /** - * pad content rev 0 [default Pad text] - * pad content rev 1 [''] - * pad content rev 2 ['Test line','','', ..., ''] - * pad content rev 3 ['Test line','',..., 'Another test line', ..., ''] - */ - - // line 40 changed - helper.contentWindow().$('#leftstep').click(); - await helper.waitForPromise(() => hasFollowedToLine(40)); - - // line 1 is the first line that changed - helper.contentWindow().$('#leftstep').click(); - await helper.waitForPromise(() => hasFollowedToLine(1)); - - // line 1 changed - helper.contentWindow().$('#leftstep').click(); - await helper.waitForPromise(() => hasFollowedToLine(1)); - - // line 1 changed - helper.contentWindow().$('#rightstep').click(); - await helper.waitForPromise(() => hasFollowedToLine(1)); - - // line 1 is the first line that changed - helper.contentWindow().$('#rightstep').click(); - await helper.waitForPromise(() => hasFollowedToLine(1)); - - // line 40 changed - helper.contentWindow().$('#rightstep').click(); - helper.waitForPromise(() => hasFollowedToLine(40)); - }); -}); - -/** - * @param {number} lineNum - * @returns {boolean} scrolled to the lineOffset? - */ -const hasFollowedToLine = (lineNum) => { - const scrollPosition = helper.contentWindow().$('#editorcontainerbox')[0].scrollTop; - const lineOffset = - helper.contentWindow().$('#innerdocbody').find(`div:nth-child(${lineNum})`)[0].offsetTop; - return Math.abs(scrollPosition - lineOffset) < 1; -}; diff --git a/src/tests/frontend/specs/timeslider_revisions.js b/src/tests/frontend/specs/timeslider_revisions.js index 8c4f07458..c2de15c42 100644 --- a/src/tests/frontend/specs/timeslider_revisions.js +++ b/src/tests/frontend/specs/timeslider_revisions.js @@ -12,7 +12,7 @@ describe('timeslider', function () { // Create a bunch of revisions. for (let i = 0; i < 99; i++) await helper.edit('a'); - chrome$('.buttonicon-savedRevision').click(); + chrome$('.buttonicon-savedRevision').trigger('click'); await helper.waitForPromise(() => helper.padChrome$('.saved-revision').length > 0); // Give some time to send the SAVE_REVISION message to the server before navigating away. await new Promise((resolve) => setTimeout(resolve, 100)); diff --git a/src/tests/frontend/specs/undo.js b/src/tests/frontend/specs/undo.js deleted file mode 100644 index d853eaf7b..000000000 --- a/src/tests/frontend/specs/undo.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; - -describe('undo button', function () { - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('undo some typing by clicking undo button', async function () { - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - // get the first text element inside the editable space - const $firstTextElement = inner$('div span').first(); - const originalValue = $firstTextElement.text(); // get the original value - - $firstTextElement.sendkeys('foo'); // send line 1 to the pad - const modifiedValue = $firstTextElement.text(); // get the modified value - expect(modifiedValue).not.to.be(originalValue); // expect the value to change - - // get clear authorship button as a variable - const $undoButton = chrome$('.buttonicon-undo'); - // click the button - $undoButton.click(); - - await helper.waitForPromise(() => inner$('div span').first().text() === originalValue); - }); - - it('undo some typing using a keypress', async function () { - const inner$ = helper.padInner$; - - // get the first text element inside the editable space - const $firstTextElement = inner$('div span').first(); - const originalValue = $firstTextElement.text(); // get the original value - - $firstTextElement.sendkeys('foo'); // send line 1 to the pad - const modifiedValue = $firstTextElement.text(); // get the modified value - expect(modifiedValue).not.to.be(originalValue); // expect the value to change - - const e = new inner$.Event(helper.evtType); - e.ctrlKey = true; // Control key - e.which = 90; // z - inner$('#innerdocbody').trigger(e); - - await helper.waitForPromise(() => inner$('div span').first().text() === originalValue); - }); -}); diff --git a/src/tests/frontend/specs/unordered_list.js b/src/tests/frontend/specs/unordered_list.js deleted file mode 100644 index 9e5439a77..000000000 --- a/src/tests/frontend/specs/unordered_list.js +++ /dev/null @@ -1,146 +0,0 @@ -'use strict'; - -describe('unordered_list.js', function () { - describe('assign unordered list', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('insert unordered list text then removes by outdent', async function () { - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - const originalText = inner$('div').first().text(); - - const $insertunorderedlistButton = chrome$('.buttonicon-insertunorderedlist'); - $insertunorderedlistButton.click(); - - await helper.waitForPromise(() => { - const newText = inner$('div').first().text(); - return newText === originalText && inner$('div').first().find('ul li').length === 1; - }); - - // remove indentation by bullet and ensure text string remains the same - chrome$('.buttonicon-outdent').click(); - await helper.waitForPromise(() => inner$('div').first().text() === originalText); - }); - }); - - describe('unassign unordered list', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('insert unordered list text then remove by clicking list again', async function () { - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - const originalText = inner$('div').first().text(); - - const $insertunorderedlistButton = chrome$('.buttonicon-insertunorderedlist'); - $insertunorderedlistButton.click(); - - await helper.waitForPromise(() => { - const newText = inner$('div').first().text(); - return newText === originalText && inner$('div').first().find('ul li').length === 1; - }); - - // remove indentation by bullet and ensure text string remains the same - $insertunorderedlistButton.click(); - await helper.waitForPromise(() => inner$('div').find('ul').length !== 1); - }); - }); - - - describe('keep unordered list on enter key', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('Keeps the unordered list on enter for the new line', async function () { - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - const $insertorderedlistButton = chrome$('.buttonicon-insertunorderedlist'); - $insertorderedlistButton.click(); - - // type a bit, make a line break and type again - const $firstTextElement = inner$('div span').first(); - $firstTextElement.sendkeys('line 1'); - $firstTextElement.sendkeys('{enter}'); - $firstTextElement.sendkeys('line 2'); - $firstTextElement.sendkeys('{enter}'); - - await helper.waitForPromise(() => inner$('div span').first().text().indexOf('line 2') === -1); - - const $newSecondLine = inner$('div').first().next(); - const hasULElement = $newSecondLine.find('ul li').length === 1; - expect(hasULElement).to.be(true); - expect($newSecondLine.text()).to.be('line 2'); - }); - }); - - describe('Pressing Tab in an UL increases and decreases indentation', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('indent and de-indent list item with keypress', async function () { - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - // get the first text element out of the inner iframe - const $firstTextElement = inner$('div').first(); - - // select this text element - $firstTextElement.sendkeys('{selectall}'); - - const $insertorderedlistButton = chrome$('.buttonicon-insertunorderedlist'); - $insertorderedlistButton.click(); - - const e = new inner$.Event(helper.evtType); - e.keyCode = 9; // tab - inner$('#innerdocbody').trigger(e); - - expect(inner$('div').first().find('.list-bullet2').length === 1).to.be(true); - e.shiftKey = true; // shift - e.keyCode = 9; // tab - inner$('#innerdocbody').trigger(e); - - await helper.waitForPromise(() => inner$('div').first().find('.list-bullet1').length === 1); - }); - }); - - describe('Pressing indent/outdent button in an UL increases and decreases indentation ' + - 'and bullet / ol formatting', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('indent and de-indent list item with indent button', async function () { - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - // get the first text element out of the inner iframe - const $firstTextElement = inner$('div').first(); - - // select this text element - $firstTextElement.sendkeys('{selectall}'); - - const $insertunorderedlistButton = chrome$('.buttonicon-insertunorderedlist'); - $insertunorderedlistButton.click(); - - const $indentButton = chrome$('.buttonicon-indent'); - $indentButton.click(); // make it indented twice - - expect(inner$('div').first().find('.list-bullet2').length === 1).to.be(true); - const $outdentButton = chrome$('.buttonicon-outdent'); - $outdentButton.click(); // make it deindented to 1 - - await helper.waitForPromise(() => inner$('div').first().find('.list-bullet1').length === 1); - }); - }); -}); diff --git a/src/tests/frontend/specs/urls_become_clickable.js b/src/tests/frontend/specs/urls_become_clickable.js deleted file mode 100644 index bb3f00b34..000000000 --- a/src/tests/frontend/specs/urls_become_clickable.js +++ /dev/null @@ -1,56 +0,0 @@ -'use strict'; - -describe('urls', function () { - // Returns the first text element. Note that any change to the text element will result in the - // element being replaced with another object. - const txt = () => helper.padInner$('div').first(); - - before(async function () { - await helper.aNewPad(); - }); - - beforeEach(async function () { - await helper.clearPad(); - }); - - describe('entering a URL makes a link', function () { - for (const url of ['https://etherpad.org', 'www.etherpad.org']) { - it(url, async function () { - this.timeout(5000); - const url = 'https://etherpad.org'; - await helper.edit(url); - await helper.waitForPromise(() => txt().find('a').length === 1, 2000); - const link = txt().find('a'); - expect(link.attr('href')).to.be(url); - expect(link.text()).to.be(url); - }); - } - }); - - describe('special characters inside URL', function () { - for (const char of '-:@_.,~%+/?=&#!;()[]$\'*') { - const url = `https://etherpad.org/${char}foo`; - it(url, async function () { - await helper.edit(url); - await helper.waitForPromise(() => txt().find('a').length === 1); - const link = txt().find('a'); - expect(link.attr('href')).to.be(url); - expect(link.text()).to.be(url); - }); - } - }); - - describe('punctuation after URL is ignored', function () { - for (const char of ':.,;?!)]\'*') { - const want = 'https://etherpad.org'; - const input = want + char; - it(input, async function () { - await helper.edit(input); - await helper.waitForPromise(() => txt().find('a').length === 1); - const link = txt().find('a'); - expect(link.attr('href')).to.be(want); - expect(link.text()).to.be(want); - }); - } - }); -}); diff --git a/src/tests/frontend/specs/xxauto_reconnect.js b/src/tests/frontend/specs/xxauto_reconnect.js index c04ca9d29..b8b3e73f7 100644 --- a/src/tests/frontend/specs/xxauto_reconnect.js +++ b/src/tests/frontend/specs/xxauto_reconnect.js @@ -32,7 +32,7 @@ describe('Automatic pad reload on Force Reconnect message', function () { context('and user clicks on Cancel', function () { beforeEach(async function () { const $errorMessageModal = helper.padChrome$('#connectivity .userdup'); - $errorMessageModal.find('#cancelreconnect').click(); + $errorMessageModal.find('#cancelreconnect').trigger('click'); await helper.waitForPromise( () => helper.padChrome$('#connectivity .userdup').is(':visible') === true); }); diff --git a/src/tests/frontend/travis/adminrunner.sh b/src/tests/frontend/travis/adminrunner.sh index 9ed6d5e74..32fd12a63 100755 --- a/src/tests/frontend/travis/adminrunner.sh +++ b/src/tests/frontend/travis/adminrunner.sh @@ -11,8 +11,8 @@ MY_DIR=$(try cd "${0%/*}" && try pwd -P) || exit 1 try cd "${MY_DIR}/../../../.." log "Assuming src/bin/installDeps.sh has already been run" -node src/node/server.js --experimental-worker "${@}" & -ep_pid=$! +( cd src && npm run dev --experimental-worker "${@}" & +ep_pid=$!) log "Waiting for Etherpad to accept connections (http://localhost:9001)..." connected=false diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index 74aefa62e..331e38568 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -93,12 +93,11 @@ const sauceTestWorker = async.queue(async ({name, pfx, browser, version, platfor }, 6); // run 6 tests in parrallel Promise.all([ - {browser: 'safari', version: 'latest', platform: 'macOS 11.00'}, + {browser: 'chrome', version: 'latest', platform: 'Windows 10'}, ...(isAdminRunner ? [] : [ + {browser: 'safari', version: 'latest', platform: 'macOS 11.00'}, {browser: 'firefox', version: 'latest', platform: 'Windows 10'}, {browser: 'MicrosoftEdge', version: 'latest', platform: 'Windows 10'}, - {browser: 'chrome', version: 'latest', platform: 'Windows 10'}, - {browser: 'chrome', version: '55.0', platform: 'Windows 7'}, ]), ].map(async ({browser, version, platform}) => { const name = `${browser} ${version}, ${platform}`; diff --git a/src/tests/frontend/travis/runner.sh b/src/tests/frontend/travis/runner.sh index 23bc13ed7..c2c2907e3 100755 --- a/src/tests/frontend/travis/runner.sh +++ b/src/tests/frontend/travis/runner.sh @@ -10,9 +10,9 @@ try() { "$@" || fatal "'$@' failed"; } MY_DIR=$(try cd "${0%/*}" && try pwd -P) || exit 1 try cd "${MY_DIR}/../../../.." -log "Assuming src/bin/installDeps.sh has already been run" -node src/node/server.js --experimental-worker "${@}" & -ep_pid=$! +log "Assuming bin/installDeps.sh has already been run" +(cd src && npm run dev --experimental-worker "${@}" & +ep_pid=$!) log "Waiting for Etherpad to accept connections (http://localhost:9001)..." connected=false diff --git a/src/tests/frontend/travis/runnerBackend.sh b/src/tests/frontend/travis/runnerBackend.sh index f12ff25c1..518e77872 100755 --- a/src/tests/frontend/travis/runnerBackend.sh +++ b/src/tests/frontend/travis/runnerBackend.sh @@ -19,8 +19,8 @@ s!"points":[^,]*!"points": 1000! log "Deprecation notice: runnerBackend.sh - Please use: cd src && npm test" log "Assuming src/bin/installDeps.sh has already been run" -node src/node/server.js "${@}" & -ep_pid=$! +(cd src && npm run dev "${@}" & +ep_pid=$!) log "Waiting for Etherpad to accept connections (http://localhost:9001)..." connected=false diff --git a/src/tests/frontend/travis/runnerLoadTest.sh b/src/tests/frontend/travis/runnerLoadTest.sh index 8a813c20a..250d01f19 100755 --- a/src/tests/frontend/travis/runnerLoadTest.sh +++ b/src/tests/frontend/travis/runnerLoadTest.sh @@ -1,15 +1,22 @@ #!/bin/sh +set -e + pecho() { printf %s\\n "$*"; } log() { pecho "$@"; } error() { log "ERROR: $@" >&2; } fatal() { error "$@"; exit 1; } try() { "$@" || fatal "'$@' failed"; } +[ -n "$1" ] && [ "$1" -gt 0 ] || fatal "no duration specified" +[ -n "$2" ] && [ "$2" -gt 0 ] || fatal "no authors specified" + # Move to the Etherpad base directory. MY_DIR=$(try cd "${0%/*}" && try pwd -P) || exit 1 try cd "${MY_DIR}/../../../.." +sed -e '/^ *"importExportRateLimiting":/,/^ *\}/ s/"max":.*/"max": 100000000/' -i settings.json.template + try sed -e ' s!"loadTest":[^,]*!"loadTest": true! # Reduce rate limit aggressiveness @@ -17,8 +24,8 @@ s!"points":[^,]*!"points": 1000! ' settings.json.template >settings.json log "Assuming src/bin/installDeps.sh has already been run" -node src/node/server.js "${@}" >/dev/null & -ep_pid=$! +(cd src && pnpm run prod & +ep_pid=$!) log "Waiting for Etherpad to accept connections (http://localhost:9001)..." connected=false @@ -28,7 +35,7 @@ can_connect() { } now() { date +%s; } start=$(now) -while [ $(($(now) - $start)) -le 15 ] && ! can_connect; do +while [ $(($(now) - $start)) -le 60 ] && ! can_connect; do sleep 1 done [ "$connected" = true ] \ @@ -44,7 +51,7 @@ sleep 10 log "Running the load tests..." # -d is duration of test, -a is number of authors to test with # by specifying the number of authors we set the overall rate of messages -etherpad-loadtest -d $1 -a $2 +etherpad-loadtest -d "$1" -a "$2" exit_code=$? kill "$ep_pid" && wait "$ep_pid" diff --git a/src/tests/ratelimit/Dockerfile.anotherip b/src/tests/ratelimit/Dockerfile.anotherip index 57f02f628..c352b4af1 100644 --- a/src/tests/ratelimit/Dockerfile.anotherip +++ b/src/tests/ratelimit/Dockerfile.anotherip @@ -1,4 +1,4 @@ -FROM node:alpine3.12 +FROM node:latest WORKDIR /tmp RUN npm i etherpad-cli-client COPY ./src/tests/ratelimit/send_changesets.js /tmp/send_changesets.js diff --git a/src/tests/settings.json b/src/tests/settings.json new file mode 100644 index 000000000..9fef0fa19 --- /dev/null +++ b/src/tests/settings.json @@ -0,0 +1 @@ +{"title":"Etherpad","favicon":null,"skinName":"colibris","skinVariants":"super-light-toolbar super-light-editor light-background","ip":"0.0.0.0","port":9001,"showSettingsInAdminPage":true,"dbType":"dirty","dbSettings":{"filename":"var/dirty.db"},"defaultPadText":"Welcome to Etherpad!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nGet involved with Etherpad at https://etherpad.org\n","padOptions":{"noColors":false,"showControls":true,"showChat":true,"showLineNumbers":true,"useMonospaceFont":false,"userName":null,"userColor":null,"rtl":false,"alwaysShowChat":false,"chatAndUsers":false,"lang":null},"padShortcutEnabled":{"altF9":true,"altC":true,"cmdShift2":true,"delete":true,"return":true,"esc":true,"cmdS":true,"tab":true,"cmdZ":true,"cmdY":true,"cmdI":true,"cmdB":true,"cmdU":true,"cmd5":true,"cmdShiftL":true,"cmdShiftN":true,"cmdShift1":true,"cmdShiftC":true,"cmdH":true,"ctrlHome":true,"pageUp":true,"pageDown":true},"suppressErrorsInPadText":false,"requireSession":false,"editOnly":false,"minify":true,"maxAge":21600,"abiword":null,"soffice":null,"allowUnknownFileEnds":true,"requireAuthentication":false,"requireAuthorization":false,"trustProxy":false,"cookie":{"keyRotationInterval":86400000,"sameSite":"Lax","sessionLifetime":864000000,"sessionRefreshInterval":86400000},"disableIPlogging":false,"automaticReconnectionTimeout":0,"scrollWhenFocusLineIsOutOfViewport":{"percentage":{"editionAboveViewport":0,"editionBelowViewport":0},"duration":0,"scrollWhenCaretIsInTheLastLineOfViewport":false,"percentageToScrollWhenUserPressesArrowUp":0},"users":{"admin":{"password":"changeme1","is_admin":true},"user":{"password":"changeme1","is_admin":false}},"socketTransportProtocols":["websocket","polling"],"socketIo":{"maxHttpBufferSize":1000000},"loadTest":false,"dumpOnUncleanExit":false,"importExportRateLimiting":{"windowMs":90000,"max":10},"importMaxFileSize":52428800,"commitRateLimiting":{"duration":1,"points":10},"exposeVersion":false,"loglevel":"INFO","customLocaleStrings":{},"enableAdminUITests":true,"lowerCasePadIds":false,"sso":{"issuer":"${SSO_ISSUER:http://localhost:9001}","clients":[{"client_id":"${ADMIN_CLIENT:admin_client}","client_secret":"${ADMIN_SECRET:admin}","grant_types":["authorization_code"],"response_types":["code"],"redirect_uris":["${ADMIN_REDIRECT:http://localhost:9001/admin/}","https://oauth.pstmn.io/v1/callback"]},{"client_id":"${USER_CLIENT:user_client}","client_secret":"${USER_SECRET:user}","grant_types":["authorization_code"],"response_types":["code"],"redirect_uris":["${USER_REDIRECT:http://localhost:9001/}"]}]}} \ No newline at end of file diff --git a/src/tsconfig.json b/src/tsconfig.json new file mode 100644 index 000000000..a42ef0188 --- /dev/null +++ b/src/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + "moduleDetection": "force", + "lib": ["ES2023"], + /* Language and Environment */ + "target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + /* Modules */ + "module": "CommonJS", /* Specify what module code is generated. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + /* Completeness */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */, + "resolveJsonModule": true + } +} diff --git a/src/vitest.config.ts b/src/vitest.config.ts new file mode 100644 index 000000000..c47c424cc --- /dev/null +++ b/src/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ["tests/backend-new/specs/**/*.ts"], + }, +}) diff --git a/src/web.config b/src/web.config index bd50a60c5..65f2cf03f 100644 --- a/src/web.config +++ b/src/web.config @@ -2,7 +2,7 @@ - + @@ -13,7 +13,7 @@ - + --> @@ -23,7 +23,7 @@ - + diff --git a/start.bat b/start.bat index 7e9264ee3..bf8f1b23d 100644 --- a/start.bat +++ b/start.bat @@ -8,4 +8,5 @@ REM around this, everything must consistently use either `src` or REM `node_modules\ep_etherpad-lite` on Windows. Because some plugins access REM Etherpad internals via `require('ep_etherpad-lite/foo')`, REM `node_modules\ep_etherpad-lite` is used here. -node node_modules\ep_etherpad-lite\node\server.js +cd src +pnpm run prod diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/ui/consent.html b/ui/consent.html new file mode 100644 index 000000000..502b95a2e --- /dev/null +++ b/ui/consent.html @@ -0,0 +1,24 @@ + + + + + + + Consent Etherpad + + +
    + +
    + + + diff --git a/ui/login.html b/ui/login.html new file mode 100644 index 000000000..0ff588363 --- /dev/null +++ b/ui/login.html @@ -0,0 +1,38 @@ + + + + + + + SSO Etherpad + + +
    + +
    + + + diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 000000000..f5cb300e7 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,17 @@ +{ + "name": "ui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "build-copy": "tsc && vite build --outDir ../src/static/oidc --emptyOutDir" + }, + "devDependencies": { + "ep_etherpad-lite": "workspace:../src", + "typescript": "^5.8.2", + "vite": "^6.3.5" + } +} diff --git a/ui/pad.html b/ui/pad.html new file mode 100644 index 000000000..e11541943 --- /dev/null +++ b/ui/pad.html @@ -0,0 +1,686 @@ + + + + + + Etherpad + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + +
    + + + +
    + + + + + + + +
    + +
    + +
    +

    + You do not have permission to access this pad +

    +
    + + +

    +
    + Loading... +

    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + +
    + + + 0 +
    + +
    +
    +
    +

    + + █   +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    + + + + + + + + + + +
    + + + + + + + + + + + + + + diff --git a/ui/src/consent.ts b/ui/src/consent.ts new file mode 100644 index 000000000..79f6214fe --- /dev/null +++ b/ui/src/consent.ts @@ -0,0 +1,35 @@ +import "./style.css" +//import {MapArrayType} from "ep_etherpad-lite/node/types/MapType"; + +const form = document.querySelector('form')!; +const sessionId = new URLSearchParams(window.location.search).get('state'); + +form.action = '/interaction/' + sessionId; + +/*form.addEventListener('submit', function (event) { + event.preventDefault(); + const formData = new FormData(form); + const data: MapArrayType = {}; + formData.forEach((value, key) => { + data[key] = value; + }); + const sessionId = new URLSearchParams(window.location.search).get('state'); + + fetch('/interaction/' + sessionId, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }).then(response => { + if (response.ok) { + if (response.redirected) { + window.location.href = response.url; + } + } else { + document.getElementById('error')!.innerText = "Error signing in"; + } + }).catch(error => { + document.getElementById('error')!.innerText = "Error signing in" + error; + }) +});*/ diff --git a/ui/src/main.ts b/ui/src/main.ts new file mode 100644 index 000000000..1ff174cdb --- /dev/null +++ b/ui/src/main.ts @@ -0,0 +1,58 @@ +import './style.css' +import {MapArrayType} from "ep_etherpad-lite/node/types/MapType.ts"; + +const searchParams = new URLSearchParams(window.location.search); + + +document.getElementById('client')!.innerText = searchParams.get('client_id')!; + +const form = document.querySelector('form')!; +form.addEventListener('submit', function (event) { + event.preventDefault(); + const formData = new FormData(form); + const data: MapArrayType = {}; + formData.forEach((value, key) => { + data[key] = value; + }); + const sessionId = new URLSearchParams(window.location.search).get('state'); + + fetch('/interaction/' + sessionId, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + redirect: 'follow', + body: JSON.stringify(data), + }).then(response => { + if (response.ok) { + if (response.redirected) { + window.location.href = response.url; + } + } else { + document.getElementById('error')!.innerText = "Error signing in"; + } + }).catch(error => { + document.getElementById('error')!.innerText = "Error signing in" + error; + }) +}); + +const hidePassword = document.querySelector('.toggle-password-visibility')! as HTMLElement +const showPassword = document.getElementById('eye-hide')! as HTMLElement +const togglePasswordVisibility = () => { + const passwordInput = document.getElementsByName('password')[0] as HTMLInputElement; + if (passwordInput.type === 'password') { + showPassword.style.display = 'block'; + hidePassword.style.display = 'none'; + passwordInput.type = 'text'; + } else { + showPassword.style.display = 'none'; + hidePassword.style.display = 'block'; + passwordInput.type = 'password'; + } +} + + +hidePassword.addEventListener('click', togglePasswordVisibility); +showPassword.addEventListener('click', togglePasswordVisibility); + + diff --git a/ui/src/style.css b/ui/src/style.css new file mode 100644 index 000000000..2e4621d15 --- /dev/null +++ b/ui/src/style.css @@ -0,0 +1,125 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + --color-etherpad: #0f775b; +} + +body { + font-size: 16px; + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +#app { + max-width: 1280px; + margin: auto; + padding: 2rem; +} + + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} + +button:hover { + border-color: #646cff; +} + +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + + a:hover { + color: #747bff; + } + + button { + background-color: #f9f9f9; + } +} + +.login-box { + background-color: #f2f6f7; + padding: 40px; + border-radius: 20px; + color: #607278; +} + +body { + background: radial-gradient(100% 100% at 50% 0%, var(--color-etherpad) 0%, #003A47 100%) fixed +} + +input { + border-radius: 8px; + border: 1px solid #d1d1d1; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #f9f9f9; + transition: border-color 0.25s; +} + +.login-inner-box { + display: flex; + flex-direction: column; + gap: 10px; +} + +.login-inner-box input[type=submit] { + background-color: var(--color-etherpad); + color: white; + border: none; + cursor: pointer; + margin-top: 20px; +} + +.password-label { + position: relative; +} + +.password-label svg { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + cursor: pointer; + width: 16px; +} + +#eye-hide { + display: none; +} + +label { + display: flex; +} + +label input { + flex-grow: 1; +} diff --git a/ui/src/typescript.svg b/ui/src/typescript.svg new file mode 100644 index 000000000..d91c910cc --- /dev/null +++ b/ui/src/typescript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/src/vite-env.d.ts b/ui/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/ui/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 000000000..75abdef26 --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/ui/vite.config.ts b/ui/vite.config.ts new file mode 100644 index 000000000..89667286b --- /dev/null +++ b/ui/vite.config.ts @@ -0,0 +1,47 @@ +// vite.config.js +import { resolve } from 'path' +import { defineConfig } from 'vite' + +export default defineConfig({ + base: '/views/', + build: { + commonjsOptions:{ + transformMixedEsModules: true, + }, + outDir: resolve(__dirname, '../src/static/oidc'), + rollupOptions: { + input: { + main: resolve(__dirname, 'consent.html'), + nested: resolve(__dirname, 'login.html'), + }, + }, + emptyOutDir: true, + }, + server:{ + proxy:{ + '/static':{ + target: 'http://localhost:9001', + changeOrigin: true, + secure: false, + }, + '/views/manifest.json':{ + target: 'http://localhost:9001', + changeOrigin: true, + secure: false, + rewrite: (path) => path.replace(/^\/views/, ''), + }, + '/locales.json':{ + target: 'http://localhost:9001', + changeOrigin: true, + secure: false, + rewrite: (path) => path.replace(/^\/views/, ''), + }, + '/locales':{ + target: 'http://localhost:9001', + changeOrigin: true, + secure: false, + rewrite: (path) => path.replace(/^\/views/, ''), + }, + } + } +}) diff --git a/var/.gitignore b/var/.gitignore index 91f8c0959..d75cb9e42 100644 --- a/var/.gitignore +++ b/var/.gitignore @@ -1,2 +1,5 @@ sqlite.db minified* +installed_plugins.json +dirty.db +rusty.db