diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml index e50491d23..7ab58c410 100644 --- a/.github/workflows/backend-tests.yml +++ b/.github/workflows/backend-tests.yml @@ -17,6 +17,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 12 + - name: Install libreoffice run: | sudo add-apt-repository -y ppa:libreoffice/ppa @@ -24,11 +28,11 @@ jobs: sudo apt install -y --no-install-recommends libreoffice libreoffice-pdfimport - name: Install all dependencies and symlink for ep_etherpad-lite - run: bin/installDeps.sh + run: src/bin/installDeps.sh # configures some settings and runs npm run test - name: Run the backend tests - run: tests/frontend/travis/runnerBackend.sh + run: src/tests/frontend/travis/runnerBackend.sh withplugins: # run on pushes to any branch @@ -43,18 +47,43 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 12 + - 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: bin/installDeps.sh + - name: Install Etherpad plugins + run: > + npm install + 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 - - name: Install etherpad plugins - run: npm install ep_align ep_author_hover ep_cursortrace ep_font_size ep_hash_auth ep_headings2 ep_markdown ep_readonly_guest 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 the backend tests - run: tests/frontend/travis/runnerBackend.sh + run: src/tests/frontend/travis/runnerBackend.sh diff --git a/.github/workflows/dockerfile.yml b/.github/workflows/dockerfile.yml index 8f6d5c3b0..5f8384705 100644 --- a/.github/workflows/dockerfile.yml +++ b/.github/workflows/dockerfile.yml @@ -21,6 +21,6 @@ jobs: run: | docker build -t etherpad:test . docker run -d -p 9001:9001 etherpad:test - ./bin/installDeps.sh + ./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 new file mode 100644 index 000000000..e42aa3bb2 --- /dev/null +++ b/.github/workflows/frontend-admin-tests.yml @@ -0,0 +1,58 @@ +name: "Frontend admin tests" + +on: [push] + +jobs: + withplugins: + 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: Run sauce-connect-action + shell: bash + env: + SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }} + run: src/tests/frontend/travis/sauce_tunnel.sh + + - name: Install all dependencies and symlink for ep_etherpad-lite + run: src/bin/installDeps.sh + + # We intentionally install a much old ep_align version to test update minor versions + - name: Install etherpad plugins + run: npm install ep_align@0.2.27 + + # 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: Write custom settings.json with loglevel WARN + run: "sed 's/\"loglevel\": \"INFO\",/\"loglevel\": \"WARN\",/' < 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: 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 + + - name: Run the frontend admin tests + shell: bash + env: + SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }} + GIT_HASH: ${{ steps.environment.outputs.sha_short }} + run: | + src/tests/frontend/travis/adminrunner.sh diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index 3b178622e..00c8dd6b5 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -11,16 +11,20 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 12 + - name: Run sauce-connect-action shell: bash env: SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }} - run: tests/frontend/travis/sauce_tunnel.sh + run: src/tests/frontend/travis/sauce_tunnel.sh - name: Install all dependencies and symlink for ep_etherpad-lite - run: bin/installDeps.sh + run: src/bin/installDeps.sh - name: export GIT_HASH to env id: environment @@ -37,7 +41,7 @@ jobs: TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }} GIT_HASH: ${{ steps.environment.outputs.sha_short }} run: | - tests/frontend/travis/runner.sh + src/tests/frontend/travis/runner.sh withplugins: name: with plugins @@ -47,19 +51,44 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 12 + - name: Run sauce-connect-action shell: bash env: SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }} - run: tests/frontend/travis/sauce_tunnel.sh + run: src/tests/frontend/travis/sauce_tunnel.sh + - name: Install Etherpad plugins + run: > + npm install + 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: bin/installDeps.sh - - - name: Install etherpad plugins - run: npm install ep_align ep_author_hover ep_cursortrace ep_font_size ep_hash_auth ep_headings2 ep_markdown ep_readonly_guest ep_spellcheck ep_subscript_and_superscript ep_table_of_contents ep_set_title_on_pad + run: src/bin/installDeps.sh - name: export GIT_HASH to env id: environment @@ -68,9 +97,12 @@ jobs: - name: Write custom settings.json with loglevel WARN run: "sed 's/\"loglevel\": \"INFO\",/\"loglevel\": \"WARN\",/' < 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" + # 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 tests/frontend/specs/* + run: rm src/tests/frontend/specs/* - name: Run the frontend tests shell: bash @@ -80,4 +112,4 @@ jobs: TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }} GIT_HASH: ${{ steps.environment.outputs.sha_short }} run: | - tests/frontend/travis/runner.sh + src/tests/frontend/travis/runner.sh diff --git a/.github/workflows/lint-package-lock.yml b/.github/workflows/lint-package-lock.yml index beef64ffe..a9596aa3c 100644 --- a/.github/workflows/lint-package-lock.yml +++ b/.github/workflows/lint-package-lock.yml @@ -17,6 +17,10 @@ jobs: - 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 diff --git a/.github/workflows/load-test.yml b/.github/workflows/load-test.yml index 095adc785..98379dfe8 100644 --- a/.github/workflows/load-test.yml +++ b/.github/workflows/load-test.yml @@ -17,14 +17,18 @@ jobs: - 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: bin/installDeps.sh + run: src/bin/installDeps.sh - name: Install etherpad-load-test run: sudo npm install -g etherpad-load-test - name: Run load test - run: tests/frontend/travis/runnerLoadTest.sh + run: src/tests/frontend/travis/runnerLoadTest.sh withplugins: # run on pushes to any branch @@ -39,15 +43,39 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 - - name: Install all dependencies and symlink for ep_etherpad-lite - run: bin/installDeps.sh + - 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 - run: npm install ep_align ep_author_hover ep_cursortrace ep_font_size ep_hash_auth ep_headings2 ep_markdown ep_readonly_guest ep_spellcheck ep_subscript_and_superscript ep_table_of_contents + run: > + npm install + 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: tests/frontend/travis/runnerLoadTest.sh + run: src/tests/frontend/travis/runnerLoadTest.sh diff --git a/.github/workflows/rate-limit.yml b/.github/workflows/rate-limit.yml index 4bdfc2194..0849f8e06 100644 --- a/.github/workflows/rate-limit.yml +++ b/.github/workflows/rate-limit.yml @@ -16,14 +16,18 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 + - 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 tests/ratelimit/Dockerfile.nginx -t nginx-latest . - docker build -f tests/ratelimit/Dockerfile.anotherip -t anotherip . + 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 & @@ -31,9 +35,9 @@ jobs: 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 + run: src/bin/installDeps.sh - name: run rate limit test run: | - cd tests/ratelimit + cd src/tests/ratelimit ./testlimits.sh diff --git a/.gitignore b/.gitignore index c75e5a61f..09618cc83 100644 --- a/.gitignore +++ b/.gitignore @@ -3,11 +3,8 @@ node_modules !settings.json.template APIKEY.txt SESSIONKEY.txt -bin/abiword.exe -bin/node.exe etherpad-lite-win.zip var/dirty.db -bin/convertSettings.json *~ *.patch npm-debug.log @@ -15,9 +12,12 @@ npm-debug.log .ep_initialized *.crt *.key -bin/etherpad-1.deb credentials.json out/ .nyc_output -./package-lock.json .idea +/package-lock.json +/src/bin/abiword.exe +/src/bin/convertSettings.json +/src/bin/etherpad-1.deb +/src/bin/node.exe diff --git a/.travis.yml b/.travis.yml index 3f8ad1cf1..8517e2a89 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,11 @@ _set_loglevel_warn: &set_loglevel_warn | settings.json.template >settings.json.template.new && mv settings.json.template.new settings.json.template +_enable_admin_tests: &enable_admin_tests | +sed -e 's/"enableAdminUITests": false/"enableAdminUITests": true,\n"users":{"admin":{"password":"changeme","is_admin":true}}/' \ + settings.json.template >settings.json.template.new && + mv settings.json.template.new settings.json.template + _install_libreoffice: &install_libreoffice >- sudo add-apt-repository -y ppa:libreoffice/ppa && sudo apt-get update && @@ -46,19 +51,20 @@ jobs: name: "Test the Frontend without Plugins" install: - *set_loglevel_warn - - "tests/frontend/travis/sauce_tunnel.sh" - - "bin/installDeps.sh" + - *enable_admin_tests + - "src/tests/frontend/travis/sauce_tunnel.sh" + - "src/bin/installDeps.sh" - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" script: - - "./tests/frontend/travis/runner.sh" + - "./src/tests/frontend/travis/runner.sh" - name: "Run the Backend tests without Plugins" install: - *install_libreoffice - *set_loglevel_warn - - "bin/installDeps.sh" + - "src/bin/installDeps.sh" - "cd src && npm install && cd -" script: - - "tests/frontend/travis/runnerBackend.sh" + - "src/tests/frontend/travis/runnerBackend.sh" - name: "Test the Dockerfile" install: - "cd src && npm install && cd -" @@ -69,24 +75,25 @@ jobs: - name: "Load test Etherpad without Plugins" install: - *set_loglevel_warn - - "bin/installDeps.sh" + - "src/bin/installDeps.sh" - "cd src && npm install && cd -" - "npm install -g etherpad-load-test" script: - - "tests/frontend/travis/runnerLoadTest.sh" + - "src/tests/frontend/travis/runnerLoadTest.sh" # we can only frontend tests from the ether/ organization and not from forks. # To request tests to be run ask a maintainer to fork your repo to ether/ - if: fork = false name: "Test the Frontend Plugins only" install: - *set_loglevel_warn - - "tests/frontend/travis/sauce_tunnel.sh" - - "bin/installDeps.sh" - - "rm tests/frontend/specs/*" + - *enable_admin_tests + - "src/tests/frontend/travis/sauce_tunnel.sh" + - "src/bin/installDeps.sh" + - "rm src/tests/frontend/specs/*" - *install_plugins - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" script: - - "./tests/frontend/travis/runner.sh" + - "./src/tests/frontend/travis/runner.sh" - name: "Lint test package-lock.json" install: - "npm install lockfile-lint" @@ -96,11 +103,11 @@ jobs: install: - *install_libreoffice - *set_loglevel_warn - - "bin/installDeps.sh" + - "src/bin/installDeps.sh" - *install_plugins - "cd src && npm install && cd -" script: - - "tests/frontend/travis/runnerBackend.sh" + - "src/tests/frontend/travis/runnerBackend.sh" - name: "Test the Dockerfile" install: - "cd src && npm install && cd -" @@ -111,24 +118,24 @@ jobs: - name: "Load test Etherpad with Plugins" install: - *set_loglevel_warn - - "bin/installDeps.sh" + - "src/bin/installDeps.sh" - *install_plugins - "cd src && npm install && cd -" - "npm install -g etherpad-load-test" script: - - "tests/frontend/travis/runnerLoadTest.sh" + - "src/tests/frontend/travis/runnerLoadTest.sh" - name: "Test rate limit" install: - "docker network create --subnet=172.23.42.0/16 ep_net" - "docker build -f Dockerfile -t epl-debian-slim ." - - "docker build -f tests/ratelimit/Dockerfile.nginx -t nginx-latest ." - - "docker build -f tests/ratelimit/Dockerfile.anotherip -t anotherip ." + - "docker build -f src/tests/ratelimit/Dockerfile.nginx -t nginx-latest ." + - "docker build -f src/tests/ratelimit/Dockerfile.anotherip -t anotherip ." - "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" - - "./bin/installDeps.sh" + - "./src/bin/installDeps.sh" script: - - "cd tests/ratelimit && bash testlimits.sh" + - "cd src/tests/ratelimit && bash testlimits.sh" notifications: irc: diff --git a/CHANGELOG.md b/CHANGELOG.md index b63f571b9..e19aba752 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,79 @@ +# 1.8.8 + +### Security patches + +* EJS has been updated to 3.1.6 to mitigate an Arbitrary Code Injection + +### Compatibility changes + +* Node.js 10.17.0 or newer is now required. +* The `bin/` and `tests/` directories were moved under `src/`. Symlinks were + added at the old locations to hopefully avoid breaking user scripts and other + tools. +* Dependencies are now installed with the `--no-optional` flag to speed + installation. Optional dependencies such as `sqlite3` must now be manually + installed (e.g., `(cd src && npm i sqlite3)`). +* Socket.IO messages are now limited to 10K bytes to make denial of service + attacks more difficult. This may cause issues when pasting large amounts of + text or with plugins that send large messages (e.g., `ep_image_upload`). You + can change the limit via `settings.json`; see `socketIo.maxHttpBufferSize`. +* The top-level `package.json` file, added in v1.8.7, has been removed due to + problematic npm behavior. Whenever you install a plugin you will see the + following benign warnings that can be safely ignored: + + ``` + npm WARN saveError ENOENT: no such file or directory, open '.../package.json' + npm WARN enoent ENOENT: no such file or directory, open '.../package.json' + npm WARN develop No description + npm WARN develop No repository field. + npm WARN develop No README data + npm WARN develop No license field. + ``` + +### Notable enhancements + +* You can now generate a link to a specific line number in a pad. Appending + `#L10` to a pad URL will cause your browser to scroll down to line 10. +* Database performance is significantly improved. +* Admin UI now has test coverage in CI. (The tests are not enabled by default; + see `settings.json`.) +* New stats/metrics: `activePads`, `httpStartTime`, `lastDisconnected`, + `memoryUsageHeap`. +* Improved import UX. +* Browser caching improvements. +* Users can now pick absolute white (`#fff`) as their color. +* The `settings.json` template used for Docker images has new variables for + controlling rate limiting. +* Admin UI now has test coverage in CI. (The tests are not enabled by default + because the admin password is required; see `settings.json`.) +* For plugin authors: + * New `callAllSerial()` function that invokes hook functions like `callAll()` + except it supports asynchronous hook functions. + * `callFirst()` and `aCallFirst()` now support the same wide range of hook + function behaviors that `callAll()`, `aCallAll()`, and `callAllSerial()` + support. Also, they now warn when a hook function misbehaves. + * The following server-side hooks now support asynchronous hook functions: + `expressConfigure`, `expressCreateServer`, `padCopy`, `padRemove` + * Backend tests for plugins can now use the + [`ep_etherpad-lite/tests/backend/common`](src/tests/backend/common.js) + module to start the server and simplify API access. + * The `checkPlugins.js` script now automatically adds GitHub CI test coverage + badges for backend tests and npm publish. + +### Notable fixes + +* Enter key now stays in focus when inserted at bottom of viewport. +* Numbering for ordered list items now properly increments when exported to + text. +* Suppressed benign socket.io connection errors +* Interface no longer loses color variants on disconnect/reconnect event. +* General code quality is further significantly improved. +* Restarting Etherpad via `/admin` actions is more robust. +* Improved reliability of server shutdown and restart. +* No longer error if no buttons are visible. +* For plugin authors: + * Fixed `collectContentLineText` return value handling. + # 1.8.7 ### Compatibility-breaking changes * **IMPORTANT:** It is no longer possible to protect a group pad with a @@ -40,7 +116,7 @@ content in `.etherpad` exports * New `expressCloseServer` hook to close Express when required * The `padUpdate` hook context now includes `revs` and `changeset` -* `checkPlugins.js` has various improvements to help plugin developers +* `checkPlugin.js` has various improvements to help plugin developers * The HTTP request object (and therefore the express-session state) is now accessible from within most `eejsBlock_*` hooks * Users without a `password` or `hash` property in `settings.json` are no longer @@ -114,7 +190,7 @@ * MINOR: Fix ?showChat URL param issue * MINOR: Issue where timeslider URI fails to be correct if padID is numeric * MINOR: Include prompt for clear authorship when entire document is selected -* MINOR: Include full document aText every 100 revisions to make pad restoration on database curruption achievable +* MINOR: Include full document aText every 100 revisions to make pad restoration on database corruption achievable * MINOR: Several Colibris CSS fixes * MINOR: Use mime library for mime types instead of hard-coded. * MINOR: Don't show "new pad button" if instance is read only @@ -364,7 +440,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`. # 1.5.3 * NEW: Accessibility support for Screen readers, includes new fonts and keyboard shortcuts * NEW: API endpoint for Append Chat Message and Chat Backend Tests - * NEW: Error messages displayed on load are included in Default Pad Text (can be supressed) + * NEW: Error messages displayed on load are included in Default Pad Text (can be suppressed) * NEW: Content Collector can handle key values * NEW: getAttributesOnPosition Method * FIX: Firefox keeps attributes (bold etc) on cut/copy -> paste @@ -433,7 +509,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`. * Fix: Timeslider UI Fix * Fix: Remove Dokuwiki * Fix: Remove long paths from windows build (stops error during extract) - * Fix: Various globals remvoed + * Fix: Various globals removed * Fix: Move all scripts into bin/ * Fix: Various CSS bugfixes for Mobile devices * Fix: Overflow Toolbar @@ -509,7 +585,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`. * FIX: HTML import (don't crash on malformed or blank HTML input; strip title out of html during import) * FIX: check if uploaded file only contains ascii chars when abiword disabled * FIX: Plugin search in /admin/plugins - * FIX: Don't create new pad if a non-existant read-only pad is accessed + * FIX: Don't create new pad if a non-existent read-only pad is accessed * FIX: Drop messages from unknown connections (would lead to a crash after a restart) * FIX: API: fix createGroupFor endpoint, if mapped group is deleted * FIX: Import form for other locales @@ -526,7 +602,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`. * NEW: Bump log4js for improved logging * Fix: Remove URL schemes which don't have RFC standard * Fix: Fix safeRun subsequent restarts issue - * Fix: Allow safeRun to pass arguements to run.sh + * Fix: Allow safeRun to pass arguments to run.sh * Fix: Include script for more efficient import * Fix: Fix sysv comptibile script * Fix: Fix client side changeset spamming @@ -565,7 +641,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`. * Fix: Support Node 0.10 * Fix: Log HTTP on DEBUG log level * Fix: Server wont crash on import fails on 0 file import. - * Fix: Import no longer fails consistantly + * Fix: Import no longer fails consistently * Fix: Language support for non existing languages * Fix: Mobile support for chat notifications are now usable * Fix: Re-Enable Editbar buttons on reconnect @@ -597,7 +673,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`. * NEW: Admin dashboard mobile device support and new hooks for Admin dashboard * NEW: Get current API version from API * NEW: CLI script to delete pads - * Fix: Automatic client reconnection on disonnect + * Fix: Automatic client reconnection on disconnect * Fix: Text Export indentation now supports multiple indentations * Fix: Bugfix getChatHistory API method * Fix: Stop Chrome losing caret after paste is texted @@ -617,7 +693,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`. * Fix: Stop Opera browser inserting two new lines on enter keypress * Fix: Stop timeslider from showing NaN on pads with only one revision * Other: Allow timeslider tests to run and provide & fix various other frontend-tests - * Other: Begin dropping referene to Lite. Etherpad Lite is now named "Etherpad" + * Other: Begin dropping reference to Lite. Etherpad Lite is now named "Etherpad" * Other: Update to latest jQuery * Other: Change loading message asking user to please wait on first build * Other: Allow etherpad to use global npm installation (Safe since node 6.3) diff --git a/Dockerfile b/Dockerfile index aa6091a59..e16e58178 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,7 +35,7 @@ WORKDIR /opt/etherpad-lite COPY --chown=etherpad:0 ./ ./ # install node dependencies for Etherpad -RUN bin/installDeps.sh && \ +RUN src/bin/installDeps.sh && \ rm -rf ~/.npm/_cacache # Install the plugins, if ETHERPAD_PLUGINS is not empty. diff --git a/Makefile b/Makefile index 0c99d9ff4..bc79166b3 100644 --- a/Makefile +++ b/Makefile @@ -9,8 +9,8 @@ UNAME := $(shell uname -s) ensure_marked_is_installed: set -eu; \ hash npm; \ - if [ $(shell npm list --prefix bin/doc >/dev/null 2>/dev/null; echo $$?) -ne "0" ]; then \ - npm ci --prefix=bin/doc; \ + 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) @@ -21,7 +21,7 @@ out/doc/assets/%: doc/assets/% out/doc/%.html: doc/%.md mkdir -p $(@D) - node bin/doc/generate.js --format=html --template=doc/template.html $< > $@ + node src/bin/doc/generate.js --format=html --template=doc/template.html $< > $@ ifeq ($(UNAME),Darwin) sed -i '' 's/__VERSION__/${VERSION}/' $@ else diff --git a/README.md b/README.md index 086441b19..3f1e6fb0f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Etherpad is a real-time collaborative editor [scalable to thousands of simultane # Installation ## Requirements -- `nodejs` >= **10.13.0**. +- `nodejs` >= **10.17.0**. ## GNU/Linux and other UNIX-like systems @@ -21,19 +21,22 @@ Etherpad is a real-time collaborative editor [scalable to thousands of simultane ``` 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 && bin/run.sh +git clone --branch master https://github.com/ether/etherpad-lite.git && +cd etherpad-lite && +src/bin/run.sh ``` ### Manual install -You'll need git and [node.js](https://nodejs.org) installed (minimum required Node version: **10.13.0**). +You'll need git and [node.js](https://nodejs.org) installed (minimum required Node version: **10.17.0**). **As any user (we recommend creating a separate user called etherpad):** 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 `bin/run.sh` and open in your browser. +3. run `src/bin/run.sh` and open in your browser. -To update to the latest released version, execute `git pull origin`. The next start with `bin/run.sh` will update the dependencies. +To update to the latest released version, execute `git pull origin`. The next +start with `src/bin/run.sh` will update the dependencies. [Next steps](#next-steps). @@ -53,11 +56,13 @@ You'll need [node.js](https://nodejs.org) and (optionally, though recommended) g 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 `bin\installOnWindows.bat` +2. With a "Run as administrator" command prompt execute + `src\bin\installOnWindows.bat` Now, run `start.bat` and open in your browser. -Update to the latest version with `git pull origin`, then run `bin\installOnWindows.bat`, again. +Update to the latest version with `git pull origin`, then run +`src\bin\installOnWindows.bat`, again. If cloning to a subdirectory within another project, you may need to do the following: @@ -73,7 +78,9 @@ Find [here](doc/docker.md) information on running Etherpad in a container. ## 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. +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. @@ -98,7 +105,7 @@ Etherpad is very customizable through plugins. Instructions for installing theme Run the following command in your Etherpad folder to get all of the features visible in the demo gif: ``` -npm install ep_headings2 ep_markdown ep_comments_page ep_align ep_page_view ep_font_color ep_webrtc ep_embedded_hyperlinks2 +npm install ep_headings2 ep_markdown ep_comments_page ep_align ep_font_color ep_webrtc ep_embedded_hyperlinks2 ``` ## Customize the style with skin variants @@ -115,9 +122,13 @@ Documentation can be found in `doc/`. # Development ## Things you should know -You can debug Etherpad using `bin/debugRun.sh`. -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 `bin/installDeps.sh` after installing a new dependency or upgrading version. +You can debug Etherpad using `src/bin/debugRun.sh`. + +You can run Etherpad quickly launching `src/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 +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). diff --git a/bin b/bin new file mode 120000 index 000000000..70feaa890 --- /dev/null +++ b/bin @@ -0,0 +1 @@ +src/bin \ No newline at end of file diff --git a/bin/checkAllPads.js b/bin/checkAllPads.js deleted file mode 100644 index f90e57aef..000000000 --- a/bin/checkAllPads.js +++ /dev/null @@ -1,91 +0,0 @@ -/* - * This is a debug tool. It checks all revisions for data corruption - */ - -if (process.argv.length != 2) { - console.error('Use: node bin/checkAllPads.js'); - process.exit(1); -} - -// load and initialize NPM -const npm = require('../src/node_modules/npm'); -npm.load({}, async () => { - try { - // initialize the database - const settings = require('../src/node/utils/Settings'); - const db = require('../src/node/db/DB'); - await db.init(); - - // load modules - const Changeset = require('../src/static/js/Changeset'); - const padManager = require('../src/node/db/PadManager'); - - // get all pads - const res = await padManager.listAllPads(); - - for (const padId of res.padIDs) { - const pad = await padManager.getPad(padId); - - // check if the pad has a pool - if (pad.pool === undefined) { - console.error(`[${pad.id}] Missing attribute pool`); - continue; - } - - // create an array with key kevisions - // key revisions always save the full pad atext - const head = pad.getHeadRevisionNumber(); - const keyRevisions = []; - for (let rev = 0; rev < head; rev += 100) { - keyRevisions.push(rev); - } - - // run through all key revisions - for (const keyRev of keyRevisions) { - // create an array of revisions we need till the next keyRevision or the End - const revisionsNeeded = []; - for (let rev = keyRev; rev <= keyRev + 100 && rev <= head; rev++) { - revisionsNeeded.push(rev); - } - - // this array will hold all revision changesets - const revisions = []; - - // run through all needed revisions and get them from the database - for (const revNum of revisionsNeeded) { - const revision = await db.get(`pad:${pad.id}:revs:${revNum}`); - revisions[revNum] = revision; - } - - // check if the revision exists - if (revisions[keyRev] == null) { - console.error(`[${pad.id}] Missing revision ${keyRev}`); - continue; - } - - // check if there is a atext in the keyRevisions - if (revisions[keyRev].meta === undefined || revisions[keyRev].meta.atext === undefined) { - console.error(`[${pad.id}] Missing atext in revision ${keyRev}`); - continue; - } - - const apool = pad.pool; - let atext = revisions[keyRev].meta.atext; - - for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) { - try { - const cs = revisions[rev].changeset; - atext = Changeset.applyToAText(cs, atext, apool); - } catch (e) { - console.error(`[${pad.id}] Bad changeset at revision ${i} - ${e.message}`); - } - } - } - console.log('finished'); - process.exit(0); - } - } catch (err) { - console.trace(err); - process.exit(1); - } -}); diff --git a/bin/checkPad.js b/bin/checkPad.js deleted file mode 100644 index 323840e72..000000000 --- a/bin/checkPad.js +++ /dev/null @@ -1,92 +0,0 @@ -/* - * This is a debug tool. It checks all revisions for data corruption - */ - -if (process.argv.length != 3) { - console.error('Use: node bin/checkPad.js $PADID'); - process.exit(1); -} - -// get the padID -const padId = process.argv[2]; - -// load and initialize NPM; -const npm = require('../src/node_modules/npm'); -npm.load({}, async () => { - try { - // initialize database - const settings = require('../src/node/utils/Settings'); - const db = require('../src/node/db/DB'); - await db.init(); - - // load modules - const Changeset = require('ep_etherpad-lite/static/js/Changeset'); - const padManager = require('../src/node/db/PadManager'); - - const exists = await padManager.doesPadExists(padId); - if (!exists) { - console.error('Pad does not exist'); - process.exit(1); - } - - // get the pad - const pad = await padManager.getPad(padId); - - // create an array with key revisions - // key revisions always save the full pad atext - const head = pad.getHeadRevisionNumber(); - const keyRevisions = []; - for (let rev = 0; rev < head; rev += 100) { - keyRevisions.push(rev); - } - - // run through all key revisions - for (const keyRev of keyRevisions) { - // create an array of revisions we need till the next keyRevision or the End - const revisionsNeeded = []; - for (let rev = keyRev; rev <= keyRev + 100 && rev <= head; rev++) { - revisionsNeeded.push(rev); - } - - // this array will hold all revision changesets - const revisions = []; - - // run through all needed revisions and get them from the database - for (const revNum of revisionsNeeded) { - const revision = await db.get(`pad:${padId}:revs:${revNum}`); - revisions[revNum] = revision; - } - - // check if the pad has a pool - if (pad.pool === undefined) { - console.error('Attribute pool is missing'); - process.exit(1); - } - - // check if there is an atext in the keyRevisions - if (revisions[keyRev] === undefined || revisions[keyRev].meta === undefined || revisions[keyRev].meta.atext === undefined) { - console.error(`No atext in key revision ${keyRev}`); - continue; - } - - const apool = pad.pool; - let atext = revisions[keyRev].meta.atext; - - for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) { - try { - // console.log("check revision " + rev); - const cs = revisions[rev].changeset; - atext = Changeset.applyToAText(cs, atext, apool); - } catch (e) { - console.error(`Bad changeset at revision ${rev} - ${e.message}`); - continue; - } - } - console.log('finished'); - process.exit(0); - } - } catch (e) { - console.trace(e); - process.exit(1); - } -}); diff --git a/bin/checkPadDeltas.js b/bin/checkPadDeltas.js deleted file mode 100644 index 1e45f7148..000000000 --- a/bin/checkPadDeltas.js +++ /dev/null @@ -1,111 +0,0 @@ -/* - * This is a debug tool. It checks all revisions for data corruption - */ - -if (process.argv.length != 3) { - console.error('Use: node bin/checkPadDeltas.js $PADID'); - process.exit(1); -} - -// get the padID -const padId = process.argv[2]; - -// load and initialize NPM; -const expect = require('expect.js'); -const diff = require('diff'); -var async = require('async'); - -const npm = require('../src/node_modules/npm'); -var async = require('ep_etherpad-lite/node_modules/async'); -const Changeset = require('ep_etherpad-lite/static/js/Changeset'); - -npm.load({}, async () => { - try { - // initialize database - const settings = require('../src/node/utils/Settings'); - const db = require('../src/node/db/DB'); - await db.init(); - - // load modules - const Changeset = require('ep_etherpad-lite/static/js/Changeset'); - const padManager = require('../src/node/db/PadManager'); - - const exists = await padManager.doesPadExists(padId); - if (!exists) { - console.error('Pad does not exist'); - process.exit(1); - } - - // get the pad - const pad = await padManager.getPad(padId); - - // create an array with key revisions - // key revisions always save the full pad atext - const head = pad.getHeadRevisionNumber(); - const keyRevisions = []; - for (var i = 0; i < head; i += 100) { - keyRevisions.push(i); - } - - // create an array with all revisions - const revisions = []; - for (var i = 0; i <= head; i++) { - revisions.push(i); - } - - let atext = Changeset.makeAText('\n'); - - // run trough all revisions - async.forEachSeries(revisions, (revNum, callback) => { - // console.log('Fetching', revNum) - db.db.get(`pad:${padId}:revs:${revNum}`, (err, revision) => { - if (err) return callback(err); - - // check if there is a atext in the keyRevisions - if (~keyRevisions.indexOf(revNum) && (revision === undefined || revision.meta === undefined || revision.meta.atext === undefined)) { - console.error(`No atext in key revision ${revNum}`); - callback(); - return; - } - - try { - // console.log("check revision ", revNum); - const cs = revision.changeset; - atext = Changeset.applyToAText(cs, atext, pad.pool); - } catch (e) { - console.error(`Bad changeset at revision ${revNum} - ${e.message}`); - callback(); - return; - } - - if (~keyRevisions.indexOf(revNum)) { - try { - expect(revision.meta.atext.text).to.eql(atext.text); - expect(revision.meta.atext.attribs).to.eql(atext.attribs); - } catch (e) { - console.error(`Atext in key revision ${revNum} doesn't match computed one.`); - console.log(diff.diffChars(atext.text, revision.meta.atext.text).map((op) => { if (!op.added && !op.removed) op.value = op.value.length; return op; })); - // console.error(e) - // console.log('KeyRev. :', revision.meta.atext) - // console.log('Computed:', atext) - callback(); - return; - } - } - - setImmediate(callback); - }); - }, (er) => { - if (pad.atext.text == atext.text) { console.log('ok'); } else { - console.error('Pad AText doesn\'t match computed one! (Computed ', atext.text.length, ', db', pad.atext.text.length, ')'); - console.log(diff.diffChars(atext.text, pad.atext.text).map((op) => { if (!op.added && !op.removed) op.value = op.value.length; return op; })); - } - callback(er); - }); - - process.exit(0); - } catch (e) { - console.trace(e); - process.exit(1); - } -}); diff --git a/bin/convert.js b/bin/convert.js deleted file mode 100644 index 47f8b2d27..000000000 --- a/bin/convert.js +++ /dev/null @@ -1,391 +0,0 @@ -const startTime = Date.now(); -const fs = require('fs'); -const ueberDB = require('../src/node_modules/ueberdb2'); -const mysql = require('../src/node_modules/ueberdb2/node_modules/mysql'); -const async = require('../src/node_modules/async'); -const Changeset = require('ep_etherpad-lite/static/js/Changeset'); -const randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; -const AttributePool = require('ep_etherpad-lite/static/js/AttributePool'); - -const settingsFile = process.argv[2]; -const sqlOutputFile = process.argv[3]; - -// stop if the settings file is not set -if (!settingsFile || !sqlOutputFile) { - console.error('Use: node convert.js $SETTINGSFILE $SQLOUTPUT'); - process.exit(1); -} - -log('read settings file...'); -// read the settings file and parse the json -const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8')); -log('done'); - -log('open output file...'); -const sqlOutput = fs.openSync(sqlOutputFile, 'w'); -const sql = 'SET CHARACTER SET UTF8;\n' + - 'CREATE TABLE IF NOT EXISTS `store` ( \n' + - '`key` VARCHAR( 100 ) NOT NULL , \n' + - '`value` LONGTEXT NOT NULL , \n' + - 'PRIMARY KEY ( `key` ) \n' + - ') ENGINE = INNODB;\n' + - 'START TRANSACTION;\n\n'; -fs.writeSync(sqlOutput, sql); -log('done'); - -const etherpadDB = mysql.createConnection({ - host: settings.etherpadDB.host, - user: settings.etherpadDB.user, - password: settings.etherpadDB.password, - database: settings.etherpadDB.database, - port: settings.etherpadDB.port, -}); - -// get the timestamp once -const timestamp = Date.now(); - -let padIDs; - -async.series([ - // get all padids out of the database... - function (callback) { - log('get all padIds out of the database...'); - - etherpadDB.query('SELECT ID FROM PAD_META', [], (err, _padIDs) => { - padIDs = _padIDs; - callback(err); - }); - }, - function (callback) { - log('done'); - - // create a queue with a concurrency 100 - const queue = async.queue((padId, callback) => { - convertPad(padId, (err) => { - incrementPadStats(); - callback(err); - }); - }, 100); - - // set the step callback as the queue callback - queue.drain = callback; - - // add the padids to the worker queue - for (let i = 0, length = padIDs.length; i < length; i++) { - queue.push(padIDs[i].ID); - } - }, -], (err) => { - if (err) throw err; - - // write the groups - let sql = ''; - for (const proID in proID2groupID) { - const groupID = proID2groupID[proID]; - const subdomain = proID2subdomain[proID]; - - sql += `REPLACE INTO store VALUES (${etherpadDB.escape(`group:${groupID}`)}, ${etherpadDB.escape(JSON.stringify(groups[groupID]))});\n`; - sql += `REPLACE INTO store VALUES (${etherpadDB.escape(`mapper2group:subdomain:${subdomain}`)}, ${etherpadDB.escape(groupID)});\n`; - } - - // close transaction - sql += 'COMMIT;'; - - // end the sql file - fs.writeSync(sqlOutput, sql, undefined, 'utf-8'); - fs.closeSync(sqlOutput); - - log('finished.'); - process.exit(0); -}); - -function log(str) { - console.log(`${(Date.now() - startTime) / 1000}\t${str}`); -} - -let padsDone = 0; - -function incrementPadStats() { - padsDone++; - - if (padsDone % 100 == 0) { - const averageTime = Math.round(padsDone / ((Date.now() - startTime) / 1000)); - log(`${padsDone}/${padIDs.length}\t${averageTime} pad/s`); - } -} - -var proID2groupID = {}; -var proID2subdomain = {}; -var groups = {}; - -function convertPad(padId, callback) { - const changesets = []; - const changesetsMeta = []; - const chatMessages = []; - const authors = []; - let apool; - let subdomain; - let padmeta; - - async.series([ - // get all needed db values - function (callback) { - async.parallel([ - // get the pad revisions - function (callback) { - const sql = 'SELECT * FROM `PAD_CHAT_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_CHAT_META` WHERE ID=?)'; - - etherpadDB.query(sql, [padId], (err, results) => { - if (!err) { - try { - // parse the pages - for (let i = 0, length = results.length; i < length; i++) { - parsePage(chatMessages, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true); - } - } catch (e) { err = e; } - } - - callback(err); - }); - }, - // get the chat entries - function (callback) { - const sql = 'SELECT * FROM `PAD_REVS_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_REVS_META` WHERE ID=?)'; - - etherpadDB.query(sql, [padId], (err, results) => { - if (!err) { - try { - // parse the pages - for (let i = 0, length = results.length; i < length; i++) { - parsePage(changesets, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, false); - } - } catch (e) { err = e; } - } - - callback(err); - }); - }, - // get the pad revisions meta data - function (callback) { - const sql = 'SELECT * FROM `PAD_REVMETA_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_REVMETA_META` WHERE ID=?)'; - - etherpadDB.query(sql, [padId], (err, results) => { - if (!err) { - try { - // parse the pages - for (let i = 0, length = results.length; i < length; i++) { - parsePage(changesetsMeta, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true); - } - } catch (e) { err = e; } - } - - callback(err); - }); - }, - // get the attribute pool of this pad - function (callback) { - const sql = 'SELECT `JSON` FROM `PAD_APOOL` WHERE `ID` = ?'; - - etherpadDB.query(sql, [padId], (err, results) => { - if (!err) { - try { - apool = JSON.parse(results[0].JSON).x; - } catch (e) { err = e; } - } - - callback(err); - }); - }, - // get the authors informations - function (callback) { - const sql = 'SELECT * FROM `PAD_AUTHORS_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_AUTHORS_META` WHERE ID=?)'; - - etherpadDB.query(sql, [padId], (err, results) => { - if (!err) { - try { - // parse the pages - for (let i = 0, length = results.length; i < length; i++) { - parsePage(authors, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true); - } - } catch (e) { err = e; } - } - - callback(err); - }); - }, - // get the pad information - function (callback) { - const sql = 'SELECT JSON FROM `PAD_META` WHERE ID=?'; - - etherpadDB.query(sql, [padId], (err, results) => { - if (!err) { - try { - padmeta = JSON.parse(results[0].JSON).x; - } catch (e) { err = e; } - } - - callback(err); - }); - }, - // get the subdomain - function (callback) { - // skip if this is no proPad - if (padId.indexOf('$') == -1) { - callback(); - return; - } - - // get the proID out of this padID - const proID = padId.split('$')[0]; - - const sql = 'SELECT subDomain FROM pro_domains WHERE ID = ?'; - - etherpadDB.query(sql, [proID], (err, results) => { - if (!err) { - subdomain = results[0].subDomain; - } - - callback(err); - }); - }, - ], callback); - }, - function (callback) { - // saves all values that should be written to the database - const values = {}; - - // this is a pro pad, let's convert it to a group pad - if (padId.indexOf('$') != -1) { - const padIdParts = padId.split('$'); - const proID = padIdParts[0]; - const padName = padIdParts[1]; - - let groupID; - - // this proID is not converted so far, do it - if (proID2groupID[proID] == null) { - groupID = `g.${randomString(16)}`; - - // create the mappers for this new group - proID2groupID[proID] = groupID; - proID2subdomain[proID] = subdomain; - groups[groupID] = {pads: {}}; - } - - // use the generated groupID; - groupID = proID2groupID[proID]; - - // rename the pad - padId = `${groupID}$${padName}`; - - // set the value for this pad in the group - groups[groupID].pads[padId] = 1; - } - - try { - const newAuthorIDs = {}; - const oldName2newName = {}; - - // replace the authors with generated authors - // we need to do that cause where the original etherpad saves pad local authors, the new (lite) etherpad uses them global - for (var i in apool.numToAttrib) { - var key = apool.numToAttrib[i][0]; - const value = apool.numToAttrib[i][1]; - - // skip non authors and anonymous authors - if (key != 'author' || value == '') continue; - - // generate new author values - const authorID = `a.${randomString(16)}`; - const authorColorID = authors[i].colorId || Math.floor(Math.random() * (exports.getColorPalette().length)); - const authorName = authors[i].name || null; - - // overwrite the authorID of the attribute pool - apool.numToAttrib[i][1] = authorID; - - // write the author to the database - values[`globalAuthor:${authorID}`] = {colorId: authorColorID, name: authorName, timestamp}; - - // save in mappers - newAuthorIDs[i] = authorID; - oldName2newName[value] = authorID; - } - - // save all revisions - for (var i = 0; i < changesets.length; i++) { - values[`pad:${padId}:revs:${i}`] = {changeset: changesets[i], - meta: { - author: newAuthorIDs[changesetsMeta[i].a], - timestamp: changesetsMeta[i].t, - atext: changesetsMeta[i].atext || undefined, - }}; - } - - // save all chat messages - for (var i = 0; i < chatMessages.length; i++) { - values[`pad:${padId}:chat:${i}`] = {text: chatMessages[i].lineText, - userId: oldName2newName[chatMessages[i].userId], - time: chatMessages[i].time}; - } - - // generate the latest atext - const fullAPool = (new AttributePool()).fromJsonable(apool); - const keyRev = Math.floor(padmeta.head / padmeta.keyRevInterval) * padmeta.keyRevInterval; - let atext = changesetsMeta[keyRev].atext; - let curRev = keyRev; - while (curRev < padmeta.head) { - curRev++; - const changeset = changesets[curRev]; - atext = Changeset.applyToAText(changeset, atext, fullAPool); - } - - values[`pad:${padId}`] = {atext, - pool: apool, - head: padmeta.head, - chatHead: padmeta.numChatMessages}; - } catch (e) { - console.error(`Error while converting pad ${padId}, pad skipped`); - console.error(e.stack ? e.stack : JSON.stringify(e)); - callback(); - return; - } - - let sql = ''; - for (var key in values) { - sql += `REPLACE INTO store VALUES (${etherpadDB.escape(key)}, ${etherpadDB.escape(JSON.stringify(values[key]))});\n`; - } - - fs.writeSync(sqlOutput, sql, undefined, 'utf-8'); - callback(); - }, - ], callback); -} - -/** - * This parses a Page like Etherpad uses them in the databases - * The offsets describes the length of a unit in the page, the data are - * all values behind each other - */ -function parsePage(array, pageStart, offsets, data, json) { - let start = 0; - const lengths = offsets.split(','); - - for (let i = 0; i < lengths.length; i++) { - let unitLength = lengths[i]; - - // skip empty units - if (unitLength == '') continue; - - // parse the number - unitLength = Number(unitLength); - - // cut the unit out of data - const unit = data.substr(start, unitLength); - - // put it into the array - array[pageStart + i] = json ? JSON.parse(unit) : unit; - - // update start - start += unitLength; - } -} diff --git a/bin/deleteAllGroupSessions.js b/bin/deleteAllGroupSessions.js deleted file mode 100644 index ee0058ffa..000000000 --- a/bin/deleteAllGroupSessions.js +++ /dev/null @@ -1,51 +0,0 @@ -/* -* A tool for deleting ALL GROUP sessions Etherpad user sessions from the CLI, -* because sometimes a brick is required to fix a face. -*/ - -const request = require('../src/node_modules/request'); -const settings = require(`${__dirname}/../tests/container/loadSettings`).loadSettings(); -const supertest = require(`${__dirname}/../src/node_modules/supertest`); -const api = supertest(`http://${settings.ip}:${settings.port}`); -const path = require('path'); -const fs = require('fs'); - -// get the API Key -const filePath = path.join(__dirname, '../APIKEY.txt'); -const apikey = fs.readFileSync(filePath, {encoding: 'utf-8'}); - -// Set apiVersion to base value, we change this later. -let apiVersion = 1; -let guids; - -// Update the apiVersion -api.get('/api/') - .expect((res) => { - apiVersion = res.body.currentVersion; - if (!res.body.currentVersion) throw new Error('No version set in API'); - return; - }) - .then(() => { - const guri = `/api/${apiVersion}/listAllGroups?apikey=${apikey}`; - api.get(guri) - .then((res) => { - guids = res.body.data.groupIDs; - guids.forEach((groupID) => { - const luri = `/api/${apiVersion}/listSessionsOfGroup?apikey=${apikey}&groupID=${groupID}`; - api.get(luri) - .then((res) => { - if (res.body.data) { - Object.keys(res.body.data).forEach((sessionID) => { - if (sessionID) { - console.log('Deleting', sessionID); - const duri = `/api/${apiVersion}/deleteSession?apikey=${apikey}&sessionID=${sessionID}`; - api.post(duri); // deletes - } - }); - } else { - // no session in this group. - } - }); - }); - }); - }); diff --git a/bin/deletePad.js b/bin/deletePad.js deleted file mode 100644 index e145d63a0..000000000 --- a/bin/deletePad.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * A tool for deleting pads from the CLI, because sometimes a brick is required - * to fix a window. - */ - -const request = require('../src/node_modules/request'); -const settings = require(`${__dirname}/../tests/container/loadSettings`).loadSettings(); -const supertest = require(`${__dirname}/../src/node_modules/supertest`); -const api = supertest(`http://${settings.ip}:${settings.port}`); -const path = require('path'); -const fs = require('fs'); -if (process.argv.length != 3) { - console.error('Use: node deletePad.js $PADID'); - process.exit(1); -} - -// get the padID -const padId = process.argv[2]; - -// get the API Key -const filePath = path.join(__dirname, '../APIKEY.txt'); -const apikey = fs.readFileSync(filePath, {encoding: 'utf-8'}); - -// Set apiVersion to base value, we change this later. -let apiVersion = 1; - -// Update the apiVersion -api.get('/api/') - .expect((res) => { - apiVersion = res.body.currentVersion; - if (!res.body.currentVersion) throw new Error('No version set in API'); - return; - }) - .end((err, res) => { - // Now we know the latest API version, let's delete pad - const uri = `/api/${apiVersion}/deletePad?apikey=${apikey}&padID=${padId}`; - api.post(uri) - .expect((res) => { - if (res.body.code === 1) { - console.error('Error deleting pad', res.body); - } else { - console.log('Deleted pad', res.body); - } - return; - }) - .end(() => {}); - }); -// end diff --git a/bin/extractPadData.js b/bin/extractPadData.js deleted file mode 100644 index a811076ef..000000000 --- a/bin/extractPadData.js +++ /dev/null @@ -1,75 +0,0 @@ -/* - * This is a debug tool. It helps to extract all datas of a pad and move it from - * a productive environment and to a develop environment to reproduce bugs - * there. It outputs a dirtydb file - */ - -if (process.argv.length != 3) { - console.error('Use: node extractPadData.js $PADID'); - process.exit(1); -} - -// get the padID -const padId = process.argv[2]; - -const npm = require('../src/node_modules/npm'); - -npm.load({}, async (er) => { - if (er) { - console.error(`Could not load NPM: ${er}`); - process.exit(1); - } - - try { - // initialize database - const settings = require('../src/node/utils/Settings'); - const db = require('../src/node/db/DB'); - await db.init(); - - // load extra modules - const dirtyDB = require('../src/node_modules/dirty'); - const padManager = require('../src/node/db/PadManager'); - const util = require('util'); - - // initialize output database - const dirty = dirtyDB(`${padId}.db`); - - // Promise wrapped get and set function - const wrapped = db.db.db.wrappedDB; - const get = util.promisify(wrapped.get.bind(wrapped)); - const set = util.promisify(dirty.set.bind(dirty)); - - // array in which required key values will be accumulated - const neededDBValues = [`pad:${padId}`]; - - // get the actual pad object - const pad = await padManager.getPad(padId); - - // add all authors - neededDBValues.push(...pad.getAllAuthors().map((author) => `globalAuthor:${author}`)); - - // add all revisions - for (let rev = 0; rev <= pad.head; ++rev) { - neededDBValues.push(`pad:${padId}:revs:${rev}`); - } - - // add all chat values - for (let chat = 0; chat <= pad.chatHead; ++chat) { - neededDBValues.push(`pad:${padId}:chat:${chat}`); - } - - for (const dbkey of neededDBValues) { - let dbvalue = await get(dbkey); - if (dbvalue && typeof dbvalue !== 'object') { - dbvalue = JSON.parse(dbvalue); - } - await set(dbkey, dbvalue); - } - - console.log('finished'); - process.exit(0); - } catch (er) { - console.error(er); - process.exit(1); - } -}); diff --git a/bin/fastRun.sh b/bin/fastRun.sh deleted file mode 100755 index 90d83dc8e..000000000 --- a/bin/fastRun.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# -# Run Etherpad directly, assuming all the dependencies are already installed. -# -# Useful for developers, or users that know what they are doing. If you just -# upgraded Etherpad version, installed a new dependency, or are simply unsure -# of what to do, please execute bin/installDeps.sh once before running this -# script. - -set -eu - -# source: https://stackoverflow.com/questions/59895/how-to-get-the-source-directory-of-a-bash-script-from-within-the-script-itself#246128 -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" - -# Source constants and usefull functions -. ${DIR}/../bin/functions.sh - -echo "Running directly, without checking/installing dependencies" - -# move to the base Etherpad directory. This will be necessary until Etherpad -# learns to run from arbitrary CWDs. -cd "${DIR}/.." - -# run Etherpad main class -node $(compute_node_args) "${DIR}/../node_modules/ep_etherpad-lite/node/server.js" "$@" diff --git a/bin/importSqlFile.js b/bin/importSqlFile.js deleted file mode 100644 index a67cb8bf0..000000000 --- a/bin/importSqlFile.js +++ /dev/null @@ -1,103 +0,0 @@ -const startTime = Date.now(); - -require('ep_etherpad-lite/node_modules/npm').load({}, (er, npm) => { - const fs = require('fs'); - - const ueberDB = require('ep_etherpad-lite/node_modules/ueberdb2'); - const settings = require('ep_etherpad-lite/node/utils/Settings'); - const log4js = require('ep_etherpad-lite/node_modules/log4js'); - - const dbWrapperSettings = { - cache: 0, - writeInterval: 100, - json: false, // data is already json encoded - }; - const db = new ueberDB.database(settings.dbType, settings.dbSettings, dbWrapperSettings, log4js.getLogger('ueberDB')); - - const sqlFile = process.argv[2]; - - // stop if the settings file is not set - if (!sqlFile) { - console.error('Use: node importSqlFile.js $SQLFILE'); - process.exit(1); - } - - log('initializing db'); - db.init((err) => { - // there was an error while initializing the database, output it and stop - if (err) { - console.error('ERROR: Problem while initializing the database'); - console.error(err.stack ? err.stack : err); - process.exit(1); - } else { - log('done'); - - log('open output file...'); - const lines = fs.readFileSync(sqlFile, 'utf8').split('\n'); - - const count = lines.length; - let keyNo = 0; - - process.stdout.write(`Start importing ${count} keys...\n`); - lines.forEach((l) => { - if (l.substr(0, 27) == 'REPLACE INTO store VALUES (') { - const pos = l.indexOf("', '"); - const key = l.substr(28, pos - 28); - let value = l.substr(pos + 3); - value = value.substr(0, value.length - 2); - console.log(`key: ${key} val: ${value}`); - console.log(`unval: ${unescape(value)}`); - db.set(key, unescape(value), null); - keyNo++; - if (keyNo % 1000 == 0) { - process.stdout.write(` ${keyNo}/${count}\n`); - } - } - }); - process.stdout.write('\n'); - process.stdout.write('done. waiting for db to finish transaction. depended on dbms this may take some time...\n'); - - db.doShutdown(() => { - log(`finished, imported ${keyNo} keys.`); - process.exit(0); - }); - } - }); -}); - -function log(str) { - console.log(`${(Date.now() - startTime) / 1000}\t${str}`); -} - -unescape = function (val) { - // value is a string - if (val.substr(0, 1) == "'") { - val = val.substr(0, val.length - 1).substr(1); - - return val.replace(/\\[0nrbtZ\\'"]/g, (s) => { - switch (s) { - case '\\0': return '\0'; - case '\\n': return '\n'; - case '\\r': return '\r'; - case '\\b': return '\b'; - case '\\t': return '\t'; - case '\\Z': return '\x1a'; - default: return s.substr(1); - } - }); - } - - // value is a boolean or NULL - if (val == 'NULL') { - return null; - } - if (val == 'true') { - return true; - } - if (val == 'false') { - return false; - } - - // value is a number - return val; -}; diff --git a/bin/migrateDirtyDBtoRealDB.js b/bin/migrateDirtyDBtoRealDB.js deleted file mode 100644 index 63425cab7..000000000 --- a/bin/migrateDirtyDBtoRealDB.js +++ /dev/null @@ -1,48 +0,0 @@ -require('ep_etherpad-lite/node_modules/npm').load({}, (er, npm) => { - process.chdir(`${npm.root}/..`); - - // This script requires that you have modified your settings.json file - // to work with a real database. Please make a backup of your dirty.db - // file before using this script, just to be safe. - - // It might be necessary to run the script using more memory: - // `node --max-old-space-size=4096 bin/migrateDirtyDBtoRealDB.js` - - - const settings = require('ep_etherpad-lite/node/utils/Settings'); - let dirty = require('../src/node_modules/dirty'); - const ueberDB = require('../src/node_modules/ueberdb2'); - const log4js = require('../src/node_modules/log4js'); - const dbWrapperSettings = { - cache: '0', // The cache slows things down when you're mostly writing. - writeInterval: 0, // Write directly to the database, don't buffer - }; - const db = new ueberDB.database(settings.dbType, settings.dbSettings, dbWrapperSettings, log4js.getLogger('ueberDB')); - let i = 0; - let length = 0; - - db.init(() => { - console.log('Waiting for dirtyDB to parse its file.'); - dirty = dirty('var/dirty.db').on('load', () => { - dirty.forEach(() => { - length++; - }); - console.log(`Found ${length} records, processing now.`); - - dirty.forEach(async (key, value) => { - const error = await db.set(key, value); - console.log(`Wrote record ${i}`); - i++; - - if (i === length) { - console.log('finished, just clearing up for a bit...'); - setTimeout(() => { - process.exit(0); - }, 5000); - } - }); - console.log('Please wait for all records to flush to database, then kill this process.'); - }); - console.log('done?'); - }); -}); diff --git a/bin/plugins/checkPlugin.js b/bin/plugins/checkPlugin.js deleted file mode 100755 index fd31c148e..000000000 --- a/bin/plugins/checkPlugin.js +++ /dev/null @@ -1,469 +0,0 @@ -/* -* -* Usage -- see README.md -* -* Normal usage: node bin/plugins/checkPlugins.js ep_whatever -* Auto fix the things it can: node bin/plugins/checkPlugins.js ep_whatever autofix -* Auto commit, push and publish(to npm) * highly dangerous: -node bin/plugins/checkPlugins.js ep_whatever autofix autocommit - -*/ - -const fs = require('fs'); -const {exec} = require('child_process'); - -// get plugin name & path from user input -const pluginName = process.argv[2]; - -if (!pluginName) { - console.error('no plugin name specified'); - process.exit(1); -} - -const pluginPath = `node_modules/${pluginName}`; - -console.log(`Checking the plugin: ${pluginName}`); - -// Should we autofix? -if (process.argv[3] && process.argv[3] === 'autofix') var autoFix = true; - -// Should we update files where possible? -if (process.argv[5] && process.argv[5] === 'autoupdate') var autoUpdate = true; - -// Should we automcommit and npm publish?! -if (process.argv[4] && process.argv[4] === 'autocommit') var autoCommit = true; - - -if (autoCommit) { - console.warn('Auto commit is enabled, I hope you know what you are doing...'); -} - -fs.readdir(pluginPath, (err, rootFiles) => { - // handling error - if (err) { - return console.log(`Unable to scan directory: ${err}`); - } - - // rewriting files to lower case - const files = []; - - // some files we need to know the actual file name. Not compulsory but might help in the future. - let readMeFileName; - let repository; - let hasAutoFixed = false; - - for (let i = 0; i < rootFiles.length; i++) { - if (rootFiles[i].toLowerCase().indexOf('readme') !== -1) readMeFileName = rootFiles[i]; - files.push(rootFiles[i].toLowerCase()); - } - - if (files.indexOf('.git') === -1) { - console.error('No .git folder, aborting'); - process.exit(1); - } - - // do a git pull... - var child_process = require('child_process'); - try { - child_process.execSync('git pull ', {cwd: `${pluginPath}/`}); - } catch (e) { - console.error('Error git pull', e); - } - - try { - const path = `${pluginPath}/.github/workflows/npmpublish.yml`; - if (!fs.existsSync(path)) { - console.log('no .github/workflows/npmpublish.yml, create one and set npm secret to auto publish to npm on commit'); - if (autoFix) { - const npmpublish = - fs.readFileSync('bin/plugins/lib/npmpublish.yml', {encoding: 'utf8', flag: 'r'}); - fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true}); - fs.writeFileSync(path, npmpublish); - hasAutoFixed = true; - console.log("If you haven't already, setup autopublish for this plugin https://github.com/ether/etherpad-lite/wiki/Plugins:-Automatically-publishing-to-npm-on-commit-to-Github-Repo"); - } else { - console.log('Setup autopublish for this plugin https://github.com/ether/etherpad-lite/wiki/Plugins:-Automatically-publishing-to-npm-on-commit-to-Github-Repo'); - } - } else { - // autopublish exists, we should check the version.. - // checkVersion takes two file paths and checks for a version string in them. - const currVersionFile = fs.readFileSync(path, {encoding: 'utf8', flag: 'r'}); - const existingConfigLocation = currVersionFile.indexOf('##ETHERPAD_NPM_V='); - const existingValue = parseInt(currVersionFile.substr(existingConfigLocation + 17, existingConfigLocation.length)); - - const reqVersionFile = fs.readFileSync('bin/plugins/lib/npmpublish.yml', {encoding: 'utf8', flag: 'r'}); - const reqConfigLocation = reqVersionFile.indexOf('##ETHERPAD_NPM_V='); - const reqValue = parseInt(reqVersionFile.substr(reqConfigLocation + 17, reqConfigLocation.length)); - - if (!existingValue || (reqValue > existingValue)) { - const npmpublish = - fs.readFileSync('bin/plugins/lib/npmpublish.yml', {encoding: 'utf8', flag: 'r'}); - fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true}); - fs.writeFileSync(path, npmpublish); - hasAutoFixed = true; - } - } - } catch (err) { - console.error(err); - } - - - try { - const path = `${pluginPath}/.github/workflows/backend-tests.yml`; - if (!fs.existsSync(path)) { - console.log('no .github/workflows/backend-tests.yml, create one and set npm secret to auto publish to npm on commit'); - if (autoFix) { - const backendTests = - fs.readFileSync('bin/plugins/lib/backend-tests.yml', {encoding: 'utf8', flag: 'r'}); - fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true}); - fs.writeFileSync(path, backendTests); - hasAutoFixed = true; - } - } else { - // autopublish exists, we should check the version.. - // checkVersion takes two file paths and checks for a version string in them. - const currVersionFile = fs.readFileSync(path, {encoding: 'utf8', flag: 'r'}); - const existingConfigLocation = currVersionFile.indexOf('##ETHERPAD_NPM_V='); - const existingValue = parseInt(currVersionFile.substr(existingConfigLocation + 17, existingConfigLocation.length)); - - const reqVersionFile = fs.readFileSync('bin/plugins/lib/backend-tests.yml', {encoding: 'utf8', flag: 'r'}); - const reqConfigLocation = reqVersionFile.indexOf('##ETHERPAD_NPM_V='); - const reqValue = parseInt(reqVersionFile.substr(reqConfigLocation + 17, reqConfigLocation.length)); - - if (!existingValue || (reqValue > existingValue)) { - const backendTests = - fs.readFileSync('bin/plugins/lib/backend-tests.yml', {encoding: 'utf8', flag: 'r'}); - fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true}); - fs.writeFileSync(path, backendTests); - hasAutoFixed = true; - } - } - } catch (err) { - console.error(err); - } - - if (files.indexOf('package.json') === -1) { - console.warn('no package.json, please create'); - } - - if (files.indexOf('package.json') !== -1) { - const packageJSON = fs.readFileSync(`${pluginPath}/package.json`, {encoding: 'utf8', flag: 'r'}); - const parsedPackageJSON = JSON.parse(packageJSON); - if (autoFix) { - let updatedPackageJSON = false; - if (!parsedPackageJSON.funding) { - updatedPackageJSON = true; - parsedPackageJSON.funding = { - type: 'individual', - url: 'https://etherpad.org/', - }; - } - if (updatedPackageJSON) { - hasAutoFixed = true; - fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2)); - } - } - - if (packageJSON.toLowerCase().indexOf('repository') === -1) { - console.warn('No repository in package.json'); - if (autoFix) { - console.warn('Repository not detected in package.json. Please add repository section manually.'); - } - } else { - // useful for creating README later. - repository = parsedPackageJSON.repository.url; - } - - // include lint config - if (packageJSON.toLowerCase().indexOf('devdependencies') === -1 || !parsedPackageJSON.devDependencies.eslint) { - console.warn('Missing eslint reference in devDependencies'); - if (autoFix) { - const devDependencies = { - 'eslint': '^7.14.0', - 'eslint-config-etherpad': '^1.0.13', - 'eslint-plugin-mocha': '^8.0.0', - 'eslint-plugin-node': '^11.1.0', - 'eslint-plugin-prefer-arrow': '^1.2.2', - 'eslint-plugin-promise': '^4.2.1', - }; - hasAutoFixed = true; - parsedPackageJSON.devDependencies = devDependencies; - fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2)); - - const child_process = require('child_process'); - try { - child_process.execSync('npm install', {cwd: `${pluginPath}/`}); - hasAutoFixed = true; - } catch (e) { - console.error('Failed to create package-lock.json'); - } - } - } - - // include peer deps config - if (packageJSON.toLowerCase().indexOf('peerdependencies') === -1 || !parsedPackageJSON.peerDependencies) { - console.warn('Missing peer deps reference in package.json'); - if (autoFix) { - const peerDependencies = { - 'ep_etherpad-lite': '>=1.8.6', - }; - hasAutoFixed = true; - parsedPackageJSON.peerDependencies = peerDependencies; - fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2)); - const child_process = require('child_process'); - try { - child_process.execSync('npm install --no-save ep_etherpad-lite@file:../../src', {cwd: `${pluginPath}/`}); - hasAutoFixed = true; - } catch (e) { - console.error('Failed to create package-lock.json'); - } - } - } - - if (packageJSON.toLowerCase().indexOf('eslintconfig') === -1) { - console.warn('No esLintConfig in package.json'); - if (autoFix) { - const eslintConfig = { - root: true, - extends: 'etherpad/plugin', - }; - hasAutoFixed = true; - parsedPackageJSON.eslintConfig = eslintConfig; - fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2)); - } - } - - if (packageJSON.toLowerCase().indexOf('scripts') === -1) { - console.warn('No scripts in package.json'); - if (autoFix) { - const scripts = { - 'lint': 'eslint .', - 'lint:fix': 'eslint --fix .', - }; - hasAutoFixed = true; - parsedPackageJSON.scripts = scripts; - fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2)); - } - } - - if ((packageJSON.toLowerCase().indexOf('engines') === -1) || !parsedPackageJSON.engines.node) { - console.warn('No engines or node engine in package.json'); - if (autoFix) { - const engines = { - node: '>=10.13.0', - }; - hasAutoFixed = true; - parsedPackageJSON.engines = engines; - fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2)); - } - } - } - - if (files.indexOf('package-lock.json') === -1) { - console.warn('package-lock.json file not found. Please run npm install in the plugin folder and commit the package-lock.json file.'); - if (autoFix) { - var child_process = require('child_process'); - try { - child_process.execSync('npm install', {cwd: `${pluginPath}/`}); - console.log('Making package-lock.json'); - hasAutoFixed = true; - } catch (e) { - console.error('Failed to create package-lock.json'); - } - } - } - - if (files.indexOf('readme') === -1 && files.indexOf('readme.md') === -1) { - console.warn('README.md file not found, please create'); - if (autoFix) { - console.log('Autofixing missing README.md file, please edit the README.md file further to include plugin specific details.'); - let readme = fs.readFileSync('bin/plugins/lib/README.md', {encoding: 'utf8', flag: 'r'}); - readme = readme.replace(/\[plugin_name\]/g, pluginName); - if (repository) { - const org = repository.split('/')[3]; - const name = repository.split('/')[4]; - readme = readme.replace(/\[org_name\]/g, org); - readme = readme.replace(/\[repo_url\]/g, name); - fs.writeFileSync(`${pluginPath}/README.md`, readme); - } else { - console.warn('Unable to find repository in package.json, aborting.'); - } - } - } - - if (files.indexOf('contributing') === -1 && files.indexOf('contributing.md') === -1) { - console.warn('CONTRIBUTING.md file not found, please create'); - if (autoFix) { - console.log('Autofixing missing CONTRIBUTING.md file, please edit the CONTRIBUTING.md file further to include plugin specific details.'); - let contributing = fs.readFileSync('bin/plugins/lib/CONTRIBUTING.md', {encoding: 'utf8', flag: 'r'}); - contributing = contributing.replace(/\[plugin_name\]/g, pluginName); - fs.writeFileSync(`${pluginPath}/CONTRIBUTING.md`, contributing); - } - } - - - if (files.indexOf('readme') !== -1 && files.indexOf('readme.md') !== -1) { - const readme = fs.readFileSync(`${pluginPath}/${readMeFileName}`, {encoding: 'utf8', flag: 'r'}); - if (readme.toLowerCase().indexOf('license') === -1) { - console.warn('No license section in README'); - if (autoFix) { - console.warn('Please add License section to README manually.'); - } - } - } - - if (files.indexOf('license') === -1 && files.indexOf('license.md') === -1) { - console.warn('LICENSE.md file not found, please create'); - if (autoFix) { - hasAutoFixed = true; - console.log('Autofixing missing LICENSE.md file, including Apache 2 license.'); - exec('git config user.name', (error, name, stderr) => { - if (error) { - console.log(`error: ${error.message}`); - return; - } - if (stderr) { - console.log(`stderr: ${stderr}`); - return; - } - let license = fs.readFileSync('bin/plugins/lib/LICENSE.md', {encoding: 'utf8', flag: 'r'}); - license = license.replace('[yyyy]', new Date().getFullYear()); - license = license.replace('[name of copyright owner]', name); - fs.writeFileSync(`${pluginPath}/LICENSE.md`, license); - }); - } - } - - let travisConfig = fs.readFileSync('bin/plugins/lib/travis.yml', {encoding: 'utf8', flag: 'r'}); - travisConfig = travisConfig.replace(/\[plugin_name\]/g, pluginName); - - if (files.indexOf('.travis.yml') === -1) { - console.warn('.travis.yml file not found, please create. .travis.yml is used for automatically CI testing Etherpad. It is useful to know if your plugin breaks another feature for example.'); - // TODO: Make it check version of the .travis file to see if it needs an update. - if (autoFix) { - hasAutoFixed = true; - console.log('Autofixing missing .travis.yml file'); - fs.writeFileSync(`${pluginPath}/.travis.yml`, travisConfig); - console.log('Travis file created, please sign into travis and enable this repository'); - } - } - if (autoFix && autoUpdate) { - // checks the file versioning of .travis and updates it to the latest. - const existingConfig = fs.readFileSync(`${pluginPath}/.travis.yml`, {encoding: 'utf8', flag: 'r'}); - const existingConfigLocation = existingConfig.indexOf('##ETHERPAD_TRAVIS_V='); - const existingValue = parseInt(existingConfig.substr(existingConfigLocation + 20, existingConfig.length)); - - const newConfigLocation = travisConfig.indexOf('##ETHERPAD_TRAVIS_V='); - const newValue = parseInt(travisConfig.substr(newConfigLocation + 20, travisConfig.length)); - if (existingConfigLocation === -1) { - console.warn('no previous .travis.yml version found so writing new.'); - // we will write the newTravisConfig to the location. - fs.writeFileSync(`${pluginPath}/.travis.yml`, travisConfig); - } else if (newValue > existingValue) { - console.log('updating .travis.yml'); - fs.writeFileSync(`${pluginPath}/.travis.yml`, travisConfig); - hasAutoFixed = true; - }// - } - - if (files.indexOf('.gitignore') === -1) { - console.warn(".gitignore file not found, please create. .gitignore files are useful to ensure files aren't incorrectly commited to a repository."); - if (autoFix) { - hasAutoFixed = true; - console.log('Autofixing missing .gitignore file'); - const gitignore = fs.readFileSync('bin/plugins/lib/gitignore', {encoding: 'utf8', flag: 'r'}); - fs.writeFileSync(`${pluginPath}/.gitignore`, gitignore); - } - } else { - let gitignore = - fs.readFileSync(`${pluginPath}/.gitignore`, {encoding: 'utf8', flag: 'r'}); - if (gitignore.indexOf('node_modules/') === -1) { - console.warn('node_modules/ missing from .gitignore'); - if (autoFix) { - gitignore += 'node_modules/'; - fs.writeFileSync(`${pluginPath}/.gitignore`, gitignore); - hasAutoFixed = true; - } - } - } - - // if we include templates but don't have translations... - if (files.indexOf('templates') !== -1 && files.indexOf('locales') === -1) { - console.warn('Translations not found, please create. Translation files help with Etherpad accessibility.'); - } - - - if (files.indexOf('.ep_initialized') !== -1) { - console.warn('.ep_initialized found, please remove. .ep_initialized should never be commited to git and should only exist once the plugin has been executed one time.'); - if (autoFix) { - hasAutoFixed = true; - console.log('Autofixing incorrectly existing .ep_initialized file'); - fs.unlinkSync(`${pluginPath}/.ep_initialized`); - } - } - - if (files.indexOf('npm-debug.log') !== -1) { - console.warn('npm-debug.log found, please remove. npm-debug.log should never be commited to your repository.'); - if (autoFix) { - hasAutoFixed = true; - console.log('Autofixing incorrectly existing npm-debug.log file'); - fs.unlinkSync(`${pluginPath}/npm-debug.log`); - } - } - - if (files.indexOf('static') !== -1) { - fs.readdir(`${pluginPath}/static`, (errRead, staticFiles) => { - if (staticFiles.indexOf('tests') === -1) { - console.warn('Test files not found, please create tests. https://github.com/ether/etherpad-lite/wiki/Creating-a-plugin#writing-and-running-front-end-tests-for-your-plugin'); - } - }); - } else { - console.warn('Test files not found, please create tests. https://github.com/ether/etherpad-lite/wiki/Creating-a-plugin#writing-and-running-front-end-tests-for-your-plugin'); - } - - // linting begins - if (autoFix) { - var lintCmd = 'npm run lint:fix'; - } else { - var lintCmd = 'npm run lint'; - } - - try { - child_process.execSync(lintCmd, {cwd: `${pluginPath}/`}); - console.log('Linting...'); - if (autoFix) { - // todo: if npm run lint doesn't do anything no need for... - hasAutoFixed = true; - } - } catch (e) { - // it is gonna throw an error anyway - console.log('Manual linting probably required, check with: npm run lint'); - } - // linting ends. - - if (hasAutoFixed) { - console.log('Fixes applied, please check git diff then run the following command:\n\n'); - // bump npm Version - if (autoCommit) { - // holy shit you brave. - console.log('Attempting autocommit and auto publish to npm'); - // github should push to npm for us :) - exec(`cd node_modules/${pluginName} && git rm -rf node_modules --ignore-unmatch && git add -A && git commit --allow-empty -m 'autofixes from Etherpad checkPlugins.js' && git push && cd ../..`, (error, name, stderr) => { - if (error) { - console.log(`error: ${error.message}`); - return; - } - if (stderr) { - console.log(`stderr: ${stderr}`); - return; - } - console.log("I think she's got it! By George she's got it!"); - process.exit(0); - }); - } else { - console.log(`cd node_modules/${pluginName} && git add -A && git commit --allow-empty -m 'autofixes from Etherpad checkPlugins.js' && npm version patch && git add package.json && git commit --allow-empty -m 'bump version' && git push && npm publish && cd ../..`); - } - } - - console.log('Finished'); -}); diff --git a/bin/plugins/lib/travis.yml b/bin/plugins/lib/travis.yml deleted file mode 100644 index 099d7e445..000000000 --- a/bin/plugins/lib/travis.yml +++ /dev/null @@ -1,70 +0,0 @@ -language: node_js - -node_js: - - "lts/*" - -cache: false - -services: - - docker - -install: - - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" - -#script: -# - "tests/frontend/travis/runner.sh" - -env: - global: - - secure: "WMGxFkOeTTlhWB+ChMucRtIqVmMbwzYdNHuHQjKCcj8HBEPdZLfCuK/kf4rG\nVLcLQiIsyllqzNhBGVHG1nyqWr0/LTm8JRqSCDDVIhpyzp9KpCJQQJG2Uwjk\n6/HIJJh/wbxsEdLNV2crYU/EiVO3A4Bq0YTHUlbhUqG3mSCr5Ec=" - - secure: "gejXUAHYscbR6Bodw35XexpToqWkv2ifeECsbeEmjaLkYzXmUUNWJGknKSu7\nEUsSfQV8w+hxApr1Z+jNqk9aX3K1I4btL3cwk2trnNI8XRAvu1c1Iv60eerI\nkE82Rsd5lwUaMEh+/HoL8ztFCZamVndoNgX7HWp5J/NRZZMmh4g=" - -jobs: - include: - - name: "Lint test package-lock" - install: - - "npm install lockfile-lint" - script: - - npx lockfile-lint --path package-lock.json --validate-https --allowed-hosts npm - - name: "Run the Backend tests" - before_install: - - sudo add-apt-repository -y ppa:libreoffice/ppa - - sudo apt-get update - - sudo apt-get -y install libreoffice - - sudo apt-get -y install libreoffice-pdfimport - install: - - "npm install" - - "mkdir [plugin_name]" - - "mv !([plugin_name]) [plugin_name]" - - "git clone https://github.com/ether/etherpad-lite.git etherpad" - - "cd etherpad" - - "mkdir -p node_modules" - - "mv ../[plugin_name] node_modules" - - "bin/installDeps.sh" - - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" - - "cd src && npm install && cd -" - script: - - "tests/frontend/travis/runnerBackend.sh" - - name: "Test the Frontend" - before_script: - - "tests/frontend/travis/sauce_tunnel.sh" - install: - - "npm install" - - "mkdir [plugin_name]" - - "mv !([plugin_name]) [plugin_name]" - - "git clone https://github.com/ether/etherpad-lite.git etherpad" - - "cd etherpad" - - "mkdir -p node_modules" - - "mv ../[plugin_name] node_modules" - - "bin/installDeps.sh" - - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" - script: - - "tests/frontend/travis/runner.sh" - -notifications: - irc: - channels: - - "irc.freenode.org#etherpad-lite-dev" - -##ETHERPAD_TRAVIS_V=9 -## Travis configuration automatically created using bin/plugins/updateAllPluginsScript.sh diff --git a/bin/rebuildPad.js b/bin/rebuildPad.js deleted file mode 100644 index 12ff21847..000000000 --- a/bin/rebuildPad.js +++ /dev/null @@ -1,126 +0,0 @@ -/* - This is a repair tool. It rebuilds an old pad at a new pad location up to a - known "good" revision. -*/ - -if (process.argv.length != 4 && process.argv.length != 5) { - console.error('Use: node bin/repairPad.js $PADID $REV [$NEWPADID]'); - process.exit(1); -} - -const npm = require('../src/node_modules/npm'); -const async = require('../src/node_modules/async'); -const ueberDB = require('../src/node_modules/ueberdb2'); - -const padId = process.argv[2]; -const newRevHead = process.argv[3]; -const newPadId = process.argv[4] || `${padId}-rebuilt`; - -let db, oldPad, newPad, settings; -let AuthorManager, ChangeSet, Pad, PadManager; - -async.series([ - function (callback) { - npm.load({}, (err) => { - if (err) { - console.error(`Could not load NPM: ${err}`); - process.exit(1); - } else { - callback(); - } - }); - }, - function (callback) { - // Get a handle into the database - db = require('../src/node/db/DB'); - db.init(callback); - }, - function (callback) { - PadManager = require('../src/node/db/PadManager'); - Pad = require('../src/node/db/Pad').Pad; - // Get references to the original pad and to a newly created pad - // HACK: This is a standalone script, so we want to write everything - // out to the database immediately. The only problem with this is - // that a driver (like the mysql driver) can hardcode these values. - db.db.db.settings = {cache: 0, writeInterval: 0, json: true}; - // Validate the newPadId if specified and that a pad with that ID does - // not already exist to avoid overwriting it. - if (!PadManager.isValidPadId(newPadId)) { - console.error('Cannot create a pad with that id as it is invalid'); - process.exit(1); - } - PadManager.doesPadExists(newPadId, (err, exists) => { - if (exists) { - console.error('Cannot create a pad with that id as it already exists'); - process.exit(1); - } - }); - PadManager.getPad(padId, (err, pad) => { - oldPad = pad; - newPad = new Pad(newPadId); - callback(); - }); - }, - function (callback) { - // Clone all Chat revisions - const chatHead = oldPad.chatHead; - for (var i = 0, curHeadNum = 0; i <= chatHead; i++) { - db.db.get(`pad:${padId}:chat:${i}`, (err, chat) => { - db.db.set(`pad:${newPadId}:chat:${curHeadNum++}`, chat); - console.log(`Created: Chat Revision: pad:${newPadId}:chat:${curHeadNum}`); - }); - } - callback(); - }, - function (callback) { - // Rebuild Pad from revisions up to and including the new revision head - AuthorManager = require('../src/node/db/AuthorManager'); - Changeset = require('ep_etherpad-lite/static/js/Changeset'); - // Author attributes are derived from changesets, but there can also be - // non-author attributes with specific mappings that changesets depend on - // and, AFAICT, cannot be recreated any other way - newPad.pool.numToAttrib = oldPad.pool.numToAttrib; - for (let curRevNum = 0; curRevNum <= newRevHead; curRevNum++) { - db.db.get(`pad:${padId}:revs:${curRevNum}`, (err, rev) => { - if (rev.meta) { - throw 'The specified revision number could not be found.'; - } - const newRevNum = ++newPad.head; - const newRevId = `pad:${newPad.id}:revs:${newRevNum}`; - db.db.set(newRevId, rev); - AuthorManager.addPad(rev.meta.author, newPad.id); - newPad.atext = Changeset.applyToAText(rev.changeset, newPad.atext, newPad.pool); - console.log(`Created: Revision: pad:${newPad.id}:revs:${newRevNum}`); - if (newRevNum == newRevHead) { - callback(); - } - }); - } - }, - function (callback) { - // Add saved revisions up to the new revision head - console.log(newPad.head); - const newSavedRevisions = []; - for (const i in oldPad.savedRevisions) { - savedRev = oldPad.savedRevisions[i]; - if (savedRev.revNum <= newRevHead) { - newSavedRevisions.push(savedRev); - console.log(`Added: Saved Revision: ${savedRev.revNum}`); - } - } - newPad.savedRevisions = newSavedRevisions; - callback(); - }, - function (callback) { - // Save the source pad - db.db.set(`pad:${newPadId}`, newPad, (err) => { - console.log(`Created: Source Pad: pad:${newPadId}`); - newPad.saveToDatabase().then(() => callback(), callback); - }); - }, -], (err) => { - if (err) { throw err; } else { - console.info('finished'); - process.exit(0); - } -}); diff --git a/bin/release.js b/bin/release.js deleted file mode 100644 index 281434409..000000000 --- a/bin/release.js +++ /dev/null @@ -1,67 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const child_process = require('child_process'); -const semver = require('../src/node_modules/semver'); - -/* - -Usage - -node bin/release.js patch - -*/ -const usage = 'node bin/release.js [patch/minor/major] -- example: "node bin/release.js patch"'; - -const release = process.argv[2]; - -if(!release) { - console.log(usage); - throw new Error('No release type included'); -} - -const changelog = fs.readFileSync('CHANGELOG.md', {encoding: 'utf8', flag: 'r'}); -let packageJson = fs.readFileSync('./src/package.json', {encoding: 'utf8', flag: 'r'}); -packageJson = JSON.parse(packageJson); -const currentVersion = packageJson.version; - -const newVersion = semver.inc(currentVersion, release); -if(!newVersion) { - console.log(usage); - throw new Error('Unable to generate new version from input'); -} - -const changelogIncludesVersion = changelog.indexOf(newVersion) !== -1; - -if(!changelogIncludesVersion) { - throw new Error('No changelog record for ', newVersion, ' - please create changelog record'); -} - -console.log('Okay looks good, lets create the package.json and package-lock.json'); - -packageJson.version = newVersion; - -fs.writeFileSync('src/package.json', JSON.stringify(packageJson, null, 2)); - -// run npm version `release` where release is patch, minor or major -child_process.execSync('npm install --package-lock-only', {cwd: `src/`}); -// run npm install --package-lock-only <-- required??? - -child_process.execSync(`git checkout -b release/${newVersion}`); -child_process.execSync(`git add src/package.json`); -child_process.execSync(`git add src/package-lock.json`); -child_process.execSync(`git commit -m 'bump version'`); -child_process.execSync(`git push origin release/${newVersion}`); - - -child_process.execSync(`make docs`); -child_process.execSync(`git clone git@github.com:ether/ether.github.com.git`); -child_process.execSync(`cp -R out/doc/ ether.github.com/doc/${newVersion}`); - -console.log('Once merged into master please run the following commands'); -console.log(`git tag -a ${newVersion} -m ${newVersion} && git push origin master`); -console.log(`cd ether.github.com && git add . && git commit -m '${newVersion} docs'`); -console.log(`Build the windows zip`) -console.log(`Visit https://github.com/ether/etherpad-lite/releases/new and create a new release with 'master' as the target and the version is ${newVersion}. Include the windows zip as an assett`) -console.log('Once the new docs are uploaded then modify the download link on etherpad.org and then pull master onto develop'); -console.log('Finally go public with an announcement via our comms channels :)'); diff --git a/bin/repairPad.js b/bin/repairPad.js deleted file mode 100644 index 8408e4b72..000000000 --- a/bin/repairPad.js +++ /dev/null @@ -1,77 +0,0 @@ -/* - * This is a repair tool. It extracts all datas of a pad, removes and inserts them again. - */ - -console.warn('WARNING: This script must not be used while etherpad is running!'); - -if (process.argv.length != 3) { - console.error('Use: node bin/repairPad.js $PADID'); - process.exit(1); -} - -// get the padID -const padId = process.argv[2]; - -const npm = require('../src/node_modules/npm'); -npm.load({}, async (er) => { - if (er) { - console.error(`Could not load NPM: ${er}`); - process.exit(1); - } - - try { - // intialize database - const settings = require('../src/node/utils/Settings'); - const db = require('../src/node/db/DB'); - await db.init(); - - // get the pad - const padManager = require('../src/node/db/PadManager'); - const pad = await padManager.getPad(padId); - - // accumulate the required keys - const neededDBValues = [`pad:${padId}`]; - - // add all authors - neededDBValues.push(...pad.getAllAuthors().map((author) => 'globalAuthor:')); - - // add all revisions - for (let rev = 0; rev <= pad.head; ++rev) { - neededDBValues.push(`pad:${padId}:revs:${rev}`); - } - - // add all chat values - for (let chat = 0; chat <= pad.chatHead; ++chat) { - neededDBValues.push(`pad:${padId}:chat:${chat}`); - } - - // - // NB: this script doesn't actually does what's documented - // since the `value` fields in the following `.forEach` - // block are just the array index numbers - // - // the script therefore craps out now before it can do - // any damage. - // - // See gitlab issue #3545 - // - console.info('aborting [gitlab #3545]'); - process.exit(1); - - // now fetch and reinsert every key - neededDBValues.forEach((key, value) => { - console.log(`Key: ${key}, value: ${value}`); - db.remove(key); - db.set(key, value); - }); - - console.info('finished'); - process.exit(0); - } catch (er) { - if (er.name === 'apierror') { - console.error(er); - } else { - console.trace(er); - } - } -}); diff --git a/doc/api/embed_parameters.md b/doc/api/embed_parameters.md index 79b60f214..d6f27af05 100644 --- a/doc/api/embed_parameters.md +++ b/doc/api/embed_parameters.md @@ -3,10 +3,10 @@ You can easily embed your etherpad-lite into any webpage by using iframes. You c Example: -Cut and paste the following code into any webpage to embed a pad. The parameters below will hide the chat and the line numbers. +Cut and paste the following code into any webpage to embed a pad. The parameters below will hide the chat and the line numbers and will auto-focus on Line 4. ``` - + ``` ## showLineNumbers @@ -66,3 +66,10 @@ Example: `lang=ar` (translates the interface into Arabic) Default: true Displays pad text from right to left. +## #L + * Int + +Default: 0 +Focuses pad at specific line number and places caret at beginning of this line +Special note: Is not a URL parameter but instead of a Hash value + diff --git a/doc/api/hooks_client-side.md b/doc/api/hooks_client-side.md index 799fd5ce5..e87536cfe 100755 --- a/doc/api/hooks_client-side.md +++ b/doc/api/hooks_client-side.md @@ -1,5 +1,7 @@ # Client-side hooks -Most of these hooks are called during or in order to set up the formatting process. + +Most of these hooks are called during or in order to set up the formatting +process. ## documentReady Called from: src/templates/pad.html @@ -11,6 +13,7 @@ nothing This hook proxies the functionality of jQuery's `$(document).ready` event. ## aceDomLinePreProcessLineAttributes + Called from: src/static/js/domline.js Things in context: @@ -18,15 +21,21 @@ Things in context: 1. domline - The current DOM line being processed 2. cls - The class of the current block element (useful for styling) -This hook is called for elements in the DOM that have the "lineMarkerAttribute" set. You can add elements into this category with the aceRegisterBlockElements hook above. This hook is run BEFORE the numbered and ordered lists logic is applied. +This hook is called for elements in the DOM that have the "lineMarkerAttribute" +set. You can add elements into this category with the aceRegisterBlockElements +hook above. This hook is run BEFORE the numbered and ordered lists logic is +applied. The return value of this hook should have the following structure: `{ preHtml: String, postHtml: String, processedMarker: Boolean }` -The preHtml and postHtml values will be added to the HTML display of the element, and if processedMarker is true, the engine won't try to process it any more. +The preHtml and postHtml values will be added to the HTML display of the +element, and if processedMarker is true, the engine won't try to process it any +more. ## aceDomLineProcessLineAttributes + Called from: src/static/js/domline.js Things in context: @@ -34,15 +43,21 @@ Things in context: 1. domline - The current DOM line being processed 2. cls - The class of the current block element (useful for styling) -This hook is called for elements in the DOM that have the "lineMarkerAttribute" set. You can add elements into this category with the aceRegisterBlockElements hook above. This hook is run AFTER the ordered and numbered lists logic is applied. +This hook is called for elements in the DOM that have the "lineMarkerAttribute" +set. You can add elements into this category with the aceRegisterBlockElements +hook above. This hook is run AFTER the ordered and numbered lists logic is +applied. The return value of this hook should have the following structure: `{ preHtml: String, postHtml: String, processedMarker: Boolean }` -The preHtml and postHtml values will be added to the HTML display of the element, and if processedMarker is true, the engine won't try to process it any more. +The preHtml and postHtml values will be added to the HTML display of the +element, and if processedMarker is true, the engine won't try to process it any +more. ## aceCreateDomLine + Called from: src/static/js/domline.js Things in context: @@ -50,43 +65,55 @@ Things in context: 1. domline - the current DOM line being processed 2. cls - The class of the current element (useful for styling) -This hook is called for any line being processed by the formatting engine, unless the aceDomLineProcessLineAttributes hook from above returned true, in which case this hook is skipped. +This hook is called for any line being processed by the formatting engine, +unless the aceDomLineProcessLineAttributes hook from above returned true, in +which case this hook is skipped. The return value of this hook should have the following structure: `{ extraOpenTags: String, extraCloseTags: String, cls: String }` -extraOpenTags and extraCloseTags will be added before and after the element in question, and cls will be the new class of the element going forward. +extraOpenTags and extraCloseTags will be added before and after the element in +question, and cls will be the new class of the element going forward. ## acePostWriteDomLineHTML + Called from: src/static/js/domline.js Things in context: 1. node - the DOM node that just got written to the page -This hook is for right after a node has been fully formatted and written to the page. +This hook is for right after a node has been fully formatted and written to the +page. ## aceAttribsToClasses + Called from: src/static/js/linestylefilter.js Things in context: -1. linestylefilter - the JavaScript object that's currently processing the ace attributes +1. linestylefilter - the JavaScript object that's currently processing the ace + attributes 2. key - the current attribute being processed 3. value - the value of the attribute being processed -This hook is called during the attribute processing procedure, and should be used to translate key, value pairs into valid HTML classes that can be inserted into the DOM. +This hook is called during the attribute processing procedure, and should be +used to translate key, value pairs into valid HTML classes that can be inserted +into the DOM. -The return value for this function should be a list of classes, which will then be parsed into a valid class string. +The return value for this function should be a list of classes, which will then +be parsed into a valid class string. ## aceAttribClasses + Called from: src/static/js/linestylefilter.js Things in context: 1. Attributes - Object of Attributes -This hook is called when attributes are investigated on a line. It is useful if you want to add another attribute type or property type to a pad. +This hook is called when attributes are investigated on a line. It is useful if +you want to add another attribute type or property type to a pad. Example: ``` @@ -97,32 +124,45 @@ exports.aceAttribClasses = function(hook_name, attr, cb){ ``` ## aceGetFilterStack + Called from: src/static/js/linestylefilter.js Things in context: -1. linestylefilter - the JavaScript object that's currently processing the ace attributes +1. linestylefilter - the JavaScript object that's currently processing the ace + attributes 2. browser - an object indicating which browser is accessing the page -This hook is called to apply custom regular expression filters to a set of styles. The one example available is the ep_linkify plugin, which adds internal links. They use it to find the telltale `[[ ]]` syntax that signifies internal links, and finding that syntax, they add in the internalHref attribute to be later used by the aceCreateDomLine hook (documented above). +This hook is called to apply custom regular expression filters to a set of +styles. The one example available is the ep_linkify plugin, which adds internal +links. They use it to find the telltale `[[ ]]` syntax that signifies internal +links, and finding that syntax, they add in the internalHref attribute to be +later used by the aceCreateDomLine hook (documented above). ## aceEditorCSS + Called from: src/static/js/ace.js Things in context: None -This hook is provided to allow custom CSS files to be loaded. The return value should be an array of resource urls or paths relative to the plugins directory. +This hook is provided to allow custom CSS files to be loaded. The return value +should be an array of resource urls or paths relative to the plugins directory. ## aceInitInnerdocbodyHead + Called from: src/static/js/ace.js Things in context: 1. iframeHTML - the HTML of the editor iframe up to this point, in array format -This hook is called during the creation of the editor HTML. The array should have lines of HTML added to it, giving the plugin author a chance to add in meta, script, link, and other tags that go into the `` element of the editor HTML document. +This hook is called during the creation of the editor HTML. The array should +have lines of HTML added to it, giving the plugin author a chance to add in +meta, script, link, and other tags that go into the `` element of the +editor HTML document. ## aceEditEvent + Called from: src/static/js/ace2_inner.js Things in context: @@ -130,16 +170,25 @@ Things in context: 1. callstack - a bunch of information about the current action 2. editorInfo - information about the user who is making the change 3. rep - information about where the change is being made -4. documentAttributeManager - information about attributes in the document (this is a mystery to me) +4. documentAttributeManager - information about attributes in the document (this + is a mystery to me) -This hook is made available to edit the edit events that might occur when changes are made. Currently you can change the editor information, some of the meanings of the edit, and so on. You can also make internal changes (internal to your plugin) that use the information provided by the edit event. +This hook is made available to edit the edit events that might occur when +changes are made. Currently you can change the editor information, some of the +meanings of the edit, and so on. You can also make internal changes (internal to +your plugin) that use the information provided by the edit event. ## aceRegisterNonScrollableEditEvents + Called from: src/static/js/ace2_inner.js Things in context: None -When aceEditEvent (documented above) finishes processing the event, it scrolls the viewport to make caret visible to the user, but if you don't want that behavior to happen you can use this hook to register which edit events should not scroll viewport. The return value of this hook should be a list of event names. +When aceEditEvent (documented above) finishes processing the event, it scrolls +the viewport to make caret visible to the user, but if you don't want that +behavior to happen you can use this hook to register which edit events should +not scroll viewport. The return value of this hook should be a list of event +names. Example: ``` @@ -149,24 +198,32 @@ exports.aceRegisterNonScrollableEditEvents = function(){ ``` ## aceRegisterBlockElements + Called from: src/static/js/ace2_inner.js Things in context: None -The return value of this hook will add elements into the "lineMarkerAttribute" category, making the aceDomLineProcessLineAttributes hook (documented below) call for those elements. +The return value of this hook will add elements into the "lineMarkerAttribute" +category, making the aceDomLineProcessLineAttributes hook (documented below) +call for those elements. ## aceInitialized + Called from: src/static/js/ace2_inner.js Things in context: -1. editorInfo - information about the user who will be making changes through the interface, and a way to insert functions into the main ace object (see ep_headings) +1. editorInfo - information about the user who will be making changes through + the interface, and a way to insert functions into the main ace object (see + ep_headings) 2. rep - information about where the user's cursor is 3. documentAttributeManager - some kind of magic -This hook is for inserting further information into the ace engine, for later use in formatting hooks. +This hook is for inserting further information into the ace engine, for later +use in formatting hooks. ## postAceInit + Called from: src/static/js/pad.js Things in context: @@ -175,6 +232,7 @@ Things in context: 2. pad - the pad object of the current pad. ## postToolbarInit + Called from: src/static/js/pad_editbar.js Things in context: @@ -189,30 +247,37 @@ Usage examples: * [https://github.com/tiblu/ep_authorship_toggle]() ## postTimesliderInit + Called from: src/static/js/timeslider.js -There doesn't appear to be any example available of this particular hook being used, but it gets fired after the timeslider is all set up. +There doesn't appear to be any example available of this particular hook being +used, but it gets fired after the timeslider is all set up. ## goToRevisionEvent + Called from: src/static/js/broadcast.js Things in context: 1. rev - The newRevision -This hook gets fired both on timeslider load (as timeslider shows a new revision) and when the new revision is showed to a user. -There doesn't appear to be any example available of this particular hook being used. +This hook gets fired both on timeslider load (as timeslider shows a new +revision) and when the new revision is showed to a user. There doesn't appear to +be any example available of this particular hook being used. ## userJoinOrUpdate + Called from: src/static/js/pad_userlist.js Things in context: 1. info - the user information -This hook is called on the client side whenever a user joins or changes. This can be used to create notifications or an alternate user list. +This hook is called on the client side whenever a user joins or changes. This +can be used to create notifications or an alternate user list. ## chatNewMessage + Called from: src/static/js/chat.js Things in context: @@ -220,14 +285,18 @@ Things in context: 1. authorName - The user that wrote this message 2. author - The authorID of the user that wrote the message 3. text - the message text -4. sticky (boolean) - if you want the gritter notification bubble to fade out on its own or just sit there +4. sticky (boolean) - if you want the gritter notification bubble to fade out on + its own or just sit there 5. timestamp - the timestamp of the chat message 6. timeStr - the timestamp as a formatted string -7. duration - for how long in milliseconds should the gritter notification appear (0 to disable) +7. duration - for how long in milliseconds should the gritter notification + appear (0 to disable) -This hook is called on the client side whenever a chat message is received from the server. It can be used to create different notifications for chat messages. +This hook is called on the client side whenever a chat message is received from +the server. It can be used to create different notifications for chat messages. ## collectContentPre + Called from: src/static/js/contentcollector.js Things in context: @@ -238,16 +307,20 @@ Things in context: 4. styl - the style applied to the node (probably CSS) -- Note the typo 5. cls - the HTML class string of the node -This hook is called before the content of a node is collected by the usual methods. The cc object can be used to do a bunch of things that modify the content of the pad. See, for example, the heading1 plugin for etherpad original. +This hook is called before the content of a node is collected by the usual +methods. The cc object can be used to do a bunch of things that modify the +content of the pad. See, for example, the heading1 plugin for etherpad original. -E.g. if you need to apply an attribute to newly inserted characters, -call cc.doAttrib(state, "attributeName") which results in an attribute attributeName=true. +E.g. if you need to apply an attribute to newly inserted characters, call +cc.doAttrib(state, "attributeName") which results in an attribute +attributeName=true. -If you want to specify also a value, call cc.doAttrib(state, "attributeName::value") -which results in an attribute attributeName=value. +If you want to specify also a value, call cc.doAttrib(state, +"attributeName::value") which results in an attribute attributeName=value. ## collectContentImage + Called from: src/static/js/contentcollector.js Things in context: @@ -259,7 +332,9 @@ Things in context: 5. cls - the HTML class string of the node 6. node - the node being modified -This hook is called before the content of an image node is collected by the usual methods. The cc object can be used to do a bunch of things that modify the content of the pad. +This hook is called before the content of an image node is collected by the +usual methods. The cc object can be used to do a bunch of things that modify the +content of the pad. Example: @@ -271,6 +346,7 @@ exports.collectContentImage = function(name, context){ ``` ## collectContentPost + Called from: src/static/js/contentcollector.js Things in context: @@ -281,20 +357,29 @@ Things in context: 4. style - the style applied to the node (probably CSS) 5. cls - the HTML class string of the node -This hook is called after the content of a node is collected by the usual methods. The cc object can be used to do a bunch of things that modify the content of the pad. See, for example, the heading1 plugin for etherpad original. +This hook is called after the content of a node is collected by the usual +methods. The cc object can be used to do a bunch of things that modify the +content of the pad. See, for example, the heading1 plugin for etherpad original. ## handleClientMessage_`name` + Called from: `src/static/js/collab_client.js` Things in context: -1. payload - the data that got sent with the message (use it for custom message content) +1. payload - the data that got sent with the message (use it for custom message + content) -This hook gets called every time the client receives a message of type `name`. This can most notably be used with the new HTTP API call, "sendClientsMessage", which sends a custom message type to all clients connected to a pad. You can also use this to handle existing types. +This hook gets called every time the client receives a message of type `name`. +This can most notably be used with the new HTTP API call, "sendClientsMessage", +which sends a custom message type to all clients connected to a pad. You can +also use this to handle existing types. -`collab_client.js` has a pretty extensive list of message types, if you want to take a look. +`collab_client.js` has a pretty extensive list of message types, if you want to +take a look. + +## aceStartLineAndCharForPoint-aceEndLineAndCharForPoint -##aceStartLineAndCharForPoint-aceEndLineAndCharForPoint Called from: src/static/js/ace2_inner.js Things in context: @@ -306,10 +391,11 @@ Things in context: 5. point - the starting/ending element where the cursor highlights 6. documentAttributeManager - information about attributes in the document -This hook is provided to allow a plugin to turn DOM node selection into [line,char] selection. -The return value should be an array of [line,char] +This hook is provided to allow a plugin to turn DOM node selection into +[line,char] selection. The return value should be an array of [line,char] + +## aceKeyEvent -##aceKeyEvent Called from: src/static/js/ace2_inner.js Things in context: @@ -323,7 +409,8 @@ Things in context: This hook is provided to allow a plugin to handle key events. The return value should be true if you have handled the event. -##collectContentLineText +## collectContentLineText + Called from: src/static/js/contentcollector.js Things in context: @@ -333,10 +420,24 @@ Things in context: 3. tname - the tag name of this node currently being processed 4. text - the text for that line -This hook allows you to validate/manipulate the text before it's sent to the server side. -The return value should be the validated/manipulated text. +This hook allows you to validate/manipulate the text before it's sent to the +server side. To change the text, either: + +* Set the `text` context property to the desired value and return `undefined`. +* (Deprecated) Return a string. If a hook function changes the `text` context + property, the return value is ignored. If no hook function changes `text` but + multiple hook functions return a string, the first one wins. + +Example: + +``` +exports.collectContentLineText = (hookName, context) => { + context.text = tweakText(context.text); +}; +``` + +## collectContentLineBreak -##collectContentLineBreak Called from: src/static/js/contentcollector.js Things in context: @@ -345,24 +446,27 @@ Things in context: 2. state - the current state of the change being made 3. tname - the tag name of this node currently being processed -This hook is provided to allow whether the br tag should induce a new magic domline or not. -The return value should be either true(break the line) or false. +This hook is provided to allow whether the br tag should induce a new magic +domline or not. The return value should be either true(break the line) or false. + +## disableAuthorColorsForThisLine -##disableAuthorColorsForThisLine Called from: src/static/js/linestylefilter.js Things in context: -1. linestylefilter - the JavaScript object that's currently processing the ace attributes +1. linestylefilter - the JavaScript object that's currently processing the ace + attributes 2. text - the line text 3. class - line class -This hook is provided to allow whether a given line should be deliniated with multiple authors. -Multiple authors in one line cause the creation of magic span lines. This might not suit you and -now you can disable it and handle your own deliniation. -The return value should be either true(disable) or false. +This hook is provided to allow whether a given line should be deliniated with +multiple authors. Multiple authors in one line cause the creation of magic span +lines. This might not suit you and now you can disable it and handle your own +deliniation. The return value should be either true(disable) or false. ## aceSetAuthorStyle + Called from: src/static/js/ace2_inner.js Things in context: @@ -374,10 +478,12 @@ Things in context: 5. author - author info 6. authorSelector - css selector for author span in inner ace -This hook is provided to allow author highlight style to be modified. -Registered hooks should return 1 if the plugin handles highlighting. If no plugin returns 1, the core will use the default background-based highlighting. +This hook is provided to allow author highlight style to be modified. Registered +hooks should return 1 if the plugin handles highlighting. If no plugin returns +1, the core will use the default background-based highlighting. ## aceSelectionChanged + Called from: src/static/js/ace2_inner.js Things in context: diff --git a/doc/api/http_api.md b/doc/api/http_api.md index fb570a393..0cfc85a07 100644 --- a/doc/api/http_api.md +++ b/doc/api/http_api.md @@ -263,7 +263,7 @@ deletes a session #### getSessionInfo(sessionID) * API >= 1 -returns informations about a session +returns information about a session *Example returns:* * `{code: 0, message:"ok", data: {authorID: "a.s8oes9dhwrvt0zif", groupID: g.s8oes9dhwrvt0zif, validUntil: 1312201246}}` diff --git a/doc/docker.md b/doc/docker.md index e12a8fb12..e6c343dcd 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -168,6 +168,8 @@ For the editor container, you can also make it full width by adding `full-width- | `IMPORT_MAX_FILE_SIZE` | maximum allowed file size when importing a pad, in bytes. | `52428800` (50 MB) | | `IMPORT_EXPORT_MAX_REQ_PER_IP` | maximum number of import/export calls per IP. | `10` | | `IMPORT_EXPORT_RATE_LIMIT_WINDOW` | the call rate for import/export requests will be estimated in this time window (in milliseconds) | `90000` | +| `COMMIT_RATE_LIMIT_DURATION` | duration of the rate limit window for commits by individual users/IPs (in seconds) | `1` | +| `COMMIT_RATE_LIMIT_POINTS` | maximum number of changes per IP to allow during the rate limit window | `10` | | `SUPPRESS_ERRORS_IN_PAD_TEXT` | Should we suppress errors from being visible in the default Pad Text? | `false` | | `REQUIRE_SESSION` | If this option is enabled, a user must have a session to access pads. This effectively allows only group pads to be accessed. | `false` | | `EDIT_ONLY` | Users may edit pads but not create new ones. Pad creation is only via the API. This applies both to group pads and regular pads. | `false` | diff --git a/doc/documentation.md b/doc/documentation.md index 307c38af8..bc18fbc07 100644 --- a/doc/documentation.md +++ b/doc/documentation.md @@ -11,5 +11,5 @@ heading. Every `.html` file is generated based on the corresponding `.md` file in the `doc/api/` folder in the source tree. The -documentation is generated using the `bin/doc/generate.js` program. +documentation is generated using the `src/bin/doc/generate.js` program. The HTML template is located at `doc/template.html`. diff --git a/doc/localization.md b/doc/localization.md index 54675e2da..d047944ff 100644 --- a/doc/localization.md +++ b/doc/localization.md @@ -95,7 +95,7 @@ For example, if you want to replace `Chat` with `Notes`, simply add... ## Customization for Administrators -As an Etherpad administrator, it is possible to overwrite core mesages as well as messages in plugins. These include error messages, labels, and user instructions. Whereas the localization in the source code is in separate files separated by locale, an administrator's custom localizations are in `settings.json` under the `customLocaleStrings` key, with each locale separated by a sub-key underneath. +As an Etherpad administrator, it is possible to overwrite core messages as well as messages in plugins. These include error messages, labels, and user instructions. Whereas the localization in the source code is in separate files separated by locale, an administrator's custom localizations are in `settings.json` under the `customLocaleStrings` key, with each locale separated by a sub-key underneath. For example, let's say you want to change the text on the "New Pad" button on Etherpad's home page. If you look in `locales/en.json` (or `locales/en-gb.json`) you'll see the key for this text is `"index.newPad"`. You could add the following to `settings.json`: diff --git a/doc/plugins.md b/doc/plugins.md index 2062378bb..d8239c68a 100644 --- a/doc/plugins.md +++ b/doc/plugins.md @@ -225,7 +225,7 @@ publish your plugin. "author": "USERNAME (REAL NAME) ", "contributors": [], "dependencies": {"MODULE": "0.3.20"}, - "engines": { "node": ">= 10.13.0"} + "engines": { "node": "^10.17.0 || >=11.14.0"} } ``` diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index ff65d83ed..000000000 --- a/package-lock.json +++ /dev/null @@ -1,10704 +0,0 @@ -{ - "requires": true, - "lockfileVersion": 1, - "dependencies": { - "@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", - "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - } - } - }, - "@eslint/eslintrc": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.2.tgz", - "integrity": "sha512-EfB5OHNYp1F4px/LI/FEnGylop7nOqkQ1LRzCM0KccA2U8tvV8w01KBv37LbO7nW4H+YhKyo2LcJhRwjjV17QQ==", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", - "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "lodash": "^4.17.19", - "minimatch": "^3.0.4", - "strip-json-comments": "^3.1.1" - } - }, - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", - "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", - "dev": true - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "requires": { - "ansi-colors": "^4.1.1" - } - }, - "ep_etherpad-lite": { - "version": "file:src", - "requires": { - "async": "^3.2.0", - "async-stacktrace": "0.0.2", - "channels": "0.0.4", - "cheerio": "0.22.0", - "clean-css": "4.2.3", - "cookie-parser": "1.4.5", - "ejs": "2.6.1", - "etherpad-require-kernel": "1.0.9", - "etherpad-yajsml": "0.0.2", - "express": "4.17.1", - "express-rate-limit": "5.1.1", - "express-session": "1.17.1", - "find-root": "1.1.0", - "formidable": "1.2.1", - "graceful-fs": "4.2.4", - "http-errors": "1.8.0", - "js-cookie": "^2.2.1", - "jsonminify": "0.4.1", - "languages4translatewiki": "0.1.3", - "lodash.clonedeep": "4.5.0", - "log4js": "0.6.35", - "measured-core": "1.11.2", - "mime-types": "^2.1.27", - "nodeify": "1.0.1", - "npm": "6.14.8", - "openapi-backend": "2.4.1", - "proxy-addr": "^2.0.6", - "rate-limiter-flexible": "^2.1.4", - "rehype": "^10.0.0", - "rehype-minify-whitespace": "^4.0.5", - "request": "2.88.2", - "resolve": "1.1.7", - "security": "1.0.0", - "semver": "5.6.0", - "slide": "1.1.6", - "socket.io": "^2.3.0", - "terser": "^4.7.0", - "threads": "^1.4.0", - "tiny-worker": "^2.3.0", - "tinycon": "0.0.1", - "ueberdb2": "^0.5.6", - "underscore": "1.8.3", - "unorm": "1.4.1" - }, - "dependencies": { - "@apidevtools/json-schema-ref-parser": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-8.0.0.tgz", - "integrity": "sha512-n4YBtwQhdpLto1BaUCyAeflizmIbaloGShsPyRtFf5qdFJxfssj+GgLavczgKJFa3Bq+3St2CKcpRJdjtB4EBw==", - "requires": { - "@jsdevtools/ono": "^7.1.0", - "call-me-maybe": "^1.0.1", - "js-yaml": "^3.13.1" - } - }, - "@apidevtools/openapi-schemas": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.0.3.tgz", - "integrity": "sha512-QoPaxGXfgqgGpK1p21FJ400z56hV681a8DOcZt3J5z0WIHgFeaIZ4+6bX5ATqmOoCpRCsH4ITEwKaOyFMz7wOA==" - }, - "@apidevtools/swagger-methods": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.1.tgz", - "integrity": "sha512-1Vlm18XYW6Yg7uHunroXeunWz5FShPFAdxBbPy8H6niB2Elz9QQsCoYHMbcc11EL1pTxaIr9HXz2An/mHXlX1Q==" - }, - "@apidevtools/swagger-parser": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-9.0.1.tgz", - "integrity": "sha512-Irqybg4dQrcHhZcxJc/UM4vO7Ksoj1Id5e+K94XUOzllqX1n47HEA50EKiXTCQbykxuJ4cYGIivjx/MRSTC5OA==", - "requires": { - "@apidevtools/json-schema-ref-parser": "^8.0.0", - "@apidevtools/openapi-schemas": "^2.0.2", - "@apidevtools/swagger-methods": "^3.0.0", - "@jsdevtools/ono": "^7.1.0", - "call-me-maybe": "^1.0.1", - "openapi-types": "^1.3.5", - "z-schema": "^4.2.2" - } - }, - "@azure/ms-rest-azure-env": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@azure/ms-rest-azure-env/-/ms-rest-azure-env-1.1.2.tgz", - "integrity": "sha512-l7z0DPCi2Hp88w12JhDTtx5d0Y3+vhfE7JKJb9O7sEz71Cwp053N8piTtTnnk/tUor9oZHgEKi/p3tQQmLPjvA==" - }, - "@azure/ms-rest-js": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-1.9.0.tgz", - "integrity": "sha512-cB4Z2Mg7eBmet1rfbf0QSO1XbhfknRW7B+mX3IHJq0KGHaGJvCPoVTgdsJdCkazEMK1jtANFNEDDzSQacxyzbA==", - "requires": { - "@types/tunnel": "0.0.0", - "axios": "^0.19.0", - "form-data": "^2.3.2", - "tough-cookie": "^2.4.3", - "tslib": "^1.9.2", - "tunnel": "0.0.6", - "uuid": "^3.2.1", - "xml2js": "^0.4.19" - } - }, - "@azure/ms-rest-nodeauth": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@azure/ms-rest-nodeauth/-/ms-rest-nodeauth-2.0.2.tgz", - "integrity": "sha512-KmNNICOxt3EwViAJI3iu2VH8t8BQg5J2rSAyO4IUYLF9ZwlyYsP419pdvl4NBUhluAP2cgN7dfD2V6E6NOMZlQ==", - "requires": { - "@azure/ms-rest-azure-env": "^1.1.2", - "@azure/ms-rest-js": "^1.8.7", - "adal-node": "^0.1.28" - } - }, - "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "requires": { - "@babel/highlight": "^7.8.3" - } - }, - "@babel/core": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.6.tgz", - "integrity": "sha512-nD3deLvbsApbHAHttzIssYqgb883yU/d9roe4RZymBCDaZryMJDbptVpEpeQuRh4BJ+SYI8le9YGxKvFEvl1Wg==", - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.9.6", - "@babel/helper-module-transforms": "^7.9.0", - "@babel/helpers": "^7.9.6", - "@babel/parser": "^7.9.6", - "@babel/template": "^7.8.6", - "@babel/traverse": "^7.9.6", - "@babel/types": "^7.9.6", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.1", - "json5": "^2.1.2", - "lodash": "^4.17.13", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "resolve": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", - "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", - "requires": { - "path-parse": "^1.0.6" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - } - } - }, - "@babel/generator": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.6.tgz", - "integrity": "sha512-+htwWKJbH2bL72HRluF8zumBxzuX0ZZUFl3JLNyoUjM/Ho8wnVpPXM6aUz8cfKDqQ/h7zHqKt4xzJteUosckqQ==", - "requires": { - "@babel/types": "^7.9.6", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - } - } - }, - "@babel/helper-function-name": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz", - "integrity": "sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==", - "requires": { - "@babel/helper-get-function-arity": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/types": "^7.9.5" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", - "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz", - "integrity": "sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-module-imports": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz", - "integrity": "sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-module-transforms": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz", - "integrity": "sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA==", - "requires": { - "@babel/helper-module-imports": "^7.8.3", - "@babel/helper-replace-supers": "^7.8.6", - "@babel/helper-simple-access": "^7.8.3", - "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/template": "^7.8.6", - "@babel/types": "^7.9.0", - "lodash": "^4.17.13" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz", - "integrity": "sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-replace-supers": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.9.6.tgz", - "integrity": "sha512-qX+chbxkbArLyCImk3bWV+jB5gTNU/rsze+JlcF6Nf8tVTigPJSI1o1oBow/9Resa1yehUO9lIipsmu9oG4RzA==", - "requires": { - "@babel/helper-member-expression-to-functions": "^7.8.3", - "@babel/helper-optimise-call-expression": "^7.8.3", - "@babel/traverse": "^7.9.6", - "@babel/types": "^7.9.6" - } - }, - "@babel/helper-simple-access": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz", - "integrity": "sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw==", - "requires": { - "@babel/template": "^7.8.3", - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", - "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" - }, - "@babel/helpers": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.9.6.tgz", - "integrity": "sha512-tI4bUbldloLcHWoRUMAj4g1bF313M/o6fBKhIsb3QnGVPwRm9JsNf/gqMkQ7zjqReABiffPV6RWj7hEglID5Iw==", - "requires": { - "@babel/template": "^7.8.3", - "@babel/traverse": "^7.9.6", - "@babel/types": "^7.9.6" - } - }, - "@babel/highlight": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", - "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/parser": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.6.tgz", - "integrity": "sha512-AoeIEJn8vt+d/6+PXDRPaksYhnlbMIiejioBZvvMQsOjW/JYK6k/0dKnvvP3EhK5GfMBWDPtrxRtegWdAcdq9Q==" - }, - "@babel/template": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", - "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.6", - "@babel/types": "^7.8.6" - } - }, - "@babel/traverse": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.6.tgz", - "integrity": "sha512-b3rAHSjbxy6VEAvlxM8OV/0X4XrG72zoxme6q1MOoe2vd0bEc+TwayhuC1+Dfgqh1QEG+pj7atQqvUprHIccsg==", - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.9.6", - "@babel/helper-function-name": "^7.9.5", - "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/parser": "^7.9.6", - "@babel/types": "^7.9.6", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "@babel/types": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.6.tgz", - "integrity": "sha512-qxXzvBO//jO9ZnoasKF1uJzHd2+M6Q2ZPIVfnFps8JJvXy0ZBbwbNOmE6SGIY5XOY6d1Bo5lb9d9RJ8nv3WSeA==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, - "@eslint/eslintrc": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.1.tgz", - "integrity": "sha512-XRUeBZ5zBWLYgSANMpThFddrZZkEbGHgUdt5UJjZfnlN9BGCiUBrf+nvbRupSjMvqzwnQN0qwCmOxITt1cfywA==", - "requires": { - "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", - "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "lodash": "^4.17.19", - "minimatch": "^3.0.4", - "strip-json-comments": "^3.1.1" - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - } - }, - "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", - "requires": { - "type-fest": "^0.8.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" - } - } - }, - "@istanbuljs/load-nyc-config": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz", - "integrity": "sha512-ZR0rq/f/E4f4XcgnDvtMWXCUJpi8eO0rssVhmztsZqLIEFA9UUP9zmpE0VxlM+kv/E1ul2I876Fwil2ayptDVg==", - "requires": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" - } - } - }, - "@istanbuljs/schema": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", - "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==" - }, - "@jsdevtools/ono": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.2.tgz", - "integrity": "sha512-qS/a24RA5FEoiJS9wiv6Pwg2c/kiUo3IVUQcfeM9JvsR6pM8Yx+yl/6xWYLckZCT5jpLNhslgjiA8p/XcGyMRQ==" - }, - "@kwsites/file-exists": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", - "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", - "requires": { - "debug": "^4.1.1" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "@kwsites/promise-deferred": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", - "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" - }, - "@sinonjs/commons": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", - "integrity": "sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==", - "requires": { - "type-detect": "4.0.8" - } - }, - "@sinonjs/fake-timers": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", - "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", - "requires": { - "@sinonjs/commons": "^1.7.0" - } - }, - "@sinonjs/formatio": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-5.0.1.tgz", - "integrity": "sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ==", - "requires": { - "@sinonjs/commons": "^1", - "@sinonjs/samsam": "^5.0.2" - } - }, - "@sinonjs/samsam": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.2.0.tgz", - "integrity": "sha512-CaIcyX5cDsjcW/ab7HposFWzV1kC++4HNsfnEdFJa7cP1QIuILAKV+BgfeqRXhcnSAc76r/Rh/O5C+300BwUIw==", - "requires": { - "@sinonjs/commons": "^1.6.0", - "lodash.get": "^4.4.2", - "type-detect": "^4.0.8" - } - }, - "@sinonjs/text-encoding": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", - "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==" - }, - "@types/caseless": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", - "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" - }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" - }, - "@types/long": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" - }, - "@types/node": { - "version": "14.14.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.13.tgz", - "integrity": "sha512-vbxr0VZ8exFMMAjCW8rJwaya0dMCDyYW2ZRdTyjtrCvJoENMpdUHOT/eTzvgyA5ZnqRZ/sI0NwqAxNHKYokLJQ==" - }, - "@types/readable-stream": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.9.tgz", - "integrity": "sha512-sqsgQqFT7HmQz/V5jH1O0fvQQnXAJO46Gg9LRO/JPfjmVmGUlcx831TZZO3Y3HtWhIkzf3kTsNT0Z0kzIhIvZw==", - "requires": { - "@types/node": "*", - "safe-buffer": "*" - } - }, - "@types/request": { - "version": "2.48.5", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.5.tgz", - "integrity": "sha512-/LO7xRVnL3DxJ1WkPGDQrp4VTV1reX9RkC85mJ+Qzykj2Bdw+mG15aAfDahc76HtknjzE16SX/Yddn6MxVbmGQ==", - "requires": { - "@types/caseless": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "form-data": "^2.5.0" - }, - "dependencies": { - "form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - } - } - }, - "@types/tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==" - }, - "@types/tunnel": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.0.tgz", - "integrity": "sha512-FGDp0iBRiBdPjOgjJmn1NH0KDLN+Z8fRmo+9J7XGBhubq1DPrGrbmG4UTlGzrpbCpesMqD0sWkzi27EYkOMHyg==", - "requires": { - "@types/node": "*" - } - }, - "@types/unist": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz", - "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==" - }, - "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - } - }, - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" - }, - "acorn-jsx": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", - "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==" - }, - "adal-node": { - "version": "0.1.28", - "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.1.28.tgz", - "integrity": "sha1-RoxLs+u9lrEnBmn0ucuk4AZepIU=", - "requires": { - "@types/node": "^8.0.47", - "async": ">=0.6.0", - "date-utils": "*", - "jws": "3.x.x", - "request": ">= 2.52.0", - "underscore": ">= 1.3.1", - "uuid": "^3.1.0", - "xmldom": ">= 0.1.x", - "xpath.js": "~1.1.0" - }, - "dependencies": { - "@types/node": { - "version": "8.10.66", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz", - "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==" - } - } - }, - "adm-zip": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz", - "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==" - }, - "after": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", - "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" - }, - "agentkeepalive": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.2.tgz", - "integrity": "sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ==", - "requires": { - "humanize-ms": "^1.2.1" - } - }, - "aggregate-error": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", - "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - } - }, - "ajv": { - "version": "6.12.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", - "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-colors": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", - "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==" - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "append-transform": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", - "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", - "requires": { - "default-require-extensions": "^3.0.0" - } - }, - "archiver": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-3.1.1.tgz", - "integrity": "sha512-5Hxxcig7gw5Jod/8Gq0OneVgLYET+oNHcxgWItq4TbhOzRLKNAFUb9edAftiMKXvXfCB0vbGrJdZDNq0dWMsxg==", - "requires": { - "archiver-utils": "^2.1.0", - "async": "^2.6.3", - "buffer-crc32": "^0.2.1", - "glob": "^7.1.4", - "readable-stream": "^3.4.0", - "tar-stream": "^2.1.0", - "zip-stream": "^2.1.2" - }, - "dependencies": { - "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "requires": { - "lodash": "^4.17.14" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", - "requires": { - "glob": "^7.1.4", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" - }, - "dependencies": { - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=" - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "arraybuffer.slice": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", - "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" - }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==" - }, - "async": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", - "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" - }, - "async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" - }, - "async-stacktrace": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/async-stacktrace/-/async-stacktrace-0.0.2.tgz", - "integrity": "sha1-i7uXh+OzjINscpp+nXwIYw210e8=" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", - "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" - }, - "axios": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", - "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", - "requires": { - "follow-redirects": "1.5.10" - } - }, - "backo2": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", - "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" - }, - "bail": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", - "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==" - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base64-arraybuffer": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", - "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" - }, - "base64-js": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" - }, - "base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" - }, - "bath-es5": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/bath-es5/-/bath-es5-3.0.3.tgz", - "integrity": "sha512-PdCioDToH3t84lP40kUFCKWCOCH389Dl1kbC8FGoqOwamxsmqxxnJSXdkTOsPoNHXjem4+sJ+bbNoQm5zeCqxg==" - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "better-assert": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", - "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", - "requires": { - "callsite": "1.0.0" - } - }, - "bignumber.js": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", - "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==" - }, - "binary-extensions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", - "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==" - }, - "binary-search": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/binary-search/-/binary-search-1.3.6.tgz", - "integrity": "sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==" - }, - "bl": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", - "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "blob": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", - "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" - }, - "bluebird": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", - "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" - }, - "body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", - "requires": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" - }, - "dependencies": { - "http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - } - } - }, - "boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "requires": { - "fill-range": "^7.0.1" - } - }, - "browser-request": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/browser-request/-/browser-request-0.3.3.tgz", - "integrity": "sha1-ns5bWsqJopkyJC4Yv5M975h2zBc=" - }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" - }, - "buffer": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", - "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" - }, - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" - }, - "buffer-writer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", - "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" - }, - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" - }, - "caching-transform": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", - "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", - "requires": { - "hasha": "^5.0.0", - "make-dir": "^3.0.0", - "package-hash": "^4.0.0", - "write-file-atomic": "^3.0.0" - } - }, - "call-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", - "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.0" - } - }, - "call-me-maybe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" - }, - "callsite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", - "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "cassandra-driver": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-4.6.1.tgz", - "integrity": "sha512-Vk0kUHlMV4vFXRPwRpKnCZEEMZkp9/RucBDB7gpaUmn9sCusKzzUzVkXeusTxKSoGuIgLJJ7YBiFJdXOctUS7A==", - "requires": { - "@types/long": "^4.0.0", - "@types/node": ">=8", - "adm-zip": "^0.4.13", - "long": "^2.2.0" - } - }, - "ccount": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.0.5.tgz", - "integrity": "sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw==" - }, - "chai": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", - "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.0", - "type-detect": "^4.0.5" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "channels": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/channels/-/channels-0.0.4.tgz", - "integrity": "sha1-G+4yPt6hUrue8E9BvG5rD1lIqUE=" - }, - "character-entities-html4": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.4.tgz", - "integrity": "sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g==" - }, - "character-entities-legacy": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", - "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==" - }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=" - }, - "cheerio": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", - "integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=", - "requires": { - "css-select": "~1.2.0", - "dom-serializer": "~0.1.0", - "entities": "~1.1.1", - "htmlparser2": "^3.9.1", - "lodash.assignin": "^4.0.9", - "lodash.bind": "^4.1.4", - "lodash.defaults": "^4.0.1", - "lodash.filter": "^4.4.0", - "lodash.flatten": "^4.2.0", - "lodash.foreach": "^4.3.0", - "lodash.map": "^4.4.0", - "lodash.merge": "^4.4.0", - "lodash.pick": "^4.2.1", - "lodash.reduce": "^4.4.0", - "lodash.reject": "^4.4.0", - "lodash.some": "^4.4.0" - } - }, - "chokidar": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", - "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.1.1", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.2.0" - } - }, - "clean-css": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", - "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", - "requires": { - "source-map": "~0.6.0" - } - }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" - }, - "cli-table": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.4.tgz", - "integrity": "sha512-1vinpnX/ZERcmE443i3SZTmU5DF0rPO9DrL4I2iVAllhxzCM9SzPlHnz19fsZB78htkKZvYBvj6SZ6vXnaxmTA==", - "requires": { - "chalk": "^2.4.1", - "string-width": "^4.2.0" - } - }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "cloudant-follow": { - "version": "0.18.2", - "resolved": "https://registry.npmjs.org/cloudant-follow/-/cloudant-follow-0.18.2.tgz", - "integrity": "sha512-qu/AmKxDqJds+UmT77+0NbM7Yab2K3w0qSeJRzsq5dRWJTEJdWeb+XpG4OpKuTE9RKOa/Awn2gR3TTnvNr3TeA==", - "requires": { - "browser-request": "~0.3.0", - "debug": "^4.0.1", - "request": "^2.88.0" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "comma-separated-tokens": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", - "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==" - }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" - }, - "component-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", - "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" - }, - "component-inherit": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", - "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" - }, - "compress-commons": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-2.1.1.tgz", - "integrity": "sha512-eVw6n7CnEMFzc3duyFVrQEuY1BlHR3rYsSztyG32ibGMW722i3C6IizEGMFmfMU+A+fALvBIwxN3czffTcdA+Q==", - "requires": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^3.0.1", - "normalize-path": "^3.0.0", - "readable-stream": "^2.3.6" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", - "requires": { - "safe-buffer": "5.1.2" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } - } - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "requires": { - "safe-buffer": "~5.1.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } - } - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" - }, - "cookie-parser": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz", - "integrity": "sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==", - "requires": { - "cookie": "0.4.0", - "cookie-signature": "1.0.6" - }, - "dependencies": { - "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" - } - } - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "cookiejar": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "requires": { - "buffer": "^5.1.0" - } - }, - "crc32-stream": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-3.0.1.tgz", - "integrity": "sha512-mctvpXlbzsvK+6z8kJwSJ5crm7yBwrQMTybJzMw1O4lLGJqjlDCXY2Zw7KheiA6XBEcBmfLx1D88mjRGVJtY9w==", - "requires": { - "crc": "^3.4.4", - "readable-stream": "^3.4.0" - } - }, - "cross-spawn": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.2.tgz", - "integrity": "sha512-PD6G8QG3S4FK/XCGFbEQrDqO2AnMMsy0meR7lerlIOHAAbkuavGU/pOqprrlvfTNjvowivTeBsjebAL0NSoMxw==", - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "dependencies": { - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "css-select": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", - "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", - "requires": { - "boolbase": "~1.0.0", - "css-what": "2.1", - "domutils": "1.5.1", - "nth-check": "~1.0.1" - } - }, - "css-what": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", - "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==" - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "date-utils": { - "version": "1.2.21", - "resolved": "https://registry.npmjs.org/date-utils/-/date-utils-1.2.21.tgz", - "integrity": "sha1-YfsWzcEnSzyayq/+n8ad+HIKK2Q=" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "requires": { - "type-detect": "^4.0.0" - } - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" - }, - "default-require-extensions": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", - "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", - "requires": { - "strip-bom": "^4.0.0" - } - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "requires": { - "object-keys": "^1.0.12" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "denque": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", - "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" - }, - "dirty": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/dirty/-/dirty-1.1.0.tgz", - "integrity": "sha1-cO3SuZlUHcmXT9Ooy9DGcP4jYHg=" - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "requires": { - "esutils": "^2.0.2" - } - }, - "dom-serializer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", - "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", - "requires": { - "domelementtype": "^1.3.0", - "entities": "^1.1.1" - } - }, - "domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" - }, - "domhandler": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", - "requires": { - "domelementtype": "1" - } - }, - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "drange": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/drange/-/drange-1.1.1.tgz", - "integrity": "sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==" - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "ejs": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz", - "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==" - }, - "elasticsearch": { - "version": "16.7.2", - "resolved": "https://registry.npmjs.org/elasticsearch/-/elasticsearch-16.7.2.tgz", - "integrity": "sha512-1ZLKZlG2ABfYVBX2d7/JgxOsKJrM5Yu62GvshWu7ZSvhxPomCN4Gas90DS51yYI56JolY0XGhyiRlUhLhIL05Q==", - "requires": { - "agentkeepalive": "^3.4.1", - "chalk": "^1.0.0", - "lodash": "^4.17.10" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "requires": { - "once": "^1.4.0" - } - }, - "engine.io": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.2.tgz", - "integrity": "sha512-b4Q85dFkGw+TqgytGPrGgACRUhsdKc9S9ErRAXpPGy/CXKs4tYoHDkvIRdsseAF7NjfVwjRFIn6KTnbw7LwJZg==", - "requires": { - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "0.3.1", - "debug": "~4.1.0", - "engine.io-parser": "~2.2.0", - "ws": "^7.1.2" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "engine.io-client": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.3.tgz", - "integrity": "sha512-0NGY+9hioejTEJCaSJZfWZLk4FPI9dN+1H1C4+wj2iuFba47UgZbJzfWs4aNFajnX/qAaYKbe2lLTfEEWzCmcw==", - "requires": { - "component-emitter": "~1.3.0", - "component-inherit": "0.0.3", - "debug": "~4.1.0", - "engine.io-parser": "~2.2.0", - "has-cors": "1.1.0", - "indexof": "0.0.1", - "parseqs": "0.0.5", - "parseuri": "0.0.5", - "ws": "~6.1.0", - "xmlhttprequest-ssl": "~1.5.4", - "yeast": "0.1.2" - }, - "dependencies": { - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "ws": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", - "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", - "requires": { - "async-limiter": "~1.0.0" - } - } - } - }, - "engine.io-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.0.tgz", - "integrity": "sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w==", - "requires": { - "after": "0.8.2", - "arraybuffer.slice": "~0.0.7", - "base64-arraybuffer": "0.1.5", - "blob": "0.0.5", - "has-binary2": "~1.0.2" - } - }, - "enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "requires": { - "ansi-colors": "^4.1.1" - }, - "dependencies": { - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==" - } - } - }, - "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" - }, - "errs": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/errs/-/errs-0.3.2.tgz", - "integrity": "sha1-eYCZstvTfKK8dJ5TinwTB9C1BJk=" - }, - "es-abstract": { - "version": "1.18.0-next.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", - "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-negative-zero": "^2.0.0", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - }, - "dependencies": { - "object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - } - } - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "eslint": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.14.0.tgz", - "integrity": "sha512-5YubdnPXrlrYAFCKybPuHIAH++PINe1pmKNc5wQRB9HSbqIK1ywAnntE3Wwua4giKu0bjligf1gLF6qxMGOYRA==", - "requires": { - "@babel/code-frame": "^7.0.0", - "@eslint/eslintrc": "^0.2.1", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "enquirer": "^2.3.5", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.0", - "esquery": "^1.2.0", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash": "^4.17.19", - "minimatch": "^3.0.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^5.2.3", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - } - }, - "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", - "requires": { - "type-fest": "^0.8.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "eslint-config-etherpad": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/eslint-config-etherpad/-/eslint-config-etherpad-1.0.13.tgz", - "integrity": "sha512-wuykfSQyQj7EDN4Ok2SR5l59QOtozeANbIfew9MloUVh0P80+PIY45pXNblMrLEa9EUxYvKQQywzUWyQxHiweQ==" - }, - "eslint-plugin-es": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", - "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", - "requires": { - "eslint-utils": "^2.0.0", - "regexpp": "^3.0.0" - } - }, - "eslint-plugin-mocha": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-8.0.0.tgz", - "integrity": "sha512-n67etbWDz6NQM+HnTwZHyBwz/bLlYPOxUbw7bPuCyFujv7ZpaT/Vn6KTAbT02gf7nRljtYIjWcTxK/n8a57rQQ==", - "requires": { - "eslint-utils": "^2.1.0", - "ramda": "^0.27.1" - } - }, - "eslint-plugin-node": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", - "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", - "requires": { - "eslint-plugin-es": "^3.0.0", - "eslint-utils": "^2.0.0", - "ignore": "^5.1.1", - "minimatch": "^3.0.4", - "resolve": "^1.10.1", - "semver": "^6.1.0" - }, - "dependencies": { - "ignore": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==" - }, - "resolve": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", - "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", - "requires": { - "is-core-module": "^2.1.0", - "path-parse": "^1.0.6" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, - "eslint-plugin-prefer-arrow": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-prefer-arrow/-/eslint-plugin-prefer-arrow-1.2.2.tgz", - "integrity": "sha512-C8YMhL+r8RMeMdYAw/rQtE6xNdMulj+zGWud/qIGnlmomiPRaLDGLMeskZ3alN6uMBojmooRimtdrXebLN4svQ==" - }, - "eslint-plugin-promise": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz", - "integrity": "sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==" - }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "requires": { - "eslint-visitor-keys": "^1.1.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==" - } - } - }, - "eslint-visitor-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", - "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==" - }, - "esm": { - "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" - }, - "espree": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz", - "integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==", - "requires": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.2.0", - "eslint-visitor-keys": "^1.3.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==" - } - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - }, - "esquery": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", - "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==" - } - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==" - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "etherpad-cli-client": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/etherpad-cli-client/-/etherpad-cli-client-0.0.9.tgz", - "integrity": "sha1-A+5+fNzA4EZLTu/djn7gzwUaVDs=", - "requires": { - "async": "*", - "socket.io-client": "*" - } - }, - "etherpad-require-kernel": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/etherpad-require-kernel/-/etherpad-require-kernel-1.0.9.tgz", - "integrity": "sha1-7Y8E6f0szsOgBVu20t/p2ZkS5+I=" - }, - "etherpad-yajsml": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/etherpad-yajsml/-/etherpad-yajsml-0.0.2.tgz", - "integrity": "sha1-HCTSaLCUduY30EnN2xxt+McptG4=" - }, - "express": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", - "requires": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", - "content-type": "~1.0.4", - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } - } - }, - "express-rate-limit": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.1.1.tgz", - "integrity": "sha512-puA1zcCx/quwWUOU6pT6daCt6t7SweD9wKChKhb+KSgFMKRwS81C224hiSAUANw/gnSHiwEhgozM/2ezEBZPeA==" - }, - "express-session": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.1.tgz", - "integrity": "sha512-UbHwgqjxQZJiWRTMyhvWGvjBQduGCSBDhhZXYenziMFjxst5rMV+aJZ6hKPHZnPyHGsrqRICxtX8jtEbm/z36Q==", - "requires": { - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-headers": "~1.0.2", - "parseurl": "~1.3.3", - "safe-buffer": "5.2.0", - "uid-safe": "~2.1.5" - }, - "dependencies": { - "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" - }, - "file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", - "requires": { - "flat-cache": "^2.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - } - }, - "find-cache-dir": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", - "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", - "requires": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - } - }, - "find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, - "flat": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.1.tgz", - "integrity": "sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA==", - "requires": { - "is-buffer": "~2.0.3" - } - }, - "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", - "requires": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - }, - "dependencies": { - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==" - }, - "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", - "requires": { - "debug": "=3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } - } - }, - "foreground-child": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", - "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", - "requires": { - "cross-spawn": "^7.0.0", - "signal-exit": "^3.0.2" - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "formidable": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", - "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==" - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "fromentries": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.2.0.tgz", - "integrity": "sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ==" - }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" - }, - "gensync": { - "version": "1.0.0-beta.1", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", - "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==" - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" - }, - "get-intrinsic": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz", - "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "requires": { - "is-glob": "^4.0.1" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" - }, - "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" - }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==" - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - } - } - }, - "has-binary2": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", - "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", - "requires": { - "isarray": "2.0.1" - }, - "dependencies": { - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" - } - } - }, - "has-cors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", - "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "has-symbols": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" - }, - "hasha": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.0.tgz", - "integrity": "sha512-2W+jKdQbAdSIrggA8Q35Br8qKadTrqCTC8+XZvBWepKDK6m9XkX6Iz1a2yh2KP01kzAR/dpuMeUnocoLYDcskw==", - "requires": { - "is-stream": "^2.0.0", - "type-fest": "^0.8.0" - } - }, - "hast-util-embedded": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-1.0.6.tgz", - "integrity": "sha512-JQMW+TJe0UAIXZMjCJ4Wf6ayDV9Yv3PBDPsHD4ExBpAspJ6MOcCX+nzVF+UJVv7OqPcg852WEMSHQPoRA+FVSw==", - "requires": { - "hast-util-is-element": "^1.1.0" - }, - "dependencies": { - "hast-util-is-element": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-1.1.0.tgz", - "integrity": "sha512-oUmNua0bFbdrD/ELDSSEadRVtWZOf3iF6Lbv81naqsIV99RnSCieTbWuWCY8BAeEfKJTKl0gRdokv+dELutHGQ==" - } - } - }, - "hast-util-from-parse5": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-5.0.3.tgz", - "integrity": "sha512-gOc8UB99F6eWVWFtM9jUikjN7QkWxB3nY0df5Z0Zq1/Nkwl5V4hAAsl0tmwlgWl/1shlTF8DnNYLO8X6wRV9pA==", - "requires": { - "ccount": "^1.0.3", - "hastscript": "^5.0.0", - "property-information": "^5.0.0", - "web-namespaces": "^1.1.2", - "xtend": "^4.0.1" - } - }, - "hast-util-is-element": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-1.0.4.tgz", - "integrity": "sha512-NFR6ljJRvDcyPP5SbV7MyPBgF47X3BsskLnmw1U34yL+X6YC0MoBx9EyMg8Jtx4FzGH95jw8+c1VPLHaRA0wDQ==" - }, - "hast-util-parse-selector": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.4.tgz", - "integrity": "sha512-gW3sxfynIvZApL4L07wryYF4+C9VvH3AUi7LAnVXV4MneGEgwOByXvFo18BgmTWnm7oHAe874jKbIB1YhHSIzA==" - }, - "hast-util-to-html": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-6.1.0.tgz", - "integrity": "sha512-IlC+LG2HGv0Y8js3wqdhg9O2sO4iVpRDbHOPwXd7qgeagpGsnY49i8yyazwqS35RA35WCzrBQE/n0M6GG/ewxA==", - "requires": { - "ccount": "^1.0.0", - "comma-separated-tokens": "^1.0.1", - "hast-util-is-element": "^1.0.0", - "hast-util-whitespace": "^1.0.0", - "html-void-elements": "^1.0.0", - "property-information": "^5.2.0", - "space-separated-tokens": "^1.0.0", - "stringify-entities": "^2.0.0", - "unist-util-is": "^3.0.0", - "xtend": "^4.0.1" - } - }, - "hast-util-whitespace": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-1.0.4.tgz", - "integrity": "sha512-I5GTdSfhYfAPNztx2xJRQpG8cuDSNt599/7YUn7Gx/WxNMsG+a835k97TDkFgk123cwjfwINaZknkKkphx/f2A==" - }, - "hastscript": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-5.1.2.tgz", - "integrity": "sha512-WlztFuK+Lrvi3EggsqOkQ52rKbxkXL3RwB6t5lwoa8QLMemoWfBuL43eDrwOamJyR7uKQKdmKYaBH1NZBiIRrQ==", - "requires": { - "comma-separated-tokens": "^1.0.0", - "hast-util-parse-selector": "^2.0.0", - "property-information": "^5.0.0", - "space-separated-tokens": "^1.0.0" - } - }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" - }, - "html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" - }, - "html-void-elements": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-1.0.5.tgz", - "integrity": "sha512-uE/TxKuyNIcx44cIWnjr/rfIATDH7ZaOMmstu0CwhFG1Dunhlp4OC6/NMbhiwoq5BpW0ubi303qnEk/PZj614w==" - }, - "htmlparser2": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", - "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", - "requires": { - "domelementtype": "^1.3.1", - "domhandler": "^2.3.0", - "domutils": "^1.5.1", - "entities": "^1.1.1", - "inherits": "^2.0.1", - "readable-stream": "^3.1.1" - } - }, - "http-errors": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz", - "integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "dependencies": { - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - } - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", - "requires": { - "ms": "^2.0.0" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" - }, - "import-fresh": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.2.tgz", - "integrity": "sha512-cTPNrlvJT6twpYy+YmKUKrTSjWFs3bjYjAhCwm+z4EOCubZxAuO+hHpRN64TqjEaYSHs7tJAE0w1CKMGmsG/lw==", - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" - } - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" - }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" - }, - "is-alphabetical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", - "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==" - }, - "is-alphanumerical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", - "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", - "requires": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" - } - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-buffer": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", - "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==" - }, - "is-callable": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", - "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" - }, - "is-core-module": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.1.0.tgz", - "integrity": "sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA==", - "requires": { - "has": "^1.0.3" - } - }, - "is-date-object": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" - }, - "is-decimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", - "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==" - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-hexadecimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==" - }, - "is-negative-zero": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", - "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==" - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, - "is-observable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz", - "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==", - "requires": { - "symbol-observable": "^1.1.0" - } - }, - "is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==" - }, - "is-promise": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz", - "integrity": "sha1-MVc3YcBX4zwukaq56W2gjO++duU=" - }, - "is-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", - "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", - "requires": { - "has-symbols": "^1.0.1" - } - }, - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" - }, - "is-symbol": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", - "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", - "requires": { - "has-symbols": "^1.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==" - }, - "istanbul-lib-hook": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", - "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", - "requires": { - "append-transform": "^2.0.0" - } - }, - "istanbul-lib-instrument": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", - "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", - "requires": { - "@babel/core": "^7.7.5", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, - "istanbul-lib-processinfo": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", - "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", - "requires": { - "archy": "^1.0.0", - "cross-spawn": "^7.0.0", - "istanbul-lib-coverage": "^3.0.0-alpha.1", - "make-dir": "^3.0.0", - "p-map": "^3.0.0", - "rimraf": "^3.0.0", - "uuid": "^3.3.3" - } - }, - "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", - "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "istanbul-reports": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", - "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, - "js-cookie": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", - "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbi": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.1.4.tgz", - "integrity": "sha512-52QRRFSsi9impURE8ZUbzAMCLjPm4THO7H2fcuIvaaeFTbSysvkodbQQXIVsNgq/ypDbq6dJiuGKL0vZ/i9hUg==" - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "json5": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", - "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", - "requires": { - "minimist": "^1.2.5" - } - }, - "jsonminify": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/jsonminify/-/jsonminify-0.4.1.tgz", - "integrity": "sha1-gF2vuzk5UYjO6atYLIHvlZ1+cQw=" - }, - "jsonschema": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.4.tgz", - "integrity": "sha512-lz1nOH69GbsVHeVgEdvyavc/33oymY1AZwtePMiMj4HZPMbP5OIKK3zT9INMWjwua/V4Z4yq7wSlBbSG+g4AEw==" - }, - "jsonschema-draft4": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/jsonschema-draft4/-/jsonschema-draft4-1.0.0.tgz", - "integrity": "sha1-8K8gBQVPDwrefqIRhhS2ncUS2GU=" - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "just-extend": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.1.tgz", - "integrity": "sha512-aWgeGFW67BP3e5181Ep1Fv2v8z//iBJfrvyTnq8wG86vEESwmonn1zPBJ0VfmT9CJq2FIT0VsETtrNFm2a+SHA==" - }, - "jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "languages4translatewiki": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/languages4translatewiki/-/languages4translatewiki-0.1.3.tgz", - "integrity": "sha1-xDYgbgUtIUkLEQF6RNURj5Ih5ds=" - }, - "lazystream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", - "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", - "requires": { - "readable-stream": "^2.0.5" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" - }, - "lodash.assignin": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", - "integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI=" - }, - "lodash.bind": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", - "integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=" - }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" - }, - "lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" - }, - "lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=" - }, - "lodash.filter": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", - "integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=" - }, - "lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" - }, - "lodash.flattendeep": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=" - }, - "lodash.foreach": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", - "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=" - }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" - }, - "lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" - }, - "lodash.map": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", - "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=" - }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" - }, - "lodash.pick": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", - "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=" - }, - "lodash.reduce": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", - "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=" - }, - "lodash.reject": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", - "integrity": "sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=" - }, - "lodash.some": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", - "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=" - }, - "lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" - }, - "log-symbols": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", - "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", - "requires": { - "chalk": "^2.4.2" - } - }, - "log4js": { - "version": "0.6.35", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-0.6.35.tgz", - "integrity": "sha1-OrHafLFII7dO04ZcSFk6zfEfG1k=", - "requires": { - "readable-stream": "~1.0.2", - "semver": "~4.3.3" - }, - "dependencies": { - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "semver": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", - "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=" - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "long": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/long/-/long-2.4.0.tgz", - "integrity": "sha1-n6GAux2VAM3CnEFWdmoZleH0Uk8=" - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "requires": { - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, - "measured-core": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/measured-core/-/measured-core-1.11.2.tgz", - "integrity": "sha1-nb6m0gdBtW9hq9hm5Jbri4Xmk0k=", - "requires": { - "binary-search": "^1.3.3", - "optional-js": "^2.0.0" - } - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "mime-db": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" - }, - "mime-types": { - "version": "2.1.27", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", - "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", - "requires": { - "mime-db": "1.44.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "requires": { - "minimist": "^1.2.5" - } - }, - "mocha": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.2.tgz", - "integrity": "sha512-o96kdRKMKI3E8U0bjnfqW4QMk12MwZ4mhdBTf+B5a1q9+aq2HRnj+3ZdJu0B/ZhJeK78MgYuv6L8d/rA5AeBJA==", - "requires": { - "ansi-colors": "3.2.3", - "browser-stdout": "1.3.1", - "chokidar": "3.3.0", - "debug": "3.2.6", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "find-up": "3.0.0", - "glob": "7.1.3", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "3.13.1", - "log-symbols": "3.0.0", - "minimatch": "3.0.4", - "mkdirp": "0.5.5", - "ms": "2.1.1", - "node-environment-flags": "1.0.6", - "object.assign": "4.1.0", - "strip-json-comments": "2.0.1", - "supports-color": "6.0.0", - "which": "1.3.1", - "wide-align": "1.1.3", - "yargs": "13.3.2", - "yargs-parser": "13.1.2", - "yargs-unparser": "1.6.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - }, - "supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "mocha-froth": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/mocha-froth/-/mocha-froth-0.2.10.tgz", - "integrity": "sha512-xyJqAYtm2zjrkG870hjeSVvGgS4Dc9tRokmN6R7XLgBKhdtAJ1ytU6zL045djblfHaPyTkSerQU4wqcjsv7Aew==" - }, - "mock-json-schema": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/mock-json-schema/-/mock-json-schema-1.0.8.tgz", - "integrity": "sha512-22yL+WggSo8HXqw0HkXgXXJjJMSBCfv54htfwN4BabaFdJ3808jL0CzE+VaBRlj8Nr0+pnSVE9YvsDG5Quu6hQ==", - "requires": { - "lodash": "^4.17.11", - "openapi-types": "^1.3.2" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "mssql": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/mssql/-/mssql-6.3.0.tgz", - "integrity": "sha512-6/BK/3J8Oe4t6BYnmdCCORHhyBtBI/Fh0Sh6l1hPzb/hKtxDrsaSDGIpck1u8bzkLzev39TH5W2nz+ffeRz7gg==", - "requires": { - "debug": "^4.3.1", - "tarn": "^1.1.5", - "tedious": "^6.7.0" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "mysql": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", - "integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==", - "requires": { - "bignumber.js": "9.0.0", - "readable-stream": "2.3.7", - "safe-buffer": "5.1.2", - "sqlstring": "2.3.1" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "nano": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/nano/-/nano-8.2.3.tgz", - "integrity": "sha512-nubyTQeZ/p+xf3ZFFMd7WrZwpcy9tUDrbaXw9HFBsM6zBY5gXspvOjvG2Zz3emT6nfJtP/h7F2/ESfsVVXnuMw==", - "requires": { - "@types/request": "^2.48.4", - "cloudant-follow": "^0.18.2", - "debug": "^4.1.1", - "errs": "^0.3.2", - "request": "^2.88.0" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "native-duplexpair": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz", - "integrity": "sha1-eJkHjmS/PIo9cyYBs9QP8F21j6A=" - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" - }, - "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" - }, - "nise": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.4.tgz", - "integrity": "sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A==", - "requires": { - "@sinonjs/commons": "^1.7.0", - "@sinonjs/fake-timers": "^6.0.0", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "path-to-regexp": "^1.7.0" - }, - "dependencies": { - "path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "requires": { - "isarray": "0.0.1" - } - } - } - }, - "node-environment-flags": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", - "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", - "requires": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - } - } - }, - "node-preload": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", - "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", - "requires": { - "process-on-spawn": "^1.0.0" - } - }, - "nodeify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/nodeify/-/nodeify-1.0.1.tgz", - "integrity": "sha1-ZKtpp7268DzhB7TwM1yHwLnpGx0=", - "requires": { - "is-promise": "~1.0.0", - "promise": "~1.3.0" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, - "npm": { - "version": "6.14.8", - "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.8.tgz", - "integrity": "sha512-HBZVBMYs5blsj94GTeQZel7s9odVuuSUHy1+AlZh7rPVux1os2ashvEGLy/STNK7vUjbrCg5Kq9/GXisJgdf6A==", - "requires": { - "JSONStream": "^1.3.5", - "abbrev": "~1.1.1", - "ansicolors": "~0.3.2", - "ansistyles": "~0.1.3", - "aproba": "^2.0.0", - "archy": "~1.0.0", - "bin-links": "^1.1.8", - "bluebird": "^3.5.5", - "byte-size": "^5.0.1", - "cacache": "^12.0.3", - "call-limit": "^1.1.1", - "chownr": "^1.1.4", - "ci-info": "^2.0.0", - "cli-columns": "^3.1.2", - "cli-table3": "^0.5.1", - "cmd-shim": "^3.0.3", - "columnify": "~1.5.4", - "config-chain": "^1.1.12", - "debuglog": "*", - "detect-indent": "~5.0.0", - "detect-newline": "^2.1.0", - "dezalgo": "~1.0.3", - "editor": "~1.0.0", - "figgy-pudding": "^3.5.1", - "find-npm-prefix": "^1.0.2", - "fs-vacuum": "~1.2.10", - "fs-write-stream-atomic": "~1.0.10", - "gentle-fs": "^2.3.1", - "glob": "^7.1.6", - "graceful-fs": "^4.2.4", - "has-unicode": "~2.0.1", - "hosted-git-info": "^2.8.8", - "iferr": "^1.0.2", - "imurmurhash": "*", - "infer-owner": "^1.0.4", - "inflight": "~1.0.6", - "inherits": "^2.0.4", - "ini": "^1.3.5", - "init-package-json": "^1.10.3", - "is-cidr": "^3.0.0", - "json-parse-better-errors": "^1.0.2", - "lazy-property": "~1.0.0", - "libcipm": "^4.0.8", - "libnpm": "^3.0.1", - "libnpmaccess": "^3.0.2", - "libnpmhook": "^5.0.3", - "libnpmorg": "^1.0.1", - "libnpmsearch": "^2.0.2", - "libnpmteam": "^1.0.2", - "libnpx": "^10.2.4", - "lock-verify": "^2.1.0", - "lockfile": "^1.0.4", - "lodash._baseindexof": "*", - "lodash._baseuniq": "~4.6.0", - "lodash._bindcallback": "*", - "lodash._cacheindexof": "*", - "lodash._createcache": "*", - "lodash._getnative": "*", - "lodash.clonedeep": "~4.5.0", - "lodash.restparam": "*", - "lodash.union": "~4.6.0", - "lodash.uniq": "~4.5.0", - "lodash.without": "~4.4.0", - "lru-cache": "^5.1.1", - "meant": "^1.0.2", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.5", - "move-concurrently": "^1.0.1", - "node-gyp": "^5.1.0", - "nopt": "^4.0.3", - "normalize-package-data": "^2.5.0", - "npm-audit-report": "^1.3.3", - "npm-cache-filename": "~1.0.2", - "npm-install-checks": "^3.0.2", - "npm-lifecycle": "^3.1.5", - "npm-package-arg": "^6.1.1", - "npm-packlist": "^1.4.8", - "npm-pick-manifest": "^3.0.2", - "npm-profile": "^4.0.4", - "npm-registry-fetch": "^4.0.7", - "npm-user-validate": "~1.0.0", - "npmlog": "~4.1.2", - "once": "~1.4.0", - "opener": "^1.5.1", - "osenv": "^0.1.5", - "pacote": "^9.5.12", - "path-is-inside": "~1.0.2", - "promise-inflight": "~1.0.1", - "qrcode-terminal": "^0.12.0", - "query-string": "^6.8.2", - "qw": "~1.0.1", - "read": "~1.0.7", - "read-cmd-shim": "^1.0.5", - "read-installed": "~4.0.3", - "read-package-json": "^2.1.1", - "read-package-tree": "^5.3.1", - "readable-stream": "^3.6.0", - "readdir-scoped-modules": "^1.1.0", - "request": "^2.88.0", - "retry": "^0.12.0", - "rimraf": "^2.7.1", - "safe-buffer": "^5.1.2", - "semver": "^5.7.1", - "sha": "^3.0.0", - "slide": "~1.1.6", - "sorted-object": "~2.0.1", - "sorted-union-stream": "~2.1.3", - "ssri": "^6.0.1", - "stringify-package": "^1.0.1", - "tar": "^4.4.13", - "text-table": "~0.2.0", - "tiny-relative-date": "^1.3.0", - "uid-number": "0.0.6", - "umask": "~1.1.0", - "unique-filename": "^1.1.1", - "unpipe": "~1.0.0", - "update-notifier": "^2.5.0", - "uuid": "^3.3.3", - "validate-npm-package-license": "^3.0.4", - "validate-npm-package-name": "~3.0.0", - "which": "^1.3.1", - "worker-farm": "^1.7.0", - "write-file-atomic": "^2.4.3" - }, - "dependencies": { - "JSONStream": { - "version": "1.3.5", - "resolved": false, - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "requires": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - } - }, - "abbrev": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "agent-base": { - "version": "4.3.0", - "resolved": false, - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", - "requires": { - "es6-promisify": "^5.0.0" - } - }, - "agentkeepalive": { - "version": "3.5.2", - "resolved": false, - "integrity": "sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ==", - "requires": { - "humanize-ms": "^1.2.1" - } - }, - "ajv": { - "version": "5.5.2", - "resolved": false, - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" - } - }, - "ansi-align": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", - "requires": { - "string-width": "^2.0.0" - } - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": false, - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": false, - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "ansicolors": { - "version": "0.3.2", - "resolved": false, - "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=" - }, - "ansistyles": { - "version": "0.1.3", - "resolved": false, - "integrity": "sha1-XeYEFb2gcbs3EnhUyGT0GyMlRTk=" - }, - "aproba": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" - }, - "archy": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=" - }, - "are-we-there-yet": { - "version": "1.1.4", - "resolved": false, - "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": false, - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "asap": { - "version": "2.0.6", - "resolved": false, - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" - }, - "asn1": { - "version": "0.2.4", - "resolved": false, - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "asynckit": { - "version": "0.4.0", - "resolved": false, - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": false, - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.8.0", - "resolved": false, - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" - }, - "balanced-match": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "optional": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "bin-links": { - "version": "1.1.8", - "resolved": false, - "integrity": "sha512-KgmVfx+QqggqP9dA3iIc5pA4T1qEEEL+hOhOhNPaUm77OTrJoOXE/C05SJLNJe6m/2wUK7F1tDSou7n5TfCDzQ==", - "requires": { - "bluebird": "^3.5.3", - "cmd-shim": "^3.0.0", - "gentle-fs": "^2.3.0", - "graceful-fs": "^4.1.15", - "npm-normalize-package-bin": "^1.0.0", - "write-file-atomic": "^2.3.0" - } - }, - "bluebird": { - "version": "3.5.5", - "resolved": false, - "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==" - }, - "boxen": { - "version": "1.3.0", - "resolved": false, - "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", - "requires": { - "ansi-align": "^2.0.0", - "camelcase": "^4.0.0", - "chalk": "^2.0.1", - "cli-boxes": "^1.0.0", - "string-width": "^2.0.0", - "term-size": "^1.2.0", - "widest-line": "^2.0.0" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": false, - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "buffer-from": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==" - }, - "builtins": { - "version": "1.0.3", - "resolved": false, - "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=" - }, - "byline": { - "version": "5.0.0", - "resolved": false, - "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=" - }, - "byte-size": { - "version": "5.0.1", - "resolved": false, - "integrity": "sha512-/XuKeqWocKsYa/cBY1YbSJSWWqTi4cFgr9S6OyM7PBaPbr9zvNGwWP33vt0uqGhwDdN+y3yhbXVILEUpnwEWGw==" - }, - "cacache": { - "version": "12.0.3", - "resolved": false, - "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==", - "requires": { - "bluebird": "^3.5.5", - "chownr": "^1.1.1", - "figgy-pudding": "^3.5.1", - "glob": "^7.1.4", - "graceful-fs": "^4.1.15", - "infer-owner": "^1.0.3", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.3", - "ssri": "^6.0.1", - "unique-filename": "^1.1.1", - "y18n": "^4.0.0" - } - }, - "call-limit": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-5twvci5b9eRBw2wCfPtN0GmlR2/gadZqyFpPhOK6CvMFoFgA+USnZ6Jpu1lhG9h85pQ3Ouil3PfXWRD4EUaRiQ==" - }, - "camelcase": { - "version": "4.1.0", - "resolved": false, - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" - }, - "capture-stack-trace": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=" - }, - "caseless": { - "version": "0.12.0", - "resolved": false, - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "chalk": { - "version": "2.4.1", - "resolved": false, - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "chownr": { - "version": "1.1.4", - "resolved": false, - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, - "ci-info": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" - }, - "cidr-regex": { - "version": "2.0.10", - "resolved": false, - "integrity": "sha512-sB3ogMQXWvreNPbJUZMRApxuRYd+KoIo4RGQ81VatjmMW6WJPo+IJZ2846FGItr9VzKo5w7DXzijPLGtSd0N3Q==", - "requires": { - "ip-regex": "^2.1.0" - } - }, - "cli-boxes": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=" - }, - "cli-columns": { - "version": "3.1.2", - "resolved": false, - "integrity": "sha1-ZzLZcpee/CrkRKHwjgj6E5yWoY4=", - "requires": { - "string-width": "^2.0.0", - "strip-ansi": "^3.0.1" - } - }, - "cli-table3": { - "version": "0.5.1", - "resolved": false, - "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", - "requires": { - "colors": "^1.1.2", - "object-assign": "^4.1.0", - "string-width": "^2.1.1" - } - }, - "cliui": { - "version": "5.0.0", - "resolved": false, - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": false, - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "3.1.0", - "resolved": false, - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": false, - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "clone": { - "version": "1.0.4", - "resolved": false, - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" - }, - "cmd-shim": { - "version": "3.0.3", - "resolved": false, - "integrity": "sha512-DtGg+0xiFhQIntSBRzL2fRQBnmtAVwXIDo4Qq46HPpObYquxMaZS4sb82U9nH91qJrlosC1wa9gwr0QyL/HypA==", - "requires": { - "graceful-fs": "^4.1.2", - "mkdirp": "~0.5.0" - } - }, - "co": { - "version": "4.6.0", - "resolved": false, - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "code-point-at": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "color-convert": { - "version": "1.9.1", - "resolved": false, - "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", - "requires": { - "color-name": "^1.1.1" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": false, - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "colors": { - "version": "1.3.3", - "resolved": false, - "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==", - "optional": true - }, - "columnify": { - "version": "1.5.4", - "resolved": false, - "integrity": "sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs=", - "requires": { - "strip-ansi": "^3.0.0", - "wcwidth": "^1.0.0" - } - }, - "combined-stream": { - "version": "1.0.6", - "resolved": false, - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": false, - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.6.2", - "resolved": false, - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": false, - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "config-chain": { - "version": "1.1.12", - "resolved": false, - "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", - "requires": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "configstore": { - "version": "3.1.5", - "resolved": false, - "integrity": "sha512-nlOhI4+fdzoK5xmJ+NY+1gZK56bwEaWZr8fYuXohZ9Vkc1o3a4T/R3M+yE/w7x/ZVJ1zF8c+oaOvF0dztdUgmA==", - "requires": { - "dot-prop": "^4.2.1", - "graceful-fs": "^4.1.2", - "make-dir": "^1.0.0", - "unique-string": "^1.0.0", - "write-file-atomic": "^2.0.0", - "xdg-basedir": "^3.0.0" - } - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, - "copy-concurrently": { - "version": "1.0.5", - "resolved": false, - "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", - "requires": { - "aproba": "^1.1.1", - "fs-write-stream-atomic": "^1.0.8", - "iferr": "^0.1.5", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.0" - }, - "dependencies": { - "aproba": { - "version": "1.2.0", - "resolved": false, - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "iferr": { - "version": "0.1.5", - "resolved": false, - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" - } - } - }, - "core-util-is": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "create-error-class": { - "version": "3.0.2", - "resolved": false, - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", - "requires": { - "capture-stack-trace": "^1.0.0" - } - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": false, - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "lru-cache": { - "version": "4.1.5", - "resolved": false, - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "yallist": { - "version": "2.1.2", - "resolved": false, - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - } - } - }, - "crypto-random-string": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=" - }, - "cyclist": { - "version": "0.2.2", - "resolved": false, - "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=" - }, - "dashdash": { - "version": "1.14.1", - "resolved": false, - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "debug": { - "version": "3.1.0", - "resolved": false, - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - }, - "dependencies": { - "ms": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "debuglog": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=" - }, - "decamelize": { - "version": "1.2.0", - "resolved": false, - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": false, - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" - }, - "deep-extend": { - "version": "0.6.0", - "resolved": false, - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" - }, - "defaults": { - "version": "1.0.3", - "resolved": false, - "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", - "requires": { - "clone": "^1.0.2" - } - }, - "define-properties": { - "version": "1.1.3", - "resolved": false, - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "requires": { - "object-keys": "^1.0.12" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "delegates": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "detect-indent": { - "version": "5.0.0", - "resolved": false, - "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=" - }, - "detect-newline": { - "version": "2.1.0", - "resolved": false, - "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=" - }, - "dezalgo": { - "version": "1.0.3", - "resolved": false, - "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", - "requires": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "dot-prop": { - "version": "4.2.1", - "resolved": false, - "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==", - "requires": { - "is-obj": "^1.0.0" - } - }, - "dotenv": { - "version": "5.0.1", - "resolved": false, - "integrity": "sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==" - }, - "duplexer3": { - "version": "0.1.4", - "resolved": false, - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" - }, - "duplexify": { - "version": "3.6.0", - "resolved": false, - "integrity": "sha512-fO3Di4tBKJpYTFHAxTU00BcfWMY9w24r/x21a6rZRbsD/ToUgGxsMbiGRmB7uVAXeGKXD9MwiLZa5E97EVgIRQ==", - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": false, - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": false, - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "optional": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "editor": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-YMf4e9YrzGqJT6jM1q+3gjok90I=" - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": false, - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" - }, - "encoding": { - "version": "0.1.12", - "resolved": false, - "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", - "requires": { - "iconv-lite": "~0.4.13" - } - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": false, - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "requires": { - "once": "^1.4.0" - } - }, - "env-paths": { - "version": "2.2.0", - "resolved": false, - "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==" - }, - "err-code": { - "version": "1.1.2", - "resolved": false, - "integrity": "sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=" - }, - "errno": { - "version": "0.1.7", - "resolved": false, - "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", - "requires": { - "prr": "~1.0.1" - } - }, - "es-abstract": { - "version": "1.12.0", - "resolved": false, - "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", - "requires": { - "es-to-primitive": "^1.1.1", - "function-bind": "^1.1.1", - "has": "^1.0.1", - "is-callable": "^1.1.3", - "is-regex": "^1.0.4" - } - }, - "es-to-primitive": { - "version": "1.2.0", - "resolved": false, - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "es6-promise": { - "version": "4.2.8", - "resolved": false, - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" - }, - "es6-promisify": { - "version": "5.0.0", - "resolved": false, - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "requires": { - "es6-promise": "^4.0.3" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": false, - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "execa": { - "version": "0.7.0", - "resolved": false, - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "dependencies": { - "get-stream": { - "version": "3.0.0", - "resolved": false, - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": false, - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extsprintf": { - "version": "1.3.0", - "resolved": false, - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "figgy-pudding": { - "version": "3.5.1", - "resolved": false, - "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==" - }, - "find-npm-prefix": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha512-KEftzJ+H90x6pcKtdXZEPsQse8/y/UnvzRKrOSQFprnrGaFuJ62fVkP34Iu2IYuMvyauCyoLTNkJZgrrGA2wkA==" - }, - "flush-write-stream": { - "version": "1.0.3", - "resolved": false, - "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==", - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.4" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": false, - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": false, - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.2", - "resolved": false, - "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "1.0.6", - "mime-types": "^2.1.12" - } - }, - "from2": { - "version": "2.3.0", - "resolved": false, - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": false, - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "fs-minipass": { - "version": "1.2.7", - "resolved": false, - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "requires": { - "minipass": "^2.6.0" - }, - "dependencies": { - "minipass": { - "version": "2.9.0", - "resolved": false, - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - } - } - }, - "fs-vacuum": { - "version": "1.2.10", - "resolved": false, - "integrity": "sha1-t2Kb7AekAxolSP35n17PHMizHjY=", - "requires": { - "graceful-fs": "^4.1.2", - "path-is-inside": "^1.0.1", - "rimraf": "^2.5.2" - } - }, - "fs-write-stream-atomic": { - "version": "1.0.10", - "resolved": false, - "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", - "requires": { - "graceful-fs": "^4.1.2", - "iferr": "^0.1.5", - "imurmurhash": "^0.1.4", - "readable-stream": "1 || 2" - }, - "dependencies": { - "iferr": { - "version": "0.1.5", - "resolved": false, - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" - }, - "readable-stream": { - "version": "2.3.6", - "resolved": false, - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "function-bind": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "gauge": { - "version": "2.7.4", - "resolved": false, - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - }, - "dependencies": { - "aproba": { - "version": "1.2.0", - "resolved": false, - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "string-width": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, - "genfun": { - "version": "5.0.0", - "resolved": false, - "integrity": "sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA==" - }, - "gentle-fs": { - "version": "2.3.1", - "resolved": false, - "integrity": "sha512-OlwBBwqCFPcjm33rF2BjW+Pr6/ll2741l+xooiwTCeaX2CA1ZuclavyMBe0/KlR21/XGsgY6hzEQZ15BdNa13Q==", - "requires": { - "aproba": "^1.1.2", - "chownr": "^1.1.2", - "cmd-shim": "^3.0.3", - "fs-vacuum": "^1.2.10", - "graceful-fs": "^4.1.11", - "iferr": "^0.1.5", - "infer-owner": "^1.0.4", - "mkdirp": "^0.5.1", - "path-is-inside": "^1.0.2", - "read-cmd-shim": "^1.0.1", - "slide": "^1.1.6" - }, - "dependencies": { - "aproba": { - "version": "1.2.0", - "resolved": false, - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "iferr": { - "version": "0.1.5", - "resolved": false, - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" - } - } - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": false, - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "get-stream": { - "version": "4.1.0", - "resolved": false, - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "requires": { - "pump": "^3.0.0" - } - }, - "getpass": { - "version": "0.1.7", - "resolved": false, - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.6", - "resolved": false, - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "global-dirs": { - "version": "0.1.1", - "resolved": false, - "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", - "requires": { - "ini": "^1.3.4" - } - }, - "got": { - "version": "6.7.1", - "resolved": false, - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", - "requires": { - "create-error-class": "^3.0.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-redirect": "^1.0.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "unzip-response": "^2.0.1", - "url-parse-lax": "^1.0.0" - }, - "dependencies": { - "get-stream": { - "version": "3.0.0", - "resolved": false, - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" - } - } - }, - "graceful-fs": { - "version": "4.2.4", - "resolved": false, - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" - }, - "har-schema": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.1.0", - "resolved": false, - "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", - "requires": { - "ajv": "^5.3.0", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": false, - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": false, - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "has-symbols": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" - }, - "has-unicode": { - "version": "2.0.1", - "resolved": false, - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, - "hosted-git-info": { - "version": "2.8.8", - "resolved": false, - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" - }, - "http-cache-semantics": { - "version": "3.8.1", - "resolved": false, - "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==" - }, - "http-proxy-agent": { - "version": "2.1.0", - "resolved": false, - "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", - "requires": { - "agent-base": "4", - "debug": "3.1.0" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": false, - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "https-proxy-agent": { - "version": "2.2.4", - "resolved": false, - "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", - "requires": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" - } - }, - "humanize-ms": { - "version": "1.2.1", - "resolved": false, - "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", - "requires": { - "ms": "^2.0.0" - } - }, - "iconv-lite": { - "version": "0.4.23", - "resolved": false, - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "iferr": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha512-9AfeLfji44r5TKInjhz3W9DyZI1zR1JAf2hVBMGhddAKPqBsupb89jGfbCTHIGZd6fGZl9WlHdn4AObygyMKwg==" - }, - "ignore-walk": { - "version": "3.0.3", - "resolved": false, - "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", - "requires": { - "minimatch": "^3.0.4" - } - }, - "import-lazy": { - "version": "2.1.0", - "resolved": false, - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=" - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": false, - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" - }, - "infer-owner": { - "version": "1.0.4", - "resolved": false, - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==" - }, - "inflight": { - "version": "1.0.6", - "resolved": false, - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": false, - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ini": { - "version": "1.3.5", - "resolved": false, - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" - }, - "init-package-json": { - "version": "1.10.3", - "resolved": false, - "integrity": "sha512-zKSiXKhQveNteyhcj1CoOP8tqp1QuxPIPBl8Bid99DGLFqA1p87M6lNgfjJHSBoWJJlidGOv5rWjyYKEB3g2Jw==", - "requires": { - "glob": "^7.1.1", - "npm-package-arg": "^4.0.0 || ^5.0.0 || ^6.0.0", - "promzard": "^0.3.0", - "read": "~1.0.1", - "read-package-json": "1 || 2", - "semver": "2.x || 3.x || 4 || 5", - "validate-npm-package-license": "^3.0.1", - "validate-npm-package-name": "^3.0.0" - } - }, - "ip": { - "version": "1.1.5", - "resolved": false, - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" - }, - "ip-regex": { - "version": "2.1.0", - "resolved": false, - "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=" - }, - "is-callable": { - "version": "1.1.4", - "resolved": false, - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==" - }, - "is-ci": { - "version": "1.2.1", - "resolved": false, - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", - "requires": { - "ci-info": "^1.5.0" - }, - "dependencies": { - "ci-info": { - "version": "1.6.0", - "resolved": false, - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==" - } - } - }, - "is-cidr": { - "version": "3.0.0", - "resolved": false, - "integrity": "sha512-8Xnnbjsb0x462VoYiGlhEi+drY8SFwrHiSYuzc/CEwco55vkehTaxAyIjEdpi3EMvLPPJAJi9FlzP+h+03gp0Q==", - "requires": { - "cidr-regex": "^2.0.10" - } - }, - "is-date-object": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-installed-globally": { - "version": "0.1.0", - "resolved": false, - "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", - "requires": { - "global-dirs": "^0.1.0", - "is-path-inside": "^1.0.0" - } - }, - "is-npm": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=" - }, - "is-obj": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" - }, - "is-path-inside": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "requires": { - "path-is-inside": "^1.0.1" - } - }, - "is-redirect": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" - }, - "is-regex": { - "version": "1.0.4", - "resolved": false, - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "requires": { - "has": "^1.0.1" - } - }, - "is-retry-allowed": { - "version": "1.2.0", - "resolved": false, - "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==" - }, - "is-stream": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" - }, - "is-symbol": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", - "requires": { - "has-symbols": "^1.0.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "isarray": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "isstream": { - "version": "0.1.2", - "resolved": false, - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "jsbn": { - "version": "0.1.1", - "resolved": false, - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" - }, - "json-schema": { - "version": "0.2.3", - "resolved": false, - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": false, - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": false, - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "jsonparse": { - "version": "1.3.1", - "resolved": false, - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" - }, - "jsprim": { - "version": "1.4.1", - "resolved": false, - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "latest-version": { - "version": "3.1.0", - "resolved": false, - "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", - "requires": { - "package-json": "^4.0.0" - } - }, - "lazy-property": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-hN3Es3Bnm6i9TNz6TAa0PVcREUc=" - }, - "libcipm": { - "version": "4.0.8", - "resolved": false, - "integrity": "sha512-IN3hh2yDJQtZZ5paSV4fbvJg4aHxCCg5tcZID/dSVlTuUiWktsgaldVljJv6Z5OUlYspx6xQkbR0efNodnIrOA==", - "requires": { - "bin-links": "^1.1.2", - "bluebird": "^3.5.1", - "figgy-pudding": "^3.5.1", - "find-npm-prefix": "^1.0.2", - "graceful-fs": "^4.1.11", - "ini": "^1.3.5", - "lock-verify": "^2.1.0", - "mkdirp": "^0.5.1", - "npm-lifecycle": "^3.0.0", - "npm-logical-tree": "^1.2.1", - "npm-package-arg": "^6.1.0", - "pacote": "^9.1.0", - "read-package-json": "^2.0.13", - "rimraf": "^2.6.2", - "worker-farm": "^1.6.0" - } - }, - "libnpm": { - "version": "3.0.1", - "resolved": false, - "integrity": "sha512-d7jU5ZcMiTfBqTUJVZ3xid44fE5ERBm9vBnmhp2ECD2Ls+FNXWxHSkO7gtvrnbLO78gwPdNPz1HpsF3W4rjkBQ==", - "requires": { - "bin-links": "^1.1.2", - "bluebird": "^3.5.3", - "find-npm-prefix": "^1.0.2", - "libnpmaccess": "^3.0.2", - "libnpmconfig": "^1.2.1", - "libnpmhook": "^5.0.3", - "libnpmorg": "^1.0.1", - "libnpmpublish": "^1.1.2", - "libnpmsearch": "^2.0.2", - "libnpmteam": "^1.0.2", - "lock-verify": "^2.0.2", - "npm-lifecycle": "^3.0.0", - "npm-logical-tree": "^1.2.1", - "npm-package-arg": "^6.1.0", - "npm-profile": "^4.0.2", - "npm-registry-fetch": "^4.0.0", - "npmlog": "^4.1.2", - "pacote": "^9.5.3", - "read-package-json": "^2.0.13", - "stringify-package": "^1.0.0" - } - }, - "libnpmaccess": { - "version": "3.0.2", - "resolved": false, - "integrity": "sha512-01512AK7MqByrI2mfC7h5j8N9V4I7MHJuk9buo8Gv+5QgThpOgpjB7sQBDDkeZqRteFb1QM/6YNdHfG7cDvfAQ==", - "requires": { - "aproba": "^2.0.0", - "get-stream": "^4.0.0", - "npm-package-arg": "^6.1.0", - "npm-registry-fetch": "^4.0.0" - } - }, - "libnpmconfig": { - "version": "1.2.1", - "resolved": false, - "integrity": "sha512-9esX8rTQAHqarx6qeZqmGQKBNZR5OIbl/Ayr0qQDy3oXja2iFVQQI81R6GZ2a02bSNZ9p3YOGX1O6HHCb1X7kA==", - "requires": { - "figgy-pudding": "^3.5.1", - "find-up": "^3.0.0", - "ini": "^1.3.5" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": false, - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": false, - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.2.0", - "resolved": false, - "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": false, - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": false, - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - } - } - }, - "libnpmhook": { - "version": "5.0.3", - "resolved": false, - "integrity": "sha512-UdNLMuefVZra/wbnBXECZPefHMGsVDTq5zaM/LgKNE9Keyl5YXQTnGAzEo+nFOpdRqTWI9LYi4ApqF9uVCCtuA==", - "requires": { - "aproba": "^2.0.0", - "figgy-pudding": "^3.4.1", - "get-stream": "^4.0.0", - "npm-registry-fetch": "^4.0.0" - } - }, - "libnpmorg": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha512-0sRUXLh+PLBgZmARvthhYXQAWn0fOsa6T5l3JSe2n9vKG/lCVK4nuG7pDsa7uMq+uTt2epdPK+a2g6btcY11Ww==", - "requires": { - "aproba": "^2.0.0", - "figgy-pudding": "^3.4.1", - "get-stream": "^4.0.0", - "npm-registry-fetch": "^4.0.0" - } - }, - "libnpmpublish": { - "version": "1.1.2", - "resolved": false, - "integrity": "sha512-2yIwaXrhTTcF7bkJKIKmaCV9wZOALf/gsTDxVSu/Gu/6wiG3fA8ce8YKstiWKTxSFNC0R7isPUb6tXTVFZHt2g==", - "requires": { - "aproba": "^2.0.0", - "figgy-pudding": "^3.5.1", - "get-stream": "^4.0.0", - "lodash.clonedeep": "^4.5.0", - "normalize-package-data": "^2.4.0", - "npm-package-arg": "^6.1.0", - "npm-registry-fetch": "^4.0.0", - "semver": "^5.5.1", - "ssri": "^6.0.1" - } - }, - "libnpmsearch": { - "version": "2.0.2", - "resolved": false, - "integrity": "sha512-VTBbV55Q6fRzTdzziYCr64+f8AopQ1YZ+BdPOv16UegIEaE8C0Kch01wo4s3kRTFV64P121WZJwgmBwrq68zYg==", - "requires": { - "figgy-pudding": "^3.5.1", - "get-stream": "^4.0.0", - "npm-registry-fetch": "^4.0.0" - } - }, - "libnpmteam": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha512-p420vM28Us04NAcg1rzgGW63LMM6rwe+6rtZpfDxCcXxM0zUTLl7nPFEnRF3JfFBF5skF/yuZDUthTsHgde8QA==", - "requires": { - "aproba": "^2.0.0", - "figgy-pudding": "^3.4.1", - "get-stream": "^4.0.0", - "npm-registry-fetch": "^4.0.0" - } - }, - "libnpx": { - "version": "10.2.4", - "resolved": false, - "integrity": "sha512-BPc0D1cOjBeS8VIBKUu5F80s6njm0wbVt7CsGMrIcJ+SI7pi7V0uVPGpEMH9H5L8csOcclTxAXFE2VAsJXUhfA==", - "requires": { - "dotenv": "^5.0.1", - "npm-package-arg": "^6.0.0", - "rimraf": "^2.6.2", - "safe-buffer": "^5.1.0", - "update-notifier": "^2.3.0", - "which": "^1.3.0", - "y18n": "^4.0.0", - "yargs": "^14.2.3" - } - }, - "lock-verify": { - "version": "2.1.0", - "resolved": false, - "integrity": "sha512-vcLpxnGvrqisKvLQ2C2v0/u7LVly17ak2YSgoK4PrdsYBXQIax19vhKiLfvKNFx7FRrpTnitrpzF/uuCMuorIg==", - "requires": { - "npm-package-arg": "^6.1.0", - "semver": "^5.4.1" - } - }, - "lockfile": { - "version": "1.0.4", - "resolved": false, - "integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==", - "requires": { - "signal-exit": "^3.0.2" - } - }, - "lodash._baseindexof": { - "version": "3.1.0", - "resolved": false, - "integrity": "sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw=" - }, - "lodash._baseuniq": { - "version": "4.6.0", - "resolved": false, - "integrity": "sha1-DrtE5FaBSveQXGIS+iybLVG4Qeg=", - "requires": { - "lodash._createset": "~4.0.0", - "lodash._root": "~3.0.0" - } - }, - "lodash._bindcallback": { - "version": "3.0.1", - "resolved": false, - "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=" - }, - "lodash._cacheindexof": { - "version": "3.0.2", - "resolved": false, - "integrity": "sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI=" - }, - "lodash._createcache": { - "version": "3.1.2", - "resolved": false, - "integrity": "sha1-VtagZAF2JeeevKa4AY4XRAvc8JM=", - "requires": { - "lodash._getnative": "^3.0.0" - } - }, - "lodash._createset": { - "version": "4.0.3", - "resolved": false, - "integrity": "sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY=" - }, - "lodash._getnative": { - "version": "3.9.1", - "resolved": false, - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=" - }, - "lodash._root": { - "version": "3.0.1", - "resolved": false, - "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=" - }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": false, - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" - }, - "lodash.restparam": { - "version": "3.6.1", - "resolved": false, - "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=" - }, - "lodash.union": { - "version": "4.6.0", - "resolved": false, - "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" - }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": false, - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" - }, - "lodash.without": { - "version": "4.4.0", - "resolved": false, - "integrity": "sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=" - }, - "lowercase-keys": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" - }, - "lru-cache": { - "version": "5.1.1", - "resolved": false, - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "requires": { - "yallist": "^3.0.2" - } - }, - "make-dir": { - "version": "1.3.0", - "resolved": false, - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "requires": { - "pify": "^3.0.0" - } - }, - "make-fetch-happen": { - "version": "5.0.2", - "resolved": false, - "integrity": "sha512-07JHC0r1ykIoruKO8ifMXu+xEU8qOXDFETylktdug6vJDACnP+HKevOu3PXyNPzFyTSlz8vrBYlBO1JZRe8Cag==", - "requires": { - "agentkeepalive": "^3.4.1", - "cacache": "^12.0.0", - "http-cache-semantics": "^3.8.1", - "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^2.2.3", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "node-fetch-npm": "^2.0.2", - "promise-retry": "^1.1.1", - "socks-proxy-agent": "^4.0.0", - "ssri": "^6.0.0" - } - }, - "meant": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha512-KN+1uowN/NK+sT/Lzx7WSGIj2u+3xe5n2LbwObfjOhPZiA+cCfCm6idVl0RkEfjThkw5XJ96CyRcanq6GmKtUg==" - }, - "mime-db": { - "version": "1.35.0", - "resolved": false, - "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==" - }, - "mime-types": { - "version": "2.1.19", - "resolved": false, - "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", - "requires": { - "mime-db": "~1.35.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": false, - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": false, - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "minizlib": { - "version": "1.3.3", - "resolved": false, - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "requires": { - "minipass": "^2.9.0" - }, - "dependencies": { - "minipass": { - "version": "2.9.0", - "resolved": false, - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - } - } - }, - "mississippi": { - "version": "3.0.0", - "resolved": false, - "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", - "requires": { - "concat-stream": "^1.5.0", - "duplexify": "^3.4.2", - "end-of-stream": "^1.1.0", - "flush-write-stream": "^1.0.0", - "from2": "^2.1.0", - "parallel-transform": "^1.1.0", - "pump": "^3.0.0", - "pumpify": "^1.3.3", - "stream-each": "^1.1.0", - "through2": "^2.0.0" - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": false, - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "requires": { - "minimist": "^1.2.5" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": false, - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - } - } - }, - "move-concurrently": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", - "requires": { - "aproba": "^1.1.1", - "copy-concurrently": "^1.0.0", - "fs-write-stream-atomic": "^1.0.8", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.3" - }, - "dependencies": { - "aproba": { - "version": "1.2.0", - "resolved": false, - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - } - } - }, - "ms": { - "version": "2.1.1", - "resolved": false, - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - }, - "mute-stream": { - "version": "0.0.7", - "resolved": false, - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" - }, - "node-fetch-npm": { - "version": "2.0.2", - "resolved": false, - "integrity": "sha512-nJIxm1QmAj4v3nfCvEeCrYSoVwXyxLnaPBK5W1W5DGEJwjlKuC2VEUycGw5oxk+4zZahRrB84PUJJgEmhFTDFw==", - "requires": { - "encoding": "^0.1.11", - "json-parse-better-errors": "^1.0.0", - "safe-buffer": "^5.1.1" - } - }, - "node-gyp": { - "version": "5.1.0", - "resolved": false, - "integrity": "sha512-OUTryc5bt/P8zVgNUmC6xdXiDJxLMAW8cF5tLQOT9E5sOQj+UeQxnnPy74K3CLCa/SOjjBlbuzDLR8ANwA+wmw==", - "requires": { - "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.2", - "mkdirp": "^0.5.1", - "nopt": "^4.0.1", - "npmlog": "^4.1.2", - "request": "^2.88.0", - "rimraf": "^2.6.3", - "semver": "^5.7.1", - "tar": "^4.4.12", - "which": "^1.3.1" - } - }, - "nopt": { - "version": "4.0.3", - "resolved": false, - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": false, - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "resolve": { - "version": "1.10.0", - "resolved": false, - "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", - "requires": { - "path-parse": "^1.0.6" - } - } - } - }, - "npm-audit-report": { - "version": "1.3.3", - "resolved": false, - "integrity": "sha512-8nH/JjsFfAWMvn474HB9mpmMjrnKb1Hx/oTAdjv4PT9iZBvBxiZ+wtDUapHCJwLqYGQVPaAfs+vL5+5k9QndXw==", - "requires": { - "cli-table3": "^0.5.0", - "console-control-strings": "^1.1.0" - } - }, - "npm-bundled": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-cache-filename": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-3tMGxbC/yHCp6fr4I7xfKD4FrhE=" - }, - "npm-install-checks": { - "version": "3.0.2", - "resolved": false, - "integrity": "sha512-E4kzkyZDIWoin6uT5howP8VDvkM+E8IQDcHAycaAxMbwkqhIg5eEYALnXOl3Hq9MrkdQB/2/g1xwBINXdKSRkg==", - "requires": { - "semver": "^2.3.0 || 3.x || 4 || 5" - } - }, - "npm-lifecycle": { - "version": "3.1.5", - "resolved": false, - "integrity": "sha512-lDLVkjfZmvmfvpvBzA4vzee9cn+Me4orq0QF8glbswJVEbIcSNWib7qGOffolysc3teCqbbPZZkzbr3GQZTL1g==", - "requires": { - "byline": "^5.0.0", - "graceful-fs": "^4.1.15", - "node-gyp": "^5.0.2", - "resolve-from": "^4.0.0", - "slide": "^1.1.6", - "uid-number": "0.0.6", - "umask": "^1.1.0", - "which": "^1.3.1" - } - }, - "npm-logical-tree": { - "version": "1.2.1", - "resolved": false, - "integrity": "sha512-AJI/qxDB2PWI4LG1CYN579AY1vCiNyWfkiquCsJWqntRu/WwimVrC8yXeILBFHDwxfOejxewlmnvW9XXjMlYIg==" - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" - }, - "npm-package-arg": { - "version": "6.1.1", - "resolved": false, - "integrity": "sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==", - "requires": { - "hosted-git-info": "^2.7.1", - "osenv": "^0.1.5", - "semver": "^5.6.0", - "validate-npm-package-name": "^3.0.0" - } - }, - "npm-packlist": { - "version": "1.4.8", - "resolved": false, - "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-pick-manifest": { - "version": "3.0.2", - "resolved": false, - "integrity": "sha512-wNprTNg+X5nf+tDi+hbjdHhM4bX+mKqv6XmPh7B5eG+QY9VARfQPfCEH013H5GqfNj6ee8Ij2fg8yk0mzps1Vw==", - "requires": { - "figgy-pudding": "^3.5.1", - "npm-package-arg": "^6.0.0", - "semver": "^5.4.1" - } - }, - "npm-profile": { - "version": "4.0.4", - "resolved": false, - "integrity": "sha512-Ta8xq8TLMpqssF0H60BXS1A90iMoM6GeKwsmravJ6wYjWwSzcYBTdyWa3DZCYqPutacBMEm7cxiOkiIeCUAHDQ==", - "requires": { - "aproba": "^1.1.2 || 2", - "figgy-pudding": "^3.4.1", - "npm-registry-fetch": "^4.0.0" - } - }, - "npm-registry-fetch": { - "version": "4.0.7", - "resolved": false, - "integrity": "sha512-cny9v0+Mq6Tjz+e0erFAB+RYJ/AVGzkjnISiobqP8OWj9c9FLoZZu8/SPSKJWE17F1tk4018wfjV+ZbIbqC7fQ==", - "requires": { - "JSONStream": "^1.3.4", - "bluebird": "^3.5.1", - "figgy-pudding": "^3.4.1", - "lru-cache": "^5.1.1", - "make-fetch-happen": "^5.0.0", - "npm-package-arg": "^6.1.0", - "safe-buffer": "^5.2.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.1", - "resolved": false, - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - } - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": false, - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "requires": { - "path-key": "^2.0.0" - } - }, - "npm-user-validate": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-jOyg9c6gTU6TUZ73LQVXp1Ei6VE=" - }, - "npmlog": { - "version": "4.1.2", - "resolved": false, - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": false, - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, - "object-assign": { - "version": "4.1.1", - "resolved": false, - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-keys": { - "version": "1.0.12", - "resolved": false, - "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==" - }, - "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": false, - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": false, - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "opener": { - "version": "1.5.1", - "resolved": false, - "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==" - }, - "os-homedir": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.5", - "resolved": false, - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "p-finally": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" - }, - "package-json": { - "version": "4.0.1", - "resolved": false, - "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", - "requires": { - "got": "^6.7.1", - "registry-auth-token": "^3.0.1", - "registry-url": "^3.0.3", - "semver": "^5.1.0" - } - }, - "pacote": { - "version": "9.5.12", - "resolved": false, - "integrity": "sha512-BUIj/4kKbwWg4RtnBncXPJd15piFSVNpTzY0rysSr3VnMowTYgkGKcaHrbReepAkjTr8lH2CVWRi58Spg2CicQ==", - "requires": { - "bluebird": "^3.5.3", - "cacache": "^12.0.2", - "chownr": "^1.1.2", - "figgy-pudding": "^3.5.1", - "get-stream": "^4.1.0", - "glob": "^7.1.3", - "infer-owner": "^1.0.4", - "lru-cache": "^5.1.1", - "make-fetch-happen": "^5.0.0", - "minimatch": "^3.0.4", - "minipass": "^2.3.5", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "normalize-package-data": "^2.4.0", - "npm-normalize-package-bin": "^1.0.0", - "npm-package-arg": "^6.1.0", - "npm-packlist": "^1.1.12", - "npm-pick-manifest": "^3.0.0", - "npm-registry-fetch": "^4.0.0", - "osenv": "^0.1.5", - "promise-inflight": "^1.0.1", - "promise-retry": "^1.1.1", - "protoduck": "^5.0.1", - "rimraf": "^2.6.2", - "safe-buffer": "^5.1.2", - "semver": "^5.6.0", - "ssri": "^6.0.1", - "tar": "^4.4.10", - "unique-filename": "^1.1.1", - "which": "^1.3.1" - }, - "dependencies": { - "minipass": { - "version": "2.9.0", - "resolved": false, - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - } - } - }, - "parallel-transform": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", - "requires": { - "cyclist": "~0.2.2", - "inherits": "^2.0.3", - "readable-stream": "^2.1.5" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": false, - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": false, - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" - }, - "path-key": { - "version": "2.0.1", - "resolved": false, - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" - }, - "path-parse": { - "version": "1.0.6", - "resolved": false, - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" - }, - "performance-now": { - "version": "2.1.0", - "resolved": false, - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "pify": { - "version": "3.0.0", - "resolved": false, - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - }, - "prepend-http": { - "version": "1.0.4", - "resolved": false, - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" - }, - "promise-inflight": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" - }, - "promise-retry": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0=", - "requires": { - "err-code": "^1.0.0", - "retry": "^0.10.0" - }, - "dependencies": { - "retry": { - "version": "0.10.1", - "resolved": false, - "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=" - } - } - }, - "promzard": { - "version": "0.3.0", - "resolved": false, - "integrity": "sha1-JqXW7ox97kyxIggwWs+5O6OCqe4=", - "requires": { - "read": "1" - } - }, - "proto-list": { - "version": "1.2.4", - "resolved": false, - "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=" - }, - "protoduck": { - "version": "5.0.1", - "resolved": false, - "integrity": "sha512-WxoCeDCoCBY55BMvj4cAEjdVUFGRWed9ZxPlqTKYyw1nDDTQ4pqmnIMAGfJlg7Dx35uB/M+PHJPTmGOvaCaPTg==", - "requires": { - "genfun": "^5.0.0" - } - }, - "prr": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" - }, - "pseudomap": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, - "psl": { - "version": "1.1.29", - "resolved": false, - "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" - }, - "pump": { - "version": "3.0.0", - "resolved": false, - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "1.5.1", - "resolved": false, - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "requires": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - }, - "dependencies": { - "pump": { - "version": "2.0.1", - "resolved": false, - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "punycode": { - "version": "1.4.1", - "resolved": false, - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "qrcode-terminal": { - "version": "0.12.0", - "resolved": false, - "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==" - }, - "qs": { - "version": "6.5.2", - "resolved": false, - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "query-string": { - "version": "6.8.2", - "resolved": false, - "integrity": "sha512-J3Qi8XZJXh93t2FiKyd/7Ec6GNifsjKXUsVFkSBj/kjLsDylWhnCz4NT1bkPcKotttPW+QbKGqqPH8OoI2pdqw==", - "requires": { - "decode-uri-component": "^0.2.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" - } - }, - "qw": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha1-77/cdA+a0FQwRCassYNBLMi5ltQ=" - }, - "rc": { - "version": "1.2.8", - "resolved": false, - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "read": { - "version": "1.0.7", - "resolved": false, - "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", - "requires": { - "mute-stream": "~0.0.4" - } - }, - "read-cmd-shim": { - "version": "1.0.5", - "resolved": false, - "integrity": "sha512-v5yCqQ/7okKoZZkBQUAfTsQ3sVJtXdNfbPnI5cceppoxEVLYA3k+VtV2omkeo8MS94JCy4fSiUwlRBAwCVRPUA==", - "requires": { - "graceful-fs": "^4.1.2" - } - }, - "read-installed": { - "version": "4.0.3", - "resolved": false, - "integrity": "sha1-/5uLZ/GH0eTCm5/rMfayI6zRkGc=", - "requires": { - "debuglog": "^1.0.1", - "graceful-fs": "^4.1.2", - "read-package-json": "^2.0.0", - "readdir-scoped-modules": "^1.0.0", - "semver": "2 || 3 || 4 || 5", - "slide": "~1.1.3", - "util-extend": "^1.0.1" - } - }, - "read-package-json": { - "version": "2.1.1", - "resolved": false, - "integrity": "sha512-dAiqGtVc/q5doFz6096CcnXhpYk0ZN8dEKVkGLU0CsASt8SrgF6SF7OTKAYubfvFhWaqofl+Y8HK19GR8jwW+A==", - "requires": { - "glob": "^7.1.1", - "graceful-fs": "^4.1.2", - "json-parse-better-errors": "^1.0.1", - "normalize-package-data": "^2.0.0", - "npm-normalize-package-bin": "^1.0.0" - } - }, - "read-package-tree": { - "version": "5.3.1", - "resolved": false, - "integrity": "sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw==", - "requires": { - "read-package-json": "^2.0.0", - "readdir-scoped-modules": "^1.0.0", - "util-promisify": "^2.1.0" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": false, - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "readdir-scoped-modules": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", - "requires": { - "debuglog": "^1.0.1", - "dezalgo": "^1.0.0", - "graceful-fs": "^4.1.2", - "once": "^1.3.0" - } - }, - "registry-auth-token": { - "version": "3.4.0", - "resolved": false, - "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", - "requires": { - "rc": "^1.1.6", - "safe-buffer": "^5.0.1" - } - }, - "registry-url": { - "version": "3.1.0", - "resolved": false, - "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", - "requires": { - "rc": "^1.0.1" - } - }, - "request": { - "version": "2.88.0", - "resolved": false, - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": false, - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "resolve-from": { - "version": "4.0.0", - "resolved": false, - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" - }, - "retry": { - "version": "0.12.0", - "resolved": false, - "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" - }, - "rimraf": { - "version": "2.7.1", - "resolved": false, - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "requires": { - "glob": "^7.1.3" - } - }, - "run-queue": { - "version": "1.0.3", - "resolved": false, - "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", - "requires": { - "aproba": "^1.1.1" - }, - "dependencies": { - "aproba": { - "version": "1.2.0", - "resolved": false, - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - } - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": false, - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": false, - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "semver": { - "version": "5.7.1", - "resolved": false, - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - }, - "semver-diff": { - "version": "2.1.0", - "resolved": false, - "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", - "requires": { - "semver": "^5.0.3" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "sha": { - "version": "3.0.0", - "resolved": false, - "integrity": "sha512-DOYnM37cNsLNSGIG/zZWch5CKIRNoLdYUQTQlcgkRkoYIUwDYjqDyye16YcDZg/OPdcbUgTKMjc4SY6TB7ZAPw==", - "requires": { - "graceful-fs": "^4.1.2" - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": false, - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" - }, - "signal-exit": { - "version": "3.0.2", - "resolved": false, - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "slide": { - "version": "1.1.6", - "resolved": false, - "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=" - }, - "smart-buffer": { - "version": "4.1.0", - "resolved": false, - "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==" - }, - "socks": { - "version": "2.3.3", - "resolved": false, - "integrity": "sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==", - "requires": { - "ip": "1.1.5", - "smart-buffer": "^4.1.0" - } - }, - "socks-proxy-agent": { - "version": "4.0.2", - "resolved": false, - "integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==", - "requires": { - "agent-base": "~4.2.1", - "socks": "~2.3.2" - }, - "dependencies": { - "agent-base": { - "version": "4.2.1", - "resolved": false, - "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", - "requires": { - "es6-promisify": "^5.0.0" - } - } - } - }, - "sorted-object": { - "version": "2.0.1", - "resolved": false, - "integrity": "sha1-fWMfS9OnmKJK8d/8+/6DM3pd9fw=" - }, - "sorted-union-stream": { - "version": "2.1.3", - "resolved": false, - "integrity": "sha1-x3lMfgd4gAUv9xqNSi27Sppjisc=", - "requires": { - "from2": "^1.3.0", - "stream-iterate": "^1.1.0" - }, - "dependencies": { - "from2": { - "version": "1.3.0", - "resolved": false, - "integrity": "sha1-iEE7qqX5pZfP3pIh2GmGzTwGHf0=", - "requires": { - "inherits": "~2.0.1", - "readable-stream": "~1.1.10" - } - }, - "isarray": { - "version": "0.0.1", - "resolved": false, - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.1.14", - "resolved": false, - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": false, - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "spdx-correct": { - "version": "3.0.0", - "resolved": false, - "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.1.0", - "resolved": false, - "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==" - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": false, - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.5", - "resolved": false, - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==" - }, - "split-on-first": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==" - }, - "sshpk": { - "version": "1.14.2", - "resolved": false, - "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "ssri": { - "version": "6.0.1", - "resolved": false, - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", - "requires": { - "figgy-pudding": "^3.5.1" - } - }, - "stream-each": { - "version": "1.2.2", - "resolved": false, - "integrity": "sha512-mc1dbFhGBxvTM3bIWmAAINbqiuAk9TATcfIQC8P+/+HJefgaiTlMn2dHvkX8qlI12KeYKSQ1Ua9RrIqrn1VPoA==", - "requires": { - "end-of-stream": "^1.1.0", - "stream-shift": "^1.0.0" - } - }, - "stream-iterate": { - "version": "1.2.0", - "resolved": false, - "integrity": "sha1-K9fHcpbBcCpGSIuK1B95hl7s1OE=", - "requires": { - "readable-stream": "^2.1.5", - "stream-shift": "^1.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": false, - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "stream-shift": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" - }, - "strict-uri-encode": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=" - }, - "string-width": { - "version": "2.1.1", - "resolved": false, - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": false, - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": false, - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "string_decoder": { - "version": "1.3.0", - "resolved": false, - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.0", - "resolved": false, - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" - } - } - }, - "stringify-package": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg==" - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": false, - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": false, - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "supports-color": { - "version": "5.4.0", - "resolved": false, - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "tar": { - "version": "4.4.13", - "resolved": false, - "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - }, - "dependencies": { - "minipass": { - "version": "2.9.0", - "resolved": false, - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - } - } - }, - "term-size": { - "version": "1.2.0", - "resolved": false, - "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", - "requires": { - "execa": "^0.7.0" - } - }, - "text-table": { - "version": "0.2.0", - "resolved": false, - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" - }, - "through": { - "version": "2.3.8", - "resolved": false, - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "through2": { - "version": "2.0.3", - "resolved": false, - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", - "requires": { - "readable-stream": "^2.1.5", - "xtend": "~4.0.1" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": false, - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "timed-out": { - "version": "4.0.1", - "resolved": false, - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=" - }, - "tiny-relative-date": { - "version": "1.3.0", - "resolved": false, - "integrity": "sha512-MOQHpzllWxDCHHaDno30hhLfbouoYlOI8YlMNtvKe1zXbjEVhbcEovQxvZrPvtiYW630GQDoMMarCnjfyfHA+A==" - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": false, - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": false, - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": false, - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, - "typedarray": { - "version": "0.0.6", - "resolved": false, - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "uid-number": { - "version": "0.0.6", - "resolved": false, - "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=" - }, - "umask": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=" - }, - "unique-filename": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "requires": { - "unique-slug": "^2.0.0" - } - }, - "unique-slug": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha1-22Z258fMBimHj/GWCXx4hVrp9Ks=", - "requires": { - "imurmurhash": "^0.1.4" - } - }, - "unique-string": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", - "requires": { - "crypto-random-string": "^1.0.0" - } - }, - "unpipe": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "unzip-response": { - "version": "2.0.1", - "resolved": false, - "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=" - }, - "update-notifier": { - "version": "2.5.0", - "resolved": false, - "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", - "requires": { - "boxen": "^1.2.1", - "chalk": "^2.0.1", - "configstore": "^3.0.0", - "import-lazy": "^2.1.0", - "is-ci": "^1.0.10", - "is-installed-globally": "^0.1.0", - "is-npm": "^1.0.0", - "latest-version": "^3.0.0", - "semver-diff": "^2.0.0", - "xdg-basedir": "^3.0.0" - } - }, - "url-parse-lax": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", - "requires": { - "prepend-http": "^1.0.1" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "util-extend": { - "version": "1.0.3", - "resolved": false, - "integrity": "sha1-p8IW0mdUUWljeztu3GypEZ4v+T8=" - }, - "util-promisify": { - "version": "2.1.0", - "resolved": false, - "integrity": "sha1-PCI2R2xNMsX/PEcAKt18E7moKlM=", - "requires": { - "object.getownpropertydescriptors": "^2.0.3" - } - }, - "uuid": { - "version": "3.3.3", - "resolved": false, - "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": false, - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "validate-npm-package-name": { - "version": "3.0.0", - "resolved": false, - "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", - "requires": { - "builtins": "^1.0.3" - } - }, - "verror": { - "version": "1.10.0", - "resolved": false, - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "wcwidth": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", - "requires": { - "defaults": "^1.0.3" - } - }, - "which": { - "version": "1.3.1", - "resolved": false, - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" - }, - "wide-align": { - "version": "1.1.2", - "resolved": false, - "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", - "requires": { - "string-width": "^1.0.2" - }, - "dependencies": { - "string-width": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, - "widest-line": { - "version": "2.0.1", - "resolved": false, - "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", - "requires": { - "string-width": "^2.1.1" - } - }, - "worker-farm": { - "version": "1.7.0", - "resolved": false, - "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", - "requires": { - "errno": "~0.1.7" - } - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": false, - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": false, - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "3.1.0", - "resolved": false, - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": false, - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write-file-atomic": { - "version": "2.4.3", - "resolved": false, - "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "xdg-basedir": { - "version": "3.0.0", - "resolved": false, - "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=" - }, - "xtend": { - "version": "4.0.1", - "resolved": false, - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" - }, - "y18n": { - "version": "4.0.0", - "resolved": false, - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" - }, - "yallist": { - "version": "3.0.3", - "resolved": false, - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" - }, - "yargs": { - "version": "14.2.3", - "resolved": false, - "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==", - "requires": { - "cliui": "^5.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^15.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": false, - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "find-up": { - "version": "3.0.0", - "resolved": false, - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "locate-path": { - "version": "3.0.0", - "resolved": false, - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": false, - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": false, - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": false, - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - }, - "string-width": { - "version": "3.1.0", - "resolved": false, - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": false, - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "yargs-parser": { - "version": "15.0.1", - "resolved": false, - "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": false, - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" - } - } - } - } - }, - "nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "requires": { - "boolbase": "~1.0.0" - } - }, - "nyc": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.0.1.tgz", - "integrity": "sha512-n0MBXYBYRqa67IVt62qW1r/d9UH/Qtr7SF1w/nQLJ9KxvWF6b2xCHImRAixHN9tnMMYHC2P14uo6KddNGwMgGg==", - "requires": { - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "caching-transform": "^4.0.0", - "convert-source-map": "^1.7.0", - "decamelize": "^1.2.0", - "find-cache-dir": "^3.2.0", - "find-up": "^4.1.0", - "foreground-child": "^2.0.0", - "glob": "^7.1.6", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-hook": "^3.0.0", - "istanbul-lib-instrument": "^4.0.0", - "istanbul-lib-processinfo": "^2.0.2", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.2", - "make-dir": "^3.0.0", - "node-preload": "^0.2.1", - "p-map": "^3.0.0", - "process-on-spawn": "^1.0.0", - "resolve-from": "^5.0.0", - "rimraf": "^3.0.0", - "signal-exit": "^3.0.2", - "spawn-wrap": "^2.0.0", - "test-exclude": "^6.0.0", - "yargs": "^15.0.2" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "yargs": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", - "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.1" - } - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, - "object-component": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", - "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" - }, - "object-inspect": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", - "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==" - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, - "object.getownpropertydescriptors": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz", - "integrity": "sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng==", - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1" - } - }, - "observable-fns": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/observable-fns/-/observable-fns-0.5.1.tgz", - "integrity": "sha512-wf7g4Jpo1Wt2KIqZKLGeiuLOEMqpaOZ5gJn7DmSdqXgTdxRwSdBhWegQQpPteQ2gZvzCKqNNpwb853wcpA0j7A==" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "openapi-backend": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/openapi-backend/-/openapi-backend-2.4.1.tgz", - "integrity": "sha512-48j8QhDD9sfV6t7Zgn9JrfJtCpJ53bmoT2bzXYYig1HhG/Xn0Aa5fJhM0cQSZq9nq78/XbU7RDEa3e+IADNkmA==", - "requires": { - "ajv": "^6.10.0", - "bath-es5": "^3.0.3", - "cookie": "^0.4.0", - "lodash": "^4.17.15", - "mock-json-schema": "^1.0.5", - "openapi-schema-validation": "^0.4.2", - "openapi-types": "^1.3.4", - "qs": "^6.6.0", - "swagger-parser": "^9.0.1" - }, - "dependencies": { - "cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" - } - } - }, - "openapi-schema-validation": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/openapi-schema-validation/-/openapi-schema-validation-0.4.2.tgz", - "integrity": "sha512-K8LqLpkUf2S04p2Nphq9L+3bGFh/kJypxIG2NVGKX0ffzT4NQI9HirhiY6Iurfej9lCu7y4Ndm4tv+lm86Ck7w==", - "requires": { - "jsonschema": "1.2.4", - "jsonschema-draft4": "^1.0.0", - "swagger-schema-official": "2.0.0-bab6bed" - } - }, - "openapi-types": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-1.3.5.tgz", - "integrity": "sha512-11oi4zYorsgvg5yBarZplAqbpev5HkuVNPlZaPTknPDzAynq+lnJdXAmruGWP0s+dNYZS7bjM+xrTpJw7184Fg==" - }, - "optional-js": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/optional-js/-/optional-js-2.1.1.tgz", - "integrity": "sha512-mUS4bDngcD5kKzzRUd1HVQkr9Lzzby3fSrrPR9wOHhQiyYo+hDS5NVli5YQzGjQRQ15k5Sno4xH9pfykJdeEUA==" - }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-map": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - }, - "package-hash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", - "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", - "requires": { - "graceful-fs": "^4.1.15", - "hasha": "^5.0.0", - "lodash.flattendeep": "^4.4.0", - "release-zalgo": "^1.0.0" - } - }, - "packet-reader": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", - "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "requires": { - "callsites": "^3.0.0" - } - }, - "parse5": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", - "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" - }, - "parseqs": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", - "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", - "requires": { - "better-assert": "~1.0.0" - } - }, - "parseuri": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", - "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", - "requires": { - "better-assert": "~1.0.0" - } - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "pg": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.5.1.tgz", - "integrity": "sha512-9wm3yX9lCfjvA98ybCyw2pADUivyNWT/yIP4ZcDVpMN0og70BUWYEGXPCTAQdGTAqnytfRADb7NERrY1qxhIqw==", - "requires": { - "buffer-writer": "2.0.0", - "packet-reader": "1.0.0", - "pg-connection-string": "^2.4.0", - "pg-pool": "^3.2.2", - "pg-protocol": "^1.4.0", - "pg-types": "^2.1.0", - "pgpass": "1.x" - } - }, - "pg-connection-string": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.4.0.tgz", - "integrity": "sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ==" - }, - "pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" - }, - "pg-pool": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.2.2.tgz", - "integrity": "sha512-ORJoFxAlmmros8igi608iVEbQNNZlp89diFVx6yV5v+ehmpMY9sK6QgpmgoXbmkNaBAx8cOOZh9g80kJv1ooyA==" - }, - "pg-protocol": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.4.0.tgz", - "integrity": "sha512-El+aXWcwG/8wuFICMQjM5ZSAm6OWiJicFdNYo+VY3QP+8vI4SvLIWVe51PppTzMhikUJR+PsyIFKqfdXPz/yxA==" - }, - "pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "requires": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - } - }, - "pgpass": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.4.tgz", - "integrity": "sha512-YmuA56alyBq7M59vxVBfPJrGSozru8QAdoNlWuW3cz8l+UX3cWge0vTvjKhsSHSJpo3Bom8/Mm6hf0TR5GY0+w==", - "requires": { - "split2": "^3.1.1" - } - }, - "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "requires": { - "find-up": "^4.0.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" - } - } - }, - "postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" - }, - "postgres-bytea": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", - "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=" - }, - "postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==" - }, - "postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "requires": { - "xtend": "^4.0.0" - } - }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "process-on-spawn": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", - "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", - "requires": { - "fromentries": "^1.2.0" - } - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" - }, - "promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/promise/-/promise-1.3.0.tgz", - "integrity": "sha1-5cyaTIJ45GZP/twBx9qEhCsEAXU=", - "requires": { - "is-promise": "~1" - } - }, - "property-information": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.5.0.tgz", - "integrity": "sha512-RgEbCx2HLa1chNgvChcx+rrCWD0ctBmGSE0M7lVm1yyv4UbvbrWoXp/BkVLZefzjrRBGW8/Js6uh/BnlHXFyjA==", - "requires": { - "xtend": "^4.0.0" - } - }, - "proxy-addr": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", - "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.9.1" - } - }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" - }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" - }, - "ramda": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz", - "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==" - }, - "randexp": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.5.3.tgz", - "integrity": "sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==", - "requires": { - "drange": "^1.0.2", - "ret": "^0.2.0" - } - }, - "random-bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", - "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" - }, - "rate-limiter-flexible": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.1.4.tgz", - "integrity": "sha512-wtbWcqZbCqyAO1k63moagJlCZuPCEqbJJ6il1y2JVoiUyxlE36+cM7ETta9K6tTom9O5pNK+CxwHMgyyyJ31Gg==" - }, - "raw-body": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", - "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "dependencies": { - "http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - } - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "readdirp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", - "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", - "requires": { - "picomatch": "^2.0.4" - } - }, - "redis": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/redis/-/redis-3.0.2.tgz", - "integrity": "sha512-PNhLCrjU6vKVuMOyFu7oSP296mwBkcE6lrAjruBYG5LgdSqtRBoVQIylrMyVZD/lkF24RSNNatzvYag6HRBHjQ==", - "requires": { - "denque": "^1.4.1", - "redis-commands": "^1.5.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0" - } - }, - "redis-commands": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.6.0.tgz", - "integrity": "sha512-2jnZ0IkjZxvguITjFTrGiLyzQZcTvaw8DAaCXxZq/dsHXz7KfMQ3OUJy7Tz9vnRtZRVz6VRCPDvruvU8Ts44wQ==" - }, - "redis-errors": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=" - }, - "redis-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", - "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=", - "requires": { - "redis-errors": "^1.0.0" - } - }, - "regexpp": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", - "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==" - }, - "rehype": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/rehype/-/rehype-10.0.0.tgz", - "integrity": "sha512-0W8M4Y91b2QuzDSTjkZgBOJo79bP089YbSQNPMqebuUVrp6iveoi+Ra6/H7fJwUxq8FCHGCGzkLaq3fvO9XnVg==", - "requires": { - "rehype-parse": "^6.0.0", - "rehype-stringify": "^6.0.0", - "unified": "^9.0.0" - } - }, - "rehype-minify-whitespace": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/rehype-minify-whitespace/-/rehype-minify-whitespace-4.0.5.tgz", - "integrity": "sha512-QC3Z+bZ5wbv+jGYQewpAAYhXhzuH/TVRx7z08rurBmh9AbG8Nu8oJnvs9LWj43Fd/C7UIhXoQ7Wddgt+ThWK5g==", - "requires": { - "hast-util-embedded": "^1.0.0", - "hast-util-is-element": "^1.0.0", - "hast-util-whitespace": "^1.0.4", - "unist-util-is": "^4.0.0" - }, - "dependencies": { - "unist-util-is": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.0.4.tgz", - "integrity": "sha512-3dF39j/u423v4BBQrk1AQ2Ve1FxY5W3JKwXxVFzBODQ6WEvccguhgp802qQLKSnxPODE6WuRZtV+ohlUg4meBA==" - } - } - }, - "rehype-parse": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-6.0.2.tgz", - "integrity": "sha512-0S3CpvpTAgGmnz8kiCyFLGuW5yA4OQhyNTm/nwPopZ7+PI11WnGl1TTWTGv/2hPEe/g2jRLlhVVSsoDH8waRug==", - "requires": { - "hast-util-from-parse5": "^5.0.0", - "parse5": "^5.0.0", - "xtend": "^4.0.0" - } - }, - "rehype-stringify": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-6.0.1.tgz", - "integrity": "sha512-JfEPRDD4DiG7jet4md7sY07v6ACeb2x+9HWQtRPm2iA6/ic31hCv1SNBUtpolJASxQ/D8gicXiviW4TJKEMPKQ==", - "requires": { - "hast-util-to-html": "^6.0.0", - "xtend": "^4.0.0" - } - }, - "release-zalgo": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", - "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", - "requires": { - "es6-error": "^4.0.1" - } - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=" - }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - } - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" - }, - "ret": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", - "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==" - }, - "rethinkdb": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/rethinkdb/-/rethinkdb-2.4.2.tgz", - "integrity": "sha512-6DzwqEpFc8cqesAdo07a845oBRxLiHvWzopTKBo/uY2ypGWIsJQFJk3wjRDtSEhczxJqLS0jnf37rwgzYAw8NQ==", - "requires": { - "bluebird": ">= 2.3.2 < 3" - } - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, - "security": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/security/-/security-1.0.0.tgz", - "integrity": "sha1-gRwwAxNoYTPvAAcSXjsO1wCXiBU=" - }, - "semver": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" - }, - "send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "dependencies": { - "http-errors": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", - "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - } - } - }, - "serve-static": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "set-cookie-parser": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.4.6.tgz", - "integrity": "sha512-mNCnTUF0OYPwYzSHbdRdCfNNHqrne+HS5tS5xNb6yJbdP9wInV0q5xPLE0EyfV/Q3tImo3y/OXpD8Jn0Jtnjrg==" - }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" - }, - "simple-git": { - "version": "2.27.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.27.0.tgz", - "integrity": "sha512-/Q4aolzErYrIx6SgyH421jmtv5l1DaAw+KYWMWy229+isW6yld/nHGxJ2xUR/aeX3SuYJnbucyUigERwaw4Xow==", - "requires": { - "@kwsites/file-exists": "^1.1.1", - "@kwsites/promise-deferred": "^1.1.1", - "debug": "^4.3.1" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "sinon": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.0.tgz", - "integrity": "sha512-eSNXz1XMcGEMHw08NJXSyTHIu6qTCOiN8x9ODACmZpNQpr0aXTBXBnI4xTzQzR+TEpOmLiKowGf9flCuKIzsbw==", - "requires": { - "@sinonjs/commons": "^1.8.1", - "@sinonjs/fake-timers": "^6.0.1", - "@sinonjs/formatio": "^5.0.1", - "@sinonjs/samsam": "^5.2.0", - "diff": "^4.0.2", - "nise": "^4.0.4", - "supports-color": "^7.1.0" - }, - "dependencies": { - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - } - } - }, - "slide": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=" - }, - "socket.io": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz", - "integrity": "sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==", - "requires": { - "debug": "~4.1.0", - "engine.io": "~3.4.0", - "has-binary2": "~1.0.2", - "socket.io-adapter": "~1.1.0", - "socket.io-client": "2.3.0", - "socket.io-parser": "~3.4.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "socket.io-adapter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz", - "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==" - }, - "socket.io-client": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz", - "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==", - "requires": { - "backo2": "1.0.2", - "base64-arraybuffer": "0.1.5", - "component-bind": "1.0.0", - "component-emitter": "1.2.1", - "debug": "~4.1.0", - "engine.io-client": "~3.4.0", - "has-binary2": "~1.0.2", - "has-cors": "1.1.0", - "indexof": "0.0.1", - "object-component": "0.0.3", - "parseqs": "0.0.5", - "parseuri": "0.0.5", - "socket.io-parser": "~3.3.0", - "to-array": "0.1.4" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "socket.io-parser": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz", - "integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==", - "requires": { - "component-emitter": "1.2.1", - "debug": "~3.1.0", - "isarray": "2.0.1" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - } - } - }, - "socket.io-parser": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz", - "integrity": "sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==", - "requires": { - "component-emitter": "1.2.1", - "debug": "~4.1.0", - "isarray": "2.0.1" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "space-separated-tokens": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", - "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==" - }, - "spawn-wrap": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", - "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", - "requires": { - "foreground-child": "^2.0.0", - "is-windows": "^1.0.2", - "make-dir": "^3.0.0", - "rimraf": "^3.0.0", - "signal-exit": "^3.0.2", - "which": "^2.0.1" - }, - "dependencies": { - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "split2": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", - "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", - "requires": { - "readable-stream": "^3.0.0" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "sqlstring": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", - "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=" - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "string.prototype.trimend": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz", - "integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==", - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - } - }, - "string.prototype.trimstart": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz", - "integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==", - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - } - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "stringify-entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-2.0.0.tgz", - "integrity": "sha512-fqqhZzXyAM6pGD9lky/GOPq6V4X0SeTAFBl0iXb/BzOegl40gpf/bV3QQP7zULNYvjr6+Dx8SCaDULjVoOru0A==", - "requires": { - "character-entities-html4": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.2", - "is-hexadecimal": "^1.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==" - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "superagent": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", - "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", - "requires": { - "component-emitter": "^1.2.0", - "cookiejar": "^2.1.0", - "debug": "^3.1.0", - "extend": "^3.0.0", - "form-data": "^2.3.1", - "formidable": "^1.2.0", - "methods": "^1.1.1", - "mime": "^1.4.1", - "qs": "^6.5.1", - "readable-stream": "^2.3.5" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "supertest": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-4.0.2.tgz", - "integrity": "sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ==", - "requires": { - "methods": "^1.1.2", - "superagent": "^3.8.3" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "superagent": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", - "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", - "requires": { - "component-emitter": "^1.2.0", - "cookiejar": "^2.1.0", - "debug": "^3.1.0", - "extend": "^3.0.0", - "form-data": "^2.3.1", - "formidable": "^1.2.0", - "methods": "^1.1.1", - "mime": "^1.4.1", - "qs": "^6.5.1", - "readable-stream": "^2.3.5" - } - } - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "swagger-parser": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-9.0.1.tgz", - "integrity": "sha512-oxOHUaeNetO9ChhTJm2fD+48DbGbLD09ZEOwPOWEqcW8J6zmjWxutXtSuOiXsoRgDWvORYlImbwM21Pn+EiuvQ==", - "requires": { - "@apidevtools/swagger-parser": "9.0.1" - } - }, - "swagger-schema-official": { - "version": "2.0.0-bab6bed", - "resolved": "https://registry.npmjs.org/swagger-schema-official/-/swagger-schema-official-2.0.0-bab6bed.tgz", - "integrity": "sha1-cAcEaNbSl3ylI3suUZyn0Gouo/0=" - }, - "symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" - }, - "table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", - "requires": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "tar-stream": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.2.tgz", - "integrity": "sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q==", - "requires": { - "bl": "^4.0.1", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - } - }, - "tarn": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/tarn/-/tarn-1.1.5.tgz", - "integrity": "sha512-PMtJ3HCLAZeedWjJPgGnCvcphbCOMbtZpjKgLq3qM5Qq9aQud+XHrL0WlrlgnTyS8U+jrjGbEXprFcQrxPy52g==" - }, - "tedious": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/tedious/-/tedious-6.7.0.tgz", - "integrity": "sha512-8qr7+sB0h4SZVQBRWUgHmYuOEflAOl2eihvxk0fVNvpvGJV4V5UC/YmSvebyfgyfwWcPO22/AnSbYVZZqf9wuQ==", - "requires": { - "@azure/ms-rest-nodeauth": "2.0.2", - "@types/node": "^12.12.17", - "@types/readable-stream": "^2.3.5", - "bl": "^3.0.0", - "depd": "^2.0.0", - "iconv-lite": "^0.5.0", - "jsbi": "^3.1.1", - "native-duplexpair": "^1.0.0", - "punycode": "^2.1.0", - "readable-stream": "^3.4.0", - "sprintf-js": "^1.1.2" - }, - "dependencies": { - "@types/node": { - "version": "12.19.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.9.tgz", - "integrity": "sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q==" - }, - "bl": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/bl/-/bl-3.0.1.tgz", - "integrity": "sha512-jrCW5ZhfQ/Vt07WX1Ngs+yn9BDqPL/gw28S7s9H6QK/gupnizNzJAss5akW20ISgOrbLTlXOOCTJeNUQqruAWQ==", - "requires": { - "readable-stream": "^3.0.1" - } - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "iconv-lite": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz", - "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" - } - } - }, - "terser": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.7.0.tgz", - "integrity": "sha512-Lfb0RiZcjRDXCC3OSHJpEkxJ9Qeqs6mp2v4jf2MHfy8vGERmVDuvjXdd/EnP5Deme5F2yBRBymKmKHCBg2echw==", - "requires": { - "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" - } - }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "dependencies": { - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" - }, - "threads": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/threads/-/threads-1.4.1.tgz", - "integrity": "sha512-LSgGCu2lwdrfqjYWmeqO+7fgxAbUtjlsa7UA5J6r4x8fCoMd015h19rMwXqz4/q8l3svdloE36Of41rpZWiYFg==", - "requires": { - "callsites": "^3.1.0", - "debug": "^4.1.1", - "is-observable": "^1.1.0", - "observable-fns": "^0.5.1", - "tiny-worker": ">= 2" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "tiny-worker": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tiny-worker/-/tiny-worker-2.3.0.tgz", - "integrity": "sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==", - "requires": { - "esm": "^3.2.25" - } - }, - "tinycon": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/tinycon/-/tinycon-0.0.1.tgz", - "integrity": "sha1-beEM1SGaHxIdmgokssEbP7JN/+0=" - }, - "to-array": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", - "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "requires": { - "is-number": "^7.0.0" - } - }, - "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "trough": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", - "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==" - }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "requires": { - "is-typedarray": "^1.0.0" - } - }, - "ueberdb2": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-0.5.6.tgz", - "integrity": "sha512-stLhNkWlxUMAO33JjEh8JCRuZvHYeDQjbo6K1C3I7R37AlMKNu9GWXSZm1wQDnAqpXAXeMVh3owBsAdj0YvOrg==", - "requires": { - "async": "^3.2.0", - "cassandra-driver": "^4.5.1", - "chai": "^4.2.0", - "channels": "0.0.4", - "cli-table": "^0.3.1", - "dirty": "^1.1.0", - "elasticsearch": "^16.7.1", - "mocha": "^7.1.2", - "mssql": "^6.2.3", - "mysql": "2.18.1", - "nano": "^8.2.2", - "pg": "^8.0.3", - "randexp": "^0.5.3", - "redis": "^3.0.2", - "rethinkdb": "^2.4.2", - "rimraf": "^3.0.2", - "simple-git": "^2.4.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "mocha": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.2.0.tgz", - "integrity": "sha512-O9CIypScywTVpNaRrCAgoUnJgozpIofjKUYmJhiCIJMiuYnLI6otcb1/kpW9/n/tJODHGZ7i8aLQoDVsMtOKQQ==", - "requires": { - "ansi-colors": "3.2.3", - "browser-stdout": "1.3.1", - "chokidar": "3.3.0", - "debug": "3.2.6", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "find-up": "3.0.0", - "glob": "7.1.3", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "3.13.1", - "log-symbols": "3.0.0", - "minimatch": "3.0.4", - "mkdirp": "0.5.5", - "ms": "2.1.1", - "node-environment-flags": "1.0.6", - "object.assign": "4.1.0", - "strip-json-comments": "2.0.1", - "supports-color": "6.0.0", - "which": "1.3.1", - "wide-align": "1.1.3", - "yargs": "13.3.2", - "yargs-parser": "13.1.2", - "yargs-unparser": "1.6.0" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - }, - "supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "uid-safe": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", - "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", - "requires": { - "random-bytes": "~1.0.0" - } - }, - "underscore": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", - "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" - }, - "unified": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/unified/-/unified-9.0.0.tgz", - "integrity": "sha512-ssFo33gljU3PdlWLjNp15Inqb77d6JnJSfyplGJPT/a+fNRNyCBeveBAYJdO5khKdF6WVHa/yYCC7Xl6BDwZUQ==", - "requires": { - "bail": "^1.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^2.0.0", - "trough": "^1.0.0", - "vfile": "^4.0.0" - } - }, - "unist-util-is": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", - "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==" - }, - "unist-util-stringify-position": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", - "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", - "requires": { - "@types/unist": "^2.0.2" - } - }, - "unorm": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.4.1.tgz", - "integrity": "sha1-NkIA1fE2RsqLzURJAnEzVhR5IwA=" - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "requires": { - "punycode": "^2.1.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" - }, - "v8-compile-cache": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", - "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==" - }, - "validator": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-12.2.0.tgz", - "integrity": "sha512-jJfE/DW6tIK1Ek8nCfNFqt8Wb3nzMoAbocBF6/Icgg1ZFSBpObdnwVY2jQj6qUqzhx5jc71fpvBWyLGO7Xl+nQ==" - }, - "vargs": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/vargs/-/vargs-0.1.0.tgz", - "integrity": "sha1-a2GE2mUgzDIEzhtAfKwm2SYJ6/8=" - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "vfile": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.1.1.tgz", - "integrity": "sha512-lRjkpyDGjVlBA7cDQhQ+gNcvB1BGaTHYuSOcY3S7OhDmBtnzX95FhtZZDecSTDm6aajFymyve6S5DN4ZHGezdQ==", - "requires": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "replace-ext": "1.0.0", - "unist-util-stringify-position": "^2.0.0", - "vfile-message": "^2.0.0" - } - }, - "vfile-message": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", - "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", - "requires": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^2.0.0" - } - }, - "wd": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/wd/-/wd-1.12.1.tgz", - "integrity": "sha512-O99X8OnOgkqfmsPyLIRzG9LmZ+rjmdGFBCyhGpnsSL4MB4xzHoeWmSVcumDiQ5QqPZcwGkszTgeJvjk2VjtiNw==", - "requires": { - "archiver": "^3.0.0", - "async": "^2.0.0", - "lodash": "^4.0.0", - "mkdirp": "^0.5.1", - "q": "^1.5.1", - "request": "2.88.0", - "vargs": "^0.1.0" - }, - "dependencies": { - "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "requires": { - "lodash": "^4.17.14" - } - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - } - } - } - }, - "web-namespaces": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.4.tgz", - "integrity": "sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==" - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "requires": { - "string-width": "^1.0.2 || 2" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "requires": { - "mkdirp": "^0.5.1" - } - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "ws": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz", - "integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==" - }, - "xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - } - }, - "xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" - }, - "xmldom": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.4.0.tgz", - "integrity": "sha512-2E93k08T30Ugs+34HBSTQLVtpi6mCddaY8uO+pMNk1pqSjV5vElzn4mmh6KLxN3hki8rNcHSYzILoh3TEWORvA==" - }, - "xmlhttprequest-ssl": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", - "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" - }, - "xpath.js": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/xpath.js/-/xpath.js-1.1.0.tgz", - "integrity": "sha512-jg+qkfS4K8E7965sqaUl8mRngXiKb3WZGfONgE18pr03FUQiuSV6G+Ej4tS55B+rIQSFEIw3phdVAQ4pPqNWfQ==" - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" - }, - "y18n": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", - "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" - }, - "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "yargs-unparser": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", - "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", - "requires": { - "flat": "^4.1.0", - "lodash": "^4.17.15", - "yargs": "^13.3.0" - } - }, - "yeast": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", - "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" - }, - "z-schema": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-4.2.3.tgz", - "integrity": "sha512-zkvK/9TC6p38IwcrbnT3ul9in1UX4cm1y/VZSs4GHKIiDCrlafc+YQBgQBUdDXLAoZHf2qvQ7gJJOo6yT1LH6A==", - "requires": { - "commander": "^2.7.1", - "lodash.get": "^4.4.2", - "lodash.isequal": "^4.5.0", - "validator": "^12.0.0" - } - }, - "zip-stream": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.1.3.tgz", - "integrity": "sha512-EkXc2JGcKhO5N5aZ7TmuNo45budRaFGHOmz24wtJR7znbNqDPmdZtUauKX6et8KAVseAMBOyWJqEpXcHTBsh7Q==", - "requires": { - "archiver-utils": "^2.1.0", - "compress-commons": "^2.1.1", - "readable-stream": "^3.4.0" - } - } - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "eslint": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.15.0.tgz", - "integrity": "sha512-Vr64xFDT8w30wFll643e7cGrIkPEU50yIiI36OdSIDoSGguIeaLzBo0vpGvzo9RECUqq7htURfwEtKqwytkqzA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@eslint/eslintrc": "^0.2.2", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "enquirer": "^2.3.5", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", - "esquery": "^1.2.0", - "esutils": "^2.0.2", - "file-entry-cache": "^6.0.0", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash": "^4.17.19", - "minimatch": "^3.0.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^5.2.3", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - } - }, - "eslint-config-etherpad": { - "version": "1.0.20", - "resolved": "https://registry.npmjs.org/eslint-config-etherpad/-/eslint-config-etherpad-1.0.20.tgz", - "integrity": "sha512-dDEmWphxOmYe7XC0Uevzb0lK7o1jDBGwYMMCdNeZlgo2EfJljnijPgodlimM4R+4OsnfegEMY6rdWoXjzdd5Rw==", - "dev": true - }, - "eslint-plugin-es": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", - "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", - "dev": true, - "requires": { - "eslint-utils": "^2.0.0", - "regexpp": "^3.0.0" - } - }, - "eslint-plugin-eslint-comments": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz", - "integrity": "sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5", - "ignore": "^5.0.5" - }, - "dependencies": { - "ignore": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", - "dev": true - } - } - }, - "eslint-plugin-mocha": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-8.0.0.tgz", - "integrity": "sha512-n67etbWDz6NQM+HnTwZHyBwz/bLlYPOxUbw7bPuCyFujv7ZpaT/Vn6KTAbT02gf7nRljtYIjWcTxK/n8a57rQQ==", - "dev": true, - "requires": { - "eslint-utils": "^2.1.0", - "ramda": "^0.27.1" - } - }, - "eslint-plugin-node": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", - "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", - "dev": true, - "requires": { - "eslint-plugin-es": "^3.0.0", - "eslint-utils": "^2.0.0", - "ignore": "^5.1.1", - "minimatch": "^3.0.4", - "resolve": "^1.10.1", - "semver": "^6.1.0" - }, - "dependencies": { - "ignore": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "eslint-plugin-prefer-arrow": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-prefer-arrow/-/eslint-plugin-prefer-arrow-1.2.2.tgz", - "integrity": "sha512-C8YMhL+r8RMeMdYAw/rQtE6xNdMulj+zGWud/qIGnlmomiPRaLDGLMeskZ3alN6uMBojmooRimtdrXebLN4svQ==", - "dev": true - }, - "eslint-plugin-promise": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz", - "integrity": "sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==", - "dev": true - }, - "eslint-plugin-you-dont-need-lodash-underscore": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-you-dont-need-lodash-underscore/-/eslint-plugin-you-dont-need-lodash-underscore-6.10.0.tgz", - "integrity": "sha512-Zu1KbHiWKf+alVvT+kFX2M5HW1gmtnkfF1l2cjmFozMnG0gbGgXo8oqK7lwk+ygeOXDmVfOyijqBd7SUub9AEQ==", - "dev": true, - "requires": { - "kebab-case": "^1.0.0" - } - }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - } - } - }, - "eslint-visitor-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", - "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", - "dev": true - }, - "espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", - "dev": true, - "requires": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - } - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "esquery": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", - "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "file-entry-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz", - "integrity": "sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA==", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.0.tgz", - "integrity": "sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", - "dev": true, - "requires": { - "type-fest": "^0.8.1" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "import-fresh": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.2.tgz", - "integrity": "sha512-cTPNrlvJT6twpYy+YmKUKrTSjWFs3bjYjAhCwm+z4EOCubZxAuO+hHpRN64TqjEaYSHs7tJAE0w1CKMGmsG/lw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "is-core-module": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.1.0.tgz", - "integrity": "sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "kebab-case": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.0.tgz", - "integrity": "sha1-P55JkK3K0MaGwOcB92RYaPdfkes=", - "dev": true - }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "ramda": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz", - "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==", - "dev": true - }, - "regexpp": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", - "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", - "dev": true - }, - "resolve": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", - "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", - "dev": true, - "requires": { - "is-core-module": "^2.1.0", - "path-parse": "^1.0.6" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", - "dev": true, - "requires": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - }, - "uri-js": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", - "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "v8-compile-cache": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", - "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 1704f8816..000000000 --- a/package.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "dependencies": { - "ep_etherpad-lite": "file:src" - }, - "devDependencies": { - "eslint": "^7.15.0", - "eslint-config-etherpad": "^1.0.20", - "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-mocha": "^8.0.0", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-prefer-arrow": "^1.2.2", - "eslint-plugin-promise": "^4.2.1", - "eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0" - }, - "eslintConfig": { - "ignorePatterns": [ - "/src/", - "/tests/frontend/lib/" - ], - "overrides": [ - { - "files": [ - "**/.eslintrc.js" - ], - "extends": "etherpad/node" - }, - { - "files": [ - "**/*" - ], - "excludedFiles": [ - "**/.eslintrc.js", - "tests/frontend/**/*" - ], - "extends": "etherpad/node" - }, - { - "files": [ - "tests/**/*" - ], - "excludedFiles": [ - "**/.eslintrc.js" - ], - "extends": "etherpad/tests", - "rules": { - "mocha/no-exports": "off", - "mocha/no-top-level-hooks": "off" - } - }, - { - "files": [ - "tests/backend/**/*" - ], - "excludedFiles": [ - "**/.eslintrc.js" - ], - "extends": "etherpad/tests/backend", - "overrides": [ - { - "files": [ - "tests/backend/**/*" - ], - "excludedFiles": [ - "tests/backend/specs/**/*" - ], - "rules": { - "mocha/no-exports": "off", - "mocha/no-top-level-hooks": "off" - } - } - ] - }, - { - "files": [ - "tests/frontend/**/*" - ], - "excludedFiles": [ - "**/.eslintrc.js" - ], - "extends": "etherpad/tests/frontend", - "overrides": [ - { - "files": [ - "tests/frontend/**/*" - ], - "excludedFiles": [ - "tests/frontend/specs/**/*" - ], - "rules": { - "mocha/no-exports": "off", - "mocha/no-top-level-hooks": "off" - } - } - ] - } - ], - "root": true - }, - "scripts": { - "lint": "eslint ." - }, - "engines": { - "node": ">=10.13.0" - } -} diff --git a/settings.json.docker b/settings.json.docker index 081af38b8..7bb2917c2 100644 --- a/settings.json.docker +++ b/settings.json.docker @@ -171,7 +171,7 @@ * * * Database specific settings are dependent on dbType, and go in dbSettings. - * Remember that since Etherpad 1.6.0 you can also store these informations in + * Remember that since Etherpad 1.6.0 you can also store this information in * credentials.json. * * For a complete list of the supported drivers, please refer to: @@ -445,6 +445,17 @@ */ "socketTransportProtocols" : ["xhr-polling", "jsonp-polling", "htmlfile"], + "socketIo": { + /* + * Maximum permitted client message size (in bytes). All messages from + * clients that are larger than this will be rejected. Large values make it + * possible to paste large amounts of text, and plugins may require a larger + * value to work properly, but increasing the value increases susceptibility + * to denial of service attacks (malicious clients can exhaust memory). + */ + "maxHttpBufferSize": 10000 + }, + /* * Allow Load Testing tools to hit the Etherpad Instance. * @@ -486,6 +497,22 @@ */ "importMaxFileSize": "${IMPORT_MAX_FILE_SIZE:52428800}", // 50 * 1024 * 1024 + /* + * From Etherpad 1.8.5 onwards, when Etherpad is in production mode commits from individual users are rate limited + * + * The default is to allow at most 10 changes per IP in a 1 second window. + * After that the change is rejected. + * + * See https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#websocket-single-connection-prevent-flooding for more options + */ + "commitRateLimiting": { + // duration of the rate limit window (seconds) + "duration": "${COMMIT_RATE_LIMIT_DURATION:1}", + + // maximum number of changes per IP to allow during the rate limit window + "points": "${COMMIT_RATE_LIMIT_POINTS:10}" + }, + /* * Toolbar buttons configuration. * diff --git a/settings.json.template b/settings.json.template index f4cfdea62..b8722fbdc 100644 --- a/settings.json.template +++ b/settings.json.template @@ -162,7 +162,7 @@ * * * Database specific settings are dependent on dbType, and go in dbSettings. - * Remember that since Etherpad 1.6.0 you can also store these informations in + * Remember that since Etherpad 1.6.0 you can also store this information in * credentials.json. * * For a complete list of the supported drivers, please refer to: @@ -450,6 +450,17 @@ */ "socketTransportProtocols" : ["xhr-polling", "jsonp-polling", "htmlfile"], + "socketIo": { + /* + * Maximum permitted client message size (in bytes). All messages from + * clients that are larger than this will be rejected. Large values make it + * possible to paste large amounts of text, and plugins may require a larger + * value to work properly, but increasing the value increases susceptibility + * to denial of service attacks (malicious clients can exhaust memory). + */ + "maxHttpBufferSize": 10000 + }, + /* * Allow Load Testing tools to hit the Etherpad Instance. * @@ -492,7 +503,7 @@ "importMaxFileSize": 52428800, // 50 * 1024 * 1024 /* - * From Etherpad 1.9.0 onwards, when Etherpad is in production mode commits from individual users are rate limited + * From Etherpad 1.8.5 onwards, when Etherpad is in production mode commits from individual users are rate limited * * The default is to allow at most 10 changes per IP in a 1 second window. * After that the change is rejected. @@ -503,7 +514,7 @@ // duration of the rate limit window (seconds) "duration": 1, - // maximum number of chanes per IP to allow during the rate limit window + // maximum number of changes per IP to allow during the rate limit window "points": 10 }, @@ -600,5 +611,8 @@ }, // logconfig /* Override any strings found in locale directories */ - "customLocaleStrings": {} + "customLocaleStrings": {}, + + /* Disable Admin UI tests */ + "enableAdminUITests": false } diff --git a/bin/buildDebian.sh b/src/bin/buildDebian.sh similarity index 96% rename from bin/buildDebian.sh rename to src/bin/buildDebian.sh index f1f5675ec..241b3f751 100755 --- a/bin/buildDebian.sh +++ b/src/bin/buildDebian.sh @@ -14,7 +14,7 @@ rm -rf ${DIST} mkdir -p ${DIST}/ rm -rf ${SRC} -rsync -a bin/deb-src/ ${SRC}/ +rsync -a src/bin/deb-src/ ${SRC}/ mkdir -p ${SYSROOT}/opt/ rsync --exclude '.git' -a . ${SYSROOT}/opt/etherpad/ --delete diff --git a/bin/buildForWindows.sh b/src/bin/buildForWindows.sh similarity index 97% rename from bin/buildForWindows.sh rename to src/bin/buildForWindows.sh index 818522ad0..d62ef810e 100755 --- a/bin/buildForWindows.sh +++ b/src/bin/buildForWindows.sh @@ -32,7 +32,7 @@ rm -f etherpad-lite-win.zip export NODE_ENV=production log "do a normal unix install first..." -bin/installDeps.sh || exit 1 +src/bin/installDeps.sh || exit 1 log "copy the windows settings template..." cp settings.json.template settings.json @@ -43,8 +43,7 @@ rm -rf node_modules mv node_modules_resolved node_modules log "download windows node..." -cd bin -wget "https://nodejs.org/dist/latest-erbium/win-x86/node.exe" -O ../node.exe +wget "https://nodejs.org/dist/latest-erbium/win-x86/node.exe" -O node.exe log "remove git history to reduce folder size" rm -rf .git/objects diff --git a/src/bin/checkAllPads.js b/src/bin/checkAllPads.js new file mode 100644 index 000000000..d15e5ec5b --- /dev/null +++ b/src/bin/checkAllPads.js @@ -0,0 +1,88 @@ +'use strict'; +/* + * This is a debug tool. It checks all revisions for data corruption + */ + +// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an +// unhandled rejection into an uncaught exception, which does cause Node.js to exit. +process.on('unhandledRejection', (err) => { throw err; }); + +if (process.argv.length !== 2) throw new Error('Use: node src/bin/checkAllPads.js'); + +(async () => { + // initialize the database + require('../node/utils/Settings'); + const db = require('../node/db/DB'); + await db.init(); + + // load modules + const Changeset = require('../static/js/Changeset'); + const padManager = require('../node/db/PadManager'); + + let revTestedCount = 0; + + // get all pads + const res = await padManager.listAllPads(); + for (const padId of res.padIDs) { + const pad = await padManager.getPad(padId); + + // check if the pad has a pool + if (pad.pool == null) { + console.error(`[${pad.id}] Missing attribute pool`); + continue; + } + // create an array with key kevisions + // key revisions always save the full pad atext + const head = pad.getHeadRevisionNumber(); + const keyRevisions = []; + for (let rev = 0; rev < head; rev += 100) { + keyRevisions.push(rev); + } + + // run through all key revisions + for (const keyRev of keyRevisions) { + // create an array of revisions we need till the next keyRevision or the End + const revisionsNeeded = []; + for (let rev = keyRev; rev <= keyRev + 100 && rev <= head; rev++) { + revisionsNeeded.push(rev); + } + + // this array will hold all revision changesets + const revisions = []; + + // run through all needed revisions and get them from the database + for (const revNum of revisionsNeeded) { + const revision = await db.get(`pad:${pad.id}:revs:${revNum}`); + revisions[revNum] = revision; + } + + // check if the revision exists + if (revisions[keyRev] == null) { + console.error(`[${pad.id}] Missing revision ${keyRev}`); + continue; + } + + // check if there is a atext in the keyRevisions + let {meta: {atext} = {}} = revisions[keyRev]; + if (atext == null) { + console.error(`[${pad.id}] Missing atext in revision ${keyRev}`); + continue; + } + + const apool = pad.pool; + for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) { + try { + const cs = revisions[rev].changeset; + atext = Changeset.applyToAText(cs, atext, apool); + revTestedCount++; + } catch (e) { + console.error(`[${pad.id}] Bad changeset at revision ${rev} - ${e.message}`); + } + } + } + } + if (revTestedCount === 0) { + throw new Error('No revisions tested'); + } + console.log(`Finished: Tested ${revTestedCount} revisions`); +})(); diff --git a/src/bin/checkPad.js b/src/bin/checkPad.js new file mode 100644 index 000000000..5b17fa31a --- /dev/null +++ b/src/bin/checkPad.js @@ -0,0 +1,82 @@ +'use strict'; +/* + * This is a debug tool. It checks all revisions for data corruption + */ + +// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an +// unhandled rejection into an uncaught exception, which does cause Node.js to exit. +process.on('unhandledRejection', (err) => { throw err; }); + +if (process.argv.length !== 3) throw new Error('Use: node src/bin/checkPad.js $PADID'); + +// get the padID +const padId = process.argv[2]; +let checkRevisionCount = 0; + +(async () => { + // initialize database + require('../node/utils/Settings'); + const db = require('../node/db/DB'); + await db.init(); + + // load modules + const Changeset = require('../static/js/Changeset'); + const padManager = require('../node/db/PadManager'); + + const exists = await padManager.doesPadExists(padId); + if (!exists) throw new Error('Pad does not exist'); + + // get the pad + const pad = await padManager.getPad(padId); + + // create an array with key revisions + // key revisions always save the full pad atext + const head = pad.getHeadRevisionNumber(); + const keyRevisions = []; + for (let rev = 0; rev < head; rev += 100) { + keyRevisions.push(rev); + } + + // run through all key revisions + for (let keyRev of keyRevisions) { + keyRev = parseInt(keyRev); + // create an array of revisions we need till the next keyRevision or the End + const revisionsNeeded = []; + for (let rev = keyRev; rev <= keyRev + 100 && rev <= head; rev++) { + revisionsNeeded.push(rev); + } + + // this array will hold all revision changesets + const revisions = []; + + // run through all needed revisions and get them from the database + for (const revNum of revisionsNeeded) { + const revision = await db.get(`pad:${padId}:revs:${revNum}`); + revisions[revNum] = revision; + } + + // check if the pad has a pool + if (pad.pool == null) throw new Error('Attribute pool is missing'); + + // check if there is an atext in the keyRevisions + let {meta: {atext} = {}} = revisions[keyRev] || {}; + if (atext == null) { + console.error(`No atext in key revision ${keyRev}`); + continue; + } + + const apool = pad.pool; + + for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) { + checkRevisionCount++; + try { + const cs = revisions[rev].changeset; + atext = Changeset.applyToAText(cs, atext, apool); + } catch (e) { + console.error(`Bad changeset at revision ${rev} - ${e.message}`); + continue; + } + } + console.log(`Finished: Checked ${checkRevisionCount} revisions`); + } +})(); diff --git a/src/bin/checkPadDeltas.js b/src/bin/checkPadDeltas.js new file mode 100644 index 000000000..852c68332 --- /dev/null +++ b/src/bin/checkPadDeltas.js @@ -0,0 +1,103 @@ +'use strict'; +/* + * This is a debug tool. It checks all revisions for data corruption + */ + +// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an +// unhandled rejection into an uncaught exception, which does cause Node.js to exit. +process.on('unhandledRejection', (err) => { throw err; }); + +if (process.argv.length !== 3) throw new Error('Use: node src/bin/checkPadDeltas.js $PADID'); + +// get the padID +const padId = process.argv[2]; + +const expect = require('../tests/frontend/lib/expect'); +const diff = require('diff'); + +(async () => { + // initialize database + require('../node/utils/Settings'); + const db = require('../node/db/DB'); + await db.init(); + + // load modules + const Changeset = require('../static/js/Changeset'); + const padManager = require('../node/db/PadManager'); + + const exists = await padManager.doesPadExists(padId); + if (!exists) throw new Error('Pad does not exist'); + + // get the pad + const pad = await padManager.getPad(padId); + + // create an array with key revisions + // key revisions always save the full pad atext + const head = pad.getHeadRevisionNumber(); + const keyRevisions = []; + for (let i = 0; i < head; i += 100) { + keyRevisions.push(i); + } + + // create an array with all revisions + const revisions = []; + for (let i = 0; i <= head; i++) { + revisions.push(i); + } + + let atext = Changeset.makeAText('\n'); + + // run through all revisions + for (const revNum of revisions) { + // console.log('Fetching', revNum) + const revision = await db.get(`pad:${padId}:revs:${revNum}`); + // check if there is a atext in the keyRevisions + const {meta: {atext: revAtext} = {}} = revision || {}; + if (~keyRevisions.indexOf(revNum) && revAtext == null) { + console.error(`No atext in key revision ${revNum}`); + continue; + } + + // try glue everything together + try { + // console.log("check revision ", revNum); + const cs = revision.changeset; + atext = Changeset.applyToAText(cs, atext, pad.pool); + } catch (e) { + console.error(`Bad changeset at revision ${revNum} - ${e.message}`); + continue; + } + + // check things are working properly + if (~keyRevisions.indexOf(revNum)) { + try { + expect(revision.meta.atext.text).to.eql(atext.text); + expect(revision.meta.atext.attribs).to.eql(atext.attribs); + } catch (e) { + console.error(`Atext in key revision ${revNum} doesn't match computed one.`); + console.log(diff.diffChars(atext.text, revision.meta.atext.text).map((op) => { + if (!op.added && !op.removed) op.value = op.value.length; + return op; + })); + // console.error(e) + // console.log('KeyRev. :', revision.meta.atext) + // console.log('Computed:', atext) + continue; + } + } + } + + // check final text is right... + if (pad.atext.text === atext.text) { + console.log('ok'); + } else { + console.error('Pad AText doesn\'t match computed one! (Computed ', + atext.text.length, ', db', pad.atext.text.length, ')'); + console.log(diff.diffChars(atext.text, pad.atext.text).map((op) => { + if (!op.added && !op.removed) { + op.value = op.value.length; + return op; + } + })); + } +})(); diff --git a/bin/cleanRun.sh b/src/bin/cleanRun.sh similarity index 73% rename from bin/cleanRun.sh rename to src/bin/cleanRun.sh index 57de27e5c..e8f4bd0d4 100755 --- a/bin/cleanRun.sh +++ b/src/bin/cleanRun.sh @@ -1,15 +1,11 @@ #!/bin/sh -# Move to the folder where ep-lite is installed -cd "$(dirname "$0")"/.. +# Move to the Etherpad base directory. +MY_DIR=$(cd "${0%/*}" && pwd -P) || exit 1 +cd "${MY_DIR}/../.." || exit 1 -# Source constants and usefull functions -. bin/functions.sh - -#Was this script started in the bin folder? if yes move out -if [ -d "../bin" ]; then - cd "../" -fi +# Source constants and useful functions +. src/bin/functions.sh ignoreRoot=0 for ARG in "$@" @@ -35,7 +31,7 @@ fi rm -rf src/node_modules #Prepare the environment -bin/installDeps.sh "$@" || exit 1 +src/bin/installDeps.sh "$@" || exit 1 #Move to the node folder and start echo "Started Etherpad..." diff --git a/bin/convertSettings.json.template b/src/bin/convertSettings.json.template similarity index 100% rename from bin/convertSettings.json.template rename to src/bin/convertSettings.json.template diff --git a/bin/createRelease.sh b/src/bin/createRelease.sh similarity index 99% rename from bin/createRelease.sh rename to src/bin/createRelease.sh index 6768702f3..531a21502 100755 --- a/bin/createRelease.sh +++ b/src/bin/createRelease.sh @@ -134,7 +134,7 @@ function create_builds { git clone $ETHER_WEB_REPO echo "Creating windows build..." cd etherpad-lite - bin/buildForWindows.sh + src/bin/buildForWindows.sh [[ $? != 0 ]] && echo "Aborting: Error creating build for windows" && exit 1 echo "Creating docs..." make docs diff --git a/bin/createUserSession.js b/src/bin/createUserSession.js similarity index 80% rename from bin/createUserSession.js rename to src/bin/createUserSession.js index 324941ec8..33dcac18e 100644 --- a/bin/createUserSession.js +++ b/src/bin/createUserSession.js @@ -1,20 +1,24 @@ +'use strict'; + /* * A tool for generating a test user session which can be used for debugging configs * that require sessions. */ -const m = (f) => `${__dirname}/../${f}`; + +// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an +// unhandled rejection into an uncaught exception, which does cause Node.js to exit. +process.on('unhandledRejection', (err) => { throw err; }); const fs = require('fs'); const path = require('path'); const querystring = require('querystring'); -const request = require(m('src/node_modules/request')); -const settings = require(m('src/node/utils/Settings')); -const supertest = require(m('src/node_modules/supertest')); +const settings = require('../node/utils/Settings'); +const supertest = require('supertest'); (async () => { const api = supertest(`http://${settings.ip}:${settings.port}`); - const filePath = path.join(__dirname, '../APIKEY.txt'); + const filePath = path.join(__dirname, '../../APIKEY.txt'); const apikey = fs.readFileSync(filePath, {encoding: 'utf-8'}); let res; diff --git a/bin/deb-src/DEBIAN/control b/src/bin/deb-src/DEBIAN/control similarity index 100% rename from bin/deb-src/DEBIAN/control rename to src/bin/deb-src/DEBIAN/control diff --git a/bin/deb-src/DEBIAN/postinst b/src/bin/deb-src/DEBIAN/postinst similarity index 100% rename from bin/deb-src/DEBIAN/postinst rename to src/bin/deb-src/DEBIAN/postinst diff --git a/bin/deb-src/DEBIAN/preinst b/src/bin/deb-src/DEBIAN/preinst similarity index 100% rename from bin/deb-src/DEBIAN/preinst rename to src/bin/deb-src/DEBIAN/preinst diff --git a/bin/deb-src/DEBIAN/prerm b/src/bin/deb-src/DEBIAN/prerm similarity index 100% rename from bin/deb-src/DEBIAN/prerm rename to src/bin/deb-src/DEBIAN/prerm diff --git a/bin/deb-src/sysroot/etc/init/etherpad.conf b/src/bin/deb-src/sysroot/etc/init/etherpad.conf similarity index 90% rename from bin/deb-src/sysroot/etc/init/etherpad.conf rename to src/bin/deb-src/sysroot/etc/init/etherpad.conf index cd6f4541c..aab40bca8 100644 --- a/bin/deb-src/sysroot/etc/init/etherpad.conf +++ b/src/bin/deb-src/sysroot/etc/init/etherpad.conf @@ -15,7 +15,7 @@ pre-start script chown $EPUSER $EPLOGS ||true chmod 0755 $EPLOGS ||true chown -R $EPUSER $EPHOME/var ||true - $EPHOME/bin/installDeps.sh >> $EPLOGS/error.log || { stop; exit 1; } + $EPHOME/src/bin/installDeps.sh >> $EPLOGS/error.log || { stop; exit 1; } end script script diff --git a/bin/debugRun.sh b/src/bin/debugRun.sh similarity index 71% rename from bin/debugRun.sh rename to src/bin/debugRun.sh index 9b2fff9bd..f418f4f64 100755 --- a/bin/debugRun.sh +++ b/src/bin/debugRun.sh @@ -1,13 +1,14 @@ #!/bin/sh -# Move to the folder where ep-lite is installed -cd "$(dirname "$0")"/.. +# Move to the Etherpad base directory. +MY_DIR=$(cd "${0%/*}" && pwd -P) || exit 1 +cd "${MY_DIR}/../.." || exit 1 -# Source constants and usefull functions -. bin/functions.sh +# Source constants and useful functions +. src/bin/functions.sh # Prepare the environment -bin/installDeps.sh || exit 1 +src/bin/installDeps.sh || exit 1 echo "If you are new to debugging Node.js with Chrome DevTools, take a look at this page:" echo "https://medium.com/@paul_irish/debugging-node-js-nightlies-with-chrome-devtools-7c4a1b95ae27" diff --git a/src/bin/deleteAllGroupSessions.js b/src/bin/deleteAllGroupSessions.js new file mode 100644 index 000000000..c95bf1075 --- /dev/null +++ b/src/bin/deleteAllGroupSessions.js @@ -0,0 +1,47 @@ +'use strict'; + +/* +* A tool for deleting ALL GROUP sessions Etherpad user sessions from the CLI, +* because sometimes a brick is required to fix a face. +*/ + +// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an +// unhandled rejection into an uncaught exception, which does cause Node.js to exit. +process.on('unhandledRejection', (err) => { throw err; }); + +const path = require('path'); +const fs = require('fs'); +const supertest = require('supertest'); + +// Set a delete counter which will increment on each delete attempt +// TODO: Check delete is successful before incrementing +let deleteCount = 0; + +// get the API Key +const filePath = path.join(__dirname, '../../APIKEY.txt'); +console.log('Deleting all group sessions, please be patient.'); + +(async () => { + const settings = require('../tests/container/loadSettings').loadSettings(); + const apikey = fs.readFileSync(filePath, {encoding: 'utf-8'}); + const api = supertest(`http://${settings.ip}:${settings.port}`); + + const apiVersionResponse = await api.get('/api/'); + const apiVersion = apiVersionResponse.body.currentVersion; // 1.12.5 + + const groupsResponse = await api.get(`/api/${apiVersion}/listAllGroups?apikey=${apikey}`); + const groups = groupsResponse.body.data.groupIDs; // ['whateverGroupID'] + + for (const groupID of groups) { + const sessionURI = `/api/${apiVersion}/listSessionsOfGroup?apikey=${apikey}&groupID=${groupID}`; + const sessionsResponse = await api.get(sessionURI); + const sessions = sessionsResponse.body.data; + + for (const sessionID of Object.keys(sessions)) { + const deleteURI = `/api/${apiVersion}/deleteSession?apikey=${apikey}&sessionID=${sessionID}`; + await api.post(deleteURI); // delete + deleteCount++; + } + } + console.log(`Deleted ${deleteCount} sessions`); +})(); diff --git a/src/bin/deletePad.js b/src/bin/deletePad.js new file mode 100644 index 000000000..51ea99639 --- /dev/null +++ b/src/bin/deletePad.js @@ -0,0 +1,38 @@ +'use strict'; + +/* + * A tool for deleting pads from the CLI, because sometimes a brick is required + * to fix a window. + */ + +// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an +// unhandled rejection into an uncaught exception, which does cause Node.js to exit. +process.on('unhandledRejection', (err) => { throw err; }); + +const settings = require('../tests/container/loadSettings').loadSettings(); +const path = require('path'); +const fs = require('fs'); +const supertest = require('supertest'); + +const api = supertest(`http://${settings.ip}:${settings.port}`); + +if (process.argv.length !== 3) throw new Error('Use: node deletePad.js $PADID'); + +// get the padID +const padId = process.argv[2]; + +// get the API Key +const filePath = path.join(__dirname, '../../APIKEY.txt'); +const apikey = fs.readFileSync(filePath, {encoding: 'utf-8'}); + +(async () => { + let apiVersion = await api.get('/api/'); + apiVersion = apiVersion.body.currentVersion; + if (!apiVersion) throw new Error('No version set in API'); + + // Now we know the latest API version, let's delete pad + const uri = `/api/${apiVersion}/deletePad?apikey=${apikey}&padID=${padId}`; + const deleteAttempt = await api.post(uri); + if (deleteAttempt.body.code === 1) throw new Error(`Error deleting pad ${deleteAttempt.body}`); + console.log('Deleted pad', deleteAttempt.body); +})(); diff --git a/bin/dirty-db-cleaner.py b/src/bin/dirty-db-cleaner.py similarity index 100% rename from bin/dirty-db-cleaner.py rename to src/bin/dirty-db-cleaner.py diff --git a/bin/doc/LICENSE b/src/bin/doc/LICENSE similarity index 100% rename from bin/doc/LICENSE rename to src/bin/doc/LICENSE diff --git a/bin/doc/README.md b/src/bin/doc/README.md similarity index 94% rename from bin/doc/README.md rename to src/bin/doc/README.md index 4646c2004..19a137fd7 100644 --- a/bin/doc/README.md +++ b/src/bin/doc/README.md @@ -72,5 +72,5 @@ Each type of heading has a description block. Run the following from the etherpad-lite root directory: ```sh -$ node bin/doc/generate doc/index.md --format=html --template=doc/template.html > out.html -``` \ No newline at end of file +$ node src/bin/doc/generate doc/index.md --format=html --template=doc/template.html > out.html +``` diff --git a/bin/doc/generate.js b/src/bin/doc/generate.js similarity index 86% rename from bin/doc/generate.js rename to src/bin/doc/generate.js index 803f5017e..d04468a8b 100644 --- a/bin/doc/generate.js +++ b/src/bin/doc/generate.js @@ -1,4 +1,7 @@ #!/usr/bin/env node + +'use strict'; + // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a @@ -20,7 +23,6 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -const marked = require('marked'); const fs = require('fs'); const path = require('path'); @@ -33,12 +35,12 @@ let template = null; let inputFile = null; args.forEach((arg) => { - if (!arg.match(/^\-\-/)) { + if (!arg.match(/^--/)) { inputFile = arg; - } else if (arg.match(/^\-\-format=/)) { - format = arg.replace(/^\-\-format=/, ''); - } else if (arg.match(/^\-\-template=/)) { - template = arg.replace(/^\-\-template=/, ''); + } else if (arg.match(/^--format=/)) { + format = arg.replace(/^--format=/, ''); + } else if (arg.match(/^--template=/)) { + template = arg.replace(/^--template=/, ''); } }); @@ -56,11 +58,11 @@ fs.readFile(inputFile, 'utf8', (er, input) => { }); -const includeExpr = /^@include\s+([A-Za-z0-9-_\/]+)(?:\.)?([a-zA-Z]*)$/gmi; +const includeExpr = /^@include\s+([A-Za-z0-9-_/]+)(?:\.)?([a-zA-Z]*)$/gmi; const includeData = {}; -function processIncludes(inputFile, input, cb) { +const processIncludes = (inputFile, input, cb) => { const includes = input.match(includeExpr); - if (includes === null) return cb(null, input); + if (includes == null) return cb(null, input); let errState = null; console.error(includes); let incCount = includes.length; @@ -70,7 +72,7 @@ function processIncludes(inputFile, input, cb) { let fname = include.replace(/^@include\s+/, ''); if (!fname.match(/\.md$/)) fname += '.md'; - if (includeData.hasOwnProperty(fname)) { + if (Object.prototype.hasOwnProperty.call(includeData, fname)) { input = input.split(include).join(includeData[fname]); incCount--; if (incCount === 0) { @@ -94,10 +96,10 @@ function processIncludes(inputFile, input, cb) { }); }); }); -} +}; -function next(er, input) { +const next = (er, input) => { if (er) throw er; switch (format) { case 'json': @@ -117,4 +119,4 @@ function next(er, input) { default: throw new Error(`Invalid format: ${format}`); } -} +}; diff --git a/bin/doc/html.js b/src/bin/doc/html.js similarity index 89% rename from bin/doc/html.js rename to src/bin/doc/html.js index 26cf3f185..2c38aec23 100644 --- a/bin/doc/html.js +++ b/src/bin/doc/html.js @@ -1,3 +1,5 @@ +'use strict'; + // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a @@ -23,17 +25,17 @@ const fs = require('fs'); const marked = require('marked'); const path = require('path'); -module.exports = toHTML; -function toHTML(input, filename, template, cb) { +const toHTML = (input, filename, template, cb) => { const lexed = marked.lexer(input); fs.readFile(template, 'utf8', (er, template) => { if (er) return cb(er); render(lexed, filename, template, cb); }); -} +}; +module.exports = toHTML; -function render(lexed, filename, template, cb) { +const render = (lexed, filename, template, cb) => { // get the section const section = getSection(lexed); @@ -52,23 +54,23 @@ function render(lexed, filename, template, cb) { // content has to be the last thing we do with // the lexed tokens, because it's destructive. - content = marked.parser(lexed); + const content = marked.parser(lexed); template = template.replace(/__CONTENT__/g, content); cb(null, template); }); -} +}; // just update the list item text in-place. // lists that come right after a heading are what we're after. -function parseLists(input) { +const parseLists = (input) => { let state = null; let depth = 0; const output = []; output.links = input.links; input.forEach((tok) => { - if (state === null) { + if (state == null) { if (tok.type === 'heading') { state = 'AFTERHEADING'; } @@ -112,29 +114,27 @@ function parseLists(input) { }); return output; -} +}; -function parseListItem(text) { - text = text.replace(/\{([^\}]+)\}/, '$1'); +const parseListItem = (text) => { + text = text.replace(/\{([^}]+)\}/, '$1'); // XXX maybe put more stuff here? return text; -} +}; // section is just the first heading -function getSection(lexed) { - const section = ''; +const getSection = (lexed) => { for (let i = 0, l = lexed.length; i < l; i++) { const tok = lexed[i]; if (tok.type === 'heading') return tok.text; } return ''; -} +}; -function buildToc(lexed, filename, cb) { - const indent = 0; +const buildToc = (lexed, filename, cb) => { let toc = []; let depth = 0; lexed.forEach((tok) => { @@ -155,18 +155,18 @@ function buildToc(lexed, filename, cb) { toc = marked.parse(toc.join('\n')); cb(null, toc); -} +}; const idCounters = {}; -function getId(text) { +const getId = (text) => { text = text.toLowerCase(); text = text.replace(/[^a-z0-9]+/g, '_'); text = text.replace(/^_+|_+$/, ''); text = text.replace(/^([^a-z])/, '_$1'); - if (idCounters.hasOwnProperty(text)) { + if (Object.prototype.hasOwnProperty.call(idCounters, text)) { text += `_${++idCounters[text]}`; } else { idCounters[text] = 0; } return text; -} +}; diff --git a/bin/doc/json.js b/src/bin/doc/json.js similarity index 92% rename from bin/doc/json.js rename to src/bin/doc/json.js index 3ce62a301..1a5ecb1d8 100644 --- a/bin/doc/json.js +++ b/src/bin/doc/json.js @@ -1,3 +1,4 @@ +'use strict'; // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a @@ -26,7 +27,7 @@ module.exports = doJSON; const marked = require('marked'); -function doJSON(input, filename, cb) { +const doJSON = (input, filename, cb) => { const root = {source: filename}; const stack = [root]; let depth = 0; @@ -40,7 +41,7 @@ function doJSON(input, filename, cb) { // // This is for cases where the markdown semantic structure is lacking. if (type === 'paragraph' || type === 'html') { - const metaExpr = /\n*/g; + const metaExpr = /\n*/g; text = text.replace(metaExpr, (_0, k, v) => { current[k.trim()] = v.trim(); return ''; @@ -146,7 +147,7 @@ function doJSON(input, filename, cb) { } return cb(null, root); -} +}; // go from something like this: @@ -191,7 +192,7 @@ function doJSON(input, filename, cb) { // desc: 'whether or not to send output to parent\'s stdio.', // default: 'false' } ] } ] -function processList(section) { +const processList = (section) => { const list = section.list; const values = []; let current; @@ -203,13 +204,13 @@ function processList(section) { if (type === 'space') return; if (type === 'list_item_start') { if (!current) { - var n = {}; + const n = {}; values.push(n); current = n; } else { current.options = current.options || []; stack.push(current); - var n = {}; + const n = {}; current.options.push(n); current = n; } @@ -247,11 +248,11 @@ function processList(section) { switch (section.type) { case 'ctor': case 'classMethod': - case 'method': + case 'method': { // each item is an argument, unless the name is 'return', // in which case it's the return value. section.signatures = section.signatures || []; - var sig = {}; + const sig = {}; section.signatures.push(sig); sig.params = values.filter((v) => { if (v.name === 'return') { @@ -262,11 +263,11 @@ function processList(section) { }); parseSignature(section.textRaw, sig); break; - - case 'property': + } + case 'property': { // there should be only one item, which is the value. // copy the data up to the section. - var value = values[0] || {}; + const value = values[0] || {}; delete value.name; section.typeof = value.type; delete value.type; @@ -274,20 +275,21 @@ function processList(section) { section[k] = value[k]; }); break; + } - case 'event': + case 'event': { // event: each item is an argument. section.params = values; break; + } } - // section.listParsed = values; delete section.list; -} +}; // textRaw = "someobject.someMethod(a, [b=100], [c])" -function parseSignature(text, sig) { +const parseSignature = (text, sig) => { let params = text.match(paramExpr); if (!params) return; params = params[1]; @@ -322,10 +324,10 @@ function parseSignature(text, sig) { if (optional) param.optional = true; if (def !== undefined) param.default = def; }); -} +}; -function parseListItem(item) { +const parseListItem = (item) => { if (item.options) item.options.forEach(parseListItem); if (!item.textRaw) return; @@ -341,7 +343,7 @@ function parseListItem(item) { item.name = 'return'; text = text.replace(retExpr, ''); } else { - const nameExpr = /^['`"]?([^'`": \{]+)['`"]?\s*:?\s*/; + const nameExpr = /^['`"]?([^'`": {]+)['`"]?\s*:?\s*/; const name = text.match(nameExpr); if (name) { item.name = name[1]; @@ -358,7 +360,7 @@ function parseListItem(item) { } text = text.trim(); - const typeExpr = /^\{([^\}]+)\}/; + const typeExpr = /^\{([^}]+)\}/; const type = text.match(typeExpr); if (type) { item.type = type[1]; @@ -376,10 +378,10 @@ function parseListItem(item) { text = text.replace(/^\s*-\s*/, ''); text = text.trim(); if (text) item.desc = text; -} +}; -function finishSection(section, parent) { +const finishSection = (section, parent) => { if (!section || !parent) { throw new Error(`Invalid finishSection call\n${ JSON.stringify(section)}\n${ @@ -416,7 +418,7 @@ function finishSection(section, parent) { ctor.signatures.forEach((sig) => { sig.desc = ctor.desc; }); - sigs.push.apply(sigs, ctor.signatures); + sigs.push(...ctor.signatures); }); delete section.ctors; } @@ -479,50 +481,50 @@ function finishSection(section, parent) { parent[plur] = parent[plur] || []; parent[plur].push(section); -} +}; // Not a general purpose deep copy. // But sufficient for these basic things. -function deepCopy(src, dest) { - Object.keys(src).filter((k) => !dest.hasOwnProperty(k)).forEach((k) => { +const deepCopy = (src, dest) => { + Object.keys(src).filter((k) => !Object.prototype.hasOwnProperty.call(dest, k)).forEach((k) => { dest[k] = deepCopy_(src[k]); }); -} +}; -function deepCopy_(src) { +const deepCopy_ = (src) => { if (!src) return src; if (Array.isArray(src)) { - var c = new Array(src.length); + const c = new Array(src.length); src.forEach((v, i) => { c[i] = deepCopy_(v); }); return c; } if (typeof src === 'object') { - var c = {}; + const c = {}; Object.keys(src).forEach((k) => { c[k] = deepCopy_(src[k]); }); return c; } return src; -} +}; // these parse out the contents of an H# tag const eventExpr = /^Event(?::|\s)+['"]?([^"']+).*$/i; const classExpr = /^Class:\s*([^ ]+).*?$/i; -const propExpr = /^(?:property:?\s*)?[^\.]+\.([^ \.\(\)]+)\s*?$/i; -const braceExpr = /^(?:property:?\s*)?[^\.\[]+(\[[^\]]+\])\s*?$/i; +const propExpr = /^(?:property:?\s*)?[^.]+\.([^ .()]+)\s*?$/i; +const braceExpr = /^(?:property:?\s*)?[^.[]+(\[[^\]]+\])\s*?$/i; const classMethExpr = - /^class\s*method\s*:?[^\.]+\.([^ \.\(\)]+)\([^\)]*\)\s*?$/i; + /^class\s*method\s*:?[^.]+\.([^ .()]+)\([^)]*\)\s*?$/i; const methExpr = - /^(?:method:?\s*)?(?:[^\.]+\.)?([^ \.\(\)]+)\([^\)]*\)\s*?$/i; -const newExpr = /^new ([A-Z][a-z]+)\([^\)]*\)\s*?$/; -var paramExpr = /\((.*)\);?$/; + /^(?:method:?\s*)?(?:[^.]+\.)?([^ .()]+)\([^)]*\)\s*?$/i; +const newExpr = /^new ([A-Z][a-z]+)\([^)]*\)\s*?$/; +const paramExpr = /\((.*)\);?$/; -function newSection(tok) { +const newSection = (tok) => { const section = {}; // infer the type from the text. const text = section.textRaw = tok.text; @@ -551,4 +553,4 @@ function newSection(tok) { section.name = text; } return section; -} +}; diff --git a/bin/doc/package-lock.json b/src/bin/doc/package-lock.json similarity index 100% rename from bin/doc/package-lock.json rename to src/bin/doc/package-lock.json diff --git a/bin/doc/package.json b/src/bin/doc/package.json similarity index 93% rename from bin/doc/package.json rename to src/bin/doc/package.json index 1a29f1b1c..2f027616c 100644 --- a/bin/doc/package.json +++ b/src/bin/doc/package.json @@ -4,7 +4,7 @@ "description": "Internal tool for generating Node.js API docs", "version": "0.0.0", "engines": { - "node": ">=0.6.10" + "node": ">=10.17.0" }, "dependencies": { "marked": "0.8.2" diff --git a/src/bin/extractPadData.js b/src/bin/extractPadData.js new file mode 100644 index 000000000..0688245d4 --- /dev/null +++ b/src/bin/extractPadData.js @@ -0,0 +1,64 @@ +'use strict'; + +/* + * This is a debug tool. It helps to extract all datas of a pad and move it from + * a productive environment and to a develop environment to reproduce bugs + * there. It outputs a dirtydb file + */ + +// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an +// unhandled rejection into an uncaught exception, which does cause Node.js to exit. +process.on('unhandledRejection', (err) => { throw err; }); + +if (process.argv.length !== 3) throw new Error('Use: node extractPadData.js $PADID'); + +// get the padID +const padId = process.argv[2]; + +(async () => { + // initialize database + require('../node/utils/Settings'); + const db = require('../node/db/DB'); + await db.init(); + + // load extra modules + const dirtyDB = require('dirty'); + const padManager = require('../node/db/PadManager'); + + // initialize output database + const dirty = dirtyDB(`${padId}.db`); + + // Promise wrapped get and set function + const wrapped = db.db.db.wrappedDB; + const get = util.promisify(wrapped.get.bind(wrapped)); + const set = util.promisify(dirty.set.bind(dirty)); + + // array in which required key values will be accumulated + const neededDBValues = [`pad:${padId}`]; + + // get the actual pad object + const pad = await padManager.getPad(padId); + + // add all authors + neededDBValues.push(...pad.getAllAuthors().map((author) => `globalAuthor:${author}`)); + + // add all revisions + for (let rev = 0; rev <= pad.head; ++rev) { + neededDBValues.push(`pad:${padId}:revs:${rev}`); + } + + // add all chat values + for (let chat = 0; chat <= pad.chatHead; ++chat) { + neededDBValues.push(`pad:${padId}:chat:${chat}`); + } + + for (const dbkey of neededDBValues) { + let dbvalue = await get(dbkey); + if (dbvalue && typeof dbvalue !== 'object') { + dbvalue = JSON.parse(dbvalue); + } + await set(dbkey, dbvalue); + } + + console.log('finished'); +})(); diff --git a/src/bin/fastRun.sh b/src/bin/fastRun.sh new file mode 100755 index 000000000..a782cafcb --- /dev/null +++ b/src/bin/fastRun.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# +# Run Etherpad directly, assuming all the dependencies are already installed. +# +# Useful for developers, or users that know what they are doing. If you just +# upgraded Etherpad version, installed a new dependency, or are simply unsure +# of what to do, please execute bin/installDeps.sh once before running this +# script. + +set -eu + +# Move to the Etherpad base directory. +MY_DIR=$(cd "${0%/*}" && pwd -P) || exit 1 +cd "${MY_DIR}/../.." || exit 1 + +# Source constants and useful functions +. src/bin/functions.sh + +echo "Running directly, without checking/installing dependencies" + +# run Etherpad main class +node $(compute_node_args) "node_modules/ep_etherpad-lite/node/server.js" "$@" diff --git a/bin/functions.sh b/src/bin/functions.sh similarity index 100% rename from bin/functions.sh rename to src/bin/functions.sh diff --git a/src/bin/importSqlFile.js b/src/bin/importSqlFile.js new file mode 100644 index 000000000..5a0520885 --- /dev/null +++ b/src/bin/importSqlFile.js @@ -0,0 +1,100 @@ +'use strict'; + +// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an +// unhandled rejection into an uncaught exception, which does cause Node.js to exit. +process.on('unhandledRejection', (err) => { throw err; }); + +const startTime = Date.now(); + +const log = (str) => { + console.log(`${(Date.now() - startTime) / 1000}\t${str}`); +}; + +const unescape = (val) => { + // value is a string + if (val.substr(0, 1) === "'") { + val = val.substr(0, val.length - 1).substr(1); + + return val.replace(/\\[0nrbtZ\\'"]/g, (s) => { + switch (s) { + case '\\0': return '\0'; + case '\\n': return '\n'; + case '\\r': return '\r'; + case '\\b': return '\b'; + case '\\t': return '\t'; + case '\\Z': return '\x1a'; + default: return s.substr(1); + } + }); + } + + // value is a boolean or NULL + if (val === 'NULL') { + return null; + } + if (val === 'true') { + return true; + } + if (val === 'false') { + return false; + } + + // value is a number + return val; +}; + +(async () => { + const fs = require('fs'); + const log4js = require('log4js'); + const settings = require('../node/utils/Settings'); + const ueberDB = require('ueberdb2'); + + const dbWrapperSettings = { + cache: 0, + writeInterval: 100, + json: false, // data is already json encoded + }; + const db = new ueberDB.database( // eslint-disable-line new-cap + settings.dbType, + settings.dbSettings, + dbWrapperSettings, + log4js.getLogger('ueberDB')); + + const sqlFile = process.argv[2]; + + // stop if the settings file is not set + if (!sqlFile) throw new Error('Use: node importSqlFile.js $SQLFILE'); + + log('initializing db'); + await util.promisify(db.init.bind(db))(); + log('done'); + + log('open output file...'); + const lines = fs.readFileSync(sqlFile, 'utf8').split('\n'); + + const count = lines.length; + let keyNo = 0; + + process.stdout.write(`Start importing ${count} keys...\n`); + lines.forEach((l) => { + if (l.substr(0, 27) === 'REPLACE INTO store VALUES (') { + const pos = l.indexOf("', '"); + const key = l.substr(28, pos - 28); + let value = l.substr(pos + 3); + value = value.substr(0, value.length - 2); + console.log(`key: ${key} val: ${value}`); + console.log(`unval: ${unescape(value)}`); + db.set(key, unescape(value), null); + keyNo++; + if (keyNo % 1000 === 0) { + process.stdout.write(` ${keyNo}/${count}\n`); + } + } + }); + process.stdout.write('\n'); + process.stdout.write('done. waiting for db to finish transaction. ' + + 'depended on dbms this may take some time..\n'); + + await util.promisify(db.close.bind(db))(); + log(`finished, imported ${keyNo} keys.`); +})(); diff --git a/bin/installDeps.sh b/src/bin/installDeps.sh similarity index 86% rename from bin/installDeps.sh rename to src/bin/installDeps.sh index bdce38fc7..94d8630cc 100755 --- a/bin/installDeps.sh +++ b/src/bin/installDeps.sh @@ -1,10 +1,11 @@ #!/bin/sh -# Move to the folder where ep-lite is installed -cd "$(dirname "$0")"/.. +# Move to the Etherpad base directory. +MY_DIR=$(cd "${0%/*}" && pwd -P) || exit 1 +cd "${MY_DIR}/../.." || exit 1 -# Source constants and usefull functions -. bin/functions.sh +# Source constants and useful functions +. src/bin/functions.sh # Is node installed? # Not checking io.js, default installation creates a symbolic link to node @@ -39,7 +40,7 @@ log "Ensure that all dependencies are up to date... If this is the first time y cd node_modules [ -e ep_etherpad-lite ] || ln -s ../src ep_etherpad-lite cd ep_etherpad-lite - npm ci + npm ci --no-optional ) || { rm -rf src/node_modules exit 1 diff --git a/bin/installOnWindows.bat b/src/bin/installOnWindows.bat similarity index 100% rename from bin/installOnWindows.bat rename to src/bin/installOnWindows.bat diff --git a/src/bin/migrateDirtyDBtoRealDB.js b/src/bin/migrateDirtyDBtoRealDB.js new file mode 100644 index 000000000..75f6cc677 --- /dev/null +++ b/src/bin/migrateDirtyDBtoRealDB.js @@ -0,0 +1,56 @@ +'use strict'; + +// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an +// unhandled rejection into an uncaught exception, which does cause Node.js to exit. +process.on('unhandledRejection', (err) => { throw err; }); + +(async () => { + // This script requires that you have modified your settings.json file + // to work with a real database. Please make a backup of your dirty.db + // file before using this script, just to be safe. + + // It might be necessary to run the script using more memory: + // `node --max-old-space-size=4096 src/bin/migrateDirtyDBtoRealDB.js` + + const dirtyDb = require('dirty'); + const log4js = require('log4js'); + const settings = require('../node/utils/Settings'); + const ueberDB = require('ueberdb2'); + const util = require('util'); + + const dbWrapperSettings = { + cache: '0', // The cache slows things down when you're mostly writing. + writeInterval: 0, // Write directly to the database, don't buffer + }; + const db = new ueberDB.database( // eslint-disable-line new-cap + settings.dbType, + settings.dbSettings, + dbWrapperSettings, + log4js.getLogger('ueberDB')); + await db.init(); + + console.log('Waiting for dirtyDB to parse its file.'); + const dirty = dirtyDb(`${__dirname}/../../var/dirty.db`); + const length = await new Promise((resolve) => { dirty.once('load', resolve); }); + + console.log(`Found ${length} records, processing now.`); + const p = []; + let numWritten = 0; + dirty.forEach((key, value) => { + let bcb, wcb; + p.push(new Promise((resolve, reject) => { + bcb = (err) => { if (err != null) return reject(err); }; + wcb = (err) => { + if (err != null) return reject(err); + if (++numWritten % 100 === 0) console.log(`Wrote record ${numWritten} of ${length}`); + resolve(); + }; + })); + db.set(key, value, bcb, wcb); + }); + await Promise.all(p); + console.log(`Wrote all ${numWritten} records`); + + await util.promisify(db.close.bind(db))(); + console.log('Finished.'); +})(); diff --git a/bin/plugins/README.md b/src/bin/plugins/README.md similarity index 61% rename from bin/plugins/README.md rename to src/bin/plugins/README.md index 81d5a4298..b14065821 100755 --- a/bin/plugins/README.md +++ b/src/bin/plugins/README.md @@ -2,28 +2,33 @@ The files in this folder are for Plugin developers. # Get suggestions to improve your Plugin -This code will check your plugin for known usual issues and some suggestions for improvements. No changes will be made to your project. +This code will check your plugin for known usual issues and some suggestions for +improvements. No changes will be made to your project. ``` -node bin/plugins/checkPlugin.js $PLUGIN_NAME$ +node src/bin/plugins/checkPlugin.js $PLUGIN_NAME$ ``` # Basic Example: + ``` -node bin/plugins/checkPlugin.js ep_webrtc +node src/bin/plugins/checkPlugin.js ep_webrtc ``` ## Autofixing - will autofix any issues it can + ``` -node bin/plugins/checkPlugins.js ep_whatever autofix +node src/bin/plugins/checkPlugin.js ep_whatever autofix ``` ## Autocommitting, push, npm minor patch and npm publish (highly dangerous) + ``` -node bin/plugins/checkPlugins.js ep_whatever autofix autocommit +node src/bin/plugins/checkPlugin.js ep_whatever autocommit ``` # All the plugins + Replace johnmclear with your github username ``` @@ -33,19 +38,15 @@ GHUSER=johnmclear; curl "https://api.github.com/users/$GHUSER/repos?per_page=100 cd .. # autofixes and autocommits /pushes & npm publishes -for dir in `ls node_modules`; -do -# echo $0 -if [[ $dir == *"ep_"* ]]; then -if [[ $dir != "ep_etherpad-lite" ]]; then -node bin/plugins/checkPlugin.js $dir autofix autocommit -fi -fi -# echo $dir +for dir in node_modules/ep_*; do + dir=${dir#node_modules/} + [ "$dir" != ep_etherpad-lite ] || continue + node src/bin/plugins/checkPlugin.js "$dir" autocommit done ``` # Automating update of ether organization plugins + ``` getCorePlugins.sh updateCorePlugins.sh diff --git a/src/bin/plugins/checkPlugin.js b/src/bin/plugins/checkPlugin.js new file mode 100755 index 000000000..ee96d63c3 --- /dev/null +++ b/src/bin/plugins/checkPlugin.js @@ -0,0 +1,458 @@ +'use strict'; + +/* + * Usage -- see README.md + * + * Normal usage: node src/bin/plugins/checkPlugin.js ep_whatever + * Auto fix the things it can: node src/bin/plugins/checkPlugin.js ep_whatever autofix + * Auto commit, push and publish to npm (highly dangerous): + * node src/bin/plugins/checkPlugin.js ep_whatever autocommit + */ + +// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an +// unhandled rejection into an uncaught exception, which does cause Node.js to exit. +process.on('unhandledRejection', (err) => { throw err; }); + +const fs = require('fs'); +const childProcess = require('child_process'); + +// get plugin name & path from user input +const pluginName = process.argv[2]; + +if (!pluginName) throw new Error('no plugin name specified'); + +const pluginPath = `node_modules/${pluginName}`; + +console.log(`Checking the plugin: ${pluginName}`); + +const optArgs = process.argv.slice(3); +const autoCommit = optArgs.indexOf('autocommit') !== -1; +const autoFix = autoCommit || optArgs.indexOf('autofix') !== -1; + +const execSync = (cmd, opts = {}) => (childProcess.execSync(cmd, { + cwd: `${pluginPath}/`, + ...opts, +}) || '').toString().replace(/\n+$/, ''); + +const writePackageJson = (obj) => { + let s = JSON.stringify(obj, null, 2); + if (s.length && s.slice(s.length - 1) !== '\n') s += '\n'; + return fs.writeFileSync(`${pluginPath}/package.json`, s); +}; + +const updateDeps = (parsedPackageJson, key, wantDeps) => { + const {[key]: deps = {}} = parsedPackageJson; + let changed = false; + for (const [pkg, verInfo] of Object.entries(wantDeps)) { + const {ver, overwrite = true} = typeof verInfo === 'string' ? {ver: verInfo} : verInfo; + if (deps[pkg] === ver) continue; + if (deps[pkg] == null) { + console.warn(`Missing dependency in ${key}: '${pkg}': '${ver}'`); + } else { + if (!overwrite) continue; + console.warn(`Dependency mismatch in ${key}: '${pkg}': '${ver}' (current: ${deps[pkg]})`); + } + if (autoFix) { + deps[pkg] = ver; + changed = true; + } + } + if (changed) { + parsedPackageJson[key] = deps; + writePackageJson(parsedPackageJson); + } +}; + +const prepareRepo = () => { + let branch = execSync('git symbolic-ref HEAD'); + if (branch !== 'refs/heads/master' && branch !== 'refs/heads/main') { + throw new Error('master/main must be checked out'); + } + branch = branch.replace(/^refs\/heads\//, ''); + execSync('git rev-parse --verify -q HEAD^0 || ' + + `{ echo "Error: no commits on ${branch}" >&2; exit 1; }`); + execSync('git rev-parse --verify @{u}'); // Make sure there's a remote tracking branch. + const modified = execSync('git diff-files --name-status'); + if (modified !== '') throw new Error(`working directory has modifications:\n${modified}`); + const untracked = execSync('git ls-files -o --exclude-standard'); + if (untracked !== '') throw new Error(`working directory has untracked files:\n${untracked}`); + const indexStatus = execSync('git diff-index --cached --name-status HEAD'); + if (indexStatus !== '') throw new Error(`uncommitted staged changes to files:\n${indexStatus}`); + execSync('git pull --ff-only', {stdio: 'inherit'}); + if (execSync('git rev-list @{u}...') !== '') throw new Error('repo contains unpushed commits'); + if (autoCommit) { + execSync('git config --get user.name'); + execSync('git config --get user.email'); + } +}; + +if (autoCommit) { + console.warn('Auto commit is enabled, I hope you know what you are doing...'); +} + +fs.readdir(pluginPath, (err, rootFiles) => { + // handling error + if (err) { + return console.log(`Unable to scan directory: ${err}`); + } + + // rewriting files to lower case + const files = []; + + // some files we need to know the actual file name. Not compulsory but might help in the future. + let readMeFileName; + let repository; + + for (let i = 0; i < rootFiles.length; i++) { + if (rootFiles[i].toLowerCase().indexOf('readme') !== -1) readMeFileName = rootFiles[i]; + files.push(rootFiles[i].toLowerCase()); + } + + if (files.indexOf('.git') === -1) throw new Error('No .git folder, aborting'); + prepareRepo(); + + try { + const path = `${pluginPath}/.github/workflows/npmpublish.yml`; + if (!fs.existsSync(path)) { + console.log('no .github/workflows/npmpublish.yml'); + console.log('create one and set npm secret to auto publish to npm on commit'); + if (autoFix) { + const npmpublish = + fs.readFileSync('src/bin/plugins/lib/npmpublish.yml', {encoding: 'utf8', flag: 'r'}); + fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true}); + fs.writeFileSync(path, npmpublish); + console.log("If you haven't already, setup autopublish for this plugin https://github.com/ether/etherpad-lite/wiki/Plugins:-Automatically-publishing-to-npm-on-commit-to-Github-Repo"); + } else { + console.log('Setup autopublish for this plugin https://github.com/ether/etherpad-lite/wiki/Plugins:-Automatically-publishing-to-npm-on-commit-to-Github-Repo'); + } + } else { + // autopublish exists, we should check the version.. + // checkVersion takes two file paths and checks for a version string in them. + const currVersionFile = fs.readFileSync(path, {encoding: 'utf8', flag: 'r'}); + const existingConfigLocation = currVersionFile.indexOf('##ETHERPAD_NPM_V='); + const existingValue = parseInt( + currVersionFile.substr(existingConfigLocation + 17, existingConfigLocation.length)); + + const reqVersionFile = + fs.readFileSync('src/bin/plugins/lib/npmpublish.yml', {encoding: 'utf8', flag: 'r'}); + const reqConfigLocation = reqVersionFile.indexOf('##ETHERPAD_NPM_V='); + const reqValue = + parseInt(reqVersionFile.substr(reqConfigLocation + 17, reqConfigLocation.length)); + + if (!existingValue || (reqValue > existingValue)) { + const npmpublish = + fs.readFileSync('src/bin/plugins/lib/npmpublish.yml', {encoding: 'utf8', flag: 'r'}); + fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true}); + fs.writeFileSync(path, npmpublish); + } + } + } catch (err) { + console.error(err); + } + + + try { + const path = `${pluginPath}/.github/workflows/backend-tests.yml`; + if (!fs.existsSync(path)) { + console.log('no .github/workflows/backend-tests.yml'); + console.log('create one and set npm secret to auto publish to npm on commit'); + if (autoFix) { + const backendTests = + fs.readFileSync('src/bin/plugins/lib/backend-tests.yml', {encoding: 'utf8', flag: 'r'}); + fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true}); + fs.writeFileSync(path, backendTests); + } + } else { + // autopublish exists, we should check the version.. + // checkVersion takes two file paths and checks for a version string in them. + const currVersionFile = fs.readFileSync(path, {encoding: 'utf8', flag: 'r'}); + const existingConfigLocation = currVersionFile.indexOf('##ETHERPAD_NPM_V='); + const existingValue = parseInt( + currVersionFile.substr(existingConfigLocation + 17, existingConfigLocation.length)); + + const reqVersionFile = + fs.readFileSync('src/bin/plugins/lib/backend-tests.yml', {encoding: 'utf8', flag: 'r'}); + const reqConfigLocation = reqVersionFile.indexOf('##ETHERPAD_NPM_V='); + const reqValue = + parseInt(reqVersionFile.substr(reqConfigLocation + 17, reqConfigLocation.length)); + + if (!existingValue || (reqValue > existingValue)) { + const backendTests = + fs.readFileSync('src/bin/plugins/lib/backend-tests.yml', {encoding: 'utf8', flag: 'r'}); + fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true}); + fs.writeFileSync(path, backendTests); + } + } + } catch (err) { + console.error(err); + } + + if (files.indexOf('package.json') === -1) { + console.warn('no package.json, please create'); + } + + if (files.indexOf('package.json') !== -1) { + const packageJSON = + fs.readFileSync(`${pluginPath}/package.json`, {encoding: 'utf8', flag: 'r'}); + const parsedPackageJSON = JSON.parse(packageJSON); + if (autoFix) { + let updatedPackageJSON = false; + if (!parsedPackageJSON.funding) { + updatedPackageJSON = true; + parsedPackageJSON.funding = { + type: 'individual', + url: 'https://etherpad.org/', + }; + } + if (updatedPackageJSON) { + writePackageJson(parsedPackageJSON); + } + } + + if (packageJSON.toLowerCase().indexOf('repository') === -1) { + console.warn('No repository in package.json'); + if (autoFix) { + console.warn('Repository not detected in package.json. Add repository section.'); + } + } else { + // useful for creating README later. + repository = parsedPackageJSON.repository.url; + } + + updateDeps(parsedPackageJSON, 'devDependencies', { + 'eslint': '^7.18.0', + 'eslint-config-etherpad': '^1.0.24', + 'eslint-plugin-eslint-comments': '^3.2.0', + 'eslint-plugin-mocha': '^8.0.0', + 'eslint-plugin-node': '^11.1.0', + 'eslint-plugin-prefer-arrow': '^1.2.3', + 'eslint-plugin-promise': '^4.2.1', + 'eslint-plugin-you-dont-need-lodash-underscore': '^6.10.0', + }); + + updateDeps(parsedPackageJSON, 'peerDependencies', { + // Some plugins require a newer version of Etherpad so don't overwrite if already set. + 'ep_etherpad-lite': {ver: '>=1.8.6', overwrite: false}, + }); + + if (packageJSON.toLowerCase().indexOf('eslintconfig') === -1) { + console.warn('No esLintConfig in package.json'); + if (autoFix) { + const eslintConfig = { + root: true, + extends: 'etherpad/plugin', + }; + parsedPackageJSON.eslintConfig = eslintConfig; + writePackageJson(parsedPackageJSON); + } + } + + if (packageJSON.toLowerCase().indexOf('scripts') === -1) { + console.warn('No scripts in package.json'); + if (autoFix) { + const scripts = { + 'lint': 'eslint .', + 'lint:fix': 'eslint --fix .', + }; + parsedPackageJSON.scripts = scripts; + writePackageJson(parsedPackageJSON); + } + } + + if ((packageJSON.toLowerCase().indexOf('engines') === -1) || !parsedPackageJSON.engines.node) { + console.warn('No engines or node engine in package.json'); + if (autoFix) { + const engines = { + node: '^10.17.0 || >=11.14.0', + }; + parsedPackageJSON.engines = engines; + writePackageJson(parsedPackageJSON); + } + } + } + + if (files.indexOf('package-lock.json') === -1) { + console.warn('package-lock.json not found'); + if (!autoFix) { + console.warn('Run npm install in the plugin folder and commit the package-lock.json file.'); + } + } + if (files.indexOf('readme') === -1 && files.indexOf('readme.md') === -1) { + console.warn('README.md file not found, please create'); + if (autoFix) { + console.log('Autofixing missing README.md file'); + console.log('please edit the README.md file further to include plugin specific details.'); + let readme = fs.readFileSync('src/bin/plugins/lib/README.md', {encoding: 'utf8', flag: 'r'}); + readme = readme.replace(/\[plugin_name\]/g, pluginName); + if (repository) { + const org = repository.split('/')[3]; + const name = repository.split('/')[4]; + readme = readme.replace(/\[org_name\]/g, org); + readme = readme.replace(/\[repo_url\]/g, name); + fs.writeFileSync(`${pluginPath}/README.md`, readme); + } else { + console.warn('Unable to find repository in package.json, aborting.'); + } + } + } + + if (files.indexOf('contributing') === -1 && files.indexOf('contributing.md') === -1) { + console.warn('CONTRIBUTING.md file not found, please create'); + if (autoFix) { + console.log('Autofixing missing CONTRIBUTING.md file, please edit the CONTRIBUTING.md ' + + 'file further to include plugin specific details.'); + let contributing = + fs.readFileSync('src/bin/plugins/lib/CONTRIBUTING.md', {encoding: 'utf8', flag: 'r'}); + contributing = contributing.replace(/\[plugin_name\]/g, pluginName); + fs.writeFileSync(`${pluginPath}/CONTRIBUTING.md`, contributing); + } + } + + + if (readMeFileName) { + let readme = + fs.readFileSync(`${pluginPath}/${readMeFileName}`, {encoding: 'utf8', flag: 'r'}); + if (readme.toLowerCase().indexOf('license') === -1) { + console.warn('No license section in README'); + if (autoFix) { + console.warn('Please add License section to README manually.'); + } + } + // eslint-disable-next-line max-len + const publishBadge = `![Publish Status](https://github.com/ether/${pluginName}/workflows/Node.js%20Package/badge.svg)`; + // eslint-disable-next-line max-len + const testBadge = `![Backend Tests Status](https://github.com/ether/${pluginName}/workflows/Backend%20tests/badge.svg)`; + if (readme.toLowerCase().indexOf('travis') !== -1) { + console.warn('Remove Travis badges'); + } + if (readme.indexOf('workflows/Node.js%20Package/badge.svg') === -1) { + console.warn('No Github workflow badge detected'); + if (autoFix) { + readme = `${publishBadge} ${testBadge}\n\n${readme}`; + // write readme to file system + fs.writeFileSync(`${pluginPath}/${readMeFileName}`, readme); + console.log('Wrote Github workflow badges to README'); + } + } + } + + if (files.indexOf('license') === -1 && files.indexOf('license.md') === -1) { + console.warn('LICENSE.md file not found, please create'); + if (autoFix) { + console.log('Autofixing missing LICENSE.md file, including Apache 2 license.'); + let license = + fs.readFileSync('src/bin/plugins/lib/LICENSE.md', {encoding: 'utf8', flag: 'r'}); + license = license.replace('[yyyy]', new Date().getFullYear()); + license = license.replace('[name of copyright owner]', execSync('git config user.name')); + fs.writeFileSync(`${pluginPath}/LICENSE.md`, license); + } + } + + if (files.indexOf('.gitignore') === -1) { + console.warn('.gitignore file not found, please create. .gitignore files are useful to ' + + "ensure files aren't incorrectly commited to a repository."); + if (autoFix) { + console.log('Autofixing missing .gitignore file'); + const gitignore = + fs.readFileSync('src/bin/plugins/lib/gitignore', {encoding: 'utf8', flag: 'r'}); + fs.writeFileSync(`${pluginPath}/.gitignore`, gitignore); + } + } else { + let gitignore = + fs.readFileSync(`${pluginPath}/.gitignore`, {encoding: 'utf8', flag: 'r'}); + if (gitignore.indexOf('node_modules/') === -1) { + console.warn('node_modules/ missing from .gitignore'); + if (autoFix) { + gitignore += 'node_modules/'; + fs.writeFileSync(`${pluginPath}/.gitignore`, gitignore); + } + } + } + + // if we include templates but don't have translations... + if (files.indexOf('templates') !== -1 && files.indexOf('locales') === -1) { + console.warn('Translations not found, please create. ' + + 'Translation files help with Etherpad accessibility.'); + } + + + if (files.indexOf('.ep_initialized') !== -1) { + console.warn( + '.ep_initialized found, please remove. .ep_initialized should never be commited to git ' + + 'and should only exist once the plugin has been executed one time.'); + if (autoFix) { + console.log('Autofixing incorrectly existing .ep_initialized file'); + fs.unlinkSync(`${pluginPath}/.ep_initialized`); + } + } + + if (files.indexOf('npm-debug.log') !== -1) { + console.warn('npm-debug.log found, please remove. npm-debug.log should never be commited to ' + + 'your repository.'); + if (autoFix) { + console.log('Autofixing incorrectly existing npm-debug.log file'); + fs.unlinkSync(`${pluginPath}/npm-debug.log`); + } + } + + if (files.indexOf('static') !== -1) { + fs.readdir(`${pluginPath}/static`, (errRead, staticFiles) => { + if (staticFiles.indexOf('tests') === -1) { + console.warn('Test files not found, please create tests. https://github.com/ether/etherpad-lite/wiki/Creating-a-plugin#writing-and-running-front-end-tests-for-your-plugin'); + } + }); + } else { + console.warn('Test files not found, please create tests. https://github.com/ether/etherpad-lite/wiki/Creating-a-plugin#writing-and-running-front-end-tests-for-your-plugin'); + } + + // Install dependencies so we can run ESLint. This should also create or update package-lock.json + // if autoFix is enabled. + const npmInstall = `npm install${autoFix ? '' : ' --no-package-lock'}`; + execSync(npmInstall, {stdio: 'inherit'}); + // The ep_etherpad-lite peer dep must be installed last otherwise `npm install` will nuke it. An + // absolute path to etherpad-lite/src is used here so that pluginPath can be a symlink. + execSync( + `${npmInstall} --no-save ep_etherpad-lite@file:${__dirname}/../../`, {stdio: 'inherit'}); + // linting begins + try { + console.log('Linting...'); + const lintCmd = autoFix ? 'npx eslint --fix .' : 'npx eslint'; + execSync(lintCmd, {stdio: 'inherit'}); + } catch (e) { + // it is gonna throw an error anyway + console.log('Manual linting probably required, check with: npm run lint'); + } + // linting ends. + + if (autoFix) { + const unchanged = JSON.parse(execSync( + 'untracked=$(git ls-files -o --exclude-standard) || exit 1; ' + + 'git diff-files --quiet && [ -z "$untracked" ] && echo true || echo false')); + if (!unchanged) { + // Display a diff of changes. Git doesn't diff untracked files, so they must be added to the + // index. Use a temporary index file to avoid modifying Git's default index file. + execSync('git read-tree HEAD; git add -A && git diff-index -p --cached HEAD && echo ""', { + env: {...process.env, GIT_INDEX_FILE: '.git/checkPlugin.index'}, + stdio: 'inherit', + }); + fs.unlinkSync(`${pluginPath}/.git/checkPlugin.index`); + + const cmd = [ + 'git add -A', + 'git commit -m "autofixes from Etherpad checkPlugin.js"', + 'git push', + ].join(' && '); + if (autoCommit) { + console.log('Attempting autocommit and auto publish to npm'); + execSync(cmd, {stdio: 'inherit'}); + } else { + console.log('Fixes applied. Check the above git diff then run the following command:'); + console.log(`(cd node_modules/${pluginName} && ${cmd})`); + } + } else { + console.log('No changes.'); + } + } + + console.log('Finished'); +}); diff --git a/bin/plugins/getCorePlugins.sh b/src/bin/plugins/getCorePlugins.sh similarity index 100% rename from bin/plugins/getCorePlugins.sh rename to src/bin/plugins/getCorePlugins.sh diff --git a/bin/plugins/lib/CONTRIBUTING.md b/src/bin/plugins/lib/CONTRIBUTING.md similarity index 98% rename from bin/plugins/lib/CONTRIBUTING.md rename to src/bin/plugins/lib/CONTRIBUTING.md index 724e02ac0..347437b22 100644 --- a/bin/plugins/lib/CONTRIBUTING.md +++ b/src/bin/plugins/lib/CONTRIBUTING.md @@ -113,7 +113,9 @@ Documentation should be kept up-to-date. This means, whenever you add a new API You can build the docs e.g. produce html, using `make docs`. At some point in the future we will provide an online documentation. The current documentation in the github wiki should always reflect the state of `master` (!), since there are no docs in master, yet. ## Testing -Front-end tests are found in the `tests/frontend/` folder in the repository. Run them by pointing your browser to `/tests/frontend`. + +Front-end tests are found in the `src/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`. diff --git a/bin/plugins/lib/LICENSE.md b/src/bin/plugins/lib/LICENSE.md similarity index 100% rename from bin/plugins/lib/LICENSE.md rename to src/bin/plugins/lib/LICENSE.md diff --git a/bin/plugins/lib/README.md b/src/bin/plugins/lib/README.md similarity index 100% rename from bin/plugins/lib/README.md rename to src/bin/plugins/lib/README.md diff --git a/bin/plugins/lib/backend-tests.yml b/src/bin/plugins/lib/backend-tests.yml similarity index 88% rename from bin/plugins/lib/backend-tests.yml rename to src/bin/plugins/lib/backend-tests.yml index 324cc4baf..f1d3a4af1 100644 --- a/bin/plugins/lib/backend-tests.yml +++ b/src/bin/plugins/lib/backend-tests.yml @@ -30,7 +30,7 @@ jobs: repository: ether/etherpad-lite - name: Install all dependencies and symlink for ep_etherpad-lite - run: bin/installDeps.sh + run: src/bin/installDeps.sh # clone this repository into node_modules/ep_plugin-name - name: Checkout plugin repository @@ -45,7 +45,7 @@ jobs: # configures some settings and runs npm run test - name: Run the backend tests - run: tests/frontend/travis/runnerBackend.sh + run: src/tests/frontend/travis/runnerBackend.sh ##ETHERPAD_NPM_V=1 -## NPM configuration automatically created using bin/plugins/updateAllPluginsScript.sh +## NPM configuration automatically created using src/bin/plugins/updateAllPluginsScript.sh diff --git a/bin/plugins/lib/gitignore b/src/bin/plugins/lib/gitignore similarity index 100% rename from bin/plugins/lib/gitignore rename to src/bin/plugins/lib/gitignore diff --git a/bin/plugins/lib/npmpublish.yml b/src/bin/plugins/lib/npmpublish.yml similarity index 77% rename from bin/plugins/lib/npmpublish.yml rename to src/bin/plugins/lib/npmpublish.yml index 8d94ce88a..4a930144e 100644 --- a/bin/plugins/lib/npmpublish.yml +++ b/src/bin/plugins/lib/npmpublish.yml @@ -64,10 +64,20 @@ jobs: - run: git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - run: npm ci - run: npm version patch + - run: git push --follow-tags + # `npm publish` must come after `git push` otherwise there is a race + # condition: If two PRs are merged back-to-back then master/main will be + # updated with the commits from the second PR before the first PR's + # workflow has a chance to push the commit generated by `npm version + # patch`. This causes the first PR's `git push` step to fail after the + # package has already been published, which in turn will cause all future + # workflow runs to fail because they will all attempt to use the same + # already-used version number. By running `npm publish` after `git push`, + # back-to-back merges will cause the first merge's workflow to fail but + # the second's will succeed. - run: npm publish env: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} - - run: git push --follow-tags -##ETHERPAD_NPM_V=1 -## NPM configuration automatically created using bin/plugins/updateAllPluginsScript.sh +##ETHERPAD_NPM_V=2 +## NPM configuration automatically created using src/bin/plugins/updateAllPluginsScript.sh diff --git a/bin/plugins/reTestAllPlugins.sh b/src/bin/plugins/reTestAllPlugins.sh similarity index 80% rename from bin/plugins/reTestAllPlugins.sh rename to src/bin/plugins/reTestAllPlugins.sh index 319d378d4..58628bdb0 100755 --- a/bin/plugins/reTestAllPlugins.sh +++ b/src/bin/plugins/reTestAllPlugins.sh @@ -4,7 +4,7 @@ do echo $dir if [[ $dir == *"ep_"* ]]; then if [[ $dir != "ep_etherpad-lite" ]]; then - # node bin/plugins/checkPlugin.js $dir autofix autocommit autoupdate + # node src/bin/plugins/checkPlugin.js $dir autofix autocommit autoupdate cd node_modules/$dir git commit -m "Automatic update: bump update to re-run latest Etherpad tests" --allow-empty git push origin master diff --git a/bin/plugins/updateAllPluginsScript.sh b/src/bin/plugins/updateAllPluginsScript.sh similarity index 90% rename from bin/plugins/updateAllPluginsScript.sh rename to src/bin/plugins/updateAllPluginsScript.sh index 763724fca..bf5280ee0 100755 --- a/bin/plugins/updateAllPluginsScript.sh +++ b/src/bin/plugins/updateAllPluginsScript.sh @@ -10,7 +10,7 @@ do # echo $0 if [[ $dir == *"ep_"* ]]; then if [[ $dir != "ep_etherpad-lite" ]]; then - node bin/plugins/checkPlugin.js $dir autofix autocommit autoupdate + node src/bin/plugins/checkPlugin.js $dir autofix autocommit autoupdate fi fi # echo $dir diff --git a/bin/plugins/updateCorePlugins.sh b/src/bin/plugins/updateCorePlugins.sh similarity index 63% rename from bin/plugins/updateCorePlugins.sh rename to src/bin/plugins/updateCorePlugins.sh index bf4e6b6d6..402a080ec 100755 --- a/bin/plugins/updateCorePlugins.sh +++ b/src/bin/plugins/updateCorePlugins.sh @@ -5,5 +5,5 @@ set -e for dir in node_modules/ep_*; do dir=${dir#node_modules/} [ "$dir" != ep_etherpad-lite ] || continue - node bin/plugins/checkPlugin.js "$dir" autofix autocommit autoupdate + node src/bin/plugins/checkPlugin.js "$dir" autofix autocommit autoupdate done diff --git a/src/bin/rebuildPad.js b/src/bin/rebuildPad.js new file mode 100644 index 000000000..73f530889 --- /dev/null +++ b/src/bin/rebuildPad.js @@ -0,0 +1,84 @@ +'use strict'; + +/* + This is a repair tool. It rebuilds an old pad at a new pad location up to a + known "good" revision. +*/ + +// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an +// unhandled rejection into an uncaught exception, which does cause Node.js to exit. +process.on('unhandledRejection', (err) => { throw err; }); + +if (process.argv.length !== 4 && process.argv.length !== 5) { + throw new Error('Use: node src/bin/repairPad.js $PADID $REV [$NEWPADID]'); +} + +const padId = process.argv[2]; +const newRevHead = process.argv[3]; +const newPadId = process.argv[4] || `${padId}-rebuilt`; + +(async () => { + const db = require('../node/db/DB'); + await db.init(); + + const PadManager = require('../node/db/PadManager'); + const Pad = require('../node/db/Pad').Pad; + // Validate the newPadId if specified and that a pad with that ID does + // not already exist to avoid overwriting it. + if (!PadManager.isValidPadId(newPadId)) { + throw new Error('Cannot create a pad with that id as it is invalid'); + } + const exists = await PadManager.doesPadExist(newPadId); + if (exists) throw new Error('Cannot create a pad with that id as it already exists'); + + const oldPad = await PadManager.getPad(padId); + const newPad = new Pad(newPadId); + + // Clone all Chat revisions + const chatHead = oldPad.chatHead; + await Promise.all([...Array(chatHead + 1).keys()].map(async (i) => { + const chat = await db.get(`pad:${padId}:chat:${i}`); + await db.set(`pad:${newPadId}:chat:${i}`, chat); + console.log(`Created: Chat Revision: pad:${newPadId}:chat:${i}`); + })); + + // Rebuild Pad from revisions up to and including the new revision head + const AuthorManager = require('../node/db/AuthorManager'); + const Changeset = require('../static/js/Changeset'); + // Author attributes are derived from changesets, but there can also be + // non-author attributes with specific mappings that changesets depend on + // and, AFAICT, cannot be recreated any other way + newPad.pool.numToAttrib = oldPad.pool.numToAttrib; + for (let curRevNum = 0; curRevNum <= newRevHead; curRevNum++) { + const rev = await db.get(`pad:${padId}:revs:${curRevNum}`); + if (!rev || !rev.meta) throw new Error('The specified revision number could not be found.'); + const newRevNum = ++newPad.head; + const newRevId = `pad:${newPad.id}:revs:${newRevNum}`; + await Promise.all([ + db.set(newRevId, rev), + AuthorManager.addPad(rev.meta.author, newPad.id), + ]); + newPad.atext = Changeset.applyToAText(rev.changeset, newPad.atext, newPad.pool); + console.log(`Created: Revision: pad:${newPad.id}:revs:${newRevNum}`); + } + + // Add saved revisions up to the new revision head + console.log(newPad.head); + const newSavedRevisions = []; + for (const savedRev of oldPad.savedRevisions) { + if (savedRev.revNum <= newRevHead) { + newSavedRevisions.push(savedRev); + console.log(`Added: Saved Revision: ${savedRev.revNum}`); + } + } + newPad.savedRevisions = newSavedRevisions; + + // Save the source pad + await db.set(`pad:${newPadId}`, newPad); + + console.log(`Created: Source Pad: pad:${newPadId}`); + await newPad.saveToDatabase(); + + await db.shutdown(); + console.info('finished'); +})(); diff --git a/src/bin/release.js b/src/bin/release.js new file mode 100644 index 000000000..42178caa9 --- /dev/null +++ b/src/bin/release.js @@ -0,0 +1,75 @@ +'use strict'; + +// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an +// unhandled rejection into an uncaught exception, which does cause Node.js to exit. +process.on('unhandledRejection', (err) => { throw err; }); + +const fs = require('fs'); +const childProcess = require('child_process'); +const semver = require('semver'); + +/* + +Usage + +node src/bin/release.js patch + +*/ +const usage = + 'node src/bin/release.js [patch/minor/major] -- example: "node src/bin/release.js patch"'; + +const release = process.argv[2]; + +if (!release) { + console.log(usage); + throw new Error('No release type included'); +} + +const changelog = fs.readFileSync('CHANGELOG.md', {encoding: 'utf8', flag: 'r'}); +let packageJson = fs.readFileSync('./src/package.json', {encoding: 'utf8', flag: 'r'}); +packageJson = JSON.parse(packageJson); +const currentVersion = packageJson.version; + +const newVersion = semver.inc(currentVersion, release); +if (!newVersion) { + console.log(usage); + throw new Error('Unable to generate new version from input'); +} + +const changelogIncludesVersion = changelog.indexOf(newVersion) !== -1; + +if (!changelogIncludesVersion) { + throw new Error(`No changelog record for ${newVersion}, please create changelog record`); +} + +console.log('Okay looks good, lets create the package.json and package-lock.json'); + +packageJson.version = newVersion; + +fs.writeFileSync('src/package.json', JSON.stringify(packageJson, null, 2)); + +// run npm version `release` where release is patch, minor or major +childProcess.execSync('npm install --package-lock-only', {cwd: 'src/'}); +// run npm install --package-lock-only <-- required??? + +childProcess.execSync(`git checkout -b release/${newVersion}`); +childProcess.execSync('git add src/package.json'); +childProcess.execSync('git add src/package-lock.json'); +childProcess.execSync('git commit -m "bump version"'); +childProcess.execSync(`git push origin release/${newVersion}`); + + +childProcess.execSync('make docs'); +childProcess.execSync('git clone git@github.com:ether/ether.github.com.git'); +childProcess.execSync(`cp -R out/doc/ ether.github.com/doc/v${newVersion}`); + +console.log('Once merged into master please run the following commands'); +console.log(`git tag -a ${newVersion} -m ${newVersion} && git push origin master`); +console.log(`cd ether.github.com && git add . && git commit -m '${newVersion} docs'`); +console.log('Build the windows zip'); +console.log('Visit https://github.com/ether/etherpad-lite/releases/new and create a new release ' + + `with 'master' as the target and the version is ${newVersion}. Include the windows ` + + 'zip as an asset'); +console.log(`Once the new docs are uploaded then modify the download + link on etherpad.org and then pull master onto develop`); +console.log('Finally go public with an announcement via our comms channels :)'); diff --git a/src/bin/repairPad.js b/src/bin/repairPad.js new file mode 100644 index 000000000..7983fc88d --- /dev/null +++ b/src/bin/repairPad.js @@ -0,0 +1,56 @@ +'use strict'; + +/* + * This is a repair tool. It extracts all datas of a pad, removes and inserts them again. + */ + +// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an +// unhandled rejection into an uncaught exception, which does cause Node.js to exit. +process.on('unhandledRejection', (err) => { throw err; }); + +console.warn('WARNING: This script must not be used while etherpad is running!'); + +if (process.argv.length !== 3) throw new Error('Use: node src/bin/repairPad.js $PADID'); + +// get the padID +const padId = process.argv[2]; + +let valueCount = 0; + +(async () => { + // initialize database + require('../node/utils/Settings'); + const db = require('../node/db/DB'); + await db.init(); + + // get the pad + const padManager = require('../node/db/PadManager'); + const pad = await padManager.getPad(padId); + + // accumulate the required keys + const neededDBValues = [`pad:${padId}`]; + + // add all authors + neededDBValues.push(...pad.getAllAuthors().map((author) => `globalAuthor:${author}`)); + + // add all revisions + for (let rev = 0; rev <= pad.head; ++rev) { + neededDBValues.push(`pad:${padId}:revs:${rev}`); + } + + // add all chat values + for (let chat = 0; chat <= pad.chatHead; ++chat) { + neededDBValues.push(`pad:${padId}:chat:${chat}`); + } + // now fetch and reinsert every key + for (const key of neededDBValues) { + const value = await db.get(key); + // if it isn't a globalAuthor value which we want to ignore.. + // console.log(`Key: ${key}, value: ${JSON.stringify(value)}`); + await db.remove(key); + await db.set(key, value); + valueCount++; + } + + console.info(`Finished: Replaced ${valueCount} values in the database`); +})(); diff --git a/bin/run.sh b/src/bin/run.sh similarity index 76% rename from bin/run.sh rename to src/bin/run.sh index 50bce4bdd..1a2aa36a9 100755 --- a/bin/run.sh +++ b/src/bin/run.sh @@ -1,10 +1,11 @@ #!/bin/sh -# Move to the folder where ep-lite is installed -cd "$(dirname "$0")"/.. +# Move to the Etherpad base directory. +MY_DIR=$(cd "${0%/*}" && pwd -P) || exit 1 +cd "${MY_DIR}/../.." || exit 1 -# Source constants and usefull functions -. bin/functions.sh +# Source constants and useful functions +. src/bin/functions.sh ignoreRoot=0 for ARG in "$@"; do @@ -26,7 +27,7 @@ EOF fi # Prepare the environment -bin/installDeps.sh "$@" || exit 1 +src/bin/installDeps.sh "$@" || exit 1 # Move to the node folder and start log "Starting Etherpad..." diff --git a/bin/safeRun.sh b/src/bin/safeRun.sh similarity index 91% rename from bin/safeRun.sh rename to src/bin/safeRun.sh index 6d43e3035..d9efa241a 100755 --- a/bin/safeRun.sh +++ b/src/bin/safeRun.sh @@ -23,8 +23,9 @@ fatal() { error "$@"; exit 1; } LAST_EMAIL_SEND=0 -# Move to the folder where ep-lite is installed -cd "$(dirname "$0")"/.. +# Move to the Etherpad base directory. +MY_DIR=$(try cd "${0%/*}" && try pwd -P) || exit 1 +try cd "${MY_DIR}/../.." # Check if a logfile parameter is set LOG="$1" @@ -39,7 +40,7 @@ while true; do [ -w "${LOG}" ] || fatal "Logfile '${LOG}' is not writeable" # Start the application - bin/run.sh "$@" >>${LOG} 2>>${LOG} + src/bin/run.sh "$@" >>${LOG} 2>>${LOG} TIME_FMT=$(date +%Y-%m-%dT%H:%M:%S%z) diff --git a/bin/updatePlugins.sh b/src/bin/updatePlugins.sh similarity index 100% rename from bin/updatePlugins.sh rename to src/bin/updatePlugins.sh diff --git a/src/locales/be-tarask.json b/src/locales/be-tarask.json index 2d3e33a52..a759eda7e 100644 --- a/src/locales/be-tarask.json +++ b/src/locales/be-tarask.json @@ -7,6 +7,13 @@ "Wizardist" ] }, + "admin.page-title": "Адміністрацыйная панэль — Etherpad", + "admin_plugins": "Кіраўнік плагінаў", + "admin_plugins.available": "Даступныя плагіны", + "admin_plugins.available_not-found": "Плагіны ня знойдзеныя.", + "admin_plugins.available_fetching": "Атрымліваем…", + "admin_plugins.available_install.value": "Усталяваць", + "admin_settings.page-title": "Налады — Etherpad", "index.newPad": "Стварыць", "index.createOpenPad": "ці тварыць/адкрыць дакумэнт з назвай:", "index.openPad": "адкрыць існы Нататнік з назваю:", @@ -17,7 +24,7 @@ "pad.toolbar.ol.title": "Упарадкаваны сьпіс (Ctrl+Shift+N)", "pad.toolbar.ul.title": "Неўпарадкаваны сьпіс (Ctrl+Shift+L)", "pad.toolbar.indent.title": "Водступ (TAB)", - "pad.toolbar.unindent.title": "Выступ (Shift+TAB)", + "pad.toolbar.unindent.title": "Водступ (Shift+TAB)", "pad.toolbar.undo.title": "Скасаваць(Ctrl-Z)", "pad.toolbar.redo.title": "Вярнуць (Ctrl-Y)", "pad.toolbar.clearAuthorship.title": "Прыбраць колер дакумэнту (Ctrl+Shift+C)", @@ -42,6 +49,7 @@ "pad.settings.fontType": "Тып шрыфту:", "pad.settings.fontType.normal": "Звычайны", "pad.settings.language": "Мова:", + "pad.settings.about": "Пра", "pad.importExport.import_export": "Імпарт/Экспарт", "pad.importExport.import": "Загрузіжайце любыя тэкставыя файлы або дакумэнты", "pad.importExport.importSuccessful": "Пасьпяхова!", @@ -54,7 +62,7 @@ "pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.abiword.innerHTML": "Вы можаце імпартаваць толькі з звычайнага тэксту або HTML. Дзеля больш пашыраных магчымасьцяў імпарту, калі ласка, усталюйце AbiWord альбо LibreOffice.", "pad.modals.connected": "Падлучыліся.", - "pad.modals.reconnecting": "Перападлучэньне да вашага дакумэнта...", + "pad.modals.reconnecting": "Перападлучэньне да вашага дакумэнта…", "pad.modals.forcereconnect": "Прымусовае перападлучэньне", "pad.modals.reconnecttimer": "Спрабуем перападключыцца праз", "pad.modals.cancel": "Скасаваць", diff --git a/src/locales/cs.json b/src/locales/cs.json index 0e0140931..a47b1a3be 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -10,11 +10,46 @@ "Leanes", "Mormegil", "Peldrjan", - "Quinn" + "Quinn", + "Spotter" ] }, + "admin.page-title": "Ovládací panel Správce - Etherpad", + "admin_plugins": "Správce zásuvných moodulů", + "admin_plugins.available": "Dostupné zásuvné moduly", + "admin_plugins.available_not-found": "Nejsou žádné zásuvné moduly", + "admin_plugins.available_fetching": "Načítání...", + "admin_plugins.available_install.value": "Instalovat", + "admin_plugins.available_search.placeholder": "Vyhledat zásuvné moduly k instalaci", + "admin_plugins.description": "Popis", + "admin_plugins.installed": "Nainstalované zásuvné moduly", + "admin_plugins.installed_fetching": "Načítání instalovaných zásuvných modulů...", + "admin_plugins.installed_nothing": "Dosud jste nenainstalovali žádné zásuvné moduly.", + "admin_plugins.installed_uninstall.value": "Odinstalovat", + "admin_plugins.last-update": "Poslední aktualizace", + "admin_plugins.name": "Název", + "admin_plugins.page-title": "Správce zásuvných modulů - Etherpad", + "admin_plugins.version": "Verze", + "admin_plugins_info": "Informace o řešení problému", + "admin_plugins_info.hooks": "Instalované hooks", + "admin_plugins_info.hooks_client": "hooks na straně klienta", + "admin_plugins_info.hooks_server": "hooks na straně serveru", + "admin_plugins_info.parts": "Nainstalované součásti", + "admin_plugins_info.plugins": "Nainstalované zásuvné moduly", + "admin_plugins_info.page-title": "Informace o zásuvných modulech - Etherpad", + "admin_plugins_info.version": "Verze Etherpad", + "admin_plugins_info.version_latest": "Poslední dostupná verze", + "admin_plugins_info.version_number": "Číslo verze", + "admin_settings": "Nastavení", + "admin_settings.current": "Aktuální konfugurace", + "admin_settings.current_example-devel": "Příklad ukázkové vývojové šablony", + "admin_settings.current_example-prod": "Příklad šablony nastavení výroby", + "admin_settings.current_restart.value": "Restartovat Etherpad", + "admin_settings.current_save.value": "Uložit nastavení", + "admin_settings.page-title": "Nastavení - Etherpad", "index.newPad": "Založ nový Pad", "index.createOpenPad": "nebo vytvoř/otevři Pad s názvem:", + "index.openPad": "otevřít existující Pad se jménem:", "pad.toolbar.bold.title": "Tučný text (Ctrl-B)", "pad.toolbar.italic.title": "Kurzíva (Ctrl-I)", "pad.toolbar.underline.title": "Podtržené písmo (Ctrl-U)", @@ -35,7 +70,7 @@ "pad.colorpicker.save": "Uložit", "pad.colorpicker.cancel": "Zrušit", "pad.loading": "Načítání...", - "pad.noCookie": "Nelze nalézt cookie. Povolte prosím cookie ve Vašem prohlížeči.", + "pad.noCookie": "Soubor cookie nebyl nalezen. Povolte prosím cookies ve svém prohlížeči! Vaše relace a nastavení se mezi návštěvami neuloží. Může to být způsobeno tím, že je Etherpad v některých prohlížečích zahrnut do iFrame. Zkontrolujte, zda je Etherpad ve stejné subdoméně / doméně jako nadřazený iFrame", "pad.permissionDenied": "Nemáte oprávnění pro přístup k tomuto Padu", "pad.settings.padSettings": "Nastavení Padu", "pad.settings.myView": "Vlastní pohled", @@ -47,6 +82,8 @@ "pad.settings.fontType": "Typ písma:", "pad.settings.fontType.normal": "Normální", "pad.settings.language": "Jazyk:", + "pad.settings.about": "O projektu", + "pad.settings.poweredBy": "Běží na", "pad.importExport.import_export": "Import/Export", "pad.importExport.import": "Nahrát libovolný textový soubor nebo dokument", "pad.importExport.importSuccessful": "Úspěšně!", @@ -57,9 +94,9 @@ "pad.importExport.exportword": "Microsoft Word", "pad.importExport.exportpdf": "PDF", "pad.importExport.exportopen": "ODF (Open Document Format)", - "pad.importExport.abiword.innerHTML": "Importovat můžeš pouze prostý text nebo HTML formátování. Pro pokročilejší funkce importu, prosím, nainstaluj „AbiWord“.", + "pad.importExport.abiword.innerHTML": "Importovat lze pouze z formátů prostého textu nebo HTML. Pokročilejší funkce pro import naleznete v instalaci AbiWord nebo LibreOffice.", "pad.modals.connected": "Připojeno.", - "pad.modals.reconnecting": "Znovupřipojování k Padu…", + "pad.modals.reconnecting": "Opětovné připojení k Padu...", "pad.modals.forcereconnect": "Vynutit znovupřipojení", "pad.modals.reconnecttimer": "Zkouším se znovu připojit", "pad.modals.cancel": "Zrušit", @@ -81,6 +118,10 @@ "pad.modals.corruptPad.cause": "To může být kvůli špatné konfiguraci serveru, nebo kvůli jinému neočekávanému chování. Kontaktujte prosím správce služby.", "pad.modals.deleted": "Odstraněno.", "pad.modals.deleted.explanation": "Tento Pad byl odebrán.", + "pad.modals.rateLimited": "Rychlost je omezená.", + "pad.modals.rateLimited.explanation": "Na tento Pad jste poslali příliš mnoho zpráv, takže vás odpojil.", + "pad.modals.rejected.explanation": "Server odmítl zprávu odeslanou vaším prohlížečem.", + "pad.modals.rejected.cause": "Server mohl být aktualizován, když jste sledovali podložku, nebo možná došlo k chybě v Etherpadu. Zkuste stránku znovu načíst.", "pad.modals.disconnected": "Byl jste odpojen.", "pad.modals.disconnected.explanation": "Připojení k serveru bylo přerušeno", "pad.modals.disconnected.cause": "Server může být nedostupný. Upozorněte administrátora služby, pokud se to bude opakovat.", @@ -93,6 +134,7 @@ "pad.chat.loadmessages": "Načíst více zpráv", "pad.chat.stick.title": "Přichytit chat k obrazovce", "pad.chat.writeMessage.placeholder": "Zde napište zprávu", + "timeslider.followContents": "Sledovat aktualizace obsahu Padu", "timeslider.pageTitle": "Časová osa {{appTitle}}", "timeslider.toolbar.returnbutton": "Návrat do Padu", "timeslider.toolbar.authors": "Autoři:", @@ -131,5 +173,6 @@ "pad.impexp.uploadFailed": "Nahrávání selhalo, zkuste to znovu", "pad.impexp.importfailed": "Import selhal", "pad.impexp.copypaste": "Vložte prosím kopii", - "pad.impexp.exportdisabled": "Export do formátu {{type}} je zakázán. Kontaktujte svého administrátora pro zjištění detailů." + "pad.impexp.exportdisabled": "Export do formátu {{type}} je zakázán. Kontaktujte svého administrátora pro zjištění detailů.", + "pad.impexp.maxFileSize": "Soubor je příliš velký. Požádejte svého správce webu o zvýšení povolené velikosti souboru pro import" } diff --git a/src/locales/de.json b/src/locales/de.json index 781abe099..439e9c9e3 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -11,24 +11,42 @@ "Sebastian Wallroth", "Thargon", "Tim.krieger", - "Wikinaut" + "Wikinaut", + "Zunkelty" ] }, + "admin.page-title": "Admin Dashboard - Etherpad", "admin_plugins": "Plugins verwalten", "admin_plugins.available": "Verfügbare Plugins", "admin_plugins.available_not-found": "Keine Plugins gefunden.", + "admin_plugins.available_fetching": "Wird abgerufen...", "admin_plugins.available_install.value": "Installieren", + "admin_plugins.available_search.placeholder": "Suche nach Plugins zum Installieren", "admin_plugins.description": "Beschreibung", + "admin_plugins.installed": "Installierte Plugins", + "admin_plugins.installed_fetching": "Rufe installierte Plugins ab...", "admin_plugins.installed_nothing": "Du hast bisher noch keine Plugins installiert.", + "admin_plugins.installed_uninstall.value": "Deinstallieren", "admin_plugins.last-update": "Letze Aktualisierung", "admin_plugins.name": "Name", + "admin_plugins.page-title": "Plugin Manager - Etherpad", "admin_plugins.version": "Version", + "admin_plugins_info": "Hilfestellung", "admin_plugins_info.hooks": "Installierte Hooks", + "admin_plugins_info.hooks_client": "Client-seitige Hooks", + "admin_plugins_info.hooks_server": "Server-seitige Hooks", + "admin_plugins_info.parts": "Installierte Teile", "admin_plugins_info.plugins": "Installierte Plugins", + "admin_plugins_info.page-title": "Plugin Informationen - Etherpad", + "admin_plugins_info.version": "Etherpad Version", + "admin_plugins_info.version_latest": "Neueste Version", "admin_plugins_info.version_number": "Versionsnummer", "admin_settings": "Einstellungen", "admin_settings.current": "Derzeitige Konfiguration", + "admin_settings.current_example-devel": "Beispielhafte Entwicklungseinstellungs-Templates", + "admin_settings.current_restart.value": "Etherpad neustarten", "admin_settings.current_save.value": "Einstellungen speichern", + "admin_settings.page-title": "Einstellungen - Etherpad", "index.newPad": "Neues Pad", "index.createOpenPad": "oder ein Pad mit folgendem Namen erstellen/öffnen:", "index.openPad": "Öffne ein vorhandenes Pad mit folgendem Namen:", @@ -78,7 +96,7 @@ "pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.abiword.innerHTML": "Du kannst nur aus reinen Text- oder HTML-Formaten importieren. Für umfangreichere Importfunktionen muss AbiWord oder LibreOffice auf dem Server installiert werden.", "pad.modals.connected": "Verbunden.", - "pad.modals.reconnecting": "Wiederherstellen der Verbindung …", + "pad.modals.reconnecting": "Dein Pad wird neu verbunden...", "pad.modals.forcereconnect": "Erneutes Verbinden erzwingen", "pad.modals.reconnecttimer": "Versuche Neuverbindung in", "pad.modals.cancel": "Abbrechen", @@ -102,6 +120,7 @@ "pad.modals.deleted.explanation": "Dieses Pad wurde entfernt.", "pad.modals.rateLimited": "Begrenzte Rate.", "pad.modals.rateLimited.explanation": "Sie haben zu viele Nachrichten an dieses Pad gesendet, so dass die Verbindung unterbrochen wurde.", + "pad.modals.rejected.explanation": "Der Server hat eine Nachricht abgelehnt, die von deinem Browser gesendet wurde.", "pad.modals.disconnected": "Ihre Verbindung wurde getrennt.", "pad.modals.disconnected.explanation": "Die Verbindung zum Server wurde unterbrochen.", "pad.modals.disconnected.cause": "Möglicherweise ist der Server nicht erreichbar. Bitte benachrichtige den Dienstadministrator, falls dies weiterhin passiert.", diff --git a/src/locales/diq.json b/src/locales/diq.json index 0c8c31f98..ef7275712 100644 --- a/src/locales/diq.json +++ b/src/locales/diq.json @@ -11,7 +11,7 @@ ] }, "admin.page-title": "Panoyê İdarekari - Etherpad", - "admin_plugins": "İdarekarê Dekerdeki", + "admin_plugins": "Gıredayışê raverberi", "admin_plugins.available": "Mewcud Dekerdeki", "admin_plugins.available_not-found": "Dekerdek nevineya", "admin_plugins.available_fetching": "Aniyeno...", @@ -132,15 +132,15 @@ "pad.chat.writeMessage.placeholder": "Mesacê xo tiya bınusne", "timeslider.followContents": "Rocaney zerrekê padi taqib bıkerê", "timeslider.pageTitle": "Ğızagê zemani {{appTitle}}", - "timeslider.toolbar.returnbutton": "Peyser şo ped", + "timeslider.toolbar.returnbutton": "Peyser şo bloknot", "timeslider.toolbar.authors": "Nuştoği:", - "timeslider.toolbar.authorsList": "Nuştoği çıniyê", + "timeslider.toolbar.authorsList": "Nuştekari çıniyê", "timeslider.toolbar.exportlink.title": "Teberdayış", "timeslider.exportCurrent": "Versiyonê enewki teber de:", "timeslider.version": "Versiyonê {{version}}", "timeslider.saved": "{{day}} {{month}}, {{year}} de biyo qeyd", "timeslider.playPause": "Zerrekê bloknoti kayfi/vındarn", - "timeslider.backRevision": "Peyser şo revizyona ena bloknoter", + "timeslider.backRevision": "Peyser şo çımraviyarnayışê na bloknoti", "timeslider.forwardRevision": "Ena bloknot de şo revizyonê bini", "timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}", "timeslider.month.january": "Çele", diff --git a/src/locales/eu.json b/src/locales/eu.json index c3205baa8..0797cf742 100644 --- a/src/locales/eu.json +++ b/src/locales/eu.json @@ -27,6 +27,10 @@ "admin_plugins.page-title": "Plugin-en kudeaketa - Etherpad", "admin_plugins.version": "Bertsioa", "admin_plugins_info": "Arazoak konpontzeko informazioa", + "admin_plugins_info.hooks": "Instalatutako kakoak", + "admin_plugins_info.hooks_client": "Bezeroaren aldeko kakoak", + "admin_plugins_info.hooks_server": "Zerbitzari aldeko kakoak", + "admin_plugins_info.parts": "Instalatutako atalaka", "admin_plugins_info.plugins": "Instalatutako plugin-ak", "admin_plugins_info.page-title": "Plugin-en informazioa - Etherpad", "admin_plugins_info.version": "Etherpad bertsioa", @@ -34,6 +38,8 @@ "admin_plugins_info.version_number": "Bertsio-zenbakia", "admin_settings": "Ezarpenak", "admin_settings.current": "Oraingo konfigurazioa", + "admin_settings.current_example-devel": "Adibiderako garapenerako ezarpenen txantiloia", + "admin_settings.current_example-prod": "Adibiderako lanerako ezarpenen txantiloia", "admin_settings.current_restart.value": "Berrabiarazi Etherpad", "admin_settings.current_save.value": "Gorde Ezarpenak", "admin_settings.page-title": "Ezarpenak - Etherpad", @@ -108,8 +114,10 @@ "pad.modals.corruptPad.cause": "Baliteke zerbitzari okerreko konfigurazioagatik edo beste ustekabeko portaera batengatik izatea. Jarri harremanetan zerbitzu-administratzailearekin.", "pad.modals.deleted": "Ezabatua.", "pad.modals.deleted.explanation": "Pad hau ezabatu da.", + "pad.modals.rateLimited": "Baloratzea Mugatuta.", "pad.modals.rateLimited.explanation": "Pad honetara mezu gehiegi bidali dituzu eta ondorioz deskonektatu zaizu.", "pad.modals.rejected.explanation": "Zerbitzariak zure nabigatzailetik bidali den mezu bat baztertu du.", + "pad.modals.rejected.cause": "Baliteke pad-a ikusten ari zinen bitartean zerbitzaria eguneratu izana, edo bestela Etherpad-en arazo bat egon liteke. Orria freskatzen saiatu zaitez.", "pad.modals.disconnected": "Deskonektatua izan zara.", "pad.modals.disconnected.explanation": "Zerbitzariarekiko konexioa galdu da", "pad.modals.disconnected.cause": "Baliteke zerbitzaria eskuragarri ez egotea. Mesedez, jakinarazi zerbitzuko administratzaileari honek gertatzen jarraitzen badu.", @@ -122,6 +130,7 @@ "pad.chat.loadmessages": "Kargatu mezu gehiago", "pad.chat.stick.title": "Itsatsi txata pantailan", "pad.chat.writeMessage.placeholder": "Idatzi hemen zure mezua", + "timeslider.followContents": "Jarraitu pad-aren edukien eguneratzeak", "timeslider.pageTitle": "{{appTitle}} Denbora-lerroa", "timeslider.toolbar.returnbutton": "Itzuli pad-era", "timeslider.toolbar.authors": "Egileak:", diff --git a/src/locales/fi.json b/src/locales/fi.json index cdba94ec9..9f569d44b 100644 --- a/src/locales/fi.json +++ b/src/locales/fi.json @@ -18,6 +18,8 @@ "VezonThunder" ] }, + "admin_plugins.available": "Saatavilla olevat liitännäiset", + "admin_plugins.available_install.value": "Lataa", "admin_plugins.available_search.placeholder": "Etsi asennettavia laajennuksia", "admin_plugins.description": "Kuvaus", "admin_plugins.installed": "Asennetut laajennukset", @@ -41,6 +43,8 @@ "admin_settings": "Asetukset", "admin_settings.current": "Nykyinen kokoonpano", "admin_settings.current_example-devel": "Esimerkki kehitysasetusten mallista", + "admin_settings.current_save.value": "Tallenna Asetukset", + "admin_settings.page-title": "asetukset - Etherpad", "index.newPad": "Uusi muistio", "index.createOpenPad": "tai luo tai avaa muistio nimellä:", "pad.toolbar.bold.title": "Lihavointi (Ctrl-B)", @@ -89,7 +93,7 @@ "pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.abiword.innerHTML": "Tuonti on tuettu vain HTML- ja raakatekstitiedostoista. Monipuoliset tuontiominaisuudet ovat käytettävissä asentamalla AbiWordin tai LibreOfficen.", "pad.modals.connected": "Yhdistetty.", - "pad.modals.reconnecting": "Muodostetaan yhteyttä muistioon uudelleen...", + "pad.modals.reconnecting": "Muodostetaan yhteyttä muistioon uudelleen…", "pad.modals.forcereconnect": "Pakota yhdistämään uudelleen", "pad.modals.reconnecttimer": "Yritetään yhdistää uudelleen", "pad.modals.cancel": "Peruuta", diff --git a/src/locales/fr.json b/src/locales/fr.json index 68c03225b..f72a887ce 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -30,29 +30,29 @@ ] }, "admin.page-title": "Tableau de bord administrateur — Etherpad", - "admin_plugins": "Gestionnaire de compléments", - "admin_plugins.available": "Compléments disponibles", - "admin_plugins.available_not-found": "Aucun complément trouvé.", - "admin_plugins.available_fetching": "Récupération…", + "admin_plugins": "Gestionnaire de greffons", + "admin_plugins.available": "Greffons disponibles", + "admin_plugins.available_not-found": "Aucun greffon trouvé.", + "admin_plugins.available_fetching": "Récupération en cours...", "admin_plugins.available_install.value": "Installer", - "admin_plugins.available_search.placeholder": "Rechercher des compléments à installer", + "admin_plugins.available_search.placeholder": "Rechercher des greffons à installer", "admin_plugins.description": "Description", - "admin_plugins.installed": "Compléments installés", - "admin_plugins.installed_fetching": "Récupération des compléments installés…", - "admin_plugins.installed_nothing": "Vous n’avez pas encore installé de complément.", + "admin_plugins.installed": "Greffons installés", + "admin_plugins.installed_fetching": "Récupération des greffons installés en cours...", + "admin_plugins.installed_nothing": "Vous n’avez encore installé aucun greffon.", "admin_plugins.installed_uninstall.value": "Désinstaller", "admin_plugins.last-update": "Dernière mise à jour", "admin_plugins.name": "Nom", - "admin_plugins.page-title": "Gestionnaire de compléments — Etherpad", + "admin_plugins.page-title": "Gestionnaire de greffons — Etherpad", "admin_plugins.version": "Version", - "admin_plugins_info": "Information de résolution de problème", + "admin_plugins_info": "Informations de résolution de problème", "admin_plugins_info.hooks": "Crochets installés", "admin_plugins_info.hooks_client": "Crochets côté client", "admin_plugins_info.hooks_server": "Crochets côté serveur", "admin_plugins_info.parts": "Parties installées", - "admin_plugins_info.plugins": "Compléments installés", - "admin_plugins_info.page-title": "Information de complément — Etherpad", - "admin_plugins_info.version": "Version Etherpad", + "admin_plugins_info.plugins": "Greffons installés", + "admin_plugins_info.page-title": "Informations du greffon — Etherpad", + "admin_plugins_info.version": "Version d’Etherpad", "admin_plugins_info.version_latest": "Dernière version disponible", "admin_plugins_info.version_number": "Numéro de version", "admin_settings": "Paramètres", @@ -64,7 +64,7 @@ "admin_settings.page-title": "Paramètres — Etherpad", "index.newPad": "Nouveau bloc-notes", "index.createOpenPad": "ou créer/ouvrir un bloc-notes intitulé :", - "index.openPad": "ouvrir un Pad existant avec le nom :", + "index.openPad": "ouvrir un bloc-note existant avec le nom :", "pad.toolbar.bold.title": "Gras (Ctrl+B)", "pad.toolbar.italic.title": "Italique (Ctrl+I)", "pad.toolbar.underline.title": "Souligné (Ctrl+U)", @@ -76,7 +76,7 @@ "pad.toolbar.undo.title": "Annuler (Ctrl+Z)", "pad.toolbar.redo.title": "Rétablir (Ctrl+Y)", "pad.toolbar.clearAuthorship.title": "Effacer le surlignage par auteur (Ctrl+Shift+C)", - "pad.toolbar.import_export.title": "Importer de/Exporter vers un format de fichier différent", + "pad.toolbar.import_export.title": "Importer/Exporter des formats de fichiers différents", "pad.toolbar.timeslider.title": "Historique dynamique", "pad.toolbar.savedRevision.title": "Enregistrer la révision", "pad.toolbar.settings.title": "Paramètres", @@ -85,7 +85,7 @@ "pad.colorpicker.save": "Enregistrer", "pad.colorpicker.cancel": "Annuler", "pad.loading": "Chargement...", - "pad.noCookie": "Un cookie n’a pas pu être trouvé. Veuillez autoriser les fichiers témoins (ou cookies) dans votre navigateur ! Votre session et vos paramètres ne seront pas enregistrés entre les visites. Cela peut être dû au fait qu’Etehrpad est inclus dans un iFrame dans certains navigateurs. Veuillez vous assurer que Etherpad est dans le même sous-domaine/domaine que son iFrame parent", + "pad.noCookie": "Un fichier témoin (ou ''cookie'') n’a pas pu être trouvé. Veuillez autoriser les fichiers témoins dans votre navigateur ! Votre session et vos paramètres ne seront pas enregistrés entre les visites. Cela peut être dû au fait qu’Etherpad est inclus dans un ''iFrame'' dans certains navigateurs. Veuillez vous assurer qu’Etherpad est dans le même sous-domaine/domaine que son ''iFrame'' parent.", "pad.permissionDenied": "Vous n’êtes pas autorisé à accéder à ce bloc-notes", "pad.settings.padSettings": "Paramètres du bloc-notes", "pad.settings.myView": "Ma vue", @@ -94,11 +94,11 @@ "pad.settings.colorcheck": "Surlignage par auteur", "pad.settings.linenocheck": "Numéros de lignes", "pad.settings.rtlcheck": "Le contenu doit-il être lu de droite à gauche ?", - "pad.settings.fontType": "Police :", + "pad.settings.fontType": "Type de police :", "pad.settings.fontType.normal": "Normal", "pad.settings.language": "Langue :", "pad.settings.about": "À propos", - "pad.settings.poweredBy": "Fourni par", + "pad.settings.poweredBy": "Propulsé par", "pad.importExport.import_export": "Importer/Exporter", "pad.importExport.import": "Charger un texte ou un document", "pad.importExport.importSuccessful": "Réussi !", @@ -111,13 +111,13 @@ "pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.abiword.innerHTML": "Vous ne pouvez importer que des formats texte brut ou HTML. Pour des fonctionnalités d’importation plus évoluées, veuillez installer AbiWord ou LibreOffice.", "pad.modals.connected": "Connecté.", - "pad.modals.reconnecting": "Reconnexion à votre bloc-notes...", + "pad.modals.reconnecting": "Reconnexion à votre bloc-notes en cours...", "pad.modals.forcereconnect": "Forcer la reconnexion", "pad.modals.reconnecttimer": "Essai de reconnexion", "pad.modals.cancel": "Annuler", "pad.modals.userdup": "Ouvert dans une autre fenêtre", "pad.modals.userdup.explanation": "Ce bloc-notes semble être ouvert dans plusieurs fenêtres sur cet ordinateur.", - "pad.modals.userdup.advice": "Se reconnecter en utilisant cette fenêtre.", + "pad.modals.userdup.advice": "Se reconnecter en utilisant plutôt cette fenêtre.", "pad.modals.unauth": "Non autorisé", "pad.modals.unauth.explanation": "Vos autorisations ont été changées lors de l’affichage de cette page. Essayez de vous reconnecter.", "pad.modals.looping.explanation": "Nous éprouvons des problèmes de communication au serveur de synchronisation.", @@ -133,10 +133,10 @@ "pad.modals.corruptPad.cause": "Cela peut être dû à une mauvaise configuration du serveur ou à un autre comportement inattendu. Veuillez contacter l’administrateur du service.", "pad.modals.deleted": "Supprimé.", "pad.modals.deleted.explanation": "Ce bloc-notes a été supprimé.", - "pad.modals.rateLimited": "Taux limité.", - "pad.modals.rateLimited.explanation": "Vous avez envoyé trop de messages à ce bloc, il vous a donc déconnecté.", + "pad.modals.rateLimited": "Flot limité.", + "pad.modals.rateLimited.explanation": "Vous avez envoyé trop de messages à ce bloc-notes, il vous a donc déconnecté.", "pad.modals.rejected.explanation": "Le serveur a rejeté un message qui a été envoyé par votre navigateur.", - "pad.modals.rejected.cause": "Le serveur peut avoir été mis à jour pendant que vous regardiez le bloc, ou il y a peut-être un bogue dans Etherpad. Essayez de recharger la page.", + "pad.modals.rejected.cause": "Le serveur peut avoir été mis à jour pendant que vous regardiez le bloc-notes, ou il y a peut-être une anomalie dans Etherpad. Essayez de recharger la page.", "pad.modals.disconnected": "Vous avez été déconnecté.", "pad.modals.disconnected.explanation": "La connexion au serveur a échoué.", "pad.modals.disconnected.cause": "Il se peut que le serveur soit indisponible. Si le problème persiste, veuillez en informer l’administrateur du service.", @@ -149,7 +149,7 @@ "pad.chat.loadmessages": "Charger davantage de messages", "pad.chat.stick.title": "Ancrer la discussion sur l’écran", "pad.chat.writeMessage.placeholder": "Entrez votre message ici", - "timeslider.followContents": "Suivre les mises à jour de contenu du bloc", + "timeslider.followContents": "Suivre les mises à jour de contenu du bloc-notes", "timeslider.pageTitle": "Historique dynamique de {{appTitle}}", "timeslider.toolbar.returnbutton": "Retourner au bloc-notes", "timeslider.toolbar.authors": "Auteurs :", @@ -158,7 +158,7 @@ "timeslider.exportCurrent": "Exporter la version actuelle sous :", "timeslider.version": "Version {{version}}", "timeslider.saved": "Enregistré le {{day}} {{month}} {{year}}", - "timeslider.playPause": "Lecture / Pause des contenus du bloc-notes", + "timeslider.playPause": "Lecture / Pause des contenus du bloc-notes", "timeslider.backRevision": "Reculer d’une révision dans ce bloc-notes", "timeslider.forwardRevision": "Avancer d’une révision dans ce bloc-notes", "timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}", @@ -184,10 +184,10 @@ "pad.impexp.importing": "Import en cours...", "pad.impexp.confirmimport": "Importer un fichier écrasera le contenu actuel du bloc-notes. Êtes-vous sûr de vouloir le faire ?", "pad.impexp.convertFailed": "Nous ne pouvons pas importer ce fichier. Veuillez utiliser un autre format de document ou faire manuellement un copier/coller du texte brut", - "pad.impexp.padHasData": "Nous n’avons pas pu importer ce fichier parce que ce bloc-notes a déjà été modifié ; veuillez l’importer vers un nouveau bloc-notes", - "pad.impexp.uploadFailed": "Le téléversement a échoué, veuillez réessayer", - "pad.impexp.importfailed": "Échec de l’importation", + "pad.impexp.padHasData": "Nous n’avons pas pu importer ce fichier parce que ce bloc-notes a déjà été modifié ; veuillez l’importer vers un nouveau bloc-notes.", + "pad.impexp.uploadFailed": "Le téléversement a échoué, veuillez réessayer.", + "pad.impexp.importfailed": "Échec de l’import", "pad.impexp.copypaste": "Veuillez copier-coller", "pad.impexp.exportdisabled": "L’exportation au format {{type}} est désactivée. Veuillez contacter votre administrateur système pour plus de détails.", - "pad.impexp.maxFileSize": "Fichier trop gros. Contactez votre administrateur de site pour augmenter la taille maximale des fichiers importés" + "pad.impexp.maxFileSize": "Fichier trop gros. Contactez votre administrateur de site pour augmenter la taille maximale des fichiers importés." } diff --git a/src/locales/fy.json b/src/locales/fy.json index ed4d605f7..093b40e32 100644 --- a/src/locales/fy.json +++ b/src/locales/fy.json @@ -35,8 +35,5 @@ "timeslider.month.october": "oktober", "timeslider.month.november": "novimber", "timeslider.month.december": "desimber", - "pad.userlist.unnamed": "sûnder namme", - "pad.userlist.guest": "Gast", - "pad.userlist.deny": "Wegerje", - "pad.userlist.approve": "Goedkarre" + "pad.userlist.unnamed": "sûnder namme" } diff --git a/src/locales/gl.json b/src/locales/gl.json index 406c99521..02a04f563 100644 --- a/src/locales/gl.json +++ b/src/locales/gl.json @@ -2,12 +2,47 @@ "@metadata": { "authors": [ "Elisardojm", + "Ghose", "Toliño" ] }, + "admin.page-title": "Panel de administración - Etherpad", + "admin_plugins": "Xestor de complementos", + "admin_plugins.available": "Complementos dispoñibles", + "admin_plugins.available_not-found": "Non se atopan complementos.", + "admin_plugins.available_fetching": "Obtendo...", + "admin_plugins.available_install.value": "Instalar", + "admin_plugins.available_search.placeholder": "Buscar complementos para instalar", + "admin_plugins.description": "Descrición", + "admin_plugins.installed": "Complementos instalados", + "admin_plugins.installed_fetching": "Obtendo os complementos instalados...", + "admin_plugins.installed_nothing": "Aínda non instalaches ningún complemento.", + "admin_plugins.installed_uninstall.value": "Desinstalar", + "admin_plugins.last-update": "Última actualización", + "admin_plugins.name": "Nome", + "admin_plugins.page-title": "Xestos de complementos - Etherpad", + "admin_plugins.version": "Versión", + "admin_plugins_info": "Información para resolver problemas", + "admin_plugins_info.hooks": "Ganchos instalados", + "admin_plugins_info.hooks_client": "Ganchos do lado do cliente", + "admin_plugins_info.hooks_server": "Ganchos do lado do servidor", + "admin_plugins_info.parts": "Partes instaladas", + "admin_plugins_info.plugins": "Complementos instalados", + "admin_plugins_info.page-title": "Información do complemento - Etherpad", + "admin_plugins_info.version": "Versión de Etherpad", + "admin_plugins_info.version_latest": "Última versión dispoñible", + "admin_plugins_info.version_number": "Número da versión", + "admin_settings": "Axustes", + "admin_settings.current": "Configuración actual", + "admin_settings.current_example-devel": "Modelo de exemplo dos axustes de desenvolvemento", + "admin_settings.current_example-prod": "Modelo de exemplo dos axustes en produción", + "admin_settings.current_restart.value": "Reiniciar Etherpad", + "admin_settings.current_save.value": "Gardar axustes", + "admin_settings.page-title": "Axustes - Etherpad", "index.newPad": "Novo documento", - "index.createOpenPad": "ou cree/abra un documento co nome:", - "pad.toolbar.bold.title": "Negra (Ctrl-B)", + "index.createOpenPad": "ou crea/abre un documento co nome:", + "index.openPad": "abrir un Pad existente co nome:", + "pad.toolbar.bold.title": "Resaltado (Ctrl-B)", "pad.toolbar.italic.title": "Cursiva (Ctrl-I)", "pad.toolbar.underline.title": "Subliñar (Ctrl-U)", "pad.toolbar.strikethrough.title": "Riscar (Ctrl+5)", @@ -17,28 +52,30 @@ "pad.toolbar.unindent.title": "Sen sangría (Maiús.+TAB)", "pad.toolbar.undo.title": "Desfacer (Ctrl-Z)", "pad.toolbar.redo.title": "Refacer (Ctrl-Y)", - "pad.toolbar.clearAuthorship.title": "Limpar as cores de identificación dos autores (Ctrl+Shift+C)", + "pad.toolbar.clearAuthorship.title": "Eliminar as cores que identifican ás autoras (Ctrl+Shift+C)", "pad.toolbar.import_export.title": "Importar/Exportar desde/a diferentes formatos de ficheiro", "pad.toolbar.timeslider.title": "Liña do tempo", "pad.toolbar.savedRevision.title": "Gardar a revisión", - "pad.toolbar.settings.title": "Configuracións", + "pad.toolbar.settings.title": "Axustes", "pad.toolbar.embed.title": "Compartir e incorporar este documento", - "pad.toolbar.showusers.title": "Mostrar os usuarios deste documento", + "pad.toolbar.showusers.title": "Mostrar as usuarias deste documento", "pad.colorpicker.save": "Gardar", "pad.colorpicker.cancel": "Cancelar", "pad.loading": "Cargando...", - "pad.noCookie": "Non se puido atopar a cookie. Por favor, habilite as cookies no seu navegador!", - "pad.permissionDenied": "Non ten permiso para acceder a este documento", + "pad.noCookie": "Non se puido atopar a cookie. Por favor, habilita as cookies no teu navegador! A túa sesión e axustes non se gardarán entre visitas. Esto podería deberse a que Etherpad está incluído nalgún iFrame nalgúns navegadores. Asegúrate de que Etherpad está no mesmo subdominio/dominio que o iFrame pai", + "pad.permissionDenied": "Non tes permiso para acceder a este documento", "pad.settings.padSettings": "Configuracións do documento", "pad.settings.myView": "A miña vista", "pad.settings.stickychat": "Chat sempre visible", "pad.settings.chatandusers": "Mostrar o chat e os usuarios", "pad.settings.colorcheck": "Cores de identificación", "pad.settings.linenocheck": "Números de liña", - "pad.settings.rtlcheck": "Quere ler o contido da dereita á esquerda?", + "pad.settings.rtlcheck": "Queres ler o contido da dereita á esquerda?", "pad.settings.fontType": "Tipo de letra:", "pad.settings.fontType.normal": "Normal", "pad.settings.language": "Lingua:", + "pad.settings.about": "Acerca de", + "pad.settings.poweredBy": "Grazas a", "pad.importExport.import_export": "Importar/Exportar", "pad.importExport.import": "Cargar un ficheiro de texto ou documento", "pad.importExport.importSuccessful": "Correcto!", @@ -49,9 +86,9 @@ "pad.importExport.exportword": "Microsoft Word", "pad.importExport.exportpdf": "PDF", "pad.importExport.exportopen": "ODF (Open Document Format)", - "pad.importExport.abiword.innerHTML": "Só pode importar texto simple ou formatos HTML. Para obter máis información sobre as características de importación avanzadas instale AbiWord.", + "pad.importExport.abiword.innerHTML": "Só podes importar texto simple ou formatos HTML. Para obter máis información sobre as características de importación avanzadas instala AbiWord.", "pad.modals.connected": "Conectado.", - "pad.modals.reconnecting": "Reconectando co seu documento...", + "pad.modals.reconnecting": "Reconectando co teu documento...", "pad.modals.forcereconnect": "Forzar a reconexión", "pad.modals.reconnecttimer": "Intentarase reconectar en", "pad.modals.cancel": "Cancelar", @@ -73,6 +110,10 @@ "pad.modals.corruptPad.cause": "Isto pode deberse a unha cofiguración errónea do servidor ou algún outro comportamento inesperado. Póñase en contacto co administrador do servizo.", "pad.modals.deleted": "Borrado.", "pad.modals.deleted.explanation": "Este documento foi eliminado.", + "pad.modals.rateLimited": "Taxa limitada.", + "pad.modals.rateLimited.explanation": "Enviaches demasiadas mensaxes a este documento polo que te desconectamos.", + "pad.modals.rejected.explanation": "O servidor rexeitou unha mensaxe que o teu navegador enviou.", + "pad.modals.rejected.cause": "O servidor podería ter sido actualizado mentras ollabas o documento, ou pode que sexa un fallo de Etherpad. Intenta recargar a páxina.", "pad.modals.disconnected": "Foi desconectado.", "pad.modals.disconnected.explanation": "Perdeuse a conexión co servidor", "pad.modals.disconnected.cause": "O servidor non está dispoñible. Póñase en contacto co administrador do servizo se o problema continúa.", @@ -83,6 +124,9 @@ "pad.chat": "Chat", "pad.chat.title": "Abrir o chat deste documento.", "pad.chat.loadmessages": "Cargar máis mensaxes", + "pad.chat.stick.title": "Pegar a conversa á pantalla", + "pad.chat.writeMessage.placeholder": "Escribe aquí a túa mensaxe", + "timeslider.followContents": "Segue as actualizacións do contido", "timeslider.pageTitle": "Liña do tempo de {{appTitle}}", "timeslider.toolbar.returnbutton": "Volver ao documento", "timeslider.toolbar.authors": "Autores:", @@ -112,7 +156,7 @@ "pad.savedrevs.timeslider": "Pode consultar as revisións gardadas visitando a liña do tempo", "pad.userlist.entername": "Insira o seu nome", "pad.userlist.unnamed": "anónimo", - "pad.editbar.clearcolors": "Quere limpar as cores de identificación dos autores en todo o documento?", + "pad.editbar.clearcolors": "Eliminar as cores relativas aos autores en todo o documento? Non se poderán recuperar", "pad.impexp.importbutton": "Importar agora", "pad.impexp.importing": "Importando...", "pad.impexp.confirmimport": "A importación dun ficheiro ha sobrescribir o texto actual do documento. Está seguro de querer continuar?", @@ -121,5 +165,6 @@ "pad.impexp.uploadFailed": "Houbo un erro ao cargar o ficheiro; inténteo de novo", "pad.impexp.importfailed": "Fallou a importación", "pad.impexp.copypaste": "Copie e pegue", - "pad.impexp.exportdisabled": "A exportación en formato {{type}} está desactivada. Póñase en contacto co administrador do sistema se quere máis detalles." + "pad.impexp.exportdisabled": "A exportación en formato {{type}} está desactivada. Póñase en contacto co administrador do sistema se quere máis detalles.", + "pad.impexp.maxFileSize": "Ficheiro demasiado granda. Contacta coa administración para aumentar o tamaño permitido para importacións" } diff --git a/src/locales/gu.json b/src/locales/gu.json index 894a5e3f8..b3a91d42d 100644 --- a/src/locales/gu.json +++ b/src/locales/gu.json @@ -45,9 +45,6 @@ "timeslider.month.december": "ડિસેમ્બર", "pad.userlist.entername": "તમારું નામ દાખલ કરો", "pad.userlist.unnamed": "અનામી", - "pad.userlist.guest": "મહેમાન", - "pad.userlist.deny": "નકારો", - "pad.userlist.approve": "મંજૂર", "pad.impexp.importbutton": "આયાત કરો", "pad.impexp.importing": "આયાત કરે છે..." } diff --git a/src/locales/hi.json b/src/locales/hi.json index c78d74d0d..b238694a5 100644 --- a/src/locales/hi.json +++ b/src/locales/hi.json @@ -31,7 +31,6 @@ "timeslider.month.october": "अक्टूबर", "timeslider.month.november": "नवम्बर", "timeslider.month.december": "दिसम्बर", - "pad.userlist.guest": "अतिथि", "pad.impexp.importbutton": "अभी आयात करें", "pad.impexp.importing": "आयात कर रहा...", "pad.impexp.importfailed": "आयात विफल हुआ", diff --git a/src/locales/it.json b/src/locales/it.json index 963b04982..0b3914b47 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -86,7 +86,7 @@ "pad.modals.disconnected.cause": "Il server potrebbe essere non disponibile. Informa l'amministrazione del servizio se il problema persiste.", "pad.share": "Condividi questo Pad", "pad.share.readonly": "Sola lettura", - "pad.share.link": "Link", + "pad.share.link": "Collegamento", "pad.share.emebdcode": "Incorpora URL", "pad.chat": "Chat", "pad.chat.title": "Apri la chat per questo Pad.", diff --git a/src/locales/ko.json b/src/locales/ko.json index 01857abf3..04c279a05 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -40,11 +40,14 @@ "admin_plugins_info.version_number": "버전 번호", "admin_settings": "설정", "admin_settings.current": "현재 구성", + "admin_settings.current_example-devel": "예시 개발용 설정 템플릿", + "admin_settings.current_example-prod": "예시 운영용 설정 템플릿", "admin_settings.current_restart.value": "이더패드 다시 시작", "admin_settings.current_save.value": "설정 저장", "admin_settings.page-title": "설정 - 이더패드", "index.newPad": "새 패드", "index.createOpenPad": "또는 다음 이름으로 패드 만들기/열기:", + "index.openPad": "이름으로 기존 패드 열기:", "pad.toolbar.bold.title": "굵게 (Ctrl+B)", "pad.toolbar.italic.title": "기울임꼴 (Ctrl+I)", "pad.toolbar.underline.title": "밑줄 (Ctrl+U)", @@ -65,7 +68,7 @@ "pad.colorpicker.save": "저장", "pad.colorpicker.cancel": "취소", "pad.loading": "불러오는 중...", - "pad.noCookie": "쿠키를 찾을 수 없습니다. 브라우저에서 쿠키를 허용해주세요!", + "pad.noCookie": "쿠키를 찾지 못했습니다. 브라우저에서 쿠키를 허용해 주십시오! 세션과 설정은 방문 간 저장되지 않습니다. 일부 브라우저의 iFrame에 이더패드가 포함된 것이 그 이유일 수 있습니다. 이더패드가 부모 iFrame과 동일한 서브도메인/도메인에 위치하는지 확인해 주십시오", "pad.permissionDenied": "이 패드에 접근할 권한이 없습니다", "pad.settings.padSettings": "패드 설정", "pad.settings.myView": "내 보기", @@ -113,7 +116,10 @@ "pad.modals.corruptPad.cause": "잘못된 서버 구성 또는 다른 예기치 않은 오류 때문에 발생했을 수 있습니다. 서버 관리자와 연락하세요.", "pad.modals.deleted": "삭제되었습니다.", "pad.modals.deleted.explanation": "이 패드를 제거했습니다.", + "pad.modals.rateLimited": "속도 제한됨.", + "pad.modals.rateLimited.explanation": "이 패드에 너무 많은 메시지를 송신하였으므로 연결을 해제했습니다.", "pad.modals.rejected.explanation": "브라우저가 보낸 메시지를 서버가 거부했습니다.", + "pad.modals.rejected.cause": "패드를 보는 동안 서버가 업데이트되었거나 이더패드의 버그일 수 있습니다. 페이지를 다시 로드해 보십시오.", "pad.modals.disconnected": "연결이 끊어졌습니다.", "pad.modals.disconnected.explanation": "서버에서 연결을 잃었습니다", "pad.modals.disconnected.cause": "서버를 사용할 수 없습니다. 이 문제가 계속 발생하면 서비스 관리자에게 알려주시기 바랍니다.", diff --git a/src/locales/krc.json b/src/locales/krc.json index 11d3b6b44..408b78630 100644 --- a/src/locales/krc.json +++ b/src/locales/krc.json @@ -34,6 +34,5 @@ "timeslider.month.october": "октябрь", "timeslider.month.november": "ноябрь", "timeslider.month.december": "декабрь", - "pad.userlist.guest": "Къонакъ", "pad.impexp.importing": "Импорт этиу…" } diff --git a/src/locales/pt-br.json b/src/locales/pt-br.json index f067466f6..0e6d9993a 100644 --- a/src/locales/pt-br.json +++ b/src/locales/pt-br.json @@ -4,6 +4,7 @@ "Cainamarques", "Dianakc", "Eduardo Addad de Oliveira", + "Eduardoaddad", "Fasouzafreitas", "Gusta", "Lpagliari", diff --git a/src/locales/pt.json b/src/locales/pt.json index d9faa3ba0..334c8bf37 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -4,6 +4,7 @@ "Athena in Wonderland", "Cainamarques", "GoEThe", + "Guilha", "Hamilton Abreu", "Imperadeiro98", "Luckas", @@ -16,9 +17,42 @@ "Waldyrious" ] }, + "admin.page-title": "Painel do administrador - Etherpad", + "admin_plugins": "Gestor de plugins", + "admin_plugins.available": "Plugins disponíveis", + "admin_plugins.available_not-found": "Não foram encontrados plugins.", + "admin_plugins.available_fetching": "A obter...", + "admin_plugins.available_install.value": "Instalar", + "admin_plugins.available_search.placeholder": "Procura plugins para instalar", + "admin_plugins.description": "Descrição", + "admin_plugins.installed": "Plugins instalados", + "admin_plugins.installed_fetching": "A obter plugins instalados...", + "admin_plugins.installed_nothing": "Não instalas-te nenhum plugin ainda.", + "admin_plugins.installed_uninstall.value": "Desinstalar", + "admin_plugins.last-update": "Ultima atualização", + "admin_plugins.name": "Nome", + "admin_plugins.page-title": "Gestor de plugins - Etherpad", + "admin_plugins.version": "Versão", + "admin_plugins_info": "Informação de resolução de problemas", + "admin_plugins_info.hooks": "Hooks instalados", + "admin_plugins_info.hooks_client": "Hooks do lado-do-cliente", + "admin_plugins_info.hooks_server": "Hooks do lado-do-servidor", + "admin_plugins_info.parts": "Partes instaladas", + "admin_plugins_info.plugins": "Plugins instalados", + "admin_plugins_info.page-title": "Informação do plugin - Etherpad", + "admin_plugins_info.version": "Versão do Etherpad", + "admin_plugins_info.version_latest": "Última versão disponível", + "admin_plugins_info.version_number": "Número de versão", + "admin_settings": "Definições", + "admin_settings.current": "Configuração atual", + "admin_settings.current_example-devel": "Exemplo do modo de Desenvolvedor", + "admin_settings.current_example-prod": "Exemplo do modo de Produção", + "admin_settings.current_restart.value": "Reiniciar Etherpad", + "admin_settings.current_save.value": "Guardar Definições", + "admin_settings.page-title": "Definições - Etherpad", "index.newPad": "Nova Nota", - "index.createOpenPad": "ou crie/abra uma nota com o nome:", - "index.openPad": "abrir uma «Nota» existente com o nome:", + "index.createOpenPad": "ou cria/abre uma nota com o nome:", + "index.openPad": "abrir uma Nota existente com o nome:", "pad.toolbar.bold.title": "Negrito (Ctrl+B)", "pad.toolbar.italic.title": "Itálico (Ctrl+I)", "pad.toolbar.underline.title": "Sublinhado (Ctrl+U)", @@ -65,7 +99,7 @@ "pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.abiword.innerHTML": "Só pode fazer importações de texto não formatado ou com formato HTML. Para funcionalidades de importação de texto mais avançadas, instale AbiWord ou LibreOffice, por favor.", "pad.modals.connected": "Ligado.", - "pad.modals.reconnecting": "A restabelecer ligação ao seu bloco…", + "pad.modals.reconnecting": "A restabelecer ligação à nota…", "pad.modals.forcereconnect": "Forçar restabelecimento de ligação", "pad.modals.reconnecttimer": "A tentar restabelecer ligação", "pad.modals.cancel": "Cancelar", @@ -89,6 +123,8 @@ "pad.modals.deleted.explanation": "Esta nota foi removida.", "pad.modals.rateLimited": "Limitado.", "pad.modals.rateLimited.explanation": "Enviou demasiadas mensagens para este pad, por isso foi desligado.", + "pad.modals.rejected.explanation": "O servidor rejeitou a mensagem que foi enviada pelo teu navegador.", + "pad.modals.rejected.cause": "O server foi atualizado enquanto estávas a ver esta nota, ou talvez seja apenas um bug do Etherpad. Tenta recarregar a página.", "pad.modals.disconnected": "Você foi desligado.", "pad.modals.disconnected.explanation": "A ligação ao servidor foi perdida", "pad.modals.disconnected.cause": "O servidor pode estar indisponível. Por favor, notifique o administrador de serviço se isto continuar a acontecer.", diff --git a/src/locales/ru.json b/src/locales/ru.json index 007fb4c51..792162e57 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -18,21 +18,39 @@ "Арсен Асхат" ] }, + "admin.page-title": "Панель администратора — Etherpad", "admin_plugins": "Менеджер плагинов", "admin_plugins.available": "Доступные плагины", "admin_plugins.available_not-found": "Плагины не найдены.", + "admin_plugins.available_fetching": "Получение…", "admin_plugins.available_install.value": "Установить", + "admin_plugins.available_search.placeholder": "Искать плагины для установки", "admin_plugins.description": "Описание", "admin_plugins.installed": "Установленные плагины", + "admin_plugins.installed_fetching": "Получение установленных плагинов…", "admin_plugins.installed_nothing": "Вы еще не установили ни одного плагина.", "admin_plugins.installed_uninstall.value": "Удалить", "admin_plugins.last-update": "Последнее обновление", + "admin_plugins.name": "Название", + "admin_plugins.page-title": "Менеджер плагинов — Etherpad", "admin_plugins.version": "Версия", + "admin_plugins_info": "Информация об устранении неполадок", "admin_plugins_info.hooks": "Установленные крючки", + "admin_plugins_info.hooks_client": "Клиентские хуки", + "admin_plugins_info.hooks_server": "Серверные хуки", + "admin_plugins_info.parts": "Установленные части", + "admin_plugins_info.plugins": "Установленные плагины", + "admin_plugins_info.page-title": "Информация о плагине — Etherpad", + "admin_plugins_info.version": "Версия Etherpad", + "admin_plugins_info.version_latest": "Последняя доступная версия", "admin_plugins_info.version_number": "Номер версии", "admin_settings": "Настройки", "admin_settings.current": "Текущая конфигурация", + "admin_settings.current_example-devel": "Пример шаблона настроек для среда разработки", + "admin_settings.current_example-prod": "Пример шаблона настроек для боевой среды", + "admin_settings.current_restart.value": "Перезагрузить Etherpad", "admin_settings.current_save.value": "Сохранить настройки", + "admin_settings.page-title": "Настройки — Etherpad", "index.newPad": "Создать", "index.createOpenPad": "или создать/открыть документ с именем:", "index.openPad": "откройте существующий документ с именем:", diff --git a/src/locales/sl.json b/src/locales/sl.json index cd0d7b979..be11d4a76 100644 --- a/src/locales/sl.json +++ b/src/locales/sl.json @@ -2,12 +2,17 @@ "@metadata": { "authors": [ "Dbc334", + "Eleassar", "HairyFotr", "Mateju", "Skalcaa", "Upwinxp" ] }, + "admin_plugins.last-update": "Zadnja posodobitev", + "admin_plugins.name": "Ime", + "admin_plugins.version": "Različica", + "admin_settings": "Nastavitve", "index.newPad": "Nov dokument", "index.createOpenPad": "ali pa ustvari/odpri dokument z imenom:", "pad.toolbar.bold.title": "Krepko (Ctrl-B)", @@ -33,15 +38,16 @@ "pad.noCookie": "Piškotka ni bilo mogoče najti. Prosimo, dovolite piškotke v vašem brskalniku!", "pad.permissionDenied": "Nimate dovoljenja za dostop do tega dokumenta.", "pad.settings.padSettings": "Nastavitve dokumenta", - "pad.settings.myView": "Moj pogled", + "pad.settings.myView": "Moj prikaz", "pad.settings.stickychat": "Vsebina klepeta je vedno na zaslonu", "pad.settings.chatandusers": "Prikaži klepet in uporabnike", "pad.settings.colorcheck": "Barve avtorstva", "pad.settings.linenocheck": "Številke vrstic", "pad.settings.rtlcheck": "Ali naj se vsebina prebira od desne proti levi?", "pad.settings.fontType": "Vrsta pisave:", - "pad.settings.fontType.normal": "Običajno", + "pad.settings.fontType.normal": "Normalno", "pad.settings.language": "Jezik:", + "pad.settings.poweredBy": "Omogoča", "pad.importExport.import_export": "Uvoz/Izvoz", "pad.importExport.import": "Naloži katerokoli besedilno datoteko ali dokument.", "pad.importExport.importSuccessful": "Opravilo je uspešno končano!", @@ -52,7 +58,7 @@ "pad.importExport.exportword": "DOC (zapis Microsoft Word)", "pad.importExport.exportpdf": "PDF (zapis Acrobat PDF)", "pad.importExport.exportopen": "ODF (zapis Open Document)", - "pad.importExport.abiword.innerHTML": "Uvoziti je mogoče le običajno neoblikovano besedilo in zapise HTML. Za naprednejše zmožnosti namestite program AbiWord.", + "pad.importExport.abiword.innerHTML": "Uvoziti je mogoče le neoblikovano besedilo in zapise HTML. Za naprednejše možnosti uvoza namestite program AbiWord.", "pad.modals.connected": "Povezano.", "pad.modals.reconnecting": "Poteka povezovanje z dokumentom ...", "pad.modals.forcereconnect": "Vsili ponovno povezavo", @@ -62,7 +68,7 @@ "pad.modals.userdup.explanation": "Videti je, da je ta dokument odprt v več kot enem oknu brskalnika na tem računalniku.", "pad.modals.userdup.advice": "Ponovno vzpostavite povezavo in uporabljajte to okno.", "pad.modals.unauth": "Nepooblaščen dostop", - "pad.modals.unauth.explanation": "Med pregledovanjem te strani so se dovoljenja za ogled spremenila. Poskusite se ponovno povezati.", + "pad.modals.unauth.explanation": "Med ogledovanjem strani so se dovoljenja za ogled spremenila. Poskusite se znova povezati.", "pad.modals.looping.explanation": "Zaznane so težave pri komunikaciji s strežnikom za usklajevanje.", "pad.modals.looping.cause": "Morda ste se povezali preko neustrezno nastavljenega požarnega zidu ali posredniškega strežnika.", "pad.modals.initsocketfail": "Strežnik je nedosegljiv.", @@ -75,12 +81,12 @@ "pad.modals.corruptPad.explanation": "Dokument, do katerega želite dostopati, je poškodovan.", "pad.modals.corruptPad.cause": "Razlog za to je morda napačna konfiguracija strežnika ali neko drugo nepričakovano vedenje. Prosimo, stopite v stik s skrbnikom storitve.", "pad.modals.deleted": "Izbrisano.", - "pad.modals.deleted.explanation": "Dokument je bil odstranjen.", + "pad.modals.deleted.explanation": "Dokument je odstranjen.", "pad.modals.disconnected": "Vaša povezava je bila prekinjena.", "pad.modals.disconnected.explanation": "Povezava s strežnikom je bila izgubljena.", "pad.modals.disconnected.cause": "Strežnik morda ni na voljo. Prosimo, obvestite skrbnika storitve, če se to zgodi večkrat.", "pad.share": "Določi souporabo dokumenta", - "pad.share.readonly": "Le za branje", + "pad.share.readonly": "Samo za branje", "pad.share.link": "Povezava", "pad.share.emebdcode": "URL za vključitev", "pad.chat": "Klepet", diff --git a/src/locales/tcy.json b/src/locales/tcy.json index 3cd815b6a..0e56ec596 100644 --- a/src/locales/tcy.json +++ b/src/locales/tcy.json @@ -18,7 +18,6 @@ "pad.colorpicker.save": "ಒರಿಪಾಲೆ", "pad.colorpicker.cancel": "ವಜಾ ಮಲ್ಪುಲೆ", "pad.loading": "ದಿಂಜಾವೊಂದುಂಡು......", - "pad.wrongPassword": "ಇರೇನಾ ಪಾಸ್ ವರ್ಡ್ ತಪ್ಪತುಂಡ್", "pad.settings.padSettings": "ಪ್ಯಾಡ್ ಸಂಯೋಜನೆ", "pad.settings.language": "ಬಾಸೆ:", "pad.importExport.exportetherpad": "Etherpad", @@ -42,7 +41,5 @@ "timeslider.month.november": "ನವಂಬರೊ", "timeslider.month.december": "ದಸಂಬರೊ", "pad.userlist.entername": "ಈರೆನೆ ಪುದರ್ ಬರೆಲೆ", - "pad.userlist.unnamed": "ಪುದರ್ ಇಜ್ಜಂತಿನವು", - "pad.userlist.guest": "ಬಿನ್ನೆರ್", - "pad.userlist.approve": "ಒಪ್ಪಂದ ಅಂಡ್" + "pad.userlist.unnamed": "ಪುದರ್ ಇಜ್ಜಂತಿನವು" } diff --git a/src/locales/uk.json b/src/locales/uk.json index 240302de7..3604e38b9 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -10,11 +10,40 @@ "Piramidion", "Steve.rusyn", "SteveR", + "Ата", "Григорій Пугач" ] }, + "admin.page-title": "Адміністративна панель — Etherpad", + "admin_plugins": "Менеджер плагінів", + "admin_plugins.available": "Доступні плагіни", + "admin_plugins.available_not-found": "Плагінів не знайдено.", + "admin_plugins.available_fetching": "Отримується…", + "admin_plugins.available_install.value": "Встановити", + "admin_plugins.available_search.placeholder": "Шукати плагіни для встановлення", + "admin_plugins.description": "Опис", + "admin_plugins.installed": "Встановлені плагіни", + "admin_plugins.installed_fetching": "Отримуються встановлені плагіни…", + "admin_plugins.installed_nothing": "Ви ще не встановили жодних плагінів.", + "admin_plugins.installed_uninstall.value": "Видалити", + "admin_plugins.last-update": "Останнє оновлення", + "admin_plugins.name": "Назва", + "admin_plugins.page-title": "Менеджер плагінів — Etherpad", + "admin_plugins.version": "Версія", + "admin_plugins_info": "Інформація щодо виправлення неполадок", + "admin_plugins_info.plugins": "Встановлені плагіни", + "admin_plugins_info.page-title": "Інформація про плагіни — Etherpad", + "admin_plugins_info.version": "Версія Etherpad", + "admin_plugins_info.version_latest": "Найсвіжіша доступна версія", + "admin_plugins_info.version_number": "Номер версії", + "admin_settings": "Налаштування", + "admin_settings.current": "Поточна конфігурація", + "admin_settings.current_restart.value": "Перезапустити Etherpad", + "admin_settings.current_save.value": "Зберегти налаштування", + "admin_settings.page-title": "Налаштування — Etherpad", "index.newPad": "Створити", "index.createOpenPad": "або створити/відкрити документ з назвою:", + "index.openPad": "відкрити наявний документ з назвою:", "pad.toolbar.bold.title": "Напівжирний (Ctrl-B)", "pad.toolbar.italic.title": "Курсив (Ctrl-I)", "pad.toolbar.underline.title": "Підкреслення (Ctrl-U)", @@ -22,7 +51,7 @@ "pad.toolbar.ol.title": "Упорядкований список (Ctrl+Shift+N)", "pad.toolbar.ul.title": "Неупорядкований список (Ctrl+Shift+L)", "pad.toolbar.indent.title": "Відступ (TAB)", - "pad.toolbar.unindent.title": "Виступ (Shift+TAB)", + "pad.toolbar.unindent.title": "Відступ (Shift+TAB)", "pad.toolbar.undo.title": "Скасувати (Ctrl-Z)", "pad.toolbar.redo.title": "Повторити (Ctrl-Y)", "pad.toolbar.clearAuthorship.title": "Очистити кольори авторства (Ctrl+Shift+C)", @@ -35,7 +64,7 @@ "pad.colorpicker.save": "Зберегти", "pad.colorpicker.cancel": "Скасувати", "pad.loading": "Завантаження…", - "pad.noCookie": "Реп'яшки не знайдено. Будь-ласка, увімкніть реп'яшки у вашому браузері!", + "pad.noCookie": "Реп'яшки не знайдено. Будь ласка, увімкніть реп'яшки у вашому браузері! Ваша сесія та налаштування не зберігатимуться між візитами. Це може спричинятися тим, що Etherpad у деяких браузерах включений через iFrame. Будь ласка, переконайтеся, що iFrame міститься на тому ж піддомені/домені, що й батьківський iFrame", "pad.permissionDenied": "У Вас немає дозволу для доступу до цього документа", "pad.settings.padSettings": "Налаштування документа", "pad.settings.myView": "Мій Вигляд", @@ -47,6 +76,8 @@ "pad.settings.fontType": "Тип шрифту:", "pad.settings.fontType.normal": "Звичайний", "pad.settings.language": "Мова:", + "pad.settings.about": "Про програму", + "pad.settings.poweredBy": "Працює на", "pad.importExport.import_export": "Імпорт/Експорт", "pad.importExport.import": "Завантажити будь-який текстовий файл або документ", "pad.importExport.importSuccessful": "Успішно!", @@ -59,7 +90,7 @@ "pad.importExport.exportopen": "ODF (документ OpenOffice)", "pad.importExport.abiword.innerHTML": "Ви можете імпортувати лише у форматі простого тексту або HTML. Для більш просунутих способів імпорту встановіть AbiWord або LibreOffice.", "pad.modals.connected": "З'єднано.", - "pad.modals.reconnecting": "Перепідлючення до Вашого документа..", + "pad.modals.reconnecting": "Перепідключення до Вашого документа…", "pad.modals.forcereconnect": "Примусове перепідключення", "pad.modals.reconnecttimer": "Триває спроба відновлення з'єднання", "pad.modals.cancel": "Скасувати", @@ -81,6 +112,10 @@ "pad.modals.corruptPad.cause": "Це може бути через неправильну конфігурацію сервера або іншу непередбачувану поведінку. Зверніться до адміністратора служби.", "pad.modals.deleted": "Вилучено.", "pad.modals.deleted.explanation": "Цей документ було вилучено.", + "pad.modals.rateLimited": "Швидкість обмежено.", + "pad.modals.rateLimited.explanation": "Ви надіслали надто багато повідомлень у цей документ, тому він вас від'єднав.", + "pad.modals.rejected.explanation": "Сервер відхилив повідомлення, надіслане вашим браузером.", + "pad.modals.rejected.cause": "Сервер міг оновитися, поки ви переглядали документ, а може це баг в Etherpad'і. Спробуйте перезавантажити сторінку.", "pad.modals.disconnected": "Вас було від'єднано.", "pad.modals.disconnected.explanation": "З'єднання з сервером втрачено", "pad.modals.disconnected.cause": "Сервер, можливо, недоступний. Будь ласка, повідомте адміністратора служби, якщо це повторюватиметься.", @@ -93,6 +128,7 @@ "pad.chat.loadmessages": "Завантажити більше повідомлень", "pad.chat.stick.title": "Закріпити чат на екрані", "pad.chat.writeMessage.placeholder": "Напишіть своє повідомлення сюди", + "timeslider.followContents": "Слідкувати за оновленнями вмісту документа", "timeslider.pageTitle": "Часова шкала {{appTitle}}", "timeslider.toolbar.returnbutton": "Повернутись до документа", "timeslider.toolbar.authors": "Автори:", diff --git a/src/node/db/API.js b/src/node/db/API.js index 2c58a69a3..ea77ac7df 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -1,3 +1,4 @@ +'use strict'; /** * This module provides all API functions */ @@ -18,8 +19,8 @@ * limitations under the License. */ -const Changeset = require('ep_etherpad-lite/static/js/Changeset'); -const customError = require('../utils/customError'); +const Changeset = require('../../static/js/Changeset'); +const CustomError = require('../utils/customError'); const padManager = require('./PadManager'); const padMessageHandler = require('../handler/PadMessageHandler'); const readOnlyManager = require('./ReadOnlyManager'); @@ -101,7 +102,7 @@ Example returns: } */ -exports.getAttributePool = async function (padID) { +exports.getAttributePool = async (padID) => { const pad = await getPadSafe(padID, true); return {pool: pad.pool}; }; @@ -119,7 +120,7 @@ Example returns: } */ -exports.getRevisionChangeset = async function (padID, rev) { +exports.getRevisionChangeset = async (padID, rev) => { // try to parse the revision number if (rev !== undefined) { rev = checkValidRev(rev); @@ -133,7 +134,7 @@ exports.getRevisionChangeset = async function (padID, rev) { if (rev !== undefined) { // check if this is a valid revision if (rev > head) { - throw new customError('rev is higher than the head revision of the pad', 'apierror'); + throw new CustomError('rev is higher than the head revision of the pad', 'apierror'); } // get the changeset for this revision @@ -152,7 +153,7 @@ Example returns: {code: 0, message:"ok", data: {text:"Welcome Text"}} {code: 1, message:"padID does not exist", data: null} */ -exports.getText = async function (padID, rev) { +exports.getText = async (padID, rev) => { // try to parse the revision number if (rev !== undefined) { rev = checkValidRev(rev); @@ -166,7 +167,7 @@ exports.getText = async function (padID, rev) { if (rev !== undefined) { // check if this is a valid revision if (rev > head) { - throw new customError('rev is higher than the head revision of the pad', 'apierror'); + throw new CustomError('rev is higher than the head revision of the pad', 'apierror'); } // get the text of this revision @@ -188,10 +189,10 @@ Example returns: {code: 1, message:"padID does not exist", data: null} {code: 1, message:"text too long", data: null} */ -exports.setText = async function (padID, text) { +exports.setText = async (padID, text) => { // text is required if (typeof text !== 'string') { - throw new customError('text is not a string', 'apierror'); + throw new CustomError('text is not a string', 'apierror'); } // get the pad @@ -212,10 +213,10 @@ Example returns: {code: 1, message:"padID does not exist", data: null} {code: 1, message:"text too long", data: null} */ -exports.appendText = async function (padID, text) { +exports.appendText = async (padID, text) => { // text is required if (typeof text !== 'string') { - throw new customError('text is not a string', 'apierror'); + throw new CustomError('text is not a string', 'apierror'); } const pad = await getPadSafe(padID, true); @@ -233,7 +234,7 @@ Example returns: {code: 0, message:"ok", data: {text:"Welcome Text"}} {code: 1, message:"padID does not exist", data: null} */ -exports.getHTML = async function (padID, rev) { +exports.getHTML = async (padID, rev) => { if (rev !== undefined) { rev = checkValidRev(rev); } @@ -245,7 +246,7 @@ exports.getHTML = async function (padID, rev) { // check if this is a valid revision const head = pad.getHeadRevisionNumber(); if (rev > head) { - throw new customError('rev is higher than the head revision of the pad', 'apierror'); + throw new CustomError('rev is higher than the head revision of the pad', 'apierror'); } } @@ -265,10 +266,10 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.setHTML = async function (padID, html) { +exports.setHTML = async (padID, html) => { // html string is required if (typeof html !== 'string') { - throw new customError('html is not a string', 'apierror'); + throw new CustomError('html is not a string', 'apierror'); } // get the pad @@ -278,7 +279,7 @@ exports.setHTML = async function (padID, html) { try { await importHtml.setPadHTML(pad, cleanText(html)); } catch (e) { - throw new customError('HTML is malformed', 'apierror'); + throw new CustomError('HTML is malformed', 'apierror'); } // update the clients on the pad @@ -294,23 +295,25 @@ getChatHistory(padId, start, end), returns a part of or the whole chat-history o Example returns: -{"code":0,"message":"ok","data":{"messages":[{"text":"foo","authorID":"a.foo","time":1359199533759,"userName":"test"}, - {"text":"bar","authorID":"a.foo","time":1359199534622,"userName":"test"}]}} +{"code":0,"message":"ok","data":{"messages":[ + {"text":"foo","authorID":"a.foo","time":1359199533759,"userName":"test"}, + {"text":"bar","authorID":"a.foo","time":1359199534622,"userName":"test"} +]}} {code: 1, message:"start is higher or equal to the current chatHead", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.getChatHistory = async function (padID, start, end) { +exports.getChatHistory = async (padID, start, end) => { if (start && end) { if (start < 0) { - throw new customError('start is below zero', 'apierror'); + throw new CustomError('start is below zero', 'apierror'); } if (end < 0) { - throw new customError('end is below zero', 'apierror'); + throw new CustomError('end is below zero', 'apierror'); } if (start > end) { - throw new customError('start is higher than end', 'apierror'); + throw new CustomError('start is higher than end', 'apierror'); } } @@ -320,16 +323,16 @@ exports.getChatHistory = async function (padID, start, end) { const chatHead = pad.chatHead; // fall back to getting the whole chat-history if a parameter is missing - if (!start || !end) { + if (!start || !end) { start = 0; end = pad.chatHead; } if (start > chatHead) { - throw new customError('start is higher than the current chatHead', 'apierror'); + throw new CustomError('start is higher than the current chatHead', 'apierror'); } if (end > chatHead) { - throw new customError('end is higher than the current chatHead', 'apierror'); + throw new CustomError('end is higher than the current chatHead', 'apierror'); } // the the whole message-log and return it to the client @@ -339,21 +342,22 @@ exports.getChatHistory = async function (padID, start, end) { }; /** -appendChatMessage(padID, text, authorID, time), creates a chat message for the pad id, time is a timestamp +appendChatMessage(padID, text, authorID, time), creates a chat message for the pad id, +time is a timestamp Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.appendChatMessage = async function (padID, text, authorID, time) { +exports.appendChatMessage = async (padID, text, authorID, time) => { // text is required if (typeof text !== 'string') { - throw new customError('text is not a string', 'apierror'); + throw new CustomError('text is not a string', 'apierror'); } // if time is not an integer value set time to current timestamp - if (time === undefined || !is_int(time)) { + if (time === undefined || !isInt(time)) { time = Date.now(); } @@ -375,7 +379,7 @@ Example returns: {code: 0, message:"ok", data: {revisions: 56}} {code: 1, message:"padID does not exist", data: null} */ -exports.getRevisionsCount = async function (padID) { +exports.getRevisionsCount = async (padID) => { // get the pad const pad = await getPadSafe(padID, true); return {revisions: pad.getHeadRevisionNumber()}; @@ -389,7 +393,7 @@ Example returns: {code: 0, message:"ok", data: {savedRevisions: 42}} {code: 1, message:"padID does not exist", data: null} */ -exports.getSavedRevisionsCount = async function (padID) { +exports.getSavedRevisionsCount = async (padID) => { // get the pad const pad = await getPadSafe(padID, true); return {savedRevisions: pad.getSavedRevisionsNumber()}; @@ -403,7 +407,7 @@ Example returns: {code: 0, message:"ok", data: {savedRevisions: [2, 42, 1337]}} {code: 1, message:"padID does not exist", data: null} */ -exports.listSavedRevisions = async function (padID) { +exports.listSavedRevisions = async (padID) => { // get the pad const pad = await getPadSafe(padID, true); return {savedRevisions: pad.getSavedRevisionsList()}; @@ -417,7 +421,7 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.saveRevision = async function (padID, rev) { +exports.saveRevision = async (padID, rev) => { // check if rev is a number if (rev !== undefined) { rev = checkValidRev(rev); @@ -430,7 +434,7 @@ exports.saveRevision = async function (padID, rev) { // the client asked for a special revision if (rev !== undefined) { if (rev > head) { - throw new customError('rev is higher than the head revision of the pad', 'apierror'); + throw new CustomError('rev is higher than the head revision of the pad', 'apierror'); } } else { rev = pad.getHeadRevisionNumber(); @@ -448,7 +452,7 @@ Example returns: {code: 0, message:"ok", data: {lastEdited: 1340815946602}} {code: 1, message:"padID does not exist", data: null} */ -exports.getLastEdited = async function (padID) { +exports.getLastEdited = async (padID) => { // get the pad const pad = await getPadSafe(padID, true); const lastEdited = await pad.getLastEdit(); @@ -463,16 +467,16 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"pad does already exist", data: null} */ -exports.createPad = async function (padID, text) { +exports.createPad = async (padID, text) => { if (padID) { // ensure there is no $ in the padID if (padID.indexOf('$') !== -1) { - throw new customError("createPad can't create group pads", 'apierror'); + throw new CustomError("createPad can't create group pads", 'apierror'); } // check for url special characters if (padID.match(/(\/|\?|&|#)/)) { - throw new customError('malformed padID: Remove special characters', 'apierror'); + throw new CustomError('malformed padID: Remove special characters', 'apierror'); } } @@ -488,7 +492,7 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.deletePad = async function (padID) { +exports.deletePad = async (padID) => { const pad = await getPadSafe(padID, true); await pad.remove(); }; @@ -501,10 +505,10 @@ exports.deletePad = async function (padID) { {code:0, message:"ok", data:null} {code: 1, message:"padID does not exist", data: null} */ -exports.restoreRevision = async function (padID, rev) { +exports.restoreRevision = async (padID, rev) => { // check if rev is a number if (rev === undefined) { - throw new customError('rev is not defined', 'apierror'); + throw new CustomError('rev is not defined', 'apierror'); } rev = checkValidRev(rev); @@ -513,7 +517,7 @@ exports.restoreRevision = async function (padID, rev) { // check if this is a valid revision if (rev > pad.getHeadRevisionNumber()) { - throw new customError('rev is higher than the head revision of the pad', 'apierror'); + throw new CustomError('rev is higher than the head revision of the pad', 'apierror'); } const atext = await pad.getInternalRevisionAText(rev); @@ -521,7 +525,7 @@ exports.restoreRevision = async function (padID, rev) { const oldText = pad.text(); atext.text += '\n'; - function eachAttribRun(attribs, func) { + const eachAttribRun = (attribs, func) => { const attribsIter = Changeset.opIterator(attribs); let textIndex = 0; const newTextStart = 0; @@ -534,7 +538,7 @@ exports.restoreRevision = async function (padID, rev) { } textIndex = nextIndex; } - } + }; // create a new changeset with a helper builder object const builder = Changeset.builder(oldText.length); @@ -569,7 +573,7 @@ Example returns: {code: 0, message:"ok", data: {padID: destinationID}} {code: 1, message:"padID does not exist", data: null} */ -exports.copyPad = async function (sourceID, destinationID, force) { +exports.copyPad = async (sourceID, destinationID, force) => { const pad = await getPadSafe(sourceID, true); await pad.copy(destinationID, force); }; @@ -583,7 +587,7 @@ Example returns: {code: 0, message:"ok", data: {padID: destinationID}} {code: 1, message:"padID does not exist", data: null} */ -exports.copyPadWithoutHistory = async function (sourceID, destinationID, force) { +exports.copyPadWithoutHistory = async (sourceID, destinationID, force) => { const pad = await getPadSafe(sourceID, true); await pad.copyPadWithoutHistory(destinationID, force); }; @@ -597,7 +601,7 @@ Example returns: {code: 0, message:"ok", data: {padID: destinationID}} {code: 1, message:"padID does not exist", data: null} */ -exports.movePad = async function (sourceID, destinationID, force) { +exports.movePad = async (sourceID, destinationID, force) => { const pad = await getPadSafe(sourceID, true); await pad.copy(destinationID, force); await pad.remove(); @@ -611,7 +615,7 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.getReadOnlyID = async function (padID) { +exports.getReadOnlyID = async (padID) => { // we don't need the pad object, but this function does all the security stuff for us await getPadSafe(padID, true); @@ -629,11 +633,11 @@ Example returns: {code: 0, message:"ok", data: {padID: padID}} {code: 1, message:"padID does not exist", data: null} */ -exports.getPadID = async function (roID) { +exports.getPadID = async (roID) => { // get the PadId const padID = await readOnlyManager.getPadId(roID); - if (padID === null) { - throw new customError('padID does not exist', 'apierror'); + if (padID == null) { + throw new CustomError('padID does not exist', 'apierror'); } return {padID}; @@ -647,7 +651,7 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.setPublicStatus = async function (padID, publicStatus) { +exports.setPublicStatus = async (padID, publicStatus) => { // ensure this is a group pad checkGroupPad(padID, 'publicStatus'); @@ -670,7 +674,7 @@ Example returns: {code: 0, message:"ok", data: {publicStatus: true}} {code: 1, message:"padID does not exist", data: null} */ -exports.getPublicStatus = async function (padID) { +exports.getPublicStatus = async (padID) => { // ensure this is a group pad checkGroupPad(padID, 'publicStatus'); @@ -687,7 +691,7 @@ Example returns: {code: 0, message:"ok", data: {authorIDs : ["a.s8oes9dhwrvt0zif", "a.akf8finncvomlqva"]} {code: 1, message:"padID does not exist", data: null} */ -exports.listAuthorsOfPad = async function (padID) { +exports.listAuthorsOfPad = async (padID) => { // get the pad const pad = await getPadSafe(padID, true); const authorIDs = pad.getAllAuthors(); @@ -717,7 +721,7 @@ Example returns: {code: 1, message:"padID does not exist"} */ -exports.sendClientsMessage = async function (padID, msg) { +exports.sendClientsMessage = async (padID, msg) => { const pad = await getPadSafe(padID, true); padMessageHandler.handleCustomMessage(padID, msg); }; @@ -730,7 +734,7 @@ Example returns: {"code":0,"message":"ok","data":null} {"code":4,"message":"no or wrong API Key","data":null} */ -exports.checkToken = async function () { +exports.checkToken = async () => { }; /** @@ -741,7 +745,7 @@ Example returns: {code: 0, message:"ok", data: {chatHead: 42}} {code: 1, message:"padID does not exist", data: null} */ -exports.getChatHead = async function (padID) { +exports.getChatHead = async (padID) => { // get the pad const pad = await getPadSafe(padID, true); return {chatHead: pad.chatHead}; @@ -751,11 +755,21 @@ exports.getChatHead = async function (padID) { createDiffHTML(padID, startRev, endRev) returns an object of diffs from 2 points in a pad Example returns: - -{"code":0,"message":"ok","data":{"html":"Welcome to Etherpad!

This 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!

Get involved with Etherpad at http://etherpad.org
aw

","authors":["a.HKIv23mEbachFYfH",""]}} +{ + "code": 0, + "message": "ok", + "data": { + "html": "...", + "authors": [ + "a.HKIv23mEbachFYfH", + "" + ] + } +} {"code":4,"message":"no or wrong API Key","data":null} + */ -exports.createDiffHTML = async function (padID, startRev, endRev) { +exports.createDiffHTML = async (padID, startRev, endRev) => { // check if startRev is a number if (startRev !== undefined) { startRev = checkValidRev(startRev); @@ -768,8 +782,9 @@ exports.createDiffHTML = async function (padID, startRev, endRev) { // get the pad const pad = await getPadSafe(padID, true); + let padDiff; try { - var padDiff = new PadDiff(pad, startRev, endRev); + padDiff = new PadDiff(pad, startRev, endRev); } catch (e) { throw {stop: e.message}; } @@ -793,7 +808,7 @@ exports.createDiffHTML = async function (padID, startRev, endRev) { {"code":4,"message":"no or wrong API Key","data":null} */ -exports.getStats = async function () { +exports.getStats = async () => { const sessionInfos = padMessageHandler.sessioninfos; const sessionKeys = Object.keys(sessionInfos); @@ -813,20 +828,18 @@ exports.getStats = async function () { **************************** */ // checks if a number is an int -function is_int(value) { - return (parseFloat(value) == parseInt(value, 10)) && !isNaN(value); -} +const isInt = (value) => (parseFloat(value) === parseInt(value, 10)) && !isNaN(value); // gets a pad safe async function getPadSafe(padID, shouldExist, text) { // check if padID is a string if (typeof padID !== 'string') { - throw new customError('padID is not a string', 'apierror'); + throw new CustomError('padID is not a string', 'apierror'); } // check if the padID maches the requirements if (!padManager.isValidPadId(padID)) { - throw new customError('padID did not match requirements', 'apierror'); + throw new CustomError('padID did not match requirements', 'apierror'); } // check if the pad exists @@ -834,12 +847,12 @@ async function getPadSafe(padID, shouldExist, text) { if (!exists && shouldExist) { // does not exist, but should - throw new customError('padID does not exist', 'apierror'); + throw new CustomError('padID does not exist', 'apierror'); } if (exists && !shouldExist) { // does exist, but shouldn't - throw new customError('padID does already exist', 'apierror'); + throw new CustomError('padID does already exist', 'apierror'); } // pad exists, let's get it @@ -848,33 +861,34 @@ async function getPadSafe(padID, shouldExist, text) { // checks if a rev is a legal number // pre-condition is that `rev` is not undefined -function checkValidRev(rev) { +const checkValidRev = (rev) => { if (typeof rev !== 'number') { rev = parseInt(rev, 10); } // check if rev is a number if (isNaN(rev)) { - throw new customError('rev is not a number', 'apierror'); + throw new CustomError('rev is not a number', 'apierror'); } // ensure this is not a negative number if (rev < 0) { - throw new customError('rev is not a negative number', 'apierror'); + throw new CustomError('rev is not a negative number', 'apierror'); } // ensure this is not a float value - if (!is_int(rev)) { - throw new customError('rev is a float value', 'apierror'); + if (!isInt(rev)) { + throw new CustomError('rev is a float value', 'apierror'); } return rev; -} +}; // checks if a padID is part of a group -function checkGroupPad(padID, field) { +const checkGroupPad = (padID, field) => { // ensure this is a group pad if (padID && padID.indexOf('$') === -1) { - throw new customError(`You can only get/set the ${field} of pads that belong to a group`, 'apierror'); + throw new CustomError( + `You can only get/set the ${field} of pads that belong to a group`, 'apierror'); } -} +}; diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index f4ef903cf..3fde1e4b1 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -1,3 +1,4 @@ +'use strict'; /** * The AuthorManager controlls all information about the Pad authors */ @@ -19,85 +20,83 @@ */ const db = require('./DB'); -const customError = require('../utils/customError'); -const randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; +const CustomError = require('../utils/customError'); +const randomString = require('../../static/js/pad_utils').randomString; -exports.getColorPalette = function () { - return [ - '#ffc7c7', - '#fff1c7', - '#e3ffc7', - '#c7ffd5', - '#c7ffff', - '#c7d5ff', - '#e3c7ff', - '#ffc7f1', - '#ffa8a8', - '#ffe699', - '#cfff9e', - '#99ffb3', - '#a3ffff', - '#99b3ff', - '#cc99ff', - '#ff99e5', - '#e7b1b1', - '#e9dcAf', - '#cde9af', - '#bfedcc', - '#b1e7e7', - '#c3cdee', - '#d2b8ea', - '#eec3e6', - '#e9cece', - '#e7e0ca', - '#d3e5c7', - '#bce1c5', - '#c1e2e2', - '#c1c9e2', - '#cfc1e2', - '#e0bdd9', - '#baded3', - '#a0f8eb', - '#b1e7e0', - '#c3c8e4', - '#cec5e2', - '#b1d5e7', - '#cda8f0', - '#f0f0a8', - '#f2f2a6', - '#f5a8eb', - '#c5f9a9', - '#ececbb', - '#e7c4bc', - '#daf0b2', - '#b0a0fd', - '#bce2e7', - '#cce2bb', - '#ec9afe', - '#edabbd', - '#aeaeea', - '#c4e7b1', - '#d722bb', - '#f3a5e7', - '#ffa8a8', - '#d8c0c5', - '#eaaedd', - '#adc6eb', - '#bedad1', - '#dee9af', - '#e9afc2', - '#f8d2a0', - '#b3b3e6', - ]; -}; +exports.getColorPalette = () => [ + '#ffc7c7', + '#fff1c7', + '#e3ffc7', + '#c7ffd5', + '#c7ffff', + '#c7d5ff', + '#e3c7ff', + '#ffc7f1', + '#ffa8a8', + '#ffe699', + '#cfff9e', + '#99ffb3', + '#a3ffff', + '#99b3ff', + '#cc99ff', + '#ff99e5', + '#e7b1b1', + '#e9dcAf', + '#cde9af', + '#bfedcc', + '#b1e7e7', + '#c3cdee', + '#d2b8ea', + '#eec3e6', + '#e9cece', + '#e7e0ca', + '#d3e5c7', + '#bce1c5', + '#c1e2e2', + '#c1c9e2', + '#cfc1e2', + '#e0bdd9', + '#baded3', + '#a0f8eb', + '#b1e7e0', + '#c3c8e4', + '#cec5e2', + '#b1d5e7', + '#cda8f0', + '#f0f0a8', + '#f2f2a6', + '#f5a8eb', + '#c5f9a9', + '#ececbb', + '#e7c4bc', + '#daf0b2', + '#b0a0fd', + '#bce2e7', + '#cce2bb', + '#ec9afe', + '#edabbd', + '#aeaeea', + '#c4e7b1', + '#d722bb', + '#f3a5e7', + '#ffa8a8', + '#d8c0c5', + '#eaaedd', + '#adc6eb', + '#bedad1', + '#dee9af', + '#e9afc2', + '#f8d2a0', + '#b3b3e6', +]; /** * Checks if the author exists */ -exports.doesAuthorExist = async function (authorID) { +exports.doesAuthorExist = async (authorID) => { const author = await db.get(`globalAuthor:${authorID}`); - return author !== null; + return author != null; }; /* exported for backwards compatibility */ @@ -107,7 +106,7 @@ exports.doesAuthorExists = exports.doesAuthorExist; * Returns the AuthorID for a token. * @param {String} token The token */ -exports.getAuthor4Token = async function (token) { +exports.getAuthor4Token = async (token) => { const author = await mapAuthorWithDBKey('token2author', token); // return only the sub value authorID @@ -119,7 +118,7 @@ exports.getAuthor4Token = async function (token) { * @param {String} token The mapper * @param {String} name The name of the author (optional) */ -exports.createAuthorIfNotExistsFor = async function (authorMapper, name) { +exports.createAuthorIfNotExistsFor = async (authorMapper, name) => { const author = await mapAuthorWithDBKey('mapper2author', authorMapper); if (name) { @@ -140,7 +139,7 @@ async function mapAuthorWithDBKey(mapperkey, mapper) { // try to map to an author const author = await db.get(`${mapperkey}:${mapper}`); - if (author === null) { + if (author == null) { // there is no author with this mapper, so create one const author = await exports.createAuthor(null); @@ -163,7 +162,7 @@ async function mapAuthorWithDBKey(mapperkey, mapper) { * Internal function that creates the database entry for an author * @param {String} name The name of the author */ -exports.createAuthor = function (name) { +exports.createAuthor = (name) => { // create the new author name const author = `a.${randomString(16)}`; @@ -185,50 +184,40 @@ exports.createAuthor = function (name) { * Returns the Author Obj of the author * @param {String} author The id of the author */ -exports.getAuthor = function (author) { - // NB: result is already a Promise - return db.get(`globalAuthor:${author}`); -}; +exports.getAuthor = (author) => db.get(`globalAuthor:${author}`); /** * Returns the color Id of the author * @param {String} author The id of the author */ -exports.getAuthorColorId = function (author) { - return db.getSub(`globalAuthor:${author}`, ['colorId']); -}; +exports.getAuthorColorId = (author) => db.getSub(`globalAuthor:${author}`, ['colorId']); /** * Sets the color Id of the author * @param {String} author The id of the author * @param {String} colorId The color id of the author */ -exports.setAuthorColorId = function (author, colorId) { - return db.setSub(`globalAuthor:${author}`, ['colorId'], colorId); -}; +exports.setAuthorColorId = (author, colorId) => db.setSub( + `globalAuthor:${author}`, ['colorId'], colorId); /** * Returns the name of the author * @param {String} author The id of the author */ -exports.getAuthorName = function (author) { - return db.getSub(`globalAuthor:${author}`, ['name']); -}; +exports.getAuthorName = (author) => db.getSub(`globalAuthor:${author}`, ['name']); /** * Sets the name of the author * @param {String} author The id of the author * @param {String} name The name of the author */ -exports.setAuthorName = function (author, name) { - return db.setSub(`globalAuthor:${author}`, ['name'], name); -}; +exports.setAuthorName = (author, name) => db.setSub(`globalAuthor:${author}`, ['name'], name); /** * Returns an array of all pads this author contributed to * @param {String} author The id of the author */ -exports.listPadsOfAuthor = async function (authorID) { +exports.listPadsOfAuthor = async (authorID) => { /* There are two other places where this array is manipulated: * (1) When the author is added to a pad, the author object is also updated * (2) When a pad is deleted, each author of that pad is also updated @@ -237,9 +226,9 @@ exports.listPadsOfAuthor = async function (authorID) { // get the globalAuthor const author = await db.get(`globalAuthor:${authorID}`); - if (author === null) { + if (author == null) { // author does not exist - throw new customError('authorID does not exist', 'apierror'); + throw new CustomError('authorID does not exist', 'apierror'); } // everything is fine, return the pad IDs @@ -253,11 +242,11 @@ exports.listPadsOfAuthor = async function (authorID) { * @param {String} author The id of the author * @param {String} padID The id of the pad the author contributes to */ -exports.addPad = async function (authorID, padID) { +exports.addPad = async (authorID, padID) => { // get the entry const author = await db.get(`globalAuthor:${authorID}`); - if (author === null) return; + if (author == null) return; /* * ACHTUNG: padIDs can also be undefined, not just null, so it is not possible @@ -280,12 +269,12 @@ exports.addPad = async function (authorID, padID) { * @param {String} author The id of the author * @param {String} padID The id of the pad the author contributes to */ -exports.removePad = async function (authorID, padID) { +exports.removePad = async (authorID, padID) => { const author = await db.get(`globalAuthor:${authorID}`); - if (author === null) return; + if (author == null) return; - if (author.padIDs !== null) { + if (author.padIDs != null) { // remove pad from author delete author.padIDs[padID]; await db.set(`globalAuthor:${authorID}`, author); diff --git a/src/node/db/DB.js b/src/node/db/DB.js index 601c08c5c..c0993e8ec 100644 --- a/src/node/db/DB.js +++ b/src/node/db/DB.js @@ -1,7 +1,7 @@ 'use strict'; /** - * The DB Module provides a database initalized with the settings + * The DB Module provides a database initialized with the settings * provided by the settings module */ @@ -36,7 +36,7 @@ const db = exports.db = null; /** - * Initalizes the database with the settings provided by the settings module + * Initializes the database with the settings provided by the settings module * @param {Function} callback */ exports.init = async () => await new Promise((resolve, reject) => { @@ -49,7 +49,7 @@ exports.init = async () => await new Promise((resolve, reject) => { } // everything ok, set up Promise-based methods - ['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove', 'doShutdown'].forEach((fn) => { + ['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove'].forEach((fn) => { exports[fn] = util.promisify(db[fn].bind(db)); }); @@ -73,6 +73,6 @@ exports.init = async () => await new Promise((resolve, reject) => { }); exports.shutdown = async (hookName, context) => { - await exports.doShutdown(); + await util.promisify(db.close.bind(db))(); console.log('Database closed'); }; diff --git a/src/node/db/GroupManager.js b/src/node/db/GroupManager.js index 1330acc43..203e21a35 100644 --- a/src/node/db/GroupManager.js +++ b/src/node/db/GroupManager.js @@ -1,3 +1,4 @@ +'use strict'; /** * The Group Manager provides functions to manage groups in the database */ @@ -18,13 +19,13 @@ * limitations under the License. */ -const customError = require('../utils/customError'); -const randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; +const CustomError = require('../utils/customError'); +const randomString = require('../../static/js/pad_utils').randomString; const db = require('./DB'); const padManager = require('./PadManager'); const sessionManager = require('./SessionManager'); -exports.listAllGroups = async function () { +exports.listAllGroups = async () => { let groups = await db.get('groups'); groups = groups || {}; @@ -32,17 +33,20 @@ exports.listAllGroups = async function () { return {groupIDs}; }; -exports.deleteGroup = async function (groupID) { +exports.deleteGroup = async (groupID) => { const group = await db.get(`group:${groupID}`); // ensure group exists if (group == null) { // group does not exist - throw new customError('groupID does not exist', 'apierror'); + throw new CustomError('groupID does not exist', 'apierror'); } // iterate through all pads of this group and delete them (in parallel) - await Promise.all(Object.keys(group.pads).map((padID) => padManager.getPad(padID).then((pad) => pad.remove()))); + await Promise.all(Object.keys(group.pads) + .map((padID) => padManager.getPad(padID) + .then((pad) => pad.remove()) + )); // iterate through group2sessions and delete all sessions const group2sessions = await db.get(`group2sessions:${groupID}`); @@ -76,14 +80,14 @@ exports.deleteGroup = async function (groupID) { await db.set('groups', newGroups); }; -exports.doesGroupExist = async function (groupID) { +exports.doesGroupExist = async (groupID) => { // try to get the group entry const group = await db.get(`group:${groupID}`); return (group != null); }; -exports.createGroup = async function () { +exports.createGroup = async () => { // search for non existing groupID const groupID = `g.${randomString(16)}`; @@ -103,10 +107,10 @@ exports.createGroup = async function () { return {groupID}; }; -exports.createGroupIfNotExistsFor = async function (groupMapper) { +exports.createGroupIfNotExistsFor = async (groupMapper) => { // ensure mapper is optional if (typeof groupMapper !== 'string') { - throw new customError('groupMapper is not a string', 'apierror'); + throw new CustomError('groupMapper is not a string', 'apierror'); } // try to get a group for this mapper @@ -128,7 +132,7 @@ exports.createGroupIfNotExistsFor = async function (groupMapper) { return result; }; -exports.createGroupPad = async function (groupID, padName, text) { +exports.createGroupPad = async (groupID, padName, text) => { // create the padID const padID = `${groupID}$${padName}`; @@ -136,7 +140,7 @@ exports.createGroupPad = async function (groupID, padName, text) { const groupExists = await exports.doesGroupExist(groupID); if (!groupExists) { - throw new customError('groupID does not exist', 'apierror'); + throw new CustomError('groupID does not exist', 'apierror'); } // ensure pad doesn't exist already @@ -144,7 +148,7 @@ exports.createGroupPad = async function (groupID, padName, text) { if (padExists) { // pad exists already - throw new customError('padName does already exist', 'apierror'); + throw new CustomError('padName does already exist', 'apierror'); } // create the pad @@ -156,12 +160,12 @@ exports.createGroupPad = async function (groupID, padName, text) { return {padID}; }; -exports.listPads = async function (groupID) { +exports.listPads = async (groupID) => { const exists = await exports.doesGroupExist(groupID); // ensure the group exists if (!exists) { - throw new customError('groupID does not exist', 'apierror'); + throw new CustomError('groupID does not exist', 'apierror'); } // group exists, let's get the pads diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index c39b3c69e..965f2793f 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -1,21 +1,21 @@ +'use strict'; /** * The pad object, defined with joose */ -const Changeset = require('ep_etherpad-lite/static/js/Changeset'); -const AttributePool = require('ep_etherpad-lite/static/js/AttributePool'); +const Changeset = require('../../static/js/Changeset'); +const AttributePool = require('../../static/js/AttributePool'); const db = require('./DB'); const settings = require('../utils/Settings'); const authorManager = require('./AuthorManager'); const padManager = require('./PadManager'); const padMessageHandler = require('../handler/PadMessageHandler'); const groupManager = require('./GroupManager'); -const customError = require('../utils/customError'); +const CustomError = require('../utils/customError'); const readOnlyManager = require('./ReadOnlyManager'); -const crypto = require('crypto'); const randomString = require('../utils/randomstring'); -const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); +const hooks = require('../../static/js/pluginfw/hooks'); const promises = require('../utils/promises'); // serialization/deserialization attributes @@ -23,13 +23,14 @@ const attributeBlackList = ['id']; const jsonableList = ['pool']; /** - * Copied from the Etherpad source code. It converts Windows line breaks to Unix line breaks and convert Tabs to spaces + * Copied from the Etherpad source code. It converts Windows line breaks to Unix + * line breaks and convert Tabs to spaces * @param txt */ -exports.cleanText = function (txt) { - return txt.replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/\t/g, ' ').replace(/\xa0/g, ' '); -}; - +exports.cleanText = (txt) => txt.replace(/\r\n/g, '\n') + .replace(/\r/g, '\n') + .replace(/\t/g, ' ') + .replace(/\xa0/g, ' '); const Pad = function Pad(id) { this.atext = Changeset.makeAText('\n'); @@ -56,7 +57,7 @@ Pad.prototype.getSavedRevisionsNumber = function getSavedRevisionsNumber() { }; Pad.prototype.getSavedRevisionsList = function getSavedRevisionsList() { - const savedRev = new Array(); + const savedRev = []; for (const rev in this.savedRevisions) { savedRev.push(this.savedRevisions[rev].revNum); } @@ -85,11 +86,11 @@ Pad.prototype.appendRevision = async function appendRevision(aChangeset, author) newRevData.meta.timestamp = Date.now(); // ex. getNumForAuthor - if (author != '') { + if (author !== '') { this.pool.putAttrib(['author', author || '']); } - if (newRev % 100 == 0) { + if (newRev % 100 === 0) { newRevData.meta.pool = this.pool; newRevData.meta.atext = this.atext; } @@ -104,7 +105,7 @@ Pad.prototype.appendRevision = async function appendRevision(aChangeset, author) p.push(authorManager.addPad(author, this.id)); } - if (this.head == 0) { + if (this.head === 0) { hooks.callAll('padCreate', {pad: this, author}); } else { hooks.callAll('padUpdate', {pad: this, author, revs: newRev, changeset: aChangeset}); @@ -153,7 +154,7 @@ Pad.prototype.getAllAuthors = function getAllAuthors() { const authors = []; for (const key in this.pool.numToAttrib) { - if (this.pool.numToAttrib[key][0] == 'author' && this.pool.numToAttrib[key][1] != '') { + if (this.pool.numToAttrib[key][0] === 'author' && this.pool.numToAttrib[key][1] !== '') { authors.push(this.pool.numToAttrib[key][1]); } } @@ -177,9 +178,10 @@ Pad.prototype.getInternalRevisionAText = async function getInternalRevisionAText // get all needed changesets const changesets = []; - await Promise.all(neededChangesets.map((item) => this.getRevisionChangeset(item).then((changeset) => { - changesets[item] = changeset; - }))); + await Promise.all( + neededChangesets.map((item) => this.getRevisionChangeset(item).then((changeset) => { + changesets[item] = changeset; + }))); // we should have the atext by now let atext = await p_atext; @@ -204,10 +206,11 @@ Pad.prototype.getAllAuthorColors = async function getAllAuthorColors() { const returnTable = {}; const colorPalette = authorManager.getColorPalette(); - await Promise.all(authors.map((author) => authorManager.getAuthorColorId(author).then((colorId) => { - // colorId might be a hex color or an number out of the palette - returnTable[author] = colorPalette[colorId] || colorId; - }))); + await Promise.all( + authors.map((author) => authorManager.getAuthorColorId(author).then((colorId) => { + // colorId might be a hex color or an number out of the palette + returnTable[author] = colorPalette[colorId] || colorId; + }))); return returnTable; }; @@ -227,7 +230,7 @@ Pad.prototype.getValidRevisionRange = function getValidRevisionRange(startRev, e endRev = head; } - if (startRev !== null && endRev !== null) { + if (startRev != null && endRev != null) { return {startRev, endRev}; } return null; @@ -251,7 +254,7 @@ Pad.prototype.setText = async function setText(newText) { // We want to ensure the pad still ends with a \n, but otherwise keep // getText() and setText() consistent. let changeset; - if (newText[newText.length - 1] == '\n') { + if (newText[newText.length - 1] === '\n') { changeset = Changeset.makeSplice(oldText, 0, oldText.length, newText); } else { changeset = Changeset.makeSplice(oldText, 0, oldText.length - 1, newText); @@ -304,9 +307,10 @@ Pad.prototype.getChatMessages = async function getChatMessages(start, end) { // get all entries out of the database const entries = []; - await Promise.all(neededEntries.map((entryObject) => this.getChatMessage(entryObject.entryNum).then((entry) => { - entries[entryObject.order] = entry; - }))); + await Promise.all( + neededEntries.map((entryObject) => this.getChatMessage(entryObject.entryNum).then((entry) => { + entries[entryObject.order] = entry; + }))); // sort out broken chat entries // it looks like in happened in the past that the chat head was @@ -384,14 +388,16 @@ Pad.prototype.copy = async function copy(destinationID, force) { // copy all chat messages const chatHead = this.chatHead; for (let i = 0; i <= chatHead; ++i) { - const p = db.get(`pad:${sourceID}:chat:${i}`).then((chat) => db.set(`pad:${destinationID}:chat:${i}`, chat)); + const p = db.get(`pad:${sourceID}:chat:${i}`) + .then((chat) => db.set(`pad:${destinationID}:chat:${i}`, chat)); promises.push(p); } // copy all revisions const revHead = this.head; for (let i = 0; i <= revHead; ++i) { - const p = db.get(`pad:${sourceID}:revs:${i}`).then((rev) => db.set(`pad:${destinationID}:revs:${i}`, rev)); + const p = db.get(`pad:${sourceID}:revs:${i}`) + .then((rev) => db.set(`pad:${destinationID}:revs:${i}`, rev)); promises.push(p); } @@ -412,7 +418,7 @@ Pad.prototype.copy = async function copy(destinationID, force) { await padManager.getPad(destinationID, null); // this runs too early. // let the plugins know the pad was copied - hooks.callAll('padCopy', {originalPad: this, destinationID}); + await hooks.aCallAll('padCopy', {originalPad: this, destinationID}); return {padID: destinationID}; }; @@ -426,7 +432,7 @@ Pad.prototype.checkIfGroupExistAndReturnIt = async function checkIfGroupExistAnd // group does not exist if (!groupExists) { - throw new customError('groupID does not exist for destinationID', 'apierror'); + throw new CustomError('groupID does not exist for destinationID', 'apierror'); } } return destGroupID; @@ -446,7 +452,7 @@ Pad.prototype.removePadIfForceIsTrueAndAlreadyExist = async function removePadIf if (exists) { if (!force) { console.error('erroring out without force'); - throw new customError('destinationID already exists', 'apierror'); + throw new CustomError('destinationID already exists', 'apierror'); } // exists and forcing @@ -514,7 +520,7 @@ Pad.prototype.copyPadWithoutHistory = async function copyPadWithoutHistory(desti const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText); newPad.appendRevision(changeset); - hooks.callAll('padCopy', {originalPad: this, destinationID}); + await hooks.aCallAll('padCopy', {originalPad: this, destinationID}); return {padID: destinationID}; }; @@ -568,7 +574,7 @@ Pad.prototype.remove = async function remove() { // delete the pad entry and delete pad from padManager p.push(padManager.removePad(padID)); - hooks.callAll('padRemove', {padID}); + p.push(hooks.aCallAll('padRemove', {padID})); await Promise.all(p); }; diff --git a/src/node/db/PadManager.js b/src/node/db/PadManager.js index 9334b92a4..48507949e 100644 --- a/src/node/db/PadManager.js +++ b/src/node/db/PadManager.js @@ -1,3 +1,4 @@ +'use strict'; /** * The Pad Manager is a Factory for pad Objects */ @@ -18,7 +19,7 @@ * limitations under the License. */ -const customError = require('../utils/customError'); +const CustomError = require('../utils/customError'); const Pad = require('../db/Pad').Pad; const db = require('./DB'); @@ -109,22 +110,22 @@ const padList = { * @param id A String with the id of the pad * @param {Function} callback */ -exports.getPad = async function (id, text) { +exports.getPad = async (id, text) => { // check if this is a valid padId if (!exports.isValidPadId(id)) { - throw new customError(`${id} is not a valid padId`, 'apierror'); + throw new CustomError(`${id} is not a valid padId`, 'apierror'); } // check if this is a valid text if (text != null) { // check if text is a string if (typeof text !== 'string') { - throw new customError('text is not a string', 'apierror'); + throw new CustomError('text is not a string', 'apierror'); } // check if text is less than 100k chars if (text.length > 100000) { - throw new customError('text must be less than 100k chars', 'apierror'); + throw new CustomError('text must be less than 100k chars', 'apierror'); } } @@ -138,7 +139,7 @@ exports.getPad = async function (id, text) { // try to load pad pad = new Pad(id); - // initalize the pad + // initialize the pad await pad.init(text); globalPads.set(id, pad); padList.addPad(id); @@ -146,14 +147,14 @@ exports.getPad = async function (id, text) { return pad; }; -exports.listAllPads = async function () { +exports.listAllPads = async () => { const padIDs = await padList.getPads(); return {padIDs}; }; // checks if a pad exists -exports.doesPadExist = async function (padId) { +exports.doesPadExist = async (padId) => { const value = await db.get(`pad:${padId}`); return (value != null && value.atext); @@ -189,9 +190,7 @@ exports.sanitizePadId = async function sanitizePadId(padId) { return padId; }; -exports.isValidPadId = function (padId) { - return /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId); -}; +exports.isValidPadId = (padId) => /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId); /** * Removes the pad from database and unloads it. @@ -204,6 +203,6 @@ exports.removePad = async (padId) => { }; // removes a pad from the cache -exports.unloadPad = function (padId) { +exports.unloadPad = (padId) => { globalPads.remove(padId); }; diff --git a/src/node/db/ReadOnlyManager.js b/src/node/db/ReadOnlyManager.js index 54f8c592e..5dabe2e06 100644 --- a/src/node/db/ReadOnlyManager.js +++ b/src/node/db/ReadOnlyManager.js @@ -1,3 +1,4 @@ +'use strict'; /** * The ReadOnlyManager manages the database and rendering releated to read only pads */ @@ -27,15 +28,13 @@ const randomString = require('../utils/randomstring'); * checks if the id pattern matches a read-only pad id * @param {String} the pad's id */ -exports.isReadOnlyId = function (id) { - return id.indexOf('r.') === 0; -}; +exports.isReadOnlyId = (id) => id.indexOf('r.') === 0; /** * returns a read only id for a pad * @param {String} padId the id of the pad */ -exports.getReadOnlyId = async function (padId) { +exports.getReadOnlyId = async (padId) => { // check if there is a pad2readonly entry let readOnlyId = await db.get(`pad2readonly:${padId}`); @@ -53,15 +52,13 @@ exports.getReadOnlyId = async function (padId) { * returns the padId for a read only id * @param {String} readOnlyId read only id */ -exports.getPadId = function (readOnlyId) { - return db.get(`readonly2pad:${readOnlyId}`); -}; +exports.getPadId = (readOnlyId) => db.get(`readonly2pad:${readOnlyId}`); /** * returns the padId and readonlyPadId in an object for any id * @param {String} padIdOrReadonlyPadId read only id or real pad id */ -exports.getIds = async function (id) { +exports.getIds = async (id) => { const readonly = (id.indexOf('r.') === 0); // Might be null, if this is an unknown read-only id diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index 64091dbdc..24d966d18 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -1,3 +1,4 @@ +'use strict'; /** * Controls the security of pad access */ @@ -19,7 +20,7 @@ */ const authorManager = require('./AuthorManager'); -const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks.js'); +const hooks = require('../../static/js/pluginfw/hooks.js'); const padManager = require('./PadManager'); const sessionManager = require('./SessionManager'); const settings = require('../utils/Settings'); @@ -47,7 +48,7 @@ const DENY = Object.freeze({accessStatus: 'deny'}); * WARNING: Tokens and session IDs MUST be kept secret, otherwise users will be able to impersonate * each other (which might allow them to gain privileges). */ -exports.checkAccess = async function (padID, sessionCookie, token, userSettings) { +exports.checkAccess = async (padID, sessionCookie, token, userSettings) => { if (!padID) { authLogger.debug('access denied: missing padID'); return DENY; diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.js index 8f1daab52..d46ed9fae 100644 --- a/src/node/db/SessionManager.js +++ b/src/node/db/SessionManager.js @@ -1,5 +1,7 @@ +'use strict'; /** - * The Session Manager provides functions to manage session in the database, it only provides session management for sessions created by the API + * The Session Manager provides functions to manage session in the database, + * it only provides session management for sessions created by the API */ /* @@ -18,7 +20,7 @@ * limitations under the License. */ -const customError = require('../utils/customError'); +const CustomError = require('../utils/customError'); const promises = require('../utils/promises'); const randomString = require('../utils/randomstring'); const db = require('./DB'); @@ -40,7 +42,8 @@ exports.findAuthorID = async (groupID, sessionCookie) => { * Sometimes, RFC 6265-compliant web servers may send back a cookie whose * value is enclosed in double quotes, such as: * - * Set-Cookie: sessionCookie="s.37cf5299fbf981e14121fba3a588c02b,s.2b21517bf50729d8130ab85736a11346"; Version=1; Path=/; Domain=localhost; Discard + * Set-Cookie: sessionCookie="s.37cf5299fbf981e14121fba3a588c02b, + * s.2b21517bf50729d8130ab85736a11346"; Version=1; Path=/; Domain=localhost; Discard * * Where the double quotes at the start and the end of the header value are * just delimiters. This is perfectly legal: Etherpad parsing logic should @@ -78,26 +81,26 @@ exports.findAuthorID = async (groupID, sessionCookie) => { return sessionInfo.authorID; }; -exports.doesSessionExist = async function (sessionID) { +exports.doesSessionExist = async (sessionID) => { // check if the database entry of this session exists const session = await db.get(`session:${sessionID}`); - return (session !== null); + return (session != null); }; /** * Creates a new session between an author and a group */ -exports.createSession = async function (groupID, authorID, validUntil) { +exports.createSession = async (groupID, authorID, validUntil) => { // check if the group exists const groupExists = await groupManager.doesGroupExist(groupID); if (!groupExists) { - throw new customError('groupID does not exist', 'apierror'); + throw new CustomError('groupID does not exist', 'apierror'); } // check if the author exists const authorExists = await authorManager.doesAuthorExist(authorID); if (!authorExists) { - throw new customError('authorID does not exist', 'apierror'); + throw new CustomError('authorID does not exist', 'apierror'); } // try to parse validUntil if it's not a number @@ -107,22 +110,22 @@ exports.createSession = async function (groupID, authorID, validUntil) { // check it's a valid number if (isNaN(validUntil)) { - throw new customError('validUntil is not a number', 'apierror'); + throw new CustomError('validUntil is not a number', 'apierror'); } // ensure this is not a negative number if (validUntil < 0) { - throw new customError('validUntil is a negative number', 'apierror'); + throw new CustomError('validUntil is a negative number', 'apierror'); } // ensure this is not a float value - if (!is_int(validUntil)) { - throw new customError('validUntil is a float value', 'apierror'); + if (!isInt(validUntil)) { + throw new CustomError('validUntil is a float value', 'apierror'); } // check if validUntil is in the future if (validUntil < Math.floor(Date.now() / 1000)) { - throw new customError('validUntil is in the past', 'apierror'); + throw new CustomError('validUntil is in the past', 'apierror'); } // generate sessionID @@ -170,13 +173,13 @@ exports.createSession = async function (groupID, authorID, validUntil) { return {sessionID}; }; -exports.getSessionInfo = async function (sessionID) { +exports.getSessionInfo = async (sessionID) => { // check if the database entry of this session exists const session = await db.get(`session:${sessionID}`); if (session == null) { // session does not exist - throw new customError('sessionID does not exist', 'apierror'); + throw new CustomError('sessionID does not exist', 'apierror'); } // everything is fine, return the sessioninfos @@ -186,11 +189,11 @@ exports.getSessionInfo = async function (sessionID) { /** * Deletes a session */ -exports.deleteSession = async function (sessionID) { +exports.deleteSession = async (sessionID) => { // ensure that the session exists const session = await db.get(`session:${sessionID}`); if (session == null) { - throw new customError('sessionID does not exist', 'apierror'); + throw new CustomError('sessionID does not exist', 'apierror'); } // everything is fine, use the sessioninfos @@ -217,22 +220,22 @@ exports.deleteSession = async function (sessionID) { } }; -exports.listSessionsOfGroup = async function (groupID) { +exports.listSessionsOfGroup = async (groupID) => { // check that the group exists const exists = await groupManager.doesGroupExist(groupID); if (!exists) { - throw new customError('groupID does not exist', 'apierror'); + throw new CustomError('groupID does not exist', 'apierror'); } const sessions = await listSessionsWithDBKey(`group2sessions:${groupID}`); return sessions; }; -exports.listSessionsOfAuthor = async function (authorID) { +exports.listSessionsOfAuthor = async (authorID) => { // check that the author exists const exists = await authorManager.doesAuthorExist(authorID); if (!exists) { - throw new customError('authorID does not exist', 'apierror'); + throw new CustomError('authorID does not exist', 'apierror'); } const sessions = await listSessionsWithDBKey(`author2sessions:${authorID}`); @@ -241,7 +244,7 @@ exports.listSessionsOfAuthor = async function (authorID) { // this function is basically the code listSessionsOfAuthor and listSessionsOfGroup has in common // required to return null rather than an empty object if there are none -async function listSessionsWithDBKey(dbkey) { +const listSessionsWithDBKey = async (dbkey) => { // get the group2sessions entry const sessionObject = await db.get(dbkey); const sessions = sessionObject ? sessionObject.sessionIDs : null; @@ -252,7 +255,7 @@ async function listSessionsWithDBKey(dbkey) { const sessionInfo = await exports.getSessionInfo(sessionID); sessions[sessionID] = sessionInfo; } catch (err) { - if (err == 'apierror: sessionID does not exist') { + if (err === 'apierror: sessionID does not exist') { console.warn(`Found bad session ${sessionID} in ${dbkey}`); sessions[sessionID] = null; } else { @@ -262,9 +265,7 @@ async function listSessionsWithDBKey(dbkey) { } return sessions; -} +}; // checks if a number is an int -function is_int(value) { - return (parseFloat(value) == parseInt(value)) && !isNaN(value); -} +const isInt = (value) => (parseFloat(value) === parseInt(value)) && !isNaN(value); diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.js index 91bd75561..2c5d1ca25 100644 --- a/src/node/db/SessionStore.js +++ b/src/node/db/SessionStore.js @@ -1,3 +1,4 @@ +'use strict'; /* * Stores session data in the database * Source; https://github.com/edy-b/SciFlowWriter/blob/develop/available_plugins/ep_sciflowwriter/db/DirtyStore.js @@ -7,9 +8,9 @@ * express-session, which can't actually use promises anyway. */ -const DB = require('ep_etherpad-lite/node/db/DB'); -const Store = require('ep_etherpad-lite/node_modules/express-session').Store; -const log4js = require('ep_etherpad-lite/node_modules/log4js'); +const DB = require('./DB'); +const Store = require('express-session').Store; +const log4js = require('log4js'); const logger = log4js.getLogger('SessionStore'); diff --git a/src/node/easysync_tests.js b/src/node/easysync_tests.js index c8a5c9853..78d9f61e8 100644 --- a/src/node/easysync_tests.js +++ b/src/node/easysync_tests.js @@ -1,6 +1,8 @@ +'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 + * 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 */ @@ -21,52 +23,47 @@ */ -const Changeset = require('ep_etherpad-lite/static/js/Changeset'); -const AttributePool = require('ep_etherpad-lite/static/js/AttributePool'); +const Changeset = require('../static/js/Changeset'); +const AttributePool = require('../static/js/AttributePool'); function random() { - this.nextInt = function (maxValue) { - return Math.floor(Math.random() * maxValue); - }; - - this.nextDouble = function (maxValue) { - return Math.random(); - }; + this.nextInt = (maxValue) => Math.floor(Math.random() * maxValue); + this.nextDouble = (maxValue) => Math.random(); } -function runTests() { - function print(str) { +const runTests = () => { + const print = (str) => { console.log(str); - } + }; - function assert(code, optMsg) { - if (!eval(code)) throw new Error(`FALSE: ${optMsg || code}`); - } + const assert = (code, optMsg) => { + if (!eval(code)) throw new Error(`FALSE: ${optMsg || code}`); /* eslint-disable-line no-eval */ + }; - function literal(v) { + const literal = (v) => { if ((typeof v) === 'string') { return `"${v.replace(/[\\\"]/g, '\\$1').replace(/\n/g, '\\n')}"`; } else { return JSON.stringify(v); } - } + }; - function assertEqualArrays(a, b) { + const assertEqualArrays = (a, b) => { assert(`JSON.stringify(${literal(a)}) == JSON.stringify(${literal(b)})`); - } + }; - function assertEqualStrings(a, b) { + const assertEqualStrings = (a, b) => { assert(`${literal(a)} == ${literal(b)}`); - } + }; - function throughIterator(opsStr) { + const throughIterator = (opsStr) => { const iter = Changeset.opIterator(opsStr); const assem = Changeset.opAssembler(); while (iter.hasNext()) { assem.append(iter.next()); } return assem.toString(); - } + }; - function throughSmartAssembler(opsStr) { + const throughSmartAssembler = (opsStr) => { const iter = Changeset.opIterator(opsStr); const assem = Changeset.smartOpAssembler(); while (iter.hasNext()) { @@ -74,50 +71,50 @@ function runTests() { } assem.endDocument(); return assem.toString(); - } + }; - (function () { + (() => { print('> throughIterator'); const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1'; assert(`throughIterator(${literal(x)}) == ${literal(x)}`); })(); - (function () { + (() => { print('> throughSmartAssembler'); const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1'; assert(`throughSmartAssembler(${literal(x)}) == ${literal(x)}`); })(); - function applyMutations(mu, arrayOfArrays) { + const applyMutations = (mu, arrayOfArrays) => { arrayOfArrays.forEach((a) => { const result = mu[a[0]].apply(mu, a.slice(1)); - if (a[0] == 'remove' && a[3]) { + if (a[0] === 'remove' && a[3]) { assertEqualStrings(a[3], result); } }); - } + }; - function mutationsToChangeset(oldLen, arrayOfArrays) { + 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') { + 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') { + } 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') { + } else if (a[0] === 'insert') { op.opcode = '+'; bank.append(a[1]); op.chars = a[1].length; @@ -129,9 +126,9 @@ function runTests() { newLen += oldLen - oldPos; assem.endDocument(); return Changeset.pack(oldLen, newLen, assem.toString(), bank.toString()); - } + }; - function runMutationTest(testId, origLines, muts, correct) { + const runMutationTest = (testId, origLines, muts, correct) => { print(`> runMutationTest#${testId}`); let lines = origLines.slice(); const mu = Changeset.textLinesMutator(lines); @@ -149,7 +146,7 @@ function runTests() { // print(literal(cs)); const outText = Changeset.applyToText(cs, inText); assertEqualStrings(correctText, outText); - } + }; runMutationTest(1, ['apple\n', 'banana\n', 'cabbage\n', 'duffle\n', 'eggplant\n'], [ ['remove', 1, 0, 'a'], @@ -220,7 +217,7 @@ function runTests() { ['skip', 1, 1, true], ], ['banana\n', 'cabbage\n', 'duffle\n']); - function poolOrArray(attribs) { + const poolOrArray = (attribs) => { if (attribs.getAttrib) { return attribs; // it's already an attrib pool } else { @@ -231,23 +228,25 @@ function runTests() { }); return p; } - } + }; - function runApplyToAttributionTest(testId, attribs, cs, inAttr, outCorrect) { + const runApplyToAttributionTest = (testId, attribs, cs, inAttr, outCorrect) => { print(`> applyToAttribution#${testId}`); const p = poolOrArray(attribs); const result = Changeset.applyToAttribution( Changeset.checkRep(cs), inAttr, p); assertEqualStrings(outCorrect, result); - } + }; // 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'); + 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'); + runApplyToAttributionTest(2, + ['bold,', 'bold,true'], 'Z:g<4*1|1=6*1=5-4$', '|2+g', '*1|1+6*1+5|1+1'); - (function () { + (() => { print('> mutatorHasMore'); const lines = ['1\n', '2\n', '3\n', '4\n']; let mu; @@ -288,7 +287,7 @@ function runTests() { assert(`${mu.hasMore()} == false`); })(); - function runMutateAttributionTest(testId, attribs, cs, alines, outCorrect) { + const runMutateAttributionTest = (testId, attribs, cs, alines, outCorrect) => { print(`> runMutateAttributionTest#${testId}`); const p = poolOrArray(attribs); const alines2 = Array.prototype.slice.call(alines); @@ -298,30 +297,35 @@ function runTests() { print(`> runMutateAttributionTest#${testId}.applyToAttribution`); - function removeQuestionMarks(a) { - return a.replace(/\?/g, ''); - } + 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); assertEqualStrings(correctMerged, mergedResult); - } + }; // 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']); + 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']); + 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']); + 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']); + 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 = (function () { + const testPoolWithChars = (() => { const p = new AttributePool(); p.putAttrib(['char', 'newline']); for (let i = 1; i < 36; i++) { @@ -332,39 +336,66 @@ function runTests() { })(); // 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']); + 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']); + runMutateAttributionTest(7, testPoolWithChars, 'Z:3>7=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']); + 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(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(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']); + 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', + ]); - function randomInlineString(len, rand) { + const randomInlineString = (len, rand) => { const assem = Changeset.stringAssembler(); for (let i = 0; i < len; i++) { assem.append(String.fromCharCode(rand.nextInt(26) + 97)); } return assem.toString(); - } + }; - function randomMultiline(approxMaxLines, approxMaxCols, rand) { + const randomMultiline = (approxMaxLines, approxMaxCols, rand) => { const numParts = rand.nextInt(approxMaxLines * 2) + 1; const txt = Changeset.stringAssembler(); txt.append(rand.nextInt(2) ? '\n' : ''); for (let i = 0; i < numParts; i++) { - if ((i % 2) == 0) { + if ((i % 2) === 0) { if (rand.nextInt(10)) { txt.append(randomInlineString(rand.nextInt(approxMaxCols) + 1, rand)); } else { @@ -375,9 +406,9 @@ function runTests() { } } return txt.toString(); - } + }; - function randomStringOperation(numCharsLeft, rand) { + const randomStringOperation = (numCharsLeft, rand) => { let result; switch (rand.nextInt(9)) { case 0: @@ -476,26 +507,26 @@ function runTests() { result.skip = Math.min(result.skip, maxOrig); } return result; - } + }; - function randomTwoPropAttribs(opcode, rand) { + const randomTwoPropAttribs = (opcode, rand) => { // assumes attrib pool like ['apple,','apple,true','banana,','banana,true'] - if (opcode == '-' || rand.nextInt(3)) { + if (opcode === '-' || rand.nextInt(3)) { return ''; } else if (rand.nextInt(3)) { - if (opcode == '+' || rand.nextInt(2)) { + if (opcode === '+' || rand.nextInt(2)) { return `*${Changeset.numToString(rand.nextInt(2) * 2 + 1)}`; } else { return `*${Changeset.numToString(rand.nextInt(2) * 2)}`; } - } else if (opcode == '+' || rand.nextInt(4) == 0) { + } else if (opcode === '+' || rand.nextInt(4) === 0) { return '*1*3'; } else { return ['*0*2', '*0*3', '*1*2'][rand.nextInt(3)]; } - } + }; - function randomTestChangeset(origText, rand, withAttribs) { + const randomTestChangeset = (origText, rand, withAttribs) => { const charBank = Changeset.stringAssembler(); let textLeft = origText; // always keep final newline const outTextAssem = Changeset.stringAssembler(); @@ -504,13 +535,13 @@ function runTests() { const nextOp = Changeset.newOp(); - function appendMultilineOp(opcode, txt) { + const appendMultilineOp = (opcode, txt) => { nextOp.opcode = opcode; if (withAttribs) { nextOp.attribs = randomTwoPropAttribs(opcode, rand); } txt.replace(/\n|[^\n]+/g, (t) => { - if (t == '\n') { + if (t === '\n') { nextOp.chars = 1; nextOp.lines = 1; opAssem.append(nextOp); @@ -521,26 +552,26 @@ function runTests() { } return ''; }); - } + }; - function doOp() { + const doOp = () => { const o = randomStringOperation(textLeft.length, rand); if (o.insert) { - var txt = o.insert; + const txt = o.insert; charBank.append(txt); outTextAssem.append(txt); appendMultilineOp('+', txt); } else if (o.skip) { - var txt = textLeft.substring(0, o.skip); + const txt = textLeft.substring(0, o.skip); textLeft = textLeft.substring(o.skip); outTextAssem.append(txt); appendMultilineOp('=', txt); } else if (o.remove) { - var txt = textLeft.substring(0, 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) @@ -549,9 +580,9 @@ function runTests() { const cs = Changeset.pack(oldLen, outText.length, opAssem.toString(), charBank.toString()); Changeset.checkRep(cs); return [cs, outText]; - } + }; - function testCompose(randomSeed) { + const testCompose = (randomSeed) => { const rand = new random(); print(`> testCompose#${randomSeed}`); @@ -583,9 +614,9 @@ function runTests() { assertEqualStrings(text2, Changeset.applyToText(change12, startText)); assertEqualStrings(text3, Changeset.applyToText(change23, text1)); assertEqualStrings(text3, Changeset.applyToText(change123, startText)); - } + }; - for (var i = 0; i < 30; i++) testCompose(i); + for (let i = 0; i < 30; i++) testCompose(i); (function simpleComposeAttributesTest() { print('> simpleComposeAttributesTest'); @@ -607,12 +638,12 @@ function runTests() { p.putAttrib(['y', 'abc']); p.putAttrib(['y', 'def']); - function testFollow(a, b, afb, bfa, merge) { + const testFollow = (a, b, afb, bfa, merge) => { assertEqualStrings(afb, Changeset.followAttributes(a, b, p)); assertEqualStrings(bfa, Changeset.followAttributes(b, a, p)); assertEqualStrings(merge, Changeset.composeAttributes(a, afb, true, p)); assertEqualStrings(merge, Changeset.composeAttributes(b, bfa, true, p)); - } + }; testFollow('', '', '', '', ''); testFollow('*0', '', '', '*0', '*0'); @@ -624,7 +655,7 @@ function runTests() { testFollow('*0*4', '*2', '', '*0*4', '*0*4'); })(); - function testFollow(randomSeed) { + const testFollow = (randomSeed) => { const rand = new random(); print(`> testFollow#${randomSeed}`); @@ -642,37 +673,37 @@ function runTests() { const merge2 = Changeset.checkRep(Changeset.compose(cs2, bfa)); assertEqualStrings(merge1, merge2); - } + }; - for (var i = 0; i < 30; i++) testFollow(i); + for (let i = 0; i < 30; i++) testFollow(i); - function testSplitJoinAttributionLines(randomSeed) { + const testSplitJoinAttributionLines = (randomSeed) => { const rand = new random(); print(`> testSplitJoinAttributionLines#${randomSeed}`); const doc = `${randomMultiline(10, 20, rand)}\n`; - function stringToOps(str) { + 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}` : ''); + o.lines = (c === '\n' ? 1 : 0); + o.attribs = (c === 'a' || c === 'b' ? `*${c}` : ''); assem.append(o); } return assem.toString(); - } + }; const theJoined = stringToOps(doc); const theSplit = doc.match(/[^\n]*\n/g).map(stringToOps); assertEqualArrays(theSplit, Changeset.splitAttributionLines(theJoined, doc)); assertEqualStrings(theJoined, Changeset.joinAttributionLines(theSplit)); - } + }; - for (var i = 0; i < 10; i++) testSplitJoinAttributionLines(i); + for (let i = 0; i < 10; i++) testSplitJoinAttributionLines(i); (function testMoveOpsToNewPool() { print('> testMoveOpsToNewPool'); @@ -685,8 +716,10 @@ function runTests() { pool2.putAttrib(['foo', 'bar']); - assertEqualStrings(Changeset.moveOpsToNewPool('Z:1>2*1+1*0+1$ab', pool1, pool2), 'Z:1>2*0+1*1+1$ab'); - assertEqualStrings(Changeset.moveOpsToNewPool('*1+1*0+1', pool1, pool2), '*0+1*1+1'); + assertEqualStrings( + Changeset.moveOpsToNewPool('Z:1>2*1+1*0+1$ab', pool1, pool2), 'Z:1>2*0+1*1+1$ab'); + assertEqualStrings( + Changeset.moveOpsToNewPool('*1+1*0+1', pool1, pool2), '*0+1*1+1'); })(); @@ -709,14 +742,15 @@ function runTests() { assertEqualArrays(correctSplices, Changeset.toSplices(cs)); })(); - function testCharacterRangeFollow(testId, cs, oldRange, insertionsAfter, correctNewRange) { + const testCharacterRangeFollow = (testId, cs, oldRange, insertionsAfter, correctNewRange) => { print(`> testCharacterRangeFollow#${testId}`); + cs = Changeset.checkRep(cs); + assertEqualArrays(correctNewRange, Changeset.characterRangeFollow( + cs, oldRange[0], oldRange[1], insertionsAfter)); + }; - var cs = Changeset.checkRep(cs); - assertEqualArrays(correctNewRange, Changeset.characterRangeFollow(cs, oldRange[0], oldRange[1], insertionsAfter)); - } - - testCharacterRangeFollow(1, 'Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk', [7, 10], false, [14, 15]); + 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]); @@ -735,23 +769,31 @@ function runTests() { p.putAttrib(['name', 'david']); p.putAttrib(['color', 'green']); - assertEqualStrings('david', Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'name', p)); - assertEqualStrings('david', Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'name', p)); - assertEqualStrings('', Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'name', p)); - assertEqualStrings('', Changeset.opAttributeValue(Changeset.stringOp('+1'), 'name', p)); - assertEqualStrings('green', Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'color', p)); - assertEqualStrings('green', Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'color', p)); - assertEqualStrings('', Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'color', p)); - assertEqualStrings('', Changeset.opAttributeValue(Changeset.stringOp('+1'), 'color', p)); + assertEqualStrings('david', + Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'name', p)); + assertEqualStrings('david', + Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'name', p)); + assertEqualStrings('', + Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'name', p)); + assertEqualStrings('', + Changeset.opAttributeValue(Changeset.stringOp('+1'), 'name', p)); + assertEqualStrings('green', + Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'color', p)); + assertEqualStrings('green', + Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'color', p)); + assertEqualStrings('', + Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'color', p)); + assertEqualStrings('', + Changeset.opAttributeValue(Changeset.stringOp('+1'), 'color', p)); })(); - function testAppendATextToAssembler(testId, atext, correctOps) { + const testAppendATextToAssembler = (testId, atext, correctOps) => { print(`> testAppendATextToAssembler#${testId}`); const assem = Changeset.smartOpAssembler(); Changeset.appendATextToAssembler(atext, assem); assertEqualStrings(correctOps, assem.toString()); - } + }; testAppendATextToAssembler(1, { text: '\n', @@ -786,13 +828,13 @@ function runTests() { attribs: '|2+2*x|2+5', }, '|2+2*x|1+1*x+3'); - function testMakeAttribsString(testId, pool, opcode, attribs, correctString) { + const testMakeAttribsString = (testId, pool, opcode, attribs, correctString) => { print(`> testMakeAttribsString#${testId}`); const p = poolOrArray(pool); const str = Changeset.makeAttribsString(opcode, attribs, p); assertEqualStrings(correctString, str); - } + }; testMakeAttribsString(1, ['bold,'], '+', [ ['bold', ''], @@ -809,12 +851,12 @@ function runTests() { ['abc', 'def'], ], '*0*1'); - function testSubattribution(testId, astr, start, end, correctOutput) { + const testSubattribution = (testId, astr, start, end, correctOutput) => { print(`> testSubattribution#${testId}`); const str = Changeset.subattribution(astr, start, end); assertEqualStrings(correctOutput, str); - } + }; testSubattribution(1, '+1', 0, 0, ''); testSubattribution(2, '+1', 0, 1, '+1'); @@ -859,39 +901,42 @@ function runTests() { 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'); - function testFilterAttribNumbers(testId, cs, filter, correctOutput) { + const testFilterAttribNumbers = (testId, cs, filter, correctOutput) => { print(`> testFilterAttribNumbers#${testId}`); const str = Changeset.filterAttribNumbers(cs, filter); assertEqualStrings(correctOutput, str); - } + }; - 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'); + 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'); - function testInverse(testId, cs, lines, alines, pool, correctOutput) { + const testInverse = (testId, cs, lines, alines, pool, correctOutput) => { print(`> testInverse#${testId}`); pool = poolOrArray(pool); const str = Changeset.inverse(Changeset.checkRep(cs), lines, alines, pool); assertEqualStrings(correctOutput, str); - } + }; // 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$'); + 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$'); - function testMutateTextLines(testId, cs, lines, correctLines) { + const testMutateTextLines = (testId, cs, lines, correctLines) => { print(`> testMutateTextLines#${testId}`); const a = lines.slice(); Changeset.mutateTextLines(cs, a); assertEqualArrays(correctLines, a); - } + }; 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']); - function testInverseRandom(randomSeed) { + const testInverseRandom = (randomSeed) => { const rand = new random(); print(`> testInverseRandom#${randomSeed}`); @@ -928,9 +973,9 @@ function runTests() { // print(lines.map(function(s) { return '3: '+s.slice(0,-1); }).join('\n')); assertEqualArrays(origLines, lines); assertEqualArrays(origALines, alines); - } + }; - for (var i = 0; i < 30; i++) testInverseRandom(i); -} + for (let i = 0; i < 30; i++) testInverseRandom(i); +}; runTests(); diff --git a/src/node/eejs/examples/bar.ejs b/src/node/eejs/examples/bar.ejs deleted file mode 100644 index 6a2cc4bab..000000000 --- a/src/node/eejs/examples/bar.ejs +++ /dev/null @@ -1,9 +0,0 @@ -a -<% e.begin_block("bar"); %> - A - <% e.begin_block("foo"); %> - XX - <% e.end_block(); %> - B -<% e.end_block(); %> -b diff --git a/src/node/eejs/examples/foo.ejs b/src/node/eejs/examples/foo.ejs deleted file mode 100644 index daee5f8e8..000000000 --- a/src/node/eejs/examples/foo.ejs +++ /dev/null @@ -1,7 +0,0 @@ -<% e.inherit("./bar.ejs"); %> - -<% e.begin_define_block("foo"); %> - YY - <% e.super(); %> - ZZ -<% e.end_define_block(); %> diff --git a/src/node/eejs/index.js b/src/node/eejs/index.js index 1da0b7cca..d7d8db5ff 100644 --- a/src/node/eejs/index.js +++ b/src/node/eejs/index.js @@ -1,3 +1,4 @@ +'use strict'; /* * Copyright (c) 2011 RedHog (Egil Möller) * @@ -16,13 +17,13 @@ /* Basic usage: * - * require("./index").require("./examples/foo.ejs") + * require("./index").require("./path/to/template.ejs") */ const ejs = require('ejs'); const fs = require('fs'); +const hooks = require('../../static/js/pluginfw/hooks.js'); const path = require('path'); -const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks.js'); const resolve = require('resolve'); const settings = require('../utils/Settings'); @@ -35,65 +36,35 @@ exports.info = { args: [], }; -function getCurrentFile() { - return exports.info.file_stack[exports.info.file_stack.length - 1]; -} +const getCurrentFile = () => exports.info.file_stack[exports.info.file_stack.length - 1]; -function createBlockId(name) { - return `${getCurrentFile().path}|${name}`; -} - -exports._init = function (b, recursive) { +exports._init = (b, recursive) => { exports.info.__output_stack.push(exports.info.__output); exports.info.__output = b; }; -exports._exit = function (b, recursive) { - getCurrentFile().inherit.forEach((item) => { - exports._require(item.name, item.args); - }); +exports._exit = (b, recursive) => { exports.info.__output = exports.info.__output_stack.pop(); }; -exports.begin_capture = function () { - exports.info.__output_stack.push(exports.info.__output.concat()); - exports.info.__output.splice(0, exports.info.__output.length); -}; - -exports.end_capture = function () { - const res = exports.info.__output.join(''); - exports.info.__output.splice.apply( - exports.info.__output, - [0, exports.info.__output.length].concat(exports.info.__output_stack.pop())); - return res; -}; - -exports.begin_define_block = function (name) { +exports.begin_block = (name) => { exports.info.block_stack.push(name); - exports.begin_capture(); + exports.info.__output_stack.push(exports.info.__output.get()); + exports.info.__output.set(''); }; -exports.end_define_block = function () { - const content = exports.end_capture(); - return content; -}; - -exports.end_block = function () { +exports.end_block = () => { const name = exports.info.block_stack.pop(); const renderContext = exports.info.args[exports.info.args.length - 1]; - const args = {content: exports.end_define_block(), renderContext}; + const content = exports.info.__output.get(); + exports.info.__output.set(exports.info.__output_stack.pop()); + const args = {content, renderContext}; hooks.callAll(`eejsBlock_${name}`, args); - exports.info.__output.push(args.content); + exports.info.__output.set(exports.info.__output.get().concat(args.content)); }; -exports.begin_block = exports.begin_define_block; - -exports.inherit = function (name, args) { - getCurrentFile().inherit.push({name, args}); -}; - -exports.require = function (name, args, mod) { - if (args == undefined) args = {}; +exports.require = (name, args, mod) => { + if (args == null) args = {}; let basedir = __dirname; let paths = []; @@ -106,43 +77,22 @@ exports.require = function (name, args, mod) { paths = mod.paths; } - const ejspath = resolve.sync( - name, - { - paths, - basedir, - extensions: ['.html', '.ejs'], - } - ); + const ejspath = resolve.sync(name, {paths, basedir, extensions: ['.html', '.ejs']}); args.e = exports; args.require = require; - let template; - if (settings.maxAge !== 0) { // don't cache if maxAge is 0 - if (!templateCache.has(ejspath)) { - template = `<% e._init(__output); %>${fs.readFileSync(ejspath).toString()}<% e._exit(); %>`; - templateCache.set(ejspath, template); - } else { - template = templateCache.get(ejspath); - } - } else { - template = `<% e._init(__output); %>${fs.readFileSync(ejspath).toString()}<% e._exit(); %>`; - } + const cache = settings.maxAge !== 0; + const template = cache && templateCache.get(ejspath) || ejs.compile( + `<% e._init({get: () => __output, set: (s) => { __output = s; }}); %>${fs.readFileSync(ejspath).toString()}<% e._exit(); %>`, + {filename: ejspath}); + if (cache) templateCache.set(ejspath, template); exports.info.args.push(args); - exports.info.file_stack.push({path: ejspath, inherit: []}); - if (settings.maxAge !== 0) { - var res = ejs.render(template, args, {cache: true, filename: ejspath}); - } else { - var res = ejs.render(template, args, {cache: false, filename: ejspath}); - } + exports.info.file_stack.push({path: ejspath}); + const res = template(args); exports.info.file_stack.pop(); exports.info.args.pop(); return res; }; - -exports._require = function (name, args) { - exports.info.__output.push(exports.require(name, args)); -}; diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js index 0708cc009..6bc6e5378 100644 --- a/src/node/handler/APIHandler.js +++ b/src/node/handler/APIHandler.js @@ -1,3 +1,4 @@ +'use strict'; /** * The API Handler handles all API http requests */ @@ -37,7 +38,8 @@ try { apikey = fs.readFileSync(apikeyFilename, 'utf8'); apiHandlerLogger.info(`Api key file read from: "${apikeyFilename}"`); } catch (e) { - apiHandlerLogger.info(`Api key file "${apikeyFilename}" not found. Creating with random contents.`); + apiHandlerLogger.info( + `Api key file "${apikeyFilename}" not found. Creating with random contents.`); apikey = randomString(32); fs.writeFileSync(apikeyFilename, apikey, 'utf8'); } diff --git a/src/node/handler/ExportHandler.js b/src/node/handler/ExportHandler.js index 0a92633f7..fbb9e57da 100644 --- a/src/node/handler/ExportHandler.js +++ b/src/node/handler/ExportHandler.js @@ -1,3 +1,4 @@ +'use strict'; /** * Handles the export requests */ @@ -25,7 +26,7 @@ const exportEtherpad = require('../utils/ExportEtherpad'); const fs = require('fs'); const settings = require('../utils/Settings'); const os = require('os'); -const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); +const hooks = require('../../static/js/pluginfw/hooks'); const TidyHtml = require('../utils/TidyHtml'); const util = require('util'); @@ -49,7 +50,7 @@ const tempDirectory = os.tmpdir(); /** * do a requested export */ -async function doExport(req, res, padId, readOnlyId, type) { +const doExport = async (req, res, padId, readOnlyId, type) => { // avoid naming the read-only file as the original pad's id let fileName = readOnlyId ? readOnlyId : padId; @@ -104,7 +105,6 @@ async function doExport(req, res, padId, readOnlyId, type) { const result = await hooks.aCallAll('exportConvert', {srcFile, destFile, req, res}); if (result.length > 0) { // console.log("export handled by plugin", destFile); - handledByPlugin = true; } else { // @TODO no Promise interface for convertors (yet) await new Promise((resolve, reject) => { @@ -115,7 +115,6 @@ async function doExport(req, res, padId, readOnlyId, type) { } // send the file - const sendFile = util.promisify(res.sendFile); await res.sendFile(destFile, null); // clean up temporary files @@ -128,9 +127,9 @@ async function doExport(req, res, padId, readOnlyId, type) { await fsp_unlink(destFile); } -} +}; -exports.doExport = function (req, res, padId, readOnlyId, type) { +exports.doExport = (req, res, padId, readOnlyId, type) => { doExport(req, res, padId, readOnlyId, type).catch((err) => { if (err !== 'stop') { throw err; diff --git a/src/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.js index c25623d76..67bef01ce 100644 --- a/src/node/handler/ImportHandler.js +++ b/src/node/handler/ImportHandler.js @@ -1,3 +1,4 @@ +'use strict'; /** * Handles the import requests */ @@ -22,7 +23,7 @@ const padManager = require('../db/PadManager'); const padMessageHandler = require('./PadMessageHandler'); -const fs = require('fs'); +const fs = require('fs').promises; const path = require('path'); const settings = require('../utils/Settings'); const formidable = require('formidable'); @@ -30,19 +31,35 @@ const os = require('os'); const importHtml = require('../utils/ImportHtml'); const importEtherpad = require('../utils/ImportEtherpad'); const log4js = require('log4js'); -const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks.js'); -const util = require('util'); +const hooks = require('../../static/js/pluginfw/hooks.js'); -const fsp_exists = util.promisify(fs.exists); -const fsp_rename = util.promisify(fs.rename); -const fsp_readFile = util.promisify(fs.readFile); -const fsp_unlink = util.promisify(fs.unlink); +const logger = log4js.getLogger('ImportHandler'); + +// `status` must be a string supported by `importErrorMessage()` in `src/static/js/pad_impexp.js`. +class ImportError extends Error { + constructor(status, ...args) { + super(...args); + if (Error.captureStackTrace) Error.captureStackTrace(this, ImportError); + this.name = 'ImportError'; + this.status = status; + const msg = this.message == null ? '' : String(this.message); + if (status !== '') this.message = msg === '' ? status : `${status}: ${msg}`; + } +} + +const rm = async (path) => { + try { + await fs.unlink(path); + } catch (err) { + if (err.code !== 'ENOENT') throw err; + } +}; let convertor = null; let exportExtension = 'htm'; // load abiword only if it is enabled and if soffice is disabled -if (settings.abiword != null && settings.soffice === null) { +if (settings.abiword != null && settings.soffice == null) { convertor = require('../utils/Abiword'); } @@ -57,9 +74,7 @@ const tmpDirectory = os.tmpdir(); /** * do a requested import */ -async function doImport(req, res, padId) { - const apiLogger = log4js.getLogger('ImportHandler'); - +const doImport = async (req, res, padId) => { // pipe to a file // convert file to html via abiword or soffice // set html in the pad @@ -96,23 +111,23 @@ async function doImport(req, res, padId) { // I hate doing indexOf here but I can't see anything to use... if (err && err.stack && err.stack.indexOf('maxFileSize') !== -1) { - reject('maxFileSize'); + return reject(new ImportError('maxFileSize')); } - reject('uploadFailed'); + return reject(new ImportError('uploadFailed')); } if (!files.file) { // might not be a graceful fix but it works - reject('uploadFailed'); - } else { - resolve(files.file.path); + return reject(new ImportError('uploadFailed')); } + resolve(files.file.path); }); }); // ensure this is a file ending we know, else we change the file ending to .txt // this allows us to accept source code files like .c or .java const fileEnding = path.extname(srcFile).toLowerCase(); - const knownFileEndings = ['.txt', '.doc', '.docx', '.pdf', '.odt', '.html', '.htm', '.etherpad', '.rtf']; + const knownFileEndings = + ['.txt', '.doc', '.docx', '.pdf', '.odt', '.html', '.htm', '.etherpad', '.rtf']; const fileEndingUnknown = (knownFileEndings.indexOf(fileEnding) < 0); if (fileEndingUnknown) { @@ -123,10 +138,10 @@ async function doImport(req, res, padId) { const oldSrcFile = srcFile; srcFile = path.join(path.dirname(srcFile), `${path.basename(srcFile, fileEnding)}.txt`); - await fsp_rename(oldSrcFile, srcFile); + await fs.rename(oldSrcFile, srcFile); } else { console.warn('Not allowing unknown file type to be imported', fileEnding); - throw 'uploadFailed'; + throw new ImportError('uploadFailed'); } } @@ -140,24 +155,24 @@ async function doImport(req, res, padId) { const fileIsHTML = (fileEnding === '.html' || fileEnding === '.htm'); const fileIsTXT = (fileEnding === '.txt'); + let directDatabaseAccess = false; if (fileIsEtherpad) { // we do this here so we can see if the pad has quite a few edits const _pad = await padManager.getPad(padId); const headCount = _pad.head; if (headCount >= 10) { - apiLogger.warn("Direct database Import attempt of a pad that already has content, we won't be doing this"); - throw 'padHasData'; + logger.warn('Aborting direct database import attempt of a pad that already has content'); + throw new ImportError('padHasData'); } - const fsp_readFile = util.promisify(fs.readFile); - const _text = await fsp_readFile(srcFile, 'utf8'); - req.directDatabaseAccess = true; + const _text = await fs.readFile(srcFile, 'utf8'); + directDatabaseAccess = true; await importEtherpad.setPadRaw(padId, _text); } // convert file to html if necessary - if (!importHandledByPlugin && !req.directDatabaseAccess) { + if (!importHandledByPlugin && !directDatabaseAccess) { if (fileIsTXT) { // Don't use convertor for text files useConvertor = false; @@ -166,7 +181,7 @@ async function doImport(req, res, padId) { // See https://github.com/ether/etherpad-lite/issues/2572 if (fileIsHTML || !useConvertor) { // if no convertor only rename - fs.renameSync(srcFile, destFile); + await fs.rename(srcFile, destFile); } else { // @TODO - no Promise interface for convertors (yet) await new Promise((resolve, reject) => { @@ -174,7 +189,7 @@ async function doImport(req, res, padId) { // catch convert errors if (err) { console.warn('Converting Error:', err); - reject('convertFailed'); + return reject(new ImportError('convertFailed')); } resolve(); }); @@ -182,15 +197,15 @@ async function doImport(req, res, padId) { } } - if (!useConvertor && !req.directDatabaseAccess) { + if (!useConvertor && !directDatabaseAccess) { // Read the file with no encoding for raw buffer access. - const buf = await fsp_readFile(destFile); + const buf = await fs.readFile(destFile); // Check if there are only ascii chars in the uploaded file const isAscii = !Array.prototype.some.call(buf, (c) => (c > 240)); if (!isAscii) { - throw 'uploadFailed'; + throw new ImportError('uploadFailed'); } } @@ -200,8 +215,8 @@ async function doImport(req, res, padId) { // read the text let text; - if (!req.directDatabaseAccess) { - text = await fsp_readFile(destFile, 'utf8'); + if (!directDatabaseAccess) { + text = await fs.readFile(destFile, 'utf8'); // node on windows has a delay on releasing of the file lock. // We add a 100ms delay to work around this @@ -211,12 +226,12 @@ async function doImport(req, res, padId) { } // change text of the pad and broadcast the changeset - if (!req.directDatabaseAccess) { + if (!directDatabaseAccess) { if (importHandledByPlugin || useConvertor || fileIsHTML) { try { await importHtml.setPadHTML(pad, text); } catch (e) { - apiLogger.warn('Error importing, possibly caused by malformed HTML'); + logger.warn('Error importing, possibly caused by malformed HTML'); } } else { await pad.setText(text); @@ -230,49 +245,31 @@ async function doImport(req, res, padId) { // direct Database Access means a pad user should perform a switchToPad // and not attempt to receive updated pad data - if (req.directDatabaseAccess) { - return; - } + if (directDatabaseAccess) return true; // tell clients to update await padMessageHandler.updatePadClients(pad); // clean up temporary files + rm(srcFile); + rm(destFile); - /* - * TODO: directly delete the file and handle the eventual error. Checking - * before for existence is prone to race conditions, and does not handle any - * errors anyway. - */ - if (await fsp_exists(srcFile)) { - fsp_unlink(srcFile); - } - - if (await fsp_exists(destFile)) { - fsp_unlink(destFile); - } -} - -exports.doImport = function (req, res, padId) { - /** - * NB: abuse the 'req' object by storing an additional - * 'directDatabaseAccess' property on it so that it can - * be passed back in the HTML below. - * - * this is necessary because in the 'throw' paths of - * the function above there's no other way to return - * a value to the caller. - */ - let status = 'ok'; - doImport(req, res, padId).catch((err) => { - // check for known errors and replace the status - if (err == 'uploadFailed' || err == 'convertFailed' || err == 'padHasData' || err == 'maxFileSize') { - status = err; - } else { - throw err; - } - }).then(() => { - // close the connection - res.send(``); - }); + return false; +}; + +exports.doImport = async (req, res, padId) => { + let httpStatus = 200; + let code = 0; + let message = 'ok'; + let directDatabaseAccess; + try { + directDatabaseAccess = await doImport(req, res, padId); + } catch (err) { + const known = err instanceof ImportError && err.status; + if (!known) logger.error(`Internal error during import: ${err.stack || err}`); + httpStatus = known ? 400 : 500; + code = known ? 1 : 2; + message = known ? err.status : 'internalError'; + } + res.status(httpStatus).json({code, message, data: {directDatabaseAccess}}); }; diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 279b08dfa..7e290b9e3 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -1,3 +1,4 @@ +'use strict'; /** * The MessageHandler handles all Messages that comes from Socket.IO and controls the sessions */ @@ -18,22 +19,20 @@ * limitations under the License. */ -/* global exports, process, require */ - const padManager = require('../db/PadManager'); -const Changeset = require('ep_etherpad-lite/static/js/Changeset'); -const AttributePool = require('ep_etherpad-lite/static/js/AttributePool'); -const AttributeManager = require('ep_etherpad-lite/static/js/AttributeManager'); +const Changeset = require('../../static/js/Changeset'); +const AttributePool = require('../../static/js/AttributePool'); +const AttributeManager = require('../../static/js/AttributeManager'); const authorManager = require('../db/AuthorManager'); const readOnlyManager = require('../db/ReadOnlyManager'); const settings = require('../utils/Settings'); const securityManager = require('../db/SecurityManager'); -const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs.js'); +const plugins = require('../../static/js/pluginfw/plugin_defs.js'); const log4js = require('log4js'); const messageLogger = log4js.getLogger('message'); const accessLogger = log4js.getLogger('access'); const _ = require('underscore'); -const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks.js'); +const hooks = require('../../static/js/pluginfw/hooks.js'); const channels = require('channels'); const stats = require('../stats'); const assert = require('assert').strict; @@ -47,7 +46,7 @@ const rateLimiter = new RateLimiterMemory({ }); /** - * A associative array that saves informations about a session + * A associative array that saves information about a session * key = sessionId * values = padId, readonlyPadId, readonly, author, rev * padId = the real padId of the pad @@ -65,7 +64,9 @@ stats.gauge('totalUsers', () => Object.keys(socketio.sockets.sockets).length); /** * A changeset queue per pad that is processed by handleUserChanges() */ -const padChannels = new channels.channels(({socket, message}, callback) => nodeify(handleUserChanges(socket, message), callback)); +const padChannels = new channels.channels( + ({socket, message}, callback) => nodeify(handleUserChanges(socket, message), callback) +); /** * Saves the Socket class we need to send and receive data from the client @@ -76,7 +77,7 @@ let socketio; * This Method is called by server.js to tell the message handler on which socket it should send * @param socket_io The Socket */ -exports.setSocketIO = function (socket_io) { +exports.setSocketIO = (socket_io) => { socketio = socket_io; }; @@ -87,14 +88,26 @@ exports.setSocketIO = function (socket_io) { exports.handleConnect = (socket) => { stats.meter('connects').mark(); - // Initalize sessioninfos for this new session + // Initialize sessioninfos for this new session sessioninfos[socket.id] = {}; + + stats.gauge('activePads', () => { + const padIds = []; + for (const session of Object.keys(sessioninfos)) { + if (sessioninfos[session].padId) { + if (padIds.indexOf(sessioninfos[session].padId) === -1) { + padIds.push(sessioninfos[session].padId); + } + } + } + return padIds.length; + }); }; /** * Kicks all sessions from a pad */ -exports.kickSessionsFromPad = function (padID) { +exports.kickSessionsFromPad = (padID) => { if (typeof socketio.sockets.clients !== 'function') return; // skip if there is nobody on this pad @@ -114,7 +127,8 @@ exports.handleDisconnect = async (socket) => { // save the padname of this session const session = sessioninfos[socket.id]; - // if this connection was already etablished with a handshake, send a disconnect message to the others + // if this connection was already etablished with a handshake, + // send a disconnect message to the others if (session && session.author) { const {session: {user} = {}} = socket.client.request; accessLogger.info(`${'[LEAVE]' + @@ -192,7 +206,8 @@ exports.handleMessage = async (socket, message) => { const auth = thisSession.auth; if (!auth) { - console.error('Auth was never applied to a session. If you are using the stress-test tool then restart Etherpad and the Stress test tool.'); + console.error('Auth was never applied to a session. If you are using the ' + + 'stress-test tool then restart Etherpad and the Stress test tool.'); return; } @@ -234,7 +249,7 @@ exports.handleMessage = async (socket, message) => { } // Call handleMessage hook. If a plugin returns null, the message will be dropped. - if ((await hooks.aCallAll('handleMessage', context)).some((m) => m === null)) { + if ((await hooks.aCallAll('handleMessage', context)).some((m) => m == null)) { return; } @@ -283,11 +298,11 @@ exports.handleMessage = async (socket, message) => { * @param socket the socket.io Socket object for the client * @param message the message from the client */ -async function handleSaveRevisionMessage(socket, message) { +const handleSaveRevisionMessage = async (socket, message) => { const {padId, author: authorId} = sessioninfos[socket.id]; const pad = await padManager.getPad(padId); await pad.addSavedRevision(pad.head, authorId); -} +}; /** * Handles a custom message, different to the function below as it handles @@ -296,7 +311,7 @@ async function handleSaveRevisionMessage(socket, message) { * @param msg {Object} the message we're sending * @param sessionID {string} the socketIO session to which we're sending this message */ -exports.handleCustomObjectMessage = function (msg, sessionID) { +exports.handleCustomObjectMessage = (msg, sessionID) => { if (msg.data.type === 'CUSTOM') { if (sessionID) { // a sessionID is targeted: directly to this sessionID @@ -314,7 +329,7 @@ exports.handleCustomObjectMessage = function (msg, sessionID) { * @param padID {Pad} the pad to which we're sending this message * @param msgString {String} the message we're sending */ -exports.handleCustomMessage = function (padID, msgString) { +exports.handleCustomMessage = (padID, msgString) => { const time = Date.now(); const msg = { type: 'COLLABROOM', @@ -331,12 +346,12 @@ exports.handleCustomMessage = function (padID, msgString) { * @param socket the socket.io Socket object for the client * @param message the message from the client */ -async function handleChatMessage(socket, message) { +const handleChatMessage = async (socket, message) => { const time = Date.now(); const text = message.data.text; const {padId, author: authorId} = sessioninfos[socket.id]; await exports.sendChatMessageToPadClients(time, authorId, text, padId); -} +}; /** * Sends a chat message to all clients of this pad @@ -345,7 +360,7 @@ async function handleChatMessage(socket, message) { * @param text the text of the chat message * @param padId the padId to send the chat message to */ -exports.sendChatMessageToPadClients = async function (time, userId, text, padId) { +exports.sendChatMessageToPadClients = async (time, userId, text, padId) => { // get the pad const pad = await padManager.getPad(padId); @@ -371,7 +386,7 @@ exports.sendChatMessageToPadClients = async function (time, userId, text, padId) * @param socket the socket.io Socket object for the client * @param message the message from the client */ -async function handleGetChatMessages(socket, message) { +const handleGetChatMessages = async (socket, message) => { if (message.data.start == null) { messageLogger.warn('Dropped message, GetChatMessages Message has no start!'); return; @@ -387,7 +402,8 @@ async function handleGetChatMessages(socket, message) { const count = end - start; if (count < 0 || count > 100) { - messageLogger.warn('Dropped message, GetChatMessages Message, client requested invalid amount of messages!'); + messageLogger.warn( + 'Dropped message, GetChatMessages Message, client requested invalid amount of messages!'); return; } @@ -405,14 +421,14 @@ async function handleGetChatMessages(socket, message) { // send the messages back to the client socket.json.send(infoMsg); -} +}; /** * Handles a handleSuggestUserName, that means a user have suggest a userName for a other user * @param socket the socket.io Socket object for the client * @param message the message from the client */ -function handleSuggestUserName(socket, message) { +const handleSuggestUserName = (socket, message) => { // check if all ok if (message.data.payload.newName == null) { messageLogger.warn('Dropped message, suggestUserName Message has no newName!'); @@ -433,14 +449,15 @@ function handleSuggestUserName(socket, message) { socket.json.send(message); } }); -} +}; /** - * Handles a USERINFO_UPDATE, that means that a user have changed his color or name. Anyway, we get both informations + * Handles a USERINFO_UPDATE, that means that a user have changed his color or name. + * Anyway, we get both informations * @param socket the socket.io Socket object for the client * @param message the message from the client */ -async function handleUserInfoUpdate(socket, message) { +const handleUserInfoUpdate = async (socket, message) => { // check if all ok if (message.data.userInfo == null) { messageLogger.warn('Dropped message, USERINFO_UPDATE Message has no userInfo!'); @@ -463,7 +480,8 @@ async function handleUserInfoUpdate(socket, message) { const author = session.author; // Check colorId is a Hex color - const isColor = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(message.data.userInfo.colorId); // for #f00 (Thanks Smamatti) + // for #f00 (Thanks Smamatti) + const isColor = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(message.data.userInfo.colorId); if (!isColor) { messageLogger.warn(`Dropped message, USERINFO_UPDATE Color is malformed.${message.data}`); return; @@ -496,7 +514,7 @@ async function handleUserInfoUpdate(socket, message) { // Block until the authorManager has stored the new attributes. await p; -} +}; /** * Handles a USER_CHANGES message, where the client submits its local @@ -512,7 +530,7 @@ async function handleUserInfoUpdate(socket, message) { * @param socket the socket.io Socket object for the client * @param message the message from the client */ -async function handleUserChanges(socket, message) { +const handleUserChanges = async (socket, message) => { // This one's no longer pending, as we're gonna process it now stats.counter('pendingEdits').dec(); @@ -578,7 +596,8 @@ async function handleUserChanges(socket, message) { // + can add text with attribs // = can change or add attribs - // - can have attribs, but they are discarded and don't show up in the attribs - but do show up in the pool + // - can have attribs, but they are discarded and don't show up in the attribs - + // but do show up in the pool op.attribs.split('*').forEach((attr) => { if (!attr) return; @@ -586,9 +605,11 @@ async function handleUserChanges(socket, message) { attr = wireApool.getAttrib(attr); if (!attr) return; - // the empty author is used in the clearAuthorship functionality so this should be the only exception + // the empty author is used in the clearAuthorship functionality so this + // should be the only exception if ('author' === attr[0] && (attr[1] !== thisSession.author && attr[1] !== '')) { - throw new Error(`Author ${thisSession.author} tried to submit changes as author ${attr[1]} in changeset ${changeset}`); + throw new Error(`Author ${thisSession.author} tried to submit changes as author ` + + `${attr[1]} in changeset ${changeset}`); } }); } @@ -628,7 +649,7 @@ async function handleUserChanges(socket, message) { if (baseRev + 1 === r && c === changeset) { socket.json.send({disconnect: 'badChangeset'}); stats.meter('failedChangesets').mark(); - throw new Error("Won't apply USER_CHANGES, because it contains an already accepted changeset"); + throw new Error("Won't apply USER_CHANGES, as it contains an already accepted changeset"); } changeset = Changeset.follow(c, changeset, false, apool); @@ -672,9 +693,9 @@ async function handleUserChanges(socket, message) { } stopWatch.end(); -} +}; -exports.updatePadClients = async function (pad) { +exports.updatePadClients = async (pad) => { // skip this if no-one is on this pad const roomSockets = _getRoomSockets(pad.id); if (roomSockets.length === 0) return; @@ -682,9 +703,12 @@ exports.updatePadClients = async function (pad) { // since all clients usually get the same set of changesets, store them in local cache // to remove unnecessary roundtrip to the datalayer // NB: note below possibly now accommodated via the change to promises/async - // TODO: in REAL world, if we're working without datalayer cache, all requests to revisions will be fired - // BEFORE first result will be landed to our cache object. The solution is to replace parallel processing - // via async.forEach with sequential for() loop. There is no real benefits of running this in parallel, + // TODO: in REAL world, if we're working without datalayer cache, + // all requests to revisions will be fired + // BEFORE first result will be landed to our cache object. + // The solution is to replace parallel processing + // via async.forEach with sequential for() loop. There is no real + // benefits of running this in parallel, // but benefit of reusing cached revision object is HUGE const revCache = {}; @@ -737,7 +761,7 @@ exports.updatePadClients = async function (pad) { /** * Copied from the Etherpad Source Code. Don't know what this method does excatly... */ -function _correctMarkersInPad(atext, apool) { +const _correctMarkersInPad = (atext, apool) => { const text = atext.text; // collect char positions of line markers (e.g. bullets) in new atext @@ -746,9 +770,11 @@ function _correctMarkersInPad(atext, apool) { const iter = Changeset.opIterator(atext.attribs); let offset = 0; while (iter.hasNext()) { - var op = iter.next(); + const op = iter.next(); - const hasMarker = _.find(AttributeManager.lineAttributes, (attribute) => Changeset.opAttributeValue(op, attribute, apool)) !== undefined; + const hasMarker = _.find( + AttributeManager.lineAttributes, + (attribute) => Changeset.opAttributeValue(op, attribute, apool)) !== undefined; if (hasMarker) { for (let i = 0; i < op.chars; i++) { @@ -778,9 +804,9 @@ function _correctMarkersInPad(atext, apool) { }); return builder.toString(); -} +}; -async function handleSwitchToPad(socket, message, _authorID) { +const handleSwitchToPad = async (socket, message, _authorID) => { const currentSessionInfo = sessioninfos[socket.id]; const padId = currentSessionInfo.padId; @@ -816,10 +842,10 @@ async function handleSwitchToPad(socket, message, _authorID) { const newSessionInfo = sessioninfos[socket.id]; createSessionInfoAuth(newSessionInfo, message); await handleClientReady(socket, message, authorID); -} +}; // Creates/replaces the auth object in the given session info. -function createSessionInfoAuth(sessionInfo, message) { +const createSessionInfoAuth = (sessionInfo, message) => { // Remember this information since we won't // have the cookie in further socket.io messages. // This information will be used to check if @@ -830,15 +856,16 @@ function createSessionInfoAuth(sessionInfo, message) { padID: message.padId, token: message.token, }; -} +}; /** - * Handles a CLIENT_READY. A CLIENT_READY is the first message from the client to the server. The Client sends his token + * Handles a CLIENT_READY. A CLIENT_READY is the first message from the client + * to the server. The Client sends his token * and the pad it wants to enter. The Server answers with the inital values (clientVars) of the pad * @param socket the socket.io Socket object for the client * @param message the message from the client */ -async function handleClientReady(socket, message, authorID) { +const handleClientReady = async (socket, message, authorID) => { // check if all ok if (!message.token) { messageLogger.warn('Dropped message, CLIENT_READY Message has no token!'); @@ -884,9 +911,11 @@ async function handleClientReady(socket, message, authorID) { const historicalAuthorData = {}; await Promise.all(authors.map((authorId) => authorManager.getAuthor(authorId).then((author) => { if (!author) { - messageLogger.error('There is no author for authorId: ', authorId, '. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802'); + messageLogger.error(`There is no author for authorId: ${authorId}. ` + + 'This is possibly related to https://github.com/ether/etherpad-lite/issues/2802'); } else { - historicalAuthorData[authorId] = {name: author.name, colorId: author.colorId}; // Filter author attribs (e.g. don't send author's pads to all clients) + // Filter author attribs (e.g. don't send author's pads to all clients) + historicalAuthorData[authorId] = {name: author.name, colorId: author.colorId}; } }))); @@ -931,7 +960,8 @@ async function handleClientReady(socket, message, authorID) { // Save the revision in sessioninfos, we take the revision from the info the client send to us sessionInfo.rev = message.client_rev; - // During the client reconnect, client might miss some revisions from other clients. By using client revision, + // During the client reconnect, client might miss some revisions from other clients. + // By using client revision, // this below code sends all the revisions missed during the client reconnect const revisionsNeeded = []; const changesets = {}; @@ -987,12 +1017,13 @@ async function handleClientReady(socket, message, authorID) { } } else { // This is a normal first connect - + let atext; + let apool; // prepare all values for the wire, there's a chance that this throws, if the pad is corrupted try { - var atext = Changeset.cloneAText(pad.atext); + atext = Changeset.cloneAText(pad.atext); const attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool); - var apool = attribsForWire.pool.toJsonable(); + apool = attribsForWire.pool.toJsonable(); atext.attribs = attribsForWire.translated; } catch (e) { console.error(e.stack || e); @@ -1147,12 +1178,12 @@ async function handleClientReady(socket, message, authorID) { socket.json.send(msg); })); } -} +}; /** * Handles a request for a rough changeset, the timeslider client needs it */ -async function handleChangesetRequest(socket, message) { +const handleChangesetRequest = async (socket, message) => { // check if all ok if (message.data == null) { messageLogger.warn('Dropped message, changeset request has no data!'); @@ -1197,15 +1228,16 @@ async function handleChangesetRequest(socket, message) { data.requestID = message.data.requestID; socket.json.send({type: 'CHANGESET_REQ', data}); } catch (err) { - console.error(`Error while handling a changeset request for ${padIds.padId}`, err.toString(), message.data); + console.error(`Error while handling a changeset request for ${padIds.padId}`, + err.toString(), message.data); } -} +}; /** * Tries to rebuild the getChangestInfo function of the original Etherpad * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L144 */ -async function getChangesetInfo(padId, startNum, endNum, granularity) { +const getChangesetInfo = async (padId, startNum, endNum, granularity) => { const pad = await padManager.getPad(padId); const head_revision = pad.getHeadRevisionNumber(); @@ -1237,15 +1269,25 @@ async function getChangesetInfo(padId, startNum, endNum, granularity) { // get all needed composite Changesets const composedChangesets = {}; - const p1 = Promise.all(compositesChangesetNeeded.map((item) => composePadChangesets(padId, item.start, item.end).then((changeset) => { - composedChangesets[`${item.start}/${item.end}`] = changeset; - }))); + const p1 = Promise.all( + compositesChangesetNeeded.map( + (item) => composePadChangesets( + padId, item.start, item.end + ).then( + (changeset) => { + composedChangesets[`${item.start}/${item.end}`] = changeset; + } + ) + ) + ); // get all needed revision Dates const revisionDate = []; - const p2 = Promise.all(revTimesNeeded.map((revNum) => pad.getRevisionDate(revNum).then((revDate) => { - revisionDate[revNum] = Math.floor(revDate / 1000); - }))); + const p2 = Promise.all(revTimesNeeded.map((revNum) => pad.getRevisionDate(revNum) + .then((revDate) => { + revisionDate[revNum] = Math.floor(revDate / 1000); + }) + )); // get the lines let lines; @@ -1288,13 +1330,13 @@ async function getChangesetInfo(padId, startNum, endNum, granularity) { return {forwardsChangesets, backwardsChangesets, apool: apool.toJsonable(), actualEndNum: endNum, timeDeltas, start: startNum, granularity}; -} +}; /** * Tries to rebuild the getPadLines function of the original Etherpad * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L263 */ -async function getPadLines(padId, revNum) { +const getPadLines = async (padId, revNum) => { const pad = await padManager.getPad(padId); // get the atext @@ -1310,13 +1352,13 @@ async function getPadLines(padId, revNum) { textlines: Changeset.splitTextLines(atext.text), alines: Changeset.splitAttributionLines(atext.attribs, atext.text), }; -} +}; /** * Tries to rebuild the composePadChangeset function of the original Etherpad * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L241 */ -async function composePadChangesets(padId, startNum, endNum) { +const composePadChangesets = async (padId, startNum, endNum) => { const pad = await padManager.getPad(padId); // fetch all changesets we need @@ -1333,7 +1375,9 @@ async function composePadChangesets(padId, startNum, endNum) { // get all changesets const changesets = {}; - await Promise.all(changesetsNeeded.map((revNum) => pad.getRevisionChangeset(revNum).then((changeset) => changesets[revNum] = changeset))); + await Promise.all(changesetsNeeded.map( + (revNum) => pad.getRevisionChangeset(revNum).then((changeset) => changesets[revNum] = changeset) + )); // compose Changesets let r; @@ -1351,9 +1395,9 @@ async function composePadChangesets(padId, startNum, endNum) { console.warn('failed to compose cs in pad:', padId, ' startrev:', startNum, ' current rev:', r); throw e; } -} +}; -function _getRoomSockets(padID) { +const _getRoomSockets = (padID) => { const roomSockets = []; const room = socketio.sockets.adapter.rooms[padID]; @@ -1364,21 +1408,19 @@ function _getRoomSockets(padID) { } return roomSockets; -} +}; /** * Get the number of users in a pad */ -exports.padUsersCount = function (padID) { - return { - padUsersCount: _getRoomSockets(padID).length, - }; -}; +exports.padUsersCount = (padID) => ({ + padUsersCount: _getRoomSockets(padID).length, +}); /** * Get the list of users in a pad */ -exports.padUsers = async function (padID) { +exports.padUsers = async (padID) => { const padUsers = []; // iterate over all clients (in parallel) diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index 56e5c5be4..35325c072 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -1,3 +1,4 @@ +'use strict'; /** * This is the Socket.IO Router. It routes the Messages between the * components of the Server. The components are at the moment: pad and timeslider @@ -21,9 +22,7 @@ const log4js = require('log4js'); const messageLogger = log4js.getLogger('message'); -const securityManager = require('../db/SecurityManager'); -const readOnlyManager = require('../db/ReadOnlyManager'); -const settings = require('../utils/Settings'); +const stats = require('../stats'); /** * Saves all components @@ -37,7 +36,7 @@ let socket; /** * adds a component */ -exports.addComponent = function (moduleName, module) { +exports.addComponent = (moduleName, module) => { // save the component components[moduleName] = module; @@ -48,14 +47,14 @@ exports.addComponent = function (moduleName, module) { /** * sets the socket.io and adds event functions for routing */ -exports.setSocketIO = function (_socket) { +exports.setSocketIO = (_socket) => { // save this socket internaly socket = _socket; socket.sockets.on('connection', (client) => { // wrap the original send function to log the messages client._send = client.send; - client.send = function (message) { + client.send = (message) => { messageLogger.debug(`to ${client.id}: ${JSON.stringify(message)}`); client._send(message); }; @@ -66,7 +65,7 @@ exports.setSocketIO = function (_socket) { } client.on('message', async (message) => { - if (message.protocolVersion && message.protocolVersion != 2) { + if (message.protocolVersion && message.protocolVersion !== 2) { messageLogger.warn(`Protocolversion header is not correct: ${JSON.stringify(message)}`); return; } @@ -79,6 +78,11 @@ exports.setSocketIO = function (_socket) { }); client.on('disconnect', () => { + // store the lastDisconnect as a timestamp, this is useful if you want to know + // when the last user disconnected. If your activePads is 0 and totalUsers is 0 + // you can say, if there has been no active pads or active users for 10 minutes + // this instance can be brought out of a scaling cluster. + stats.gauge('lastDisconnect', () => Date.now()); // tell all components about this disconnect for (const i in components) { components[i].handleDisconnect(client); diff --git a/src/node/hooks/express.js b/src/node/hooks/express.js index b3d4f34e4..351ab5bf2 100644 --- a/src/node/hooks/express.js +++ b/src/node/hooks/express.js @@ -2,6 +2,7 @@ const _ = require('underscore'); const cookieParser = require('cookie-parser'); +const events = require('events'); const express = require('express'); const expressSession = require('express-session'); const fs = require('fs'); @@ -14,17 +15,38 @@ const util = require('util'); const logger = log4js.getLogger('http'); let serverName; +const sockets = new Set(); +const socketsEvents = new events.EventEmitter(); +const startTime = stats.settableGauge('httpStartTime'); exports.server = null; const closeServer = async () => { if (exports.server == null) return; logger.info('Closing HTTP server...'); - await Promise.all([ - util.promisify(exports.server.close.bind(exports.server))(), - hooks.aCallAll('expressCloseServer'), - ]); + // Call exports.server.close() to reject new connections but don't await just yet because the + // Promise won't resolve until all preexisting connections are closed. + const p = util.promisify(exports.server.close.bind(exports.server))(); + await hooks.aCallAll('expressCloseServer'); + // Give existing connections some time to close on their own before forcibly terminating. The time + // should be long enough to avoid interrupting most preexisting transmissions but short enough to + // avoid a noticeable outage. + const timeout = setTimeout(async () => { + logger.info(`Forcibly terminating remaining ${sockets.size} HTTP connections...`); + for (const socket of sockets) socket.destroy(new Error('HTTP server is closing')); + }, 5000); + let lastLogged = 0; + while (sockets.size > 0) { + if (Date.now() - lastLogged > 1000) { // Rate limit to avoid filling logs. + logger.info(`Waiting for ${sockets.size} HTTP clients to disconnect...`); + lastLogged = Date.now(); + } + await events.once(socketsEvents, 'updated'); + } + await p; + clearTimeout(timeout); exports.server = null; + startTime.setValue(0); logger.info('HTTP server closed'); }; @@ -182,10 +204,21 @@ exports.restartServer = async () => { app.use(cookieParser(settings.sessionKey, {})); - hooks.callAll('expressConfigure', {app}); - hooks.callAll('expressCreateServer', {app, server: exports.server}); - + await Promise.all([ + hooks.aCallAll('expressConfigure', {app}), + hooks.aCallAll('expressCreateServer', {app, server: exports.server}), + ]); + exports.server.on('connection', (socket) => { + sockets.add(socket); + socketsEvents.emit('updated'); + socket.on('close', () => { + sockets.delete(socket); + socketsEvents.emit('updated'); + }); + }); await util.promisify(exports.server.listen).bind(exports.server)(settings.port, settings.ip); + startTime.setValue(Date.now()); + logger.info('HTTP server listening for connections'); }; exports.shutdown = async (hookName, context) => { diff --git a/src/node/hooks/express/admin.js b/src/node/hooks/express/admin.js index 417939600..763698332 100644 --- a/src/node/hooks/express/admin.js +++ b/src/node/hooks/express/admin.js @@ -1,8 +1,9 @@ -const eejs = require('ep_etherpad-lite/node/eejs'); +'use strict'; +const eejs = require('../../eejs'); -exports.expressCreateServer = function (hook_name, args, cb) { +exports.expressCreateServer = (hookName, args, cb) => { args.app.get('/admin', (req, res) => { - if ('/' != req.path[req.path.length - 1]) return res.redirect('./admin/'); + if ('/' !== req.path[req.path.length - 1]) return res.redirect('./admin/'); res.send(eejs.require('ep_etherpad-lite/templates/admin/index.html', {req})); }); return cb(); diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.js index 0a6d97808..7c0db0973 100644 --- a/src/node/hooks/express/adminplugins.js +++ b/src/node/hooks/express/adminplugins.js @@ -3,15 +3,15 @@ const eejs = require('../../eejs'); const settings = require('../../utils/Settings'); const installer = require('../../../static/js/pluginfw/installer'); -const plugins = require('../../../static/js/pluginfw/plugin_defs'); -const _ = require('underscore'); +const pluginDefs = require('../../../static/js/pluginfw/plugin_defs'); +const plugins = require('../../../static/js/pluginfw/plugins'); const semver = require('semver'); const UpdateCheck = require('../../utils/UpdateCheck'); exports.expressCreateServer = (hookName, args, cb) => { args.app.get('/admin/plugins', (req, res) => { res.send(eejs.require('ep_etherpad-lite/templates/admin/plugins.html', { - plugins: plugins.plugins, + plugins: pluginDefs.plugins, req, errors: [], })); @@ -24,6 +24,10 @@ exports.expressCreateServer = (hookName, args, cb) => { res.send(eejs.require('ep_etherpad-lite/templates/admin/plugins-info.html', { gitCommit, epVersion, + installedPlugins: `
${plugins.formatPlugins().replace(/, /g, '\n')}
`, + installedParts: `
${plugins.formatParts()}
`, + installedServerHooks: `
${plugins.formatHooks()}
`, + installedClientHooks: `
${plugins.formatHooks('client_hooks')}
`, latestVersion: UpdateCheck.getLatestVersion(), req, })); @@ -41,7 +45,7 @@ exports.socketio = (hookName, args, cb) => { socket.on('getInstalled', (query) => { // send currently installed plugins const installed = - Object.keys(plugins.plugins).map((plugin) => plugins.plugins[plugin].package); + Object.keys(pluginDefs.plugins).map((plugin) => pluginDefs.plugins[plugin].package); socket.emit('results:installed', {installed}); }); @@ -51,18 +55,18 @@ exports.socketio = (hookName, args, cb) => { try { const results = await installer.getAvailablePlugins(/* maxCacheAge:*/ 60 * 10); - const updatable = _(plugins.plugins).keys().filter((plugin) => { + const updatable = Object.keys(pluginDefs.plugins).filter((plugin) => { if (!results[plugin]) return false; const latestVersion = results[plugin].version; - const currentVersion = plugins.plugins[plugin].package.version; + const currentVersion = pluginDefs.plugins[plugin].package.version; return semver.gt(latestVersion, currentVersion); }); socket.emit('results:updatable', {updatable}); - } catch (er) { - console.warn(er); + } catch (err) { + console.warn(err.stack || err.toString()); socket.emit('results:updatable', {updatable: {}}); } @@ -83,7 +87,7 @@ exports.socketio = (hookName, args, cb) => { const results = await installer.search(query.searchTerm, /* maxCacheAge:*/ 60 * 10); let res = Object.keys(results) .map((pluginName) => results[pluginName]) - .filter((plugin) => !plugins.plugins[plugin.name]); + .filter((plugin) => !pluginDefs.plugins[plugin.name]); res = sortPluginList(res, query.sortBy, query.sortDir) .slice(query.offset, query.offset + query.limit); socket.emit('results:search', {results: res, query}); @@ -95,22 +99,22 @@ exports.socketio = (hookName, args, cb) => { }); socket.on('install', (pluginName) => { - installer.install(pluginName, (er) => { - if (er) console.warn(er); + installer.install(pluginName, (err) => { + if (err) console.warn(err.stack || err.toString()); socket.emit('finished:install', { plugin: pluginName, - code: er ? er.code : null, - error: er ? er.message : null, + code: err ? err.code : null, + error: err ? err.message : null, }); }); }); socket.on('uninstall', (pluginName) => { - installer.uninstall(pluginName, (er) => { - if (er) console.warn(er); + installer.uninstall(pluginName, (err) => { + if (err) console.warn(err.stack || err.toString()); - socket.emit('finished:uninstall', {plugin: pluginName, error: er ? er.message : null}); + socket.emit('finished:uninstall', {plugin: pluginName, error: err ? err.message : null}); }); }); }); diff --git a/src/node/hooks/express/adminsettings.js b/src/node/hooks/express/adminsettings.js index 139cce1b1..8cbf3762a 100644 --- a/src/node/hooks/express/adminsettings.js +++ b/src/node/hooks/express/adminsettings.js @@ -3,6 +3,7 @@ const eejs = require('../../eejs'); const fs = require('fs'); const hooks = require('../../../static/js/pluginfw/hooks'); +const plugins = require('../../../static/js/pluginfw/plugins'); const settings = require('../../utils/Settings'); exports.expressCreateServer = (hookName, args, cb) => { @@ -47,6 +48,8 @@ exports.socketio = (hookName, args, cb) => { socket.on('restartServer', async () => { console.log('Admin request to restart server through a socket on /admin/settings'); settings.reloadSettings(); + await plugins.update(); + await hooks.aCallAll('loadSettings', {settings}); await hooks.aCallAll('restartServer'); }); }); diff --git a/src/node/hooks/express/apicalls.js b/src/node/hooks/express/apicalls.js index c87998e94..b72ed11e5 100644 --- a/src/node/hooks/express/apicalls.js +++ b/src/node/hooks/express/apicalls.js @@ -1,9 +1,11 @@ +'use strict'; + const log4js = require('log4js'); const clientLogger = log4js.getLogger('client'); const formidable = require('formidable'); const apiHandler = require('../../handler/APIHandler'); -exports.expressCreateServer = function (hook_name, args, cb) { +exports.expressCreateServer = (hookName, args, cb) => { // The Etherpad client side sends information about how a disconnect happened args.app.post('/ep/pad/connection-diagnostic-info', (req, res) => { new formidable.IncomingForm().parse(req, (err, fields, files) => { @@ -15,8 +17,9 @@ exports.expressCreateServer = function (hook_name, args, cb) { // The Etherpad client side sends information about client side javscript errors args.app.post('/jserror', (req, res) => { new formidable.IncomingForm().parse(req, (err, fields, files) => { + let data; try { - var data = JSON.parse(fields.errorInfo); + data = JSON.parse(fields.errorInfo); } catch (e) { return res.end(); } diff --git a/src/node/hooks/express/errorhandling.js b/src/node/hooks/express/errorhandling.js index 4a20b70d2..884ca9be0 100644 --- a/src/node/hooks/express/errorhandling.js +++ b/src/node/hooks/express/errorhandling.js @@ -1,6 +1,8 @@ -const stats = require('ep_etherpad-lite/node/stats'); +'use strict'; -exports.expressCreateServer = function (hook_name, args, cb) { +const stats = require('../../stats'); + +exports.expressCreateServer = (hook_name, args, cb) => { exports.app = args.app; // Handle errors diff --git a/src/node/hooks/express/importexport.js b/src/node/hooks/express/importexport.js index 7a6c38655..598629632 100644 --- a/src/node/hooks/express/importexport.js +++ b/src/node/hooks/express/importexport.js @@ -1,74 +1,82 @@ -const assert = require('assert').strict; +'use strict'; + const hasPadAccess = require('../../padaccess'); const settings = require('../../utils/Settings'); const exportHandler = require('../../handler/ExportHandler'); const importHandler = require('../../handler/ImportHandler'); const padManager = require('../../db/PadManager'); const readOnlyManager = require('../../db/ReadOnlyManager'); -const authorManager = require('../../db/AuthorManager'); const rateLimit = require('express-rate-limit'); const securityManager = require('../../db/SecurityManager'); const webaccess = require('./webaccess'); -settings.importExportRateLimiting.onLimitReached = function (req, res, options) { +settings.importExportRateLimiting.onLimitReached = (req, res, options) => { // when the rate limiter triggers, write a warning in the logs - console.warn(`Import/Export rate limiter triggered on "${req.originalUrl}" for IP address ${req.ip}`); + console.warn('Import/Export rate limiter triggered on ' + + `"${req.originalUrl}" for IP address ${req.ip}`); }; const limiter = rateLimit(settings.importExportRateLimiting); -exports.expressCreateServer = function (hook_name, args, cb) { +exports.expressCreateServer = (hookName, args, cb) => { // handle export requests args.app.use('/p/:pad/:rev?/export/:type', limiter); - args.app.get('/p/:pad/:rev?/export/:type', async (req, res, next) => { - const types = ['pdf', 'doc', 'txt', 'html', 'odt', 'etherpad']; - // send a 404 if we don't support this filetype - if (types.indexOf(req.params.type) == -1) { - return next(); - } - - // if abiword is disabled, and this is a format we only support with abiword, output a message - if (settings.exportAvailable() == 'no' && - ['odt', 'pdf', 'doc'].indexOf(req.params.type) !== -1) { - console.error(`Impossible to export pad "${req.params.pad}" in ${req.params.type} format. There is no converter configured`); - - // ACHTUNG: do not include req.params.type in res.send() because there is no HTML escaping and it would lead to an XSS - res.send('This export is not enabled at this Etherpad instance. Set the path to Abiword or soffice (LibreOffice) in settings.json to enable this feature'); - return; - } - - res.header('Access-Control-Allow-Origin', '*'); - - if (await hasPadAccess(req, res)) { - let padId = req.params.pad; - - let readOnlyId = null; - if (readOnlyManager.isReadOnlyId(padId)) { - readOnlyId = padId; - padId = await readOnlyManager.getPadId(readOnlyId); - } - - const exists = await padManager.doesPadExists(padId); - if (!exists) { - console.warn(`Someone tried to export a pad that doesn't exist (${padId})`); + args.app.get('/p/:pad/:rev?/export/:type', (req, res, next) => { + (async () => { + const types = ['pdf', 'doc', 'txt', 'html', 'odt', 'etherpad']; + // send a 404 if we don't support this filetype + if (types.indexOf(req.params.type) === -1) { return next(); } - console.log(`Exporting pad "${req.params.pad}" in ${req.params.type} format`); - exportHandler.doExport(req, res, padId, readOnlyId, req.params.type); - } + // if abiword is disabled, and this is a format we only support with abiword, output a message + if (settings.exportAvailable() === 'no' && + ['odt', 'pdf', 'doc'].indexOf(req.params.type) !== -1) { + console.error(`Impossible to export pad "${req.params.pad}" in ${req.params.type} format.` + + ' There is no converter configured'); + + // ACHTUNG: do not include req.params.type in res.send() because there is + // no HTML escaping and it would lead to an XSS + res.send('This export is not enabled at this Etherpad instance. Set the path to Abiword' + + ' or soffice (LibreOffice) in settings.json to enable this feature'); + return; + } + + res.header('Access-Control-Allow-Origin', '*'); + + if (await hasPadAccess(req, res)) { + let padId = req.params.pad; + + let readOnlyId = null; + if (readOnlyManager.isReadOnlyId(padId)) { + readOnlyId = padId; + padId = await readOnlyManager.getPadId(readOnlyId); + } + + const exists = await padManager.doesPadExists(padId); + if (!exists) { + console.warn(`Someone tried to export a pad that doesn't exist (${padId})`); + return next(); + } + + console.log(`Exporting pad "${req.params.pad}" in ${req.params.type} format`); + exportHandler.doExport(req, res, padId, readOnlyId, req.params.type); + } + })().catch((err) => next(err || new Error(err))); }); // handle import requests args.app.use('/p/:pad/import', limiter); - args.app.post('/p/:pad/import', async (req, res, next) => { - const {session: {user} = {}} = req; - const {accessStatus} = await securityManager.checkAccess( - req.params.pad, req.cookies.sessionID, req.cookies.token, user); - if (accessStatus !== 'grant' || !webaccess.userCanModify(req.params.pad, req)) { - return res.status(403).send('Forbidden'); - } - await importHandler.doImport(req, res, req.params.pad); + args.app.post('/p/:pad/import', (req, res, next) => { + (async () => { + const {session: {user} = {}} = req; + const {accessStatus} = await securityManager.checkAccess( + req.params.pad, req.cookies.sessionID, req.cookies.token, user); + if (accessStatus !== 'grant' || !webaccess.userCanModify(req.params.pad, req)) { + return res.status(403).send('Forbidden'); + } + await importHandler.doImport(req, res, req.params.pad); + })().catch((err) => next(err || new Error(err))); }); return cb(); diff --git a/src/node/hooks/express/isValidJSONPName.js b/src/node/hooks/express/isValidJSONPName.js index 442c963e9..c8ca5bea1 100644 --- a/src/node/hooks/express/isValidJSONPName.js +++ b/src/node/hooks/express/isValidJSONPName.js @@ -1,3 +1,5 @@ +'use strict'; + const RESERVED_WORDS = [ 'abstract', 'arguments', @@ -65,9 +67,9 @@ const RESERVED_WORDS = [ 'yield', ]; -const regex = /^[a-zA-Z_$][0-9a-zA-Z_$]*(?:\[(?:".+"|\'.+\'|\d+)\])*?$/; +const regex = /^[a-zA-Z_$][0-9a-zA-Z_$]*(?:\[(?:".+"|'.+'|\d+)\])*?$/; -module.exports.check = function (inputStr) { +module.exports.check = (inputStr) => { let isValid = true; inputStr.split('.').forEach((part) => { if (!regex.test(part)) { diff --git a/src/node/hooks/express/openapi.js b/src/node/hooks/express/openapi.js index 8ea9529c7..bf7e5b147 100644 --- a/src/node/hooks/express/openapi.js +++ b/src/node/hooks/express/openapi.js @@ -1,3 +1,5 @@ +'use strict'; + /** * node/hooks/express/openapi.js * @@ -23,7 +25,7 @@ const settings = require('../../utils/Settings'); const isValidJSONPName = require('./isValidJSONPName'); const log4js = require('log4js'); -const apiLogger = log4js.getLogger('API'); +const logger = log4js.getLogger('API'); // https://github.com/OAI/OpenAPI-Specification/tree/master/schemas/v3.0 const OPENAPI_VERSION = '3.0.2'; // Swagger/OAS version @@ -31,7 +33,9 @@ const OPENAPI_VERSION = '3.0.2'; // Swagger/OAS version const info = { title: 'Etherpad API', description: - 'Etherpad is a real-time collaborative editor scalable to thousands of simultaneous real time users. It provides full data export capabilities, and runs on your server, under your control.', + 'Etherpad is a real-time collaborative editor scalable to thousands of simultaneous ' + + 'real time users. It provides full data export capabilities, and runs on your server, ' + + 'under your control.', termsOfService: 'https://etherpad.org/', contact: { name: 'The Etherpad Foundation', @@ -80,7 +84,9 @@ const resources = { listSessions: { operationId: 'listSessionsOfGroup', summary: '', - responseSchema: {sessions: {type: 'array', items: {$ref: '#/components/schemas/SessionInfo'}}}, + responseSchema: { + sessions: {type: 'array', items: {$ref: '#/components/schemas/SessionInfo'}}, + }, }, list: { operationId: 'listAllGroups', @@ -109,7 +115,9 @@ const resources = { listSessions: { operationId: 'listSessionsOfAuthor', summary: 'returns all sessions of an author', - responseSchema: {sessions: {type: 'array', items: {$ref: '#/components/schemas/SessionInfo'}}}, + responseSchema: { + sessions: {type: 'array', items: {$ref: '#/components/schemas/SessionInfo'}}, + }, }, // We need an operation that return a UserInfo so it can be picked up by the codegen :( getName: { @@ -133,7 +141,7 @@ const resources = { // We need an operation that returns a SessionInfo so it can be picked up by the codegen :( info: { operationId: 'getSessionInfo', - summary: 'returns informations about a session', + summary: 'returns information about a session', responseSchema: {info: {$ref: '#/components/schemas/SessionInfo'}}, }, }, @@ -153,7 +161,8 @@ const resources = { create: { operationId: 'createPad', description: - 'creates a new (non-group) pad. Note that if you need to create a group Pad, you should call createGroupPad', + 'creates a new (non-group) pad. Note that if you need to create a group Pad, ' + + 'you should call createGroupPad', }, getText: { operationId: 'getText', @@ -382,9 +391,9 @@ const defaultResponseRefs = { // convert to a dictionary of operation objects const operations = {}; -for (const resource in resources) { - for (const action in resources[resource]) { - const {operationId, responseSchema, ...operation} = resources[resource][action]; +for (const [resource, actions] of Object.entries(resources)) { + for (const [action, spec] of Object.entries(actions)) { + const {operationId, responseSchema, ...operation} = spec; // add response objects const responses = {...defaultResponseRefs}; @@ -598,8 +607,9 @@ exports.expressCreateServer = (hookName, args, cb) => { const fields = Object.assign({}, header, params, query, formData); - // log request - apiLogger.info(`REQUEST, v${version}:${funcName}, ${JSON.stringify(fields)}`); + if (logger.isDebugEnabled()) { + logger.debug(`REQUEST, v${version}:${funcName}, ${JSON.stringify(fields)}`); + } // pass to api handler const data = await apiHandler.handle(version, funcName, fields, req, res).catch((err) => { @@ -607,14 +617,14 @@ exports.expressCreateServer = (hookName, args, cb) => { if (createHTTPError.isHttpError(err)) { // pass http errors thrown by handler forward throw err; - } else if (err.name == 'apierror') { + } else if (err.name === 'apierror') { // parameters were wrong and the api stopped execution, pass the error // convert to http error throw new createHTTPError.BadRequest(err.message); } else { // an unknown error happened // log it and throw internal error - apiLogger.error(err); + logger.error(err.stack || err.toString()); throw new createHTTPError.InternalError('internal error'); } }); @@ -622,8 +632,9 @@ exports.expressCreateServer = (hookName, args, cb) => { // return in common format const response = {code: 0, message: 'ok', data: data || null}; - // log response - apiLogger.info(`RESPONSE, ${funcName}, ${JSON.stringify(response)}`); + if (logger.isDebugEnabled()) { + logger.debug(`RESPONSE, ${funcName}, ${JSON.stringify(response)}`); + } // return the response data return response; diff --git a/src/node/hooks/express/padreadonly.js b/src/node/hooks/express/padreadonly.js index f17f7f0d6..4dda67b1f 100644 --- a/src/node/hooks/express/padreadonly.js +++ b/src/node/hooks/express/padreadonly.js @@ -1,8 +1,10 @@ +'use strict'; + const readOnlyManager = require('../../db/ReadOnlyManager'); const hasPadAccess = require('../../padaccess'); const exporthtml = require('../../utils/ExportHtml'); -exports.expressCreateServer = function (hook_name, args, cb) { +exports.expressCreateServer = (hookName, args, cb) => { // serve read only pad args.app.get('/ro/:id', async (req, res) => { // translate the read only pad to a padId diff --git a/src/node/hooks/express/padurlsanitize.js b/src/node/hooks/express/padurlsanitize.js index 8a287a961..c4c1b6751 100644 --- a/src/node/hooks/express/padurlsanitize.js +++ b/src/node/hooks/express/padurlsanitize.js @@ -1,7 +1,9 @@ +'use strict'; + const padManager = require('../../db/PadManager'); const url = require('url'); -exports.expressCreateServer = function (hook_name, args, cb) { +exports.expressCreateServer = (hookName, args, cb) => { // redirects browser to the pad's sanitized url if needed. otherwise, renders the html args.app.param('pad', async (req, res, next, padId) => { // ensure the padname is valid and the url doesn't end with a / @@ -17,12 +19,12 @@ exports.expressCreateServer = function (hook_name, args, cb) { next(); } else { // the pad id was sanitized, so we redirect to the sanitized version - let real_url = sanitizedPadId; - real_url = encodeURIComponent(real_url); + let realURL = sanitizedPadId; + realURL = encodeURIComponent(realURL); const query = url.parse(req.url).query; - if (query) real_url += `?${query}`; - res.header('Location', real_url); - res.status(302).send(`You should be redirected to ${real_url}`); + if (query) realURL += `?${query}`; + res.header('Location', realURL); + res.status(302).send(`You should be redirected to ${realURL}`); } }); return cb(); diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js index 3d9e9debe..47a657747 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.js @@ -1,31 +1,49 @@ 'use strict'; +const events = require('events'); const express = require('../express'); +const log4js = require('log4js'); const proxyaddr = require('proxy-addr'); const settings = require('../../utils/Settings'); const socketio = require('socket.io'); const socketIORouter = require('../../handler/SocketIORouter'); const hooks = require('../../../static/js/pluginfw/hooks'); const padMessageHandler = require('../../handler/PadMessageHandler'); -const util = require('util'); let io; +const logger = log4js.getLogger('socket.io'); +const sockets = new Set(); +const socketsEvents = new events.EventEmitter(); exports.expressCloseServer = async () => { - // According to the socket.io documentation every client is always in the default namespace (and - // may also be in other namespaces). - const ns = io.sockets; // The Namespace object for the default namespace. - // Disconnect all socket.io clients. This probably isn't necessary; closing the socket.io Engine - // (see below) probably gracefully disconnects all clients. But that is not documented, and this - // doesn't seem to hurt, so hedge against surprising and undocumented socket.io behavior. - for (const id of await util.promisify(ns.clients.bind(ns))()) { - ns.connected[id].disconnect(true); - } - // Don't call io.close() because that closes the underlying HTTP server, which is already done - // elsewhere. (Closing an HTTP server twice throws an exception.) The `engine` property of - // socket.io Server objects is undocumented, but I don't see any other way to shut down socket.io - // without also closing the HTTP server. + if (io == null) return; + logger.info('Closing socket.io engine...'); + // Close the socket.io engine to disconnect existing clients and reject new clients. Don't call + // io.close() because that closes the underlying HTTP server, which is already done elsewhere. + // (Closing an HTTP server twice throws an exception.) The `engine` property of socket.io Server + // objects is undocumented, but I don't see any other way to shut down socket.io without also + // closing the HTTP server. io.engine.close(); + // Closing the socket.io engine should disconnect all clients but it is not documented. Wait for + // all of the connections to close to make sure, and log the progress so that we can troubleshoot + // if socket.io's behavior ever changes. + // + // Note: `io.sockets.clients()` should not be used here to track the remaining clients. + // `io.sockets.clients()` works with socket.io 2.x, but not with 3.x: With socket.io 2.x all + // clients are always added to the default namespace (`io.sockets`) even if they specified a + // different namespace upon connection, but with socket.io 3.x clients are NOT added to the + // default namespace if they have specified a different namespace. With socket.io 3.x there does + // not appear to be a way to get all clients across all namespaces without tracking them + // ourselves, so that is what we do. + let lastLogged = 0; + while (sockets.size > 0) { + if (Date.now() - lastLogged > 1000) { // Rate limit to avoid filling logs. + logger.info(`Waiting for ${sockets.size} socket.io clients to disconnect...`); + lastLogged = Date.now(); + } + await events.once(socketsEvents, 'updated'); + } + logger.info('All socket.io clients have disconnected'); }; exports.expressCreateServer = (hookName, args, cb) => { @@ -56,6 +74,16 @@ exports.expressCreateServer = (hookName, args, cb) => { * https://github.com/socketio/socket.io/issues/2276#issuecomment-147184662 (not totally true, actually, see above) */ cookie: false, + maxHttpBufferSize: settings.socketIo.maxHttpBufferSize, + }); + + io.on('connect', (socket) => { + sockets.add(socket); + socketsEvents.emit('updated'); + socket.on('disconnect', () => { + sockets.delete(socket); + socketsEvents.emit('updated'); + }); }); io.use((socket, next) => { @@ -88,7 +116,7 @@ exports.expressCreateServer = (hookName, args, cb) => { // http://stackoverflow.com/questions/23981741/minify-socket-io-socket-io-js-with-1-0 // if(settings.minify) io.enable('browser client minification'); - // Initalize the Socket.IO Router + // Initialize the Socket.IO Router socketIORouter.setSocketIO(io); socketIORouter.addComponent('pad', padMessageHandler); diff --git a/src/node/hooks/express/specialpages.js b/src/node/hooks/express/specialpages.js index f53ce1ac7..61471348d 100644 --- a/src/node/hooks/express/specialpages.js +++ b/src/node/hooks/express/specialpages.js @@ -1,14 +1,16 @@ +'use strict'; + const path = require('path'); -const eejs = require('ep_etherpad-lite/node/eejs'); -const toolbar = require('ep_etherpad-lite/node/utils/toolbar'); -const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); +const eejs = require('../../eejs'); +const toolbar = require('../../utils/toolbar'); +const hooks = require('../../../static/js/pluginfw/hooks'); const settings = require('../../utils/Settings'); const webaccess = require('./webaccess'); -exports.expressCreateServer = function (hook_name, args, cb) { +exports.expressCreateServer = (hookName, args, cb) => { // expose current stats args.app.get('/stats', (req, res) => { - res.json(require('ep_etherpad-lite/node/stats').toJSON()); + res.json(require('../../stats').toJSON()); }); // serve index.html under / @@ -24,7 +26,14 @@ exports.expressCreateServer = function (hook_name, args, cb) { // serve robots.txt args.app.get('/robots.txt', (req, res) => { - let filePath = path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'robots.txt'); + let filePath = path.join( + settings.root, + 'src', + 'static', + 'skins', + settings.skinName, + 'robots.txt' + ); res.sendFile(filePath, (err) => { // there is no custom robots.txt, send the default robots.txt which dissallows all if (err) { @@ -66,8 +75,15 @@ exports.expressCreateServer = function (hook_name, args, cb) { // serve favicon.ico from all path levels except as a pad name args.app.get(/\/favicon.ico$/, (req, res) => { - let filePath = path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'); - + let filePath = path.join( + settings.root, + 'src', + 'static', + 'skins', + settings.skinName, + 'favicon.ico' + ); + res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); res.sendFile(filePath, (err) => { // there is no custom favicon, send the default favicon if (err) { diff --git a/src/node/hooks/express/static.js b/src/node/hooks/express/static.js index 2df757e64..d1dec8714 100644 --- a/src/node/hooks/express/static.js +++ b/src/node/hooks/express/static.js @@ -1,11 +1,34 @@ -const minify = require('../../utils/Minify'); -const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs'); -const CachingMiddleware = require('../../utils/caching_middleware'); -const settings = require('../../utils/Settings'); -const Yajsml = require('etherpad-yajsml'); -const _ = require('underscore'); +'use strict'; -exports.expressCreateServer = function (hook_name, args, cb) { +const fs = require('fs').promises; +const minify = require('../../utils/Minify'); +const path = require('path'); +const plugins = require('../../../static/js/pluginfw/plugin_defs'); +const settings = require('../../utils/Settings'); +const CachingMiddleware = require('../../utils/caching_middleware'); +const Yajsml = require('etherpad-yajsml'); + +// Rewrite tar to include modules with no extensions and proper rooted paths. +const getTar = async () => { + const prefixLocalLibraryPath = (path) => { + if (path.charAt(0) === '$') { + return path.slice(1); + } else { + return `ep_etherpad-lite/static/js/${path}`; + } + }; + const tarJson = await fs.readFile(path.join(settings.root, 'src/node/utils/tar.json'), 'utf8'); + const tar = {}; + for (const [key, relativeFiles] of Object.entries(JSON.parse(tarJson))) { + const files = relativeFiles.map(prefixLocalLibraryPath); + tar[prefixLocalLibraryPath(key)] = files + .concat(files.map((p) => p.replace(/\.js$/, ''))) + .concat(files.map((p) => `${p.replace(/\.js$/, '')}/index.js`)); + } + return tar; +}; + +exports.expressCreateServer = async (hookName, args) => { // Cache both minified and static. const assetCache = new CachingMiddleware(); args.app.all(/\/javascripts\/(.*)/, assetCache.handle); @@ -26,33 +49,25 @@ exports.expressCreateServer = function (hook_name, args, cb) { }); const StaticAssociator = Yajsml.associators.StaticAssociator; - const associations = - Yajsml.associators.associationsForSimpleMapping(minify.tar); + const associations = Yajsml.associators.associationsForSimpleMapping(await getTar()); const associator = new StaticAssociator(associations); jsServer.setAssociator(associator); args.app.use(jsServer.handle.bind(jsServer)); // serve plugin definitions - // not very static, but served here so that client can do require("pluginfw/static/js/plugin-definitions.js"); + // not very static, but served here so that client can do + // require("pluginfw/static/js/plugin-definitions.js"); args.app.get('/pluginfw/plugin-definitions.json', (req, res, next) => { - const clientParts = _(plugins.parts) - .filter((part) => _(part).has('client_hooks')); - + const clientParts = plugins.parts.filter((part) => part.client_hooks != null); const clientPlugins = {}; - - _(clientParts).chain() - .map((part) => part.plugin) - .uniq() - .each((name) => { - clientPlugins[name] = _(plugins.plugins[name]).clone(); - delete clientPlugins[name].package; - }); - - res.header('Content-Type', 'application/json; charset=utf-8'); + for (const name of new Set(clientParts.map((part) => part.plugin))) { + clientPlugins[name] = {...plugins.plugins[name]}; + delete clientPlugins[name].package; + } + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); res.write(JSON.stringify({plugins: clientPlugins, parts: clientParts})); res.end(); }); - - return cb(); }; diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js index 7b32a322d..825c495ca 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.js @@ -1,9 +1,12 @@ +'use strict'; + const path = require('path'); const npm = require('npm'); const fs = require('fs'); const util = require('util'); +const settings = require('../../utils/Settings'); -exports.expressCreateServer = function (hook_name, args, cb) { +exports.expressCreateServer = (hookName, args, cb) => { args.app.get('/tests/frontend/specs_list.js', async (req, res) => { const [coreTests, pluginTests] = await Promise.all([ exports.getCoreTests(), @@ -16,17 +19,22 @@ exports.expressCreateServer = function (hook_name, args, cb) { // Keep only *.js files files = files.filter((f) => f.endsWith('.js')); + // remove admin tests if the setting to enable them isn't in settings.json + if (!settings.enableAdminUITests) { + files = files.filter((file) => file.indexOf('admin') !== 0); + } + console.debug('Sent browser the following test specs:', files); - res.setHeader('content-type', 'text/javascript'); + res.setHeader('content-type', 'application/javascript'); res.end(`var specs_list = ${JSON.stringify(files)};\n`); }); // path.join seems to normalize by default, but we'll just be explicit const rootTestFolder = path.normalize(path.join(npm.root, '../tests/frontend/')); - const url2FilePath = function (url) { + const url2FilePath = (url) => { let subPath = url.substr('/tests/frontend'.length); - if (subPath == '') { + if (subPath === '') { subPath = 'index.html'; } subPath = subPath.split('?')[0]; @@ -47,8 +55,11 @@ exports.expressCreateServer = function (hook_name, args, cb) { fs.readFile(specFilePath, (err, content) => { if (err) { return res.send(500); } - content = `describe(${JSON.stringify(specFileName)}, function(){ ${content} });`; + content = `describe(${JSON.stringify(specFileName)}, function(){${content}});`; + if (!specFilePath.endsWith('index.html')) { + res.setHeader('content-type', 'application/javascript'); + } res.send(content); }); }); @@ -67,7 +78,7 @@ exports.expressCreateServer = function (hook_name, args, cb) { const readdir = util.promisify(fs.readdir); -exports.getPluginTests = async function (callback) { +exports.getPluginTests = async (callback) => { const moduleDir = 'node_modules/'; const specPath = '/static/tests/frontend/specs/'; const staticDir = '/static/plugins/'; @@ -86,7 +97,4 @@ exports.getPluginTests = async function (callback) { return Promise.all(promises).then(() => pluginSpecs); }; -exports.getCoreTests = function () { - // get the core test specs - return readdir('tests/frontend/specs'); -}; +exports.getCoreTests = () => readdir('src/tests/frontend/specs'); diff --git a/src/node/hooks/i18n.js b/src/node/hooks/i18n.js index 610c3f68f..31848b484 100644 --- a/src/node/hooks/i18n.js +++ b/src/node/hooks/i18n.js @@ -1,24 +1,23 @@ +'use strict'; + const languages = require('languages4translatewiki'); const fs = require('fs'); const path = require('path'); const _ = require('underscore'); const npm = require('npm'); -const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs.js').plugins; -const semver = require('semver'); +const plugins = require('../../static/js/pluginfw/plugin_defs.js').plugins; const existsSync = require('../utils/path_exists'); -const settings = require('../utils/Settings') -; - +const settings = require('../utils/Settings'); // returns all existing messages merged together and grouped by langcode // {es: {"foo": "string"}, en:...} -function getAllLocales() { +const getAllLocales = () => { const locales2paths = {}; // Puts the paths of all locale files contained in a given directory // into `locales2paths` (files from various dirs are grouped by lang code) // (only json files with valid language code as name) - function extractLangs(dir) { + const extractLangs = (dir) => { if (!existsSync(dir)) return; let stat = fs.lstatSync(dir); if (!stat.isDirectory() || stat.isSymbolicLink()) return; @@ -31,12 +30,12 @@ function getAllLocales() { const ext = path.extname(file); const locale = path.basename(file, ext).toLowerCase(); - if ((ext == '.json') && languages.isValid(locale)) { + if ((ext === '.json') && languages.isValid(locale)) { if (!locales2paths[locale]) locales2paths[locale] = []; locales2paths[locale].push(file); } }); - } + }; // add core supported languages first extractLangs(`${npm.root}/ep_etherpad-lite/locales`); @@ -78,29 +77,29 @@ function getAllLocales() { } return locales; -} +}; // returns a hash of all available languages availables with nativeName and direction // e.g. { es: {nativeName: "español", direction: "ltr"}, ... } -function getAvailableLangs(locales) { +const getAvailableLangs = (locales) => { const result = {}; _.each(_.keys(locales), (langcode) => { result[langcode] = languages.getLanguageInfo(langcode); }); return result; -} +}; // returns locale index that will be served in /locales.json -const generateLocaleIndex = function (locales) { +const generateLocaleIndex = (locales) => { const result = _.clone(locales); // keep English strings _.each(_.keys(locales), (langcode) => { - if (langcode != 'en') result[langcode] = `locales/${langcode}.json`; + if (langcode !== 'en') result[langcode] = `locales/${langcode}.json`; }); return JSON.stringify(result); }; -exports.expressCreateServer = function (n, args, cb) { +exports.expressCreateServer = (n, args, cb) => { // regenerate locales on server restart const locales = getAllLocales(); const localeIndex = generateLocaleIndex(locales); @@ -110,6 +109,7 @@ exports.expressCreateServer = function (n, args, cb) { // works with /locale/en and /locale/en.json requests const locale = req.params.locale.split('.')[0]; if (exports.availableLangs.hasOwnProperty(locale)) { + res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.send(`{"${locale}":${JSON.stringify(locales[locale])}}`); } else { @@ -118,6 +118,7 @@ exports.expressCreateServer = function (n, args, cb) { }); args.app.get('/locales.json', (req, res) => { + res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.send(localeIndex); }); diff --git a/src/node/padaccess.js b/src/node/padaccess.js index 617056a97..241302852 100644 --- a/src/node/padaccess.js +++ b/src/node/padaccess.js @@ -1,3 +1,4 @@ +'use strict'; const securityManager = require('./db/SecurityManager'); // checks for padAccess diff --git a/src/node/server.js b/src/node/server.js index 3219f5185..0f0b0147f 100755 --- a/src/node/server.js +++ b/src/node/server.js @@ -3,7 +3,7 @@ 'use strict'; /** - * This module is started with bin/run.sh. It sets up a Express HTTP and a Socket.IO Server. + * This module is started with src/bin/run.sh. It sets up a Express HTTP and a Socket.IO Server. * Static file Requests are answered directly from this module, Socket.IO messages are passed * to MessageHandler and minfied requests are passed to minified. */ @@ -27,109 +27,237 @@ const log4js = require('log4js'); log4js.replaceConsole(); +// wtfnode should be loaded after log4js.replaceConsole() so that it uses log4js for logging, and it +// should be above everything else so that it can hook in before resources are used. +const wtfnode = require('wtfnode'); + /* * early check for version compatibility before calling * any modules that require newer versions of NodeJS */ const NodeVersion = require('./utils/NodeVersion'); -NodeVersion.enforceMinNodeVersion('10.13.0'); -NodeVersion.checkDeprecationStatus('10.13.0', '1.8.3'); +NodeVersion.enforceMinNodeVersion('10.17.0'); +NodeVersion.checkDeprecationStatus('10.17.0', '1.8.8'); const UpdateCheck = require('./utils/UpdateCheck'); const db = require('./db/DB'); const express = require('./hooks/express'); const hooks = require('../static/js/pluginfw/hooks'); const npm = require('npm/lib/npm.js'); +const pluginDefs = require('../static/js/pluginfw/plugin_defs'); const plugins = require('../static/js/pluginfw/plugins'); const settings = require('./utils/Settings'); const util = require('util'); -let started = false; -let stopped = false; +const logger = log4js.getLogger('server'); +const State = { + INITIAL: 1, + STARTING: 2, + RUNNING: 3, + STOPPING: 4, + STOPPED: 5, + EXITING: 6, + WAITING_FOR_EXIT: 7, + STATE_TRANSITION_FAILED: 8, +}; + +let state = State.INITIAL; + +class Gate extends Promise { + constructor() { + let res; + super((resolve) => { res = resolve; }); + this.resolve = res; + } +} + +const removeSignalListener = (signal, listener) => { + logger.debug(`Removing ${signal} listener because it might interfere with shutdown tasks. ` + + `Function code:\n${listener.toString()}\n` + + `Current stack:\n${(new Error()).stack.split('\n').slice(1).join('\n')}`); + process.off(signal, listener); +}; + +let startDoneGate; exports.start = async () => { - if (started) return express.server; - started = true; - if (stopped) throw new Error('restart not supported'); - - // Check if Etherpad version is up-to-date - UpdateCheck.check(); - - // start up stats counting system - const stats = require('./stats'); - stats.gauge('memoryUsage', () => process.memoryUsage().rss); - - await util.promisify(npm.load)(); - + switch (state) { + case State.INITIAL: + break; + case State.STARTING: + await startDoneGate; + // Retry. Don't fall through because it might have transitioned to STATE_TRANSITION_FAILED. + return await exports.start(); + case State.RUNNING: + return express.server; + case State.STOPPING: + case State.STOPPED: + case State.EXITING: + case State.WAITING_FOR_EXIT: + case State.STATE_TRANSITION_FAILED: + throw new Error('restart not supported'); + default: + throw new Error(`unknown State: ${state.toString()}`); + } + logger.info('Starting Etherpad...'); + startDoneGate = new Gate(); + state = State.STARTING; try { + // Check if Etherpad version is up-to-date + UpdateCheck.check(); + + // start up stats counting system + const stats = require('./stats'); + stats.gauge('memoryUsage', () => process.memoryUsage().rss); + stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed); + + process.on('uncaughtException', (err) => exports.exit(err)); + // As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an + // unhandled rejection into an uncaught exception, which does cause Node.js to exit. + process.on('unhandledRejection', (err) => { throw err; }); + + for (const signal of ['SIGINT', 'SIGTERM']) { + // Forcibly remove other signal listeners to prevent them from terminating node before we are + // done cleaning up. See https://github.com/andywer/threads.js/pull/329 for an example of a + // problematic listener. This means that exports.exit is solely responsible for performing all + // necessary cleanup tasks. + for (const listener of process.listeners(signal)) { + removeSignalListener(signal, listener); + } + process.on(signal, exports.exit); + // Prevent signal listeners from being added in the future. + process.on('newListener', (event, listener) => { + if (event !== signal) return; + removeSignalListener(signal, listener); + }); + } + + await util.promisify(npm.load)(); await db.init(); await plugins.update(); - console.info(`Installed plugins: ${plugins.formatPluginsWithVersion()}`); - console.debug(`Installed parts:\n${plugins.formatParts()}`); - console.debug(`Installed hooks:\n${plugins.formatHooks()}`); + const installedPlugins = Object.values(pluginDefs.plugins) + .filter((plugin) => plugin.package.name !== 'ep_etherpad-lite') + .map((plugin) => `${plugin.package.name}@${plugin.package.version}`) + .join(', '); + logger.info(`Installed plugins: ${installedPlugins}`); + logger.debug(`Installed parts:\n${plugins.formatParts()}`); + logger.debug(`Installed hooks:\n${plugins.formatHooks()}`); await hooks.aCallAll('loadSettings', {settings}); await hooks.aCallAll('createServer'); - } catch (e) { - console.error(`exception thrown: ${e.message}`); - if (e.stack) console.log(e.stack); - process.exit(1); + } catch (err) { + logger.error('Error occurred while starting Etherpad'); + state = State.STATE_TRANSITION_FAILED; + startDoneGate.resolve(); + return await exports.exit(err); } - process.on('uncaughtException', exports.exit); - - /* - * Connect graceful shutdown with sigint and uncaught exception - * - * Until Etherpad 1.7.5, process.on('SIGTERM') and process.on('SIGINT') were - * not hooked up under Windows, because old nodejs versions did not support - * them. - * - * According to nodejs 6.x documentation, it is now safe to do so. This - * allows to gracefully close the DB connection when hitting CTRL+C under - * Windows, for example. - * - * Source: https://nodejs.org/docs/latest-v6.x/api/process.html#process_signal_events - * - * - SIGTERM is not supported on Windows, it can be listened on. - * - SIGINT from the terminal is supported on all platforms, and can usually - * be generated with +C (though this may be configurable). It is not - * generated when terminal raw mode is enabled. - */ - process.on('SIGINT', exports.exit); - - // When running as PID1 (e.g. in docker container) allow graceful shutdown on SIGTERM c.f. #3265. - // Pass undefined to exports.exit because this is not an abnormal termination. - process.on('SIGTERM', () => exports.exit()); + logger.info('Etherpad is running'); + state = State.RUNNING; + startDoneGate.resolve(); // Return the HTTP server to make it easier to write tests. return express.server; }; +let stopDoneGate; exports.stop = async () => { - if (stopped) return; - stopped = true; - console.log('Stopping Etherpad...'); - await new Promise(async (resolve, reject) => { - const id = setTimeout(() => reject(new Error('Timed out waiting for shutdown tasks')), 3000); - await hooks.aCallAll('shutdown'); - clearTimeout(id); - resolve(); - }); + switch (state) { + case State.STARTING: + await exports.start(); + // Don't fall through to State.RUNNING in case another caller is also waiting for startup. + return await exports.stop(); + case State.RUNNING: + break; + case State.STOPPING: + await stopDoneGate; + // fall through + case State.INITIAL: + case State.STOPPED: + case State.EXITING: + case State.WAITING_FOR_EXIT: + case State.STATE_TRANSITION_FAILED: + return; + default: + throw new Error(`unknown State: ${state.toString()}`); + } + logger.info('Stopping Etherpad...'); + const stopDoneGate = new Gate(); + state = State.STOPPING; + try { + let timeout = null; + await Promise.race([ + hooks.aCallAll('shutdown'), + new Promise((resolve, reject) => { + timeout = setTimeout(() => reject(new Error('Timed out waiting for shutdown tasks')), 3000); + }), + ]); + clearTimeout(timeout); + } catch (err) { + logger.error('Error occurred while stopping Etherpad'); + state = State.STATE_TRANSITION_FAILED; + stopDoneGate.resolve(); + return await exports.exit(err); + } + logger.info('Etherpad stopped'); + state = State.STOPPED; + stopDoneGate.resolve(); }; -exports.exit = async (err) => { - let exitCode = 0; - if (err) { - exitCode = 1; - console.error(err.stack ? err.stack : err); +let exitGate; +let exitCalled = false; +exports.exit = async (err = null) => { + /* eslint-disable no-process-exit */ + if (err === 'SIGTERM') { + // Termination from SIGTERM is not treated as an abnormal termination. + logger.info('Received SIGTERM signal'); + err = null; + } else if (err != null) { + logger.error(err.stack || err.toString()); + process.exitCode = 1; + if (exitCalled) { + logger.error('Error occurred while waiting to exit. Forcing an immediate unclean exit...'); + process.exit(1); + } } - try { - await exports.stop(); - } catch (err) { - exitCode = 1; - console.error(err.stack ? err.stack : err); + exitCalled = true; + switch (state) { + case State.STARTING: + case State.RUNNING: + case State.STOPPING: + await exports.stop(); + // Don't fall through to State.STOPPED in case another caller is also waiting for stop(). + // Don't pass err to exports.exit() because this err has already been processed. (If err is + // passed again to exit() then exit() will think that a second error occurred while exiting.) + return await exports.exit(); + case State.INITIAL: + case State.STOPPED: + case State.STATE_TRANSITION_FAILED: + break; + case State.EXITING: + await exitGate; + // fall through + case State.WAITING_FOR_EXIT: + return; + default: + throw new Error(`unknown State: ${state.toString()}`); } - process.exit(exitCode); + logger.info('Exiting...'); + exitGate = new Gate(); + state = State.EXITING; + exitGate.resolve(); + // Node.js should exit on its own without further action. Add a timeout to force Node.js to exit + // just in case something failed to get cleaned up during the shutdown hook. unref() is called on + // the timeout so that the timeout itself does not prevent Node.js from exiting. + setTimeout(() => { + logger.error('Something that should have been cleaned up during the shutdown hook (such as ' + + 'a timer, worker thread, or open connection) is preventing Node.js from exiting'); + wtfnode.dump(); + logger.error('Forcing an unclean exit...'); + process.exit(1); + }, 5000).unref(); + logger.info('Waiting for Node.js to exit...'); + state = State.WAITING_FOR_EXIT; + /* eslint-enable no-process-exit */ }; if (require.main === module) exports.start(); diff --git a/src/node/utils/Abiword.js b/src/node/utils/Abiword.js index b75487d75..b93646cd5 100644 --- a/src/node/utils/Abiword.js +++ b/src/node/utils/Abiword.js @@ -1,3 +1,4 @@ +'use strict'; /** * Controls the communication with the Abiword application */ @@ -25,11 +26,12 @@ const os = require('os'); let doConvertTask; -// on windows we have to spawn a process for each convertion, cause the plugin abicommand doesn't exist on this platform +// on windows we have to spawn a process for each convertion, +// cause the plugin abicommand doesn't exist on this platform if (os.type().indexOf('Windows') > -1) { let stdoutBuffer = ''; - doConvertTask = function (task, callback) { + doConvertTask = (task, callback) => { // span an abiword process to perform the conversion const abiword = spawn(settings.abiword, [`--to=${task.destFile}`, task.srcFile]); @@ -46,11 +48,11 @@ if (os.type().indexOf('Windows') > -1) { // throw exceptions if abiword is dieing abiword.on('exit', (code) => { - if (code != 0) { + if (code !== 0) { return callback(`Abiword died with exit code ${code}`); } - if (stdoutBuffer != '') { + if (stdoutBuffer !== '') { console.log(stdoutBuffer); } @@ -58,17 +60,17 @@ if (os.type().indexOf('Windows') > -1) { }); }; - exports.convertFile = function (srcFile, destFile, type, callback) { + exports.convertFile = (srcFile, destFile, type, callback) => { doConvertTask({srcFile, destFile, type}, callback); }; -} -// on unix operating systems, we can start abiword with abicommand and communicate with it via stdin/stdout -// thats much faster, about factor 10 -else { + // on unix operating systems, we can start abiword with abicommand and + // communicate with it via stdin/stdout + // thats much faster, about factor 10 +} else { // spawn the abiword process let abiword; let stdoutCallback = null; - var spawnAbiword = function () { + const spawnAbiword = () => { abiword = spawn(settings.abiword, ['--plugin', 'AbiCommand']); let stdoutBuffer = ''; let firstPrompt = true; @@ -90,9 +92,9 @@ else { stdoutBuffer += data.toString(); // we're searching for the prompt, cause this means everything we need is in the buffer - if (stdoutBuffer.search('AbiWord:>') != -1) { + if (stdoutBuffer.search('AbiWord:>') !== -1) { // filter the feedback message - const err = stdoutBuffer.search('OK') != -1 ? null : stdoutBuffer; + const err = stdoutBuffer.search('OK') !== -1 ? null : stdoutBuffer; // reset the buffer stdoutBuffer = ''; @@ -110,10 +112,10 @@ else { }; spawnAbiword(); - doConvertTask = function (task, callback) { + doConvertTask = (task, callback) => { abiword.stdin.write(`convert ${task.srcFile} ${task.destFile} ${task.type}\n`); // create a callback that calls the task callback and the caller callback - stdoutCallback = function (err) { + stdoutCallback = (err) => { callback(); console.log('queue continue'); try { @@ -126,7 +128,7 @@ else { // Queue with the converts we have to do const queue = async.queue(doConvertTask, 1); - exports.convertFile = function (srcFile, destFile, type, callback) { + exports.convertFile = (srcFile, destFile, type, callback) => { queue.push({srcFile, destFile, type, callback}); }; } diff --git a/src/node/utils/AbsolutePaths.js b/src/node/utils/AbsolutePaths.js index 22294cfe2..5b364ed80 100644 --- a/src/node/utils/AbsolutePaths.js +++ b/src/node/utils/AbsolutePaths.js @@ -1,3 +1,4 @@ +'use strict'; /** * Library for deterministic relative filename expansion for Etherpad. */ @@ -40,7 +41,7 @@ let etherpadRoot = null; * @return {string[]|boolean} The shortened array, or false if there was no * overlap. */ -const popIfEndsWith = function (stringArray, lastDesiredElements) { +const popIfEndsWith = (stringArray, lastDesiredElements) => { if (stringArray.length <= lastDesiredElements.length) { absPathLogger.debug(`In order to pop "${lastDesiredElements.join(path.sep)}" from "${stringArray.join(path.sep)}", it should contain at least ${lastDesiredElements.length + 1} elements`); @@ -72,8 +73,8 @@ const popIfEndsWith = function (stringArray, lastDesiredElements) { * @return {string} The identified absolute base path. If such path cannot be * identified, prints a log and exits the application. */ -exports.findEtherpadRoot = function () { - if (etherpadRoot !== null) { +exports.findEtherpadRoot = () => { + if (etherpadRoot != null) { return etherpadRoot; } @@ -126,7 +127,7 @@ exports.findEtherpadRoot = function () { * it is returned unchanged. Otherwise it is interpreted * relative to exports.root. */ -exports.makeAbsolute = function (somePath) { +exports.makeAbsolute = (somePath) => { if (path.isAbsolute(somePath)) { return somePath; } @@ -145,7 +146,7 @@ exports.makeAbsolute = function (somePath) { * a subdirectory of the base one * @return {boolean} */ -exports.isSubdir = function (parent, arbitraryDir) { +exports.isSubdir = (parent, arbitraryDir) => { // modified from: https://stackoverflow.com/questions/37521893/determine-if-a-path-is-subdirectory-of-another-in-node-js#45242825 const relative = path.relative(parent, arbitraryDir); const isSubdir = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative); diff --git a/src/node/utils/Cli.js b/src/node/utils/Cli.js index 6297a4f8c..a5cdee83a 100644 --- a/src/node/utils/Cli.js +++ b/src/node/utils/Cli.js @@ -1,3 +1,4 @@ +'use strict'; /** * The CLI module handles command line parameters */ @@ -30,22 +31,22 @@ for (let i = 0; i < argv.length; i++) { arg = argv[i]; // Override location of settings.json file - if (prevArg == '--settings' || prevArg == '-s') { + if (prevArg === '--settings' || prevArg === '-s') { exports.argv.settings = arg; } // Override location of credentials.json file - if (prevArg == '--credentials') { + if (prevArg === '--credentials') { exports.argv.credentials = arg; } // Override location of settings.json file - if (prevArg == '--sessionkey') { + if (prevArg === '--sessionkey') { exports.argv.sessionkey = arg; } // Override location of settings.json file - if (prevArg == '--apikey') { + if (prevArg === '--apikey') { exports.argv.apikey = arg; } diff --git a/src/node/utils/ExportEtherpad.js b/src/node/utils/ExportEtherpad.js index ace298ab7..48c850af9 100644 --- a/src/node/utils/ExportEtherpad.js +++ b/src/node/utils/ExportEtherpad.js @@ -1,3 +1,4 @@ +'use strict'; /** * 2014 John McLear (Etherpad Foundation / McLear Ltd) * @@ -16,9 +17,9 @@ const db = require('../db/DB'); -const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); +const hooks = require('../../static/js/pluginfw/hooks'); -exports.getPadRaw = async function (padId) { +exports.getPadRaw = async (padId) => { const padKey = `pad:${padId}`; const padcontent = await db.get(padKey); diff --git a/src/node/utils/ExportHelper.js b/src/node/utils/ExportHelper.js index e498d4c42..0c593eca1 100644 --- a/src/node/utils/ExportHelper.js +++ b/src/node/utils/ExportHelper.js @@ -1,3 +1,4 @@ +'use strict'; /** * Helpers for export requests */ @@ -18,9 +19,9 @@ * limitations under the License. */ -const Changeset = require('ep_etherpad-lite/static/js/Changeset'); +const Changeset = require('../../static/js/Changeset'); -exports.getPadPlainText = function (pad, revNum) { +exports.getPadPlainText = (pad, revNum) => { const _analyzeLine = exports._analyzeLine; const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) : pad.atext); const textLines = atext.text.slice(0, -1).split('\n'); @@ -43,7 +44,7 @@ exports.getPadPlainText = function (pad, revNum) { }; -exports._analyzeLine = function (text, aline, apool) { +exports._analyzeLine = (text, aline, apool) => { const line = {}; // identify list @@ -81,6 +82,5 @@ exports._analyzeLine = function (text, aline, apool) { }; -exports._encodeWhitespace = function (s) { - return s.replace(/[^\x21-\x7E\s\t\n\r]/gu, (c) => `&#${c.codePointAt(0)};`); -}; +exports._encodeWhitespace = + (s) => s.replace(/[^\x21-\x7E\s\t\n\r]/gu, (c) => `&#${c.codePointAt(0)};`); diff --git a/src/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.js index 2f5a77c9a..999b22639 100644 --- a/src/node/utils/ExportHtml.js +++ b/src/node/utils/ExportHtml.js @@ -1,3 +1,4 @@ +'use strict'; /** * Copyright 2009 Google Inc. * @@ -14,32 +15,29 @@ * limitations under the License. */ -const Changeset = require('ep_etherpad-lite/static/js/Changeset'); +const Changeset = require('../../static/js/Changeset'); const padManager = require('../db/PadManager'); const _ = require('underscore'); -const Security = require('ep_etherpad-lite/static/js/security'); -const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); -const eejs = require('ep_etherpad-lite/node/eejs'); +const Security = require('../../static/js/security'); +const hooks = require('../../static/js/pluginfw/hooks'); +const eejs = require('../eejs'); const _analyzeLine = require('./ExportHelper')._analyzeLine; const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace; const padutils = require('../../static/js/pad_utils').padutils; -async function getPadHTML(pad, revNum) { +const getPadHTML = async (pad, revNum) => { let atext = pad.atext; // fetch revision atext - if (revNum != undefined) { + if (revNum !== undefined) { atext = await pad.getInternalRevisionAText(revNum); } // convert atext to html return await getHTMLFromAtext(pad, atext); -} +}; -exports.getPadHTML = getPadHTML; -exports.getHTMLFromAtext = getHTMLFromAtext; - -async function getHTMLFromAtext(pad, atext, authorColors) { +const getHTMLFromAtext = async (pad, atext, authorColors) => { const apool = pad.apool(); const textLines = atext.text.slice(0, -1).split('\n'); const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); @@ -72,9 +70,7 @@ async function getHTMLFromAtext(pad, atext, authorColors) { const anumMap = {}; let css = ''; - const stripDotFromAuthorID = function (id) { - return id.replace(/\./g, '_'); - }; + const stripDotFromAuthorID = (id) => id.replace(/\./g, '_'); if (authorColors) { css += ''); } - for (var i = 0, ii = remoteFiles.length; i < ii; i++) { - var file = remoteFiles[i]; - buffer.push(``); + for (const file of remoteFiles) { + buffer.push(``); } - } + }; - editor.destroy = pendingInit(() => { + this.destroy = pendingInit(() => { info.ace_dispose(); info.frame.parentNode.removeChild(info.frame); delete ace2.registry[info.id]; info = null; // prevent IE 6 closure memory leaks }); - editor.init = function (containerId, initialCode, doneFunc) { - editor.importText(initialCode); + this.init = function (containerId, initialCode, doneFunc) { + this.importText(initialCode); - info.onEditorReady = function () { + info.onEditorReady = () => { loaded = true; doActionsPendingInit(); doneFunc(); }; - (function () { + (() => { const doctype = ''; const iframeHTML = []; @@ -223,8 +176,8 @@ function Ace2Editor() { // and compressed, putting the compressed code from the named file directly into the // source here. // these lines must conform to a specific format because they are passed by the build script: - var includedCSS = []; - var $$INCLUDE_CSS = function (filename) { includedCSS.push(filename); }; + let includedCSS = []; + let $$INCLUDE_CSS = (filename) => { includedCSS.push(filename); }; $$INCLUDE_CSS('../static/css/iframe_editor.css'); // disableCustomScriptsAndStyles can be used to disable loading of custom scripts @@ -232,18 +185,19 @@ function Ace2Editor() { $$INCLUDE_CSS(`../static/css/pad.css?v=${clientVars.randomVersionString}`); } - var additionalCSS = _(hooks.callAll('aceEditorCSS')).map((path) => { + let additionalCSS = hooks.callAll('aceEditorCSS').map((path) => { if (path.match(/\/\//)) { // Allow urls to external CSS - http(s):// and //some/path.css return path; } return `../static/plugins/${path}`; }); includedCSS = includedCSS.concat(additionalCSS); - $$INCLUDE_CSS(`../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`); + $$INCLUDE_CSS( + `../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`); pushStyleTagsFor(iframeHTML, includedCSS); - if (!Ace2Editor.EMBEDED && Ace2Editor.EMBEDED[KERNEL_SOURCE]) { + if (!Ace2Editor.EMBEDED || !Ace2Editor.EMBEDED[KERNEL_SOURCE]) { // Remotely src'd script tag will not work in IE; it must be embedded, so // throw an error if it is not. throw new Error('Require kernel could not be found.'); @@ -272,15 +226,16 @@ plugins.ensure(function () {\n\ iframeHTML, }); - iframeHTML.push(' '); + iframeHTML.push(' '); - // Expose myself to global for my child frame. - const thisFunctionsName = 'ChildAccessibleAce2Editor'; - (function () { return this; }())[thisFunctionsName] = Ace2Editor; + // eslint-disable-next-line node/no-unsupported-features/es-builtins + const gt = typeof globalThis === 'object' ? globalThis : window; + gt.ChildAccessibleAce2Editor = Ace2Editor; const outerScript = `\ editorId = ${JSON.stringify(info.id)};\n\ -editorInfo = parent[${JSON.stringify(thisFunctionsName)}].registry[editorId];\n\ +editorInfo = parent.ChildAccessibleAce2Editor.registry[editorId];\n\ window.onload = function () {\n\ window.onload = null;\n\ setTimeout(function () {\n\ @@ -306,23 +261,24 @@ window.onload = function () {\n\ }, 0);\n\ }`; - const outerHTML = [doctype, ``]; + const outerHTML = + [doctype, ``]; - var includedCSS = []; - var $$INCLUDE_CSS = function (filename) { includedCSS.push(filename); }; + includedCSS = []; + $$INCLUDE_CSS = (filename) => { includedCSS.push(filename); }; $$INCLUDE_CSS('../static/css/iframe_editor.css'); $$INCLUDE_CSS(`../static/css/pad.css?v=${clientVars.randomVersionString}`); - var additionalCSS = _(hooks.callAll('aceEditorCSS')).map((path) => { + additionalCSS = hooks.callAll('aceEditorCSS').map((path) => { if (path.match(/\/\//)) { // Allow urls to external CSS - http(s):// and //some/path.css return path; } return `../static/plugins/${path}`; - } - ); + }); includedCSS = includedCSS.concat(additionalCSS); - $$INCLUDE_CSS(`../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`); + $$INCLUDE_CSS( + `../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`); pushStyleTagsFor(outerHTML, includedCSS); @@ -353,8 +309,10 @@ window.onload = function () {\n\ editorDocument.close(); })(); }; +}; - return editor; -} +Ace2Editor.registry = { + nextId: 1, +}; exports.Ace2Editor = Ace2Editor; diff --git a/src/static/js/ace2_common.js b/src/static/js/ace2_common.js index 9055b34e3..d3f86f699 100644 --- a/src/static/js/ace2_common.js +++ b/src/static/js/ace2_common.js @@ -1,3 +1,5 @@ +'use strict'; + /** * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. @@ -22,33 +24,23 @@ const Security = require('./security'); -function isNodeText(node) { - return (node.nodeType == 3); -} +const isNodeText = (node) => (node.nodeType === 3); -function object(o) { - const f = function () {}; - f.prototype = o; - return new f(); -} +const getAssoc = (obj, name) => obj[`_magicdom_${name}`]; -function getAssoc(obj, name) { - return obj[`_magicdom_${name}`]; -} - -function setAssoc(obj, name, value) { +const setAssoc = (obj, name, value) => { // note that in IE designMode, properties of a node can get // copied to new nodes that are spawned during editing; also, // properties representable in HTML text can survive copy-and-paste obj[`_magicdom_${name}`] = value; -} +}; // "func" is a function over 0..(numItems-1) that is monotonically // "increasing" with index (false, then true). Finds the boundary // between false and true, a number between 0 and numItems inclusive. -function binarySearch(numItems, func) { +const binarySearch = (numItems, func) => { if (numItems < 1) return 0; if (func(0)) return 0; if (!func(numItems - 1)) return numItems; @@ -60,22 +52,19 @@ function binarySearch(numItems, func) { else low = x; } return high; -} +}; -function binarySearchInfinite(expectedLength, func) { +const binarySearchInfinite = (expectedLength, func) => { let i = 0; while (!func(i)) i += expectedLength; return binarySearch(i, func); -} +}; -function htmlPrettyEscape(str) { - return Security.escapeHTML(str).replace(/\r?\n/g, '\\n'); -} +const htmlPrettyEscape = (str) => Security.escapeHTML(str).replace(/\r?\n/g, '\\n'); -const noop = function () {}; +const noop = () => {}; exports.isNodeText = isNodeText; -exports.object = object; exports.getAssoc = getAssoc; exports.setAssoc = setAssoc; exports.binarySearch = binarySearch; diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index fc339ab78..a11c489b5 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -1,11 +1,8 @@ -/** - * This code is mostly from the old Etherpad. Please help us to comment this code. - * This helps other people to understand this code better and helps them to improve it. - * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED - */ +'use strict'; /** * Copyright 2009 Google Inc. + * Copyright 2020 John McLear - The Etherpad Foundation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,23 +16,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +let documentAttributeManager; -const padutils = require('./pad_utils').padutils; - -let _, $, jQuery, plugins, Ace2Common; const browser = require('./browser'); - -Ace2Common = require('./ace2_common'); - -plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins'); -$ = jQuery = require('./rjquery').$; -_ = require('./underscore'); +const padutils = require('./pad_utils').padutils; +const Ace2Common = require('./ace2_common'); +const $ = require('./rjquery').$; const isNodeText = Ace2Common.isNodeText; const getAssoc = Ace2Common.getAssoc; const setAssoc = Ace2Common.setAssoc; -const isTextNode = Ace2Common.isTextNode; -const binarySearchInfinite = Ace2Common.binarySearchInfinite; const htmlPrettyEscape = Ace2Common.htmlPrettyEscape; const noop = Ace2Common.noop; const hooks = require('./pluginfw/hooks'); @@ -54,10 +44,7 @@ function Ace2Inner() { const undoModule = require('./undomodule').undoModule; const AttributeManager = require('./AttributeManager'); const Scroll = require('./scroll'); - - const DEBUG = false; // $$ build script replaces the string "var DEBUG=true;//$$" with "var DEBUG=false;" - // changed to false - let isSetUp = false; + const DEBUG = false; const THE_TAB = ' '; // 4 const MAX_LIST_LEVEL = 16; @@ -72,6 +59,9 @@ function Ace2Inner() { let disposed = false; const editorInfo = parent.editorInfo; + const focus = () => { + window.focus(); + }; const iframe = window.frameElement; const outerWin = iframe.ace_outerWin; @@ -80,14 +70,22 @@ function Ace2Inner() { const lineMetricsDiv = sideDiv.nextSibling; let lineNumbersShown; let sideDivInner; + + const initLineNumbers = () => { + const htmlOpen = '
1'; + const htmlClose = '
'; + lineNumbersShown = 1; + sideDiv.innerHTML = `${htmlOpen}${htmlClose}`; + sideDivInner = outerWin.document.getElementById('sidedivinner'); + $(sideDiv).addClass('sidediv'); + }; + initLineNumbers(); const scroll = Scroll.init(outerWin); let outsideKeyDown = noop; - - let outsideKeyPress = function (e) { return true; }; - + let outsideKeyPress = (e) => true; let outsideNotifyDirty = noop; // selFocusAtStart -- determines whether the selection extends "backwards", so that the focus @@ -116,25 +114,39 @@ function Ace2Inner() { let isStyled = true; let console = (DEBUG && window.console); - let documentAttributeManager; if (!window.console) { - const names = ['log', 'debug', 'info', 'warn', 'error', 'assert', 'dir', 'dirxml', 'group', 'groupEnd', 'time', 'timeEnd', 'count', 'trace', 'profile', 'profileEnd']; + const names = [ + 'log', + 'debug', + 'info', + 'warn', + 'error', + 'assert', + 'dir', + 'dirxml', + 'group', + 'groupEnd', + 'time', + 'timeEnd', + 'count', + 'trace', + 'profile', + 'profileEnd', + ]; console = {}; for (let i = 0; i < names.length; ++i) console[names[i]] = noop; } let PROFILER = window.PROFILER; if (!PROFILER) { - PROFILER = function () { - return { - start: noop, - mark: noop, - literal: noop, - end: noop, - cancel: noop, - }; - }; + PROFILER = () => ({ + start: noop, + mark: noop, + literal: noop, + end: noop, + cancel: noop, + }); } // "dmesg" is for displaying messages in the in-page output pane @@ -150,22 +162,42 @@ function Ace2Inner() { let outerDynamicCSS = null; let parentDynamicCSS = null; - function initDynamicCSS() { + const performDocumentReplaceRange = (start, end, newText) => { + if (start === undefined) start = rep.selStart; + if (end === undefined) end = rep.selEnd; + + // dmesg(String([start.toSource(),end.toSource(),newText.toSource()])); + // start[0]: <--- start[1] --->CCCCCCCCCCC\n + // CCCCCCCCCCCCCCCCCCCC\n + // CCCC\n + // end[0]: -------\n + const builder = Changeset.builder(rep.lines.totalWidth()); + ChangesetUtils.buildKeepToStartOfRange(rep, builder, start); + ChangesetUtils.buildRemoveRange(rep, builder, start, end); + builder.insert(newText, [ + ['author', thisAuthor], + ], rep.apool); + const cs = builder.toString(); + + performDocumentApplyChangeset(cs); + }; + + const initDynamicCSS = () => { dynamicCSS = makeCSSManager('dynamicsyntax'); outerDynamicCSS = makeCSSManager('dynamicsyntax', 'outer'); parentDynamicCSS = makeCSSManager('dynamicsyntax', 'parent'); - } + }; const changesetTracker = makeChangesetTracker(scheduler, rep.apool, { - withCallbacks(operationName, f) { + withCallbacks: (operationName, f) => { inCallStackIfNecessary(operationName, () => { fastIncorp(1); f( { - setDocumentAttributedText(atext) { + setDocumentAttributedText: (atext) => { setDocAText(atext); }, - applyChangesetToDocument(changeset, preferInsertionAfterCaret) { + applyChangesetToDocument: (changeset, preferInsertionAfterCaret) => { const oldEventType = currentCallStack.editEvent.eventType; currentCallStack.startNewEvent('nonundoable'); @@ -179,13 +211,10 @@ function Ace2Inner() { }); const authorInfos = {}; // presence of key determines if author is present in doc - - function getAuthorInfos() { - return authorInfos; - } + const getAuthorInfos = () => authorInfos; editorInfo.ace_getAuthorInfos = getAuthorInfos; - function setAuthorStyle(author, info) { + const setAuthorStyle = (author, info) => { if (!dynamicCSS) { return; } @@ -201,7 +230,7 @@ function Ace2Inner() { }); // Prevent default behaviour if any hook says so - if (_.any(authorStyleSet, (it) => it)) { + if (authorStyleSet.some((it) => it)) { return; } @@ -221,13 +250,15 @@ function Ace2Inner() { authorStyle.backgroundColor = bgcolor; parentAuthorStyle.backgroundColor = bgcolor; - const textColor = colorutils.textColorFromBackgroundColor(bgcolor, parent.parent.clientVars.skinName); + const textColor = + colorutils.textColorFromBackgroundColor(bgcolor, parent.parent.clientVars.skinName); authorStyle.color = textColor; parentAuthorStyle.color = textColor; } - } + }; - function setAuthorInfo(author, info) { + const setAuthorInfo = (author, info) => { + if (!author) return; // author ID not set for some reason if ((typeof author) !== 'string') { // Potentially caused by: https://github.com/ether/etherpad-lite/issues/2802"); throw new Error(`setAuthorInfo: author (${author}) is not a string`); @@ -238,19 +269,17 @@ function Ace2Inner() { authorInfos[author] = info; } setAuthorStyle(author, info); - } + }; - function getAuthorClassName(author) { - return `author-${author.replace(/[^a-y0-9]/g, (c) => { - if (c == '.') return '-'; - return `z${c.charCodeAt(0)}z`; - })}`; - } + const getAuthorClassName = (author) => `author-${author.replace(/[^a-y0-9]/g, (c) => { + if (c === '.') return '-'; + return `z${c.charCodeAt(0)}z`; + })}`; - function className2Author(className) { - if (className.substring(0, 7) == 'author-') { + const className2Author = (className) => { + if (className.substring(0, 7) === 'author-') { return className.substring(7).replace(/[a-y0-9]+|-|z.+?z/g, (cc) => { - if (cc == '-') { return '.'; } else if (cc.charAt(0) == 'z') { + if (cc === '-') { return '.'; } else if (cc.charAt(0) === 'z') { return String.fromCharCode(Number(cc.slice(1, -1))); } else { return cc; @@ -258,62 +287,47 @@ function Ace2Inner() { }); } return null; - } + }; - function getAuthorColorClassSelector(oneClassName) { - return `.authorColors .${oneClassName}`; - } + const getAuthorColorClassSelector = (oneClassName) => `.authorColors .${oneClassName}`; - function fadeColor(colorCSS, fadeFrac) { + const fadeColor = (colorCSS, fadeFrac) => { let color = colorutils.css2triple(colorCSS); color = colorutils.blend(color, [1, 1, 1], fadeFrac); return colorutils.triple2css(color); - } - - editorInfo.ace_getRep = function () { - return rep; }; - editorInfo.ace_getAuthor = function () { - return thisAuthor; - }; + editorInfo.ace_getRep = () => rep; + + editorInfo.ace_getAuthor = () => thisAuthor; const _nonScrollableEditEvents = { applyChangesToBase: 1, }; - _.each(hooks.callAll('aceRegisterNonScrollableEditEvents'), (eventType) => { + hooks.callAll('aceRegisterNonScrollableEditEvents').forEach((eventType) => { _nonScrollableEditEvents[eventType] = 1; }); - function isScrollableEditEvent(eventType) { - return !_nonScrollableEditEvents[eventType]; - } + const isScrollableEditEvent = (eventType) => !_nonScrollableEditEvents[eventType]; - var currentCallStack = null; + let currentCallStack = null; - function inCallStack(type, action) { + const inCallStack = (type, action) => { if (disposed) return; if (currentCallStack) { - // Do not uncomment this in production. It will break Etherpad being provided in iFrames. I'm leaving this in for testing usefulness. - // top.console.error("Can't enter callstack " + type + ", already in " + currentCallStack.type); + // Do not uncomment this in production. It will break Etherpad being provided in iFrames. + // I am leaving this in for testing usefulness. + // top.console.error(`Can't enter callstack ${type}, already in ${currentCallStack.type}`); } - let profiling = false; + const newEditEvent = (eventType) => ({ + eventType, + backset: null, + }); - function profileRest() { - profiling = true; - } - - function newEditEvent(eventType) { - return { - eventType, - backset: null, - }; - } - - function submitOldEvent(evt) { + const submitOldEvent = (evt) => { if (rep.selStart && rep.selEnd) { const selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1]; const selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1]; @@ -326,7 +340,7 @@ function Ace2Inner() { try { if (isPadLoading(evt.eventType)) { undoModule.clearHistory(); - } else if (evt.eventType == 'nonundoable') { + } else if (evt.eventType === 'nonundoable') { if (evt.changeset) { undoModule.reportExternalChange(evt.changeset); } @@ -340,16 +354,16 @@ function Ace2Inner() { } } } - } + }; - function startNewEvent(eventType, dontSubmitOld) { + const startNewEvent = (eventType, dontSubmitOld) => { const oldEvent = currentCallStack.editEvent; if (!dontSubmitOld) { submitOldEvent(oldEvent); } currentCallStack.editEvent = newEditEvent(eventType); return oldEvent; - } + }; currentCallStack = { type, @@ -357,7 +371,6 @@ function Ace2Inner() { selectionAffected: false, userChangedSelection: false, domClean: false, - profileRest, isUserChange: false, // is this a "user change" type of call-stack repChanged: false, @@ -389,7 +402,7 @@ function Ace2Inner() { const cs = currentCallStack; if (cleanExit) { submitOldEvent(cs.editEvent); - if (cs.domClean && cs.type != 'setup') { + if (cs.domClean && cs.type !== 'setup') { // if (cs.isUserChange) // { // if (cs.repChanged) parenModule.notifyChange(); @@ -405,36 +418,32 @@ function Ace2Inner() { outsideNotifyDirty(); } } - } else { - // non-clean exit - if (currentCallStack.type == 'idleWorkTimer') { - idleWorkTimer.atLeast(1000); - } + } else if (currentCallStack.type === 'idleWorkTimer') { + idleWorkTimer.atLeast(1000); } currentCallStack = null; } return result; - } + }; editorInfo.ace_inCallStack = inCallStack; - function inCallStackIfNecessary(type, action) { + const inCallStackIfNecessary = (type, action) => { if (!currentCallStack) { inCallStack(type, action); } else { action(); } - } + }; editorInfo.ace_inCallStackIfNecessary = inCallStackIfNecessary; - function dispose() { + const dispose = () => { disposed = true; if (idleWorkTimer) idleWorkTimer.never(); teardown(); - } + }; - function setWraps(newVal) { + const setWraps = (newVal) => { doesWrap = newVal; - const dwClass = 'doesWrap'; root.classList.toggle('doesWrap', doesWrap); scheduler.setTimeout(() => { inCallStackIfNecessary('setWraps', () => { @@ -443,51 +452,49 @@ function Ace2Inner() { fixView(); }); }, 0); - } + }; - function setStyled(newVal) { + const setStyled = (newVal) => { const oldVal = isStyled; isStyled = !!newVal; - if (newVal != oldVal) { + if (newVal !== oldVal) { if (!newVal) { // clear styles inCallStackIfNecessary('setStyled', () => { fastIncorp(12); const clearStyles = []; - for (const k in STYLE_ATTRIBS) { + for (const k of Object.keys(STYLE_ATTRIBS)) { clearStyles.push([k, '']); } performDocumentApplyAttributesToCharRange(0, rep.alltext.length, clearStyles); }); } } - } + }; - function setTextFace(face) { + const setTextFace = (face) => { root.style.fontFamily = face; lineMetricsDiv.style.fontFamily = face; - } + }; - function recreateDOM() { + const recreateDOM = () => { // precond: normalized recolorLinesInRange(0, rep.alltext.length); - } + }; - function setEditable(newVal) { + const setEditable = (newVal) => { isEditable = newVal; root.contentEditable = isEditable ? 'true' : 'false'; root.classList.toggle('static', !isEditable); - } + }; - function enforceEditability() { - setEditable(isEditable); - } + const enforceEditability = () => setEditable(isEditable); - function importText(text, undoable, dontProcess) { + const importText = (text, undoable, dontProcess) => { let lines; if (dontProcess) { - if (text.charAt(text.length - 1) != '\n') { + if (text.charAt(text.length - 1) !== '\n') { throw new Error('new raw text must end with newline'); } if (/[\r\t\xa0]/.exec(text)) { @@ -495,7 +502,7 @@ function Ace2Inner() { } lines = text.substring(0, text.length - 1).split('\n'); } else { - lines = _.map(text.split('\n'), textify); + lines = text.split('\n').map(textify); } let newText = '\n'; if (lines.length > 0) { @@ -506,12 +513,12 @@ function Ace2Inner() { setDocText(newText); }); - if (dontProcess && rep.alltext != text) { + if (dontProcess && rep.alltext !== text) { throw new Error('mismatch error setting raw text in importText'); } - } + }; - function importAText(atext, apoolJsonObj, undoable) { + const importAText = (atext, apoolJsonObj, undoable) => { atext = Changeset.cloneAText(atext); if (apoolJsonObj) { const wireApool = (new AttribPool()).fromJsonable(apoolJsonObj); @@ -520,9 +527,9 @@ function Ace2Inner() { inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => { setDocAText(atext); }); - } + }; - function setDocAText(atext) { + const setDocAText = (atext) => { if (atext.text === '') { /* * The server is fine with atext.text being an empty string, but the front @@ -558,54 +565,55 @@ function Ace2Inner() { Changeset.pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1))); performDocumentApplyChangeset(changeset); - performSelectionChange([0, rep.lines.atIndex(0).lineMarker], [0, rep.lines.atIndex(0).lineMarker]); + performSelectionChange( + [0, rep.lines.atIndex(0).lineMarker], + [0, rep.lines.atIndex(0).lineMarker] + ); idleWorkTimer.atMost(100); - if (rep.alltext != atext.text) { + if (rep.alltext !== atext.text) { dmesg(htmlPrettyEscape(rep.alltext)); dmesg(htmlPrettyEscape(atext.text)); throw new Error('mismatch error setting raw text in setDocAText'); } - } + }; - function setDocText(text) { + const setDocText = (text) => { setDocAText(Changeset.makeAText(text)); - } + }; - function getDocText() { + const getDocText = () => { const alltext = rep.alltext; let len = alltext.length; if (len > 0) len--; // final extra newline return alltext.substring(0, len); - } + }; - function exportText() { + const exportText = () => { if (currentCallStack && !currentCallStack.domClean) { inCallStackIfNecessary('exportText', () => { fastIncorp(2); }); } return getDocText(); - } + }; - function editorChangedSize() { - fixView(); - } + const editorChangedSize = () => fixView(); - function setOnKeyPress(handler) { + const setOnKeyPress = (handler) => { outsideKeyPress = handler; - } + }; - function setOnKeyDown(handler) { + const setOnKeyDown = (handler) => { outsideKeyDown = handler; - } + }; - function setNotifyDirty(handler) { + const setNotifyDirty = (handler) => { outsideNotifyDirty = handler; - } + }; - function getFormattedCode() { + const getFormattedCode = () => { if (currentCallStack && !currentCallStack.domClean) { inCallStackIfNecessary('getFormattedCode', incorporateUserChanges); } @@ -615,15 +623,17 @@ function Ace2Inner() { let entry = rep.lines.atIndex(0); while (entry) { const domInfo = entry.domInfo; - buf.push((domInfo && domInfo.getInnerHTML()) || domline.processSpaces(domline.escapeHTML(entry.text), doesWrap) || ' ' /* empty line*/); + buf.push((domInfo && domInfo.getInnerHTML()) || + domline.processSpaces(domline.escapeHTML(entry.text), doesWrap) || + ' ' /* empty line*/); entry = rep.lines.next(entry); } } return `
${buf.join('
\n
')}
`; - } + }; const CMDS = { - clearauthorship(prompt) { + clearauthorship: (prompt) => { if ((!(rep.selStart && rep.selEnd)) || isCaret()) { if (prompt) { prompt(); @@ -638,53 +648,29 @@ function Ace2Inner() { }, }; - function execCommand(cmd) { + const execCommand = (cmd, ...args) => { cmd = cmd.toLowerCase(); - const cmdArgs = Array.prototype.slice.call(arguments, 1); if (CMDS[cmd]) { inCallStackIfNecessary(cmd, () => { fastIncorp(9); - CMDS[cmd].apply(CMDS, cmdArgs); + CMDS[cmd](...args); }); } - } + }; - function replaceRange(start, end, text) { + const replaceRange = (start, end, text) => { inCallStackIfNecessary('replaceRange', () => { fastIncorp(9); performDocumentReplaceRange(start, end, text); }); - } + }; - editorInfo.ace_focus = focus; - editorInfo.ace_importText = importText; - editorInfo.ace_importAText = importAText; - editorInfo.ace_exportText = exportText; - editorInfo.ace_editorChangedSize = editorChangedSize; - editorInfo.ace_setOnKeyPress = setOnKeyPress; - editorInfo.ace_setOnKeyDown = setOnKeyDown; - editorInfo.ace_setNotifyDirty = setNotifyDirty; - editorInfo.ace_dispose = dispose; - editorInfo.ace_getFormattedCode = getFormattedCode; - editorInfo.ace_setEditable = setEditable; - editorInfo.ace_execCommand = execCommand; - editorInfo.ace_replaceRange = replaceRange; - editorInfo.ace_getAuthorInfos = getAuthorInfos; - editorInfo.ace_performDocumentReplaceRange = performDocumentReplaceRange; - editorInfo.ace_performDocumentReplaceCharRange = performDocumentReplaceCharRange; - editorInfo.ace_renumberList = renumberList; - editorInfo.ace_doReturnKey = doReturnKey; - editorInfo.ace_isBlockElement = isBlockElement; - editorInfo.ace_getLineListType = getLineListType; - - editorInfo.ace_callWithAce = function (fn, callStack, normalize) { - let wrapper = function () { - return fn(editorInfo); - }; + editorInfo.ace_callWithAce = (fn, callStack, normalize) => { + let wrapper = () => fn(editorInfo); if (normalize !== undefined) { const wrapper1 = wrapper; - wrapper = function () { + wrapper = () => { editorInfo.ace_fastIncorp(9); wrapper1(); }; @@ -700,26 +686,25 @@ function Ace2Inner() { // This methed exposes a setter for some ace properties // @param key the name of the parameter // @param value the value to set to - editorInfo.ace_setProperty = function (key, value) { + editorInfo.ace_setProperty = (key, value) => { // These properties are exposed const setters = { wraps: setWraps, showsauthorcolors: (val) => root.classList.toggle('authorColors', !!val), showsuserselections: (val) => root.classList.toggle('userSelections', !!val), - showslinenumbers(value) { + showslinenumbers: (value) => { hasLineNumbers = !!value; sideDiv.parentNode.classList.toggle('line-numbers-hidden', !hasLineNumbers); fixView(); }, - grayedout: (val) => outerWin.document.body.classList.toggle('grayedout', !!val), - dmesg() { dmesg = window.dmesg = value; }, - userauthor(value) { + dmesg: () => { dmesg = window.dmesg = value; }, + userauthor: (value) => { thisAuthor = String(value); documentAttributeManager.author = thisAuthor; }, styled: setStyled, textface: setTextFace, - rtlistrue(value) { + rtlistrue: (value) => { root.classList.toggle('rtl', value); root.classList.toggle('ltr', !value); document.documentElement.dir = value ? 'rtl' : 'ltr'; @@ -734,63 +719,54 @@ function Ace2Inner() { } }; - editorInfo.ace_setBaseText = function (txt) { + editorInfo.ace_setBaseText = (txt) => { changesetTracker.setBaseText(txt); }; - editorInfo.ace_setBaseAttributedText = function (atxt, apoolJsonObj) { + editorInfo.ace_setBaseAttributedText = (atxt, apoolJsonObj) => { changesetTracker.setBaseAttributedText(atxt, apoolJsonObj); }; - editorInfo.ace_applyChangesToBase = function (c, optAuthor, apoolJsonObj) { + editorInfo.ace_applyChangesToBase = (c, optAuthor, apoolJsonObj) => { changesetTracker.applyChangesToBase(c, optAuthor, apoolJsonObj); }; - editorInfo.ace_prepareUserChangeset = function () { - return changesetTracker.prepareUserChangeset(); - }; - editorInfo.ace_applyPreparedChangesetToBase = function () { + editorInfo.ace_prepareUserChangeset = () => changesetTracker.prepareUserChangeset(); + editorInfo.ace_applyPreparedChangesetToBase = () => { changesetTracker.applyPreparedChangesetToBase(); }; - editorInfo.ace_setUserChangeNotificationCallback = function (f) { + editorInfo.ace_setUserChangeNotificationCallback = (f) => { changesetTracker.setUserChangeNotificationCallback(f); }; - editorInfo.ace_setAuthorInfo = function (author, info) { + editorInfo.ace_setAuthorInfo = (author, info) => { setAuthorInfo(author, info); }; - editorInfo.ace_setAuthorSelectionRange = function (author, start, end) { + editorInfo.ace_setAuthorSelectionRange = (author, start, end) => { changesetTracker.setAuthorSelectionRange(author, start, end); }; - editorInfo.ace_getUnhandledErrors = function () { - return caughtErrors.slice(); - }; + editorInfo.ace_getUnhandledErrors = () => caughtErrors.slice(); - editorInfo.ace_getDocument = function () { - return doc; - }; + editorInfo.ace_getDocument = () => doc; - editorInfo.ace_getDebugProperty = function (prop) { - if (prop == 'debugger') { + editorInfo.ace_getDebugProperty = (prop) => { + if (prop === 'debugger') { // obfuscate "eval" so as not to scare yuicompressor window['ev' + 'al']('debugger'); - } else if (prop == 'rep') { + } else if (prop === 'rep') { return rep; - } else if (prop == 'window') { + } else if (prop === 'window') { return window; - } else if (prop == 'document') { + } else if (prop === 'document') { return document; } return undefined; }; - function now() { - return Date.now(); - } + const now = () => Date.now(); - function newTimeLimit(ms) { + const newTimeLimit = (ms) => { const startTime = now(); - let lastElapsed = 0; let exceededAlready = false; let printedTrace = false; - const isTimeUp = function () { + const isTimeUp = () => { if (exceededAlready) { if ((!printedTrace)) { // && now() - startTime - ms > 300) { printedTrace = true; @@ -802,44 +778,42 @@ function Ace2Inner() { exceededAlready = true; return true; } else { - lastElapsed = elapsed; return false; } }; - isTimeUp.elapsed = function () { - return now() - startTime; - }; + isTimeUp.elapsed = () => now() - startTime; return isTimeUp; - } + }; - function makeIdleAction(func) { + const makeIdleAction = (func) => { let scheduledTimeout = null; let scheduledTime = 0; - function unschedule() { + const unschedule = () => { if (scheduledTimeout) { scheduler.clearTimeout(scheduledTimeout); scheduledTimeout = null; } - } + }; - function reschedule(time) { + const reschedule = (time) => { unschedule(); scheduledTime = time; let delay = time - now(); if (delay < 0) delay = 0; scheduledTimeout = scheduler.setTimeout(callback, delay); - } + }; - function callback() { + const callback = () => { scheduledTimeout = null; // func may reschedule the action func(); - } + }; + return { - atMost(ms) { + atMost: (ms) => { const latestTime = now() + ms; if ((!scheduledTimeout) || scheduledTime > latestTime) { reschedule(latestTime); @@ -848,25 +822,25 @@ function Ace2Inner() { // atLeast(ms) will schedule the action if not scheduled yet. // In other words, "infinity" is replaced by ms, even though // it is technically larger. - atLeast(ms) { + atLeast: (ms) => { const earliestTime = now() + ms; if ((!scheduledTimeout) || scheduledTime < earliestTime) { reschedule(earliestTime); } }, - never() { + never: () => { unschedule(); }, }; - } + }; - function fastIncorp(n) { + const fastIncorp = (n) => { // normalize but don't do any lexing or anything incorporateUserChanges(); - } + }; editorInfo.ace_fastIncorp = fastIncorp; - var idleWorkTimer = makeIdleAction(() => { + const idleWorkTimer = makeIdleAction(() => { if (inInternationalComposition) { // don't do idle input incorporation during international input composition idleWorkTimer.atLeast(500); @@ -886,9 +860,6 @@ function Ace2Inner() { updateLineNumbers(); // update line numbers if any time left if (isTimeUp()) return; - - const visibleRange = scroll.getVisibleCharRange(rep); - const docRange = [0, rep.lines.totalWidth()]; finishedImportantWork = true; finishedWork = true; } finally { @@ -910,16 +881,16 @@ function Ace2Inner() { let _nextId = 1; - function uniqueId(n) { + const uniqueId = (n) => { // not actually guaranteed to be unique, e.g. if user copy-pastes // nodes with ids const nid = n.id; if (nid) return nid; return (n.id = `magicdomid${_nextId++}`); - } + }; - function recolorLinesInRange(startChar, endChar) { + const recolorLinesInRange = (startChar, endChar) => { if (endChar <= startChar) return; if (startChar < 0 || startChar >= rep.lines.totalWidth()) return; let lineEntry = rep.lines.atOffset(startChar); // rounds down to line boundary @@ -927,31 +898,27 @@ function Ace2Inner() { let lineIndex = rep.lines.indexOfEntry(lineEntry); let selectionNeedsResetting = false; let firstLine = null; - let lastLine = null; // tokenFunc function; accesses current value of lineEntry and curDocChar, // also mutates curDocChar - let curDocChar; - const tokenFunc = function (tokenText, tokenClass) { + const tokenFunc = (tokenText, tokenClass) => { lineEntry.domInfo.appendSpan(tokenText, tokenClass); }; while (lineEntry && lineStart < endChar) { const lineEnd = lineStart + lineEntry.width; - - curDocChar = lineStart; lineEntry.domInfo.clearSpans(); getSpansForLine(lineEntry, tokenFunc, lineStart); lineEntry.domInfo.finishUpdate(); markNodeClean(lineEntry.lineNode); - if (rep.selStart && rep.selStart[0] == lineIndex || rep.selEnd && rep.selEnd[0] == lineIndex) { + if (rep.selStart && rep.selStart[0] === lineIndex || + rep.selEnd && rep.selEnd[0] === lineIndex) { selectionNeedsResetting = true; } - if (firstLine === null) firstLine = lineIndex; - lastLine = lineIndex; + if (firstLine == null) firstLine = lineIndex; lineStart = lineEnd; lineEntry = rep.lines.next(lineEntry); lineIndex++; @@ -959,27 +926,25 @@ function Ace2Inner() { if (selectionNeedsResetting) { currentCallStack.selectionAffected = true; } - } + }; // like getSpansForRange, but for a line, and the func takes (text,class) // instead of (width,class); excludes the trailing '\n' from // consideration by func - function getSpansForLine(lineEntry, textAndClassFunc, lineEntryOffsetHint) { + const getSpansForLine = (lineEntry, textAndClassFunc, lineEntryOffsetHint) => { let lineEntryOffset = lineEntryOffsetHint; if ((typeof lineEntryOffset) !== 'number') { lineEntryOffset = rep.lines.offsetOfEntry(lineEntry); } const text = lineEntry.text; - const width = lineEntry.width; // text.length+1 if (text.length === 0) { // allow getLineStyleFilter to set line-div styles const func = linestylefilter.getLineStyleFilter( 0, '', textAndClassFunc, rep.apool); func('', ''); } else { - const offsetIntoLine = 0; let filteredFunc = linestylefilter.getFilterStack(text, textAndClassFunc, browser); const lineNum = rep.lines.indexOfEntry(lineEntry); const aline = rep.alines[lineNum]; @@ -987,19 +952,19 @@ function Ace2Inner() { text.length, aline, filteredFunc, rep.apool); filteredFunc(text, ''); } - } + }; let observedChanges; - function clearObservedChanges() { + const clearObservedChanges = () => { observedChanges = { cleanNodesNearChanges: {}, }; - } + }; clearObservedChanges(); - function getCleanNodeByKey(key) { - const p = PROFILER('getCleanNodeByKey', false); + const getCleanNodeByKey = (key) => { + const p = PROFILER('getCleanNodeByKey', false); // eslint-disable-line new-cap p.extra = 0; let n = doc.getElementById(key); // copying and pasting can lead to duplicate ids @@ -1011,9 +976,9 @@ function Ace2Inner() { p.literal(p.extra, 'extra'); p.end(); return n; - } + }; - function observeChangesAroundNode(node) { + const observeChangesAroundNode = (node) => { // Around this top-level DOM node, look for changes to the document // (from how it looks in our representation) and record them in a way // that can be used to "normalize" the document (apply the changes to our @@ -1022,9 +987,10 @@ function Ace2Inner() { let hasAdjacentDirtyness; if (!isNodeDirty(node)) { cleanNode = node; - var prevSib = cleanNode.previousSibling; - var nextSib = cleanNode.nextSibling; - hasAdjacentDirtyness = ((prevSib && isNodeDirty(prevSib)) || (nextSib && isNodeDirty(nextSib))); + const prevSib = cleanNode.previousSibling; + const nextSib = cleanNode.nextSibling; + hasAdjacentDirtyness = ((prevSib && isNodeDirty(prevSib)) || + (nextSib && isNodeDirty(nextSib))); } else { // node is dirty, look for clean node above let upNode = node.previousSibling; @@ -1056,25 +1022,25 @@ function Ace2Inner() { } else { // next and prev lines are clean (if they exist) const lineKey = uniqueId(cleanNode); - var prevSib = cleanNode.previousSibling; - var nextSib = cleanNode.nextSibling; + const prevSib = cleanNode.previousSibling; + const nextSib = cleanNode.nextSibling; const actualPrevKey = ((prevSib && uniqueId(prevSib)) || null); const actualNextKey = ((nextSib && uniqueId(nextSib)) || null); const repPrevEntry = rep.lines.prev(rep.lines.atKey(lineKey)); const repNextEntry = rep.lines.next(rep.lines.atKey(lineKey)); const repPrevKey = ((repPrevEntry && repPrevEntry.key) || null); const repNextKey = ((repNextEntry && repNextEntry.key) || null); - if (actualPrevKey != repPrevKey || actualNextKey != repNextKey) { + if (actualPrevKey !== repPrevKey || actualNextKey !== repNextKey) { observedChanges.cleanNodesNearChanges[`$${uniqueId(cleanNode)}`] = true; } } - } + }; - function observeChangesAroundSelection() { + const observeChangesAroundSelection = () => { if (currentCallStack.observedSelection) return; currentCallStack.observedSelection = true; - const p = PROFILER('getSelection', false); + const p = PROFILER('getSelection', false); // eslint-disable-line new-cap const selection = getSelection(); p.end(); @@ -1082,34 +1048,34 @@ function Ace2Inner() { const node1 = topLevel(selection.startPoint.node); const node2 = topLevel(selection.endPoint.node); if (node1) observeChangesAroundNode(node1); - if (node2 && node1 != node2) { + if (node2 && node1 !== node2) { observeChangesAroundNode(node2); } } - } + }; - function observeSuspiciousNodes() { + const observeSuspiciousNodes = () => { // inspired by Firefox bug #473255, where pasting formatted text // causes the cursor to jump away, making the new HTML never found. if (root.getElementsByTagName) { const nds = root.getElementsByTagName('style'); for (let i = 0; i < nds.length; i++) { const n = topLevel(nds[i]); - if (n && n.parentNode == root) { + if (n && n.parentNode === root) { observeChangesAroundNode(n); } } } - } + }; - function incorporateUserChanges() { + const incorporateUserChanges = () => { if (currentCallStack.domClean) return false; currentCallStack.isUserChange = true; if (DEBUG && window.DONT_INCORP || window.DEBUG_DONT_INCORP) return false; - const p = PROFILER('incorp', false); + const p = PROFILER('incorp', false); // eslint-disable-line new-cap // returns true if dom changes were made if (!root.firstChild) { @@ -1124,10 +1090,13 @@ function Ace2Inner() { let dirtyRangesCheckOut = true; let j = 0; let a, b; + let scrollToTheLeftNeeded = false; + while (j < dirtyRanges.length) { a = dirtyRanges[j][0]; b = dirtyRanges[j][1]; - if (!((a === 0 || getCleanNodeByKey(rep.lines.atIndex(a - 1).key)) && (b == rep.lines.length() || getCleanNodeByKey(rep.lines.atIndex(b).key)))) { + if (!((a === 0 || getCleanNodeByKey(rep.lines.atIndex(a - 1).key)) && + (b === rep.lines.length() || getCleanNodeByKey(rep.lines.atIndex(b).key)))) { dirtyRangesCheckOut = false; break; } @@ -1135,7 +1104,7 @@ function Ace2Inner() { } if (!dirtyRangesCheckOut) { const numBodyNodes = root.childNodes.length; - for (var k = 0; k < numBodyNodes; k++) { + for (let k = 0; k < numBodyNodes; k++) { const bodyNode = root.childNodes.item(k); if ((bodyNode.tagName) && ((!bodyNode.id) || (!rep.lines.containsKey(bodyNode.id)))) { observeChangesAroundNode(bodyNode); @@ -1161,15 +1130,20 @@ function Ace2Inner() { const range = dirtyRanges[i]; a = range[0]; b = range[1]; - let firstDirtyNode = (((a === 0) && root.firstChild) || getCleanNodeByKey(rep.lines.atIndex(a - 1).key).nextSibling); + let firstDirtyNode = (((a === 0) && root.firstChild) || + getCleanNodeByKey(rep.lines.atIndex(a - 1).key).nextSibling); firstDirtyNode = (firstDirtyNode && isNodeDirty(firstDirtyNode) && firstDirtyNode); - let lastDirtyNode = (((b == rep.lines.length()) && root.lastChild) || getCleanNodeByKey(rep.lines.atIndex(b).key).previousSibling); + + let lastDirtyNode = (((b === rep.lines.length()) && root.lastChild) || + getCleanNodeByKey(rep.lines.atIndex(b).key).previousSibling); + lastDirtyNode = (lastDirtyNode && isNodeDirty(lastDirtyNode) && lastDirtyNode); if (firstDirtyNode && lastDirtyNode) { - const cc = makeContentCollector(isStyled, browser, rep.apool, null, className2Author); + const cc = makeContentCollector(isStyled, browser, rep.apool, className2Author); cc.notifySelection(selection); const dirtyNodes = []; - for (let n = firstDirtyNode; n && !(n.previousSibling && n.previousSibling == lastDirtyNode); + for (let n = firstDirtyNode; n && + !(n.previousSibling && n.previousSibling === lastDirtyNode); n = n.nextSibling) { cc.collectContent(n); dirtyNodes.push(n); @@ -1195,7 +1169,6 @@ function Ace2Inner() { lines = ccData.lines; const lineAttribs = ccData.lineAttribs; const linesWrapped = ccData.linesWrapped; - var scrollToTheLeftNeeded = false; if (linesWrapped > 0) { // Chrome decides in its infinite wisdom that it's okay to put the browser's visisble @@ -1203,7 +1176,7 @@ function Ace2Inner() { // string are no longer visible to the user.. Yay chrome.. Move the browser's visible area // to the left hand side of the span. Firefox isn't quite so bad, but it's still pretty // quirky. - var scrollToTheLeftNeeded = true; + scrollToTheLeftNeeded = true; } if (ss[0] >= 0) selStart = [ss[0] + a + netNumLinesChangeSoFar, ss[1]]; @@ -1212,7 +1185,7 @@ function Ace2Inner() { const entries = []; const nodeToAddAfter = lastDirtyNode; const lineNodeInfos = new Array(lines.length); - for (var k = 0; k < lines.length; k++) { + for (let k = 0; k < lines.length; k++) { const lineString = lines[k]; const newEntry = createDomLineEntry(lineString); entries.push(newEntry); @@ -1220,7 +1193,7 @@ function Ace2Inner() { } // var fragment = magicdom.wrapDom(document.createDocumentFragment()); domInsertsNeeded.push([nodeToAddAfter, lineNodeInfos]); - _.each(dirtyNodes, (n) => { + dirtyNodes.forEach((n) => { toDeleteAtEnd.push(n); }); const spliceHints = {}; @@ -1241,19 +1214,19 @@ function Ace2Inner() { // update the representation p.mark('splice'); - _.each(splicesToDo, (splice) => { + splicesToDo.forEach((splice) => { doIncorpLineSplice(splice[0], splice[1], splice[2], splice[3], splice[4]); }); // do DOM inserts p.mark('insert'); - _.each(domInsertsNeeded, (ins) => { + domInsertsNeeded.forEach((ins) => { insertDomLines(ins[0], ins[1]); }); p.mark('del'); // delete old dom nodes - _.each(toDeleteAtEnd, (n) => { + toDeleteAtEnd.forEach((n) => { // var id = n.uniqueId(); // parent of n may not be "root" in IE due to non-tree-shaped DOM (wtf) if (n.parentNode) n.parentNode.removeChild(n); @@ -1261,7 +1234,8 @@ function Ace2Inner() { // dmesg(htmlPrettyEscape(htmlForRemovedChild(n))); }); - if (scrollToTheLeftNeeded) { // needed to stop chrome from breaking the ui when long strings without spaces are pasted + // needed to stop chrome from breaking the ui when long strings without spaces are pasted + if (scrollToTheLeftNeeded) { $('#innerdocbody').scrollLeft(0); } @@ -1278,7 +1252,8 @@ function Ace2Inner() { point: selection.startPoint, documentAttributeManager, }); - selStart = (selStartFromHook == null || selStartFromHook.length == 0) ? getLineAndCharForPoint(selection.startPoint) : selStartFromHook; + selStart = (selStartFromHook == null || selStartFromHook.length === 0) + ? getLineAndCharForPoint(selection.startPoint) : selStartFromHook; } if (selection && !selEnd) { const selEndFromHook = hooks.callAll('aceEndLineAndCharForPoint', { @@ -1289,7 +1264,9 @@ function Ace2Inner() { point: selection.endPoint, documentAttributeManager, }); - selEnd = (selEndFromHook == null || selEndFromHook.length == 0) ? getLineAndCharForPoint(selection.endPoint) : selEndFromHook; + selEnd = (selEndFromHook == null || + selEndFromHook.length === 0) + ? getLineAndCharForPoint(selection.endPoint) : selEndFromHook; } // selection from content collection can, in various ways, extend past final @@ -1328,9 +1305,9 @@ function Ace2Inner() { p.end('END'); return domChanges; - } + }; - var STYLE_ATTRIBS = { + const STYLE_ATTRIBS = { bold: true, italic: true, underline: true, @@ -1338,25 +1315,18 @@ function Ace2Inner() { list: true, }; - function isStyleAttribute(aname) { - return !!STYLE_ATTRIBS[aname]; - } + const isStyleAttribute = (aname) => !!STYLE_ATTRIBS[aname]; - function isDefaultLineAttribute(aname) { - return AttributeManager.DEFAULT_LINE_ATTRIBUTES.indexOf(aname) !== -1; - } + const isDefaultLineAttribute = + (aname) => AttributeManager.DEFAULT_LINE_ATTRIBUTES.indexOf(aname) !== -1; - function insertDomLines(nodeToAddAfter, infoStructs) { + const insertDomLines = (nodeToAddAfter, infoStructs) => { let lastEntry; let lineStartOffset; if (infoStructs.length < 1) return; - const startEntry = rep.lines.atKey(uniqueId(infoStructs[0].node)); - const endEntry = rep.lines.atKey(uniqueId(infoStructs[infoStructs.length - 1].node)); - const charStart = rep.lines.offsetOfEntry(startEntry); - const charEnd = rep.lines.offsetOfEntry(endEntry) + endEntry.width; - _.each(infoStructs, (info) => { - const p2 = PROFILER('insertLine', false); + infoStructs.forEach((info) => { + const p2 = PROFILER('insertLine', false); // eslint-disable-line new-cap const node = info.node; const key = uniqueId(node); let entry; @@ -1364,7 +1334,7 @@ function Ace2Inner() { if (lastEntry) { // optimization to avoid recalculation const next = rep.lines.next(lastEntry); - if (next && next.key == key) { + if (next && next.key === key) { entry = next; lineStartOffset += lastEntry.width; } @@ -1393,32 +1363,32 @@ function Ace2Inner() { markNodeClean(node); p2.end(); }); - } + }; - function isCaret() { - return (rep.selStart && rep.selEnd && rep.selStart[0] == rep.selEnd[0] && rep.selStart[1] == rep.selEnd[1]); - } + const isCaret = () => ( + rep.selStart && + rep.selEnd && + rep.selStart[0] === rep.selEnd[0] && + rep.selStart[1] === rep.selEnd[1] + ); editorInfo.ace_isCaret = isCaret; // prereq: isCaret() - function caretLine() { - return rep.selStart[0]; - } + const caretLine = () => rep.selStart[0]; + editorInfo.ace_caretLine = caretLine; - function caretColumn() { - return rep.selStart[1]; - } + const caretColumn = () => rep.selStart[1]; + editorInfo.ace_caretColumn = caretColumn; - function caretDocChar() { - return rep.lines.offsetOfIndex(caretLine()) + caretColumn(); - } + const caretDocChar = () => rep.lines.offsetOfIndex(caretLine()) + caretColumn(); + editorInfo.ace_caretDocChar = caretDocChar; - function handleReturnIndentation() { + const handleReturnIndentation = () => { // on return, indent to level of previous line if (isCaret() && caretColumn() === 0 && caretLine() > 0) { const lineNum = caretLine(); @@ -1427,7 +1397,7 @@ function Ace2Inner() { const prevLineText = prevLine.text; let theIndent = /^ *(?:)/.exec(prevLineText)[0]; const shouldIndent = parent.parent.clientVars.indentationOnNewLine; - if (shouldIndent && /[\[\(\:\{]\s*$/.exec(prevLineText)) { + if (shouldIndent && /[[(:{]\s*$/.exec(prevLineText)) { theIndent += THE_TAB; } const cs = Changeset.builder(rep.lines.totalWidth()).keep( @@ -1438,9 +1408,9 @@ function Ace2Inner() { performDocumentApplyChangeset(cs); performSelectionChange([lineNum, theIndent.length], [lineNum, theIndent.length]); } - } + }; - function getPointForLineAndChar(lineAndChar) { + const getPointForLineAndChar = (lineAndChar) => { const line = lineAndChar[0]; let charsLeft = lineAndChar[1]; // Do not uncomment this in production it will break iFrames. @@ -1455,14 +1425,13 @@ function Ace2Inner() { let n = lineNode; let after = false; if (charsLeft === 0) { - let index = 0; return { node: lineNode, - index, + index: 0, maxIndex: 1, }; } - while (!(n == lineNode && after)) { + while (!(n === lineNode && after)) { if (after) { if (n.nextSibling) { n = n.nextSibling; @@ -1486,17 +1455,15 @@ function Ace2Inner() { index: 1, maxIndex: 1, }; - } + }; - function nodeText(n) { - return n.textContent || n.nodeValue || ''; - } + const nodeText = (n) => n.textContent || n.nodeValue || ''; - function getLineAndCharForPoint(point) { + const getLineAndCharForPoint = (point) => { // Turn DOM node selection into [line,char] selection. // This method has to work when the DOM is not pristine, // assuming the point is not in a dirty node. - if (point.node == root) { + if (point.node === root) { if (point.index === 0) { return [0, 0]; } else { @@ -1515,7 +1482,7 @@ function Ace2Inner() { col = nodeText(n).length; } let parNode, prevSib; - while ((parNode = n.parentNode) != root) { + while ((parNode = n.parentNode) !== root) { if ((prevSib = n.previousSibling)) { n = prevSib; col += nodeText(n).length; @@ -1530,10 +1497,10 @@ function Ace2Inner() { const lineNum = rep.lines.indexOfEntry(lineEntry); return [lineNum, col]; } - } + }; editorInfo.ace_getLineAndCharForPoint = getLineAndCharForPoint; - function createDomLineEntry(lineString) { + const createDomLineEntry = (lineString) => { const info = doCreateDomLine(lineString.length > 0); const newNode = info.node; return { @@ -1543,46 +1510,10 @@ function Ace2Inner() { domInfo: info, lineMarker: 0, }; - } + }; - function canApplyChangesetToDocument(changes) { - return Changeset.oldLen(changes) == rep.alltext.length; - } - - function performDocumentApplyChangeset(changes, insertsAfterSelection) { - doRepApplyChangeset(changes, insertsAfterSelection); - - let requiredSelectionSetting = null; - if (rep.selStart && rep.selEnd) { - const selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1]; - const selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1]; - const result = Changeset.characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection); - requiredSelectionSetting = [result[0], result[1], rep.selFocusAtStart]; - } - - const linesMutatee = { - splice(start, numRemoved, newLinesVA) { - const args = Array.prototype.slice.call(arguments, 2); - domAndRepSplice(start, numRemoved, _.map(args, (s) => s.slice(0, -1))); - }, - get(i) { - return `${rep.lines.atIndex(i).text}\n`; - }, - length() { - return rep.lines.length(); - }, - slice_notused(start, end) { - return _.map(rep.lines.slice(start, end), (e) => `${e.text}\n`); - }, - }; - - Changeset.mutateTextLines(changes, linesMutatee); - - if (requiredSelectionSetting) { - performSelectionChange(lineAndColumnFromChar(requiredSelectionSetting[0]), lineAndColumnFromChar(requiredSelectionSetting[1]), requiredSelectionSetting[2]); - } - - function domAndRepSplice(startLine, deleteCount, newLineStrings) { + const performDocumentApplyChangeset = (changes, insertsAfterSelection) => { + const domAndRepSplice = (startLine, deleteCount, newLineStrings) => { const keysToDelete = []; if (deleteCount > 0) { let entryToDelete = rep.lines.atIndex(startLine); @@ -1592,7 +1523,7 @@ function Ace2Inner() { } } - const lineEntries = _.map(newLineStrings, createDomLineEntry); + const lineEntries = newLineStrings.map(createDomLineEntry); doRepLineSplice(startLine, deleteCount, lineEntries); @@ -1601,27 +1532,67 @@ function Ace2Inner() { nodeToAddAfter = getCleanNodeByKey(rep.lines.atIndex(startLine - 1).key); } else { nodeToAddAfter = null; } - insertDomLines(nodeToAddAfter, _.map(lineEntries, (entry) => entry.domInfo)); + insertDomLines(nodeToAddAfter, lineEntries.map((entry) => entry.domInfo)); - _.each(keysToDelete, (k) => { + keysToDelete.forEach((k) => { const n = doc.getElementById(k); n.parentNode.removeChild(n); }); - if ((rep.selStart && rep.selStart[0] >= startLine && rep.selStart[0] <= startLine + deleteCount) || (rep.selEnd && rep.selEnd[0] >= startLine && rep.selEnd[0] <= startLine + deleteCount)) { + if ( + (rep.selStart && + rep.selStart[0] >= startLine && + rep.selStart[0] <= startLine + deleteCount) || + (rep.selEnd && rep.selEnd[0] >= startLine && rep.selEnd[0] <= startLine + deleteCount)) { currentCallStack.selectionAffected = true; } - } - } + }; - function doRepApplyChangeset(changes, insertsAfterSelection) { + doRepApplyChangeset(changes, insertsAfterSelection); + + let requiredSelectionSetting = null; + if (rep.selStart && rep.selEnd) { + const selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1]; + const selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1]; + const result = + Changeset.characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection); + requiredSelectionSetting = [result[0], result[1], rep.selFocusAtStart]; + } + + const linesMutatee = { + // TODO: Rhansen to check usage of args here. + splice: (start, numRemoved, ...args) => { + domAndRepSplice(start, numRemoved, args.map((s) => s.slice(0, -1))); + }, + get: (i) => `${rep.lines.atIndex(i).text}\n`, + length: () => rep.lines.length(), + }; + + Changeset.mutateTextLines(changes, linesMutatee); + + if (requiredSelectionSetting) { + performSelectionChange( + lineAndColumnFromChar( + requiredSelectionSetting[0] + ), + lineAndColumnFromChar(requiredSelectionSetting[1]), + requiredSelectionSetting[2] + ); + } + }; + + const doRepApplyChangeset = (changes, insertsAfterSelection) => { Changeset.checkRep(changes); - if (Changeset.oldLen(changes) != rep.alltext.length) throw new Error(`doRepApplyChangeset length mismatch: ${Changeset.oldLen(changes)}/${rep.alltext.length}`); + if (Changeset.oldLen(changes) !== rep.alltext.length) { + const errMsg = `${Changeset.oldLen(changes)}/${rep.alltext.length}`; + throw new Error(`doRepApplyChangeset length mismatch: ${errMsg}`); + } - (function doRecordUndoInformation(changes) { + // (function doRecordUndoInformation(changes) { + ((changes) => { const editEvent = currentCallStack.editEvent; - if (editEvent.eventType == 'nonundoable') { + if (editEvent.eventType === 'nonundoable') { if (!editEvent.changeset) { editEvent.changeset = changes; } else { @@ -1629,12 +1600,8 @@ function Ace2Inner() { } } else { const inverseChangeset = Changeset.inverse(changes, { - get(i) { - return `${rep.lines.atIndex(i).text}\n`; - }, - length() { - return rep.lines.length(); - }, + get: (i) => `${rep.lines.atIndex(i).text}\n`, + length: () => rep.lines.length(), }, rep.alines, rep.apool); if (!editEvent.backset) { @@ -1651,28 +1618,28 @@ function Ace2Inner() { if (changesetTracker.isTracking()) { changesetTracker.composeUserChangeset(changes); } - } + }; /* Converts the position of a char (index in String) into a [row, col] tuple */ - function lineAndColumnFromChar(x) { + const lineAndColumnFromChar = (x) => { const lineEntry = rep.lines.atOffset(x); const lineStart = rep.lines.offsetOfEntry(lineEntry); const lineNum = rep.lines.indexOfEntry(lineEntry); return [lineNum, x - lineStart]; - } + }; - function performDocumentReplaceCharRange(startChar, endChar, newText) { - if (startChar == endChar && newText.length === 0) { + const performDocumentReplaceCharRange = (startChar, endChar, newText) => { + if (startChar === endChar && newText.length === 0) { return; } // Requires that the replacement preserve the property that the // internal document text ends in a newline. Given this, we // rewrite the splice so that it doesn't touch the very last // char of the document. - if (endChar == rep.alltext.length) { - if (startChar == endChar) { + if (endChar === rep.alltext.length) { + if (startChar === endChar) { // an insert at end startChar--; endChar--; @@ -1687,49 +1654,31 @@ function Ace2Inner() { newText = newText.substring(0, newText.length - 1); } } - performDocumentReplaceRange(lineAndColumnFromChar(startChar), lineAndColumnFromChar(endChar), newText); - } + performDocumentReplaceRange(lineAndColumnFromChar(startChar), + lineAndColumnFromChar(endChar), newText); + }; - function performDocumentReplaceRange(start, end, newText) { - if (start === undefined) start = rep.selStart; - if (end === undefined) end = rep.selEnd; - - // dmesg(String([start.toSource(),end.toSource(),newText.toSource()])); - // start[0]: <--- start[1] --->CCCCCCCCCCC\n - // CCCCCCCCCCCCCCCCCCCC\n - // CCCC\n - // end[0]: -------\n - const builder = Changeset.builder(rep.lines.totalWidth()); - ChangesetUtils.buildKeepToStartOfRange(rep, builder, start); - ChangesetUtils.buildRemoveRange(rep, builder, start, end); - builder.insert(newText, [ - ['author', thisAuthor], - ], rep.apool); - const cs = builder.toString(); - - performDocumentApplyChangeset(cs); - } - - function performDocumentApplyAttributesToCharRange(start, end, attribs) { + const performDocumentApplyAttributesToCharRange = (start, end, attribs) => { end = Math.min(end, rep.alltext.length - 1); - documentAttributeManager.setAttributesOnRange(lineAndColumnFromChar(start), lineAndColumnFromChar(end), attribs); - } - editorInfo.ace_performDocumentApplyAttributesToCharRange = performDocumentApplyAttributesToCharRange; + documentAttributeManager.setAttributesOnRange( + lineAndColumnFromChar(start), lineAndColumnFromChar(end), attribs); + }; + editorInfo.ace_performDocumentApplyAttributesToCharRange = + performDocumentApplyAttributesToCharRange; - function setAttributeOnSelection(attributeName, attributeValue) { + const setAttributeOnSelection = (attributeName, attributeValue) => { if (!(rep.selStart && rep.selEnd)) return; documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [ [attributeName, attributeValue], ]); - } + }; editorInfo.ace_setAttributeOnSelection = setAttributeOnSelection; - - function getAttributeOnSelection(attributeName, prevChar) { + const getAttributeOnSelection = (attributeName, prevChar) => { if (!(rep.selStart && rep.selEnd)) return; - const isNotSelection = (rep.selStart[0] == rep.selEnd[0] && rep.selEnd[1] === rep.selStart[1]); + const isNotSelection = (rep.selStart[0] === rep.selEnd[0] && rep.selEnd[1] === rep.selStart[1]); if (isNotSelection) { if (prevChar) { // If it's not the start of the line @@ -1743,21 +1692,18 @@ function Ace2Inner() { [attributeName, 'true'], ], rep.apool); const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`); - function hasIt(attribs) { - return withItRegex.test(attribs); - } + const hasIt = (attribs) => withItRegex.test(attribs); - return rangeHasAttrib(rep.selStart, rep.selEnd); - - function rangeHasAttrib(selStart, selEnd) { + const rangeHasAttrib = (selStart, selEnd) => { // if range is collapsed -> no attribs in range - if (selStart[1] == selEnd[1] && selStart[0] == selEnd[0]) return false; + if (selStart[1] === selEnd[1] && selStart[0] === selEnd[0]) return false; - if (selStart[0] != selEnd[0]) { // -> More than one line selected - var hasAttrib = true; + if (selStart[0] !== selEnd[0]) { // -> More than one line selected + let hasAttrib = true; // from selStart to the end of the first line - hasAttrib = hasAttrib && rangeHasAttrib(selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length]); + hasAttrib = hasAttrib && + rangeHasAttrib(selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length]); // for all lines in between for (let n = selStart[0] + 1; n < selEnd[0]; n++) { @@ -1775,7 +1721,7 @@ function Ace2Inner() { const lineNum = selStart[0]; const start = selStart[1]; const end = selEnd[1]; - var hasAttrib = true; + let hasAttrib = true; // Iterate over attribs on this line @@ -1789,7 +1735,8 @@ function Ace2Inner() { if (!hasIt(op.attribs)) { // does op overlap selection? if (!(opEndInLine <= start || opStartInLine >= end)) { - hasAttrib = false; // since it's overlapping but hasn't got the attrib -> range hasn't got it + // since it's overlapping but hasn't got the attrib -> range hasn't got it + hasAttrib = false; break; } } @@ -1797,12 +1744,13 @@ function Ace2Inner() { } return hasAttrib; - } - } + }; + return rangeHasAttrib(rep.selStart, rep.selEnd); + }; editorInfo.ace_getAttributeOnSelection = getAttributeOnSelection; - function toggleAttributeOnSelection(attributeName) { + const toggleAttributeOnSelection = (attributeName) => { if (!(rep.selStart && rep.selEnd)) return; let selectionAllHasIt = true; @@ -1811,9 +1759,7 @@ function Ace2Inner() { ], rep.apool); const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`); - function hasIt(attribs) { - return withItRegex.test(attribs); - } + const hasIt = (attribs) => withItRegex.test(attribs); const selStartLine = rep.selStart[0]; const selEndLine = rep.selEnd[0]; @@ -1825,10 +1771,10 @@ function Ace2Inner() { selectionStartInLine = 1; // ignore "*" used as line marker } let selectionEndInLine = rep.lines.atIndex(n).text.length; // exclude newline - if (n == selStartLine) { + if (n === selStartLine) { selectionStartInLine = rep.selStart[1]; } - if (n == selEndLine) { + if (n === selEndLine) { selectionEndInLine = rep.selEnd[1]; } while (opIter.hasNext()) { @@ -1851,47 +1797,42 @@ function Ace2Inner() { const attributeValue = selectionAllHasIt ? '' : 'true'; - documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [[attributeName, attributeValue]]); + documentAttributeManager.setAttributesOnRange( + rep.selStart, + rep.selEnd, + [[attributeName, attributeValue]] + ); if (attribIsFormattingStyle(attributeName)) { updateStyleButtonState(attributeName, !selectionAllHasIt); // italic, bold, ... } - } + }; editorInfo.ace_toggleAttributeOnSelection = toggleAttributeOnSelection; - function performDocumentReplaceSelection(newText) { + const performDocumentReplaceSelection = (newText) => { if (!(rep.selStart && rep.selEnd)) return; performDocumentReplaceRange(rep.selStart, rep.selEnd, newText); - } + }; // Change the abstract representation of the document to have a different set of lines. // Must be called after rep.alltext is set. - - - function doRepLineSplice(startLine, deleteCount, newLineEntries) { - _.each(newLineEntries, (entry) => { + const doRepLineSplice = (startLine, deleteCount, newLineEntries) => { + newLineEntries.forEach((entry) => { entry.width = entry.text.length + 1; }); const startOldChar = rep.lines.offsetOfIndex(startLine); const endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount); - const oldRegionStart = rep.lines.offsetOfIndex(startLine); - const oldRegionEnd = rep.lines.offsetOfIndex(startLine + deleteCount); rep.lines.splice(startLine, deleteCount, newLineEntries); currentCallStack.docTextChanged = true; currentCallStack.repChanged = true; - const newRegionEnd = rep.lines.offsetOfIndex(startLine + newLineEntries.length); + const newText = newLineEntries.map((e) => `${e.text}\n`).join(''); - const newText = _.map(newLineEntries, (e) => `${e.text}\n`).join(''); + rep.alltext = rep.alltext.substring(0, startOldChar) + + newText + rep.alltext.substring(endOldChar, rep.alltext.length); + }; - rep.alltext = rep.alltext.substring(0, startOldChar) + newText + rep.alltext.substring(endOldChar, rep.alltext.length); - - // var newTotalLength = rep.alltext.length; - // rep.lexer.updateBuffer(rep.alltext, oldRegionStart, oldRegionEnd - oldRegionStart, - // newRegionEnd - oldRegionStart); - } - - function doIncorpLineSplice(startLine, deleteCount, newLineEntries, lineAttribs, hints) { + const doIncorpLineSplice = (startLine, deleteCount, newLineEntries, lineAttribs, hints) => { const startOldChar = rep.lines.offsetOfIndex(startLine); const endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount); @@ -1899,17 +1840,20 @@ function Ace2Inner() { let selStartHintChar, selEndHintChar; if (hints && hints.selStart) { - selStartHintChar = rep.lines.offsetOfIndex(hints.selStart[0]) + hints.selStart[1] - oldRegionStart; + selStartHintChar = + rep.lines.offsetOfIndex(hints.selStart[0]) + hints.selStart[1] - oldRegionStart; } if (hints && hints.selEnd) { selEndHintChar = rep.lines.offsetOfIndex(hints.selEnd[0]) + hints.selEnd[1] - oldRegionStart; } - const newText = _.map(newLineEntries, (e) => `${e.text}\n`).join(''); + const newText = newLineEntries.map((e) => `${e.text}\n`).join(''); const oldText = rep.alltext.substring(startOldChar, endOldChar); const oldAttribs = rep.alines.slice(startLine, startLine + deleteCount).join(''); const newAttribs = `${lineAttribs.join('|1+1')}|1+1`; // not valid in a changeset - const analysis = analyzeChange(oldText, newText, oldAttribs, newAttribs, selStartHintChar, selEndHintChar); + const analysis = analyzeChange( + oldText, newText, oldAttribs, newAttribs, selStartHintChar, selEndHintChar + ); const commonStart = analysis[0]; let commonEnd = analysis[1]; let shortOldText = oldText.substring(commonStart, oldText.length - commonEnd); @@ -1920,7 +1864,8 @@ function Ace2Inner() { // adjust the splice to not involve the final newline of the document; // be very defensive - if (shortOldText.charAt(shortOldText.length - 1) == '\n' && shortNewText.charAt(shortNewText.length - 1) == '\n') { + if (shortOldText.charAt(shortOldText.length - 1) === '\n' && + shortNewText.charAt(shortNewText.length - 1) === '\n') { // replacing text that ends in newline with text that also ends in newline // (still, after analysis, somehow) shortOldText = shortOldText.slice(0, -1); @@ -1928,16 +1873,20 @@ function Ace2Inner() { spliceEnd--; commonEnd++; } - if (shortOldText.length === 0 && spliceStart == rep.alltext.length && shortNewText.length > 0) { + if (shortOldText.length === 0 && + spliceStart === rep.alltext.length && + shortNewText.length > 0) { // inserting after final newline, bad spliceStart--; spliceEnd--; shortNewText = `\n${shortNewText.slice(0, -1)}`; shiftFinalNewlineToBeforeNewText = true; } - if (spliceEnd == rep.alltext.length && shortOldText.length > 0 && shortNewText.length === 0) { + if (spliceEnd === rep.alltext.length && + shortOldText.length > 0 && + shortNewText.length === 0) { // deletion at end of rep.alltext - if (rep.alltext.charAt(spliceStart - 1) == '\n') { + if (rep.alltext.charAt(spliceStart - 1) === '\n') { // (if not then what the heck? it will definitely lead // to a rep.alltext without a final newline) spliceStart--; @@ -1952,14 +1901,14 @@ function Ace2Inner() { const spliceStartLine = rep.lines.indexOfOffset(spliceStart); const spliceStartLineStart = rep.lines.offsetOfIndex(spliceStartLine); - const startBuilder = function () { + const startBuilder = () => { const builder = Changeset.builder(oldLen); builder.keep(spliceStartLineStart, spliceStartLine); builder.keep(spliceStart - spliceStartLineStart); return builder; }; - const eachAttribRun = function (attribs, func /* (startInNewText, endInNewText, attribs)*/) { + const eachAttribRun = (attribs, func /* (startInNewText, endInNewText, attribs)*/) => { const attribsIter = Changeset.opIterator(attribs); let textIndex = 0; const newTextStart = commonStart; @@ -1974,7 +1923,7 @@ function Ace2Inner() { } }; - const justApplyStyles = (shortNewText == shortOldText); + const justApplyStyles = (shortNewText === shortOldText); let theChangeset; if (justApplyStyles) { @@ -1982,13 +1931,16 @@ function Ace2Inner() { // the existing text. we compose this with the // changeset the applies the styles found in the DOM. // This allows us to incorporate, e.g., Safari's native "unbold". - const incorpedAttribClearer = cachedStrFunc((oldAtts) => Changeset.mapAttribNumbers(oldAtts, (n) => { - const k = rep.apool.getAttribKey(n); - if (isStyleAttribute(k)) { - return rep.apool.putAttrib([k, '']); - } - return false; - })); + const incorpedAttribClearer = cachedStrFunc( + (oldAtts) => Changeset.mapAttribNumbers(oldAtts, (n) => { + const k = rep.apool.getAttribKey(n); + if (isStyleAttribute(k)) { + return rep.apool.putAttrib([k, '']); + } + return false; + } + ) + ); const builder1 = startBuilder(); if (shiftFinalNewlineToBeforeNewText) { @@ -2038,7 +1990,7 @@ function Ace2Inner() { let foundDomAuthor = ''; eachAttribRun(newAttribs, (start, end, attribs) => { const a = Changeset.attribsAttributeValue(attribs, 'author', rep.apool); - if (a && a != foundDomAuthor) { + if (a && a !== foundDomAuthor) { if (!foundDomAuthor) { foundDomAuthor = a; } else { @@ -2064,27 +2016,26 @@ function Ace2Inner() { // do this no matter what, because we need to get the right // line keys into the rep. doRepLineSplice(startLine, deleteCount, newLineEntries); - } + }; - function cachedStrFunc(func) { + const cachedStrFunc = (func) => { const cache = {}; - return function (s) { + return (s) => { if (!cache[s]) { cache[s] = func(s); } return cache[s]; }; - } + }; - function analyzeChange(oldText, newText, oldAttribs, newAttribs, optSelStartHint, optSelEndHint) { + const analyzeChange = ( + oldText, newText, oldAttribs, newAttribs, optSelStartHint, optSelEndHint) => { // we need to take into account both the styles attributes & attributes defined by // the plugins, so basically we can ignore only the default line attribs used by // Etherpad - function incorpedAttribFilter(anum) { - return !isDefaultLineAttribute(rep.apool.getAttribKey(anum)); - } + const incorpedAttribFilter = (anum) => !isDefaultLineAttribute(rep.apool.getAttribKey(anum)); - function attribRuns(attribs) { + const attribRuns = (attribs) => { const lengs = []; const atts = []; const iter = Changeset.opIterator(attribs); @@ -2094,14 +2045,14 @@ function Ace2Inner() { atts.push(op.attribs); } return [lengs, atts]; - } + }; - function attribIterator(runs, backward) { + const attribIterator = (runs, backward) => { const lengs = runs[0]; const atts = runs[1]; let i = (backward ? lengs.length - 1 : 0); let j = 0; - return function next() { + const next = () => { while (j >= lengs[i]) { if (backward) i--; else i++; @@ -2111,7 +2062,8 @@ function Ace2Inner() { j++; return a; }; - } + return next; + }; const oldLen = oldText.length; const newLen = newText.length; @@ -2124,7 +2076,8 @@ function Ace2Inner() { const oldStartIter = attribIterator(oldARuns, false); const newStartIter = attribIterator(newARuns, false); while (commonStart < minLen) { - if (oldText.charAt(commonStart) == newText.charAt(commonStart) && oldStartIter() == newStartIter()) { + if (oldText.charAt(commonStart) === newText.charAt(commonStart) && + oldStartIter() === newStartIter()) { commonStart++; } else { break; } } @@ -2138,7 +2091,9 @@ function Ace2Inner() { oldEndIter(); newEndIter(); commonEnd++; - } else if (oldText.charAt(oldLen - 1 - commonEnd) == newText.charAt(newLen - 1 - commonEnd) && oldEndIter() == newEndIter()) { + } else if ( + oldText.charAt(oldLen - 1 - commonEnd) === newText.charAt(newLen - 1 - commonEnd) && + oldEndIter() === newEndIter()) { commonEnd++; } else { break; } } @@ -2151,8 +2106,8 @@ function Ace2Inner() { if (commonStart + commonEnd > oldLen) { // ambiguous insertion - var minCommonEnd = oldLen - commonStart; - var maxCommonEnd = commonEnd; + const minCommonEnd = oldLen - commonStart; + const maxCommonEnd = commonEnd; if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd) { commonEnd = hintedCommonEnd; } else { @@ -2162,8 +2117,8 @@ function Ace2Inner() { } if (commonStart + commonEnd > newLen) { // ambiguous deletion - var minCommonEnd = newLen - commonStart; - var maxCommonEnd = commonEnd; + const minCommonEnd = newLen - commonStart; + const maxCommonEnd = commonEnd; if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd) { commonEnd = hintedCommonEnd; } else { @@ -2173,31 +2128,36 @@ function Ace2Inner() { } return [commonStart, commonEnd]; - } + }; - function equalLineAndChars(a, b) { + const equalLineAndChars = (a, b) => { if (!a) return !b; if (!b) return !a; - return (a[0] == b[0] && a[1] == b[1]); - } + return (a[0] === b[0] && a[1] === b[1]); + }; - function performSelectionChange(selectStart, selectEnd, focusAtStart) { + const performSelectionChange = (selectStart, selectEnd, focusAtStart) => { if (repSelectionChange(selectStart, selectEnd, focusAtStart)) { currentCallStack.selectionAffected = true; } - } + }; editorInfo.ace_performSelectionChange = performSelectionChange; // Change the abstract representation of the document to have a different selection. // Should not rely on the line representation. Should not affect the DOM. - function repSelectionChange(selectStart, selectEnd, focusAtStart) { + const repSelectionChange = (selectStart, selectEnd, focusAtStart) => { focusAtStart = !!focusAtStart; - const newSelFocusAtStart = (focusAtStart && ((!selectStart) || (!selectEnd) || (selectStart[0] != selectEnd[0]) || (selectStart[1] != selectEnd[1]))); + const newSelFocusAtStart = (focusAtStart && ((!selectStart) || + (!selectEnd) || + (selectStart[0] !== selectEnd[0]) || + (selectStart[1] !== selectEnd[1]))); - if ((!equalLineAndChars(rep.selStart, selectStart)) || (!equalLineAndChars(rep.selEnd, selectEnd)) || (rep.selFocusAtStart != newSelFocusAtStart)) { + if ((!equalLineAndChars(rep.selStart, selectStart)) || + (!equalLineAndChars(rep.selEnd, selectEnd)) || + (rep.selFocusAtStart !== newSelFocusAtStart)) { rep.selStart = selectStart; rep.selEnd = selectEnd; rep.selFocusAtStart = newSelFocusAtStart; @@ -2216,9 +2176,12 @@ function Ace2Inner() { // when this settings is enabled const docTextChanged = currentCallStack.docTextChanged; if (!docTextChanged) { - const isScrollableEvent = !isPadLoading(currentCallStack.type) && isScrollableEditEvent(currentCallStack.type); + const isScrollableEvent = !isPadLoading(currentCallStack.type) && + isScrollableEditEvent(currentCallStack.type); const innerHeight = getInnerHeight(); - scroll.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary(rep, isScrollableEvent, innerHeight); + scroll.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary( + rep, isScrollableEvent, innerHeight * 2 + ); } return true; @@ -2229,35 +2192,33 @@ function Ace2Inner() { return false; // Do not uncomment this in production it will break iFrames. // top.console.log("%o %o %s", rep.selStart, rep.selEnd, rep.selFocusAtStart); - } + }; - function isPadLoading(eventType) { - return (eventType === 'setup') || (eventType === 'setBaseText') || (eventType === 'importText'); - } + const isPadLoading = (eventType) => ( + eventType === 'setup') || + (eventType === 'setBaseText') || + (eventType === 'importText' + ); - function updateStyleButtonState(attribName, hasStyleOnRepSelection) { + const updateStyleButtonState = (attribName, hasStyleOnRepSelection) => { const $formattingButton = parent.parent.$(`[data-key="${attribName}"]`).find('a'); $formattingButton.toggleClass(SELECT_BUTTON_CLASS, hasStyleOnRepSelection); - } + }; - function attribIsFormattingStyle(attributeName) { - return _.contains(FORMATTING_STYLES, attributeName); - } + const attribIsFormattingStyle = (attribName) => FORMATTING_STYLES.indexOf(attribName) !== -1; - function selectFormattingButtonIfLineHasStyleApplied(rep) { - _.each(FORMATTING_STYLES, (style) => { - const hasStyleOnRepSelection = documentAttributeManager.hasAttributeOnSelectionOrCaretPosition(style); + const selectFormattingButtonIfLineHasStyleApplied = (rep) => { + FORMATTING_STYLES.forEach((style) => { + const hasStyleOnRepSelection = + documentAttributeManager.hasAttributeOnSelectionOrCaretPosition(style); updateStyleButtonState(style, hasStyleOnRepSelection); }); - } + }; - function doCreateDomLine(nonEmpty) { - return domline.createDomLine(nonEmpty, doesWrap, browser, doc); - } + const doCreateDomLine = (nonEmpty) => domline.createDomLine(nonEmpty, doesWrap, browser, doc); - function textify(str) { - return str.replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, ' '); - } + const textify = + (str) => str.replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, ' '); const _blockElems = { div: 1, @@ -2268,22 +2229,21 @@ function Ace2Inner() { ul: 1, }; - _.each(hooks.callAll('aceRegisterBlockElements'), (element) => { + hooks.callAll('aceRegisterBlockElements').forEach((element) => { _blockElems[element] = 1; }); - function isBlockElement(n) { - return !!_blockElems[(n.tagName || '').toLowerCase()]; - } + const isBlockElement = (n) => !!_blockElems[(n.tagName || '').toLowerCase()]; + editorInfo.ace_isBlockElement = isBlockElement; - function getDirtyRanges() { + const getDirtyRanges = () => { // based on observedChanges, return a list of ranges of original lines // that need to be removed or replaced with new user content to incorporate // the user's changes into the line representation. ranges may be zero-length, // indicating inserted content. for example, [0,0] means content was inserted // at the top of the document, while [3,4] means line 3 was deleted, modified, // or replaced with one or more new lines of content. ranges do not touch. - const p = PROFILER('getDirtyRanges', false); + const p = PROFILER('getDirtyRanges', false); // eslint-disable-line new-cap p.forIndices = 0; p.consecutives = 0; p.corrections = 0; @@ -2292,7 +2252,7 @@ function Ace2Inner() { const N = rep.lines.length(); // old number of lines - function cleanNodeForIndex(i) { + const cleanNodeForIndex = (i) => { // if line (i) in the un-updated line representation maps to a clean node // in the document, return that node. // if (i) is out of bounds, return true. else return false. @@ -2308,13 +2268,13 @@ function Ace2Inner() { cleanNodeForIndexCache[i] = result; } return cleanNodeForIndexCache[i]; - } + }; const isConsecutiveCache = {}; - function isConsecutive(i) { + const isConsecutive = (i) => { if (isConsecutiveCache[i] === undefined) { p.consecutives++; - isConsecutiveCache[i] = (function () { + isConsecutiveCache[i] = (() => { // returns whether line (i) and line (i-1), assumed to be map to clean DOM nodes, // or document boundaries, are consecutive in the changed DOM const a = cleanNodeForIndex(i - 1); @@ -2324,17 +2284,16 @@ function Ace2Inner() { if ((a === true) && b.previousSibling) return false; if ((b === true) && a.nextSibling) return false; if ((a === true) || (b === true)) return true; - return a.nextSibling == b; + return a.nextSibling === b; })(); } return isConsecutiveCache[i]; - } + }; + + // returns whether line (i) in the un-updated representation maps to a clean node, + // or is outside the bounds of the document + const isClean = (i) => !!cleanNodeForIndex(i); - function isClean(i) { - // returns whether line (i) in the un-updated representation maps to a clean node, - // or is outside the bounds of the document - return !!cleanNodeForIndex(i); - } // list of pairs, each representing a range of lines that is clean and consecutive // in the changed DOM. lines (-1) and (N) are always clean, but may or may not // be consecutive with lines in the document. pairs are in sorted order. @@ -2342,38 +2301,39 @@ function Ace2Inner() { [-1, N + 1], ]; - function rangeForLine(i) { + const rangeForLine = (i) => { // returns index of cleanRange containing i, or -1 if none let answer = -1; - _.each(cleanRanges, (r, idx) => { + cleanRanges.forEach((r, idx) => { if (i >= r[1]) return false; // keep looking if (i < r[0]) return true; // not found, stop looking answer = idx; return true; // found, stop looking }); return answer; - } + }; - function removeLineFromRange(rng, line) { + const removeLineFromRange = (rng, line) => { // rng is index into cleanRanges, line is line number // precond: line is in rng const a = cleanRanges[rng][0]; const b = cleanRanges[rng][1]; - if ((a + 1) == b) cleanRanges.splice(rng, 1); - else if (line == a) cleanRanges[rng][0]++; - else if (line == (b - 1)) cleanRanges[rng][1]--; + if ((a + 1) === b) cleanRanges.splice(rng, 1); + else if (line === a) cleanRanges[rng][0]++; + else if (line === (b - 1)) cleanRanges[rng][1]--; else cleanRanges.splice(rng, 1, [a, line], [line + 1, b]); - } + }; - function splitRange(rng, pt) { + const splitRange = (rng, pt) => { // precond: pt splits cleanRanges[rng] into two non-empty ranges const a = cleanRanges[rng][0]; const b = cleanRanges[rng][1]; cleanRanges.splice(rng, 1, [a, pt], [pt, b]); - } + }; + const correctedLines = {}; - function correctlyAssignLine(line) { + const correctlyAssignLine = (line) => { if (correctedLines[line]) return true; p.corrections++; correctedLines[line] = true; @@ -2412,9 +2372,9 @@ function Ace2Inner() { } return !didSomething; } - } + }; - function detectChangesAroundLine(line, reqInARow) { + const detectChangesAroundLine = (line, reqInARow) => { // make sure cleanRanges is correct about line number "line" and the surrounding // lines; only stops checking at end of document or after no changes need // making for several consecutive lines. note that iteration is over old lines, @@ -2436,7 +2396,7 @@ function Ace2Inner() { } else { correctInARow = 0; } currentIndex++; } - } + }; if (N === 0) { p.cancel(); @@ -2450,10 +2410,12 @@ function Ace2Inner() { p.mark('obs'); for (const k in observedChanges.cleanNodesNearChanges) { - const key = k.substring(1); - if (rep.lines.containsKey(key)) { - const line = rep.lines.indexOfKey(key); - detectChangesAroundLine(line, 2); + if (observedChanges.cleanNodesNearChanges[k]) { + const key = k.substring(1); + if (rep.lines.containsKey(key)) { + const line = rep.lines.indexOfKey(key); + detectChangesAroundLine(line, 2); + } } } p.mark('stats&calc'); @@ -2470,74 +2432,36 @@ function Ace2Inner() { p.end(); return dirtyRanges; - } + }; - function markNodeClean(n) { + const markNodeClean = (n) => { // clean nodes have knownHTML that matches their innerHTML const dirtiness = {}; dirtiness.nodeId = uniqueId(n); dirtiness.knownHTML = n.innerHTML; setAssoc(n, 'dirtiness', dirtiness); - } + }; - function isNodeDirty(n) { - const p = PROFILER('cleanCheck', false); - if (n.parentNode != root) return true; + const isNodeDirty = (n) => { + const p = PROFILER('cleanCheck', false); // eslint-disable-line new-cap + if (n.parentNode !== root) return true; const data = getAssoc(n, 'dirtiness'); if (!data) return true; if (n.id !== data.nodeId) return true; if (n.innerHTML !== data.knownHTML) return true; p.end(); return false; - } + }; - function getViewPortTopBottom() { - const theTop = scroll.getScrollY(); - const doc = outerWin.document; - const height = doc.documentElement.clientHeight; // includes padding - - // we have to get the exactly height of the viewport. So it has to subtract all the values which changes - // the viewport height (E.g. padding, position top) - const viewportExtraSpacesAndPosition = getEditorPositionTop() + getPaddingTopAddedWhenPageViewIsEnable(); - return { - top: theTop, - bottom: (theTop + height - viewportExtraSpacesAndPosition), - }; - } - - - function getEditorPositionTop() { - const editor = parent.document.getElementsByTagName('iframe'); - const editorPositionTop = editor[0].offsetTop; - return editorPositionTop; - } - - // ep_page_view adds padding-top, which makes the viewport smaller - function getPaddingTopAddedWhenPageViewIsEnable() { - const rootDocument = parent.parent.document; - const aceOuter = rootDocument.getElementsByName('ace_outer'); - const aceOuterPaddingTop = parseInt($(aceOuter).css('padding-top')); - return aceOuterPaddingTop; - } - - function handleCut(evt) { - inCallStackIfNecessary('handleCut', () => { - doDeleteKey(evt); - }); - return true; - } - - function handleClick(evt) { + const handleClick = (evt) => { inCallStackIfNecessary('handleClick', () => { idleWorkTimer.atMost(200); }); - function isLink(n) { - return (n.tagName || '').toLowerCase() == 'a' && n.href; - } + const isLink = (n) => (n.tagName || '').toLowerCase() === 'a' && n.href; // only want to catch left-click - if ((!evt.ctrlKey) && (evt.button != 2) && (evt.button != 3)) { + if ((!evt.ctrlKey) && (evt.button !== 2) && (evt.button !== 3)) { // find A tag with HREF let n = evt.target; while (n && n.parentNode && !isLink(n)) { @@ -2554,15 +2478,93 @@ function Ace2Inner() { } hideEditBarDropdowns(); - } + }; - function hideEditBarDropdowns() { + const hideEditBarDropdowns = () => { if (window.parent.parent.padeditbar) { // required in case its in an iframe should probably use parent.. See Issue 327 https://github.com/ether/etherpad-lite/issues/327 window.parent.parent.padeditbar.toggleDropDown('none'); } - } + }; - function doReturnKey() { + const renumberList = (lineNum) => { + // 1-check we are in a list + let type = getLineListType(lineNum); + if (!type) { + return null; + } + type = /([a-z]+)[0-9]+/.exec(type); + if (type[1] === 'indent') { + return null; + } + + // 2-find the first line of the list + while (lineNum - 1 >= 0 && (type = getLineListType(lineNum - 1))) { + type = /([a-z]+)[0-9]+/.exec(type); + if (type[1] === 'indent') break; + lineNum--; + } + + // 3-renumber every list item of the same level from the beginning, level 1 + // IMPORTANT: never skip a level because there imbrication may be arbitrary + const builder = Changeset.builder(rep.lines.totalWidth()); + let loc = [0, 0]; + const applyNumberList = (line, level) => { + // init + let position = 1; + let curLevel = level; + let listType; + // loop over the lines + while ((listType = getLineListType(line))) { + // apply new num + listType = /([a-z]+)([0-9]+)/.exec(listType); + curLevel = Number(listType[2]); + if (isNaN(curLevel) || listType[0] === 'indent') { + return line; + } else if (curLevel === level) { + ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 0])); + ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 1]), [ + ['start', position], + ], rep.apool); + + position++; + line++; + } else if (curLevel < level) { + return line;// back to parent + } else { + line = applyNumberList(line, level + 1);// recursive call + } + } + return line; + }; + + applyNumberList(lineNum, 1); + const cs = builder.toString(); + if (!Changeset.isIdentity(cs)) { + performDocumentApplyChangeset(cs); + } + + // 4-apply the modifications + }; + editorInfo.ace_renumberList = renumberList; + + const setLineListType = (lineNum, listType) => { + if (listType === '') { + documentAttributeManager.removeAttributeOnLine(lineNum, listAttributeName); + documentAttributeManager.removeAttributeOnLine(lineNum, 'start'); + } else { + documentAttributeManager.setAttributeOnLine(lineNum, listAttributeName, listType); + } + + // if the list has been removed, it is necessary to renumber + // starting from the *next* line because the list may have been + // separated. If it returns null, it means that the list was not cut, try + // from the current one. + if (renumberList(lineNum + 1) == null) { + renumberList(lineNum); + } + }; + + const doReturnKey = () => { if (!(rep.selStart && rep.selEnd)) { return; } @@ -2593,19 +2595,21 @@ function Ace2Inner() { performDocumentReplaceSelection('\n'); handleReturnIndentation(); } - } + }; + editorInfo.ace_doReturnKey = doReturnKey; - function doIndentOutdent(isOut) { + const doIndentOutdent = (isOut) => { if (!((rep.selStart && rep.selEnd) || - ((rep.selStart[0] == rep.selEnd[0]) && (rep.selStart[1] == rep.selEnd[1]) && rep.selEnd[1] > 1)) && - (isOut != true) + ((rep.selStart[0] === rep.selEnd[0]) && + (rep.selStart[1] === rep.selEnd[1]) && + rep.selEnd[1] > 1)) && + (isOut !== true) ) { return false; } - let firstLine, lastLine; - firstLine = rep.selStart[0]; - lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0)); + const firstLine = rep.selStart[0]; + const lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0)); const mods = []; for (let n = firstLine; n <= lastLine; n++) { let listType = getLineListType(n); @@ -2619,32 +2623,32 @@ function Ace2Inner() { } } const newLevel = Math.max(0, Math.min(MAX_LIST_LEVEL, level + (isOut ? -1 : 1))); - if (level != newLevel) { + if (level !== newLevel) { mods.push([n, (newLevel > 0) ? t + newLevel : '']); } } - _.each(mods, (mod) => { + mods.forEach((mod) => { setLineListType(mod[0], mod[1]); }); return true; - } + }; editorInfo.ace_doIndentOutdent = doIndentOutdent; - function doTabKey(shiftDown) { + const doTabKey = (shiftDown) => { if (!doIndentOutdent(shiftDown)) { performDocumentReplaceSelection(THE_TAB); } - } + }; - function doDeleteKey(optEvt) { + const doDeleteKey = (optEvt) => { const evt = optEvt || {}; let handled = false; if (rep.selStart) { if (isCaret()) { const lineNum = caretLine(); const col = caretColumn(); - var lineEntry = rep.lines.atIndex(lineNum); + const lineEntry = rep.lines.atIndex(lineNum); const lineText = lineEntry.text; const lineMarker = lineEntry.lineMarker; if (/^ +$/.exec(lineText.substring(lineMarker, col))) { @@ -2659,14 +2663,14 @@ function Ace2Inner() { if (!handled) { if (isCaret()) { const theLine = caretLine(); - var lineEntry = rep.lines.atIndex(theLine); + const lineEntry = rep.lines.atIndex(theLine); if (caretColumn() <= lineEntry.lineMarker) { // delete at beginning of line - const action = 'delete_newline'; const prevLineListType = (theLine > 0 ? getLineListType(theLine - 1) : ''); const thisLineListType = getLineListType(theLine); const prevLineEntry = (theLine > 0 && rep.lines.atIndex(theLine - 1)); - const prevLineBlank = (prevLineEntry && prevLineEntry.text.length == prevLineEntry.lineMarker); + const prevLineBlank = (prevLineEntry && + prevLineEntry.text.length === prevLineEntry.lineMarker); const thisLineHasMarker = documentAttributeManager.lineHasMarker(theLine); @@ -2674,17 +2678,26 @@ function Ace2Inner() { // this line is a list if (prevLineBlank && !prevLineListType) { // previous line is blank, remove it - performDocumentReplaceRange([theLine - 1, prevLineEntry.text.length], [theLine, 0], ''); + performDocumentReplaceRange( + [theLine - 1, prevLineEntry.text.length], + [theLine, 0], '' + ); } else { // delistify performDocumentReplaceRange([theLine, 0], [theLine, lineEntry.lineMarker], ''); } } else if (thisLineHasMarker && prevLineEntry) { // If the line has any attributes assigned, remove them by removing the marker '*' - performDocumentReplaceRange([theLine - 1, prevLineEntry.text.length], [theLine, lineEntry.lineMarker], ''); + performDocumentReplaceRange( + [theLine - 1, prevLineEntry.text.length], + [theLine, lineEntry.lineMarker], '' + ); } else if (theLine > 0) { // remove newline - performDocumentReplaceRange([theLine - 1, prevLineEntry.text.length], [theLine, 0], ''); + performDocumentReplaceRange( + [theLine - 1, prevLineEntry.text.length], + [theLine, 0], '' + ); } } else { const docChar = caretDocChar(); @@ -2694,7 +2707,8 @@ function Ace2Inner() { // always delete one char, delete further even if that first char // isn't actually a word char. let deleteBackTo = docChar - 1; - while (deleteBackTo > lineEntry.lineMarker && isWordChar(rep.alltext.charAt(deleteBackTo - 1))) { + while (deleteBackTo > lineEntry.lineMarker && + isWordChar(rep.alltext.charAt(deleteBackTo - 1))) { deleteBackTo--; } performDocumentReplaceCharRange(deleteBackTo, docChar, ''); @@ -2714,52 +2728,15 @@ function Ace2Inner() { // separated. If it returns null, it means that the list was not cut, try // from the current one. const line = caretLine(); - if (line != -1 && renumberList(line + 1) === null) { + if (line !== -1 && renumberList(line + 1) == null) { renumberList(line); } - } - - const REGEX_SPACE = /\s/; + }; const isWordChar = (c) => padutils.wordCharRegex.test(c); editorInfo.ace_isWordChar = isWordChar; - function isSpaceChar(c) { - return !!REGEX_SPACE.exec(c); - } - - function moveByWordInLine(lineText, initialIndex, forwardNotBack) { - let i = initialIndex; - - function nextChar() { - if (forwardNotBack) return lineText.charAt(i); - else return lineText.charAt(i - 1); - } - - function advance() { - if (forwardNotBack) i++; - else i--; - } - - function isDone() { - if (forwardNotBack) return i >= lineText.length; - else return i <= 0; - } - - // On Mac and Linux, move right moves to end of word and move left moves to start; - // on Windows, always move to start of word. - // On Windows, Firefox and IE disagree on whether to stop for punctuation (FF says no). - while ((!isDone()) && !isWordChar(nextChar())) { - advance(); - } - while ((!isDone()) && isWordChar(nextChar())) { - advance(); - } - - return i; - } - - function handleKeyEvent(evt) { + const handleKeyEvent = (evt) => { if (!isEditable) return; const type = evt.type; const charCode = evt.charCode; @@ -2768,36 +2745,40 @@ function Ace2Inner() { const altKey = evt.altKey; const shiftKey = evt.shiftKey; - // Is caret potentially hidden by the chat button? - const myselection = document.getSelection(); // get the current caret selection - const caretOffsetTop = myselection.focusNode.parentNode.offsetTop | myselection.focusNode.offsetTop; // get the carets selection offset in px IE 214 - - if (myselection.focusNode.wholeText) { // Is there any content? If not lineHeight will report wrong.. - var lineHeight = myselection.focusNode.parentNode.offsetHeight; // line height of populated links - } else { - var lineHeight = myselection.focusNode.offsetHeight; // line height of blank lines - } - // dmesg("keyevent type: "+type+", which: "+which); // Don't take action based on modifier keys going up and down. // Modifier keys do not generate "keypress" events. // 224 is the command-key under Mac Firefox. // 91 is the Windows key in IE; it is ASCII for open-bracket but isn't the keycode for that key // 20 is capslock in IE. - const isModKey = ((!charCode) && ((type == 'keyup') || (type == 'keydown')) && (keyCode == 16 || keyCode == 17 || keyCode == 18 || keyCode == 20 || keyCode == 224 || keyCode == 91)); + const isModKey = ((!charCode) && + ((type === 'keyup') || (type === 'keydown')) && + ( + keyCode === 16 || keyCode === 17 || keyCode === 18 || + keyCode === 20 || keyCode === 224 || keyCode === 91 + )); if (isModKey) return; - // If the key is a keypress and the browser is opera and the key is enter, do nothign at all as this fires twice. - if (keyCode == 13 && browser.opera && (type == 'keypress')) { - return; // This stops double enters in Opera but double Tabs still show on single tab keypress, adding keyCode == 9 to this doesn't help as the event is fired twice + // If the key is a keypress and the browser is opera and the key is enter, + // do nothign at all as this fires twice. + if (keyCode === 13 && browser.opera && (type === 'keypress')) { + // This stops double enters in Opera but double Tabs still show on single + // tab keypress, adding keyCode == 9 to this doesn't help as the event is fired twice + return; } let specialHandled = false; - const isTypeForSpecialKey = ((browser.safari || browser.chrome || browser.firefox) ? (type == 'keydown') : (type == 'keypress')); - const isTypeForCmdKey = ((browser.safari || browser.chrome || browser.firefox) ? (type == 'keydown') : (type == 'keypress')); + + const isTypeForSpecialKey = ((browser.safari || + browser.chrome || + browser.firefox) ? (type === 'keydown') : (type === 'keypress')); + const isTypeForCmdKey = ((browser.safari || + browser.chrome || + browser.firefox) ? (type === 'keydown') : (type === 'keypress')); + let stopped = false; inCallStackIfNecessary('handleKeyEvent', function () { - if (type == 'keypress' || (isTypeForSpecialKey && keyCode == 13 /* return*/)) { + if (type === 'keypress' || (isTypeForSpecialKey && keyCode === 13 /* return*/)) { // in IE, special keys don't send keypress, the keydown does the action if (!outsideKeyPress(evt)) { evt.preventDefault(); @@ -2807,7 +2788,7 @@ function Ace2Inner() { // If it's a dead key we don't want to do any Etherpad behavior. stopped = true; return true; - } else if (type == 'keydown') { + } else if (type === 'keydown') { outsideKeyDown(evt); } if (!stopped) { @@ -2821,28 +2802,49 @@ function Ace2Inner() { // if any hook returned true, set specialHandled with true if (specialHandledInHook) { - specialHandled = _.contains(specialHandledInHook, true); + specialHandled = specialHandledInHook.indexOf(true) !== -1; } const padShortcutEnabled = parent.parent.clientVars.padShortcutEnabled; - if ((!specialHandled) && altKey && isTypeForSpecialKey && keyCode == 120 && padShortcutEnabled.altF9) { + if ( + (!specialHandled) && + altKey && + isTypeForSpecialKey && + keyCode === 120 && + padShortcutEnabled.altF9 + ) { // Alt F9 focuses on the File Menu and/or editbar. // Note that while most editors use Alt F10 this is not desirable // As ubuntu cannot use Alt F10.... - // Focus on the editbar. -- TODO: Move Focus back to previous state (we know it so we can use it) - const firstEditbarElement = parent.parent.$('#editbar').children('ul').first().children().first().children().first().children().first(); + // Focus on the editbar. + // -- TODO: Move Focus back to previous state (we know it so we can use it) + const firstEditbarElement = parent.parent.$('#editbar') + .children('ul').first().children().first() + .children().first().children().first(); $(this).blur(); firstEditbarElement.focus(); evt.preventDefault(); } - if ((!specialHandled) && altKey && keyCode == 67 && type === 'keydown' && padShortcutEnabled.altC) { + if ( + (!specialHandled) && + altKey && keyCode === 67 && + type === 'keydown' && + padShortcutEnabled.altC + ) { // Alt c focuses on the Chat window $(this).blur(); parent.parent.chat.show(); parent.parent.$('#chatinput').focus(); evt.preventDefault(); } - if ((!specialHandled) && evt.ctrlKey && shiftKey && keyCode == 50 && type === 'keydown' && padShortcutEnabled.cmdShift2) { + if ( + (!specialHandled) && + evt.ctrlKey && + shiftKey && + keyCode === 50 && + type === 'keydown' && + padShortcutEnabled.cmdShift2 + ) { // Control-Shift-2 shows a gritter popup showing a line author const lineNumber = rep.selEnd[0]; const alineAttrs = rep.alines[lineNumber]; @@ -2852,16 +2854,14 @@ function Ace2Inner() { // TODO: Still work when authorship colors have been cleared // TODO: i18n // TODO: There appears to be a race condition or so. - + const authors = []; let author = null; if (alineAttrs) { - var authors = []; - var authorNames = []; const opIter = Changeset.opIterator(alineAttrs); while (opIter.hasNext()) { const op = opIter.next(); - authorId = Changeset.opAttributeValue(op, 'author', apool); + const authorId = Changeset.opAttributeValue(op, 'author', apool); // Only push unique authors and ones with values if (authors.indexOf(authorId) === -1 && authorId !== '') { @@ -2870,9 +2870,10 @@ function Ace2Inner() { } } - // No author information is available IE on a new pad. + let authorString; + const authorNames = []; if (authors.length === 0) { - var authorString = 'No author information is available'; + authorString = 'No author information is available'; } else { // Known authors info, both current and historical const padAuthors = parent.parent.pad.userList(); @@ -2900,11 +2901,11 @@ function Ace2Inner() { }); } if (authors.length === 1) { - var authorString = `The author of this line is ${authorNames}`; + authorString = `The author of this line is ${authorNames[0]}`; } if (authors.length > 1) { - var authorString = `The authors of this line are ${authorNames.join(' & ')}`; - } + authorString = `The authors of this line are ${authorNames.join(' & ')}`; + } parent.parent.$.gritter.add({ // (string | mandatory) the heading of the notification @@ -2917,7 +2918,11 @@ function Ace2Inner() { time: '4000', }); } - if ((!specialHandled) && isTypeForSpecialKey && keyCode == 8 && padShortcutEnabled.delete) { + if ((!specialHandled) && + isTypeForSpecialKey && + keyCode === 8 && + padShortcutEnabled.delete + ) { // "delete" key; in mozilla, if we're at the beginning of a line, normalize now, // or else deleting a blank line can take two delete presses. // -- @@ -2930,7 +2935,11 @@ function Ace2Inner() { doDeleteKey(evt); specialHandled = true; } - if ((!specialHandled) && isTypeForSpecialKey && keyCode == 13 && padShortcutEnabled.return) { + if ((!specialHandled) && + isTypeForSpecialKey && + keyCode === 13 && + padShortcutEnabled.return + ) { // return key, handle specially; // note that in mozilla we need to do an incorporation for proper return behavior anyway. fastIncorp(4); @@ -2942,7 +2951,11 @@ function Ace2Inner() { }, 0); specialHandled = true; } - if ((!specialHandled) && isTypeForSpecialKey && keyCode == 27 && padShortcutEnabled.esc) { + if ((!specialHandled) && + isTypeForSpecialKey && + keyCode === 27 && + padShortcutEnabled.esc + ) { // prevent esc key; // in mozilla versions 14-19 avoid reconnecting pad. @@ -2953,27 +2966,45 @@ function Ace2Inner() { // close all gritters when the user hits escape key parent.parent.$.gritter.removeAll(); } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 's' && (evt.metaKey || evt.ctrlKey) && !evt.altKey && padShortcutEnabled.cmdS) /* Do a saved revision on ctrl S */ - { + if ( + (!specialHandled) && + /* Do a saved revision on ctrl S */ + isTypeForCmdKey && + String.fromCharCode(which).toLowerCase() === 's' && + (evt.metaKey || evt.ctrlKey) && + !evt.altKey && + padShortcutEnabled.cmdS + ) { evt.preventDefault(); const originalBackground = parent.parent.$('#revisionlink').css('background'); parent.parent.$('#revisionlink').css({background: 'lightyellow'}); scheduler.setTimeout(() => { parent.parent.$('#revisionlink').css({background: originalBackground}); }, 1000); - parent.parent.pad.collabClient.sendMessage({type: 'SAVE_REVISION'}); /* The parent.parent part of this is BAD and I feel bad.. It may break something */ + /* The parent.parent part of this is BAD and I feel bad.. It may break something */ + parent.parent.pad.collabClient.sendMessage({type: 'SAVE_REVISION'}); specialHandled = true; } - if ((!specialHandled) && isTypeForSpecialKey && keyCode == 9 && !(evt.metaKey || evt.ctrlKey) && padShortcutEnabled.tab) { + if ((!specialHandled) && // tab + isTypeForSpecialKey && + keyCode === 9 && + !(evt.metaKey || evt.ctrlKey) && + padShortcutEnabled.tab) { fastIncorp(5); evt.preventDefault(); doTabKey(evt.shiftKey); // scrollSelectionIntoView(); specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'z' && (evt.metaKey || evt.ctrlKey) && !evt.altKey && padShortcutEnabled.cmdZ) { + if ((!specialHandled) && // cmd-Z (undo) + isTypeForCmdKey && + String.fromCharCode(which).toLowerCase() === 'z' && + (evt.metaKey || evt.ctrlKey) && + !evt.altKey && + padShortcutEnabled.cmdZ + ) { fastIncorp(6); evt.preventDefault(); if (evt.shiftKey) { @@ -2983,72 +3014,126 @@ function Ace2Inner() { } specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'y' && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdY) { - // cmd-Y (redo) + if ((!specialHandled) && + // cmd-Y (redo) + isTypeForCmdKey && + String.fromCharCode(which).toLowerCase() === 'y' && + (evt.metaKey || evt.ctrlKey) && + padShortcutEnabled.cmdY + ) { fastIncorp(10); evt.preventDefault(); doUndoRedo('redo'); specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'b' && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdB) { + if ((!specialHandled) && // cmd-B (bold) + isTypeForCmdKey && + String.fromCharCode(which).toLowerCase() === 'b' && + (evt.metaKey || evt.ctrlKey) && + padShortcutEnabled.cmdB) { fastIncorp(13); evt.preventDefault(); toggleAttributeOnSelection('bold'); specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'i' && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdI) { + if ((!specialHandled) && // cmd-I (italic) + isTypeForCmdKey && + String.fromCharCode(which).toLowerCase() === 'i' && + (evt.metaKey || evt.ctrlKey) && + padShortcutEnabled.cmdI + ) { fastIncorp(14); evt.preventDefault(); toggleAttributeOnSelection('italic'); specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'u' && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdU) { + if ((!specialHandled) && + isTypeForCmdKey && + String.fromCharCode(which).toLowerCase() === 'u' && + (evt.metaKey || evt.ctrlKey) && + padShortcutEnabled.cmdU + ) { // cmd-U (underline) fastIncorp(15); evt.preventDefault(); toggleAttributeOnSelection('underline'); specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == '5' && (evt.metaKey || evt.ctrlKey) && evt.altKey !== true && padShortcutEnabled.cmd5) { + if ((!specialHandled) && // cmd-5 (strikethrough) + isTypeForCmdKey && + String.fromCharCode(which).toLowerCase() === '5' && + (evt.metaKey || evt.ctrlKey) && + evt.altKey !== true && + padShortcutEnabled.cmd5 + ) { fastIncorp(13); evt.preventDefault(); toggleAttributeOnSelection('strikethrough'); specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'l' && (evt.metaKey || evt.ctrlKey) && evt.shiftKey && padShortcutEnabled.cmdShiftL) { + if ((!specialHandled) && // cmd-shift-L (unorderedlist) + isTypeForCmdKey && + String.fromCharCode(which).toLowerCase() === 'l' && + (evt.metaKey || evt.ctrlKey) && + evt.shiftKey && + padShortcutEnabled.cmdShiftL + ) { fastIncorp(9); evt.preventDefault(); doInsertUnorderedList(); specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && ((String.fromCharCode(which).toLowerCase() == 'n' && padShortcutEnabled.cmdShiftN) || (String.fromCharCode(which) == 1 && padShortcutEnabled.cmdShift1)) && (evt.metaKey || evt.ctrlKey) && evt.shiftKey) { + if ((!specialHandled) && // cmd-shift-N and cmd-shift-1 (orderedlist) + isTypeForCmdKey && + ( + (String.fromCharCode(which).toLowerCase() === 'n' && + padShortcutEnabled.cmdShiftN) || (String.fromCharCode(which) === '1' && + padShortcutEnabled.cmdShift1) + ) && (evt.metaKey || evt.ctrlKey) && + evt.shiftKey + ) { fastIncorp(9); evt.preventDefault(); doInsertOrderedList(); specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'c' && (evt.metaKey || evt.ctrlKey) && evt.shiftKey && padShortcutEnabled.cmdShiftC) { + if ((!specialHandled) && // cmd-shift-C (clearauthorship) + isTypeForCmdKey && + String.fromCharCode(which).toLowerCase() === 'c' && + (evt.metaKey || evt.ctrlKey) && + evt.shiftKey && padShortcutEnabled.cmdShiftC + ) { fastIncorp(9); evt.preventDefault(); CMDS.clearauthorship(); } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'h' && (evt.ctrlKey) && padShortcutEnabled.cmdH) { + if ((!specialHandled) && // cmd-H (backspace) + isTypeForCmdKey && + String.fromCharCode(which).toLowerCase() === 'h' && + (evt.ctrlKey) && + padShortcutEnabled.cmdH + ) { fastIncorp(20); evt.preventDefault(); doDeleteKey(); specialHandled = true; } - if ((evt.which == 36 && evt.ctrlKey == true) && padShortcutEnabled.ctrlHome) { scroll.setScrollY(0); } // Control Home send to Y = 0 - if ((evt.which == 33 || evt.which == 34) && type == 'keydown' && !evt.ctrlKey) { - evt.preventDefault(); // This is required, browsers will try to do normal default behavior on page up / down and the default behavior SUCKS - + if ((evt.which === 36 && evt.ctrlKey === true) && + // Control Home send to Y = 0 + padShortcutEnabled.ctrlHome) { + scroll.setScrollY(0); + } + if ((evt.which === 33 || evt.which === 34) && type === 'keydown' && !evt.ctrlKey) { + // This is required, browsers will try to do normal default behavior on + // page up / down and the default behavior SUCKS + evt.preventDefault(); const oldVisibleLineRange = scroll.getVisibleLineRange(rep); let topOffset = rep.selStart[0] - oldVisibleLineRange[0]; if (topOffset < 0) { @@ -3059,19 +3144,30 @@ function Ace2Inner() { const isPageUp = evt.which === 33; scheduler.setTimeout(() => { - const newVisibleLineRange = scroll.getVisibleLineRange(rep); // the visible lines IE 1,10 - const linesCount = rep.lines.length(); // total count of lines in pad IE 10 - const numberOfLinesInViewport = newVisibleLineRange[1] - newVisibleLineRange[0]; // How many lines are in the viewport right now? + // the visible lines IE 1,10 + const newVisibleLineRange = scroll.getVisibleLineRange(rep); + // total count of lines in pad IE 10 + const linesCount = rep.lines.length(); + // How many lines are in the viewport right now? + const numberOfLinesInViewport = newVisibleLineRange[1] - newVisibleLineRange[0]; if (isPageUp && padShortcutEnabled.pageUp) { - rep.selEnd[0] = rep.selEnd[0] - numberOfLinesInViewport; // move to the bottom line +1 in the viewport (essentially skipping over a page) - rep.selStart[0] = rep.selStart[0] - numberOfLinesInViewport; // move to the bottom line +1 in the viewport (essentially skipping over a page) + // move to the bottom line +1 in the viewport (essentially skipping over a page) + rep.selEnd[0] -= numberOfLinesInViewport; + // move to the bottom line +1 in the viewport (essentially skipping over a page) + rep.selStart[0] -= numberOfLinesInViewport; } - if (isPageDown && padShortcutEnabled.pageDown) { // if we hit page down - if (rep.selEnd[0] >= oldVisibleLineRange[0]) { // If the new viewpoint position is actually further than where we are right now - rep.selStart[0] = oldVisibleLineRange[1] - 1; // dont go further in the page down than what's visible IE go from 0 to 50 if 50 is visible on screen but dont go below that else we miss content - rep.selEnd[0] = oldVisibleLineRange[1] - 1; // dont go further in the page down than what's visible IE go from 0 to 50 if 50 is visible on screen but dont go below that else we miss content + // if we hit page down + if (isPageDown && padShortcutEnabled.pageDown) { + // If the new viewpoint position is actually further than where we are right now + if (rep.selEnd[0] >= oldVisibleLineRange[0]) { + // dont go further in the page down than what's visible IE go from 0 to 50 + // if 50 is visible on screen but dont go below that else we miss content + rep.selStart[0] = oldVisibleLineRange[1] - 1; + // dont go further in the page down than what's visible IE go from 0 to 50 + // if 50 is visible on screen but dont go below that else we miss content + rep.selEnd[0] = oldVisibleLineRange[1] - 1; } } @@ -3086,24 +3182,33 @@ function Ace2Inner() { rep.selEnd[0] = linesCount - 1; } updateBrowserSelectionFromRep(); - const myselection = document.getSelection(); // get the current caret selection, can't use rep. here because that only gives us the start position not the current - let caretOffsetTop = myselection.focusNode.parentNode.offsetTop || myselection.focusNode.offsetTop; // get the carets selection offset in px IE 214 + // get the current caret selection, can't use rep. here because that only gives + // us the start position not the current + const myselection = document.getSelection(); + // get the carets selection offset in px IE 214 + let caretOffsetTop = myselection.focusNode.parentNode.offsetTop || + myselection.focusNode.offsetTop; - // sometimes the first selection is -1 which causes problems (Especially with ep_page_view) + // sometimes the first selection is -1 which causes problems + // (Especially with ep_page_view) // so use focusNode.offsetTop value. if (caretOffsetTop === -1) caretOffsetTop = myselection.focusNode.offsetTop; - scroll.setScrollY(caretOffsetTop); // set the scrollY offset of the viewport on the document + // set the scrollY offset of the viewport on the document + scroll.setScrollY(caretOffsetTop); }, 200); } // scroll to viewport when user presses arrow keys and caret is out of the viewport - if ((evt.which == 37 || evt.which == 38 || evt.which == 39 || evt.which == 40)) { - // we use arrowKeyWasReleased to avoid triggering the animation when a key is continuously pressed + if ((evt.which === 37 || evt.which === 38 || evt.which === 39 || evt.which === 40)) { + // we use arrowKeyWasReleased to avoid triggering the animation when a key + // is continuously pressed // this makes the scroll smooth if (!continuouslyPressingArrowKey(type)) { - // We use getSelection() instead of rep to get the caret position. This avoids errors like when - // the caret position is not synchronized with the rep. For example, when an user presses arrow - // down to scroll the pad without releasing the key. When the key is released the rep is not + // the caret position is not synchronized with the rep. + // For example, when an user presses arrow + // We use getSelection() instead of rep to get the caret position. + // This avoids errors like when down to scroll the pad without releasing the key. + // When the key is released the rep is not // synchronized, so we don't get the right node where caret is. const selection = getSelection(); @@ -3116,25 +3221,28 @@ function Ace2Inner() { } } - if (type == 'keydown') { + if (type === 'keydown') { idleWorkTimer.atLeast(500); - } else if (type == 'keypress') { - if ((!specialHandled) && false /* parenModule.shouldNormalizeOnChar(charCode)*/) { + } else if (type === 'keypress') { + // OPINION ASKED. What's going on here? :D + if (!specialHandled) { idleWorkTimer.atMost(0); } else { idleWorkTimer.atLeast(500); } - } else if (type == 'keyup') { + } else if (type === 'keyup') { const wait = 0; idleWorkTimer.atLeast(wait); idleWorkTimer.atMost(wait); } // Is part of multi-keystroke international character on Firefox Mac - const isFirefoxHalfCharacter = (browser.firefox && evt.altKey && charCode === 0 && keyCode === 0); + const isFirefoxHalfCharacter = + (browser.firefox && evt.altKey && charCode === 0 && keyCode === 0); // Is part of multi-keystroke international character on Safari Mac - const isSafariHalfCharacter = (browser.safari && evt.altKey && keyCode == 229); + const isSafariHalfCharacter = + (browser.safari && evt.altKey && keyCode === 229); if (thisKeyDoesntTriggerNormalize || isFirefoxHalfCharacter || isSafariHalfCharacter) { idleWorkTimer.atLeast(3000); // give user time to type @@ -3143,37 +3251,38 @@ function Ace2Inner() { } if ((!specialHandled) && (!thisKeyDoesntTriggerNormalize) && (!inInternationalComposition)) { - if (type != 'keyup') { + if (type !== 'keyup') { observeChangesAroundSelection(); } } - if (type == 'keyup') { + if (type === 'keyup') { thisKeyDoesntTriggerNormalize = false; } }); - } - - var thisKeyDoesntTriggerNormalize = false; + }; + let thisKeyDoesntTriggerNormalize = false; let arrowKeyWasReleased = true; - function continuouslyPressingArrowKey(type) { + const continuouslyPressingArrowKey = (type) => { let firstTimeKeyIsContinuouslyPressed = false; - if (type == 'keyup') { arrowKeyWasReleased = true; } else if (type == 'keydown' && arrowKeyWasReleased) { + if (type === 'keyup') { + arrowKeyWasReleased = true; + } else if (type === 'keydown' && arrowKeyWasReleased) { firstTimeKeyIsContinuouslyPressed = true; arrowKeyWasReleased = false; } return !firstTimeKeyIsContinuouslyPressed; - } + }; - function doUndoRedo(which) { + const doUndoRedo = (which) => { // precond: normalized DOM if (undoModule.enabled) { let whichMethod; - if (which == 'undo') whichMethod = 'performUndo'; - if (which == 'redo') whichMethod = 'performRedo'; + if (which === 'undo') whichMethod = 'performUndo'; + if (which === 'redo') whichMethod = 'performRedo'; if (whichMethod) { const oldEventType = currentCallStack.editEvent.eventType; currentCallStack.startNewEvent(which); @@ -3182,157 +3291,62 @@ function Ace2Inner() { performDocumentApplyChangeset(backset); } if (selectionInfo) { - performSelectionChange(lineAndColumnFromChar(selectionInfo.selStart), lineAndColumnFromChar(selectionInfo.selEnd), selectionInfo.selFocusAtStart); + performSelectionChange( + lineAndColumnFromChar( + selectionInfo.selStart + ), + lineAndColumnFromChar(selectionInfo.selEnd), + selectionInfo.selFocusAtStart + ); } const oldEvent = currentCallStack.startNewEvent(oldEventType, true); return oldEvent; }); } } - } + }; editorInfo.ace_doUndoRedo = doUndoRedo; - function updateBrowserSelectionFromRep() { - // requires normalized DOM! - const selStart = rep.selStart; - const selEnd = rep.selEnd; - - if (!(selStart && selEnd)) { - setSelection(null); - return; - } - - const selection = {}; - - const ss = [selStart[0], selStart[1]]; - selection.startPoint = getPointForLineAndChar(ss); - - const se = [selEnd[0], selEnd[1]]; - selection.endPoint = getPointForLineAndChar(se); - - selection.focusAtStart = !!rep.selFocusAtStart; - setSelection(selection); - } - editorInfo.ace_updateBrowserSelectionFromRep = updateBrowserSelectionFromRep; - - function nodeMaxIndex(nd) { - if (isNodeText(nd)) return nd.nodeValue.length; - else return 1; - } - - function getSelection() { - // returns null, or a structure containing startPoint and endPoint, - // each of which has node (a magicdom node), index, and maxIndex. If the node - // is a text node, maxIndex is the length of the text; else maxIndex is 1. - // index is between 0 and maxIndex, inclusive. - var browserSelection = window.getSelection(); - if (!browserSelection || browserSelection.type === 'None' || - browserSelection.rangeCount === 0) { - return null; - } - const range = browserSelection.getRangeAt(0); - - function isInBody(n) { - while (n && !(n.tagName && n.tagName.toLowerCase() == 'body')) { - n = n.parentNode; - } - return !!n; - } - - function pointFromRangeBound(container, offset) { - if (!isInBody(container)) { - // command-click in Firefox selects whole document, HEAD and BODY! - return { - node: root, - index: 0, - maxIndex: 1, - }; - } - const n = container; - const childCount = n.childNodes.length; - if (isNodeText(n)) { - return { - node: n, - index: offset, - maxIndex: n.nodeValue.length, - }; - } else if (childCount === 0) { - return { - node: n, - index: 0, - maxIndex: 1, - }; - } - // treat point between two nodes as BEFORE the second (rather than after the first) - // if possible; this way point at end of a line block-element is treated as - // at beginning of next line - else if (offset == childCount) { - var nd = n.childNodes.item(childCount - 1); - var max = nodeMaxIndex(nd); - return { - node: nd, - index: max, - maxIndex: max, - }; - } else { - var nd = n.childNodes.item(offset); - var max = nodeMaxIndex(nd); - return { - node: nd, - index: 0, - maxIndex: max, - }; - } - } - var selection = {}; - selection.startPoint = pointFromRangeBound(range.startContainer, range.startOffset); - selection.endPoint = pointFromRangeBound(range.endContainer, range.endOffset); - selection.focusAtStart = (((range.startContainer != range.endContainer) || (range.startOffset != range.endOffset)) && browserSelection.anchorNode && (browserSelection.anchorNode == range.endContainer) && (browserSelection.anchorOffset == range.endOffset)); - - if (selection.startPoint.node.ownerDocument !== window.document) { - return null; - } - - return selection; - } - - function setSelection(selection) { - function copyPoint(pt) { - return { - node: pt.node, - index: pt.index, - maxIndex: pt.maxIndex, - }; - } + const setSelection = (selection) => { + const copyPoint = (pt) => ({ + node: pt.node, + index: pt.index, + maxIndex: pt.maxIndex, + }); let isCollapsed; - function pointToRangeBound(pt) { + const pointToRangeBound = (pt) => { const p = copyPoint(pt); // Make sure Firefox cursor is deep enough; fixes cursor jumping when at top level, // and also problem where cut/copy of a whole line selected with fake arrow-keys // copies the next line too. if (isCollapsed) { - function diveDeep() { + const diveDeep = () => { while (p.node.childNodes.length > 0) { // && (p.node == root || p.node.parentNode == root)) { if (p.index === 0) { p.node = p.node.firstChild; p.maxIndex = nodeMaxIndex(p.node); - } else if (p.index == p.maxIndex) { + } else if (p.index === p.maxIndex) { p.node = p.node.lastChild; p.maxIndex = nodeMaxIndex(p.node); p.index = p.maxIndex; } else { break; } } - } + }; // now fix problem where cursor at end of text node at end of span-like element // with background doesn't seem to show up... - if (isNodeText(p.node) && p.index == p.maxIndex) { + if (isNodeText(p.node) && p.index === p.maxIndex) { let n = p.node; - while ((!n.nextSibling) && (n != root) && (n.parentNode != root)) { + while ((!n.nextSibling) && (n !== root) && (n.parentNode !== root)) { n = n.parentNode; } - if (n.nextSibling && (!((typeof n.nextSibling.tagName) === 'string' && n.nextSibling.tagName.toLowerCase() == 'br')) && (n != p.node) && (n != root) && (n.parentNode != root)) { + if ( + n.nextSibling && + (!((typeof n.nextSibling.tagName) === 'string' && + n.nextSibling.tagName.toLowerCase() === 'br')) && + (n !== p.node) && (n !== root) && (n.parentNode !== root) + ) { // found a parent, go to next node and dive in p.node = n.nextSibling; p.maxIndex = nodeMaxIndex(p.node); @@ -3358,22 +3372,30 @@ function Ace2Inner() { offset: childIndex(p.node) + p.index, }; } - } + }; const browserSelection = window.getSelection(); if (browserSelection) { browserSelection.removeAllRanges(); if (selection) { - isCollapsed = (selection.startPoint.node === selection.endPoint.node && selection.startPoint.index === selection.endPoint.index); + isCollapsed = ( + selection.startPoint.node === selection.endPoint.node && + selection.startPoint.index === selection.endPoint.index + ); const start = pointToRangeBound(selection.startPoint); const end = pointToRangeBound(selection.endPoint); - if ((!isCollapsed) && selection.focusAtStart && browserSelection.collapse && browserSelection.extend) { + if ( + (!isCollapsed) && + selection.focusAtStart && + browserSelection.collapse && + browserSelection.extend + ) { // can handle "backwards"-oriented selection, shift-arrow-keys move start // of selection browserSelection.collapse(end.container, end.offset); browserSelection.extend(start.container, start.offset); } else { - var range = doc.createRange(); + const range = doc.createRange(); range.setStart(start.container, start.offset); range.setEnd(end.container, end.offset); browserSelection.removeAllRanges(); @@ -3381,53 +3403,170 @@ function Ace2Inner() { } } } - } + }; - function childIndex(n) { + const updateBrowserSelectionFromRep = () => { + // requires normalized DOM! + const selStart = rep.selStart; + const selEnd = rep.selEnd; + + if (!(selStart && selEnd)) { + setSelection(null); + return; + } + + const selection = {}; + + const ss = [selStart[0], selStart[1]]; + selection.startPoint = getPointForLineAndChar(ss); + + const se = [selEnd[0], selEnd[1]]; + selection.endPoint = getPointForLineAndChar(se); + + selection.focusAtStart = !!rep.selFocusAtStart; + setSelection(selection); + }; + editorInfo.ace_updateBrowserSelectionFromRep = updateBrowserSelectionFromRep; + editorInfo.ace_focus = focus; + editorInfo.ace_importText = importText; + editorInfo.ace_importAText = importAText; + editorInfo.ace_exportText = exportText; + editorInfo.ace_editorChangedSize = editorChangedSize; + editorInfo.ace_setOnKeyPress = setOnKeyPress; + editorInfo.ace_setOnKeyDown = setOnKeyDown; + editorInfo.ace_setNotifyDirty = setNotifyDirty; + editorInfo.ace_dispose = dispose; + editorInfo.ace_getFormattedCode = getFormattedCode; + editorInfo.ace_setEditable = setEditable; + editorInfo.ace_execCommand = execCommand; + editorInfo.ace_replaceRange = replaceRange; + editorInfo.ace_getAuthorInfos = getAuthorInfos; + editorInfo.ace_performDocumentReplaceRange = performDocumentReplaceRange; + editorInfo.ace_performDocumentReplaceCharRange = performDocumentReplaceCharRange; + editorInfo.ace_setSelection = setSelection; + + const nodeMaxIndex = (nd) => { + if (isNodeText(nd)) return nd.nodeValue.length; + else return 1; + }; + + const getSelection = () => { + // returns null, or a structure containing startPoint and endPoint, + // each of which has node (a magicdom node), index, and maxIndex. If the node + // is a text node, maxIndex is the length of the text; else maxIndex is 1. + // index is between 0 and maxIndex, inclusive. + const browserSelection = window.getSelection(); + if (!browserSelection || browserSelection.type === 'None' || + browserSelection.rangeCount === 0) { + return null; + } + const range = browserSelection.getRangeAt(0); + + const isInBody = (n) => { + while (n && !(n.tagName && n.tagName.toLowerCase() === 'body')) { + n = n.parentNode; + } + return !!n; + }; + + const pointFromRangeBound = (container, offset) => { + if (!isInBody(container)) { + // command-click in Firefox selects whole document, HEAD and BODY! + return { + node: root, + index: 0, + maxIndex: 1, + }; + } + const n = container; + const childCount = n.childNodes.length; + if (isNodeText(n)) { + return { + node: n, + index: offset, + maxIndex: n.nodeValue.length, + }; + } else if (childCount === 0) { + return { + node: n, + index: 0, + maxIndex: 1, + }; + // treat point between two nodes as BEFORE the second (rather than after the first) + // if possible; this way point at end of a line block-element is treated as + // at beginning of next line + } else if (offset === childCount) { + const nd = n.childNodes.item(childCount - 1); + const max = nodeMaxIndex(nd); + return { + node: nd, + index: max, + maxIndex: max, + }; + } else { + const nd = n.childNodes.item(offset); + const max = nodeMaxIndex(nd); + return { + node: nd, + index: 0, + maxIndex: max, + }; + } + }; + const selection = { + startPoint: pointFromRangeBound(range.startContainer, range.startOffset), + endPoint: pointFromRangeBound(range.endContainer, range.endOffset), + focusAtStart: + (range.startContainer !== range.endContainer || range.startOffset !== range.endOffset) && + browserSelection.anchorNode && + browserSelection.anchorNode === range.endContainer && + browserSelection.anchorOffset === range.endOffset, + }; + + if (selection.startPoint.node.ownerDocument !== window.document) { + return null; + } + + return selection; + }; + + const childIndex = (n) => { let idx = 0; while (n.previousSibling) { idx++; n = n.previousSibling; } return idx; - } + }; - function fixView() { + const fixView = () => { // calling this method repeatedly should be fast if (getInnerWidth() === 0 || getInnerHeight() === 0) { return; } - const win = outerWin; - enforceEditability(); $(sideDiv).addClass('sidedivdelayed'); - } + }; const _teardownActions = []; - function teardown() { - _.each(_teardownActions, (a) => { - a(); - }); - } + const teardown = () => _teardownActions.forEach((a) => a()); - var inInternationalComposition = false; - function handleCompositionEvent(evt) { + let inInternationalComposition = false; + const handleCompositionEvent = (evt) => { // international input events, fired in FF3, at least; allow e.g. Japanese input - if (evt.type == 'compositionstart') { + if (evt.type === 'compositionstart') { inInternationalComposition = true; - } else if (evt.type == 'compositionend') { + } else if (evt.type === 'compositionend') { inInternationalComposition = false; } - } - - editorInfo.ace_getInInternationalComposition = function () { - return inInternationalComposition; }; - function bindTheEventHandlers() { + editorInfo.ace_getInInternationalComposition = () => inInternationalComposition; + + const bindTheEventHandlers = () => { $(document).on('keydown', handleKeyEvent); $(document).on('keypress', handleKeyEvent); $(document).on('keyup', handleKeyEvent); @@ -3438,8 +3577,6 @@ function Ace2Inner() { // Will break OL re-numbering: https://github.com/ether/etherpad-lite/pull/2533 // $(document).on("cut", handleCut); - $(root).on('blur', handleBlur); - // If non-nullish, pasting on a link should be suppressed. let suppressPasteOnLink = null; @@ -3517,48 +3654,23 @@ function Ace2Inner() { $(document.documentElement).on('compositionstart', handleCompositionEvent); $(document.documentElement).on('compositionend', handleCompositionEvent); - } + }; - function topLevel(n) { - if ((!n) || n == root) return null; - while (n.parentNode != root) { + const topLevel = (n) => { + if ((!n) || n === root) return null; + while (n.parentNode !== root) { n = n.parentNode; } return n; - } + }; - function getClassArray(elem, optFilter) { - const bodyClasses = []; - (elem.className || '').replace(/\S+/g, (c) => { - if ((!optFilter) || (optFilter(c))) { - bodyClasses.push(c); - } - }); - return bodyClasses; - } - - function setClassArray(elem, array) { - elem.className = array.join(' '); - } - - function focus() { - window.focus(); - } - - function handleBlur(evt) {} - - function getSelectionPointX(point) { + const getSelectionPointX = (point) => { // doesn't work in wrap-mode const node = point.node; const index = point.index; + const leftOf = (n) => n.offsetLeft; + const rightOf = (n) => n.offsetLeft + n.offsetWidth; - function leftOf(n) { - return n.offsetLeft; - } - - function rightOf(n) { - return n.offsetLeft + n.offsetWidth; - } if (!isNodeText(node)) { if (index === 0) return leftOf(node); else return rightOf(node); @@ -3568,7 +3680,10 @@ function Ace2Inner() { let charsToLeft = index; let charsToRight = node.nodeValue.length - index; let n; - for (n = node.previousSibling; n && isNodeText(n); n = n.previousSibling) charsToLeft += n.nodeValue; + for (n = node.previousSibling; n && + isNodeText(n); n = n.previousSibling) { + charsToLeft += n.nodeValue; + } const leftEdge = (n ? rightOf(n) : leftOf(node.parentNode)); for (n = node.nextSibling; n && isNodeText(n); n = n.nextSibling) charsToRight += n.nodeValue; const rightEdge = (n ? leftOf(n) : rightOf(node.parentNode)); @@ -3576,25 +3691,9 @@ function Ace2Inner() { const pixLoc = leftEdge + frac * (rightEdge - leftEdge); return Math.round(pixLoc); } - } + }; - function getPageHeight() { - const win = outerWin; - const odoc = win.document; - if (win.innerHeight && win.scrollMaxY) return win.innerHeight + win.scrollMaxY; - else if (odoc.body.scrollHeight > odoc.body.offsetHeight) return odoc.body.scrollHeight; - else return odoc.body.offsetHeight; - } - - function getPageWidth() { - const win = outerWin; - const odoc = win.document; - if (win.innerWidth && win.scrollMaxX) return win.innerWidth + win.scrollMaxX; - else if (odoc.body.scrollWidth > odoc.body.offsetWidth) return odoc.body.scrollWidth; - else return odoc.body.offsetWidth; - } - - function getInnerHeight() { + const getInnerHeight = () => { const win = outerWin; const odoc = win.document; let h; @@ -3605,17 +3704,16 @@ function Ace2Inner() { // deal with case where iframe is hidden, hope that // style.height of iframe container is set in px return Number(editorInfo.frame.parentNode.style.height.replace(/[^0-9]/g, '') || 0); - } + }; - function getInnerWidth() { + const getInnerWidth = () => { const win = outerWin; const odoc = win.document; return odoc.documentElement.clientWidth; - } + }; - function scrollXHorizontallyIntoView(pixelX) { + const scrollXHorizontallyIntoView = (pixelX) => { const win = outerWin; - const odoc = outerWin.document; const distInsideLeft = pixelX - win.scrollX; const distInsideRight = win.scrollX + getInnerWidth() - pixelX; if (distInsideLeft < 0) { @@ -3623,9 +3721,9 @@ function Ace2Inner() { } else if (distInsideRight < 0) { win.scrollBy(-distInsideRight + 1, 0); } - } + }; - function scrollSelectionIntoView() { + const scrollSelectionIntoView = () => { if (!rep.selStart) return; fixView(); const innerHeight = getInnerHeight(); @@ -3633,134 +3731,57 @@ function Ace2Inner() { if (!doesWrap) { const browserSelection = getSelection(); if (browserSelection) { - const focusPoint = (browserSelection.focusAtStart ? browserSelection.startPoint : browserSelection.endPoint); + const focusPoint = ( + browserSelection.focusAtStart ? browserSelection.startPoint : browserSelection.endPoint + ); const selectionPointX = getSelectionPointX(focusPoint); scrollXHorizontallyIntoView(selectionPointX); fixView(); } } - } + }; const listAttributeName = 'list'; - function getLineListType(lineNum) { - return documentAttributeManager.getAttributeOnLine(lineNum, listAttributeName); - } - - function setLineListType(lineNum, listType) { - if (listType == '') { - documentAttributeManager.removeAttributeOnLine(lineNum, listAttributeName); - documentAttributeManager.removeAttributeOnLine(lineNum, 'start'); - } else { - documentAttributeManager.setAttributeOnLine(lineNum, listAttributeName, listType); - } - - // if the list has been removed, it is necessary to renumber - // starting from the *next* line because the list may have been - // separated. If it returns null, it means that the list was not cut, try - // from the current one. - if (renumberList(lineNum + 1) == null) { - renumberList(lineNum); - } - } - - function renumberList(lineNum) { - // 1-check we are in a list - let type = getLineListType(lineNum); - if (!type) { - return null; - } - type = /([a-z]+)[0-9]+/.exec(type); - if (type[1] == 'indent') { - return null; - } - - // 2-find the first line of the list - while (lineNum - 1 >= 0 && (type = getLineListType(lineNum - 1))) { - type = /([a-z]+)[0-9]+/.exec(type); - if (type[1] == 'indent') break; - lineNum--; - } - - // 3-renumber every list item of the same level from the beginning, level 1 - // IMPORTANT: never skip a level because there imbrication may be arbitrary - const builder = Changeset.builder(rep.lines.totalWidth()); - let loc = [0, 0]; - function applyNumberList(line, level) { - // init - let position = 1; - let curLevel = level; - let listType; - // loop over the lines - while (listType = getLineListType(line)) { - // apply new num - listType = /([a-z]+)([0-9]+)/.exec(listType); - curLevel = Number(listType[2]); - if (isNaN(curLevel) || listType[0] == 'indent') { - return line; - } else if (curLevel == level) { - ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 0])); - ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 1]), [ - ['start', position], - ], rep.apool); - - position++; - line++; - } else if (curLevel < level) { - return line;// back to parent - } else { - line = applyNumberList(line, level + 1);// recursive call - } - } - return line; - } - - applyNumberList(lineNum, 1); - const cs = builder.toString(); - if (!Changeset.isIdentity(cs)) { - performDocumentApplyChangeset(cs); - } - - // 4-apply the modifications - } + const getLineListType = (lineNum) => documentAttributeManager + .getAttributeOnLine(lineNum, listAttributeName); + editorInfo.ace_getLineListType = getLineListType; - function doInsertList(type) { + const doInsertList = (type) => { if (!(rep.selStart && rep.selEnd)) { return; } - let firstLine, lastLine; - firstLine = rep.selStart[0]; - lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0)); + const firstLine = rep.selStart[0]; + const lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0)); let allLinesAreList = true; - for (var n = firstLine; n <= lastLine; n++) { - var listType = getLineListType(n); - if (!listType || listType.slice(0, type.length) != type) { + for (let n = firstLine; n <= lastLine; n++) { + const listType = getLineListType(n); + if (!listType || listType.slice(0, type.length) !== type) { allLinesAreList = false; break; } } const mods = []; - for (var n = firstLine; n <= lastLine; n++) { - var t = ''; + for (let n = firstLine; n <= lastLine; n++) { + // var t = ''; let level = 0; - var listType = /([a-z]+)([0-9]+)/.exec(getLineListType(n)); + let togglingOn = true; + const listType = /([a-z]+)([0-9]+)/.exec(getLineListType(n)); // Used to outdent if ol is removed if (allLinesAreList) { - var togglingOn = false; - } else { - var togglingOn = true; + togglingOn = false; } if (listType) { - t = listType[1]; + // t = listType[1]; level = Number(listType[2]); } - var t = getLineListType(n); + const t = getLineListType(n); if (t === listType) togglingOn = false; @@ -3779,29 +3800,23 @@ function Ace2Inner() { } } - _.each(mods, (mod) => { + mods.forEach((mod) => { setLineListType(mod[0], mod[1]); }); - } + }; - function doInsertUnorderedList() { + const doInsertUnorderedList = () => { doInsertList('bullet'); - } - function doInsertOrderedList() { + }; + const doInsertOrderedList = () => { doInsertList('number'); - } + }; editorInfo.ace_doInsertUnorderedList = doInsertUnorderedList; editorInfo.ace_doInsertOrderedList = doInsertOrderedList; - function initLineNumbers() { - lineNumbersShown = 1; - sideDiv.innerHTML = '
1
'; - sideDivInner = outerWin.document.getElementById('sidedivinner'); - $(sideDiv).addClass('sidediv'); - } // We apply the height of a line in the doc body, to the corresponding sidediv line number - function updateLineNumbers() { + const updateLineNumbers = () => { if (!currentCallStack || currentCallStack && !currentCallStack.domClean) return; // Refs #4228, to avoid layout trashing, we need to first calculate all the heights, @@ -3820,7 +3835,10 @@ function Ace2Inner() { // didn't do this special case, we would miss out on any top margin // included on the first line. The default stylesheet doesn't add // extra margins/padding, but plugins might. - h = docLine.nextSibling.offsetTop - parseInt(window.getComputedStyle(doc.body).getPropertyValue('padding-top').split('px')[0]); + h = docLine.nextSibling.offsetTop - parseInt( + window.getComputedStyle(doc.body) + .getPropertyValue('padding-top').split('px')[0] + ); } else { h = docLine.nextSibling.offsetTop - docLine.offsetTop; } @@ -3840,14 +3858,14 @@ function Ace2Inner() { // Apply height to existing sidediv lines currentLine = 0; while (sidebarLine && currentLine <= lineNumbersShown) { - if (lineHeights[currentLine]) { + if (lineHeights[currentLine] != null) { sidebarLine.style.height = `${lineHeights[currentLine]}px`; } sidebarLine = sidebarLine.nextSibling; currentLine++; } - if (newNumLines != lineNumbersShown) { + if (newNumLines !== lineNumbersShown) { const container = sideDivInner; const odoc = outerWin.document; const fragment = odoc.createDocumentFragment(); @@ -3871,16 +3889,16 @@ function Ace2Inner() { lineNumbersShown--; } } - } + }; // Init documentAttributeManager documentAttributeManager = new AttributeManager(rep, performDocumentApplyChangeset); - editorInfo.ace_performDocumentApplyAttributesToRange = function () { - return documentAttributeManager.setAttributesOnRange.apply(documentAttributeManager, arguments); - }; - this.init = function () { + editorInfo.ace_performDocumentApplyAttributesToRange = + (...args) => documentAttributeManager.setAttributesOnRange(args); + + this.init = () => { $(document).ready(() => { doc = document; // defined as a var in scope outside inCallStack('setup', () => { @@ -3915,13 +3933,11 @@ function Ace2Inner() { scheduler.setTimeout(() => { parent.readyFunc(); // defined in code that sets up the inner iframe }, 0); - - isSetUp = true; }); }; } -exports.init = function () { +exports.init = () => { const editor = new Ace2Inner(); editor.init(); }; diff --git a/src/static/js/caretPosition.js b/src/static/js/caretPosition.js index e59fb4be5..32f6d919b 100644 --- a/src/static/js/caretPosition.js +++ b/src/static/js/caretPosition.js @@ -1,18 +1,20 @@ +'use strict'; + // One rep.line(div) can be broken in more than one line in the browser. // This function is useful to get the caret position of the line as // is represented by the browser -exports.getPosition = function () { +exports.getPosition = () => { let rect, line; - const editor = $('#innerdocbody')[0]; const range = getSelectionRange(); - const isSelectionInsideTheEditor = range && $(range.endContainer).closest('body')[0].id === 'innerdocbody'; + const isSelectionInsideTheEditor = range && + $(range.endContainer).closest('body')[0].id === 'innerdocbody'; if (isSelectionInsideTheEditor) { // when we have the caret in an empty line, e.g. a line with only a
, // getBoundingClientRect() returns all dimensions value as 0 const selectionIsInTheBeginningOfLine = range.endOffset > 0; if (selectionIsInTheBeginningOfLine) { - var clonedRange = createSelectionRange(range); + const clonedRange = createSelectionRange(range); line = getPositionOfElementOrSelection(clonedRange); clonedRange.detach(); } @@ -20,7 +22,7 @@ exports.getPosition = function () { // when there's a
or any element that has no height, we can't get // the dimension of the element where the caret is if (!rect || rect.height === 0) { - var clonedRange = createSelectionRange(range); + const clonedRange = createSelectionRange(range); // as we can't get the element height, we create a text node to get the dimensions // on the position @@ -36,8 +38,8 @@ exports.getPosition = function () { return line; }; -var createSelectionRange = function (range) { - clonedRange = range.cloneRange(); +const createSelectionRange = (range) => { + const clonedRange = range.cloneRange(); // we set the selection start and end to avoid error when user selects a text bigger than // the viewport height and uses the arrow keys to expand the selection. In this particular @@ -48,7 +50,7 @@ var createSelectionRange = function (range) { return clonedRange; }; -const getPositionOfRepLineAtOffset = function (node, offset) { +const getPositionOfRepLineAtOffset = (node, offset) => { // it is not a text node, so we cannot make a selection if (node.tagName === 'BR' || node.tagName === 'EMPTY') { return getPositionOfElementOrSelection(node); @@ -66,7 +68,7 @@ const getPositionOfRepLineAtOffset = function (node, offset) { return linePosition; }; -function getPositionOfElementOrSelection(element) { +const getPositionOfElementOrSelection = (element) => { const rect = element.getBoundingClientRect(); const linePosition = { bottom: rect.bottom, @@ -74,15 +76,15 @@ function getPositionOfElementOrSelection(element) { top: rect.top, }; return linePosition; -} +}; // here we have two possibilities: -// [1] the line before the caret line has the same type, so both of them has the same margin, padding -// height, etc. So, we can use the caret line to make calculation necessary to know where is the top -// of the previous line +// [1] the line before the caret line has the same type, so both of them has the same margin, +// padding height, etc. So, we can use the caret line to make calculation necessary to know +// where is the top of the previous line // [2] the line before is part of another rep line. It's possible this line has different margins // height. So we have to get the exactly position of the line -exports.getPositionTopOfPreviousBrowserLine = function (caretLinePosition, rep) { +exports.getPositionTopOfPreviousBrowserLine = (caretLinePosition, rep) => { let previousLineTop = caretLinePosition.top - caretLinePosition.height; // [1] const isCaretLineFirstBrowserLine = caretLineIsFirstBrowserLine(caretLinePosition.top, rep); @@ -91,13 +93,14 @@ exports.getPositionTopOfPreviousBrowserLine = function (caretLinePosition, rep) if (isCaretLineFirstBrowserLine) { // [2] const lineBeforeCaretLine = rep.selStart[0] - 1; const firstLineVisibleBeforeCaretLine = getPreviousVisibleLine(lineBeforeCaretLine, rep); - const linePosition = getDimensionOfLastBrowserLineOfRepLine(firstLineVisibleBeforeCaretLine, rep); + const linePosition = + getDimensionOfLastBrowserLineOfRepLine(firstLineVisibleBeforeCaretLine, rep); previousLineTop = linePosition.top; } return previousLineTop; }; -function caretLineIsFirstBrowserLine(caretLineTop, rep) { +const caretLineIsFirstBrowserLine = (caretLineTop, rep) => { const caretRepLine = rep.selStart[0]; const lineNode = rep.lines.atIndex(caretRepLine).lineNode; const firstRootNode = getFirstRootChildNode(lineNode); @@ -105,37 +108,28 @@ function caretLineIsFirstBrowserLine(caretLineTop, rep) { // to get the position of the node we get the position of the first char const positionOfFirstRootNode = getPositionOfRepLineAtOffset(firstRootNode, 1); return positionOfFirstRootNode.top === caretLineTop; -} +}; // find the first root node, usually it is a text node -function getFirstRootChildNode(node) { +const getFirstRootChildNode = (node) => { if (!node.firstChild) { return node; } else { return getFirstRootChildNode(node.firstChild); } -} +}; -function getPreviousVisibleLine(line, rep) { - if (line < 0) { - return 0; - } else if (isLineVisible(line, rep)) { - return line; - } else { - return getPreviousVisibleLine(line - 1, rep); - } -} - -function getDimensionOfLastBrowserLineOfRepLine(line, rep) { +const getDimensionOfLastBrowserLineOfRepLine = (line, rep) => { const lineNode = rep.lines.atIndex(line).lineNode; const lastRootChildNode = getLastRootChildNode(lineNode); // we get the position of the line in the last char of it - const lastRootChildNodePosition = getPositionOfRepLineAtOffset(lastRootChildNode.node, lastRootChildNode.length); + const lastRootChildNodePosition = + getPositionOfRepLineAtOffset(lastRootChildNode.node, lastRootChildNode.length); return lastRootChildNodePosition; -} +}; -function getLastRootChildNode(node) { +const getLastRootChildNode = (node) => { if (!node.lastChild) { return { node, @@ -144,39 +138,42 @@ function getLastRootChildNode(node) { } else { return getLastRootChildNode(node.lastChild); } -} +}; // here we have two possibilities: // [1] The next line is part of the same rep line of the caret line, so we have the same dimensions. // So, we can use the caret line to calculate the bottom of the line. -// [2] the next line is part of another rep line. It's possible this line has different dimensions, so we -// have to get the exactly dimension of it -exports.getBottomOfNextBrowserLine = function (caretLinePosition, rep) { +// [2] the next line is part of another rep line. +// It's possible this line has different dimensions, so we have to get the exactly dimension of it +exports.getBottomOfNextBrowserLine = (caretLinePosition, rep) => { let nextLineBottom = caretLinePosition.bottom + caretLinePosition.height; // [1] - const isCaretLineLastBrowserLine = caretLineIsLastBrowserLineOfRepLine(caretLinePosition.top, rep); + const isCaretLineLastBrowserLine = + caretLineIsLastBrowserLineOfRepLine(caretLinePosition.top, rep); // the caret is at the end of a rep line, so we can get the next browser line dimension // using the position of the first char of the next rep line if (isCaretLineLastBrowserLine) { // [2] const nextLineAfterCaretLine = rep.selStart[0] + 1; const firstNextLineVisibleAfterCaretLine = getNextVisibleLine(nextLineAfterCaretLine, rep); - const linePosition = getDimensionOfFirstBrowserLineOfRepLine(firstNextLineVisibleAfterCaretLine, rep); + const linePosition = + getDimensionOfFirstBrowserLineOfRepLine(firstNextLineVisibleAfterCaretLine, rep); nextLineBottom = linePosition.bottom; } return nextLineBottom; }; -function caretLineIsLastBrowserLineOfRepLine(caretLineTop, rep) { +const caretLineIsLastBrowserLineOfRepLine = (caretLineTop, rep) => { const caretRepLine = rep.selStart[0]; const lineNode = rep.lines.atIndex(caretRepLine).lineNode; const lastRootChildNode = getLastRootChildNode(lineNode); // we take a rep line and get the position of the last char of it - const lastRootChildNodePosition = getPositionOfRepLineAtOffset(lastRootChildNode.node, lastRootChildNode.length); + const lastRootChildNodePosition = + getPositionOfRepLineAtOffset(lastRootChildNode.node, lastRootChildNode.length); return lastRootChildNodePosition.top === caretLineTop; -} +}; -function getPreviousVisibleLine(line, rep) { +const getPreviousVisibleLine = (line, rep) => { const firstLineOfPad = 0; if (line <= firstLineOfPad) { return firstLineOfPad; @@ -185,10 +182,12 @@ function getPreviousVisibleLine(line, rep) { } else { return getPreviousVisibleLine(line - 1, rep); } -} +}; + + exports.getPreviousVisibleLine = getPreviousVisibleLine; -function getNextVisibleLine(line, rep) { +const getNextVisibleLine = (line, rep) => { const lastLineOfThePad = rep.lines.length() - 1; if (line >= lastLineOfThePad) { return lastLineOfThePad; @@ -197,31 +196,28 @@ function getNextVisibleLine(line, rep) { } else { return getNextVisibleLine(line + 1, rep); } -} +}; exports.getNextVisibleLine = getNextVisibleLine; -function isLineVisible(line, rep) { - return rep.lines.atIndex(line).lineNode.offsetHeight > 0; -} +const isLineVisible = (line, rep) => rep.lines.atIndex(line).lineNode.offsetHeight > 0; -function getDimensionOfFirstBrowserLineOfRepLine(line, rep) { +const getDimensionOfFirstBrowserLineOfRepLine = (line, rep) => { const lineNode = rep.lines.atIndex(line).lineNode; const firstRootChildNode = getFirstRootChildNode(lineNode); // we can get the position of the line, getting the position of the first char of the rep line const firstRootChildNodePosition = getPositionOfRepLineAtOffset(firstRootChildNode, 1); return firstRootChildNodePosition; -} +}; -function getSelectionRange() { - let selection; +const getSelectionRange = () => { if (!window.getSelection) { return; } - selection = window.getSelection(); + const selection = window.getSelection(); if (selection.rangeCount > 0) { return selection.getRangeAt(0); } else { return null; } -} +}; diff --git a/src/static/js/colorutils.js b/src/static/js/colorutils.js index 6feba3a75..9688b8e59 100644 --- a/src/static/js/colorutils.js +++ b/src/static/js/colorutils.js @@ -1,3 +1,5 @@ +'use strict'; + /** * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. @@ -26,24 +28,24 @@ const colorutils = {}; // Check that a given value is a css hex color value, e.g. // "#ffffff" or "#fff" -colorutils.isCssHex = function (cssColor) { - return /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(cssColor); -}; +colorutils.isCssHex = (cssColor) => /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(cssColor); // "#ffffff" or "#fff" or "ffffff" or "fff" to [1.0, 1.0, 1.0] -colorutils.css2triple = function (cssColor) { +colorutils.css2triple = (cssColor) => { const sixHex = colorutils.css2sixhex(cssColor); - function hexToFloat(hh) { - return Number(`0x${hh}`) / 255; - } - return [hexToFloat(sixHex.substr(0, 2)), hexToFloat(sixHex.substr(2, 2)), hexToFloat(sixHex.substr(4, 2))]; + const hexToFloat = (hh) => Number(`0x${hh}`) / 255; + return [ + hexToFloat(sixHex.substr(0, 2)), + hexToFloat(sixHex.substr(2, 2)), + hexToFloat(sixHex.substr(4, 2)), + ]; }; // "#ffffff" or "#fff" or "ffffff" or "fff" to "ffffff" -colorutils.css2sixhex = function (cssColor) { +colorutils.css2sixhex = (cssColor) => { let h = /[0-9a-fA-F]+/.exec(cssColor)[0]; - if (h.length != 6) { + if (h.length !== 6) { const a = h.charAt(0); const b = h.charAt(1); const c = h.charAt(2); @@ -53,66 +55,54 @@ colorutils.css2sixhex = function (cssColor) { }; // [1.0, 1.0, 1.0] -> "#ffffff" -colorutils.triple2css = function (triple) { - function floatToHex(n) { +colorutils.triple2css = (triple) => { + const floatToHex = (n) => { const n2 = colorutils.clamp(Math.round(n * 255), 0, 255); return (`0${n2.toString(16)}`).slice(-2); - } + }; return `#${floatToHex(triple[0])}${floatToHex(triple[1])}${floatToHex(triple[2])}`; }; -colorutils.clamp = function (v, bot, top) { - return v < bot ? bot : (v > top ? top : v); -}; -colorutils.min3 = function (a, b, c) { - return (a < b) ? (a < c ? a : c) : (b < c ? b : c); -}; -colorutils.max3 = function (a, b, c) { - return (a > b) ? (a > c ? a : c) : (b > c ? b : c); -}; -colorutils.colorMin = function (c) { - return colorutils.min3(c[0], c[1], c[2]); -}; -colorutils.colorMax = function (c) { - return colorutils.max3(c[0], c[1], c[2]); -}; -colorutils.scale = function (v, bot, top) { - return colorutils.clamp(bot + v * (top - bot), 0, 1); -}; -colorutils.unscale = function (v, bot, top) { - return colorutils.clamp((v - bot) / (top - bot), 0, 1); -}; +colorutils.clamp = (v, bot, top) => v < bot ? bot : (v > top ? top : v); +colorutils.min3 = (a, b, c) => (a < b) ? (a < c ? a : c) : (b < c ? b : c); +colorutils.max3 = (a, b, c) => (a > b) ? (a > c ? a : c) : (b > c ? b : c); +colorutils.colorMin = (c) => colorutils.min3(c[0], c[1], c[2]); +colorutils.colorMax = (c) => colorutils.max3(c[0], c[1], c[2]); +colorutils.scale = (v, bot, top) => colorutils.clamp(bot + v * (top - bot), 0, 1); +colorutils.unscale = (v, bot, top) => colorutils.clamp((v - bot) / (top - bot), 0, 1); -colorutils.scaleColor = function (c, bot, top) { - return [colorutils.scale(c[0], bot, top), colorutils.scale(c[1], bot, top), colorutils.scale(c[2], bot, top)]; -}; +colorutils.scaleColor = (c, bot, top) => [ + colorutils.scale(c[0], bot, top), + colorutils.scale(c[1], bot, top), + colorutils.scale(c[2], bot, top), +]; -colorutils.unscaleColor = function (c, bot, top) { - return [colorutils.unscale(c[0], bot, top), colorutils.unscale(c[1], bot, top), colorutils.unscale(c[2], bot, top)]; -}; +colorutils.unscaleColor = (c, bot, top) => [ + colorutils.unscale(c[0], bot, top), + colorutils.unscale(c[1], bot, top), + colorutils.unscale(c[2], bot, top), +]; -colorutils.luminosity = function (c) { - // rule of thumb for RGB brightness; 1.0 is white - return c[0] * 0.30 + c[1] * 0.59 + c[2] * 0.11; -}; +// rule of thumb for RGB brightness; 1.0 is white +colorutils.luminosity = (c) => c[0] * 0.30 + c[1] * 0.59 + c[2] * 0.11; -colorutils.saturate = function (c) { +colorutils.saturate = (c) => { const min = colorutils.colorMin(c); const max = colorutils.colorMax(c); if (max - min <= 0) return [1.0, 1.0, 1.0]; return colorutils.unscaleColor(c, min, max); }; -colorutils.blend = function (c1, c2, t) { - return [colorutils.scale(t, c1[0], c2[0]), colorutils.scale(t, c1[1], c2[1]), colorutils.scale(t, c1[2], c2[2])]; -}; +colorutils.blend = (c1, c2, t) => [ + colorutils.scale(t, c1[0], c2[0]), + colorutils.scale(t, c1[1], c2[1]), + colorutils.scale(t, c1[2], c2[2]), +]; -colorutils.invert = function (c) { - return [1 - c[0], 1 - c[1], 1 - c[2]]; -}; +colorutils.invert = (c) => [1 - c[0], 1 - c[1], 1 - c[2]]; -colorutils.complementary = function (c) { +colorutils.complementary = (c) => { const inv = colorutils.invert(c); return [ (inv[0] >= c[0]) ? Math.min(inv[0] * 1.30, 1) : (c[0] * 0.30), @@ -121,9 +111,9 @@ colorutils.complementary = function (c) { ]; }; -colorutils.textColorFromBackgroundColor = function (bgcolor, skinName) { - const white = skinName == 'colibris' ? 'var(--super-light-color)' : '#fff'; - const black = skinName == 'colibris' ? 'var(--super-dark-color)' : '#222'; +colorutils.textColorFromBackgroundColor = (bgcolor, skinName) => { + const white = skinName === 'colibris' ? 'var(--super-light-color)' : '#fff'; + const black = skinName === 'colibris' ? 'var(--super-dark-color)' : '#222'; return colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.5 ? white : black; }; diff --git a/src/static/js/contentcollector.js b/src/static/js/contentcollector.js index 7177732db..7ac27168f 100644 --- a/src/static/js/contentcollector.js +++ b/src/static/js/contentcollector.js @@ -1,3 +1,4 @@ +'use strict'; /** * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. @@ -28,49 +29,34 @@ const _MAX_LIST_LEVEL = 16; const UNorm = require('unorm'); const Changeset = require('./Changeset'); const hooks = require('./pluginfw/hooks'); -const _ = require('./underscore'); -function sanitizeUnicode(s) { - return UNorm.nfc(s); -} +const sanitizeUnicode = (s) => UNorm.nfc(s); -function makeContentCollector(collectStyles, abrowser, apool, domInterface, className2Author) { - abrowser = abrowser || {}; - // I don't like the above. +// This file is used both in browsers and with cheerio in Node.js (for importing HTML). Cheerio's +// Node-like objects are not 100% API compatible with the DOM specification; the following functions +// abstract away the differences. - const dom = domInterface || { - isNodeText(n) { - return (n.nodeType == 3); - }, - nodeTagName(n) { - return n.tagName; - }, - nodeValue(n) { - return n.nodeValue; - }, - nodeNumChildren(n) { - if (n.childNodes == null) return 0; - return n.childNodes.length; - }, - nodeChild(n, i) { - if (n.childNodes.item == null) { - return n.childNodes[i]; - } - return n.childNodes.item(i); - }, - nodeProp(n, p) { - return n[p]; - }, - nodeAttr(n, a) { - if (n.getAttribute != null) return n.getAttribute(a); - if (n.attribs != null) return n.attribs[a]; - return null; - }, - optNodeInnerHTML(n) { - return n.innerHTML; - }, - }; +// .nodeType works with DOM and cheerio 0.22.0, but cheerio 0.22.0 does not provide the Node.*_NODE +// constants so they cannot be used here. +const isElementNode = (n) => n.nodeType === 1; // Node.ELEMENT_NODE +const isTextNode = (n) => n.nodeType === 3; // Node.TEXT_NODE +// .tagName works with DOM and cheerio 0.22.0, but: +// * With DOM, .tagName is an uppercase string. +// * With cheerio 0.22.0, .tagName is a lowercase string. +// For consistency, this function always returns a lowercase string. +const tagName = (n) => n.tagName && n.tagName.toLowerCase(); +// .childNodes works with DOM and cheerio 0.22.0, except in cheerio the .childNodes property does +// not exist on text nodes (and maybe other non-element nodes). +const childNodes = (n) => n.childNodes || []; +const getAttribute = (n, a) => { + // .getAttribute() works with DOM but not with cheerio 0.22.0. + if (n.getAttribute != null) return n.getAttribute(a); + // .attribs[] works with cheerio 0.22.0 but not with DOM. + if (n.attribs != null) return n.attribs[a]; + return null; +}; +const makeContentCollector = (collectStyles, abrowser, apool, className2Author) => { const _blockElems = { div: 1, p: 1, @@ -78,58 +64,45 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas li: 1, }; - _.each(hooks.callAll('ccRegisterBlockElements'), (element) => { + hooks.callAll('ccRegisterBlockElements').forEach((element) => { _blockElems[element] = 1; }); - function isBlockElement(n) { - return !!_blockElems[(dom.nodeTagName(n) || '').toLowerCase()]; - } + const isBlockElement = (n) => !!_blockElems[tagName(n) || '']; - function textify(str) { - return sanitizeUnicode( - str.replace(/(\n | \n)/g, ' ').replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, ' ')); - } + const textify = (str) => sanitizeUnicode( + str.replace(/(\n | \n)/g, ' ') + .replace(/[\n\r ]/g, ' ') + .replace(/\xa0/g, ' ') + .replace(/\t/g, ' ')); - function getAssoc(node, name) { - return dom.nodeProp(node, `_magicdom_${name}`); - } + const getAssoc = (node, name) => node[`_magicdom_${name}`]; - const lines = (function () { + const lines = (() => { const textArray = []; const attribsArray = []; let attribsBuilder = null; const op = Changeset.newOp('+'); - var self = { - length() { - return textArray.length; - }, - atColumnZero() { - return textArray[textArray.length - 1] === ''; - }, - startNew() { + const self = { + length: () => textArray.length, + atColumnZero: () => textArray[textArray.length - 1] === '', + startNew: () => { textArray.push(''); self.flush(true); attribsBuilder = Changeset.smartOpAssembler(); }, - textOfLine(i) { - return textArray[i]; - }, - appendText(txt, attrString) { + textOfLine: (i) => textArray[i], + appendText: (txt, attrString) => { textArray[textArray.length - 1] += txt; // dmesg(txt+" / "+attrString); op.attribs = attrString; op.chars = txt.length; attribsBuilder.append(op); }, - textLines() { - return textArray.slice(); - }, - attribLines() { - return attribsArray; - }, + textLines: () => textArray.slice(), + attribLines: () => attribsArray, // call flush only when you're done - flush(withNewline) { + flush: (withNewline) => { if (attribsBuilder) { attribsArray.push(attribsBuilder.toString()); attribsBuilder = null; @@ -138,60 +111,67 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas }; self.startNew(); return self; - }()); + })(); const cc = {}; - function _ensureColumnZero(state) { + const _ensureColumnZero = (state) => { if (!lines.atColumnZero()) { cc.startNewLine(state); } - } + }; let selection, startPoint, endPoint; let selStart = [-1, -1]; let selEnd = [-1, -1]; - function _isEmpty(node, state) { + const _isEmpty = (node, state) => { // consider clean blank lines pasted in IE to be empty - if (dom.nodeNumChildren(node) == 0) return true; - if (dom.nodeNumChildren(node) == 1 && getAssoc(node, 'shouldBeEmpty') && dom.optNodeInnerHTML(node) == ' ' && !getAssoc(node, 'unpasted')) { + if (childNodes(node).length === 0) return true; + if (childNodes(node).length === 1 && + getAssoc(node, 'shouldBeEmpty') && + // Note: The .innerHTML property exists on DOM Element objects but not on cheerio's + // Element-like objects (cheerio v0.22.0) so this equality check will always be false. + // Cheerio's Element-like objects have no equivalent to .innerHTML. (Cheerio objects have an + // .html() method, but that isn't accessible here.) + node.innerHTML === ' ' && + !getAssoc(node, 'unpasted')) { if (state) { - const child = dom.nodeChild(node, 0); + const child = childNodes(node)[0]; _reachPoint(child, 0, state); _reachPoint(child, 1, state); } return true; } return false; - } + }; - function _pointHere(charsAfter, state) { + const _pointHere = (charsAfter, state) => { const ln = lines.length() - 1; let chr = lines.textOfLine(ln).length; - if (chr == 0 && !_.isEmpty(state.lineAttributes)) { + if (chr === 0 && Object.keys(state.lineAttributes).length !== 0) { chr += 1; // listMarker } chr += charsAfter; return [ln, chr]; - } + }; - function _reachBlockPoint(nd, idx, state) { - if (!dom.isNodeText(nd)) _reachPoint(nd, idx, state); - } + const _reachBlockPoint = (nd, idx, state) => { + if (!isTextNode(nd)) _reachPoint(nd, idx, state); + }; - function _reachPoint(nd, idx, state) { - if (startPoint && nd == startPoint.node && startPoint.index == idx) { + const _reachPoint = (nd, idx, state) => { + if (startPoint && nd === startPoint.node && startPoint.index === idx) { selStart = _pointHere(0, state); } - if (endPoint && nd == endPoint.node && endPoint.index == idx) { + if (endPoint && nd === endPoint.node && endPoint.index === idx) { selEnd = _pointHere(0, state); } - } - cc.incrementFlag = function (state, flagName) { + }; + cc.incrementFlag = (state, flagName) => { state.flags[flagName] = (state.flags[flagName] || 0) + 1; }; - cc.decrementFlag = function (state, flagName) { + cc.decrementFlag = (state, flagName) => { state.flags[flagName]--; }; - cc.incrementAttrib = function (state, attribName) { + cc.incrementAttrib = (state, attribName) => { if (!state.attribs[attribName]) { state.attribs[attribName] = 1; } else { @@ -199,15 +179,15 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas } _recalcAttribString(state); }; - cc.decrementAttrib = function (state, attribName) { + cc.decrementAttrib = (state, attribName) => { state.attribs[attribName]--; _recalcAttribString(state); }; - function _enterList(state, listType) { + const _enterList = (state, listType) => { if (!listType) return; const oldListType = state.lineAttributes.list; - if (listType != 'none') { + if (listType !== 'none') { state.listNesting = (state.listNesting || 0) + 1; // reminder that listType can be "number2", "number3" etc. if (listType.indexOf('number') !== -1) { @@ -222,56 +202,55 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas } _recalcAttribString(state); return oldListType; - } + }; - function _exitList(state, oldListType) { + const _exitList = (state, oldListType) => { if (state.lineAttributes.list) { state.listNesting--; } - if (oldListType && oldListType != 'none') { + if (oldListType && oldListType !== 'none') { state.lineAttributes.list = oldListType; } else { delete state.lineAttributes.list; delete state.lineAttributes.start; } _recalcAttribString(state); - } + }; - function _enterAuthor(state, author) { + const _enterAuthor = (state, author) => { const oldAuthor = state.author; state.authorLevel = (state.authorLevel || 0) + 1; state.author = author; _recalcAttribString(state); return oldAuthor; - } + }; - function _exitAuthor(state, oldAuthor) { + const _exitAuthor = (state, oldAuthor) => { state.authorLevel--; state.author = oldAuthor; _recalcAttribString(state); - } + }; - function _recalcAttribString(state) { + const _recalcAttribString = (state) => { const lst = []; - for (const a in state.attribs) { - if (state.attribs[a]) { - // The following splitting of the attribute name is a workaround - // to enable the content collector to store key-value attributes - // see https://github.com/ether/etherpad-lite/issues/2567 for more information - // in long term the contentcollector should be refactored to get rid of this workaround - const ATTRIBUTE_SPLIT_STRING = '::'; + for (const [a, count] of Object.entries(state.attribs)) { + if (!count) continue; + // The following splitting of the attribute name is a workaround + // to enable the content collector to store key-value attributes + // see https://github.com/ether/etherpad-lite/issues/2567 for more information + // in long term the contentcollector should be refactored to get rid of this workaround + const ATTRIBUTE_SPLIT_STRING = '::'; - // see if attributeString is splittable - const attributeSplits = a.split(ATTRIBUTE_SPLIT_STRING); - if (attributeSplits.length > 1) { - // the attribute name follows the convention key::value - // so save it as a key value attribute - lst.push([attributeSplits[0], attributeSplits[1]]); - } else { - // the "normal" case, the attribute is just a switch - // so set it true - lst.push([a, 'true']); - } + // see if attributeString is splittable + const attributeSplits = a.split(ATTRIBUTE_SPLIT_STRING); + if (attributeSplits.length > 1) { + // the attribute name follows the convention key::value + // so save it as a key value attribute + lst.push([attributeSplits[0], attributeSplits[1]]); + } else { + // the "normal" case, the attribute is just a switch + // so set it true + lst.push([a, 'true']); } } if (state.authorLevel > 0) { @@ -283,35 +262,34 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas } } state.attribString = Changeset.makeAttribsString('+', lst, apool); - } + }; - function _produceLineAttributesMarker(state) { + const _produceLineAttributesMarker = (state) => { // TODO: This has to go to AttributeManager. const attributes = [ ['lmkr', '1'], ['insertorder', 'first'], - ].concat( - _.map(state.lineAttributes, (value, key) => [key, value]) - ); + ...Object.entries(state.lineAttributes), + ]; lines.appendText('*', Changeset.makeAttribsString('+', attributes, apool)); - } - cc.startNewLine = function (state) { + }; + cc.startNewLine = (state) => { if (state) { - const atBeginningOfLine = lines.textOfLine(lines.length() - 1).length == 0; - if (atBeginningOfLine && !_.isEmpty(state.lineAttributes)) { + const atBeginningOfLine = lines.textOfLine(lines.length() - 1).length === 0; + if (atBeginningOfLine && Object.keys(state.lineAttributes).length !== 0) { _produceLineAttributesMarker(state); } } lines.startNew(); }; - cc.notifySelection = function (sel) { + cc.notifySelection = (sel) => { if (sel) { selection = sel; startPoint = selection.startPoint; endPoint = selection.endPoint; } }; - cc.doAttrib = function (state, na) { + cc.doAttrib = (state, na) => { state.localAttribs = (state.localAttribs || []); state.localAttribs.push(na); cc.incrementAttrib(state, na); @@ -341,33 +319,24 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas if (isBlock) _ensureColumnZero(state); const startLine = lines.length() - 1; _reachBlockPoint(node, 0, state); - if (dom.isNodeText(node)) { - let txt = dom.nodeValue(node); - var tname = dom.nodeAttr(node.parentNode, 'name'); - const txtFromHook = hooks.callAll('collectContentLineText', { - cc: this, - state, - tname, - node, - text: txt, - styl: null, - cls: null, - }); - - if (typeof (txtFromHook) === 'object') { - txt = dom.nodeValue(node); - } else if (txtFromHook) { - txt = txtFromHook; - } + if (isTextNode(node)) { + const tname = getAttribute(node.parentNode, 'name'); + const context = {cc: this, state, tname, node, text: node.nodeValue}; + // Hook functions may either return a string (deprecated) or modify context.text. If any hook + // function modifies context.text then all returned strings are ignored. If no hook functions + // modify context.text, the first hook function to return a string wins. + const [hookTxt] = + hooks.callAll('collectContentLineText', context).filter((s) => typeof s === 'string'); + let txt = context.text === node.nodeValue && hookTxt != null ? hookTxt : context.text; let rest = ''; let x = 0; // offset into original text - if (txt.length == 0) { - if (startPoint && node == startPoint.node) { + if (txt.length === 0) { + if (startPoint && node === startPoint.node) { selStart = _pointHere(0, state); } - if (endPoint && node == endPoint.node) { + if (endPoint && node === endPoint.node) { selEnd = _pointHere(0, state); } } @@ -380,10 +349,10 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas txt = firstLine; } else { /* will only run this loop body once */ } - if (startPoint && node == startPoint.node && startPoint.index - x <= txt.length) { + if (startPoint && node === startPoint.node && startPoint.index - x <= txt.length) { selStart = _pointHere(startPoint.index - x, state); } - if (endPoint && node == endPoint.node && endPoint.index - x <= txt.length) { + if (endPoint && node === endPoint.node && endPoint.index - x <= txt.length) { selEnd = _pointHere(endPoint.index - x, state); } let txt2 = txt; @@ -394,12 +363,12 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas // removing "\n" from pasted HTML will collapse words together. txt2 = ''; } - const atBeginningOfLine = lines.textOfLine(lines.length() - 1).length == 0; + const atBeginningOfLine = lines.textOfLine(lines.length() - 1).length === 0; if (atBeginningOfLine) { // newlines in the source mustn't become spaces at beginning of line box txt2 = txt2.replace(/^\n*/, ''); } - if (atBeginningOfLine && !_.isEmpty(state.lineAttributes)) { + if (atBeginningOfLine && Object.keys(state.lineAttributes).length !== 0) { _produceLineAttributesMarker(state); } lines.appendText(textify(txt2), state.attribString); @@ -409,16 +378,16 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas cc.startNewLine(state); } } - } else { - var tname = (dom.nodeTagName(node) || '').toLowerCase(); + } else if (isElementNode(node)) { + const tname = tagName(node) || ''; - if (tname == 'img') { - const collectContentImage = hooks.callAll('collectContentImage', { + if (tname === 'img') { + hooks.callAll('collectContentImage', { cc, state, tname, - styl, - cls, + styl: null, + cls: null, node, }); } else { @@ -426,10 +395,10 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas delete state.lineAttributes.img; } - if (tname == 'br') { + if (tname === 'br') { this.breakLine = true; - const tvalue = dom.nodeAttr(node, 'value'); - const induceLineBreak = hooks.callAll('collectContentLineBreak', { + const tvalue = getAttribute(node, 'value'); + const [startNewLine = true] = hooks.callAll('collectContentLineBreak', { cc: this, state, tname, @@ -437,17 +406,16 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas styl: null, cls: null, }); - const startNewLine = (typeof (induceLineBreak) === 'object' && induceLineBreak.length == 0) ? true : induceLineBreak[0]; if (startNewLine) { cc.startNewLine(state); } - } else if (tname == 'script' || tname == 'style') { + } else if (tname === 'script' || tname === 'style') { // ignore } else if (!isEmpty) { - var styl = dom.nodeAttr(node, 'style'); - var cls = dom.nodeAttr(node, 'class'); - let isPre = (tname == 'pre'); - if ((!isPre) && abrowser.safari) { + let styl = getAttribute(node, 'style'); + let cls = getAttribute(node, 'class'); + let isPre = (tname === 'pre'); + if ((!isPre) && abrowser && abrowser.safari) { isPre = (styl && /\bwhite-space:\s*pre\b/i.exec(styl)); } if (isPre) cc.incrementFlag(state, 'preMode'); @@ -459,10 +427,10 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas styl = null; cls = null; - // We have to return here but this could break things in the future, for now it shows how to fix the problem + // We have to return here but this could break things in the future, + // for now it shows how to fix the problem return; } - if (collectStyles) { hooks.callAll('collectContentPre', { cc, @@ -471,41 +439,44 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas styl, cls, }); - if (tname == 'b' || (styl && /\bfont-weight:\s*bold\b/i.exec(styl)) || tname == 'strong') { + if (tname === 'b' || + (styl && /\bfont-weight:\s*bold\b/i.exec(styl)) || + tname === 'strong') { cc.doAttrib(state, 'bold'); } - if (tname == 'i' || (styl && /\bfont-style:\s*italic\b/i.exec(styl)) || tname == 'em') { + if (tname === 'i' || + (styl && /\bfont-style:\s*italic\b/i.exec(styl)) || + tname === 'em') { cc.doAttrib(state, 'italic'); } - if (tname == 'u' || (styl && /\btext-decoration:\s*underline\b/i.exec(styl)) || tname == 'ins') { + if (tname === 'u' || + (styl && /\btext-decoration:\s*underline\b/i.exec(styl)) || + tname === 'ins') { cc.doAttrib(state, 'underline'); } - if (tname == 's' || (styl && /\btext-decoration:\s*line-through\b/i.exec(styl)) || tname == 'del') { + if (tname === 's' || + (styl && /\btext-decoration:\s*line-through\b/i.exec(styl)) || + tname === 'del') { cc.doAttrib(state, 'strikethrough'); } - if (tname == 'ul' || tname == 'ol') { - if (node.attribs) { - var type = node.attribs.class; - } else { - var type = null; - } + if (tname === 'ul' || tname === 'ol') { + let type = getAttribute(node, 'class'); const rr = cls && /(?:^| )list-([a-z]+[0-9]+)\b/.exec(cls); - // lists do not need to have a type, so before we make a wrong guess, check if we find a better hint within the node's children + // lists do not need to have a type, so before we make a wrong guess + // check if we find a better hint within the node's children if (!rr && !type) { - for (var i in node.children) { - if (node.children[i] && node.children[i].name == 'ul') { - type = node.children[i].attribs.class; - if (type) { - break; - } - } + for (const child of childNodes(node)) { + if (tagName(child) !== 'ul') continue; + type = getAttribute(child, 'class'); + if (type) break; } } if (rr && rr[1]) { type = rr[1]; } else { - if (tname == 'ul') { - if ((type && type.match('indent')) || (node.attribs && node.attribs.class && node.attribs.class.match('indent'))) { + if (tname === 'ul') { + const cls = getAttribute(node, 'class'); + if ((type && type.match('indent')) || (cls && cls.match('indent'))) { type = 'indent'; } else { type = 'bullet'; @@ -516,11 +487,11 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas type += String(Math.min(_MAX_LIST_LEVEL, (state.listNesting || 0) + 1)); } oldListTypeOrNull = (_enterList(state, type) || 'none'); - } else if ((tname == 'div' || tname == 'p') && cls && cls.match(/(?:^| )ace-line\b/)) { + } else if ((tname === 'div' || tname === 'p') && cls && cls.match(/(?:^| )ace-line\b/)) { // This has undesirable behavior in Chrome but is right in other browsers. // See https://github.com/ether/etherpad-lite/issues/2412 for reasoning - if (!abrowser.chrome) oldListTypeOrNull = (_enterList(state, type) || 'none'); - } else if ((tname === 'li')) { + if (!abrowser.chrome) oldListTypeOrNull = (_enterList(state, undefined) || 'none'); + } else if (tname === 'li') { state.lineAttributes.start = state.start || 0; _recalcAttribString(state); if (state.lineAttributes.list.indexOf('number') !== -1) { @@ -530,7 +501,7 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas Note how the
    item has to be inside a
  1. Because of this we don't increment the start number */ - if (node.parent && node.parent.name !== 'ol') { + if (node.parentNode && tagName(node.parentNode) !== 'ol') { /* TODO: start number has to increment based on indentLevel(numberX) This means we have to build an object IE @@ -547,7 +518,7 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas } } // UL list items never modify the start value. - if (node.parent && node.parent.name === 'ul') { + if (node.parentNode && tagName(node.parentNode) === 'ul') { state.start++; // TODO, this is hacky. // Because if the first item is an UL it will increment a list no? @@ -564,8 +535,8 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas if (className2Author && cls) { const classes = cls.match(/\S+/g); if (classes && classes.length > 0) { - for (var i = 0; i < classes.length; i++) { - var c = classes[i]; + for (let i = 0; i < classes.length; i++) { + const c = classes[i]; const a = className2Author(c); if (a) { oldAuthorOrNull = (_enterAuthor(state, a) || 'none'); @@ -576,9 +547,7 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas } } - const nc = dom.nodeNumChildren(node); - for (var i = 0; i < nc; i++) { - var c = dom.nodeChild(node, i); + for (const c of childNodes(node)) { cc.collectContent(c, state); } @@ -594,7 +563,7 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas if (isPre) cc.decrementFlag(state, 'preMode'); if (state.localAttribs) { - for (var i = 0; i < state.localAttribs.length; i++) { + for (let i = 0; i < state.localAttribs.length; i++) { cc.decrementAttrib(state, state.localAttribs[i]); } } @@ -608,7 +577,7 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas } _reachBlockPoint(node, 1, state); if (isBlock) { - if (lines.length() - 1 == startLine) { + if (lines.length() - 1 === startLine) { // added additional check to resolve https://github.com/JohnMcLear/ep_copy_paste_images/issues/20 // this does mean that images etc can't be pasted on lists but imho that's fine @@ -625,7 +594,7 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas state.localAttribs = localAttribs; }; // can pass a falsy value for end of doc - cc.notifyNextNode = function (node) { + cc.notifyNextNode = (node) => { // an "empty block" won't end a line; this addresses an issue in IE with // typing into a blank line at the end of the document. typed text // goes into the body, and the empty line div still looks clean. @@ -636,21 +605,15 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas } }; // each returns [line, char] or [-1,-1] - const getSelectionStart = function () { - return selStart; - }; - const getSelectionEnd = function () { - return selEnd; - }; + const getSelectionStart = () => selStart; + const getSelectionEnd = () => selEnd; // returns array of strings for lines found, last entry will be "" if // last line is complete (i.e. if a following span should be on a new line). // can be called at any point - cc.getLines = function () { - return lines.textLines(); - }; + cc.getLines = () => lines.textLines(); - cc.finish = function () { + cc.finish = () => { lines.flush(); const lineAttribs = lines.attribLines(); const lineStrings = cc.getLines(); @@ -661,17 +624,17 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas const ss = getSelectionStart(); const se = getSelectionEnd(); - function fixLongLines() { + const fixLongLines = () => { // design mode does not deal with with really long lines! const lineLimit = 2000; // chars const buffer = 10; // chars allowed over before wrapping let linesWrapped = 0; let numLinesAfter = 0; - for (var i = lineStrings.length - 1; i >= 0; i--) { + for (let i = lineStrings.length - 1; i >= 0; i--) { let oldString = lineStrings[i]; let oldAttribString = lineAttribs[i]; if (oldString.length > lineLimit + buffer) { - var newStrings = []; + const newStrings = []; const newAttribStrings = []; while (oldString.length > lineLimit) { // var semiloc = oldString.lastIndexOf(';', lineLimit-1); @@ -687,13 +650,13 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas newAttribStrings.push(oldAttribString); } - function fixLineNumber(lineChar) { + const fixLineNumber = (lineChar) => { if (lineChar[0] < 0) return; let n = lineChar[0]; let c = lineChar[1]; if (n > i) { n += (newStrings.length - 1); - } else if (n == i) { + } else if (n === i) { let a = 0; while (c > newStrings[a].length) { c -= newStrings[a].length; @@ -703,23 +666,20 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas } lineChar[0] = n; lineChar[1] = c; - } + }; fixLineNumber(ss); fixLineNumber(se); linesWrapped++; numLinesAfter += newStrings.length; - - newStrings.unshift(i, 1); - lineStrings.splice.apply(lineStrings, newStrings); - newAttribStrings.unshift(i, 1); - lineAttribs.splice.apply(lineAttribs, newAttribStrings); + lineStrings.splice(i, 1, ...newStrings); + lineAttribs.splice(i, 1, ...newAttribStrings); } } return { linesWrapped, numLinesAfter, }; - } + }; const wrapData = fixLongLines(); return { @@ -733,7 +693,7 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas }; return cc; -} +}; exports.sanitizeUnicode = sanitizeUnicode; exports.makeContentCollector = makeContentCollector; diff --git a/src/static/js/cssmanager.js b/src/static/js/cssmanager.js index e0c5e9926..0fcdad403 100644 --- a/src/static/js/cssmanager.js +++ b/src/static/js/cssmanager.js @@ -1,3 +1,5 @@ +'use strict'; + /** * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. @@ -20,14 +22,15 @@ * limitations under the License. */ -function makeCSSManager(emptyStylesheetTitle, doc) { +const makeCSSManager = (emptyStylesheetTitle, doc) => { if (doc === true) { doc = 'parent'; } else if (!doc) { doc = 'inner'; } - function getSheetByTitle(title) { + const getSheetByTitle = (title) => { + let win; if (doc === 'parent') { win = window.parent.parent; } else if (doc === 'inner') { @@ -35,46 +38,44 @@ function makeCSSManager(emptyStylesheetTitle, doc) { } else if (doc === 'outer') { win = window.parent; } else { - throw 'Unknown dynamic style container'; + throw new Error('Unknown dynamic style container'); } const allSheets = win.document.styleSheets; for (let i = 0; i < allSheets.length; i++) { const s = allSheets[i]; - if (s.title == title) { + if (s.title === title) { return s; } } return null; - } + }; const browserSheet = getSheetByTitle(emptyStylesheetTitle); - function browserRules() { - return (browserSheet.cssRules || browserSheet.rules); - } + const browserRules = () => (browserSheet.cssRules || browserSheet.rules); - function browserDeleteRule(i) { + const browserDeleteRule = (i) => { if (browserSheet.deleteRule) browserSheet.deleteRule(i); else browserSheet.removeRule(i); - } + }; - function browserInsertRule(i, selector) { + const browserInsertRule = (i, selector) => { if (browserSheet.insertRule) browserSheet.insertRule(`${selector} {}`, i); else browserSheet.addRule(selector, null, i); - } + }; const selectorList = []; - function indexOfSelector(selector) { + const indexOfSelector = (selector) => { for (let i = 0; i < selectorList.length; i++) { - if (selectorList[i] == selector) { + if (selectorList[i] === selector) { return i; } } return -1; - } + }; - function selectorStyle(selector) { + const selectorStyle = (selector) => { let i = indexOfSelector(selector); if (i < 0) { // add selector @@ -83,23 +84,21 @@ function makeCSSManager(emptyStylesheetTitle, doc) { i = 0; } return browserRules().item(i).style; - } + }; - function removeSelectorStyle(selector) { + const removeSelectorStyle = (selector) => { const i = indexOfSelector(selector); if (i >= 0) { browserDeleteRule(i); selectorList.splice(i, 1); } - } + }; return { selectorStyle, removeSelectorStyle, - info() { - return `${selectorList.length}:${browserRules().length}`; - }, + info: () => `${selectorList.length}:${browserRules().length}`, }; -} +}; exports.makeCSSManager = makeCSSManager; diff --git a/src/static/js/domline.js b/src/static/js/domline.js index 9ec708ce2..324e13535 100644 --- a/src/static/js/domline.js +++ b/src/static/js/domline.js @@ -1,8 +1,4 @@ -/** - * This code is mostly from the old Etherpad. Please help us to comment this code. - * This helps other people to understand this code better and helps them to improve it. - * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED - */ +'use strict'; // THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.domline // %APPJET%: import("etherpad.admin.plugins"); @@ -30,17 +26,17 @@ const Security = require('./security'); const hooks = require('./pluginfw/hooks'); const _ = require('./underscore'); const lineAttributeMarker = require('./linestylefilter').lineAttributeMarker; -const noop = function () {}; +const noop = () => {}; const domline = {}; -domline.addToLineClass = function (lineClass, cls) { +domline.addToLineClass = (lineClass, cls) => { // an "empty span" at any point can be used to add classes to // the line, using line:className. otherwise, we ignore // the span. cls.replace(/\S+/g, (c) => { - if (c.indexOf('line:') == 0) { + if (c.indexOf('line:') === 0) { // add class to line lineClass = (lineClass ? `${lineClass} ` : '') + c.substring(5); } @@ -50,7 +46,7 @@ domline.addToLineClass = function (lineClass, cls) { // if "document" is falsy we don't create a DOM node, just // an object with innerHTML and className -domline.createDomLine = function (nonEmpty, doesWrap, optBrowser, optDocument) { +domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => { const result = { node: null, appendSpan: noop, @@ -77,15 +73,12 @@ domline.createDomLine = function (nonEmpty, doesWrap, optBrowser, optDocument) { let postHtml = ''; let curHTML = null; - function processSpaces(s) { - return domline.processSpaces(s, doesWrap); - } - + const processSpaces = (s) => domline.processSpaces(s, doesWrap); const perTextNodeProcess = (doesWrap ? _.identity : processSpaces); const perHtmlLineProcess = (doesWrap ? processSpaces : _.identity); let lineClass = 'ace-line'; - result.appendSpan = function (txt, cls) { + result.appendSpan = (txt, cls) => { let processedMarker = false; // Handle lineAttributeMarker, if present if (cls.indexOf(lineAttributeMarker) >= 0) { @@ -100,7 +93,6 @@ domline.createDomLine = function (nonEmpty, doesWrap, optBrowser, optDocument) { postHtml += modifier.postHtml; processedMarker |= modifier.processedMarker; }); - if (listType) { listType = listType[1]; if (listType) { @@ -109,12 +101,15 @@ domline.createDomLine = function (nonEmpty, doesWrap, optBrowser, optDocument) { postHtml = `
  2. ${postHtml}`; } else { if (start) { // is it a start of a list with more than one item in? - if (start[1] == 1) { // if its the first one at this level? - lineClass = `${lineClass} ` + `list-start-${listType}`; // Add start class to DIV node + if (Number.parseInt(start[1]) === 1) { // if its the first one at this level? + // Add start class to DIV node + lineClass = `${lineClass} ` + `list-start-${listType}`; } - preHtml += `
    1. `; + preHtml += + `
      1. `; } else { - preHtml += `
        1. `; // Handles pasted contents into existing lists + // Handles pasted contents into existing lists + preHtml += `
          1. `; } postHtml += '
          '; } @@ -166,19 +161,21 @@ domline.createDomLine = function (nonEmpty, doesWrap, optBrowser, optDocument) { lineClass = domline.addToLineClass(lineClass, cls); } else if (txt) { if (href) { - urn_schemes = new RegExp('^(about|geo|mailto|tel):'); - if (!~href.indexOf('://') && !urn_schemes.test(href)) // if the url doesn't include a protocol prefix, assume http - { + const urn_schemes = new RegExp('^(about|geo|mailto|tel):'); + // if the url doesn't include a protocol prefix, assume http + if (!~href.indexOf('://') && !urn_schemes.test(href)) { href = `http://${href}`; } - // Using rel="noreferrer" stops leaking the URL/location of the pad when clicking links in the document. + // Using rel="noreferrer" stops leaking the URL/location of the pad when + // clicking links in the document. // Not all browsers understand this attribute, but it's part of the HTML5 standard. // https://html.spec.whatwg.org/multipage/links.html#link-type-noreferrer // Additionally, we do rel="noopener" to ensure a higher level of referrer security. // https://html.spec.whatwg.org/multipage/links.html#link-type-noopener // https://mathiasbynens.github.io/rel-noopener/ // https://github.com/ether/etherpad-lite/pull/3636 - extraOpenTags = `${extraOpenTags}`; + const escapedHref = Security.escapeHTMLAttribute(href); + extraOpenTags = `${extraOpenTags}`; extraCloseTags = `${extraCloseTags}`; } if (simpleTags) { @@ -187,16 +184,22 @@ domline.createDomLine = function (nonEmpty, doesWrap, optBrowser, optDocument) { simpleTags.reverse(); extraCloseTags = `${extraCloseTags}`; } - html.push('', extraOpenTags, perTextNodeProcess(Security.escapeHTML(txt)), extraCloseTags, ''); + html.push( + '', + extraOpenTags, + perTextNodeProcess(Security.escapeHTML(txt)), + extraCloseTags, + ''); } }; - result.clearSpans = function () { + result.clearSpans = () => { html = []; lineClass = 'ace-line'; result.lineMarker = 0; }; - function writeHTML() { + const writeHTML = () => { let newHTML = perHtmlLineProcess(html.join('')); if (!newHTML) { if ((!document) || (!optBrowser)) { @@ -213,21 +216,19 @@ domline.createDomLine = function (nonEmpty, doesWrap, optBrowser, optDocument) { curHTML = newHTML; result.node.innerHTML = curHTML; } - if (lineClass !== null) result.node.className = lineClass; + if (lineClass != null) result.node.className = lineClass; hooks.callAll('acePostWriteDomLineHTML', { node: result.node, }); - } + }; result.prepareForAdd = writeHTML; result.finishUpdate = writeHTML; - result.getInnerHTML = function () { - return curHTML || ''; - }; + result.getInnerHTML = () => curHTML || ''; return result; }; -domline.processSpaces = function (s, doesWrap) { +domline.processSpaces = (s, doesWrap) => { if (s.indexOf('<') < 0 && !doesWrap) { // short-cut return s.replace(/ /g, ' '); @@ -241,31 +242,31 @@ domline.processSpaces = function (s, doesWrap) { let beforeSpace = false; // last space in a run is normal, others are nbsp, // end of line is nbsp - for (var i = parts.length - 1; i >= 0; i--) { - var p = parts[i]; - if (p == ' ') { + for (let i = parts.length - 1; i >= 0; i--) { + const p = parts[i]; + if (p === ' ') { if (endOfLine || beforeSpace) parts[i] = ' '; endOfLine = false; beforeSpace = true; - } else if (p.charAt(0) != '<') { + } else if (p.charAt(0) !== '<') { endOfLine = false; beforeSpace = false; } } // beginning of line is nbsp - for (var i = 0; i < parts.length; i++) { - var p = parts[i]; - if (p == ' ') { + for (let i = 0; i < parts.length; i++) { + const p = parts[i]; + if (p === ' ') { parts[i] = ' '; break; - } else if (p.charAt(0) != '<') { + } else if (p.charAt(0) !== '<') { break; } } } else { - for (var i = 0; i < parts.length; i++) { - var p = parts[i]; - if (p == ' ') { + for (let i = 0; i < parts.length; i++) { + const p = parts[i]; + if (p === ' ') { parts[i] = ' '; } } diff --git a/src/static/js/excanvas.js b/src/static/js/excanvas.js deleted file mode 100644 index a34ca1da3..000000000 --- a/src/static/js/excanvas.js +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2006 Google Inc. -// -// 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. -document.createElement("canvas").getContext||(function(){var s=Math,j=s.round,F=s.sin,G=s.cos,V=s.abs,W=s.sqrt,k=10,v=k/2;function X(){return this.context_||(this.context_=new H(this))}var L=Array.prototype.slice;function Y(b,a){var c=L.call(arguments,2);return function(){return b.apply(a,c.concat(L.call(arguments)))}}var M={init:function(b){if(/MSIE/.test(navigator.userAgent)&&!window.opera){var a=b||document;a.createElement("canvas");a.attachEvent("onreadystatechange",Y(this.init_,this,a))}},init_:function(b){b.namespaces.g_vml_|| -b.namespaces.add("g_vml_","urn:schemas-microsoft-com:vml","#default#VML");b.namespaces.g_o_||b.namespaces.add("g_o_","urn:schemas-microsoft-com:office:office","#default#VML");if(!b.styleSheets.ex_canvas_){var a=b.createStyleSheet();a.owningElement.id="ex_canvas_";a.cssText="canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}g_vml_\\:*{behavior:url(#default#VML)}g_o_\\:*{behavior:url(#default#VML)}"}var c=b.getElementsByTagName("canvas"),d=0;for(;d','","");this.element_.insertAdjacentHTML("BeforeEnd",t.join(""))};i.stroke=function(b){var a=[],c=P(b?this.fillStyle:this.strokeStyle),d=c.color,f=c.alpha*this.globalAlpha;a.push("g.x)g.x=e.x;if(h.y==null||e.yg.y)g.y=e.y}}a.push(' ">');if(b)if(typeof this.fillStyle=="object"){var m=this.fillStyle,r=0,n={x:0,y:0},o=0,q=1;if(m.type_=="gradient"){var t=m.x1_/this.arcScaleX_,E=m.y1_/this.arcScaleY_,p=this.getCoords_(m.x0_/this.arcScaleX_,m.y0_/this.arcScaleY_), -z=this.getCoords_(t,E);r=Math.atan2(z.x-p.x,z.y-p.y)*180/Math.PI;if(r<0)r+=360;if(r<1.0E-6)r=0}else{var p=this.getCoords_(m.x0_,m.y0_),w=g.x-h.x,x=g.y-h.y;n={x:(p.x-h.x)/w,y:(p.y-h.y)/x};w/=this.arcScaleX_*k;x/=this.arcScaleY_*k;var R=s.max(w,x);o=2*m.r0_/R;q=2*m.r1_/R-o}var u=m.colors_;u.sort(function(ba,ca){return ba.offset-ca.offset});var J=u.length,da=u[0].color,ea=u[J-1].color,fa=u[0].alpha*this.globalAlpha,ga=u[J-1].alpha*this.globalAlpha,S=[],l=0;for(;l')}else a.push('');else{var K=this.lineScale_*this.lineWidth;if(K<1)f*=K;a.push("')}a.push("");this.element_.insertAdjacentHTML("beforeEnd",a.join(""))};i.fill=function(){this.stroke(true)};i.closePath=function(){this.currentPath_.push({type:"close"})};i.getCoords_=function(b,a){var c=this.m_;return{x:k*(b*c[0][0]+a*c[1][0]+c[2][0])-v,y:k*(b*c[0][1]+a*c[1][1]+c[2][1])-v}};i.save=function(){var b={};O(this,b);this.aStack_.push(b);this.mStack_.push(this.m_);this.m_=y(I(),this.m_)};i.restore=function(){O(this.aStack_.pop(), -this);this.m_=this.mStack_.pop()};function ha(b){var a=0;for(;a<3;a++){var c=0;for(;c<2;c++)if(!isFinite(b[a][c])||isNaN(b[a][c]))return false}return true}function A(b,a,c){if(!!ha(a)){b.m_=a;if(c)b.lineScale_=W(V(a[0][0]*a[1][1]-a[0][1]*a[1][0]))}}i.translate=function(b,a){A(this,y([[1,0,0],[0,1,0],[b,a,1]],this.m_),false)};i.rotate=function(b){var a=G(b),c=F(b);A(this,y([[a,c,0],[-c,a,0],[0,0,1]],this.m_),false)};i.scale=function(b,a){this.arcScaleX_*=b;this.arcScaleY_*=a;A(this,y([[b,0,0],[0,a, -0],[0,0,1]],this.m_),true)};i.transform=function(b,a,c,d,f,h){A(this,y([[b,a,0],[c,d,0],[f,h,1]],this.m_),true)};i.setTransform=function(b,a,c,d,f,h){A(this,[[b,a,0],[c,d,0],[f,h,1]],true)};i.clip=function(){};i.arcTo=function(){};i.createPattern=function(){return new U};function D(b){this.type_=b;this.r1_=this.y1_=this.x1_=this.r0_=this.y0_=this.x0_=0;this.colors_=[]}D.prototype.addColorStop=function(b,a){a=P(a);this.colors_.push({offset:b,color:a.color,alpha:a.alpha})};function U(){}G_vmlCanvasManager= -M;CanvasRenderingContext2D=H;CanvasGradient=D;CanvasPattern=U})(); diff --git a/src/static/js/farbtastic.js b/src/static/js/farbtastic.js index acabea49d..c6e12da44 100644 --- a/src/static/js/farbtastic.js +++ b/src/static/js/farbtastic.js @@ -1,9 +1,13 @@ // Farbtastic 2.0 alpha +// Original can be found at: +// https://github.com/mattfarina/farbtastic/blob/71ca15f4a09c8e5a08a1b0d1cf37ef028adf22f0/src/farbtastic.js +// Licensed under the terms of the GNU General Public License v2.0: +// https://github.com/mattfarina/farbtastic/blob/71ca15f4a09c8e5a08a1b0d1cf37ef028adf22f0/LICENSE.txt // edited by Sebastian Castro on 2020-04-06 (function ($) { var __debug = false; -var __factor = 0.8; +var __factor = 1; $.fn.farbtastic = function (options) { $.farbtastic(this, options); @@ -82,16 +86,6 @@ $._farbtastic = function (container, options) { } ///////////////////////////////////////////////////// - //excanvas-compatible building of canvases - fb._makeCanvas = function(className){ - var c = document.createElement('canvas'); - if (!c.getContext) { // excanvas hack - c = window.G_vmlCanvasManager.initElement(c); - c.getContext(); //this creates the excanvas children - } - $(c).addClass(className); - return c; - } /** * Initialize the color picker widget. @@ -107,15 +101,27 @@ $._farbtastic = function (container, options) { .html( '
          ' + '
          ' + + '' + + '' + '
          ' ) - .children('.farbtastic') - .append(fb._makeCanvas('farbtastic-mask')) - .append(fb._makeCanvas('farbtastic-overlay')) - .end() .find('*').attr(dim).css(dim).end() .find('div>*').css('position', 'absolute'); + // IE Fix: Recreate canvas elements with doc.createElement and excanvas. + browser.msie && $('canvas', container).each(function () { + // Fetch info. + var attr = { 'class': $(this).attr('class'), style: this.getAttribute('style') }, + e = document.createElement('canvas'); + // Replace element. + $(this).before($(e).attr(attr)).remove(); + // Init with explorerCanvas. + G_vmlCanvasManager && G_vmlCanvasManager.initElement(e); + // Set explorerCanvas elements dimensions and absolute positioning. + $(e).attr(dim).css(dim).css('position', 'absolute') + .find('*').attr(dim).css(dim); + }); + // Determine layout fb.radius = (options.width - options.wheelWidth) / 2 - 1; fb.square = Math.floor((fb.radius - options.wheelWidth / 2) * 0.7) - 1; diff --git a/src/static/js/linestylefilter.js b/src/static/js/linestylefilter.js index ddab47224..254168990 100644 --- a/src/static/js/linestylefilter.js +++ b/src/static/js/linestylefilter.js @@ -1,3 +1,5 @@ +'use strict'; + /** * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. @@ -31,7 +33,6 @@ const Changeset = require('./Changeset'); const hooks = require('./pluginfw/hooks'); const linestylefilter = {}; -const _ = require('./underscore'); const AttributeManager = require('./AttributeManager'); const padutils = require('./pad_utils').padutils; @@ -45,32 +46,30 @@ linestylefilter.ATTRIB_CLASSES = { const lineAttributeMarker = 'lineAttribMarker'; exports.lineAttributeMarker = lineAttributeMarker; -linestylefilter.getAuthorClassName = function (author) { - return `author-${author.replace(/[^a-y0-9]/g, (c) => { - if (c == '.') return '-'; - return `z${c.charCodeAt(0)}z`; - })}`; -}; +linestylefilter.getAuthorClassName = (author) => `author-${author.replace(/[^a-y0-9]/g, (c) => { + if (c === '.') return '-'; + return `z${c.charCodeAt(0)}z`; +})}`; // lineLength is without newline; aline includes newline, // but may be falsy if lineLength == 0 -linestylefilter.getLineStyleFilter = function (lineLength, aline, textAndClassFunc, apool) { +linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool) => { // Plugin Hook to add more Attrib Classes for (const attribClasses of hooks.callAll('aceAttribClasses', linestylefilter.ATTRIB_CLASSES)) { Object.assign(linestylefilter.ATTRIB_CLASSES, attribClasses); } - if (lineLength == 0) return textAndClassFunc; + if (lineLength === 0) return textAndClassFunc; const nextAfterAuthorColors = textAndClassFunc; - const authorColorFunc = (function () { + const authorColorFunc = (() => { const lineEnd = lineLength; let curIndex = 0; let extraClasses; let leftInAuthor; - function attribsToClasses(attribs) { + const attribsToClasses = (attribs) => { let classes = ''; let isLineAttribMarker = false; @@ -81,24 +80,21 @@ linestylefilter.getLineStyleFilter = function (lineLength, aline, textAndClassFu if (key) { const value = apool.getAttribValue(n); if (value) { - if (!isLineAttribMarker && _.indexOf(AttributeManager.lineAttributes, key) >= 0) { + if (!isLineAttribMarker && AttributeManager.lineAttributes.indexOf(key) >= 0) { isLineAttribMarker = true; } - if (key == 'author') { + if (key === 'author') { classes += ` ${linestylefilter.getAuthorClassName(value)}`; - } else if (key == 'list') { + } else if (key === 'list') { classes += ` list:${value}`; - } else if (key == 'start') { + } else if (key === 'start') { // Needed to introduce the correct Ordered list item start number on import classes += ` start:${value}`; } else if (linestylefilter.ATTRIB_CLASSES[key]) { classes += ` ${linestylefilter.ATTRIB_CLASSES[key]}`; } else { - classes += hooks.callAllStr('aceAttribsToClasses', { - linestylefilter, - key, - value, - }, ' ', ' ', ''); + const results = hooks.callAll('aceAttribsToClasses', {linestylefilter, key, value}); + classes += ` ${results.join(' ')}`; } } } @@ -106,37 +102,38 @@ linestylefilter.getLineStyleFilter = function (lineLength, aline, textAndClassFu if (isLineAttribMarker) classes += ` ${lineAttributeMarker}`; return classes.substring(1); - } + }; const attributionIter = Changeset.opIterator(aline); let nextOp, nextOpClasses; - function goNextOp() { + const goNextOp = () => { nextOp = attributionIter.next(); nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs)); - } + }; goNextOp(); - function nextClasses() { + const nextClasses = () => { if (curIndex < lineEnd) { extraClasses = nextOpClasses; leftInAuthor = nextOp.chars; goNextOp(); - while (nextOp.opcode && nextOpClasses == extraClasses) { + while (nextOp.opcode && nextOpClasses === extraClasses) { leftInAuthor += nextOp.chars; goNextOp(); } } - } + }; nextClasses(); - return function (txt, cls) { + return (txt, cls) => { const disableAuthColorForThisLine = hooks.callAll('disableAuthorColorsForThisLine', { linestylefilter, text: txt, class: cls, }, ' ', ' ', ''); - const disableAuthors = (disableAuthColorForThisLine == null || disableAuthColorForThisLine.length == 0) ? false : disableAuthColorForThisLine[0]; + const disableAuthors = (disableAuthColorForThisLine == null || + disableAuthColorForThisLine.length === 0) ? false : disableAuthColorForThisLine[0]; while (txt.length > 0) { if (leftInAuthor <= 0 || disableAuthors) { // prevent infinite loop if something funny's going on @@ -151,7 +148,7 @@ linestylefilter.getLineStyleFilter = function (lineLength, aline, textAndClassFu nextAfterAuthorColors(curTxt, (cls && `${cls} `) + extraClasses); curIndex += spanSize; leftInAuthor -= spanSize; - if (leftInAuthor == 0) { + if (leftInAuthor === 0) { nextClasses(); } } @@ -160,7 +157,7 @@ linestylefilter.getLineStyleFilter = function (lineLength, aline, textAndClassFu return authorColorFunc; }; -linestylefilter.getAtSignSplitterFilter = function (lineText, textAndClassFunc) { +linestylefilter.getAtSignSplitterFilter = (lineText, textAndClassFunc) => { const at = /@/g; at.lastIndex = 0; let splitPoints = null; @@ -177,66 +174,66 @@ linestylefilter.getAtSignSplitterFilter = function (lineText, textAndClassFunc) return linestylefilter.textAndClassFuncSplitter(textAndClassFunc, splitPoints); }; -linestylefilter.getRegexpFilter = function (regExp, tag) { - return function (lineText, textAndClassFunc) { - regExp.lastIndex = 0; - let regExpMatchs = null; - let splitPoints = null; - let execResult; - while ((execResult = regExp.exec(lineText))) { - if (!regExpMatchs) { - regExpMatchs = []; - splitPoints = []; - } - const startIndex = execResult.index; - const regExpMatch = execResult[0]; - regExpMatchs.push([startIndex, regExpMatch]); - splitPoints.push(startIndex, startIndex + regExpMatch.length); +linestylefilter.getRegexpFilter = (regExp, tag) => (lineText, textAndClassFunc) => { + regExp.lastIndex = 0; + let regExpMatchs = null; + let splitPoints = null; + let execResult; + while ((execResult = regExp.exec(lineText))) { + if (!regExpMatchs) { + regExpMatchs = []; + splitPoints = []; } + const startIndex = execResult.index; + const regExpMatch = execResult[0]; + regExpMatchs.push([startIndex, regExpMatch]); + splitPoints.push(startIndex, startIndex + regExpMatch.length); + } - if (!regExpMatchs) return textAndClassFunc; + if (!regExpMatchs) return textAndClassFunc; - function regExpMatchForIndex(idx) { - for (let k = 0; k < regExpMatchs.length; k++) { - const u = regExpMatchs[k]; - if (idx >= u[0] && idx < u[0] + u[1].length) { - return u[1]; - } + const regExpMatchForIndex = (idx) => { + for (let k = 0; k < regExpMatchs.length; k++) { + const u = regExpMatchs[k]; + if (idx >= u[0] && idx < u[0] + u[1].length) { + return u[1]; } - return false; } - - const handleRegExpMatchsAfterSplit = (function () { - let curIndex = 0; - return function (txt, cls) { - const txtlen = txt.length; - let newCls = cls; - const regExpMatch = regExpMatchForIndex(curIndex); - if (regExpMatch) { - newCls += ` ${tag}:${regExpMatch}`; - } - textAndClassFunc(txt, newCls); - curIndex += txtlen; - }; - })(); - - return linestylefilter.textAndClassFuncSplitter(handleRegExpMatchsAfterSplit, splitPoints); + return false; }; + + const handleRegExpMatchsAfterSplit = (() => { + let curIndex = 0; + return (txt, cls) => { + const txtlen = txt.length; + let newCls = cls; + const regExpMatch = regExpMatchForIndex(curIndex); + if (regExpMatch) { + newCls += ` ${tag}:${regExpMatch}`; + } + textAndClassFunc(txt, newCls); + curIndex += txtlen; + }; + })(); + + return linestylefilter.textAndClassFuncSplitter(handleRegExpMatchsAfterSplit, splitPoints); }; linestylefilter.getURLFilter = linestylefilter.getRegexpFilter(padutils.urlRegex, 'url'); -linestylefilter.textAndClassFuncSplitter = function (func, splitPointsOpt) { +linestylefilter.textAndClassFuncSplitter = (func, splitPointsOpt) => { let nextPointIndex = 0; let idx = 0; // don't split at 0 - while (splitPointsOpt && nextPointIndex < splitPointsOpt.length && splitPointsOpt[nextPointIndex] == 0) { + while (splitPointsOpt && + nextPointIndex < splitPointsOpt.length && + splitPointsOpt[nextPointIndex] === 0) { nextPointIndex++; } - function spanHandler(txt, cls) { + const spanHandler = (txt, cls) => { if ((!splitPointsOpt) || nextPointIndex >= splitPointsOpt.length) { func(txt, cls); idx += txt.length; @@ -247,7 +244,7 @@ linestylefilter.textAndClassFuncSplitter = function (func, splitPointsOpt) { if (pointLocInSpan >= txtlen) { func(txt, cls); idx += txt.length; - if (pointLocInSpan == txtlen) { + if (pointLocInSpan === txtlen) { nextPointIndex++; } } else { @@ -260,18 +257,18 @@ linestylefilter.textAndClassFuncSplitter = function (func, splitPointsOpt) { spanHandler(txt.substring(pointLocInSpan), cls); } } - } + }; return spanHandler; }; -linestylefilter.getFilterStack = function (lineText, textAndClassFunc, abrowser) { +linestylefilter.getFilterStack = (lineText, textAndClassFunc, abrowser) => { let func = linestylefilter.getURLFilter(lineText, textAndClassFunc); const hookFilters = hooks.callAll('aceGetFilterStack', { linestylefilter, browser: abrowser, }); - _.map(hookFilters, (hookFilter) => { + hookFilters.map((hookFilter) => { func = hookFilter(lineText, func); }); @@ -279,16 +276,16 @@ linestylefilter.getFilterStack = function (lineText, textAndClassFunc, abrowser) }; // domLineObj is like that returned by domline.createDomLine -linestylefilter.populateDomLine = function (textLine, aline, apool, domLineObj) { +linestylefilter.populateDomLine = (textLine, aline, apool, domLineObj) => { // remove final newline from text if any let text = textLine; - if (text.slice(-1) == '\n') { + if (text.slice(-1) === '\n') { text = text.substring(0, text.length - 1); } - function textAndClassFunc(tokenText, tokenClass) { + const textAndClassFunc = (tokenText, tokenClass) => { domLineObj.appendSpan(tokenText, tokenClass); - } + }; let func = linestylefilter.getFilterStack(text, textAndClassFunc); func = linestylefilter.getLineStyleFilter(text.length, aline, func, apool); diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 4dc7d0884..d8aaa4fef 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -28,7 +28,6 @@ let socket; // assigns to the global `$` and augments it with plugins. require('./jquery'); require('./farbtastic'); -require('./excanvas'); require('./gritter'); const Cookies = require('./pad_utils').Cookies; @@ -272,7 +271,9 @@ const handshake = () => { pad.collabClient.setStateIdle(); pad.collabClient.setIsPendingRevision(true); } - throw new Error(`socket.io connection error: ${JSON.stringify(error)}`); + // Don't throw an exception. Error events do not indicate problems that are not already + // addressed by reconnection logic, so throwing an exception each time there's a socket.io error + // just annoys users and fills logs. }); socket.on('message', (obj) => { @@ -295,7 +296,7 @@ const handshake = () => { // set some client vars window.clientVars = obj.data; - // initalize the pad + // initialize the pad pad._afterHandshake(); if (clientVars.readonly) { @@ -719,10 +720,6 @@ const pad = { .val(JSON.stringify(pad.collabClient.getMissedChanges())); $('form#reconnectform').submit(); }, - // this is called from code put into a frame from the server: - handleImportExportFrameCall: (callName, varargs) => { - padimpexp.handleFrameCall.call(padimpexp, callName, Array.prototype.slice.call(arguments, 1)); - }, callWhenNotCommitting: (f) => { pad.collabClient.callWhenNotCommitting(f); }, diff --git a/src/static/js/pad_automatic_reconnect.js b/src/static/js/pad_automatic_reconnect.js index db803e896..9d9ee953a 100644 --- a/src/static/js/pad_automatic_reconnect.js +++ b/src/static/js/pad_automatic_reconnect.js @@ -113,7 +113,7 @@ const reconnectionTries = { nextTry() { // double the time to try to reconnect on every time reconnection fails - const nextCounterFactor = Math.pow(2, this.counter); + const nextCounterFactor = 2 ** this.counter; this.counter++; return nextCounterFactor; diff --git a/src/static/js/pad_editor.js b/src/static/js/pad_editor.js index b7b94f720..a32af1644 100644 --- a/src/static/js/pad_editor.js +++ b/src/static/js/pad_editor.js @@ -1,5 +1,4 @@ 'use strict'; - /** * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. @@ -44,6 +43,15 @@ const padeditor = (() => { $('#editorloadingbox').hide(); if (readyFunc) { readyFunc(); + + // Listen for clicks on sidediv items + const $outerdoc = $('iframe[name="ace_outer"]').contents().find('#outerdocbody'); + $outerdoc.find('#sidedivinner').on('click', 'div', function () { + const targetLineNumber = $(this).index() + 1; + window.location.hash = `L${targetLineNumber}`; + }); + + exports.focusOnLine(self.ace); } }; @@ -55,7 +63,6 @@ const padeditor = (() => { } self.initViewOptions(); self.setViewOptions(initialViewOptions); - // view bar $('#viewbarcontents').show(); }, @@ -89,6 +96,7 @@ const padeditor = (() => { html10n.bind('localized', () => { $('#languagemenu').val(html10n.getLanguage()); // translate the value of 'unnamed' and 'Enter your name' textboxes in the userlist + // this does not interfere with html10n's normal value-setting because // html10n just ingores s // also, a value which has been set by the user will be not overwritten @@ -153,7 +161,6 @@ const padeditor = (() => { }, disable: () => { if (self.ace) { - self.ace.setProperty('grayedOut', true); self.ace.setEditable(false); } }, @@ -166,3 +173,50 @@ const padeditor = (() => { })(); exports.padeditor = padeditor; + +exports.focusOnLine = (ace) => { + // If a number is in the URI IE #L124 go to that line number + const lineNumber = window.location.hash.substr(1); + if (lineNumber) { + if (lineNumber[0] === 'L') { + const $outerdoc = $('iframe[name="ace_outer"]').contents().find('#outerdocbody'); + const lineNumberInt = parseInt(lineNumber.substr(1)); + if (lineNumberInt) { + const $inner = $('iframe[name="ace_outer"]').contents().find('iframe') + .contents().find('#innerdocbody'); + const line = $inner.find(`div:nth-child(${lineNumberInt})`); + if (line.length !== 0) { + let offsetTop = line.offset().top; + offsetTop += parseInt($outerdoc.css('padding-top').replace('px', '')); + const hasMobileLayout = $('body').hasClass('mobile-layout'); + if (!hasMobileLayout) { + offsetTop += parseInt($inner.css('padding-top').replace('px', '')); + } + const $outerdocHTML = $('iframe[name="ace_outer"]').contents() + .find('#outerdocbody').parent(); + $outerdoc.css({top: `${offsetTop}px`}); // Chrome + $outerdocHTML.animate({scrollTop: offsetTop}); // needed for FF + const node = line[0]; + ace.callWithAce((ace) => { + const selection = { + startPoint: { + index: 0, + focusAtStart: true, + maxIndex: 1, + node, + }, + endPoint: { + index: 0, + focusAtStart: true, + maxIndex: 1, + node, + }, + }; + ace.ace_setSelection(selection); + }); + } + } + } + } + // End of setSelection / set Y position of editor +}; diff --git a/src/static/js/pad_impexp.js b/src/static/js/pad_impexp.js index a44c69d8a..60a51354f 100644 --- a/src/static/js/pad_impexp.js +++ b/src/static/js/pad_impexp.js @@ -23,9 +23,9 @@ */ const padimpexp = (() => { - // /// import - let currentImportTimer = null; + let pad; + // /// import const addImportFrames = () => { $('#import .importframe').remove(); const iframe = $('`); - + $iframe = $(``); // needed for retry const origPadName = padName; @@ -132,7 +138,8 @@ var helper = {}; if (opts.padPrefs) { helper.setPadPrefCookie(opts.padPrefs); } - helper.waitFor(() => !$iframe.contents().find('#editorloadingbox').is(':visible'), 10000).done(() => { + helper.waitFor(() => !$iframe.contents().find('#editorloadingbox') + .is(':visible'), 10000).done(() => { helper.padOuter$ = getFrameJQuery(helper.padChrome$('iframe[name="ace_outer"]')); helper.padInner$ = getFrameJQuery(helper.padOuter$('iframe[name="ace_inner"]')); @@ -174,8 +181,28 @@ var helper = {}; return padName; }; - helper.waitFor = function (conditionFunc, timeoutTime = 1900, intervalTime = 10) { - const deferred = $.Deferred(); + helper.newAdmin = async (page) => { + // define the iframe + $iframe = $(``); + + // clean up inner iframe references + helper.admin$ = null; + + // remove old iframe + $('#iframe-container iframe').remove(); + // set new iframe + $('#iframe-container').append($iframe); + $iframe.one('load', () => { + helper.admin$ = getFrameJQuery($('#iframe-container iframe')); + }); + }; + + helper.waitFor = (conditionFunc, timeoutTime = 1900, intervalTime = 10) => { + // Create an Error object to use if the condition is never satisfied. This is created here so + // that the Error has a useful stack trace associated with it. + const timeoutError = + new Error(`waitFor condition never became true ${conditionFunc.toString()}`); + const deferred = new $.Deferred(); const _fail = deferred.fail.bind(deferred); let listenForFail = false; @@ -184,9 +211,9 @@ var helper = {}; return _fail(...args); }; - const check = () => { + const check = async () => { try { - if (!conditionFunc()) return; + if (!await conditionFunc()) return; deferred.resolve(); } catch (err) { deferred.reject(err); @@ -199,11 +226,10 @@ var helper = {}; const timeout = setTimeout(() => { clearInterval(intervalCheck); - const error = new Error(`wait for condition never became true ${conditionFunc.toString()}`); - deferred.reject(error); + deferred.reject(timeoutError); if (!listenForFail) { - throw error; + throw timeoutError; } }, timeoutTime); @@ -219,14 +245,12 @@ var helper = {}; * @returns {Promise} * */ - helper.waitForPromise = async function (...args) { - // Note: waitFor() has a strange API: On timeout it rejects, but it also throws an uncatchable - // exception unless .fail() has been called. That uncatchable exception is disabled here by - // passing a no-op function to .fail(). - return await this.waitFor(...args).fail(() => {}); - }; + // Note: waitFor() has a strange API: On timeout it rejects, but it also throws an uncatchable + // exception unless .fail() has been called. That uncatchable exception is disabled here by + // passing a no-op function to .fail(). + helper.waitForPromise = async (...args) => await helper.waitFor(...args).fail(() => {}); - helper.selectLines = function ($startLine, $endLine, startOffset, endOffset) { + helper.selectLines = ($startLine, $endLine, startOffset, endOffset) => { // if no offset is provided, use beginning of start line and end of end line startOffset = startOffset || 0; endOffset = endOffset === undefined ? $endLine.text().length : endOffset; @@ -245,7 +269,7 @@ var helper = {}; selection.addRange(range); }; - var getTextNodeAndOffsetOf = function ($targetLine, targetOffsetAtLine) { + const getTextNodeAndOffsetOf = ($targetLine, targetOffsetAtLine) => { const $textNodes = $targetLine.find('*').contents().filter(function () { return this.nodeType === Node.TEXT_NODE; }); @@ -268,7 +292,7 @@ var helper = {}; }); // edge cases - if (textNodeWhereOffsetIs === null) { + if (textNodeWhereOffsetIs == null) { // there was no text node inside $targetLine, so it is an empty line (
          ). // Use beginning of line textNodeWhereOffsetIs = $targetLine.get(0); @@ -287,5 +311,5 @@ var helper = {}; /* Ensure console.log doesn't blow up in IE, ugly but ok for a test framework imho*/ window.console = window.console || {}; - window.console.log = window.console.log || function () {}; + window.console.log = window.console.log || (() => {}); })(); diff --git a/tests/frontend/helper/methods.js b/src/tests/frontend/helper/methods.js similarity index 85% rename from tests/frontend/helper/methods.js rename to src/tests/frontend/helper/methods.js index 4c7fe1204..4b0391774 100644 --- a/tests/frontend/helper/methods.js +++ b/src/tests/frontend/helper/methods.js @@ -4,14 +4,14 @@ * Spys on socket.io messages and saves them into several arrays * that are visible in tests */ -helper.spyOnSocketIO = function () { +helper.spyOnSocketIO = () => { helper.contentWindow().pad.socket.on('message', (msg) => { - if (msg.type == 'COLLABROOM') { - if (msg.data.type == 'ACCEPT_COMMIT') { + if (msg.type === 'COLLABROOM') { + if (msg.data.type === 'ACCEPT_COMMIT') { helper.commits.push(msg); - } else if (msg.data.type == 'USER_NEWINFO') { + } else if (msg.data.type === 'USER_NEWINFO') { helper.userInfos.push(msg); - } else if (msg.data.type == 'CHAT_MESSAGE') { + } else if (msg.data.type === 'CHAT_MESSAGE') { helper.chatMessages.push(msg); } } @@ -30,7 +30,7 @@ helper.spyOnSocketIO = function () { * @todo needs to support writing to a specified caret position * */ -helper.edit = async function (message, line) { +helper.edit = async (message, line) => { const editsNum = helper.commits.length; line = line ? line - 1 : 0; helper.linesDiv()[line].sendkeys(message); @@ -45,7 +45,7 @@ helper.edit = async function (message, line) { * * @returns {Array.} array of divs */ -helper.linesDiv = function () { +helper.linesDiv = () => { return helper.padInner$('.ace-line').map(function () { return $(this); }).get(); @@ -57,18 +57,15 @@ helper.linesDiv = function () { * * @returns {Array.} lines of text */ -helper.textLines = function () { - return helper.linesDiv().map((div) => div.text()); -}; +helper.textLines = () => helper.linesDiv().map((div) => div.text()); /** * The default pad text transmitted via `clientVars` * * @returns {string} */ -helper.defaultText = function () { - return helper.padChrome$.window.clientVars.collab_client_vars.initialAttributedText.text; -}; +helper.defaultText = + () => helper.padChrome$.window.clientVars.collab_client_vars.initialAttributedText.text; /** * Sends a chat `message` via `sendKeys` @@ -84,7 +81,7 @@ helper.defaultText = function () { * @param {string} message the chat message to be sent * @returns {Promise} */ -helper.sendChatMessage = function (message) { +helper.sendChatMessage = (message) => { const noOfChatMessages = helper.chatMessages.length; helper.padChrome$('#chatinput').sendkeys(message); return helper.waitForPromise(() => noOfChatMessages + 1 === helper.chatMessages.length); @@ -95,7 +92,7 @@ helper.sendChatMessage = function (message) { * * @returns {Promise} */ -helper.showSettings = function () { +helper.showSettings = () => { if (!helper.isSettingsShown()) { helper.settingsButton().click(); return helper.waitForPromise(() => helper.isSettingsShown(), 2000); @@ -108,7 +105,7 @@ helper.showSettings = function () { * @returns {Promise} * @todo untested */ -helper.hideSettings = function () { +helper.hideSettings = () => { if (helper.isSettingsShown()) { helper.settingsButton().click(); return helper.waitForPromise(() => !helper.isSettingsShown(), 2000); @@ -121,7 +118,7 @@ helper.hideSettings = function () { * * @returns {Promise} */ -helper.enableStickyChatviaSettings = function () { +helper.enableStickyChatviaSettings = () => { const stickyChat = helper.padChrome$('#options-stickychat'); if (helper.isSettingsShown() && !stickyChat.is(':checked')) { stickyChat.click(); @@ -135,7 +132,7 @@ helper.enableStickyChatviaSettings = function () { * * @returns {Promise} */ -helper.disableStickyChatviaSettings = function () { +helper.disableStickyChatviaSettings = () => { const stickyChat = helper.padChrome$('#options-stickychat'); if (helper.isSettingsShown() && stickyChat.is(':checked')) { stickyChat.click(); @@ -149,7 +146,7 @@ helper.disableStickyChatviaSettings = function () { * * @returns {Promise} */ -helper.enableStickyChatviaIcon = function () { +helper.enableStickyChatviaIcon = () => { const stickyChat = helper.padChrome$('#titlesticky'); if (helper.isChatboxShown() && !helper.isChatboxSticky()) { stickyChat.click(); @@ -163,7 +160,7 @@ helper.enableStickyChatviaIcon = function () { * * @returns {Promise} */ -helper.disableStickyChatviaIcon = function () { +helper.disableStickyChatviaIcon = () => { if (helper.isChatboxShown() && helper.isChatboxSticky()) { helper.titlecross().click(); return helper.waitForPromise(() => !helper.isChatboxSticky(), 2000); @@ -182,7 +179,7 @@ helper.disableStickyChatviaIcon = function () { * @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 */ -helper.gotoTimeslider = function (revision) { +helper.gotoTimeslider = (revision) => { revision = Number.isInteger(revision) ? `#${revision}` : ''; const iframe = $('#iframe-container iframe'); iframe.attr('src', `${iframe.attr('src')}/timeslider${revision}`); @@ -198,7 +195,7 @@ helper.gotoTimeslider = function (revision) { * @todo no mousemove test * @param {number} X coordinate */ -helper.sliderClick = function (X) { +helper.sliderClick = (X) => { const sliderBar = helper.sliderBar(); const edown = new jQuery.Event('mousedown'); const eup = new jQuery.Event('mouseup'); @@ -214,11 +211,9 @@ helper.sliderClick = function (X) { * * @returns {Array.} lines of text */ -helper.timesliderTextLines = function () { - return helper.contentWindow().$('.ace-line').map(function () { - return $(this).text(); - }).get(); -}; +helper.timesliderTextLines = () => helper.contentWindow().$('.ace-line').map(function () { + return $(this).text(); +}).get(); helper.padIsEmpty = () => ( !helper.padInner$.document.getSelection().isCollapsed || diff --git a/tests/frontend/helper/ui.js b/src/tests/frontend/helper/ui.js similarity index 52% rename from tests/frontend/helper/ui.js rename to src/tests/frontend/helper/ui.js index d83cbee97..84d00589b 100644 --- a/tests/frontend/helper/ui.js +++ b/src/tests/frontend/helper/ui.js @@ -1,11 +1,11 @@ +'use strict'; + /** * the contentWindow is either the normal pad or timeslider * * @returns {HTMLElement} contentWindow */ -helper.contentWindow = function () { - return $('#iframe-container iframe')[0].contentWindow; -}; +helper.contentWindow = () => $('#iframe-container iframe')[0].contentWindow; /** * Opens the chat unless it is already open via an @@ -13,7 +13,7 @@ helper.contentWindow = function () { * * @returns {Promise} */ -helper.showChat = function () { +helper.showChat = () => { const chaticon = helper.chatIcon(); if (chaticon.hasClass('visible')) { chaticon.click(); @@ -26,7 +26,7 @@ helper.showChat = function () { * * @returns {Promise} */ -helper.hideChat = function () { +helper.hideChat = () => { if (helper.isChatboxShown() && !helper.isChatboxSticky()) { helper.titlecross().click(); return helper.waitForPromise(() => !helper.isChatboxShown(), 2000); @@ -38,69 +38,100 @@ helper.hideChat = function () { * * @returns {HTMLElement} the chat icon */ -helper.chatIcon = function () { return helper.padChrome$('#chaticon'); }; +helper.chatIcon = () => helper.padChrome$('#chaticon'); /** * The chat messages from the UI * * @returns {Array.} */ -helper.chatTextParagraphs = function () { return helper.padChrome$('#chattext').children('p'); }; +helper.chatTextParagraphs = () => helper.padChrome$('#chattext').children('p'); /** * Returns true if the chat box is sticky * * @returns {boolean} stickyness of the chat box */ -helper.isChatboxSticky = function () { - return helper.padChrome$('#chatbox').hasClass('stickyChat'); -}; +helper.isChatboxSticky = () => helper.padChrome$('#chatbox').hasClass('stickyChat'); /** * Returns true if the chat box is shown * * @returns {boolean} visibility of the chat box */ -helper.isChatboxShown = function () { - return helper.padChrome$('#chatbox').hasClass('visible'); -}; +helper.isChatboxShown = () => helper.padChrome$('#chatbox').hasClass('visible'); /** * Gets the settings menu * * @returns {HTMLElement} the settings menu */ -helper.settingsMenu = function () { return helper.padChrome$('#settings'); }; +helper.settingsMenu = () => helper.padChrome$('#settings'); /** * Gets the settings button * * @returns {HTMLElement} the settings button */ -helper.settingsButton = function () { return helper.padChrome$("button[data-l10n-id='pad.toolbar.settings.title']"); }; +helper.settingsButton = + () => helper.padChrome$("button[data-l10n-id='pad.toolbar.settings.title']"); + +/** + * Toggles user list + */ +helper.toggleUserList = async () => { + const isVisible = helper.userListShown(); + const button = helper.padChrome$("button[data-l10n-id='pad.toolbar.showusers.title']"); + button.click(); + await helper.waitForPromise(() => !isVisible); +}; + +/** + * Gets the user name input field + * + * @returns {HTMLElement} user name input field + */ +helper.usernameField = () => helper.padChrome$("input[data-l10n-id='pad.userlist.entername']"); + +/** + * Is the user list popup shown? + * + * @returns {boolean} + */ +helper.userListShown = () => helper.padChrome$('div#users').hasClass('popup-show'); + +/** + * Sets the user name + * + */ +helper.setUserName = async (name) => { + const userElement = helper.usernameField(); + userElement.click(); + userElement.val(name); + userElement.blur(); + await helper.waitForPromise(() => !helper.usernameField().hasClass('editactive')); +}; /** * Gets the titlecross icon * * @returns {HTMLElement} the titlecross icon */ -helper.titlecross = function () { return helper.padChrome$('#titlecross'); }; +helper.titlecross = () => helper.padChrome$('#titlecross'); /** * Returns true if the settings menu is visible * * @returns {boolean} is the settings menu shown? */ -helper.isSettingsShown = function () { - return helper.padChrome$('#settings').hasClass('popup-show'); -}; +helper.isSettingsShown = () => helper.padChrome$('#settings').hasClass('popup-show'); /** * Gets the timer div of a timeslider that has the datetime of the revision * * @returns {HTMLElement} timer */ -helper.timesliderTimer = function () { +helper.timesliderTimer = () => { if (typeof helper.contentWindow().$ === 'function') { return helper.contentWindow().$('#timer'); } @@ -111,7 +142,7 @@ helper.timesliderTimer = function () { * * @returns {HTMLElement} timer */ -helper.timesliderTimerTime = function () { +helper.timesliderTimerTime = () => { if (helper.timesliderTimer()) { return helper.timesliderTimer().text(); } @@ -122,9 +153,7 @@ helper.timesliderTimerTime = function () { * * @returns {HTMLElement} */ -helper.sliderBar = function () { - return helper.contentWindow().$('#ui-slider-bar'); -}; +helper.sliderBar = () => helper.contentWindow().$('#ui-slider-bar'); /** * revision_date element @@ -132,9 +161,7 @@ helper.sliderBar = function () { * * @returns {HTMLElement} */ -helper.revisionDateElem = function () { - return helper.contentWindow().$('#revision_date').text(); -}; +helper.revisionDateElem = () => helper.contentWindow().$('#revision_date').text(); /** * revision_label element @@ -142,6 +169,4 @@ helper.revisionDateElem = function () { * * @returns {HTMLElement} */ -helper.revisionLabelElem = function () { - return helper.contentWindow().$('#revision_label'); -}; +helper.revisionLabelElem = () => helper.contentWindow().$('#revision_label'); diff --git a/tests/frontend/index.html b/src/tests/frontend/index.html similarity index 100% rename from tests/frontend/index.html rename to src/tests/frontend/index.html diff --git a/tests/frontend/lib/expect.js b/src/tests/frontend/lib/expect.js similarity index 100% rename from tests/frontend/lib/expect.js rename to src/tests/frontend/lib/expect.js diff --git a/tests/frontend/lib/mocha.js b/src/tests/frontend/lib/mocha.js similarity index 99% rename from tests/frontend/lib/mocha.js rename to src/tests/frontend/lib/mocha.js index a9669b1b7..031b6e446 100644 --- a/tests/frontend/lib/mocha.js +++ b/src/tests/frontend/lib/mocha.js @@ -3216,10 +3216,10 @@ function HTML(runner, options) { runner.on(EVENT_TEST_PASS, function(test) { var url = self.testURL(test); var markup = - '
        2. %e%ems ' + + '
        3. %e

          %ems ' + '' + playIcon + - '
        4. '; + ''; var el = fragment(markup, test.speed, test.title, test.duration, url); self.addCodeToggle(el, test.body); appendToStack(el); diff --git a/tests/frontend/lib/sendkeys.js b/src/tests/frontend/lib/sendkeys.js similarity index 100% rename from tests/frontend/lib/sendkeys.js rename to src/tests/frontend/lib/sendkeys.js diff --git a/tests/frontend/lib/underscore.js b/src/tests/frontend/lib/underscore.js similarity index 100% rename from tests/frontend/lib/underscore.js rename to src/tests/frontend/lib/underscore.js diff --git a/tests/frontend/runner.css b/src/tests/frontend/runner.css similarity index 90% rename from tests/frontend/runner.css rename to src/tests/frontend/runner.css index 14cc96986..b35918028 100644 --- a/tests/frontend/runner.css +++ b/src/tests/frontend/runner.css @@ -102,11 +102,11 @@ body { } #mocha .test.pass.medium .duration { - background: #C09853; + background: #ffd285; } #mocha .test.pass.slow .duration { - background: #B94A48; + background: #ffc2c0; } #mocha .test.pass::before { @@ -122,19 +122,11 @@ body { font-size: 9px; margin-left: 5px; padding: 2px 5px; - color: white; - -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); - -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); - box-shadow: inset 0 1px 1px rgba(0,0,0,.2); - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - -ms-border-radius: 5px; - -o-border-radius: 5px; border-radius: 5px; } #mocha .test.pass.fast .duration { - display: none; + background: #d3ffe9; } #mocha .test.pending { diff --git a/tests/frontend/runner.js b/src/tests/frontend/runner.js similarity index 95% rename from tests/frontend/runner.js rename to src/tests/frontend/runner.js index 616c065b3..d9f37bcae 100644 --- a/tests/frontend/runner.js +++ b/src/tests/frontend/runner.js @@ -65,8 +65,8 @@ $(() => { runner.on('pass', (test) => { if (killTimeout) clearTimeout(killTimeout); killTimeout = setTimeout(() => { - append('FINISHED - [red]no test started since 3 minutes, tests stopped[clear]'); - }, 60000 * 3); + append('FINISHED - [red]no test started since 5 minutes, tests stopped[clear]'); + }, 60000 * 5); const medium = test.slow() / 2; test.speed = test.duration > test.slow() @@ -82,8 +82,8 @@ $(() => { runner.on('fail', (test, err) => { if (killTimeout) clearTimeout(killTimeout); killTimeout = setTimeout(() => { - append('FINISHED - [red]no test started since 3 minutes, tests stopped[clear]'); - }, 60000 * 3); + append('FINISHED - [red]no test started since 5 minutes, tests stopped[clear]'); + }, 60000 * 5); stats.failures++; test.err = err; @@ -93,8 +93,8 @@ $(() => { runner.on('pending', (test) => { if (killTimeout) clearTimeout(killTimeout); killTimeout = setTimeout(() => { - append('FINISHED - [red]no test started since 3 minutes, tests stopped[clear]'); - }, 60000 * 3); + append('FINISHED - [red]no test started since 5 minutes, tests stopped[clear]'); + }, 60000 * 5); stats.pending++; append(`-> [yellow]PENDING[clear]: ${test.title}`); @@ -170,7 +170,7 @@ $(() => { } }); - // initalize the test helper + // initialize the test helper helper.init(() => { // configure and start the test framework const grep = getURLParameter('grep'); diff --git a/src/tests/frontend/specs/adminsettings.js b/src/tests/frontend/specs/adminsettings.js new file mode 100644 index 000000000..3eb6e41a6 --- /dev/null +++ b/src/tests/frontend/specs/adminsettings.js @@ -0,0 +1,83 @@ +'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 () { + // 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); + + // saves + helper.admin$('#saveSettings').click(); + await helper.waitForPromise(() => helper.admin$('#response').is(':visible'), 5000); + + // 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 > 0, 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); + + helper.admin$('#saveSettings').click(); // saves + await helper.waitForPromise(() => helper.admin$('#response').is(':visible')); + + // settings should have the old value + helper.newAdmin('settings'); + await helper.waitForPromise( + () => helper.admin$ && helper.admin$('.settings').val().length > 0, 36000); + expect(settings).to.be(helper.admin$('.settings').val()); + }); + + it('restart works', async function () { + this.timeout(60000); + 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) { + return null; + } + }; + await helper.waitForPromise(async () => { + const startTime = await getStartTime(); + return startTime != null && startTime > 0 && Date.now() > startTime; + }, 1000, 500); + const clickTime = Date.now(); + helper.admin$('#restartEtherpad').click(); + await helper.waitForPromise(async () => { + const startTime = await getStartTime(); + return startTime != null && startTime >= clickTime; + }, 60000, 500); + }); +}); diff --git a/src/tests/frontend/specs/admintroubleshooting.js b/src/tests/frontend/specs/admintroubleshooting.js new file mode 100755 index 000000000..6e428d3b1 --- /dev/null +++ b/src/tests/frontend/specs/admintroubleshooting.js @@ -0,0 +1,47 @@ +'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 new file mode 100755 index 000000000..6bcf9cafc --- /dev/null +++ b/src/tests/frontend/specs/adminupdateplugins.js @@ -0,0 +1,115 @@ +'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 < 300, 5000); + await helper.waitForPromise(() => helper.admin$('.results').children().length > 0, 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.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/tests/frontend/specs/alphabet.js b/src/tests/frontend/specs/alphabet.js similarity index 95% rename from tests/frontend/specs/alphabet.js rename to src/tests/frontend/specs/alphabet.js index a0ad61bdf..158bc734c 100644 --- a/tests/frontend/specs/alphabet.js +++ b/src/tests/frontend/specs/alphabet.js @@ -1,3 +1,5 @@ +'use strict'; + describe('All the alphabet works n stuff', function () { const expectedString = 'abcdefghijklmnopqrstuvwxyz'; @@ -8,8 +10,8 @@ describe('All the alphabet works n stuff', function () { }); it('when you enter any char it appears right', function (done) { + this.timeout(250); const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; // get the first text element out of the inner iframe const firstTextElement = inner$('div').first(); diff --git a/tests/frontend/specs/authorship_of_editions.js b/src/tests/frontend/specs/authorship_of_editions.js similarity index 86% rename from tests/frontend/specs/authorship_of_editions.js rename to src/tests/frontend/specs/authorship_of_editions.js index 6cf14b869..f6f29d491 100644 --- a/tests/frontend/specs/authorship_of_editions.js +++ b/src/tests/frontend/specs/authorship_of_editions.js @@ -1,3 +1,5 @@ +'use strict'; + describe('author of pad edition', function () { const REGULAR_LINE = 0; const LINE_WITH_ORDERED_LIST = 1; @@ -5,10 +7,11 @@ describe('author of pad edition', function () { // author 1 creates a new pad with some content (regular lines and lists) before(function (done) { - var padId = helper.newPad(() => { + const padId = helper.newPad(() => { // make sure pad has at least 3 lines const $firstLine = helper.padInner$('div').first(); - const threeLines = ['regular line', 'line with ordered list', 'line with unordered list'].join('
          '); + const threeLines = ['regular line', 'line with ordered list', 'line with unordered list'] + .join('
          '); $firstLine.html(threeLines); // wait for lines to be processed by Etherpad @@ -43,7 +46,8 @@ describe('author of pad edition', function () { setTimeout(() => { // Expire cookie, so author is changed after reloading the pad. // See https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#Example_4_Reset_the_previous_cookie - helper.padChrome$.document.cookie = 'token=foo;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; + helper.padChrome$.document.cookie = + 'token=foo;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; helper.newPad(done, padId); }, 1000); @@ -59,24 +63,22 @@ describe('author of pad edition', function () { changeLineAndCheckOnlyThatChangeIsFromThisAuthor(REGULAR_LINE, 'x', done); }); - it('marks only the new content as changes of the second user on a line with ordered list', function (done) { + it('marks only the new content as changes of the second user on a ' + + 'line with ordered list', function (done) { changeLineAndCheckOnlyThatChangeIsFromThisAuthor(LINE_WITH_ORDERED_LIST, 'y', done); }); - it('marks only the new content as changes of the second user on a line with unordered list', function (done) { + it('marks only the new content as changes of the second user on ' + + 'a line with unordered list', function (done) { changeLineAndCheckOnlyThatChangeIsFromThisAuthor(LINE_WITH_UNORDERED_LIST, 'z', done); }); /* ********************** Helper functions ************************ */ - var getLine = function (lineNumber) { - return helper.padInner$('div').eq(lineNumber); - }; + const getLine = (lineNumber) => helper.padInner$('div').eq(lineNumber); - const getAuthorFromClassList = function (classes) { - return classes.find((cls) => cls.startsWith('author')); - }; + const getAuthorFromClassList = (classes) => classes.find((cls) => cls.startsWith('author')); - var changeLineAndCheckOnlyThatChangeIsFromThisAuthor = function (lineNumber, textChange, done) { + const changeLineAndCheckOnlyThatChangeIsFromThisAuthor = (lineNumber, textChange, done) => { // get original author class const classes = getLine(lineNumber).find('span').first().attr('class').split(' '); const originalAuthor = getAuthorFromClassList(classes); diff --git a/tests/frontend/specs/bold.js b/src/tests/frontend/specs/bold.js similarity index 85% rename from tests/frontend/specs/bold.js rename to src/tests/frontend/specs/bold.js index a7c46e1bc..61d8133ad 100644 --- a/tests/frontend/specs/bold.js +++ b/src/tests/frontend/specs/bold.js @@ -1,3 +1,5 @@ +'use strict'; + describe('bold button', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -6,6 +8,7 @@ describe('bold button', function () { }); it('makes text bold on click', function (done) { + this.timeout(100); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; @@ -19,7 +22,6 @@ describe('bold button', function () { const $boldButton = chrome$('.buttonicon-bold'); $boldButton.click(); - // ace creates a new dom element when you press a button, so just get the first text element again const $newFirstTextElement = inner$('div').first(); // is there a element now? @@ -35,8 +37,8 @@ describe('bold button', function () { }); it('makes text bold on keypress', function (done) { + this.timeout(100); const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; // get the first text element out of the inner iframe const $firstTextElement = inner$('div').first(); @@ -44,12 +46,11 @@ describe('bold button', function () { // select this text element $firstTextElement.sendkeys('{selectall}'); - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = 66; // b inner$('#innerdocbody').trigger(e); - // ace creates a new dom element when you press a button, so just get the first text element again const $newFirstTextElement = inner$('div').first(); // is there a element now? diff --git a/tests/frontend/specs/change_user_color.js b/src/tests/frontend/specs/change_user_color.js similarity index 90% rename from tests/frontend/specs/change_user_color.js rename to src/tests/frontend/specs/change_user_color.js index e8c16db37..1f41dcce2 100644 --- a/tests/frontend/specs/change_user_color.js +++ b/src/tests/frontend/specs/change_user_color.js @@ -1,3 +1,5 @@ +'use strict'; + describe('change user color', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -5,8 +7,9 @@ describe('change user color', function () { this.timeout(60000); }); - it('Color picker matches original color and remembers the user color after a refresh', function (done) { - this.timeout(60000); + it('Color picker matches original color and remembers the user color' + + ' after a refresh', function (done) { + this.timeout(10000); const chrome$ = helper.padChrome$; // click on the settings button to make settings visible @@ -60,7 +63,7 @@ describe('change user color', function () { }); it('Own user color is shown when you enter a chat', function (done) { - const inner$ = helper.padInner$; + this.timeout(1000); const chrome$ = helper.padChrome$; const $colorOption = helper.padChrome$('#options-colorscheck'); @@ -90,13 +93,15 @@ describe('change user color', function () { $chatButton.click(); const $chatInput = chrome$('#chatinput'); $chatInput.sendkeys('O hi'); // simulate a keypress of typing user - $chatInput.sendkeys('{enter}'); // simulate a keypress of enter actually does evt.which = 10 not 13 + // simulate a keypress of enter actually does evt.which = 10 not 13 + $chatInput.sendkeys('{enter}'); - // check if chat shows up - helper.waitFor(() => chrome$('#chattext').children('p').length !== 0 // wait until the chat message shows up + // wait until the chat message shows up + helper.waitFor(() => chrome$('#chattext').children('p').length !== 0 ).done(() => { const $firstChatMessage = chrome$('#chattext').children('p'); - expect($firstChatMessage.css('background-color')).to.be(testColorRGB); // expect the first chat message to be of the user's color + // 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 new file mode 100644 index 000000000..8ba5e637a --- /dev/null +++ b/src/tests/frontend/specs/change_user_name.js @@ -0,0 +1,38 @@ +'use strict'; + +describe('change username value', function () { + // create a new pad before each test run + beforeEach(function (cb) { + helper.newPad(cb); + }); + + it('Remembers the user name after a refresh', async function () { + this.timeout(1500); + helper.toggleUserList(); + helper.setUserName('😃'); + + helper.newPad({ // get a new pad, but don't clear the cookies + clearCookies: false, + cb() { + helper.toggleUserList(); + + expect(helper.usernameField().val()).to.be('😃'); + }, + }); + }); + + it('Own user name is shown when you enter a chat', async function () { + this.timeout(1500); + helper.toggleUserList(); + helper.setUserName('😃'); + + helper.showChat(); + helper.sendChatMessage('O hi{enter}'); + + await helper.waitForPromise(() => { + // username:hours:minutes text + const chatText = helper.chatTextParagraphs().text(); + return chatText.indexOf('😃') === 0; + }); + }); +}); diff --git a/tests/frontend/specs/chat.js b/src/tests/frontend/specs/chat.js similarity index 84% rename from tests/frontend/specs/chat.js rename to src/tests/frontend/specs/chat.js index d45988d60..be080755a 100644 --- a/tests/frontend/specs/chat.js +++ b/src/tests/frontend/specs/chat.js @@ -1,10 +1,14 @@ +'use strict'; + describe('Chat messages and UI', function () { // create a new pad before each test run beforeEach(function (cb) { helper.newPad(cb); }); - it('opens chat, sends a message, makes sure it exists on the page and hides chat', async function () { + 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(); @@ -31,7 +35,8 @@ describe('Chat messages and UI', function () { await helper.showChat(); - await helper.sendChatMessage(`{enter}${chatValue}{enter}`); // simulate a keypress of typing enter, mluto and enter (to send 'mluto') + // simulate a keypress of typing enter, mluto and enter (to send 'mluto') + await helper.sendChatMessage(`{enter}${chatValue}{enter}`); const chat = helper.chatTextParagraphs(); @@ -44,7 +49,9 @@ describe('Chat messages and UI', function () { expect(chat.text()).to.be(`${username}${time} ${chatValue}`); }); - it('makes chat stick to right side of the screen via settings, remove sticky via settings, close it', async function () { + 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(); @@ -60,7 +67,9 @@ describe('Chat messages and UI', function () { 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 () { + 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(); @@ -76,10 +85,9 @@ describe('Chat messages and UI', function () { expect(helper.isChatboxShown()).to.be(false); }); - xit('Checks showChat=false URL Parameter hides chat then when removed it shows chat', function (done) { + xit('Checks showChat=false URL Parameter hides chat then' + + ' when removed it shows chat', function (done) { this.timeout(60000); - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; setTimeout(() => { // give it a second to save the username on the server side helper.newPad({ // get a new pad, but don't clear the cookies @@ -106,6 +114,6 @@ describe('Chat messages and UI', function () { }, 1000); }, }); - }, 1000); + }, 3000); }); }); diff --git a/tests/frontend/specs/chat_load_messages.js b/src/tests/frontend/specs/chat_load_messages.js similarity index 72% rename from tests/frontend/specs/chat_load_messages.js rename to src/tests/frontend/specs/chat_load_messages.js index 29c1734ca..6b34e614b 100644 --- a/tests/frontend/specs/chat_load_messages.js +++ b/src/tests/frontend/specs/chat_load_messages.js @@ -1,3 +1,5 @@ +'use strict'; + describe('chat-load-messages', function () { let padName; @@ -6,8 +8,7 @@ describe('chat-load-messages', function () { this.timeout(60000); }); - it('adds a lot of messages', function (done) { - const inner$ = helper.padInner$; + it('adds a lot of messages', async function () { const chrome$ = helper.padChrome$; const chatButton = chrome$('#chaticon'); chatButton.click(); @@ -19,18 +20,17 @@ describe('chat-load-messages', function () { const messages = 140; for (let i = 1; i <= messages; i++) { let num = `${i}`; - if (num.length == 1) num = `00${num}`; - if (num.length == 2) num = `0${num}`; + if (num.length === 1) num = `00${num}`; + if (num.length === 2) num = `0${num}`; chatInput.sendkeys(`msg${num}`); chatInput.sendkeys('{enter}'); + await helper.waitForPromise(() => chatText.children('p').length === i); } - helper.waitFor(() => chatText.children('p').length == messages, 60000).always(() => { - expect(chatText.children('p').length).to.be(messages); - helper.newPad(done, padName); - }); + await new Promise((resolve) => helper.newPad(() => resolve(), padName)); }); it('checks initial message count', function (done) { + this.timeout(1000); let chatText; const expectedCount = 101; const chrome$ = helper.padChrome$; @@ -38,7 +38,7 @@ describe('chat-load-messages', function () { const chatButton = chrome$('#chaticon'); chatButton.click(); chatText = chrome$('#chattext'); - return chatText.children('p').length == expectedCount; + return chatText.children('p').length === expectedCount; }).always(() => { expect(chatText.children('p').length).to.be(expectedCount); done(); @@ -46,6 +46,7 @@ describe('chat-load-messages', function () { }); it('loads more messages', function (done) { + this.timeout(3000); const expectedCount = 122; const chrome$ = helper.padChrome$; const chatButton = chrome$('#chaticon'); @@ -54,24 +55,24 @@ describe('chat-load-messages', function () { const loadMsgBtn = chrome$('#chatloadmessagesbutton'); loadMsgBtn.click(); - helper.waitFor(() => chatText.children('p').length == expectedCount).always(() => { + helper.waitFor(() => chatText.children('p').length === expectedCount).always(() => { expect(chatText.children('p').length).to.be(expectedCount); done(); }); }); it('checks for button vanishing', function (done) { + this.timeout(2000); const expectedDisplay = 'none'; const chrome$ = helper.padChrome$; const chatButton = chrome$('#chaticon'); chatButton.click(); - const chatText = chrome$('#chattext'); const loadMsgBtn = chrome$('#chatloadmessagesbutton'); const loadMsgBall = chrome$('#chatloadmessagesball'); loadMsgBtn.click(); - helper.waitFor(() => loadMsgBtn.css('display') == expectedDisplay && - loadMsgBall.css('display') == expectedDisplay).always(() => { + helper.waitFor(() => loadMsgBtn.css('display') === expectedDisplay && + loadMsgBall.css('display') === expectedDisplay).always(() => { expect(loadMsgBtn.css('display')).to.be(expectedDisplay); expect(loadMsgBall.css('display')).to.be(expectedDisplay); done(); diff --git a/tests/frontend/specs/clear_authorship_colors.js b/src/tests/frontend/specs/clear_authorship_colors.js similarity index 74% rename from tests/frontend/specs/clear_authorship_colors.js rename to src/tests/frontend/specs/clear_authorship_colors.js index f622e912a..ae5603949 100644 --- a/tests/frontend/specs/clear_authorship_colors.js +++ b/src/tests/frontend/specs/clear_authorship_colors.js @@ -1,3 +1,5 @@ +'use strict'; + describe('clear authorship colors button', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -6,6 +8,7 @@ describe('clear authorship colors button', function () { }); it('makes text clear authorship colors', function (done) { + this.timeout(2500); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; @@ -17,9 +20,6 @@ describe('clear authorship colors button', function () { // get the first text element out of the inner iframe const $firstTextElement = inner$('div').first(); - // Get the original text - const originalText = inner$('div').first().text(); - // Set some new text const sentText = 'Hello'; @@ -28,7 +28,8 @@ describe('clear authorship colors button', function () { $firstTextElement.sendkeys(sentText); $firstTextElement.sendkeys('{rightarrow}'); - helper.waitFor(() => inner$('div span').first().attr('class').indexOf('author') !== -1 // wait until we have the full value available + // wait until we have the full value available + helper.waitFor(() => inner$('div span').first().attr('class').indexOf('author') !== -1 ).done(() => { // IE hates you if you don't give focus to the inner frame bevore you do a clearAuthorship inner$('div').first().focus(); @@ -37,16 +38,13 @@ describe('clear authorship colors button', function () { const $clearauthorshipcolorsButton = chrome$('.buttonicon-clearauthorship'); $clearauthorshipcolorsButton.click(); - // does the first divs span include an author class? - var hasAuthorClass = inner$('div span').first().attr('class').indexOf('author') !== -1; - // expect(hasAuthorClass).to.be(false); - // does the first div include an author class? - var hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1; + const hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1; expect(hasAuthorClass).to.be(false); helper.waitFor(() => { - const disconnectVisible = chrome$('div.disconnected').attr('class').indexOf('visible') === -1; + const disconnectVisible = + chrome$('div.disconnected').attr('class').indexOf('visible') === -1; return (disconnectVisible === true); }); @@ -58,6 +56,7 @@ describe('clear authorship colors button', function () { }); it("makes text clear authorship colors and checks it can't be undone", function (done) { + this.timeout(1500); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; @@ -69,9 +68,6 @@ describe('clear authorship colors button', function () { // get the first text element out of the inner iframe const $firstTextElement = inner$('div').first(); - // Get the original text - const originalText = inner$('div').first().text(); - // Set some new text const sentText = 'Hello'; @@ -80,7 +76,9 @@ describe('clear authorship colors button', function () { $firstTextElement.sendkeys(sentText); $firstTextElement.sendkeys('{rightarrow}'); - helper.waitFor(() => inner$('div span').first().attr('class').indexOf('author') !== -1 // wait until we have the full value available + // wait until we have the full value available + helper.waitFor( + () => inner$('div span').first().attr('class').indexOf('author') !== -1 ).done(() => { // IE hates you if you don't give focus to the inner frame bevore you do a clearAuthorship inner$('div').first().focus(); @@ -89,15 +87,11 @@ describe('clear authorship colors button', function () { const $clearauthorshipcolorsButton = chrome$('.buttonicon-clearauthorship'); $clearauthorshipcolorsButton.click(); - // does the first divs span include an author class? - var hasAuthorClass = inner$('div span').first().attr('class').indexOf('author') !== -1; - // expect(hasAuthorClass).to.be(false); - // does the first div include an author class? - var hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1; + let hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1; expect(hasAuthorClass).to.be(false); - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = 90; // z inner$('#innerdocbody').trigger(e); // shouldn't od anything @@ -115,7 +109,8 @@ describe('clear authorship colors button', function () { expect(hasAuthorClass).to.be(false); helper.waitFor(() => { - const disconnectVisible = chrome$('div.disconnected').attr('class').indexOf('visible') === -1; + const disconnectVisible = + chrome$('div.disconnected').attr('class').indexOf('visible') === -1; return (disconnectVisible === true); }); diff --git a/tests/frontend/specs/delete.js b/src/tests/frontend/specs/delete.js similarity index 73% rename from tests/frontend/specs/delete.js rename to src/tests/frontend/specs/delete.js index 4267aeec7..1ffbbd51c 100644 --- a/tests/frontend/specs/delete.js +++ b/src/tests/frontend/specs/delete.js @@ -1,3 +1,5 @@ +'use strict'; + describe('delete keystroke', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -6,8 +8,8 @@ describe('delete keystroke', function () { }); it('makes text delete', function (done) { + this.timeout(50); const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; // get the first text element out of the inner iframe const $firstTextElement = inner$('div').first(); @@ -15,15 +17,10 @@ describe('delete keystroke', function () { // get the original length of this element const elementLength = $firstTextElement.text().length; - // get the original string value minus the last char - const originalTextValue = $firstTextElement.text(); - const originalTextValueMinusFirstChar = originalTextValue.substring(1, originalTextValue.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 - // ace creates a new dom element when you press a keystroke, so just get the first text element again const $newFirstTextElement = inner$('div').first(); // get the new length of this element diff --git a/tests/frontend/specs/drag_and_drop.js b/src/tests/frontend/specs/drag_and_drop.js similarity index 82% rename from tests/frontend/specs/drag_and_drop.js rename to src/tests/frontend/specs/drag_and_drop.js index a9726111c..8937b375e 100644 --- a/tests/frontend/specs/drag_and_drop.js +++ b/src/tests/frontend/specs/drag_and_drop.js @@ -1,4 +1,6 @@ -// WARNING: drag and drop is only simulated on these tests, so manual testing might also be necessary +'use strict'; + +// WARNING: drag and drop is only simulated on these tests, manual testing might also be necessary describe('drag and drop', function () { before(function (done) { helper.newPad(() => { @@ -21,12 +23,15 @@ describe('drag and drop', function () { }); context('and user triggers UNDO', function () { - before(function () { + before(async function () { + const originalHTML = helper.padInner$('body').html(); const $undoButton = helper.padChrome$('.buttonicon-undo'); $undoButton.click(); + await helper.waitForPromise(() => helper.padInner$('body').html() !== originalHTML); }); it('moves text back to its original place', function (done) { + this.timeout(50); // test text was removed from drop target const $targetLine = getLine(TARGET_LINE); expect($targetLine.text()).to.be('Target line []'); @@ -56,12 +61,15 @@ describe('drag and drop', function () { }); context('and user triggers UNDO', function () { - before(function () { + before(async function () { + const originalHTML = helper.padInner$('body').html(); const $undoButton = helper.padChrome$('.buttonicon-undo'); $undoButton.click(); + await helper.waitForPromise(() => helper.padInner$('body').html() !== originalHTML); }); it('moves text back to its original place', function (done) { + this.timeout(50); // test text was removed from drop target const $targetLine = getLine(TARGET_LINE); expect($targetLine.text()).to.be('Target line []'); @@ -78,18 +86,19 @@ describe('drag and drop', function () { }); /* ********************* Helper functions/constants ********************* */ - var TARGET_LINE = 2; - var FIRST_SOURCE_LINE = 5; + const TARGET_LINE = 2; + const FIRST_SOURCE_LINE = 5; - var getLine = function (lineNumber) { + const getLine = (lineNumber) => { const $lines = helper.padInner$('div'); return $lines.slice(lineNumber, lineNumber + 1); }; - var createScriptWithSeveralLines = function (done) { + const createScriptWithSeveralLines = (done) => { // create some lines to be used on the tests const $firstLine = helper.padInner$('div').first(); - $firstLine.html('...
          ...
          Target line []
          ...
          ...
          Source line 1.
          Source line 2.
          '); + $firstLine.html('...
          ...
          Target line []
          ...
          ...
          ' + + 'Source line 1.
          Source line 2.
          '); // wait for lines to be split helper.waitFor(() => { @@ -98,7 +107,7 @@ describe('drag and drop', function () { }).done(done); }; - var selectPartOfSourceLine = function () { + const selectPartOfSourceLine = () => { const $sourceLine = getLine(FIRST_SOURCE_LINE); // select 'line 1' from 'Source line 1.' @@ -106,14 +115,14 @@ describe('drag and drop', function () { const end = start + 'line 1'.length; helper.selectLines($sourceLine, $sourceLine, start, end); }; - var selectMultipleSourceLines = function () { + const selectMultipleSourceLines = () => { const $firstSourceLine = getLine(FIRST_SOURCE_LINE); const $lastSourceLine = getLine(FIRST_SOURCE_LINE + 1); helper.selectLines($firstSourceLine, $lastSourceLine); }; - var dragSelectedTextAndDropItIntoMiddleOfLine = function (targetLineNumber) { + const dragSelectedTextAndDropItIntoMiddleOfLine = (targetLineNumber) => { // dragstart: start dragging content triggerEvent('dragstart'); @@ -126,7 +135,7 @@ describe('drag and drop', function () { triggerEvent('dragend'); }; - var getHtmlFromSelectedText = function () { + const getHtmlFromSelectedText = () => { const innerDocument = helper.padInner$.document; const range = innerDocument.getSelection().getRangeAt(0); @@ -139,12 +148,12 @@ describe('drag and drop', function () { return draggedHtml; }; - var triggerEvent = function (eventName) { - const event = helper.padInner$.Event(eventName); + const triggerEvent = (eventName) => { + const event = new helper.padInner$.Event(eventName); helper.padInner$('#innerdocbody').trigger(event); }; - var moveSelectionIntoTarget = function (draggedHtml, targetLineNumber) { + const moveSelectionIntoTarget = (draggedHtml, targetLineNumber) => { const innerDocument = helper.padInner$.document; // delete original content diff --git a/tests/frontend/specs/embed_value.js b/src/tests/frontend/specs/embed_value.js similarity index 97% rename from tests/frontend/specs/embed_value.js rename to src/tests/frontend/specs/embed_value.js index d6fb8c977..ee1bf8966 100644 --- a/tests/frontend/specs/embed_value.js +++ b/src/tests/frontend/specs/embed_value.js @@ -1,3 +1,5 @@ +'use strict'; + describe('embed links', function () { const objectify = function (str) { const hash = {}; @@ -55,6 +57,7 @@ describe('embed links', function () { describe('the share link', function () { it('is the actual pad url', function (done) { + this.timeout(50); const chrome$ = helper.padChrome$; // open share dropdown @@ -71,6 +74,7 @@ describe('embed links', function () { describe('the embed as iframe code', function () { it('is an iframe with the the correct url parameters and correct size', function (done) { + this.timeout(50); const chrome$ = helper.padChrome$; // open share dropdown @@ -94,6 +98,7 @@ describe('embed links', function () { describe('the share link', function () { it('shows a read only url', function (done) { + this.timeout(50); const chrome$ = helper.padChrome$; // open share dropdown @@ -112,6 +117,7 @@ describe('embed links', function () { describe('the embed as iframe code', function () { it('is an iframe with the the correct url parameters and correct size', function (done) { + this.timeout(50); const chrome$ = helper.padChrome$; // open share dropdown diff --git a/src/tests/frontend/specs/enter.js b/src/tests/frontend/specs/enter.js new file mode 100644 index 000000000..69cd9d48a --- /dev/null +++ b/src/tests/frontend/specs/enter.js @@ -0,0 +1,62 @@ +'use strict'; + +describe('enter keystroke', function () { + // create a new pad before each test run + beforeEach(function (cb) { + helper.newPad(cb); + this.timeout(60000); + }); + it('creates a new line & puts cursor onto a new line', function (done) { + 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}'); + + helper.waitFor(() => inner$('div').first().text() === '').done(() => { + 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); + done(); + }); + }); + + 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/tests/frontend/specs/font_type.js b/src/tests/frontend/specs/font_type.js similarity index 93% rename from tests/frontend/specs/font_type.js rename to src/tests/frontend/specs/font_type.js index 51971da39..9790873b3 100644 --- a/tests/frontend/specs/font_type.js +++ b/src/tests/frontend/specs/font_type.js @@ -1,3 +1,5 @@ +'use strict'; + describe('font select', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -6,6 +8,7 @@ describe('font select', function () { }); it('makes text RobotoMono', function (done) { + this.timeout(100); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; @@ -15,7 +18,6 @@ describe('font select', function () { // get the font menu and RobotoMono option const $viewfontmenu = chrome$('#viewfontmenu'); - const $RobotoMonooption = $viewfontmenu.find('[value=RobotoMono]'); // select RobotoMono and fire change event // $RobotoMonooption.attr('selected','selected'); diff --git a/tests/frontend/specs/helper.js b/src/tests/frontend/specs/helper.js similarity index 92% rename from tests/frontend/specs/helper.js rename to src/tests/frontend/specs/helper.js index 6bc6a3643..fdd896ea9 100644 --- a/tests/frontend/specs/helper.js +++ b/src/tests/frontend/specs/helper.js @@ -1,3 +1,5 @@ +'use strict'; + describe('the test helper', function () { describe('the newPad method', function () { xit("doesn't leak memory if you creates iframes over and over again", function (done) { @@ -5,7 +7,7 @@ describe('the test helper', function () { let times = 10; - var loadPad = function () { + const loadPad = () => { helper.newPad(() => { times--; if (times > 0) { @@ -75,13 +77,14 @@ describe('the test helper', function () { // Before refreshing, make sure the name is there expect($usernameInput.val()).to.be('John McLear'); - // Now that we have a chrome, we can set a pad cookie, so we can confirm it gets wiped as well + // Now that we have a chrome, we can set a pad cookie + // so we can confirm it gets wiped as well chrome$.document.cookie = 'prefsHtml=baz;expires=Thu, 01 Jan 3030 00:00:00 GMT'; expect(chrome$.document.cookie).to.contain('prefsHtml=baz'); - // Cookies are weird. Because it's attached to chrome$ (as helper.setPadCookies does), AND we - // didn't put path=/, we shouldn't expect it to be visible on window.document.cookie. Let's just - // be sure. + // Cookies are weird. Because it's attached to chrome$ (as helper.setPadCookies does) + // AND we didn't put path=/, we shouldn't expect it to be visible on + // window.document.cookie. Let's just be sure. expect(window.document.cookie).to.not.contain('prefsHtml=baz'); setTimeout(() => { // give it a second to save the username on the server side @@ -207,6 +210,19 @@ describe('the test helper', function () { await helper.waitFor(() => true, 0); }); }); + + it('accepts async functions', async function () { + await helper.waitFor(async () => true).fail(() => {}); + // Make sure it checks the truthiness of the Promise's resolved value, not the truthiness of + // the Promise itself (a Promise is always truthy). + let ok = false; + try { + await helper.waitFor(async () => false, 0).fail(() => {}); + } catch (err) { + ok = true; + } + expect(ok).to.be(true); + }); }); describe('the waitForPromise method', function () { @@ -266,7 +282,8 @@ describe('the test helper', function () { this.timeout(60000); }); - it('changes editor selection to be between startOffset of $startLine and endOffset of $endLine', function (done) { + it('changes editor selection to be between startOffset of $startLine ' + + 'and endOffset of $endLine', function (done) { const inner$ = helper.padInner$; const startOffset = 2; @@ -313,7 +330,8 @@ describe('the test helper', function () { * is not consistent between browsers but that's the situation so that's * how I'm covering it in this test. */ - expect(cleanText(selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('ort lines to test'); + expect(cleanText( + selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('ort lines to test'); done(); }); @@ -365,12 +383,14 @@ describe('the test helper', function () { * is not consistent between browsers but that's the situation so that's * how I'm covering it in this test. */ - expect(cleanText(selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('ort lines to test'); + expect(cleanText( + selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('ort lines to test'); done(); }); - it('selects all text between beginning of $startLine and end of $endLine when no offset is provided', function (done) { + it('selects all text between beginning of $startLine and end of $endLine ' + + 'when no offset is provided', function (done) { const inner$ = helper.padInner$; const $lines = inner$('div'); @@ -388,7 +408,8 @@ describe('the test helper', function () { * is not consistent between browsers but that's the situation so that's * how I'm covering it in this test. */ - expect(cleanText(selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('short lines to test'); + expect(cleanText( + selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('short lines to test'); done(); }); diff --git a/src/tests/frontend/specs/importexport.js b/src/tests/frontend/specs/importexport.js new file mode 100644 index 000000000..4eb95eeb0 --- /dev/null +++ b/src/tests/frontend/specs/importexport.js @@ -0,0 +1,329 @@ +'use strict'; + +describe('import functionality', function () { + beforeEach(function (cb) { + helper.newPad(cb); // creates a new pad + this.timeout(60000); + }); + + function getinnertext() { + const inner = helper.padInner$; + if (!inner) { + return ''; + } + let newtext = ''; + inner('div').each((line, el) => { + newtext += `${el.innerHTML}\n`; + }); + return newtext; + } + function importrequest(data, importurl, type) { + let error; + const result = $.ajax({ + url: importurl, + type: 'post', + processData: false, + async: false, + contentType: 'multipart/form-data; boundary=boundary', + accepts: { + text: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + }, + data: [ + 'Content-Type: multipart/form-data; boundary=--boundary', + '', + '--boundary', + `Content-Disposition: form-data; name="file"; filename="import.${type}"`, + 'Content-Type: text/plain', + '', + data, + '', + '--boundary', + ].join('\r\n'), + error(res) { + error = res; + }, + }); + expect(error).to.be(undefined); + return result; + } + function exportfunc(link) { + const exportresults = []; + $.ajaxSetup({ + async: false, + }); + $.get(`${link}/export/html`, (data) => { + const start = data.indexOf(''); + const end = data.indexOf(''); + const html = data.substr(start + 6, end - start - 6); + exportresults.push(['html', html]); + }); + $.get(`${link}/export/txt`, (data) => { + exportresults.push(['txt', data]); + }); + return exportresults; + } + + xit('import a pad with newlines from txt', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const textWithNewLines = 'imported text\nnewline'; + importrequest(textWithNewLines, importurl, 'txt'); + helper.waitFor(() => expect(getinnertext()) + .to.be('imported text\nnewline\n
          \n')); + const results = exportfunc(helper.padChrome$.window.location.href); + expect(results[0][1]).to.be('imported text
          newline

          '); + expect(results[1][1]).to.be('imported text\nnewline\n\n'); + done(); + }); + xit('import a pad with newlines from html', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const htmlWithNewLines = 'htmltext
          newline'; + importrequest(htmlWithNewLines, importurl, 'html'); + helper.waitFor(() => expect(getinnertext()) + .to.be('htmltext\nnewline\n
          \n')); + const results = exportfunc(helper.padChrome$.window.location.href); + expect(results[0][1]).to.be('htmltext
          newline

          '); + expect(results[1][1]).to.be('htmltext\nnewline\n\n'); + done(); + }); + xit('import a pad with attributes from html', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const htmlWithNewLines = 'htmltext
          ' + + 'newline'; + importrequest(htmlWithNewLines, importurl, 'html'); + helper.waitFor(() => expect(getinnertext()) + .to.be('htmltext\n' + + 'newline\n
          \n')); + const results = exportfunc(helper.padChrome$.window.location.href); + expect(results[0][1]) + .to.be('htmltext
          newline

          '); + expect(results[1][1]).to.be('htmltext\nnewline\n\n'); + done(); + }); + xit('import a pad with bullets from html', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const htmlWithBullets = '
          • bullet line 1
          • ' + + '
          • bullet line 2
            • bullet2 line 1
            • ' + + '
            • bullet2 line 2
          '; + importrequest(htmlWithBullets, importurl, 'html'); + helper.waitFor(() => expect(getinnertext()).to.be( + '
          • bullet line 1
          \n' + + '
          • bullet line 2
          \n' + + '
          • bullet2 line 1
          \n' + + '
          • bullet2 line 2
          \n' + + '
          \n')); + const results = exportfunc(helper.padChrome$.window.location.href); + expect(results[0][1]).to.be( + '
          • bullet line 1
          • bullet line 2
          • ' + + '
            • bullet2 line 1
            • bullet2 line 2

          '); + expect(results[1][1]) + .to.be('\t* bullet line 1\n\t* bullet line 2\n' + + '\t\t* bullet2 line 1\n\t\t* bullet2 line 2\n\n'); + done(); + }); + xit('import a pad with bullets and newlines from html', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const htmlWithBullets = '
          • bullet line 1
          • ' + + '

          • bullet line 2
            • ' + + '
            • bullet2 line 1

            ' + + '
            • bullet2 line 2
          '; + importrequest(htmlWithBullets, importurl, 'html'); + helper.waitFor(() => expect(getinnertext()).to.be( + '
          • bullet line 1
          \n' + + '
          \n' + + '
          • bullet line 2
          \n' + + '
          • bullet2 line 1
          \n' + + '
          \n' + + '
          • bullet2 line 2
          \n' + + '
          \n')); + const results = exportfunc(helper.padChrome$.window.location.href); + expect(results[0][1]).to.be( + '
          • bullet line 1

            ' + + '
          • bullet line 2
            • bullet2 line 1
            ' + + '

            • bullet2 line 2

          '); + expect(results[1][1]).to.be( + '\t* bullet line 1\n\n\t* bullet line 2\n\t\t* bullet2 line 1\n\n\t\t* bullet2 line 2\n\n'); + done(); + }); + xit('import a pad with bullets and newlines and attributes from html', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const htmlWithBullets = '
          • bullet line 1
          • ' + + '

          • bullet line 2
          • ' + + '
            • bullet2 line 1
          ' + + '
                ' + + '
                • ' + + 'bullet4 line 2 bisu
                • ' + + 'bullet4 line 2 bs
                • ' + + '
                • bullet4 line 2 u' + + 'uis
          '; + importrequest(htmlWithBullets, importurl, 'html'); + helper.waitFor(() => expect(getinnertext()).to.be( + '
          • bullet line 1
          \n
          \n' + + '
          • bullet line 2
          \n' + + '
          • bullet2 line 1
          \n
          \n' + + '
          • ' + + 'bullet4 line 2 bisu
          \n' + + '
          • ' + + 'bullet4 line 2 bs
          \n' + + '
          • bullet4 line 2 u' + + 'uis
          \n' + + '
          \n')); + const results = exportfunc(helper.padChrome$.window.location.href); + expect(results[0][1]).to.be( + '
          • bullet line 1
          ' + + '
          • bullet line 2
            • bullet2 line 1
            • ' + + '

                • bullet4 line 2 bisu' + + '
                • bullet4 line 2 bs' + + '
                • bullet4 line 2 uuis

          '); + expect(results[1][1]).to.be( + '\t* bullet line 1\n\n\t* bullet line 2\n\t\t* bullet2 line 1\n\n\t\t\t\t* bullet4 line 2' + + ' bisu\n\t\t\t\t* bullet4 line 2 bs\n\t\t\t\t* bullet4 line 2 uuis\n\n'); + done(); + }); + xit('import a pad with nested bullets from html', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const htmlWithBullets = '
          • bullet line 1
          • ' + + '
          • bullet line 2
            • ' + + '
            • bullet2 line 1
              ' + + '
                • bullet4 line 2
                • ' + + '
                • bullet4 line 2
                • bullet4 line 2
              • bullet3 line 1
              ' + + '
          • bullet2 line 1
          '; + importrequest(htmlWithBullets, importurl, 'html'); + const oldtext = getinnertext(); + helper.waitFor(() => oldtext !== getinnertext() + // return expect(getinnertext()).to.be('\ + //
          • bullet line 1
          \n\ + //
          • bullet line 2
          \n\ + //
          • bullet2 line 1
          \n\ + //
          • bullet4 line 2
          \n\ + //
          • bullet4 line 2
          \n\ + //
          • bullet4 line 2
          \n\ + //
          \n') + ); + + const results = exportfunc(helper.padChrome$.window.location.href); + expect(results[0][1]).to.be( + '
          • bullet line 1
          • bullet line 2
          • ' + + '
            • bullet2 line 1
                • bullet4 line 2
                • ' + + '
                • bullet4 line 2
                • bullet4 line 2
              • bullet3 line 1
            ' + + '
          • bullet2 line 1

          '); + expect(results[1][1]).to.be( + '\t* bullet line 1\n\t* bullet line 2\n\t\t* bullet2 line 1\n\t\t\t\t* bullet4 line 2' + + '\n\t\t\t\t* bullet4 line 2\n\t\t\t\t* bullet4 line 2\n\t\t\t* bullet3 line 1' + + '\n\t* bullet2 line 1\n\n'); + done(); + }); + xit('import with 8 levels of bullets and newlines and attributes from html', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const htmlWithBullets = + '
          • bullet line 1
          • ' + + '

          • bullet line 2
            • ' + + 'bullet2 line 1

              ' + + '
                • ' + + 'bullet4 line 2 bisu
                • ' + + 'bullet4 line 2 bs
                • bullet4 line 2 u' + + 'uis
                • ' + + '
                        ' + + '
                        • foo
                        • ' + + 'foobar bs
                    ' + + '
                  • foobar
            '; + importrequest(htmlWithBullets, importurl, 'html'); + helper.waitFor(() => expect(getinnertext()).to.be( + '
            • bullet line 1
            \n
            \n' + + '
            • bullet line 2
            \n' + + '
            • bullet2 line 1
            \n
            \n' + + '
            • bullet4 line 2 bisu' + + '
            \n' + + '
            • bullet4 line 2 bs' + + '
            \n' + + '
            • bullet4 line 2 u' + + 'uis' + + '
            \n' + + '
            • foo
            \n' + + '
            • foobar bs' + + '
            \n' + + '
            • foobar
            \n' + + '
            \n')); + const results = exportfunc(helper.padChrome$.window.location.href); + expect(results[0][1]).to.be( + '
            • bullet line 1

              ' + + '
            • bullet line 2
              • bullet2 line 1
            ' + + '
                  • ' + + 'bullet4 line 2 bisu
                  • ' + + 'bullet4 line 2 bs
                  • bullet4 line 2 u' + + 'uis
                          • foo
                          • ' + + '
                          • foobar bs
                    • foobar
                    • ' + + '

            '); + expect(results[1][1]).to.be( + '\t* bullet line 1\n\n\t* bullet line 2\n\t\t* ' + + 'bullet2 line 1\n\n\t\t\t\t* bullet4 line 2 bisu\n\t\t\t\t* bullet4 line 2 ' + + 'bs\n\t\t\t\t* bullet4 line 2 uuis\n\t\t\t\t\t\t\t\t* foo\n\t\t\t\t\t\t\t\t* ' + + 'foobar bs\n\t\t\t\t\t* foobar\n\n'); + done(); + }); + + xit('import a pad with ordered lists from html', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const htmlWithBullets = '
              ' + + '
            1. number 1 line 1
              ' + + '
            1. number 2 line 2
            '; + importrequest(htmlWithBullets, importurl, 'html'); + console.error(getinnertext()); + expect(getinnertext()).to.be( + '
            1. number 1 line 1
            \n' + + '
            1. number 2 line 2
            \n' + + '
            \n'); + const results = exportfunc(helper.padChrome$.window.location.href); + expect(results[0][1]).to.be( + '
            1. number 1 line 1
            2. ' + + '
            1. number 2 line 2
            '); + expect(results[1][1]).to.be(''); + done(); + }); + xit('import a pad with ordered lists and newlines from html', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const htmlWithBullets = '
              ' + + '
            1. number 9 line 1

              ' + + '
            1. number 10 line 2
              1. ' + + '
              2. number 2 times line 1

              ' + + '
              1. number 2 times line 2
            '; + importrequest(htmlWithBullets, importurl, 'html'); + expect(getinnertext()).to.be( + '
            1. number 9 line 1
            \n' + + '
            \n' + + '
            1. number 10 line 2
            2. ' + + '
            \n' + + '
            1. number 2 times line 1
            \n' + + '
            \n' + + '
            1. number 2 times line 2
            \n' + + '
            \n'); + const results = exportfunc(helper.padChrome$.window.location.href); + console.error(results); + done(); + }); + xit('import with nested ordered lists and attributes and newlines from html', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const htmlWithBullets = '
            1. ' + + 'bold strikethrough italics underline' + + ' line 1bold
            2. ' + + '

              ' + + '
            1. number 10 line 2
              1. ' + + '
              2. number 2 times line 1

            ' + + '
                ' + + '
              1. number 2 times line 2
            '; + importrequest(htmlWithBullets, importurl, 'html'); + expect(getinnertext()).to.be( + '
            1. ' + + 'bold strikethrough italics underline' + + ' line 1bold
            \n' + + '
            \n' + + '
            1. number 10 line 2
            \n' + + '
            1. ' + + 'number 2 times line 1
            \n' + + '
            \n' + + '
            1. number 2 times line 2
            \n' + + '
            \n'); + const results = exportfunc(helper.padChrome$.window.location.href); + console.error(results); + done(); + }); +}); diff --git a/tests/frontend/specs/importindents.js b/src/tests/frontend/specs/importindents.js similarity index 62% rename from tests/frontend/specs/importindents.js rename to src/tests/frontend/specs/importindents.js index 6209236df..eecbbce59 100644 --- a/tests/frontend/specs/importindents.js +++ b/src/tests/frontend/specs/importindents.js @@ -1,3 +1,5 @@ +'use strict'; + describe('import indents functionality', function () { beforeEach(function (cb) { helper.newPad(cb); // creates a new pad @@ -13,7 +15,6 @@ describe('import indents functionality', function () { return newtext; } function importrequest(data, importurl, type) { - let success; let error; const result = $.ajax({ url: importurl, @@ -24,7 +25,17 @@ describe('import indents functionality', function () { accepts: { text: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', }, - data: `Content-Type: multipart/form-data; boundary=--boundary\r\n\r\n--boundary\r\nContent-Disposition: form-data; name="file"; filename="import.${type}"\r\nContent-Type: text/plain\r\n\r\n${data}\r\n\r\n--boundary`, + data: [ + 'Content-Type: multipart/form-data; boundary=--boundary', + '', + '--boundary', + `Content-Disposition: form-data; name="file"; filename="import.${type}"`, + 'Content-Type: text/plain', + '', + data, + '', + '--boundary', + ].join('\r\n'), error(res) { error = res; }, @@ -51,54 +62,67 @@ describe('import indents functionality', function () { xit('import a pad with indents from html', function (done) { const importurl = `${helper.padChrome$.window.location.href}/import`; + /* eslint-disable-next-line max-len */ const htmlWithIndents = '
            • indent line 1
            • indent line 2
              • indent2 line 1
              • indent2 line 2
            '; importrequest(htmlWithIndents, importurl, 'html'); - helper.waitFor(() => expect(getinnertext()).to.be('\ -
            • indent line 1
            \n\ -
            • indent line 2
            \n\ -
            • indent2 line 1
            \n\ -
            • indent2 line 2
            \n\ -
            \n')); + helper.waitFor(() => expect(getinnertext()).to.be( + '
            • indent line 1
            \n' + + '
            • indent line 2
            \n' + + '
            • indent2 line 1
            \n' + + '
            • indent2 line 2
            \n' + + '
            \n')); const results = exportfunc(helper.padChrome$.window.location.href); + /* eslint-disable-next-line max-len */ expect(results[0][1]).to.be('
            • indent line 1
            • indent line 2
              • indent2 line 1
              • indent2 line 2

            '); - expect(results[1][1]).to.be('\tindent line 1\n\tindent line 2\n\t\tindent2 line 1\n\t\tindent2 line 2\n\n'); + expect(results[1][1]) + .to.be('\tindent line 1\n\tindent line 2\n\t\tindent2 line 1\n\t\tindent2 line 2\n\n'); done(); }); xit('import a pad with indented lists and newlines from html', function (done) { const importurl = `${helper.padChrome$.window.location.href}/import`; + /* eslint-disable-next-line max-len */ const htmlWithIndents = '
            • indent line 1

            • indent 1 line 2
              • indent 2 times line 1

              • indent 2 times line 2
            '; importrequest(htmlWithIndents, importurl, 'html'); - helper.waitFor(() => expect(getinnertext()).to.be('\ -
            • indent line 1
            \n\ -
            \n\ -
            • indent 1 line 2
            \n\ -
            • indent 2 times line 1
            \n\ -
            \n\ -
            • indent 2 times line 2
            \n\ -
            \n')); + helper.waitFor(() => expect(getinnertext()).to.be( + '
            • indent line 1
            \n' + + '
            \n' + + '
            • indent 1 line 2
            \n' + + '
            • indent 2 times line 1
            \n' + + '
            \n' + + '
            • indent 2 times line 2
            \n' + + '
            \n')); const results = exportfunc(helper.padChrome$.window.location.href); + /* eslint-disable-next-line max-len */ expect(results[0][1]).to.be('
            • indent line 1

            • indent 1 line 2
              • indent 2 times line 1

              • indent 2 times line 2

            '); + /* eslint-disable-next-line max-len */ expect(results[1][1]).to.be('\tindent line 1\n\n\tindent 1 line 2\n\t\tindent 2 times line 1\n\n\t\tindent 2 times line 2\n\n'); done(); }); - xit('import a pad with 8 levels of indents and newlines and attributes from html', function (done) { + xit('import with 8 levels of indents and newlines and attributes from html', function (done) { const importurl = `${helper.padChrome$.window.location.href}/import`; + /* eslint-disable-next-line max-len */ const htmlWithIndents = '
            • indent line 1

            • indent line 2
              • indent2 line 1

                  • indent4 line 2 bisu
                  • indent4 line 2 bs
                  • indent4 line 2 uuis
                          • foo
                          • foobar bs
                    • foobar
              '; importrequest(htmlWithIndents, importurl, 'html'); - helper.waitFor(() => expect(getinnertext()).to.be('\ -
              • indent line 1
              \n\
              \n\ -
              • indent line 2
              \n\ -
              • indent2 line 1
              \n
              \n\ -
              • indent4 line 2 bisu
              \n\ -
              • indent4 line 2 bs
              \n\ -
              • indent4 line 2 uuis
              \n\ -
              • foo
              \n\ -
              • foobar bs
              \n\ -
              • foobar
              \n\ -
              \n')); + helper.waitFor(() => expect(getinnertext()).to.be( + '
              • indent line 1
              \n
              \n' + + '
              • indent line 2
              \n' + + '
              • indent2 line 1
              \n
              \n' + + '
              • indent4 ' + + 'line 2 bisu
              \n' + + '
              • ' + + 'indent4 line 2 bs
              \n' + + '
              • indent4 line 2 u' + + 'uis
              \n' + + '
              • foo
              \n' + + '
              • foobar bs' + + '
              \n' + + '
              • foobar
              \n' + + '
              \n')); const results = exportfunc(helper.padChrome$.window.location.href); + /* eslint-disable-next-line max-len */ expect(results[0][1]).to.be('
              • indent line 1

              • indent line 2
                • indent2 line 1

                    • indent4 line 2 bisu
                    • indent4 line 2 bs
                    • indent4 line 2 uuis
                            • foo
                            • foobar bs
                      • foobar

              '); + /* eslint-disable-next-line max-len */ expect(results[1][1]).to.be('\tindent line 1\n\n\tindent line 2\n\t\tindent2 line 1\n\n\t\t\t\tindent4 line 2 bisu\n\t\t\t\tindent4 line 2 bs\n\t\t\t\tindent4 line 2 uuis\n\t\t\t\t\t\t\t\tfoo\n\t\t\t\t\t\t\t\tfoobar bs\n\t\t\t\t\tfoobar\n\n'); done(); }); diff --git a/tests/frontend/specs/indentation.js b/src/tests/frontend/specs/indentation.js similarity index 78% rename from tests/frontend/specs/indentation.js rename to src/tests/frontend/specs/indentation.js index f35b7ca00..377b1af74 100644 --- a/tests/frontend/specs/indentation.js +++ b/src/tests/frontend/specs/indentation.js @@ -1,3 +1,5 @@ +'use strict'; + describe('indentation button', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -6,8 +8,8 @@ describe('indentation button', function () { }); it('indent text with keypress', function (done) { + this.timeout(100); const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; // get the first text element out of the inner iframe const $firstTextElement = inner$('div').first(); @@ -15,7 +17,7 @@ describe('indentation button', function () { // select this text element $firstTextElement.sendkeys('{selectall}'); - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.keyCode = 9; // tab :| inner$('#innerdocbody').trigger(e); @@ -23,6 +25,7 @@ describe('indentation button', function () { }); it('indent text with button', function (done) { + this.timeout(100); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; @@ -33,6 +36,7 @@ describe('indentation button', function () { }); it('keeps the indent on enter for the new line', function (done) { + this.timeout(1200); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; @@ -56,9 +60,10 @@ describe('indentation button', function () { }); }); - it("indents text with spaces on enter if previous line ends with ':', '[', '(', or '{'", function (done) { + it('indents text with spaces on enter if previous line ends ' + + "with ':', '[', '(', or '{'", function (done) { + this.timeout(1200); const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; // type a bit, make a line break and type again const $firstTextElement = inner$('div').first(); @@ -77,7 +82,8 @@ describe('indentation button', function () { // curly braces const $lineWithCurlyBraces = inner$('div').first().next().next().next(); $lineWithCurlyBraces.sendkeys('{{}'); - pressEnter(); // cannot use sendkeys('{enter}') here, browser does not read the command properly + // 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 @@ -106,9 +112,10 @@ describe('indentation button', function () { }); }); - it("appends indentation to the indent of previous line if previous line ends with ':', '[', '(', or '{'", function (done) { + it('appends indentation to the indent of previous line if previous line ends ' + + "with ':', '[', '(', or '{'", function (done) { + this.timeout(1200); const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; // type a bit, make a line break and type again const $firstTextElement = inner$('div').first(); @@ -124,47 +131,48 @@ describe('indentation button', function () { $lineWithColon.sendkeys(':'); pressEnter(); const $lineAfterColon = inner$('div').first().next(); - expect($lineAfterColon.text()).to.match(/\s{6}/); // previous line indentation + regular tab (4 spaces) + // previous line indentation + regular tab (4 spaces) + expect($lineAfterColon.text()).to.match(/\s{6}/); done(); }); }); - it("issue #2772 shows '*' when multiple indented lines receive a style and are outdented", function (done) { + 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}'); - helper.waitFor(() => inner$('div').first().text().trim() === 'First').done(() => { - // indent first 2 lines - const $lines = inner$('div'); - const $firstLine = $lines.first(); - const $secondLine = $lines.slice(1, 2); - helper.selectLines($firstLine, $secondLine); + await helper.waitForPromise(() => inner$('div').first().text().trim() === 'First'); - const $indentButton = chrome$('.buttonicon-indent'); - $indentButton.click(); + // indent first 2 lines + const $lines = inner$('div'); + const $firstLine = $lines.first(); + let $secondLine = $lines.slice(1, 2); + helper.selectLines($firstLine, $secondLine); - helper.waitFor(() => inner$('div').first().find('ul li').length === 1).done(() => { - // apply bold - const $boldButton = chrome$('.buttonicon-bold'); - $boldButton.click(); + const $indentButton = chrome$('.buttonicon-indent'); + $indentButton.click(); - helper.waitFor(() => inner$('div').first().find('b').length === 1).done(() => { - // outdent first 2 lines - const $outdentButton = chrome$('.buttonicon-outdent'); - $outdentButton.click(); - helper.waitFor(() => inner$('div').first().find('ul li').length === 0).done(() => { - // check if '*' is displayed - const $secondLine = inner$('div').slice(1, 2); - expect($secondLine.text().trim()).to.be('Second'); + await helper.waitForPromise(() => inner$('div').first().find('ul li').length === 1); - done(); - }); - }); - }); - }); + // 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'); }); /* @@ -184,7 +192,6 @@ describe('indentation button', function () { var $indentButton = testHelper.$getPadChrome().find(".buttonicon-indent"); $indentButton.click(); - //ace creates a new dom element when you press a button, so just get the first text element again var newFirstTextElement = $inner.find("div").first(); // is there a list-indent class element now? @@ -222,7 +229,6 @@ describe('indentation button', function () { $outdentButton.click(); $outdentButton.click(); - //ace creates a new dom element when you press a button, so just get the first text element again var newFirstTextElement = $inner.find("div").first(); // is there a list-indent class element now? @@ -269,7 +275,9 @@ describe('indentation button', function () { //get the second text element out of the inner iframe setTimeout(function(){ // THIS IS REALLY BAD - var secondTextElement = $('iframe').contents().find('iframe').contents().find('iframe').contents().find('body > div').get(1); // THIS IS UGLY + var 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? var firstChild = secondTextElement.children(":first"); @@ -284,7 +292,10 @@ describe('indentation button', function () { expect(isLI).to.be(true); //get the first text element out of the inner iframe - var thirdTextElement = $('iframe').contents().find('iframe').contents().find('iframe').contents().find('body > div').get(2); // THIS IS UGLY TOO + var 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? var firstChild = thirdTextElement.children(":first"); @@ -302,9 +313,9 @@ describe('indentation button', function () { });*/ }); -function pressEnter() { +const pressEnter = () => { const inner$ = helper.padInner$; - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.keyCode = 13; // enter :| inner$('#innerdocbody').trigger(e); -} +}; diff --git a/tests/frontend/specs/italic.js b/src/tests/frontend/specs/italic.js similarity index 84% rename from tests/frontend/specs/italic.js rename to src/tests/frontend/specs/italic.js index 3660f71f3..e7d551c8e 100644 --- a/tests/frontend/specs/italic.js +++ b/src/tests/frontend/specs/italic.js @@ -1,3 +1,5 @@ +'use strict'; + describe('italic some text', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -6,6 +8,7 @@ describe('italic some text', function () { }); it('makes text italic using button', function (done) { + this.timeout(100); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; @@ -19,7 +22,7 @@ describe('italic some text', function () { const $boldButton = chrome$('.buttonicon-italic'); $boldButton.click(); - // ace creates a new dom element when you press a button, so just get the first text element again + // 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? @@ -35,8 +38,8 @@ describe('italic some text', function () { }); it('makes text italic using keypress', function (done) { + this.timeout(100); const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; // get the first text element out of the inner iframe const $firstTextElement = inner$('div').first(); @@ -44,12 +47,12 @@ describe('italic some text', function () { // select this text element $firstTextElement.sendkeys('{selectall}'); - const e = inner$.Event(helper.evtType); + 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, so just get the first text element again + // 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? diff --git a/tests/frontend/specs/language.js b/src/tests/frontend/specs/language.js similarity index 90% rename from tests/frontend/specs/language.js rename to src/tests/frontend/specs/language.js index d29b2407e..e5240aaab 100644 --- a/tests/frontend/specs/language.js +++ b/src/tests/frontend/specs/language.js @@ -1,6 +1,8 @@ -function deletecookie(name) { +'use strict'; + +const deletecookie = (name) => { document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:01 GMT;`; -} +}; describe('Language select and change', function () { // Destroy language cookies @@ -14,7 +16,7 @@ describe('Language select and change', function () { // Destroy language cookies it('makes text german', function (done) { - const inner$ = helper.padInner$; + this.timeout(1000); const chrome$ = helper.padChrome$; // click on the settings button to make settings visible @@ -29,7 +31,7 @@ describe('Language select and change', function () { $languageoption.attr('selected', 'selected'); $language.change(); - helper.waitFor(() => chrome$('.buttonicon-bold').parent()[0].title == 'Fett (Strg-B)') + helper.waitFor(() => chrome$('.buttonicon-bold').parent()[0].title === 'Fett (Strg-B)') .done(() => { // get the value of the bold button const $boldButton = chrome$('.buttonicon-bold').parent(); @@ -44,7 +46,7 @@ describe('Language select and change', function () { }); it('makes text English', function (done) { - const inner$ = helper.padInner$; + this.timeout(1000); const chrome$ = helper.padChrome$; // click on the settings button to make settings visible @@ -60,7 +62,7 @@ describe('Language select and change', function () { // get the value of the bold button const $boldButton = chrome$('.buttonicon-bold').parent(); - helper.waitFor(() => $boldButton[0].title != 'Fett (Strg+B)') + helper.waitFor(() => $boldButton[0].title !== 'Fett (Strg+B)') .done(() => { // get the value of the bold button const $boldButton = chrome$('.buttonicon-bold').parent(); @@ -75,7 +77,7 @@ describe('Language select and change', function () { }); it('changes direction when picking an rtl lang', function (done) { - const inner$ = helper.padInner$; + this.timeout(1000); const chrome$ = helper.padChrome$; // click on the settings button to make settings visible @@ -91,7 +93,7 @@ describe('Language select and change', function () { $language.val('ar'); $languageoption.change(); - helper.waitFor(() => chrome$('html')[0].dir != 'ltr') + helper.waitFor(() => chrome$('html')[0].dir !== 'ltr') .done(() => { // check if the document's direction was changed expect(chrome$('html')[0].dir).to.be('rtl'); @@ -100,7 +102,6 @@ describe('Language select and change', function () { }); it('changes direction when picking an ltr lang', function (done) { - const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; // click on the settings button to make settings visible @@ -117,7 +118,7 @@ describe('Language select and change', function () { $language.val('en'); $languageoption.change(); - helper.waitFor(() => chrome$('html')[0].dir != 'rtl') + helper.waitFor(() => chrome$('html')[0].dir !== 'rtl') .done(() => { // check if the document's direction was changed expect(chrome$('html')[0].dir).to.be('ltr'); diff --git a/tests/frontend/specs/multiple_authors_clear_authorship_colors.js b/src/tests/frontend/specs/multiple_authors_clear_authorship_colors.js similarity index 87% rename from tests/frontend/specs/multiple_authors_clear_authorship_colors.js rename to src/tests/frontend/specs/multiple_authors_clear_authorship_colors.js index f532ea4be..818dd161f 100755 --- a/tests/frontend/specs/multiple_authors_clear_authorship_colors.js +++ b/src/tests/frontend/specs/multiple_authors_clear_authorship_colors.js @@ -1,7 +1,9 @@ +'use strict'; + describe('author of pad edition', function () { // author 1 creates a new pad with some content (regular lines and lists) before(function (done) { - var padId = helper.newPad(() => { + const padId = helper.newPad(() => { // make sure pad has at least 3 lines const $firstLine = helper.padInner$('div').first(); $firstLine.html('Hello World'); @@ -13,7 +15,8 @@ describe('author of pad edition', function () { setTimeout(() => { // Expire cookie, so author is changed after reloading the pad. // See https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#Example_4_Reset_the_previous_cookie - helper.padChrome$.document.cookie = 'token=foo;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; + helper.padChrome$.document.cookie = + 'token=foo;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; helper.newPad(done, padId); }, 1000); @@ -24,10 +27,11 @@ describe('author of pad edition', function () { // author 2 makes some changes on the pad it('Clears Authorship by second user', function (done) { + this.timeout(100); clearAuthorship(done); }); - var clearAuthorship = function (done) { + const clearAuthorship = (done) => { const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; diff --git a/src/tests/frontend/specs/ordered_list.js b/src/tests/frontend/specs/ordered_list.js new file mode 100644 index 000000000..a7403d6bc --- /dev/null +++ b/src/tests/frontend/specs/ordered_list.js @@ -0,0 +1,243 @@ +'use strict'; + +describe('ordered_list.js', function () { + describe('assign ordered list', function () { + // create a new pad before each test run + beforeEach(function (cb) { + helper.newPad(cb); + this.timeout(60000); + }); + + it('inserts ordered list text', function (done) { + this.timeout(200); + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; + + const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist'); + $insertorderedlistButton.click(); + + helper.waitFor(() => inner$('div').first().find('ol li').length === 1).done(done); + }); + + 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', function (done) { + this.timeout(50); + helper.waitFor(() => helper.padInner$('div').first().find('ol li').length === 1) + .done(done); + }); + }); + + 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', function (done) { + this.timeout(3000); + helper.waitFor(() => helper.padInner$('div').first().find('ol li').length === 1) + .done(() => { + expect().fail(() => 'Unordered list inserted, should ignore shortcut'); + }) + .fail(() => { + done(); + }); + }); + }); + }); + + 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', function (done) { + this.timeout(200); + helper.waitFor(() => helper.padInner$('div').first().find('ol li').length === 1) + .done(done); + }); + }); + + 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', function (done) { + this.timeout(3000); + helper.waitFor(() => helper.padInner$('div').first().find('ol li').length === 1) + .done(() => { + expect().fail(() => 'Unordered list inserted, should ignore shortcut'); + }) + .fail(() => { + done(); + }); + }); + }); + }); + + it('issue #4748 keeps numbers increment on OL', function (done) { + 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); + done(); + }); + + xit('issue #1125 keeps the numbered list on enter for the new line', function (done) { + // 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}'); + + helper.waitFor(() => inner$('div span').first().text().indexOf('line 2') === -1).done(() => { + 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); + done(); + }); + }); + + 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(function (cb) { + helper.newPad(cb); + this.timeout(60000); + }); + + it('indent and de-indent list item with keypress', function (done) { + this.timeout(200); + 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); + + helper.waitFor(() => inner$('div').first().find('.list-number1').length === 1).done(done); + }); + }); + + + 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(function (cb) { + helper.newPad(cb); + this.timeout(60000); + }); + + it('indent and de-indent list item with indent button', function (done) { + 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 + + helper.waitFor(() => inner$('div').first().find('.list-number1').length === 1).done(done); + }); + }); +}); diff --git a/tests/frontend/specs/pad_modal.js b/src/tests/frontend/specs/pad_modal.js similarity index 82% rename from tests/frontend/specs/pad_modal.js rename to src/tests/frontend/specs/pad_modal.js index 1711e38b8..5c3b9d5c4 100644 --- a/tests/frontend/specs/pad_modal.js +++ b/src/tests/frontend/specs/pad_modal.js @@ -1,3 +1,5 @@ +'use strict'; + describe('Pad modal', function () { context('when modal is a "force reconnect" message', function () { const MODAL_SELECTOR = '#connectivity'; @@ -16,17 +18,16 @@ describe('Pad modal', function () { }); it('disables editor', function (done) { + this.timeout(200); expect(isEditorDisabled()).to.be(true); done(); }); context('and user clicks on editor', function () { - beforeEach(function () { - clickOnPadInner(); - }); - it('does not close the modal', function (done) { + this.timeout(200); + clickOnPadInner(); const $modal = helper.padChrome$(MODAL_SELECTOR); const modalIsVisible = $modal.hasClass('popup-show'); @@ -37,11 +38,9 @@ describe('Pad modal', function () { }); context('and user clicks on pad outer', function () { - beforeEach(function () { - clickOnPadOuter(); - }); - it('does not close the modal', function (done) { + this.timeout(200); + clickOnPadOuter(); const $modal = helper.padChrome$(MODAL_SELECTOR); const modalIsVisible = $modal.hasClass('popup-show'); @@ -71,39 +70,33 @@ describe('Pad modal', function () { }); */ context('and user clicks on editor', function () { - beforeEach(function () { + it('closes the modal', async function () { + this.timeout(200); clickOnPadInner(); - }); - - it('closes the modal', function (done) { - expect(isModalOpened(MODAL_SELECTOR)).to.be(false); - done(); + await helper.waitForPromise(() => isModalOpened(MODAL_SELECTOR) === false); }); }); context('and user clicks on pad outer', function () { - beforeEach(function () { + it('closes the modal', async function () { + this.timeout(200); clickOnPadOuter(); - }); - - it('closes the modal', function (done) { - expect(isModalOpened(MODAL_SELECTOR)).to.be(false); - done(); + await helper.waitForPromise(() => isModalOpened(MODAL_SELECTOR) === false); }); }); }); - var clickOnPadInner = function () { + const clickOnPadInner = () => { const $editor = helper.padInner$('#innerdocbody'); $editor.click(); }; - var clickOnPadOuter = function () { + const clickOnPadOuter = () => { const $lineNumbersColumn = helper.padOuter$('#sidedivinner'); $lineNumbersColumn.click(); }; - var openSettingsAndWaitForModalToBeVisible = function (done) { + const openSettingsAndWaitForModalToBeVisible = (done) => { helper.padChrome$('.buttonicon-settings').click(); // wait for modal to be displayed @@ -111,7 +104,7 @@ describe('Pad modal', function () { helper.waitFor(() => isModalOpened(modalSelector), 10000).done(done); }; - var isEditorDisabled = function () { + const isEditorDisabled = () => { const editorDocument = helper.padOuter$("iframe[name='ace_inner']").get(0).contentDocument; const editorBody = editorDocument.getElementById('innerdocbody'); @@ -121,7 +114,7 @@ describe('Pad modal', function () { return editorIsDisabled; }; - var isModalOpened = function (modalSelector) { + const isModalOpened = (modalSelector) => { const $modal = helper.padChrome$(modalSelector); return $modal.hasClass('popup-show'); diff --git a/tests/frontend/specs/redo.js b/src/tests/frontend/specs/redo.js similarity index 93% rename from tests/frontend/specs/redo.js rename to src/tests/frontend/specs/redo.js index 58d5b6c12..67d49c659 100644 --- a/tests/frontend/specs/redo.js +++ b/src/tests/frontend/specs/redo.js @@ -1,3 +1,5 @@ +'use strict'; + describe('undo button then redo button', function () { beforeEach(function (cb) { helper.newPad(cb); // creates a new pad @@ -5,6 +7,7 @@ describe('undo button then redo button', function () { }); it('redo some typing with button', function (done) { + this.timeout(200); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; @@ -32,8 +35,8 @@ describe('undo button then redo button', function () { }); it('redo some typing with keypress', function (done) { + this.timeout(200); const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; // get the first text element inside the editable space const $firstTextElement = inner$('div span').first(); @@ -44,12 +47,12 @@ describe('undo button then redo button', function () { const modifiedValue = $firstTextElement.text(); // get the modified value expect(modifiedValue).not.to.be(originalValue); // expect the value to change - var e = inner$.Event(helper.evtType); + let e = new inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = 90; // z inner$('#innerdocbody').trigger(e); - var e = inner$.Event(helper.evtType); + e = new inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = 121; // y inner$('#innerdocbody').trigger(e); diff --git a/tests/frontend/specs/responsiveness.js b/src/tests/frontend/specs/responsiveness.js similarity index 78% rename from tests/frontend/specs/responsiveness.js rename to src/tests/frontend/specs/responsiveness.js index 63803f641..ec63faa10 100644 --- a/tests/frontend/specs/responsiveness.js +++ b/src/tests/frontend/specs/responsiveness.js @@ -1,14 +1,19 @@ +'use strict'; + // Test for https://github.com/ether/etherpad-lite/issues/1763 // This test fails in Opera, IE and Safari -// Opera fails due to a weird way of handling the order of execution, yet actual performance seems fine +// Opera fails due to a weird way of handling the order of execution, +// yet actual performance seems fine // Safari fails due the delay being too great yet the actual performance seems fine // Firefox might panic that the script is taking too long so will fail // IE will fail due to running out of memory as it can't fit 2M chars in memory. -// Just FYI Google Docs crashes on large docs whilst trying to Save, it's likely the limitations we are +// Just FYI Google Docs crashes on large docs whilst trying to Save, +// it's likely the limitations we are // experiencing are more to do with browser limitations than improper implementation. -// A ueber fix for this would be to have a separate lower cpu priority thread that handles operations that aren't +// A ueber fix for this would be to have a separate lower cpu priority +// thread that handles operations that aren't // visible to the user. // Adapted from John McLear's original test case. @@ -20,16 +25,18 @@ xdescribe('Responsiveness of Editor', function () { this.timeout(6000); }); // JM commented out on 8th Sep 2020 for a release, after release this needs uncommenting - // And the test needs to be fixed to work in Firefox 52 on Windows 7. I am not sure why it fails on this specific platform - // The errors show this.timeout... then crash the browser but I am sure something is actually causing the stack trace and + // And the test needs to be fixed to work in Firefox 52 on Windows 7. + // I am not sure why it fails on this specific platform + // The errors show this.timeout... then crash the browser but + // I am sure something is actually causing the stack trace and // I just need to narrow down what, offers to help accepted. it('Fast response to keypress in pad with large amount of contents', function (done) { // skip on Windows Firefox 52.0 - if (window.bowser && window.bowser.windows && window.bowser.firefox && window.bowser.version == '52.0') { + if (window.bowser && + window.bowser.windows && window.bowser.firefox && window.bowser.version === '52.0') { this.skip(); } const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; const chars = '0000000000'; // row of placeholder chars const amount = 200000; // number of blocks of chars we will insert const length = (amount * (chars.length) + 1); // include a counter for each space @@ -39,7 +46,7 @@ xdescribe('Responsiveness of Editor', function () { // get keys to send const keyMultiplier = 10; // multiplier * 10 == total number of key events let keysToSend = ''; - for (var i = 0; i <= keyMultiplier; i++) { + for (let i = 0; i <= keyMultiplier; i++) { keysToSend += chars; } @@ -47,23 +54,23 @@ xdescribe('Responsiveness of Editor', function () { textElement.sendkeys('{selectall}'); // select all textElement.sendkeys('{del}'); // clear the pad text - for (var i = 0; i <= amount; i++) { + for (let i = 0; i <= amount; i++) { text = `${text + chars} `; // add the chars and space to the text contents } inner$('div').first().text(text); // Put the text contents into the pad - helper.waitFor(() => // Wait for the new contents to be on the pad - inner$('div').text().length > length - ).done(() => { - expect(inner$('div').text().length).to.be.greaterThan(length); // has the text changed? + // Wait for the new contents to be on the pad + helper.waitFor(() => inner$('div').text().length > length).done(() => { + // has the text changed? + expect(inner$('div').text().length).to.be.greaterThan(length); const start = Date.now(); // get the start time // send some new text to the screen (ensure all 3 key events are sent) const el = inner$('div').first(); for (let i = 0; i < keysToSend.length; ++i) { - var x = keysToSend.charCodeAt(i); + const x = keysToSend.charCodeAt(i); ['keyup', 'keypress', 'keydown'].forEach((type) => { - const e = $.Event(type); + const e = new $.Event(type); e.keyCode = x; el.trigger(e); }); diff --git a/src/tests/frontend/specs/scrollTo.js b/src/tests/frontend/specs/scrollTo.js new file mode 100755 index 000000000..20b24a56e --- /dev/null +++ b/src/tests/frontend/specs/scrollTo.js @@ -0,0 +1,45 @@ +'use strict'; + +describe('scrollTo.js', function () { + describe('scrolls to line', function () { + // create a new pad with URL hash set before each test run + before(async function () { + this.timeout(60000); + await new Promise((resolve, reject) => helper.newPad({ + cb: (err) => (err != null) ? reject(err) : resolve(), + hash: 'L4', + })); + }); + + it('Scrolls down to Line 4', async function () { + this.timeout(100); + const chrome$ = helper.padChrome$; + await helper.waitForPromise(() => { + const topOffset = parseInt(chrome$('iframe').first('iframe') + .contents().find('#outerdocbody').css('top')); + return (topOffset >= 100); + }); + }); + }); + + describe('doesnt break on weird hash input', function () { + // create a new pad with URL hash set before each test run + before(async function () { + this.timeout(60000); + await new Promise((resolve, reject) => helper.newPad({ + cb: (err) => (err != null) ? reject(err) : resolve(), + hash: '#DEEZ123123NUTS', + })); + }); + + it('Does NOT change scroll', async function () { + this.timeout(100); + const chrome$ = helper.padChrome$; + await helper.waitForPromise(() => { + const topOffset = parseInt(chrome$('iframe').first('iframe') + .contents().find('#outerdocbody').css('top')); + return (!topOffset); // no css top should be set. + }); + }); + }); +}); diff --git a/tests/frontend/specs/select_formatting_buttons.js b/src/tests/frontend/specs/select_formatting_buttons.js similarity index 77% rename from tests/frontend/specs/select_formatting_buttons.js rename to src/tests/frontend/specs/select_formatting_buttons.js index 52595a044..63be6f8e2 100644 --- a/tests/frontend/specs/select_formatting_buttons.js +++ b/src/tests/frontend/specs/select_formatting_buttons.js @@ -1,3 +1,5 @@ +'use strict'; + describe('select formatting buttons when selection has style applied', function () { const STYLES = ['italic', 'bold', 'underline', 'strikethrough']; const SHORTCUT_KEYS = ['I', 'B', 'U', '5']; // italic, bold, underline, strikethrough @@ -21,7 +23,7 @@ describe('select formatting buttons when selection has style applied', function return $formattingButton.parent().hasClass('selected'); }; - var selectLine = function (lineNumber, offsetStart, offsetEnd) { + const selectLine = function (lineNumber, offsetStart, offsetEnd) { const inner$ = helper.padInner$; const $line = inner$('div').eq(lineNumber); helper.selectLines($line, $line, offsetStart, offsetEnd); @@ -33,19 +35,23 @@ describe('select formatting buttons when selection has style applied', function $line.sendkeys('{leftarrow}'); }; - const undo = function () { + const undo = async function () { + const originalHTML = helper.padInner$('body').html(); const $undoButton = helper.padChrome$('.buttonicon-undo'); $undoButton.click(); + await helper.waitForPromise(() => helper.padInner$('body').html() !== originalHTML); }; const testIfFormattingButtonIsDeselected = function (style) { it(`deselects the ${style} button`, function (done) { + this.timeout(50); helper.waitFor(() => isButtonSelected(style) === false).done(done); }); }; const testIfFormattingButtonIsSelected = function (style) { it(`selects the ${style} button`, function (done) { + this.timeout(50); helper.waitFor(() => isButtonSelected(style)).done(done); }); }; @@ -58,7 +64,7 @@ describe('select formatting buttons when selection has style applied', function applyStyleOnLineOnFullLineAndRemoveSelection(line, style, placeCaretOnLine, cb); }; - var applyStyleOnLineOnFullLineAndRemoveSelection = function (line, style, selectTarget, cb) { + const applyStyleOnLineOnFullLineAndRemoveSelection = function (line, style, selectTarget, cb) { // see if line html has changed const inner$ = helper.padInner$; const oldLineHTML = inner$.find('div')[line]; @@ -78,9 +84,9 @@ describe('select formatting buttons when selection has style applied', function // }, 1000); }; - const pressFormattingShortcutOnSelection = function (key) { + const pressFormattingShortcutOnSelection = async function (key) { const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; + const originalHTML = helper.padInner$('body').html(); // get the first text element out of the inner iframe const $firstTextElement = inner$('div').first(); @@ -88,10 +94,11 @@ describe('select formatting buttons when selection has style applied', function // select this text element $firstTextElement.sendkeys('{selectall}'); - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = key.charCodeAt(0); // I, U, B, 5 inner$('#innerdocbody').trigger(e); + await helper.waitForPromise(() => helper.padInner$('body').html() !== originalHTML); }; STYLES.forEach((style) => { @@ -101,8 +108,8 @@ describe('select formatting buttons when selection has style applied', function applyStyleOnLineAndSelectIt(FIRST_LINE, style, done); }); - after(function () { - undo(); + after(async function () { + await undo(); }); testIfFormattingButtonIsSelected(style); @@ -114,8 +121,8 @@ describe('select formatting buttons when selection has style applied', function applyStyleOnLineAndPlaceCaretOnit(FIRST_LINE, style, done); }); - after(function () { - undo(); + after(async function () { + await undo(); }); testIfFormattingButtonIsSelected(style); @@ -123,34 +130,27 @@ describe('select formatting buttons when selection has style applied', function }); context('when user applies a style and the selection does not change', function () { - const style = STYLES[0]; // italic - before(function () { + it('selects the style button', async function () { + this.timeout(50); + const style = STYLES[0]; // italic applyStyleOnLine(style, FIRST_LINE); - }); - - // clean the style applied - after(function () { + await helper.waitForPromise(() => isButtonSelected(style) === true); applyStyleOnLine(style, FIRST_LINE); }); - - it('selects the style button', function (done) { - expect(isButtonSelected(style)).to.be(true); - done(); - }); }); SHORTCUT_KEYS.forEach((key, index) => { const styleOfTheShortcut = STYLES[index]; // italic, bold, ... context(`when user presses CMD + ${key}`, function () { - before(function () { - pressFormattingShortcutOnSelection(key); + before(async function () { + await pressFormattingShortcutOnSelection(key); }); testIfFormattingButtonIsSelected(styleOfTheShortcut); context(`and user presses CMD + ${key} again`, function () { - before(function () { - pressFormattingShortcutOnSelection(key); + before(async function () { + await pressFormattingShortcutOnSelection(key); }); testIfFormattingButtonIsDeselected(styleOfTheShortcut); diff --git a/tests/frontend/specs/strikethrough.js b/src/tests/frontend/specs/strikethrough.js similarity index 88% rename from tests/frontend/specs/strikethrough.js rename to src/tests/frontend/specs/strikethrough.js index d8feae3be..9d5461226 100644 --- a/tests/frontend/specs/strikethrough.js +++ b/src/tests/frontend/specs/strikethrough.js @@ -1,3 +1,5 @@ +'use strict'; + describe('strikethrough button', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -6,6 +8,7 @@ describe('strikethrough button', function () { }); it('makes text strikethrough', function (done) { + this.timeout(100); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; @@ -19,7 +22,7 @@ describe('strikethrough button', function () { const $strikethroughButton = chrome$('.buttonicon-strikethrough'); $strikethroughButton.click(); - // ace creates a new dom element when you press a button, so just get the first text element again + // 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? diff --git a/tests/frontend/specs/timeslider.js b/src/tests/frontend/specs/timeslider.js similarity index 97% rename from tests/frontend/specs/timeslider.js rename to src/tests/frontend/specs/timeslider.js index bea7932df..10f94b3cb 100644 --- a/tests/frontend/specs/timeslider.js +++ b/src/tests/frontend/specs/timeslider.js @@ -1,3 +1,5 @@ +'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(function (cb) { @@ -12,7 +14,6 @@ xdescribe('timeslider button takes you to the timeslider of a pad', function () // get the first text element inside the editable space const $firstTextElement = inner$('div span').first(); const originalValue = $firstTextElement.text(); // get the original value - const newValue = `Testing${originalValue}`; $firstTextElement.sendkeys('Testing'); // send line 1 to the pad const modifiedValue = $firstTextElement.text(); // get the modified value diff --git a/tests/frontend/specs/timeslider_follow.js b/src/tests/frontend/specs/timeslider_follow.js similarity index 99% rename from tests/frontend/specs/timeslider_follow.js rename to src/tests/frontend/specs/timeslider_follow.js index 9f86ddc77..eaa3cd7e7 100644 --- a/tests/frontend/specs/timeslider_follow.js +++ b/src/tests/frontend/specs/timeslider_follow.js @@ -9,6 +9,7 @@ describe('timeslider follow', function () { // 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(10000); // send 6 revisions const revs = 6; const message = 'a\n\n\n\n\n\n\n\n\n\n'; diff --git a/tests/frontend/specs/timeslider_labels.js b/src/tests/frontend/specs/timeslider_labels.js similarity index 93% rename from tests/frontend/specs/timeslider_labels.js rename to src/tests/frontend/specs/timeslider_labels.js index c7a4aca5a..08a4eb33a 100644 --- a/tests/frontend/specs/timeslider_labels.js +++ b/src/tests/frontend/specs/timeslider_labels.js @@ -1,3 +1,5 @@ +'use strict'; + describe('timeslider', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -7,7 +9,8 @@ describe('timeslider', function () { /** * @todo test authorsList */ - it("Shows a date and time in the timeslider and make sure it doesn't include NaN", async function () { + it("Shows a date/time in the timeslider and make sure it doesn't include NaN", async function () { + this.timeout(12000); // make some changes to produce 3 revisions const revs = 3; diff --git a/tests/frontend/specs/timeslider_numeric_padID.js b/src/tests/frontend/specs/timeslider_numeric_padID.js similarity index 96% rename from tests/frontend/specs/timeslider_numeric_padID.js rename to src/tests/frontend/specs/timeslider_numeric_padID.js index 53eb4a29c..019ad5307 100644 --- a/tests/frontend/specs/timeslider_numeric_padID.js +++ b/src/tests/frontend/specs/timeslider_numeric_padID.js @@ -1,3 +1,5 @@ +'use strict'; + describe('timeslider', function () { const padId = 735773577357 + (Math.round(Math.random() * 1000)); @@ -7,6 +9,7 @@ describe('timeslider', function () { }); it('Makes sure the export URIs are as expected when the padID is numeric', async function () { + this.timeout(60000); await helper.edit('a\n'); await helper.gotoTimeslider(1); diff --git a/tests/frontend/specs/timeslider_revisions.js b/src/tests/frontend/specs/timeslider_revisions.js similarity index 81% rename from tests/frontend/specs/timeslider_revisions.js rename to src/tests/frontend/specs/timeslider_revisions.js index fbfbb3615..1bae6b254 100644 --- a/tests/frontend/specs/timeslider_revisions.js +++ b/src/tests/frontend/specs/timeslider_revisions.js @@ -1,3 +1,5 @@ +'use strict'; + describe('timeslider', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -6,6 +8,7 @@ describe('timeslider', function () { }); it('loads adds a hundred revisions', function (done) { // passes + this.timeout(100000); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; @@ -23,7 +26,8 @@ describe('timeslider', function () { setTimeout(() => { // go to timeslider - $('#iframe-container iframe').attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider`); + $('#iframe-container iframe').attr('src', + `${$('#iframe-container iframe').attr('src')}/timeslider`); setTimeout(() => { const timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; @@ -66,7 +70,6 @@ describe('timeslider', function () { // Disabled as jquery trigger no longer works properly xit('changes the url when clicking on the timeslider', function (done) { const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; // make some changes to produce 7 revisions const timePerRev = 1000; @@ -81,13 +84,13 @@ describe('timeslider', function () { setTimeout(() => { // go to timeslider - $('#iframe-container iframe').attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider`); + $('#iframe-container iframe').attr('src', + `${$('#iframe-container iframe').attr('src')}/timeslider`); setTimeout(() => { const timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; const $sliderBar = timeslider$('#ui-slider-bar'); - const latestContents = timeslider$('#innerdocbody').text(); const oldUrl = $('#iframe-container iframe')[0].contentWindow.location.hash; // Click somewhere on the timeslider @@ -96,20 +99,23 @@ describe('timeslider', function () { e.clientY = e.pageY = 60; $sliderBar.trigger(e); - helper.waitFor(() => $('#iframe-container iframe')[0].contentWindow.location.hash != oldUrl, 6000).always(() => { - expect($('#iframe-container iframe')[0].contentWindow.location.hash).not.to.eql(oldUrl); - done(); - }); + helper.waitFor( + () => $('#iframe-container iframe')[0].contentWindow.location.hash !== oldUrl, 6000) + .always(() => { + expect( + $('#iframe-container iframe')[0].contentWindow.location.hash + ).not.to.eql(oldUrl); + done(); + }); }, 6000); }, revs * timePerRev); }); it('jumps to a revision given in the url', function (done) { const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; this.timeout(40000); // wait for the text to be loaded - helper.waitFor(() => inner$('body').text().length != 0, 10000).always(() => { + helper.waitFor(() => inner$('body').text().length !== 0, 10000).always(() => { const newLines = inner$('body div').length; const oldLength = inner$('body').text().length + newLines / 2; expect(oldLength).to.not.eql(0); @@ -120,22 +126,25 @@ describe('timeslider', function () { helper.waitFor(() => { // newLines takes the new lines into account which are strippen when using // inner$('body').text(), one
              is used for one line in ACE. - const lenOkay = inner$('body').text().length + newLines / 2 != oldLength; + const lenOkay = inner$('body').text().length + newLines / 2 !== oldLength; // this waits for the color to be added to our , which means that the revision // was accepted by the server. - const colorOkay = inner$('span').first().attr('class').indexOf('author-') == 0; + const colorOkay = inner$('span').first().attr('class').indexOf('author-') === 0; return lenOkay && colorOkay; }, 10000).always(() => { // go to timeslider with a specific revision set - $('#iframe-container iframe').attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider#0`); + $('#iframe-container iframe').attr('src', + `${$('#iframe-container iframe').attr('src')}/timeslider#0`); // wait for the timeslider to be loaded helper.waitFor(() => { try { timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; - } catch (e) {} + } catch (e) { + // Empty catch block <3 + } if (timeslider$) { - return timeslider$('#innerdocbody').text().length == oldLength; + return timeslider$('#innerdocbody').text().length === oldLength; } }, 10000).always(() => { expect(timeslider$('#innerdocbody').text().length).to.eql(oldLength); @@ -147,24 +156,26 @@ describe('timeslider', function () { it('checks the export url', function (done) { const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; this.timeout(11000); inner$('div').first().sendkeys('a'); setTimeout(() => { // go to timeslider - $('#iframe-container iframe').attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider#0`); + $('#iframe-container iframe').attr('src', + `${$('#iframe-container iframe').attr('src')}/timeslider#0`); let timeslider$; let exportLink; helper.waitFor(() => { try { timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; - } catch (e) {} + } catch (e) { + // Empty catch block <3 + } if (!timeslider$) return false; exportLink = timeslider$('#exportplaina').attr('href'); if (!exportLink) return false; - return exportLink.substr(exportLink.length - 12) == '0/export/txt'; + return exportLink.substr(exportLink.length - 12) === '0/export/txt'; }, 6000).always(() => { expect(exportLink.substr(exportLink.length - 12)).to.eql('0/export/txt'); done(); diff --git a/tests/frontend/specs/undo.js b/src/tests/frontend/specs/undo.js similarity index 93% rename from tests/frontend/specs/undo.js rename to src/tests/frontend/specs/undo.js index 0c94f2230..039ba6903 100644 --- a/tests/frontend/specs/undo.js +++ b/src/tests/frontend/specs/undo.js @@ -1,3 +1,5 @@ +'use strict'; + describe('undo button', function () { beforeEach(function (cb) { helper.newPad(cb); // creates a new pad @@ -5,6 +7,8 @@ describe('undo button', function () { }); it('undo some typing by clicking undo button', function (done) { + this.timeout(100); + this.timeout(150); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; @@ -29,8 +33,8 @@ describe('undo button', function () { }); it('undo some typing using a keypress', function (done) { + this.timeout(150); const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; // get the first text element inside the editable space const $firstTextElement = inner$('div span').first(); @@ -40,7 +44,7 @@ describe('undo button', function () { const modifiedValue = $firstTextElement.text(); // get the modified value expect(modifiedValue).not.to.be(originalValue); // expect the value to change - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = 90; // z inner$('#innerdocbody').trigger(e); diff --git a/src/tests/frontend/specs/unordered_list.js b/src/tests/frontend/specs/unordered_list.js new file mode 100644 index 000000000..f82b620c9 --- /dev/null +++ b/src/tests/frontend/specs/unordered_list.js @@ -0,0 +1,172 @@ +'use strict'; + +describe('unordered_list.js', function () { + describe('assign unordered list', function () { + // create a new pad before each test run + beforeEach(function (cb) { + helper.newPad(cb); + this.timeout(60000); + }); + + it('insert unordered list text then removes by outdent', function (done) { + this.timeout(150); + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; + const originalText = inner$('div').first().text(); + + const $insertunorderedlistButton = chrome$('.buttonicon-insertunorderedlist'); + $insertunorderedlistButton.click(); + + helper.waitFor(() => { + const newText = inner$('div').first().text(); + if (newText === originalText) { + return inner$('div').first().find('ul li').length === 1; + } + }).done(() => { + // remove indentation by bullet and ensure text string remains the same + chrome$('.buttonicon-outdent').click(); + helper.waitFor(() => { + const newText = inner$('div').first().text(); + return (newText === originalText); + }).done(() => { + done(); + }); + }); + }); + }); + + describe('unassign unordered list', function () { + // create a new pad before each test run + beforeEach(function (cb) { + helper.newPad(cb); + this.timeout(60000); + }); + + it('insert unordered list text then remove by clicking list again', function (done) { + this.timeout(150); + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; + const originalText = inner$('div').first().text(); + + const $insertunorderedlistButton = chrome$('.buttonicon-insertunorderedlist'); + $insertunorderedlistButton.click(); + + helper.waitFor(() => { + const newText = inner$('div').first().text(); + if (newText === originalText) { + return inner$('div').first().find('ul li').length === 1; + } + }).done(() => { + // remove indentation by bullet and ensure text string remains the same + $insertunorderedlistButton.click(); + helper.waitFor(() => { + const isList = inner$('div').find('ul').length === 1; + // sohuldn't be list + return (isList === false); + }).done(() => { + done(); + }); + }); + }); + }); + + + describe('keep unordered list on enter key', function () { + // create a new pad before each test run + beforeEach(function (cb) { + helper.newPad(cb); + this.timeout(60000); + }); + + it('Keeps the unordered list on enter for the new line', function (done) { + this.timeout(250); + 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}'); + + helper.waitFor(() => inner$('div span').first().text().indexOf('line 2') === -1).done(() => { + 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'); + done(); + }); + }); + }); + + describe('Pressing Tab in an UL increases and decreases indentation', function () { + // create a new pad before each test run + beforeEach(function (cb) { + helper.newPad(cb); + this.timeout(60000); + }); + + it('indent and de-indent list item with keypress', function (done) { + this.timeout(150); + 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); + + helper.waitFor(() => inner$('div').first().find('.list-bullet1').length === 1).done(done); + }); + }); + + 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(function (cb) { + helper.newPad(cb); + this.timeout(60000); + }); + + it('indent and de-indent list item with indent button', function (done) { + this.timeout(150); + 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 + + helper.waitFor(() => inner$('div').first().find('.list-bullet1').length === 1).done(done); + }); + }); +}); diff --git a/tests/frontend/specs/urls_become_clickable.js b/src/tests/frontend/specs/urls_become_clickable.js similarity index 98% rename from tests/frontend/specs/urls_become_clickable.js rename to src/tests/frontend/specs/urls_become_clickable.js index a027de9ff..8a038b8b1 100644 --- a/tests/frontend/specs/urls_become_clickable.js +++ b/src/tests/frontend/specs/urls_become_clickable.js @@ -20,6 +20,7 @@ describe('urls', function () { 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); diff --git a/tests/frontend/specs/xxauto_reconnect.js b/src/tests/frontend/specs/xxauto_reconnect.js similarity index 88% rename from tests/frontend/specs/xxauto_reconnect.js rename to src/tests/frontend/specs/xxauto_reconnect.js index 574616ce5..af35e528e 100644 --- a/tests/frontend/specs/xxauto_reconnect.js +++ b/src/tests/frontend/specs/xxauto_reconnect.js @@ -1,3 +1,5 @@ +'use strict'; + describe('Automatic pad reload on Force Reconnect message', function () { let padId, $originalPadFrame; @@ -32,9 +34,11 @@ describe('Automatic pad reload on Force Reconnect message', function () { }); context('and user clicks on Cancel', function () { - beforeEach(function () { + beforeEach(async function () { const $errorMessageModal = helper.padChrome$('#connectivity .userdup'); $errorMessageModal.find('#cancelreconnect').click(); + await helper.waitForPromise( + () => helper.padChrome$('#connectivity .userdup').is(':visible') === true); }); it('does not show Cancel button nor timer anymore', function (done) { @@ -52,16 +56,16 @@ describe('Automatic pad reload on Force Reconnect message', function () { context('and user does not click on Cancel until timer expires', function () { let padWasReloaded = false; - beforeEach(function () { + beforeEach(async function () { $originalPadFrame.one('load', () => { padWasReloaded = true; }); }); it('reloads the pad', function (done) { - helper.waitFor(() => padWasReloaded, 5000).done(done); + helper.waitFor(() => padWasReloaded, 10000).done(done); - this.timeout(5000); + this.timeout(10000); }); }); }); diff --git a/tests/frontend/travis/.gitignore b/src/tests/frontend/travis/.gitignore similarity index 100% rename from tests/frontend/travis/.gitignore rename to src/tests/frontend/travis/.gitignore diff --git a/src/tests/frontend/travis/adminrunner.sh b/src/tests/frontend/travis/adminrunner.sh new file mode 100755 index 000000000..da20d2801 --- /dev/null +++ b/src/tests/frontend/travis/adminrunner.sh @@ -0,0 +1,44 @@ +#!/bin/sh + +pecho() { printf %s\\n "$*"; } +log() { pecho "$@"; } +error() { log "ERROR: $@" >&2; } +fatal() { error "$@"; exit 1; } +try() { "$@" || fatal "'$@' failed"; } + +[ -n "${SAUCE_USERNAME}" ] || fatal "SAUCE_USERNAME is unset - exiting" +[ -n "${SAUCE_ACCESS_KEY}" ] || fatal "SAUCE_ACCESS_KEY is unset - exiting" + +# Move to the Etherpad base directory. +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 node_modules/ep_etherpad-lite/node/server.js --experimental-worker "${@}" & +ep_pid=$! + +log "Waiting for Etherpad to accept connections (http://localhost:9001)..." +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 +[ "$connected" = true ] \ + || fatal "Timed out waiting for Etherpad to accept connections" +log "Successfully connected to Etherpad on http://localhost:9001" + +# start the remote runner +try cd "${MY_DIR}" +log "Starting the remote runner..." +node remote_runner.js admin +exit_code=$? + +kill "$(cat /tmp/sauce.pid)" +kill "$ep_pid" && wait "$ep_pid" +log "Done." +exit "$exit_code" diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js new file mode 100644 index 000000000..5a75dbe66 --- /dev/null +++ b/src/tests/frontend/travis/remote_runner.js @@ -0,0 +1,194 @@ +'use strict'; + +const async = require('async'); +const wd = require('wd'); + +const config = { + host: 'ondemand.saucelabs.com', + port: 80, + username: process.env.SAUCE_USER, + accessKey: process.env.SAUCE_ACCESS_KEY, +}; + +const isAdminRunner = process.argv[2] === 'admin'; + +let allTestsPassed = true; +// overwrite the default exit code +// in case not all worker can be run (due to saucelabs limits), +// `queue.drain` below will not be called +// and the script would silently exit with error code 0 +process.exitCode = 2; +process.on('exit', (code) => { + if (code === 2) { + console.log('\x1B[31mFAILED\x1B[39m Not all saucelabs runner have been started.'); + } +}); + +const sauceTestWorker = async.queue((testSettings, callback) => { + const browser = wd.promiseChainRemote( + config.host, config.port, config.username, config.accessKey); + const name = + `${process.env.GIT_HASH} - ${testSettings.browserName} ` + + `${testSettings.version}, ${testSettings.platform}`; + testSettings.name = name; + testSettings.public = true; + testSettings.build = process.env.GIT_HASH; + // console.json can be downloaded via saucelabs, + // don't know how to print them into output of the tests + testSettings.extendedDebugging = true; + testSettings.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER; + + browser.init(testSettings).get('http://localhost:9001/tests/frontend/', () => { + const url = `https://saucelabs.com/jobs/${browser.sessionID}`; + console.log(`Remote sauce test '${name}' started! ${url}`); + + // tear down the test excecution + const stopSauce = (success, timesup) => { + clearInterval(getStatusInterval); + clearTimeout(timeout); + + browser.quit(() => { + if (!success) { + allTestsPassed = false; + } + + // if stopSauce is called via timeout + // (in contrast to via getStatusInterval) than the log of up to the last + // five seconds may not be available here. It's an error anyway, so don't care about it. + printLog(logIndex); + + if (timesup) { + console.log(`[${testSettings.browserName} ${testSettings.platform}` + + `${testSettings.version === '' ? '' : (` ${testSettings.version}`)}]` + + ' \x1B[31mFAILED\x1B[39m allowed test duration exceeded'); + } + console.log(`Remote sauce test '${name}' finished! ${url}`); + + callback(); + }); + }; + + /** + * timeout if a test hangs or the job exceeds 14.5 minutes + * It's necessary because if travis kills the saucelabs session due to inactivity, + * we don't get any output + * @todo this should be configured in testSettings, see + * https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options#TestConfigurationOptions-Timeouts + */ + const timeout = setTimeout(() => { + stopSauce(false, true); + }, 870000); // travis timeout is 15 minutes, set this to a slightly lower value + + let knownConsoleText = ''; + // how many characters of the log have been sent to travis + let logIndex = 0; + const getStatusInterval = setInterval(() => { + browser.eval("$('#console').text()", (err, consoleText) => { + if (!consoleText || err) { + return; + } + knownConsoleText = consoleText; + + if (knownConsoleText.indexOf('FINISHED') > 0) { + const match = knownConsoleText.match( + /FINISHED.*([0-9]+) tests passed, ([0-9]+) tests failed/); + // finished without failures + if (match[2] && match[2] === '0') { + stopSauce(true); + + // finished but some tests did not return or some tests failed + } else { + stopSauce(false); + } + } else { + // not finished yet + printLog(logIndex); + logIndex = knownConsoleText.length; + } + }); + }, 5000); + + /** + * Replaces color codes in the test runners log, appends + * browser name, platform etc. to every line and prints them. + * + * @param {number} index offset from where to start + */ + const printLog = (index) => { + let testResult = knownConsoleText.substring(index) + .replace(/\[red\]/g, '\x1B[31m').replace(/\[yellow\]/g, '\x1B[33m') + .replace(/\[green\]/g, '\x1B[32m').replace(/\[clear\]/g, '\x1B[39m'); + testResult = testResult.split('\\n').map((line) => `[${testSettings.browserName} ` + + `${testSettings.platform}` + + `${testSettings.version === '' ? '' : (` ${testSettings.version}`)}]` + + `${line}`).join('\n'); + + console.log(testResult); + }; + }); +}, 6); // run 6 tests in parrallel + +if (!isAdminRunner) { + // 1) Firefox on Linux + sauceTestWorker.push({ + platform: 'Windows 7', + browserName: 'firefox', + version: '52.0', + }); + + // 2) Chrome on Linux + sauceTestWorker.push({ + platform: 'Windows 7', + browserName: 'chrome', + version: '55.0', + args: ['--use-fake-device-for-media-stream'], + }); + + /* + // 3) Safari on OSX 10.15 + sauceTestWorker.push({ + 'platform' : 'OS X 10.15' + , 'browserName' : 'safari' + , 'version' : '13.1' + }); + */ + + // 4) Safari on OSX 10.14 + sauceTestWorker.push({ + platform: 'OS X 10.15', + browserName: 'safari', + version: '13.1', + }); + // IE 10 doesn't appear to be working anyway + /* + // 4) IE 10 on Win 8 + sauceTestWorker.push({ + 'platform' : 'Windows 8' + , 'browserName' : 'iexplore' + , 'version' : '10.0' + }); + */ + // 5) Edge on Win 10 + sauceTestWorker.push({ + platform: 'Windows 10', + browserName: 'microsoftedge', + version: '83.0', + }); + // 6) Firefox on Win 7 + sauceTestWorker.push({ + platform: 'Windows 7', + browserName: 'firefox', + version: '78.0', + }); +} else { + // 4) Safari on OSX 10.14 + sauceTestWorker.push({ + platform: 'OS X 10.15', + browserName: 'safari', + version: '13.1', + }); +} + +sauceTestWorker.drain(() => { + process.exit(allTestsPassed ? 0 : 1); +}); diff --git a/tests/frontend/travis/runner.sh b/src/tests/frontend/travis/runner.sh similarity index 84% rename from tests/frontend/travis/runner.sh rename to src/tests/frontend/travis/runner.sh index da28ec1ef..b19c2873f 100755 --- a/tests/frontend/travis/runner.sh +++ b/src/tests/frontend/travis/runner.sh @@ -9,12 +9,11 @@ try() { "$@" || fatal "'$@' failed"; } [ -n "${SAUCE_USERNAME}" ] || fatal "SAUCE_USERNAME is unset - exiting" [ -n "${SAUCE_ACCESS_KEY}" ] || fatal "SAUCE_ACCESS_KEY is unset - exiting" -MY_DIR=$(try cd "${0%/*}" && try pwd) || exit 1 +# Move to the Etherpad base directory. +MY_DIR=$(try cd "${0%/*}" && try pwd -P) || exit 1 +try cd "${MY_DIR}/../../../.." -# reliably move to the etherpad base folder before running it -try cd "${MY_DIR}/../../../" - -log "Assuming bin/installDeps.sh has already been run" +log "Assuming src/bin/installDeps.sh has already been run" node node_modules/ep_etherpad-lite/node/server.js --experimental-worker "${@}" & ep_pid=$! diff --git a/tests/frontend/travis/runnerBackend.sh b/src/tests/frontend/travis/runnerBackend.sh similarity index 77% rename from tests/frontend/travis/runnerBackend.sh rename to src/tests/frontend/travis/runnerBackend.sh index f829d0387..8a2e1bab4 100755 --- a/tests/frontend/travis/runnerBackend.sh +++ b/src/tests/frontend/travis/runnerBackend.sh @@ -6,21 +6,18 @@ error() { log "ERROR: $@" >&2; } fatal() { error "$@"; exit 1; } try() { "$@" || fatal "'$@' failed"; } -MY_DIR=$(try cd "${0%/*}" && try pwd) || fatal "failed to find script directory" - -# reliably move to the etherpad base folder before running it -try cd "${MY_DIR}/../../../" +# Move to the Etherpad base directory. +MY_DIR=$(try cd "${0%/*}" && try pwd -P) || exit 1 +try cd "${MY_DIR}/../../../.." try sed -e ' s!"soffice":[^,]*!"soffice": "/usr/bin/soffice"! # Reduce rate limit aggressiveness s!"max":[^,]*!"max": 100! s!"points":[^,]*!"points": 1000! -# GitHub does not like our output -s!"loglevel":[^,]*!"loglevel": "WARN"! ' settings.json.template >settings.json -log "Assuming bin/installDeps.sh has already been run" +log "Assuming src/bin/installDeps.sh has already been run" node node_modules/ep_etherpad-lite/node/server.js "${@}" & ep_pid=$! diff --git a/tests/frontend/travis/runnerLoadTest.sh b/src/tests/frontend/travis/runnerLoadTest.sh similarity index 85% rename from tests/frontend/travis/runnerLoadTest.sh rename to src/tests/frontend/travis/runnerLoadTest.sh index 50ffcbb49..3e9d7d406 100755 --- a/tests/frontend/travis/runnerLoadTest.sh +++ b/src/tests/frontend/travis/runnerLoadTest.sh @@ -6,10 +6,9 @@ error() { log "ERROR: $@" >&2; } fatal() { error "$@"; exit 1; } try() { "$@" || fatal "'$@' failed"; } -MY_DIR=$(try cd "${0%/*}" && try pwd) || exit 1 - -# reliably move to the etherpad base folder before running it -try cd "${MY_DIR}/../../../" +# Move to the Etherpad base directory. +MY_DIR=$(try cd "${0%/*}" && try pwd -P) || exit 1 +try cd "${MY_DIR}/../../../.." try sed -e ' s!"loadTest":[^,]*!"loadTest": true! @@ -17,7 +16,7 @@ s!"loadTest":[^,]*!"loadTest": true! s!"points":[^,]*!"points": 1000! ' settings.json.template >settings.json -log "Assuming bin/installDeps.sh has already been run" +log "Assuming src/bin/installDeps.sh has already been run" node node_modules/ep_etherpad-lite/node/server.js "${@}" >/dev/null & ep_pid=$! diff --git a/tests/frontend/travis/sauce_tunnel.sh b/src/tests/frontend/travis/sauce_tunnel.sh similarity index 100% rename from tests/frontend/travis/sauce_tunnel.sh rename to src/tests/frontend/travis/sauce_tunnel.sh diff --git a/src/tests/ratelimit/Dockerfile.anotherip b/src/tests/ratelimit/Dockerfile.anotherip new file mode 100644 index 000000000..57f02f628 --- /dev/null +++ b/src/tests/ratelimit/Dockerfile.anotherip @@ -0,0 +1,4 @@ +FROM node:alpine3.12 +WORKDIR /tmp +RUN npm i etherpad-cli-client +COPY ./src/tests/ratelimit/send_changesets.js /tmp/send_changesets.js diff --git a/src/tests/ratelimit/Dockerfile.nginx b/src/tests/ratelimit/Dockerfile.nginx new file mode 100644 index 000000000..c2a1dae4e --- /dev/null +++ b/src/tests/ratelimit/Dockerfile.nginx @@ -0,0 +1,2 @@ +FROM nginx +COPY ./src/tests/ratelimit/nginx.conf /etc/nginx/nginx.conf diff --git a/tests/ratelimit/nginx.conf b/src/tests/ratelimit/nginx.conf similarity index 100% rename from tests/ratelimit/nginx.conf rename to src/tests/ratelimit/nginx.conf diff --git a/tests/ratelimit/send_changesets.js b/src/tests/ratelimit/send_changesets.js similarity index 67% rename from tests/ratelimit/send_changesets.js rename to src/tests/ratelimit/send_changesets.js index b0d994c8c..8f4f93d03 100644 --- a/tests/ratelimit/send_changesets.js +++ b/src/tests/ratelimit/send_changesets.js @@ -1,9 +1,7 @@ -try { - var etherpad = require('../../src/node_modules/etherpad-cli-client'); - // ugly -} catch { - var etherpad = require('etherpad-cli-client'); -} +'use strict'; + +const etherpad = require('etherpad-cli-client'); + const pad = etherpad.connect(process.argv[2]); pad.on('connected', () => { setTimeout(() => { @@ -18,7 +16,7 @@ pad.on('connected', () => { }); // in case of disconnect exit code 1 pad.on('message', (message) => { - if (message.disconnect == 'rateLimited') { + if (message.disconnect === 'rateLimited') { process.exit(1); } }); diff --git a/tests/ratelimit/testlimits.sh b/src/tests/ratelimit/testlimits.sh similarity index 100% rename from tests/ratelimit/testlimits.sh rename to src/tests/ratelimit/testlimits.sh diff --git a/tests b/tests new file mode 120000 index 000000000..cce6a5cdf --- /dev/null +++ b/tests @@ -0,0 +1 @@ +src/tests \ No newline at end of file diff --git a/tests/backend/specs/api/api.js b/tests/backend/specs/api/api.js deleted file mode 100644 index de009c559..000000000 --- a/tests/backend/specs/api/api.js +++ /dev/null @@ -1,76 +0,0 @@ -/** - * API specs - * - * Tests for generic overarching HTTP API related features not related to any - * specific part of the data model or domain. For example: tests for versioning - * and openapi definitions. - */ - -const common = require('../../common'); -const supertest = require(`${__dirname}/../../../../src/node_modules/supertest`); -const settings = require(`${__dirname}/../../../../src/node/utils/Settings`); -const api = supertest(`http://${settings.ip}:${settings.port}`); - -const validateOpenAPI = require(`${__dirname}/../../../../src/node_modules/openapi-schema-validation`).validate; - -const apiKey = common.apiKey; -let apiVersion = 1; - -const testPadId = makeid(); - -describe(__filename, function () { - describe('API Versioning', function () { - it('errors if can not connect', function (done) { - api - .get('/api/') - .expect((res) => { - apiVersion = res.body.currentVersion; - if (!res.body.currentVersion) throw new Error('No version set in API'); - return; - }) - .expect(200, done); - }); - }); - - describe('OpenAPI definition', function () { - it('generates valid openapi definition document', function (done) { - api - .get('/api/openapi.json') - .expect((res) => { - const {valid, errors} = validateOpenAPI(res.body, 3); - if (!valid) { - const prettyErrors = JSON.stringify(errors, null, 2); - throw new Error(`Document is not valid OpenAPI. ${errors.length} validation errors:\n${prettyErrors}`); - } - return; - }) - .expect(200, done); - }); - }); - - describe('jsonp support', function () { - it('supports jsonp calls', function (done) { - api - .get(`${endPoint('createPad')}&jsonp=jsonp_1&padID=${testPadId}`) - .expect((res) => { - if (!res.text.match('jsonp_1')) throw new Error('no jsonp call seen'); - }) - .expect('Content-Type', /javascript/) - .expect(200, done); - }); - }); -}); - -var endPoint = function (point) { - return `/api/${apiVersion}/${point}?apikey=${apiKey}`; -}; - -function makeid() { - let text = ''; - const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - - for (let i = 0; i < 5; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; -} diff --git a/tests/backend/specs/api/importexport.js b/tests/backend/specs/api/importexport.js deleted file mode 100644 index 5252f1300..000000000 --- a/tests/backend/specs/api/importexport.js +++ /dev/null @@ -1,325 +0,0 @@ -'use strict'; -/* - * ACHTUNG: there is a copied & modified version of this file in - * /tests/container/spacs/api/pad.js - * - * TODO: unify those two files, and merge in a single one. - */ - -/* eslint-disable max-len */ - -const common = require('../../common'); -const supertest = require(`${__dirname}/../../../../src/node_modules/supertest`); -const settings = require(`${__dirname}/../../../../tests/container/loadSettings.js`).loadSettings(); -const api = supertest(`http://${settings.ip}:${settings.port}`); - -const apiKey = common.apiKey; -const apiVersion = 1; - -const testImports = { - 'malformed': { - input: '
            • wtf
            ', - expectedHTML: 'wtf

            ', - expectedText: 'wtf\n\n', - }, - 'nonelistiteminlist #3620': { - input: '
              test
            • FOO
            ', - expectedHTML: '
              test
            • FOO

            ', - expectedText: '\ttest\n\t* FOO\n\n', - }, - 'whitespaceinlist #3620': { - input: '
            • FOO
            ', - expectedHTML: '
            • FOO

            ', - expectedText: '\t* FOO\n\n', - }, - 'prefixcorrectlinenumber': { - input: '
            1. should be 1
            2. should be 2
            ', - expectedHTML: '
            1. should be 1
            2. should be 2

            ', - expectedText: '\t1. should be 1\n\t2. should be 2\n\n', - }, - 'prefixcorrectlinenumbernested': { - input: '
            1. should be 1
              1. foo
            2. should be 2
            ', - expectedHTML: '
            1. should be 1
              1. foo
            2. should be 2

            ', - expectedText: '\t1. should be 1\n\t\t1.1. foo\n\t2. should be 2\n\n', - }, - - /* - "prefixcorrectlinenumber when introduced none list item - currently not supported see #3450":{ - input: '
            1. should be 1
            2. test
            3. should be 2
            ', - expectedHTML: '
            1. should be 1
            2. test
            3. should be 2

            ', - expectedText: '\t1. should be 1\n\ttest\n\t2. should be 2\n\n' - } - , - "newlinesshouldntresetlinenumber #2194":{ - input: '
            1. should be 1
            2. test
            3. should be 2
            ', - expectedHTML: '
            1. should be 1
            2. test
            3. should be 2

            ', - expectedText: '\t1. should be 1\n\ttest\n\t2. should be 2\n\n' - } - */ - 'ignoreAnyTagsOutsideBody': { - description: 'Content outside body should be ignored', - input: 'titleempty
            ', - expectedHTML: 'empty

            ', - expectedText: 'empty\n\n', - }, - 'indentedListsAreNotBullets': { - description: 'Indented lists are represented with tabs and without bullets', - input: '
            • indent
            • indent
            ', - expectedHTML: '
            • indent
            • indent

            ', - expectedText: '\tindent\n\tindent\n\n' - }, - lineWithMultipleSpaces: { - description: 'Multiple spaces should be collapsed', - input: 'Text with more than one space.
            ', - expectedHTML: 'Text with more than one space.

            ', - expectedText: 'Text with more than one space.\n\n' - }, - lineWithMultipleNonBreakingAndNormalSpaces: { - // XXX the HTML between "than" and "one" looks strange - description: 'non-breaking space should be preserved, but can be replaced when it', - input: 'Text with  more   than  one space.
            ', - expectedHTML: 'Text with  more   than  one space.

            ', - expectedText: 'Text with more than one space.\n\n' - }, - multiplenbsp: { - description: 'Multiple non-breaking space should be preserved', - input: '  
            ', - expectedHTML: '  

            ', - expectedText: ' \n\n' - }, - multipleNonBreakingSpaceBetweenWords: { - description: 'A normal space is always inserted before a word', - input: '  word1  word2   word3
            ', - expectedHTML: '  word1  word2   word3

            ', - expectedText: ' word1 word2 word3\n\n' - }, - nonBreakingSpacePreceededBySpaceBetweenWords: { - description: 'A non-breaking space preceeded by a normal space', - input: '  word1  word2  word3
            ', - expectedHTML: ' word1  word2  word3

            ', - expectedText: ' word1 word2 word3\n\n' - }, - nonBreakingSpaceFollowededBySpaceBetweenWords: { - description: 'A non-breaking space followed by a normal space', - input: '  word1  word2  word3
            ', - expectedHTML: '  word1  word2  word3

            ', - expectedText: ' word1 word2 word3\n\n' - }, - spacesAfterNewline: { - description: 'Collapse spaces that follow a newline', - input:'something
            something
            ', - expectedHTML: 'something
            something

            ', - expectedText: 'something\nsomething\n\n' - }, - spacesAfterNewlineP: { - description: 'Collapse spaces that follow a paragraph', - input:'something

            something
            ', - expectedHTML: 'something

            something

            ', - expectedText: 'something\n\nsomething\n\n' - }, - spacesAtEndOfLine: { - description: 'Collapse spaces that preceed/follow a newline', - input:'something
            something
            ', - expectedHTML: 'something
            something

            ', - expectedText: 'something\nsomething\n\n' - }, - spacesAtEndOfLineP: { - description: 'Collapse spaces that preceed/follow a paragraph', - input:'something

            something
            ', - expectedHTML: 'something

            something

            ', - expectedText: 'something\n\nsomething\n\n' - }, - nonBreakingSpacesAfterNewlines: { - description: 'Don\'t collapse non-breaking spaces that follow a newline', - input:'something
               something
            ', - expectedHTML: 'something
               something

            ', - expectedText: 'something\n something\n\n' - }, - nonBreakingSpacesAfterNewlinesP: { - description: 'Don\'t collapse non-breaking spaces that follow a paragraph', - input:'something

               something
            ', - expectedHTML: 'something

               something

            ', - expectedText: 'something\n\n something\n\n' - }, - collapseSpacesInsideElements: { - description: 'Preserve only one space when multiple are present', - input: 'Need more space s !
            ', - expectedHTML: 'Need more space s !

            ', - expectedText: 'Need more space s !\n\n' - }, - collapseSpacesAcrossNewlines: { - description: 'Newlines and multiple spaces across newlines should be collapsed', - input: ` - Need - more - space - s - !
            `, - expectedHTML: 'Need more space s !

            ', - expectedText: 'Need more space s !\n\n' - }, - multipleNewLinesAtBeginning: { - description: 'Multiple new lines and paragraphs at the beginning should be preserved', - input: '

            first line

            second line
            ', - expectedHTML: '



            first line

            second line

            ', - expectedText: '\n\n\n\nfirst line\n\nsecond line\n\n' - }, - multiLineParagraph:{ - description: "A paragraph with multiple lines should not loose spaces when lines are combined", - input:` -

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

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

            ', - expectedText: 'а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь\n\n' - }, - multiLineParagraphWithPre:{ - //XXX why is there   before "in"? - description: "lines in preformatted text should be kept intact", - input:` -

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

            multiple
            -   lines
            - in
            -      pre
            -

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

            -`, - expectedHTML: 'а б в г ґ д е є ж з и і ї й к л м н о
            multiple
               lines
             in
                  pre

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

            ', - expectedText: 'а б в г ґ д е є ж з и і ї й к л м н о\nmultiple\n lines\n in\n pre\n\nп р с т у ф х ц ч ш щ ю я ь\n\n' - }, - preIntroducesASpace: { - description: "pre should be on a new line not preceeded by a space", - input:`

            - 1 -

            preline
            -

            `, - expectedHTML: '1
            preline


            ', - expectedText: '1\npreline\n\n\n' - }, - dontDeleteSpaceInsideElements: { - description: 'Preserve spaces inside elements', - input: 'Need more space s !
            ', - expectedHTML: 'Need more space s !

            ', - expectedText: 'Need more space s !\n\n' - }, - dontDeleteSpaceOutsideElements: { - description: 'Preserve spaces outside elements', - input: 'Need more space s !
            ', - expectedHTML: 'Need more space s !

            ', - expectedText: 'Need more space s !\n\n' - }, - dontDeleteSpaceAtEndOfElement: { - description: 'Preserve spaces at the end of an element', - input: 'Need more space s !
            ', - expectedHTML: 'Need more space s !

            ', - expectedText: 'Need more space s !\n\n' - }, - dontDeleteSpaceAtBeginOfElements: { - description: 'Preserve spaces at the start of an element', - input: 'Need more space s !
            ', - expectedHTML: 'Need more space s !

            ', - expectedText: 'Need more space s !\n\n' - }, -}; - -describe(__filename, function () { - Object.keys(testImports).forEach((testName) => { - const testPadId = makeid(); - const test = testImports[testName]; - if (test.disabled) { - return xit(`DISABLED: ${testName}`, function (done) { - done(); - }); - } - describe(`createPad ${testName}`, function () { - it('creates a new Pad', function (done) { - api.get(`${endPoint('createPad')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Unable to create new Pad'); - }) - .expect('Content-Type', /json/) - .expect(200, done); - }); - }); - - describe(`setHTML ${testName}`, function () { - it('Sets the HTML', function (done) { - api.get(`${endPoint('setHTML')}&padID=${testPadId}&html=${encodeURIComponent(test.input)}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error(`Error:${testName}`); - }) - .expect('Content-Type', /json/) - .expect(200, done); - }); - }); - - describe(`getHTML ${testName}`, function () { - it('Gets back the HTML of a Pad', function (done) { - api.get(`${endPoint('getHTML')}&padID=${testPadId}`) - .expect((res) => { - const receivedHtml = res.body.data.html; - if (receivedHtml !== test.expectedHTML) { - throw new Error(`HTML received from export is not the one we were expecting. - Test Name: - ${testName} - - Received: - ${JSON.stringify(receivedHtml)} - - Expected: - ${JSON.stringify(test.expectedHTML)} - - Which is a different version of the originally imported one: - ${test.input}`); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); - }); - }); - - describe(`getText ${testName}`, function () { - it('Gets back the Text of a Pad', function (done) { - api.get(`${endPoint('getText')}&padID=${testPadId}`) - .expect((res) => { - const receivedText = res.body.data.text; - if (receivedText !== test.expectedText) { - throw new Error(`Text received from export is not the one we were expecting. - Test Name: - ${testName} - - Received: - ${JSON.stringify(receivedText)} - - Expected: - ${JSON.stringify(test.expectedText)} - - Which is a different version of the originally imported one: - ${test.input}`); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); - }); - }); - }); -}); - - -function endPoint(point, version) { - version = version || apiVersion; - return `/api/${version}/${point}?apikey=${apiKey}`; -}; - -function makeid() { - let text = ''; - const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - - for (let i = 0; i < 5; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; -} diff --git a/tests/frontend/specs/caret.js b/tests/frontend/specs/caret.js deleted file mode 100644 index 1fb8d8aa7..000000000 --- a/tests/frontend/specs/caret.js +++ /dev/null @@ -1,333 +0,0 @@ -describe('As the caret is moved is the UI properly updated?', function () { - let padName; - const numberOfRows = 50; - /* - - //create a new pad before each test run - beforeEach(function(cb){ - helper.newPad(cb); - this.timeout(60000); - }); - - xit("creates a pad", function(done) { - padName = helper.newPad(done); - this.timeout(60000); - }); -*/ - - /* Tests to do - * Keystroke up (38), down (40), left (37), right (39) with and without special keys IE control / shift - * Page up (33) / down (34) with and without special keys - * Page up on the first line shouldn't move the viewport - * Down down on the last line shouldn't move the viewport - * Down arrow on any other line except the last lines shouldn't move the viewport - * Do all of the above tests after a copy/paste event - */ - - /* Challenges - * How do we keep the authors focus on a line if the lines above the author are modified? We should only redraw the user to a location if they are typing and make sure shift and arrow keys aren't redrawing the UI else highlight - copy/paste would get broken - * How can we simulate an edit event in the test framework? - */ - /* - // THIS DOESNT WORK IN CHROME AS IT DOESNT MOVE THE CURSOR! - it("down arrow", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; - - var $newFirstTextElement = inner$("div").first(); - $newFirstTextElement.focus(); - keyEvent(inner$, 37, false, false); // arrow down - keyEvent(inner$, 37, false, false); // arrow down - - done(); - }); - - it("Creates N lines", function(done){ - var inner$ = helper.padInner$; -console.log(inner$); - var chrome$ = helper.padChrome$; - var $newFirstTextElement = inner$("div").first(); - - prepareDocument(numberOfRows, $newFirstTextElement); // N lines into the first div as a target - helper.waitFor(function(){ // Wait for the DOM to register the new items - return inner$("div").first().text().length == 6; - }).done(function(){ // Once the DOM has registered the items - done(); - }); - }); - - it("Moves caret up a line", function(done){ - var inner$ = helper.padInner$; - var $newFirstTextElement = inner$("div").first(); - var originalCaretPosition = caretPosition(inner$); - var originalPos = originalCaretPosition.y; - var newCaretPos; - keyEvent(inner$, 38, false, false); // arrow up - - helper.waitFor(function(){ // Wait for the DOM to register the new items - var newCaretPosition = caretPosition(inner$); - newCaretPos = newCaretPosition.y; - return (newCaretPos < originalPos); - }).done(function(){ - expect(newCaretPos).to.be.lessThan(originalPos); - done(); - }); - }); - - it("Moves caret down a line", function(done){ - var inner$ = helper.padInner$; - var $newFirstTextElement = inner$("div").first(); - var originalCaretPosition = caretPosition(inner$); - var originalPos = originalCaretPosition.y; - var newCaretPos; - keyEvent(inner$, 40, false, false); // arrow down - - helper.waitFor(function(){ // Wait for the DOM to register the new items - var newCaretPosition = caretPosition(inner$); - newCaretPos = newCaretPosition.y; - return (newCaretPos > originalPos); - }).done(function(){ - expect(newCaretPos).to.be.moreThan(originalPos); - done(); - }); - }); - - it("Moves caret to top of doc", function(done){ - var inner$ = helper.padInner$; - var $newFirstTextElement = inner$("div").first(); - var originalCaretPosition = caretPosition(inner$); - var originalPos = originalCaretPosition.y; - var newCaretPos; - - var i = 0; - while(i < numberOfRows){ // press pageup key N times - keyEvent(inner$, 33, false, false); - i++; - } - - helper.waitFor(function(){ // Wait for the DOM to register the new items - var newCaretPosition = caretPosition(inner$); - newCaretPos = newCaretPosition.y; - return (newCaretPos < originalPos); - }).done(function(){ - expect(newCaretPos).to.be.lessThan(originalPos); - done(); - }); - }); - - it("Moves caret right a position", function(done){ - var inner$ = helper.padInner$; - var $newFirstTextElement = inner$("div").first(); - var originalCaretPosition = caretPosition(inner$); - var originalPos = originalCaretPosition.x; - var newCaretPos; - keyEvent(inner$, 39, false, false); // arrow right - - helper.waitFor(function(){ // Wait for the DOM to register the new items - var newCaretPosition = caretPosition(inner$); - newCaretPos = newCaretPosition.x; - return (newCaretPos > originalPos); - }).done(function(){ - expect(newCaretPos).to.be.moreThan(originalPos); - done(); - }); - }); - - it("Moves caret left a position", function(done){ - var inner$ = helper.padInner$; - var $newFirstTextElement = inner$("div").first(); - var originalCaretPosition = caretPosition(inner$); - var originalPos = originalCaretPosition.x; - var newCaretPos; - keyEvent(inner$, 33, false, false); // arrow left - - helper.waitFor(function(){ // Wait for the DOM to register the new items - var newCaretPosition = caretPosition(inner$); - newCaretPos = newCaretPosition.x; - return (newCaretPos < originalPos); - }).done(function(){ - expect(newCaretPos).to.be.lessThan(originalPos); - done(); - }); - }); - - it("Moves caret to the next line using right arrow", function(done){ - var inner$ = helper.padInner$; - var $newFirstTextElement = inner$("div").first(); - var originalCaretPosition = caretPosition(inner$); - var originalPos = originalCaretPosition.y; - var newCaretPos; - keyEvent(inner$, 39, false, false); // arrow right - keyEvent(inner$, 39, false, false); // arrow right - keyEvent(inner$, 39, false, false); // arrow right - keyEvent(inner$, 39, false, false); // arrow right - keyEvent(inner$, 39, false, false); // arrow right - keyEvent(inner$, 39, false, false); // arrow right - keyEvent(inner$, 39, false, false); // arrow right - - helper.waitFor(function(){ // Wait for the DOM to register the new items - var newCaretPosition = caretPosition(inner$); - newCaretPos = newCaretPosition.y; - return (newCaretPos > originalPos); - }).done(function(){ - expect(newCaretPos).to.be.moreThan(originalPos); - done(); - }); - }); - - it("Moves caret to the previous line using left arrow", function(done){ - var inner$ = helper.padInner$; - var $newFirstTextElement = inner$("div").first(); - var originalCaretPosition = caretPosition(inner$); - var originalPos = originalCaretPosition.y; - var newCaretPos; - keyEvent(inner$, 33, false, false); // arrow left - - helper.waitFor(function(){ // Wait for the DOM to register the new items - var newCaretPosition = caretPosition(inner$); - newCaretPos = newCaretPosition.y; - return (newCaretPos < originalPos); - }).done(function(){ - expect(newCaretPos).to.be.lessThan(originalPos); - done(); - }); - }); - - -/* - it("Creates N rows, changes height of rows, updates UI by caret key events", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; - var numberOfRows = 50; - - //ace creates a new dom element when you press a keystroke, so just get the first text element again - var $newFirstTextElement = inner$("div").first(); - var originalDivHeight = inner$("div").first().css("height"); - prepareDocument(numberOfRows, $newFirstTextElement); // N lines into the first div as a target - - helper.waitFor(function(){ // Wait for the DOM to register the new items - return inner$("div").first().text().length == 6; - }).done(function(){ // Once the DOM has registered the items - inner$("div").each(function(index){ // Randomize the item heights (replicates images / headings etc) - var random = Math.floor(Math.random() * (50)) + 20; - $(this).css("height", random+"px"); - }); - - console.log(caretPosition(inner$)); - var newDivHeight = inner$("div").first().css("height"); - var heightHasChanged = originalDivHeight != newDivHeight; // has the new div height changed from the original div height - expect(heightHasChanged).to.be(true); // expect the first line to be blank - }); - - // Is this Element now visible to the pad user? - helper.waitFor(function(){ // Wait for the DOM to register the new items - return isScrolledIntoView(inner$("div:nth-child("+numberOfRows+")"), inner$); // Wait for the DOM to scroll into place - }).done(function(){ // Once the DOM has registered the items - inner$("div").each(function(index){ // Randomize the item heights (replicates images / headings etc) - var random = Math.floor(Math.random() * (80 - 20 + 1)) + 20; - $(this).css("height", random+"px"); - }); - - var newDivHeight = inner$("div").first().css("height"); - var heightHasChanged = originalDivHeight != newDivHeight; // has the new div height changed from the original div height - expect(heightHasChanged).to.be(true); // expect the first line to be blank - }); - var i = 0; - while(i < numberOfRows){ // press down arrow - keyEvent(inner$, 40, false, false); - i++; - } - - // Does scrolling back up the pad with the up arrow show the correct contents? - helper.waitFor(function(){ // Wait for the new position to be in place - try{ - return isScrolledIntoView(inner$("div:nth-child("+numberOfRows+")"), inner$); // Wait for the DOM to scroll into place - }catch(e){ - return false; - } - }).done(function(){ // Once the DOM has registered the items - - var i = 0; - while(i < numberOfRows){ // press down arrow - keyEvent(inner$, 33, false, false); // doesn't work - i++; - } - - // Does scrolling back up the pad with the up arrow show the correct contents? - helper.waitFor(function(){ // Wait for the new position to be in place - try{ - return isScrolledIntoView(inner$("div:nth-child(0)"), inner$); // Wait for the DOM to scroll into place - }catch(e){ - return false; - } - }).done(function(){ // Once the DOM has registered the items - - - }); - }); - - - var i = 0; - while(i < numberOfRows){ // press down arrow - keyEvent(inner$, 33, false, false); // doesn't work - i++; - } - - - // Does scrolling back up the pad with the up arrow show the correct contents? - helper.waitFor(function(){ // Wait for the new position to be in place - return isScrolledIntoView(inner$("div:nth-child(1)"), inner$); // Wait for the DOM to scroll into place - }).done(function(){ // Once the DOM has registered the items - expect(true).to.be(true); - done(); - }); -*/ -}); - -function prepareDocument(n, target) { // generates a random document with random content on n lines - let i = 0; - while (i < n) { // for each line - target.sendkeys(makeStr()); // generate a random string and send that to the editor - target.sendkeys('{enter}'); // generator an enter keypress - i++; // rinse n times - } -} - -function keyEvent(target, charCode, ctrl, shift) { // sends a charCode to the window - const e = target.Event(helper.evtType); - if (ctrl) { - e.ctrlKey = true; // Control key - } - if (shift) { - e.shiftKey = true; // Shift Key - } - e.which = charCode; - e.keyCode = charCode; - target('#innerdocbody').trigger(e); -} - - -function makeStr() { // from http://stackoverflow.com/questions/1349404/generate-a-string-of-5-random-characters-in-javascript - let text = ''; - const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - - for (let i = 0; i < 5; i++) text += possible.charAt(Math.floor(Math.random() * possible.length)); - return text; -} - -function isScrolledIntoView(elem, $) { // from http://stackoverflow.com/questions/487073/check-if-element-is-visible-after-scrolling - const docViewTop = $(window).scrollTop(); - const docViewBottom = docViewTop + $(window).height(); - const elemTop = $(elem).offset().top; // how far the element is from the top of it's container - let elemBottom = elemTop + $(elem).height(); // how far plus the height of the elem.. IE is it all in? - elemBottom -= 16; // don't ask, sorry but this is needed.. - return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop)); -} - -function caretPosition($) { - const doc = $.window.document; - const pos = doc.getSelection(); - pos.y = pos.anchorNode.parentElement.offsetTop; - pos.x = pos.anchorNode.parentElement.offsetLeft; - return pos; -} diff --git a/tests/frontend/specs/change_user_name.js b/tests/frontend/specs/change_user_name.js deleted file mode 100644 index e144a2340..000000000 --- a/tests/frontend/specs/change_user_name.js +++ /dev/null @@ -1,71 +0,0 @@ -describe('change username value', function () { - // create a new pad before each test run - beforeEach(function (cb) { - helper.newPad(cb); - this.timeout(60000); - }); - - it('Remembers the user name after a refresh', function (done) { - this.timeout(60000); - const chrome$ = helper.padChrome$; - - // click on the settings button to make settings visible - const $userButton = chrome$('.buttonicon-showusers'); - $userButton.click(); - - const $usernameInput = chrome$('#myusernameedit'); - $usernameInput.click(); - - $usernameInput.val('John McLear'); - $usernameInput.blur(); - - setTimeout(() => { // give it a second to save the username on the server side - helper.newPad({ // get a new pad, but don't clear the cookies - clearCookies: false, - cb() { - const chrome$ = helper.padChrome$; - - // click on the settings button to make settings visible - const $userButton = chrome$('.buttonicon-showusers'); - $userButton.click(); - - const $usernameInput = chrome$('#myusernameedit'); - expect($usernameInput.val()).to.be('John McLear'); - done(); - }, - }); - }, 1000); - }); - - - it('Own user name is shown when you enter a chat', function (done) { - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - // click on the settings button to make settings visible - const $userButton = chrome$('.buttonicon-showusers'); - $userButton.click(); - - const $usernameInput = chrome$('#myusernameedit'); - $usernameInput.click(); - - $usernameInput.val('John McLear'); - $usernameInput.blur(); - - // 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 JohnMcLear - $chatInput.sendkeys('{enter}'); // simulate a keypress of enter actually does evt.which = 10 not 13 - - // check if chat shows up - helper.waitFor(() => chrome$('#chattext').children('p').length !== 0 // wait until the chat message shows up - ).done(() => { - const $firstChatMessage = chrome$('#chattext').children('p'); - const containsJohnMcLear = $firstChatMessage.text().indexOf('John McLear') !== -1; // does the string contain John McLear - expect(containsJohnMcLear).to.be(true); // expect the first chat message to contain JohnMcLear - done(); - }); - }); -}); diff --git a/tests/frontend/specs/enter.js b/tests/frontend/specs/enter.js deleted file mode 100644 index 6108d7f82..000000000 --- a/tests/frontend/specs/enter.js +++ /dev/null @@ -1,32 +0,0 @@ -describe('enter keystroke', function () { - // create a new pad before each test run - beforeEach(function (cb) { - helper.newPad(cb); - this.timeout(60000); - }); - - it('creates a new line & puts cursor onto a new line', 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(); - - // get the original string value minus the last char - const originalTextValue = $firstTextElement.text(); - - // simulate key presses to enter content - $firstTextElement.sendkeys('{enter}'); - - // ace creates a new dom element when you press a keystroke, so just get the first text element again - const $newFirstTextElement = inner$('div').first(); - - helper.waitFor(() => inner$('div').first().text() === '').done(() => { - const $newSecondLine = inner$('div').first().next(); - const newFirstTextElementValue = inner$('div').first().text(); - expect(newFirstTextElementValue).to.be(''); // expect the first line to be blank - expect($newSecondLine.text()).to.be(originalTextValue); // expect the second line to be the same as the original first line. - done(); - }); - }); -}); diff --git a/tests/frontend/specs/importexport.js b/tests/frontend/specs/importexport.js deleted file mode 100644 index 0be2a0744..000000000 --- a/tests/frontend/specs/importexport.js +++ /dev/null @@ -1,222 +0,0 @@ -describe('import functionality', function () { - beforeEach(function (cb) { - helper.newPad(cb); // creates a new pad - this.timeout(60000); - }); - - function getinnertext() { - const inner = helper.padInner$; - if (!inner) { - return ''; - } - let newtext = ''; - inner('div').each((line, el) => { - newtext += `${el.innerHTML}\n`; - }); - return newtext; - } - function importrequest(data, importurl, type) { - let success; - let error; - const result = $.ajax({ - url: importurl, - type: 'post', - processData: false, - async: false, - contentType: 'multipart/form-data; boundary=boundary', - accepts: { - text: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - }, - data: `Content-Type: multipart/form-data; boundary=--boundary\r\n\r\n--boundary\r\nContent-Disposition: form-data; name="file"; filename="import.${type}"\r\nContent-Type: text/plain\r\n\r\n${data}\r\n\r\n--boundary`, - error(res) { - error = res; - }, - }); - expect(error).to.be(undefined); - return result; - } - function exportfunc(link) { - const exportresults = []; - $.ajaxSetup({ - async: false, - }); - $.get(`${link}/export/html`, (data) => { - const start = data.indexOf(''); - const end = data.indexOf(''); - const html = data.substr(start + 6, end - start - 6); - exportresults.push(['html', html]); - }); - $.get(`${link}/export/txt`, (data) => { - exportresults.push(['txt', data]); - }); - return exportresults; - } - - xit('import a pad with newlines from txt', function (done) { - const importurl = `${helper.padChrome$.window.location.href}/import`; - const textWithNewLines = 'imported text\nnewline'; - importrequest(textWithNewLines, importurl, 'txt'); - helper.waitFor(() => expect(getinnertext()).to.be('imported text\nnewline\n
            \n')); - const results = exportfunc(helper.padChrome$.window.location.href); - expect(results[0][1]).to.be('imported text
            newline

            '); - expect(results[1][1]).to.be('imported text\nnewline\n\n'); - done(); - }); - xit('import a pad with newlines from html', function (done) { - const importurl = `${helper.padChrome$.window.location.href}/import`; - const htmlWithNewLines = 'htmltext
            newline'; - importrequest(htmlWithNewLines, importurl, 'html'); - helper.waitFor(() => expect(getinnertext()).to.be('htmltext\nnewline\n
            \n')); - const results = exportfunc(helper.padChrome$.window.location.href); - expect(results[0][1]).to.be('htmltext
            newline

            '); - expect(results[1][1]).to.be('htmltext\nnewline\n\n'); - done(); - }); - xit('import a pad with attributes from html', function (done) { - const importurl = `${helper.padChrome$.window.location.href}/import`; - const htmlWithNewLines = 'htmltext
            newline'; - importrequest(htmlWithNewLines, importurl, 'html'); - helper.waitFor(() => expect(getinnertext()).to.be('htmltext\nnewline\n
            \n')); - const results = exportfunc(helper.padChrome$.window.location.href); - expect(results[0][1]).to.be('htmltext
            newline

            '); - expect(results[1][1]).to.be('htmltext\nnewline\n\n'); - done(); - }); - xit('import a pad with bullets from html', function (done) { - const importurl = `${helper.padChrome$.window.location.href}/import`; - const htmlWithBullets = '
            • bullet line 1
            • bullet line 2
              • bullet2 line 1
              • bullet2 line 2
            '; - importrequest(htmlWithBullets, importurl, 'html'); - helper.waitFor(() => expect(getinnertext()).to.be('\ -
            • bullet line 1
            \n\ -
            • bullet line 2
            \n\ -
            • bullet2 line 1
            \n\ -
            • bullet2 line 2
            \n\ -
            \n')); - const results = exportfunc(helper.padChrome$.window.location.href); - expect(results[0][1]).to.be('
            • bullet line 1
            • bullet line 2
              • bullet2 line 1
              • bullet2 line 2

            '); - expect(results[1][1]).to.be('\t* bullet line 1\n\t* bullet line 2\n\t\t* bullet2 line 1\n\t\t* bullet2 line 2\n\n'); - done(); - }); - xit('import a pad with bullets and newlines from html', function (done) { - const importurl = `${helper.padChrome$.window.location.href}/import`; - const htmlWithBullets = '
            • bullet line 1

            • bullet line 2
              • bullet2 line 1

              • bullet2 line 2
            '; - importrequest(htmlWithBullets, importurl, 'html'); - helper.waitFor(() => expect(getinnertext()).to.be('\ -
            • bullet line 1
            \n\ -
            \n\ -
            • bullet line 2
            \n\ -
            • bullet2 line 1
            \n\ -
            \n\ -
            • bullet2 line 2
            \n\ -
            \n')); - const results = exportfunc(helper.padChrome$.window.location.href); - expect(results[0][1]).to.be('
            • bullet line 1

            • bullet line 2
              • bullet2 line 1

              • bullet2 line 2

            '); - expect(results[1][1]).to.be('\t* bullet line 1\n\n\t* bullet line 2\n\t\t* bullet2 line 1\n\n\t\t* bullet2 line 2\n\n'); - done(); - }); - xit('import a pad with bullets and newlines and attributes from html', function (done) { - const importurl = `${helper.padChrome$.window.location.href}/import`; - const htmlWithBullets = '
            • bullet line 1

            • bullet line 2
              • bullet2 line 1

                  • bullet4 line 2 bisu
                  • bullet4 line 2 bs
                  • bullet4 line 2 uuis
            '; - importrequest(htmlWithBullets, importurl, 'html'); - helper.waitFor(() => expect(getinnertext()).to.be('\ -
            • bullet line 1
            \n\
            \n\ -
            • bullet line 2
            \n\ -
            • bullet2 line 1
            \n
            \n\ -
            • bullet4 line 2 bisu
            \n\ -
            • bullet4 line 2 bs
            \n\ -
            • bullet4 line 2 uuis
            \n\ -
            \n')); - const results = exportfunc(helper.padChrome$.window.location.href); - expect(results[0][1]).to.be('
            • bullet line 1

            • bullet line 2
              • bullet2 line 1

                  • bullet4 line 2 bisu
                  • bullet4 line 2 bs
                  • bullet4 line 2 uuis

            '); - expect(results[1][1]).to.be('\t* bullet line 1\n\n\t* bullet line 2\n\t\t* bullet2 line 1\n\n\t\t\t\t* bullet4 line 2 bisu\n\t\t\t\t* bullet4 line 2 bs\n\t\t\t\t* bullet4 line 2 uuis\n\n'); - done(); - }); - xit('import a pad with nested bullets from html', function (done) { - const importurl = `${helper.padChrome$.window.location.href}/import`; - const htmlWithBullets = '
            • bullet line 1
            • bullet line 2
              • bullet2 line 1
                  • bullet4 line 2
                  • bullet4 line 2
                  • bullet4 line 2
                • bullet3 line 1
            • bullet2 line 1
            '; - importrequest(htmlWithBullets, importurl, 'html'); - const oldtext = getinnertext(); - helper.waitFor(() => oldtext != getinnertext() - // return expect(getinnertext()).to.be('\ - //
            • bullet line 1
            \n\ - //
            • bullet line 2
            \n\ - //
            • bullet2 line 1
            \n\ - //
            • bullet4 line 2
            \n\ - //
            • bullet4 line 2
            \n\ - //
            • bullet4 line 2
            \n\ - //
            \n') - ); - - const results = exportfunc(helper.padChrome$.window.location.href); - expect(results[0][1]).to.be('
            • bullet line 1
            • bullet line 2
              • bullet2 line 1
                  • bullet4 line 2
                  • bullet4 line 2
                  • bullet4 line 2
                • bullet3 line 1
            • bullet2 line 1

            '); - expect(results[1][1]).to.be('\t* bullet line 1\n\t* bullet line 2\n\t\t* bullet2 line 1\n\t\t\t\t* bullet4 line 2\n\t\t\t\t* bullet4 line 2\n\t\t\t\t* bullet4 line 2\n\t\t\t* bullet3 line 1\n\t* bullet2 line 1\n\n'); - done(); - }); - xit('import a pad with 8 levels of bullets and newlines and attributes from html', function (done) { - const importurl = `${helper.padChrome$.window.location.href}/import`; - const htmlWithBullets = '
            • bullet line 1

            • bullet line 2
              • bullet2 line 1

                  • bullet4 line 2 bisu
                  • bullet4 line 2 bs
                  • bullet4 line 2 uuis
                          • foo
                          • foobar bs
                    • foobar
              '; - importrequest(htmlWithBullets, importurl, 'html'); - helper.waitFor(() => expect(getinnertext()).to.be('\ -
              • bullet line 1
              \n\
              \n\ -
              • bullet line 2
              \n\ -
              • bullet2 line 1
              \n
              \n\ -
              • bullet4 line 2 bisu
              \n\ -
              • bullet4 line 2 bs
              \n\ -
              • bullet4 line 2 uuis
              \n\ -
              • foo
              \n\ -
              • foobar bs
              \n\ -
              • foobar
              \n\ -
              \n')); - const results = exportfunc(helper.padChrome$.window.location.href); - expect(results[0][1]).to.be('
              • bullet line 1

              • bullet line 2
                • bullet2 line 1

                    • bullet4 line 2 bisu
                    • bullet4 line 2 bs
                    • bullet4 line 2 uuis
                            • foo
                            • foobar bs
                      • foobar

              '); - expect(results[1][1]).to.be('\t* bullet line 1\n\n\t* bullet line 2\n\t\t* bullet2 line 1\n\n\t\t\t\t* bullet4 line 2 bisu\n\t\t\t\t* bullet4 line 2 bs\n\t\t\t\t* bullet4 line 2 uuis\n\t\t\t\t\t\t\t\t* foo\n\t\t\t\t\t\t\t\t* foobar bs\n\t\t\t\t\t* foobar\n\n'); - done(); - }); - - xit('import a pad with ordered lists from html', function (done) { - const importurl = `${helper.padChrome$.window.location.href}/import`; - const htmlWithBullets = '
              1. number 1 line 1
              1. number 2 line 2
              '; - importrequest(htmlWithBullets, importurl, 'html'); - console.error(getinnertext()); - expect(getinnertext()).to.be('\ -
              1. number 1 line 1
              \n\ -
              1. number 2 line 2
              \n\ -
              \n'); - const results = exportfunc(helper.padChrome$.window.location.href); - expect(results[0][1]).to.be('
              1. number 1 line 1
              1. number 2 line 2
              '); - expect(results[1][1]).to.be(''); - done(); - }); - xit('import a pad with ordered lists and newlines from html', function (done) { - const importurl = `${helper.padChrome$.window.location.href}/import`; - const htmlWithBullets = '
              1. number 9 line 1

              1. number 10 line 2
                1. number 2 times line 1

                1. number 2 times line 2
              '; - importrequest(htmlWithBullets, importurl, 'html'); - expect(getinnertext()).to.be('\ -
              1. number 9 line 1
              \n\ -
              \n\ -
              1. number 10 line 2
              \n\ -
              1. number 2 times line 1
              \n\ -
              \n\ -
              1. number 2 times line 2
              \n\ -
              \n'); - const results = exportfunc(helper.padChrome$.window.location.href); - console.error(results); - done(); - }); - xit('import a pad with nested ordered lists and attributes and newlines from html', function (done) { - const importurl = `${helper.padChrome$.window.location.href}/import`; - const htmlWithBullets = '
              1. bold strikethrough italics underline line 1bold

              1. number 10 line 2
                1. number 2 times line 1

                1. number 2 times line 2
              '; - importrequest(htmlWithBullets, importurl, 'html'); - expect(getinnertext()).to.be('\ -
              1. bold strikethrough italics underline line 1bold
              \n\ -
              \n\ -
              1. number 10 line 2
              \n\ -
              1. number 2 times line 1
              \n\ -
              \n\ -
              1. number 2 times line 2
              \n\ -
              \n'); - const results = exportfunc(helper.padChrome$.window.location.href); - console.error(results); - done(); - }); -}); diff --git a/tests/frontend/specs/ordered_list.js b/tests/frontend/specs/ordered_list.js deleted file mode 100644 index a932335e8..000000000 --- a/tests/frontend/specs/ordered_list.js +++ /dev/null @@ -1,180 +0,0 @@ -describe('assign ordered list', function () { - // create a new pad before each test run - beforeEach(function (cb) { - helper.newPad(cb); - this.timeout(60000); - }); - - it('inserts ordered list text', function (done) { - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist'); - $insertorderedlistButton.click(); - - helper.waitFor(() => inner$('div').first().find('ol li').length === 1).done(done); - }); - - context('when user presses Ctrl+Shift+N', function () { - context('and pad shortcut is enabled', function () { - beforeEach(function () { - makeSureShortcutIsEnabled('cmdShiftN'); - triggerCtrlShiftShortcut('N'); - }); - - it('inserts unordered list', function (done) { - helper.waitFor(() => helper.padInner$('div').first().find('ol li').length === 1).done(done); - }); - }); - - context('and pad shortcut is disabled', function () { - beforeEach(function () { - makeSureShortcutIsDisabled('cmdShiftN'); - triggerCtrlShiftShortcut('N'); - }); - - it('does not insert unordered list', function (done) { - helper.waitFor(() => helper.padInner$('div').first().find('ol li').length === 1).done(() => { - expect().fail(() => 'Unordered list inserted, should ignore shortcut'); - }).fail(() => { - done(); - }); - }); - }); - }); - - context('when user presses Ctrl+Shift+1', function () { - context('and pad shortcut is enabled', function () { - beforeEach(function () { - makeSureShortcutIsEnabled('cmdShift1'); - triggerCtrlShiftShortcut('1'); - }); - - it('inserts unordered list', function (done) { - helper.waitFor(() => helper.padInner$('div').first().find('ol li').length === 1).done(done); - }); - }); - - context('and pad shortcut is disabled', function () { - beforeEach(function () { - makeSureShortcutIsDisabled('cmdShift1'); - triggerCtrlShiftShortcut('1'); - }); - - it('does not insert unordered list', function (done) { - helper.waitFor(() => helper.padInner$('div').first().find('ol li').length === 1).done(() => { - expect().fail(() => 'Unordered list inserted, should ignore shortcut'); - }).fail(() => { - done(); - }); - }); - }); - }); - - xit('issue #1125 keeps the numbered list on enter for the new line - EMULATES PASTING INTO A PAD', function (done) { - 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}'); - - helper.waitFor(() => inner$('div span').first().text().indexOf('line 2') === -1).done(() => { - 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; - expect(hasLineNumber).to.be(true); // This doesn't work because pasting in content doesn't work - done(); - }); - }); - - var triggerCtrlShiftShortcut = function (shortcutChar) { - const inner$ = helper.padInner$; - const e = inner$.Event(helper.evtType); - e.ctrlKey = true; - e.shiftKey = true; - e.which = shortcutChar.toString().charCodeAt(0); - inner$('#innerdocbody').trigger(e); - }; - - var makeSureShortcutIsDisabled = function (shortcut) { - helper.padChrome$.window.clientVars.padShortcutEnabled[shortcut] = false; - }; - var makeSureShortcutIsEnabled = function (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(function (cb) { - helper.newPad(cb); - this.timeout(60000); - }); - - it('indent and de-indent list item with keypress', 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}'); - - const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist'); - $insertorderedlistButton.click(); - - const e = 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); - - helper.waitFor(() => inner$('div').first().find('.list-number1').length === 1).done(done); - }); -}); - - -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(function (cb) { - helper.newPad(cb); - this.timeout(60000); - }); - - it('indent and de-indent list item with indent button', 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}'); - - 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 - - helper.waitFor(() => inner$('div').first().find('.list-number1').length === 1).done(done); - }); -}); diff --git a/tests/frontend/specs/unordered_list.js b/tests/frontend/specs/unordered_list.js deleted file mode 100644 index 4cbdabfac..000000000 --- a/tests/frontend/specs/unordered_list.js +++ /dev/null @@ -1,162 +0,0 @@ -describe('assign unordered list', function () { - // create a new pad before each test run - beforeEach(function (cb) { - helper.newPad(cb); - this.timeout(60000); - }); - - it('insert unordered list text then removes by outdent', function (done) { - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - const originalText = inner$('div').first().text(); - - const $insertunorderedlistButton = chrome$('.buttonicon-insertunorderedlist'); - $insertunorderedlistButton.click(); - - helper.waitFor(() => { - const newText = inner$('div').first().text(); - if (newText === originalText) { - return inner$('div').first().find('ul li').length === 1; - } - }).done(() => { - // remove indentation by bullet and ensure text string remains the same - chrome$('.buttonicon-outdent').click(); - helper.waitFor(() => { - const newText = inner$('div').first().text(); - return (newText === originalText); - }).done(() => { - done(); - }); - }); - }); -}); - -describe('unassign unordered list', function () { - // create a new pad before each test run - beforeEach(function (cb) { - helper.newPad(cb); - this.timeout(60000); - }); - - it('insert unordered list text then remove by clicking list again', function (done) { - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - const originalText = inner$('div').first().text(); - - const $insertunorderedlistButton = chrome$('.buttonicon-insertunorderedlist'); - $insertunorderedlistButton.click(); - - helper.waitFor(() => { - const newText = inner$('div').first().text(); - if (newText === originalText) { - return inner$('div').first().find('ul li').length === 1; - } - }).done(() => { - // remove indentation by bullet and ensure text string remains the same - $insertunorderedlistButton.click(); - helper.waitFor(() => { - const isList = inner$('div').find('ul').length === 1; - // sohuldn't be list - return (isList === false); - }).done(() => { - done(); - }); - }); - }); -}); - - -describe('keep unordered list on enter key', function () { - // create a new pad before each test run - beforeEach(function (cb) { - helper.newPad(cb); - this.timeout(60000); - }); - - it('Keeps the unordered list on enter for the new line', function (done) { - 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}'); - - helper.waitFor(() => inner$('div span').first().text().indexOf('line 2') === -1).done(() => { - 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'); - done(); - }); - }); -}); - -describe('Pressing Tab in an UL increases and decreases indentation', function () { - // create a new pad before each test run - beforeEach(function (cb) { - helper.newPad(cb); - this.timeout(60000); - }); - - it('indent and de-indent list item with keypress', 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}'); - - const $insertorderedlistButton = chrome$('.buttonicon-insertunorderedlist'); - $insertorderedlistButton.click(); - - const e = 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); - - helper.waitFor(() => inner$('div').first().find('.list-bullet1').length === 1).done(done); - }); -}); - -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(function (cb) { - helper.newPad(cb); - this.timeout(60000); - }); - - it('indent and de-indent list item with indent button', 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}'); - - 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 - - helper.waitFor(() => inner$('div').first().find('.list-bullet1').length === 1).done(done); - }); -}); diff --git a/tests/frontend/travis/remote_runner.js b/tests/frontend/travis/remote_runner.js deleted file mode 100644 index 70c850ca8..000000000 --- a/tests/frontend/travis/remote_runner.js +++ /dev/null @@ -1,166 +0,0 @@ -var srcFolder = '../../../src/node_modules/'; -var wd = require(`${srcFolder}wd`); -var async = require(`${srcFolder}async`); - -var config = { - host: 'ondemand.saucelabs.com', - port: 80, - username: process.env.SAUCE_USER, - accessKey: process.env.SAUCE_ACCESS_KEY, -}; - -var allTestsPassed = true; -// overwrite the default exit code -// in case not all worker can be run (due to saucelabs limits), `queue.drain` below will not be called -// and the script would silently exit with error code 0 -process.exitCode = 2; -process.on('exit', (code) => { - if (code === 2) { - console.log('\x1B[31mFAILED\x1B[39m Not all saucelabs runner have been started.'); - } -}); - -var sauceTestWorker = async.queue((testSettings, callback) => { - const browser = wd.promiseChainRemote(config.host, config.port, config.username, config.accessKey); - const name = `${process.env.GIT_HASH} - ${testSettings.browserName} ${testSettings.version}, ${testSettings.platform}`; - testSettings.name = name; - testSettings.public = true; - testSettings.build = process.env.GIT_HASH; - testSettings.extendedDebugging = true; // console.json can be downloaded via saucelabs, don't know how to print them into output of the tests - testSettings.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER; - - browser.init(testSettings).get('http://localhost:9001/tests/frontend/', () => { - const url = `https://saucelabs.com/jobs/${browser.sessionID}`; - console.log(`Remote sauce test '${name}' started! ${url}`); - - // tear down the test excecution - const stopSauce = function (success, timesup) { - clearInterval(getStatusInterval); - clearTimeout(timeout); - - browser.quit(() => { - if (!success) { - allTestsPassed = false; - } - - // if stopSauce is called via timeout (in contrast to via getStatusInterval) than the log of up to the last - // five seconds may not be available here. It's an error anyway, so don't care about it. - printLog(logIndex); - - if (timesup) { - console.log(`[${testSettings.browserName} ${testSettings.platform}${testSettings.version === '' ? '' : (` ${testSettings.version}`)}] \x1B[31mFAILED\x1B[39m allowed test duration exceeded`); - } - console.log(`Remote sauce test '${name}' finished! ${url}`); - - callback(); - }); - }; - - /** - * timeout if a test hangs or the job exceeds 14.5 minutes - * It's necessary because if travis kills the saucelabs session due to inactivity, we don't get any output - * @todo this should be configured in testSettings, see https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options#TestConfigurationOptions-Timeouts - */ - var timeout = setTimeout(() => { - stopSauce(false, true); - }, 870000); // travis timeout is 15 minutes, set this to a slightly lower value - - let knownConsoleText = ''; - // how many characters of the log have been sent to travis - let logIndex = 0; - var getStatusInterval = setInterval(() => { - browser.eval("$('#console').text()", (err, consoleText) => { - if (!consoleText || err) { - return; - } - knownConsoleText = consoleText; - - if (knownConsoleText.indexOf('FINISHED') > 0) { - const match = knownConsoleText.match(/FINISHED.*([0-9]+) tests passed, ([0-9]+) tests failed/); - // finished without failures - if (match[2] && match[2] == '0') { - stopSauce(true); - - // finished but some tests did not return or some tests failed - } else { - stopSauce(false); - } - } else { - // not finished yet - printLog(logIndex); - logIndex = knownConsoleText.length; - } - }); - }, 5000); - - /** - * Replaces color codes in the test runners log, appends - * browser name, platform etc. to every line and prints them. - * - * @param {number} index offset from where to start - */ - function printLog(index) { - let testResult = knownConsoleText.substring(index).replace(/\[red\]/g, '\x1B[31m').replace(/\[yellow\]/g, '\x1B[33m') - .replace(/\[green\]/g, '\x1B[32m').replace(/\[clear\]/g, '\x1B[39m'); - testResult = testResult.split('\\n').map((line) => `[${testSettings.browserName} ${testSettings.platform}${testSettings.version === '' ? '' : (` ${testSettings.version}`)}] ${line}`).join('\n'); - - console.log(testResult); - } - }); -}, 6); // run 6 tests in parrallel - -// 1) Firefox on Linux -sauceTestWorker.push({ - platform: 'Windows 7', - browserName: 'firefox', - version: '52.0', -}); - -// 2) Chrome on Linux -sauceTestWorker.push({ - platform: 'Windows 7', - browserName: 'chrome', - version: '55.0', - args: ['--use-fake-device-for-media-stream'], -}); - -/* -// 3) Safari on OSX 10.15 -sauceTestWorker.push({ - 'platform' : 'OS X 10.15' - , 'browserName' : 'safari' - , 'version' : '13.1' -}); -*/ - -// 4) Safari on OSX 10.14 -sauceTestWorker.push({ - platform: 'OS X 10.15', - browserName: 'safari', - version: '13.1', -}); -// IE 10 doesn't appear to be working anyway -/* -// 4) IE 10 on Win 8 -sauceTestWorker.push({ - 'platform' : 'Windows 8' - , 'browserName' : 'iexplore' - , 'version' : '10.0' -}); -*/ -// 5) Edge on Win 10 -sauceTestWorker.push({ - platform: 'Windows 10', - browserName: 'microsoftedge', - version: '83.0', -}); -// 6) Firefox on Win 7 -sauceTestWorker.push({ - platform: 'Windows 7', - browserName: 'firefox', - version: '78.0', -}); - -sauceTestWorker.drain(() => { - process.exit(allTestsPassed ? 0 : 1); -}); diff --git a/tests/ratelimit/Dockerfile.anotherip b/tests/ratelimit/Dockerfile.anotherip deleted file mode 100644 index 5b9d1d21a..000000000 --- a/tests/ratelimit/Dockerfile.anotherip +++ /dev/null @@ -1,4 +0,0 @@ -FROM node:alpine3.12 -WORKDIR /tmp -RUN npm i etherpad-cli-client -COPY ./tests/ratelimit/send_changesets.js /tmp/send_changesets.js diff --git a/tests/ratelimit/Dockerfile.nginx b/tests/ratelimit/Dockerfile.nginx deleted file mode 100644 index ba8dd358f..000000000 --- a/tests/ratelimit/Dockerfile.nginx +++ /dev/null @@ -1,2 +0,0 @@ -FROM nginx -COPY ./tests/ratelimit/nginx.conf /etc/nginx/nginx.conf