diff --git a/.gitignore b/.gitignore index 91ea81bfc4..1eb785451f 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ config/gitlab.yml config/gitlab_ci.yml config/initializers/rack_attack.rb config/initializers/smtp_settings.rb +config/initializers/relative_url.rb config/resque.yml config/unicorn.rb config/secrets.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d803f3c860..8a729f957a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,11 +5,16 @@ services: - postgres:latest - redis:latest +cache: + key: "ruby21" + paths: + - vendor + variables: MYSQL_ALLOW_EMPTY_PASSWORD: "1" before_script: - - ./scripts/prepare_build.sh + - source ./scripts/prepare_build.sh - ruby -v - which ruby - gem install bundler --no-ri --no-rdoc @@ -17,7 +22,7 @@ before_script: - touch log/application.log - touch log/test.log - bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" - - bundle exec rake db:reset db:create RAILS_ENV=test + - RAILS_ENV=test bundle exec rake db:drop db:create db:schema:load db:migrate spec:feature: script: @@ -127,10 +132,155 @@ flay: - mysql bundler:audit: - script: + script: - "bundle exec bundle-audit update" - "bundle exec bundle-audit check" tags: - ruby - mysql allow_failure: true + +# Ruby 2.2 jobs + +spec:feature:ruby22: + image: ruby:2.2 + only: + - master + script: + - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature + cache: + key: "ruby22" + paths: + - vendor + tags: + - ruby + - mysql + +spec:api:ruby22: + image: ruby:2.2 + only: + - master + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api + cache: + key: "ruby22" + paths: + - vendor + tags: + - ruby + - mysql + +spec:models:ruby22: + image: ruby:2.2 + only: + - master + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models + cache: + key: "ruby22" + paths: + - vendor + tags: + - ruby + - mysql + +spec:lib:ruby22: + image: ruby:2.2 + only: + - master + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib + cache: + key: "ruby22" + paths: + - vendor + tags: + - ruby + - mysql + +spec:services:ruby22: + image: ruby:2.2 + only: + - master + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services + cache: + key: "ruby22" + paths: + - vendor + tags: + - ruby + - mysql + +spec:benchmark:ruby22: + image: ruby:2.2 + only: + - master + script: + - RAILS_ENV=test bundle exec rake spec:benchmark + cache: + key: "ruby22" + paths: + - vendor + tags: + - ruby + - mysql + allow_failure: true + +spec:other:ruby22: + image: ruby:2.2 + only: + - master + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other + cache: + key: "ruby22" + paths: + - vendor + tags: + - ruby + - mysql + +spinach:project:half:ruby22: + image: ruby:2.2 + only: + - master + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half + cache: + key: "ruby22" + paths: + - vendor + tags: + - ruby + - mysql + +spinach:project:rest:ruby22: + image: ruby:2.2 + only: + - master + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest + cache: + key: "ruby22" + paths: + - vendor + tags: + - ruby + - mysql + +spinach:other:ruby22: + image: ruby:2.2 + only: + - master + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other + cache: + key: "ruby22" + paths: + - vendor + tags: + - ruby + - mysql + diff --git a/.ruby-version b/.ruby-version index 04b10b4f15..ebf14b4698 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.1.7 +2.1.8 diff --git a/CHANGELOG b/CHANGELOG index 24c08358b6..bb7760bfce 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,163 @@ Please view this file on the master branch, on stable branches it's out of date. +v 8.6.0 (unreleased) + - Contributions to forked projects are included in calendar + - Improve the formatting for the user page bio (Connor Shea) + - Fix issue when pushing to projects ending in .wiki + - Fix avatar stretching by providing a cropping feature (Johann Pardanaud) + - Don't load all of GitLab in mail_room + - Strip leading and trailing spaces in URL validator (evuez) + - Return empty array instead of 404 when commit has no statuses in commit status API + - Update documentation to reflect Guest role not being enforced on internal projects + - Allow search for logged out users + - Don't show Issues/MRs from archived projects in Groups view + - Increase the notes polling timeout over time (Roberto Dip) + - Add shortcut to toggle markdown preview (Florent Baldino) + - Show labels in dashboard and group milestone views + - Add main language of a project in the list of projects (Tiago Botelho) + - Add ability to show archived projects on dashboard, explore and group pages + +v 8.5.8 + - Bump Git version requirement to 2.7.4 + +v 8.5.7 + - Bump Git version requirement to 2.7.3 + +v 8.5.6 + - Obtain a lease before querying LDAP + +v 8.5.5 + - Ensure removing a project removes associated Todo entries + - Prevent a 500 error in Todos when author was removed + - Fix pagination for filtered dashboard and explore pages + - Fix "Show all" link behavior + +v 8.5.4 + - Do not cache requests for badges (including builds badge) + +v 8.5.3 + - Flush repository caches before renaming projects + +v 8.5.2 + - Fix sidebar overlapping content when screen width was below 1200px + - Don't repeat labels listed on Labels tab + - Bring the "branded appearance" feature from EE to CE + - Fix error 500 when commenting on a commit + - Show days remaining instead of elapsed time for Milestone + - Fix broken icons on installations with relative URL (Artem Sidorenko) + - Fix issue where tag list wasn't refreshed after deleting a tag + - Fix import from gitlab.com (KazSawada) + - Improve implementation to check read access to forks and add pagination + - Don't show any "2FA required" message if it's not actually required + - Fix help keyboard shortcut on relative URL setups (Artem Sidorenko) + - Update Rails to 4.2.5.2 + - Fix permissions for deprecated CI build status badge + - Don't show "Welcome to GitLab" when the search didn't return any projects + - Add Todos documentation + +v 8.5.1 + - Fix group projects styles + - Show Crowd login tab when sign in is disabled and Crowd is enabled (Peter Hudec) + - Fix a set of small UI glitches in project, profile, and wiki pages + - Restrict permissions on public/uploads + - Fix the merge request side-by-side view after loading diff results + - Fix the look of tooltip for the "Revert" button + - Add when the Builds & Runners API changes got introduced + - Fix error 500 on some merged merge requests + - Fix an issue causing the content of the issuable sidebar to disappear + - Fix error 500 when trying to mark an already done todo as "done" + - Fix an issue where MRs weren't sortable + - Issues can now be dragged & dropped into empty milestone lists. This is also + possible with MRs + - Changed padding & background color for highlighted notes + - Re-add the newrelic_rpm gem which was removed without any deprecation or warning (Stan Hu) + - Update sentry-raven gem to 0.15.6 + +v 8.5.0 + - Fix duplicate "me" in tooltip of the "thumbsup" awards Emoji (Stan Hu) + - Cache various Repository methods to improve performance (Yorick Peterse) + - Fix duplicated branch creation/deletion Web hooks/service notifications when using Web UI (Stan Hu) + - Ensure rake tasks that don't need a DB connection can be run without one + - Update New Relic gem to 3.14.1.311 (Stan Hu) + - Add "visibility" flag to GET /projects api endpoint + - Add an option to supply root email through an environmental variable (Koichiro Mikami) + - Ignore binary files in code search to prevent Error 500 (Stan Hu) + - Render sanitized SVG images (Stan Hu) + - Support download access by PRIVATE-TOKEN header (Stan Hu) + - Upgrade gitlab_git to 7.2.23 to fix commit message mentions in first branch push + - Add option to include the sender name in body of Notify email (Jason Lee) + - New UI for pagination + - Don't prevent sign out when 2FA enforcement is enabled and user hasn't yet + set it up + - API: Added "merge_requests/:merge_request_id/closes_issues" (Gal Schlezinger) + - Fix diff comments loaded by AJAX to load comment with diff in discussion tab + - Fix relative links in other markup formats (Ben Boeckel) + - Whitelist raw "abbr" elements when parsing Markdown (Benedict Etzel) + - Fix label links for a merge request pointing to issues list + - Don't vendor minified JS + - Increase project import timeout to 15 minutes + - Be more permissive with email address validation: it only has to contain a single '@' + - Display 404 error on group not found + - Track project import failure + - Support Two-factor Authentication for LDAP users + - Display database type and version in Administration dashboard + - Allow limited Markdown in Broadcast Messages + - Fix visibility level text in admin area (Zeger-Jan van de Weg) + - Warn admin during OAuth of granting admin rights (Zeger-Jan van de Weg) + - Update the ExternalIssue regex pattern (Blake Hitchcock) + - Remember user's inline/side-by-side diff view preference in a cookie (Kirill Katsnelson) + - Optimized performance of finding issues to be closed by a merge request + - Add `avatar_url`, `description`, `git_ssh_url`, `git_http_url`, `path_with_namespace` + and `default_branch` in `project` in push, issue, merge-request and note webhooks data (Kirill Zaitsev) + - Deprecate the `ssh_url` in favor of `git_ssh_url` and `http_url` in favor of `git_http_url` + in `project` for push, issue, merge-request and note webhooks data (Kirill Zaitsev) + - Deprecate the `repository` key in push, issue, merge-request and note webhooks data, use `project` instead (Kirill Zaitsev) + - API: Expose MergeRequest#merge_status (Andrei Dziahel) + - Revert "Add IP check against DNSBLs at account sign-up" + - Actually use the `skip_merges` option in Repository#commits (Tony Chu) + - Fix API to keep request parameters in Link header (Michael Potthoff) + - Deprecate API "merge_request/:merge_request_id/comments". Use "merge_requests/:merge_request_id/notes" instead + - Deprecate API "merge_request/:merge_request_id/...". Use "merge_requests/:merge_request_id/..." instead + - Prevent parse error when name of project ends with .atom and prevent path issues + - Discover branches for commit statuses ref-less when doing merge when succeeded + - Mark inline difference between old and new paths when a file is renamed + - Support Akismet spam checking for creation of issues via API (Stan Hu) + - API: Allow to set or update a merge-request's milestone (Kirill Skachkov) + - Improve UI consistency between projects and groups lists + - Add sort dropdown to dashboard projects page + - Fixed logo animation on Safari (Roman Rott) + - Fix Merge When Succeeded when multiple stages + - Hide remove source branch button when the MR is merged but new commits are pushed (Zeger-Jan van de Weg) + - In seach autocomplete show only groups and projects you are member of + - Don't process cross-reference notes from forks + - Fix: init.d script not working on OS X + - Faster snippet search + - Added API to download build artifacts + - Title for milestones should be unique (Zeger-Jan van de Weg) + - Validate correctness of maximum attachment size application setting + - Replaces "Create merge request" link with one to the "Merge Request" when one exists + - Fix CI builds badge, add a new link to builds badge, deprecate the old one + - Fix broken link to project in build notification emails + - Ability to see and sort on vote count from Issues and MR lists + - Fix builds scheduler when first build in stage was allowed to fail + - User project limit is reached notice is hidden if the projects limit is zero + - Add API support for managing runners and project's runners + - Allow SAML users to login with no previous account without having to allow + all Omniauth providers to do so. + - Allow existing users to auto link their SAML credentials by logging in via SAML + - Make it possible to erase a build (trace, artifacts) using UI and API + - Ability to revert changes from a Merge Request or Commit + - Emoji comment on diffs are not award emoji + - Add label description (Nuttanart Pornprasitsakul) + - Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul) + - Add Todos + +v 8.4.4 + - Update omniauth-saml gem to 1.4.2 + - Prevent long-running backup tasks from timing out the database connection + - Add a Project setting to allow guests to view build logs (defaults to true) + - Sort project milestones by due date including issue editor (Oliver Rogers / Orih) + v 8.4.3 - Increase lfs_objects size column to 8-byte integer to allow files larger than 2.1GB @@ -22,12 +180,14 @@ v 8.4.2 track them in Performance Monitoring. - Increase contrast between highlighted code comments and inline diff marker - Fix method undefined when using external commit status in builds + - Fix highlighting in blame view. v 8.4.1 - Apply security updates for Rails (4.2.5.1), rails-html-sanitizer (1.0.3), and Nokogiri (1.6.7.2) - Fix redirect loop during import - Fix diff highlighting for all syntax themes + - Delete project and associations in a background worker v 8.4.0 - Allow LDAP users to change their email if it was not set by the LDAP server @@ -71,7 +231,7 @@ v 8.4.0 - Show 'All' tab by default in the builds page - Add Open Graph and Twitter Card data to all pages - Fix API project lookups when querying with a namespace with dots (Stan Hu) - - Enable forcing Two-Factor authentication sitewide, with optional grace period + - Enable forcing Two-factor authentication sitewide, with optional grace period - Import GitHub Pull Requests into GitLab - Change single user API endpoint to return more detailed data (Michael Potthoff) - Update version check images to use SVG @@ -149,6 +309,7 @@ v 8.3.0 - Handle and report SSL errors in Web hook test (Stan Hu) - Bump Redis requirement to 2.8 for Sidekiq 4 (Stan Hu) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) + - WIP identifier on merge requests no longer requires trailing space - Add rake tasks for git repository maintainance (Zeger-Jan van de Weg) - Fix 500 error when update group member permission - Fix: As an admin, cannot add oneself as a member to a group/project @@ -331,6 +492,7 @@ v 8.1.0 - Improved performance of the trending projects page - Remove CI migration task - Improved performance of finding projects by their namespace + - Add assignee data to Issuables' hook_data (Bram Daams) - Fix bug where transferring a project would result in stale commit links (Stan Hu) - Fix build trace updating - Include full path of source and target branch names in New Merge Request page (Stan Hu) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1eabbdc5ca..c4522998f4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,29 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Contribute to GitLab](#contribute-to-gitlab) + - [Contributor license agreement](#contributor-license-agreement) + - [Security vulnerability disclosure](#security-vulnerability-disclosure) + - [Closing policy for issues and merge requests](#closing-policy-for-issues-and-merge-requests) + - [Helping others](#helping-others) + - [I want to contribute!](#i-want-to-contribute) + - [Issue tracker](#issue-tracker) + - [Feature proposals](#feature-proposals) + - [Issue tracker guidelines](#issue-tracker-guidelines) + - [Issue weight](#issue-weight) + - [Regression issues](#regression-issues) + - [Merge requests](#merge-requests) + - [Merge request guidelines](#merge-request-guidelines) + - [Merge request description format](#merge-request-description-format) + - [Contribution acceptance criteria](#contribution-acceptance-criteria) + - [Changes for Stable Releases](#changes-for-stable-releases) + - [Definition of done](#definition-of-done) + - [Style guides](#style-guides) + - [Code of conduct](#code-of-conduct) + + + # Contribute to GitLab Thank you for your interest in contributing to GitLab. This guide details how @@ -147,7 +173,7 @@ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true) sudo gitlab-rake gitlab:env:info) (For installations from source run and paste the output of: -sudo -u git -H bundle exec rake gitlab:env:info) +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production) ## Possible fixes @@ -177,6 +203,26 @@ is probably 1, adding a new Git Hook maybe 4 or 5, big features 7-9. issues or chunks. You can simply not set the weight of a parent issue and set weights to children issues. +### Regression issues + +Every monthly release has a corresponding issue on the CE issue tracker to keep +track of functionality broken by that release and any fixes that need to be +included in a patch release (see [8.3 Regressions] as an example). + +As outlined in the issue description, the intended workflow is to post one note +with a reference to an issue describing the regression, and then to update that +note with a reference to the merge request that fixes it as it becomes available. + +If you're a contributor who doesn't have the required permissions to update +other users' notes, please post a new note with a reference to both the issue +and the merge request. + +The release manager will [update the notes] in the regression issue as fixes are +addressed. + +[8.3 Regressions]: https://gitlab.com/gitlab-org/gitlab-ce/issues/4127 +[update the notes]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/pro-tips.md#update-the-regression-issue + ## Merge requests We welcome merge requests with fixes and improvements to GitLab code, tests, @@ -214,15 +260,17 @@ request is as follows: 1. Add your changes to the [CHANGELOG](CHANGELOG) 1. If you are changing the README, some documentation or other things which have no effect on the tests, add `[ci skip]` somewhere in the commit message + and make sure to read the [documentation styleguide][doc-styleguide] 1. If you have multiple commits please combine them into one commit by [squashing them][git-squash] 1. Push the commit(s) to your fork 1. Submit a merge request (MR) to the master branch 1. The MR title should describe the change you want to make 1. The MR description should give a motive for your change and the method you - used to achieve it + used to achieve it, see the [merge request description format] + (#merge-request-description-format) 1. If the MR changes the UI it should include before and after screenshots -1. If the MR changes CSS classes please include the list of affected pages +1. If the MR changes CSS classes please include the list of affected pages, `grep css-class ./app -R` 1. Link any relevant [issues][ce-tracker] in the merge request description and leave a comment on them with a link back to the MR @@ -255,6 +303,69 @@ For examples of feedback on merge requests please look at already request feel free to mention one of the Merge Marshalls of the [core team][]. Please ensure that your merge request meets the contribution acceptance criteria. +When having your code reviewed and when reviewing merge requests please take the +[thoughtbot code review guidelines](https://github.com/thoughtbot/guides/tree/master/code-review) +into account. + +### Merge request description format + +Please submit merge requests using the following template in the merge request +description area. Copy-paste it to retain the markdown format. + +``` +## What does this MR do? + +## Are there points in the code the reviewer needs to double check? + +## Why was this MR needed? + +## What are the relevant issue numbers? + +## Screenshots (if relevant) +``` + +### Contribution acceptance criteria + +1. The change is as small as possible +1. Include proper tests and make all tests pass (unless it contains a test + exposing a bug in existing code) +1. If you suspect a failing CI build is unrelated to your contribution, you may + try and restart the failing CI job or ask a developer to fix the + aforementioned failing test +1. Your MR initially contains a single commit (please use `git rebase -i` to + squash commits) +1. Your changes can merge without problems (if not please merge `master`, never + rebase commits pushed to the remote server) +1. Does not break any existing functionality +1. Fixes one specific issue or implements one specific feature (do not combine + things, send separate merge requests if needed) +1. Migrations should do only one thing (e.g., either create a table, move data + to a new table or remove an old table) to aid retrying on failure +1. Keeps the GitLab code base clean and well structured +1. Contains functionality we think other users will benefit from too +1. Doesn't add configuration options since they complicate future changes +1. Changes after submitting the merge request should be in separate commits + (no squashing). If necessary, you will be asked to squash when the review is + over, before merging. +1. It conforms to the [style guides](#style-guides) and the following: + - If your change touches a line that does not follow the style, modify the + entire line to follow it. This prevents linting tools from generating warnings. + - Don't touch neighbouring lines. As an exception, automatic mass + refactoring modifications may leave style non-compliant. + +## Changes for Stable Releases + +Sometimes certain changes have to be added to an existing stable release. +Two examples are bug fixes and performance improvements. In these cases the +corresponding merge request should be updated to have the following: + +1. A milestone indicating what release the merge request should be merged into. +1. The label "Pick into Stable" + +This makes it easier for release managers to keep track of what still has to be +merged and where changes have to be merged into. +Like all merge requests the target should be master so all bugfixes are in master. + ## Definition of done If you contribute to GitLab please know that changes involve more than just @@ -264,7 +375,7 @@ the feature you contribute through all of these steps. 1. Description explaining the relevancy (see following item) 1. Working and clean code that is commented where needed 1. Unit and integration tests that pass on the CI server -1. Documented in the /doc directory +1. [Documented][doc-styleguide] in the /doc directory 1. Changelog entry added 1. Reviewed and any concerns are addressed 1. Merged by the project lead @@ -285,43 +396,6 @@ merge request: 1. Test suite https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/prepare_build.sh 1. Omnibus package creator https://gitlab.com/gitlab-org/omnibus-gitlab -## Merge request description format - -1. What does this MR do? -1. Are there points in the code the reviewer needs to double check? -1. Why was this MR needed? -1. What are the relevant issue numbers? -1. Screenshots (if relevant) - -## Contribution acceptance criteria - -1. The change is as small as possible (see the above paragraph for details) -1. Include proper tests and make all tests pass (unless it contains a test - exposing a bug in existing code) -1. If you suspect a failing CI build is unrelated to your contribution, you may - try and restart the failing CI job or ask a developer to fix the - aforementioned failing test -1. Your MR initially contains a single commit (please use `git rebase -i` to - squash commits) -1. Your changes can merge without problems (if not please merge `master`, never - rebase commits pushed to the remote server) -1. Does not break any existing functionality -1. Fixes one specific issue or implements one specific feature (do not combine - things, send separate merge requests if needed) -1. Migrations should do only one thing (eg: either create a table, move data to - a new table or remove an old table) to aid retrying on failure -1. Keeps the GitLab code base clean and well structured -1. Contains functionality we think other users will benefit from too -1. Doesn't add configuration options since they complicate future changes -1. Changes after submitting the merge request should be in separate commits - (no squashing). If necessary, you will be asked to squash when the review is - over, before merging. -1. It conforms to the following style guides: - * If your change touches a line that does not follow the style, modify the - entire line to follow it. This prevents linting tools from generating warnings. - * Don't touch neighbouring lines. As an exception, automatic mass - refactoring modifications may leave style non-compliant. - ## Style guides 1. [Ruby](https://github.com/bbatsov/ruby-style-guide). @@ -336,7 +410,7 @@ merge request: contributors to enhance security 1. [Database Migrations](doc/development/migration_style_guide.md) 1. [Markdown](http://www.cirosantilli.com/markdown-styleguide) -1. [Documentation styleguide](doc/development/doc_styleguide.md) +1. [Documentation styleguide][doc-styleguide] 1. Interface text should be written subjectively instead of objectively. It should be the GitLab core team addressing a person. It should be written in present time and never use past tense (has been/was). For example instead @@ -377,7 +451,7 @@ reported by emailing `contact@gitlab.com`. This Code of Conduct is adapted from the [Contributor Covenant][], version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/). -[core team]: https://about.gitlab.com/core-team/ +[core-team]: https://about.gitlab.com/core-team/ [getting help page]: https://about.gitlab.com/getting-help/ [Codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq [up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=up-for-grabs @@ -398,3 +472,4 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [Contributor Covenant]: http://contributor-covenant.org [rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout [rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming +[doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide" diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index b616048743..d2b13eb644 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -0.6.2 +0.6.4 diff --git a/Gemfile b/Gemfile index acd187400a..db0e7d9766 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source "https://rubygems.org" -gem 'rails', '4.2.5.1' +gem 'rails', '4.2.5.2' gem 'rails-deprecated_sanitizer', '~> 1.0.3' # Responders respond_to and respond_with @@ -21,7 +21,7 @@ gem "pg", '~> 0.18.2', group: :postgres gem 'devise', '~> 3.5.4' gem 'devise-async', '~> 0.9.0' gem 'doorkeeper', '~> 2.2.0' -gem 'omniauth', '~> 1.2.2' +gem 'omniauth', '~> 1.3.1' gem 'omniauth-azure-oauth2', '~> 0.0.6' gem 'omniauth-bitbucket', '~> 0.0.2' gem 'omniauth-cas3', '~> 1.1.2' @@ -30,14 +30,15 @@ gem 'omniauth-github', '~> 1.1.1' gem 'omniauth-gitlab', '~> 1.0.0' gem 'omniauth-google-oauth2', '~> 0.2.0' gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos -gem 'omniauth-saml', '~> 1.4.0' +gem 'omniauth-saml', '~> 1.4.2' gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth_crowd', '~> 2.2.0' gem 'rack-oauth2', '~> 1.2.1' -# reCAPTCHA protection +# Spam and anti-bot protection gem 'recaptcha', require: 'recaptcha/rails' +gem 'akismet', '~> 2.0' # Two-factor authentication gem 'devise-two-factor', '~> 2.0.0' @@ -49,7 +50,7 @@ gem "browser", '~> 1.0.0' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '~> 7.2.22' +gem "gitlab_git", '~> 8.2' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes @@ -104,7 +105,7 @@ gem 'rouge', '~> 1.10.1' # See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s # and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM -gem 'nokogiri', '1.6.7.2' +gem 'nokogiri', '~> 1.6.7', '>= 1.6.7.2' # Diffs gem 'diffy', '~> 3.0.3' @@ -179,6 +180,9 @@ gem "underscore-rails", "~> 1.8.0" gem "sanitize", '~> 2.0' gem 'babosa', '~> 1.0.2' +# Sanitizes SVG input +gem "loofah", "~> 2.0.3" + # Protect against bruteforcing gem "rack-attack", '~> 4.3.1' @@ -200,7 +204,7 @@ gem 'jquery-turbolinks', '~> 2.1.0' gem 'addressable', '~> 2.3.8' gem 'bootstrap-sass', '~> 3.3.0' gem 'font-awesome-rails', '~> 4.2' -gem 'gitlab_emoji', '~> 0.2.0' +gem 'gitlab_emoji', '~> 0.3.0' gem 'gon', '~> 6.0.1' gem 'jquery-atwho-rails', '~> 1.3.2' gem 'jquery-rails', '~> 4.0.0' @@ -213,6 +217,9 @@ gem 'select2-rails', '~> 3.5.9' gem 'virtus', '~> 1.0.1' gem 'net-ssh', '~> 3.0.1' +# Sentry integration +gem 'sentry-raven', '~> 0.15' + # Metrics group :metrics do gem 'allocations', '~> 1.0', require: false, platform: :mri @@ -294,15 +301,11 @@ end group :production do gem "gitlab_meta", '7.0' - - # Sentry integration - gem 'sentry-raven' end -gem "newrelic_rpm", '~> 3.9.4.245' -gem 'newrelic-grape' +gem "newrelic_rpm", '~> 3.14' -gem 'octokit', '~> 3.7.0' +gem 'octokit', '~> 3.8.0' gem "mail_room", "~> 0.6.1" diff --git a/Gemfile.lock b/Gemfile.lock index 8128203e2f..946842b4e2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,41 +4,41 @@ GEM CFPropertyList (2.3.2) RedCloth (4.2.9) ace-rails-ap (2.0.1) - actionmailer (4.2.5.1) - actionpack (= 4.2.5.1) - actionview (= 4.2.5.1) - activejob (= 4.2.5.1) + actionmailer (4.2.5.2) + actionpack (= 4.2.5.2) + actionview (= 4.2.5.2) + activejob (= 4.2.5.2) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.5.1) - actionview (= 4.2.5.1) - activesupport (= 4.2.5.1) + actionpack (4.2.5.2) + actionview (= 4.2.5.2) + activesupport (= 4.2.5.2) rack (~> 1.6) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.5.1) - activesupport (= 4.2.5.1) + actionview (4.2.5.2) + activesupport (= 4.2.5.2) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - activejob (4.2.5.1) - activesupport (= 4.2.5.1) + activejob (4.2.5.2) + activesupport (= 4.2.5.2) globalid (>= 0.3.0) - activemodel (4.2.5.1) - activesupport (= 4.2.5.1) + activemodel (4.2.5.2) + activesupport (= 4.2.5.2) builder (~> 3.1) - activerecord (4.2.5.1) - activemodel (= 4.2.5.1) - activesupport (= 4.2.5.1) + activerecord (4.2.5.2) + activemodel (= 4.2.5.2) + activesupport (= 4.2.5.2) arel (~> 6.0) activerecord-deprecated_finders (1.0.4) activerecord-session_store (0.1.2) actionpack (>= 4.0.0, < 5) activerecord (>= 4.0.0, < 5) railties (>= 4.0.0, < 5) - activesupport (4.2.5.1) + activesupport (4.2.5.2) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) @@ -49,7 +49,8 @@ GEM addressable (2.3.8) after_commit_queue (1.3.0) activerecord (>= 3.0) - allocations (1.0.3) + akismet (2.0.0) + allocations (1.0.4) annotate (2.6.10) activerecord (>= 3.2, <= 4.3) rake (~> 10.4) @@ -335,11 +336,11 @@ GEM ruby-progressbar (~> 1.4) gemnasium-gitlab-service (0.2.6) rugged (~> 0.21) - gemojione (2.1.1) + gemojione (2.2.1) json get_process_mem (0.2.0) gherkin-ruby (0.3.2) - github-linguist (4.7.3) + github-linguist (4.7.5) charlock_holmes (~> 0.7.3) escape_utils (~> 1.1.0) mime-types (>= 1.19) @@ -354,13 +355,13 @@ GEM diff-lcs (~> 1.1) mime-types (~> 1.15) posix-spawn (~> 0.3) - gitlab_emoji (0.2.0) - gemojione (~> 2.1) - gitlab_git (7.2.24) + gitlab_emoji (0.3.1) + gemojione (~> 2.2, >= 2.2.1) + gitlab_git (8.2.0) activesupport (~> 4.0) charlock_holmes (~> 0.7.3) github-linguist (~> 4.7.0) - rugged (~> 0.23.3) + rugged (~> 0.24.0b13) gitlab_meta (7.0) gitlab_omniauth-ldap (1.2.1) net-ldap (~> 0.9) @@ -478,10 +479,7 @@ GEM net-ldap (0.12.1) net-ssh (3.0.1) netrc (0.11.0) - newrelic-grape (2.1.0) - grape - newrelic_rpm - newrelic_rpm (3.9.4.245) + newrelic_rpm (3.14.1.311) nokogiri (1.6.7.2) mini_portile2 (~> 2.0.0.rc2) nprogress-rails (0.1.6.7) @@ -492,11 +490,11 @@ GEM multi_json (~> 1.3) multi_xml (~> 0.5) rack (~> 1.2) - octokit (3.7.1) + octokit (3.8.0) sawyer (~> 0.6.0, >= 0.5.3) - omniauth (1.2.2) + omniauth (1.3.1) hashie (>= 1.2, < 4) - rack (~> 1.0) + rack (>= 1.0, < 3) omniauth-azure-oauth2 (0.0.6) jwt (~> 1.0) omniauth (~> 1.0) @@ -534,9 +532,9 @@ GEM omniauth-oauth2 (1.3.1) oauth2 (~> 1.0) omniauth (~> 1.2) - omniauth-saml (1.4.1) + omniauth-saml (1.4.2) omniauth (~> 1.1) - ruby-saml (~> 1.0.0) + ruby-saml (~> 1.1, >= 1.1.1) omniauth-shibboleth (1.2.1) omniauth (>= 1.0.0) omniauth-twitter (1.2.1) @@ -588,16 +586,16 @@ GEM rack rack-test (0.6.3) rack (>= 1.0) - rails (4.2.5.1) - actionmailer (= 4.2.5.1) - actionpack (= 4.2.5.1) - actionview (= 4.2.5.1) - activejob (= 4.2.5.1) - activemodel (= 4.2.5.1) - activerecord (= 4.2.5.1) - activesupport (= 4.2.5.1) + rails (4.2.5.2) + actionmailer (= 4.2.5.2) + actionpack (= 4.2.5.2) + actionview (= 4.2.5.2) + activejob (= 4.2.5.2) + activemodel (= 4.2.5.2) + activerecord (= 4.2.5.2) + activesupport (= 4.2.5.2) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.5.1) + railties (= 4.2.5.2) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) @@ -607,9 +605,9 @@ GEM rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.3) loofah (~> 2.0) - railties (4.2.5.1) - actionpack (= 4.2.5.1) - activesupport (= 4.2.5.1) + railties (4.2.5.2) + actionpack (= 4.2.5.2) + activesupport (= 4.2.5.2) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.0.0) @@ -692,7 +690,7 @@ GEM ruby-fogbugz (0.2.1) crack (~> 0.4) ruby-progressbar (1.7.5) - ruby-saml (1.0.0) + ruby-saml (1.1.1) nokogiri (>= 1.5.10) uuid (~> 2.3) ruby2ruby (2.2.0) @@ -703,7 +701,7 @@ GEM rubyntlm (0.5.2) rubypants (0.2.0) rufus-scheduler (3.1.10) - rugged (0.23.3) + rugged (0.24.0b13) safe_yaml (1.0.4) sanitize (2.1.0) nokogiri (>= 1.4.4) @@ -725,7 +723,7 @@ GEM activesupport (>= 3.1, < 4.3) select2-rails (3.5.9.3) thor (~> 0.14) - sentry-raven (0.15.4) + sentry-raven (0.15.6) faraday (>= 0.7.6) settingslogic (2.0.9) sexp_processor (4.6.0) @@ -884,6 +882,7 @@ DEPENDENCIES acts-as-taggable-on (~> 3.4) addressable (~> 2.3.8) after_commit_queue + akismet (~> 2.0) allocations (~> 1.0) annotate (~> 2.6.0) asana (~> 0.4.0) @@ -933,8 +932,8 @@ DEPENDENCIES github-linguist (~> 4.7.0) github-markup (~> 1.3.1) gitlab-flowdock-git-hook (~> 1.0.1) - gitlab_emoji (~> 0.2.0) - gitlab_git (~> 7.2.22) + gitlab_emoji (~> 0.3.0) + gitlab_git (~> 8.2) gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.1.0) @@ -953,6 +952,7 @@ DEPENDENCIES jquery-ui-rails (~> 5.0.0) kaminari (~> 0.16.3) letter_opener (~> 1.1.2) + loofah (~> 2.0.3) mail_room (~> 0.6.1) method_source (~> 0.8) minitest (~> 5.7.0) @@ -960,13 +960,12 @@ DEPENDENCIES mysql2 (~> 0.3.16) nested_form (~> 0.3.2) net-ssh (~> 3.0.1) - newrelic-grape - newrelic_rpm (~> 3.9.4.245) - nokogiri (= 1.6.7.2) + newrelic_rpm (~> 3.14) + nokogiri (~> 1.6.7, >= 1.6.7.2) nprogress-rails (~> 0.1.6.7) oauth2 (~> 1.0.0) - octokit (~> 3.7.0) - omniauth (~> 1.2.2) + octokit (~> 3.8.0) + omniauth (~> 1.3.1) omniauth-azure-oauth2 (~> 0.0.6) omniauth-bitbucket (~> 0.0.2) omniauth-cas3 (~> 1.1.2) @@ -975,7 +974,7 @@ DEPENDENCIES omniauth-gitlab (~> 1.0.0) omniauth-google-oauth2 (~> 0.2.0) omniauth-kerberos (~> 0.3.0) - omniauth-saml (~> 1.4.0) + omniauth-saml (~> 1.4.2) omniauth-shibboleth (~> 1.2.0) omniauth-twitter (~> 1.2.0) omniauth_crowd (~> 2.2.0) @@ -988,7 +987,7 @@ DEPENDENCIES rack-attack (~> 4.3.1) rack-cors (~> 0.4.0) rack-oauth2 (~> 1.2.1) - rails (= 4.2.5.1) + rails (= 4.2.5.2) rails-deprecated_sanitizer (~> 1.0.3) raphael-rails (~> 2.1.2) rblineprof @@ -1010,7 +1009,7 @@ DEPENDENCIES sdoc (~> 0.3.20) seed-fu (~> 2.3.5) select2-rails (~> 3.5.9) - sentry-raven + sentry-raven (~> 0.15) settingslogic (~> 2.0.9) sham_rack shoulda-matchers (~> 2.8.0) diff --git a/README.md b/README.md index 3ec1d4a776..afa60116eb 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ GitLab is a Ruby on Rails application that runs on the following software: - Ubuntu/Debian/CentOS/RHEL - Ruby (MRI) 2.1 -- Git 1.7.10+ +- Git 2.7.4+ - Redis 2.8+ - MySQL or PostgreSQL diff --git a/Rakefile b/Rakefile index 35b2f05cbb..5dd389d567 100755 --- a/Rakefile +++ b/Rakefile @@ -4,4 +4,7 @@ require File.expand_path('../config/application', __FILE__) +relative_url_conf = File.expand_path('../config/initializers/relative_url', __FILE__) +require relative_url_conf if File.exist?("#{relative_url_conf}.rb") + Gitlab::Application.load_tasks diff --git a/VERSION b/VERSION index 01129dc3ec..1336777f6b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.4.3 \ No newline at end of file +8.5.8 \ No newline at end of file diff --git a/app/assets/images/emoji.png b/app/assets/images/emoji.png index a8ad7b6eab..1e7cf79ea4 100644 Binary files a/app/assets/images/emoji.png and b/app/assets/images/emoji.png differ diff --git a/app/assets/images/emoji@2x.png b/app/assets/images/emoji@2x.png new file mode 100644 index 0000000000..74d67f7520 Binary files /dev/null and b/app/assets/images/emoji@2x.png differ diff --git a/app/assets/javascripts/admin.js.coffee b/app/assets/javascripts/admin.js.coffee index eb951f7171..b2b8e1b7ff 100644 --- a/app/assets/javascripts/admin.js.coffee +++ b/app/assets/javascripts/admin.js.coffee @@ -12,19 +12,6 @@ class @Admin e.preventDefault() $('.js-toggle-colors-container').toggle() - $('input#broadcast_message_color').on 'input', -> - previewColor = $(@).val() - $('div.broadcast-message-preview').css('background-color', previewColor) - - $('input#broadcast_message_font').on 'input', -> - previewColor = $(@).val() - $('div.broadcast-message-preview').css('color', previewColor) - - $('textarea#broadcast_message_message').on 'input', -> - previewMessage = $(@).val() - previewMessage = "Your message here" if previewMessage.trim() == '' - $('div.broadcast-message-preview span').text(previewMessage) - $('.log-tabs a').click (e) -> e.preventDefault() $(this).tab('show') diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index 746fa3cea8..3e0fdb3f79 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -47,7 +47,7 @@ callback(namespaces) # Return projects list. Filtered by query - projects: (query, callback) -> + projects: (query, order, callback) -> url = Api.buildUrl(Api.projects_path) $.ajax( @@ -55,6 +55,7 @@ data: private_token: gon.api_token search: query + order_by: order per_page: 20 dataType: "json" ).done (projects) -> diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index c095e5ae2b..367bd098bf 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -5,7 +5,10 @@ # the compiled file. # #= require jquery -#= require jquery-ui +#= require jquery-ui/autocomplete +#= require jquery-ui/datepicker +#= require jquery-ui/effect-highlight +#= require jquery-ui/sortable #= require jquery_ujs #= require jquery.cookie #= require jquery.endless-scroll @@ -21,9 +24,9 @@ #= require bootstrap #= require select2 #= require raphael -#= require g.raphael-min -#= require g.bar-min -#= require chart-lib.min +#= require g.raphael +#= require g.bar +#= require Chart #= require branch-graph #= require ace/ace #= require ace/ext-searchbox @@ -38,9 +41,9 @@ #= require shortcuts_dashboard_navigation #= require shortcuts_issuable #= require shortcuts_network -#= require jquery.nicescroll.min +#= require jquery.nicescroll #= require_tree . -#= require fuzzaldrin-plus.min +#= require fuzzaldrin-plus window.slugify = (text) -> text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase() @@ -203,4 +206,94 @@ $ -> form = btn.closest("form") new ConfirmDangerModal(form, text) + $('input[type="search"]').each -> + $this = $(this) + $this.attr 'value', $this.val() + return + + $(document) + .off 'keyup', 'input[type="search"]' + .on 'keyup', 'input[type="search"]' , (e) -> + $this = $(this) + $this.attr 'value', $this.val() + + $(document) + .off 'breakpoint:change' + .on 'breakpoint:change', (e, breakpoint) -> + if breakpoint is 'sm' or breakpoint is 'xs' + $gutterIcon = $('.gutter-toggle').find('i') + if $gutterIcon.hasClass('fa-angle-double-right') + $gutterIcon.closest('a').trigger('click') + + $(document) + .off 'click', 'aside .gutter-toggle' + .on 'click', 'aside .gutter-toggle', (e) -> + e.preventDefault() + $this = $(this) + $thisIcon = $this.find 'i' + if $thisIcon.hasClass('fa-angle-double-right') + $thisIcon + .removeClass('fa-angle-double-right') + .addClass('fa-angle-double-left') + $this + .closest('aside') + .removeClass('right-sidebar-expanded') + .addClass('right-sidebar-collapsed') + $('.page-with-sidebar') + .removeClass('right-sidebar-expanded') + .addClass('right-sidebar-collapsed') + else + $thisIcon + .removeClass('fa-angle-double-left') + .addClass('fa-angle-double-right') + $this + .closest('aside') + .removeClass('right-sidebar-collapsed') + .addClass('right-sidebar-expanded') + $('.page-with-sidebar') + .removeClass('right-sidebar-collapsed') + .addClass('right-sidebar-expanded') + $.cookie("collapsed_gutter", + $('.right-sidebar') + .hasClass('right-sidebar-collapsed'), { path: '/' }) + + bootstrapBreakpoint = undefined; + checkBootstrapBreakpoints = -> + if $('.device-xs').is(':visible') + bootstrapBreakpoint = "xs" + else if $('.device-sm').is(':visible') + bootstrapBreakpoint = "sm" + else if $('.device-md').is(':visible') + bootstrapBreakpoint = "md" + else if $('.device-lg').is(':visible') + bootstrapBreakpoint = "lg" + + setBootstrapBreakpoints = -> + if $('.device-xs').length + return + + $("body") + .append('
'+ + '
'+ + '
'+ + '
') + checkBootstrapBreakpoints() + + fitSidebarForSize = -> + oldBootstrapBreakpoint = bootstrapBreakpoint + checkBootstrapBreakpoints() + if bootstrapBreakpoint != oldBootstrapBreakpoint + $(document).trigger('breakpoint:change', [bootstrapBreakpoint]) + + checkInitialSidebarSize = -> + if bootstrapBreakpoint is "xs" or "sm" + $(document).trigger('breakpoint:change', [bootstrapBreakpoint]) + + $(window) + .off "resize" + .on "resize", (e) -> + fitSidebarForSize() + + setBootstrapBreakpoints() + checkInitialSidebarSize() new Aside() diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 1ef31c7700..360acb864f 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -4,6 +4,7 @@ class @AwardsHandler event.stopPropagation() event.preventDefault() $(".emoji-menu").show() + $("#emoji_search").focus() $("html").on 'click', (event) -> if !$(event.target).closest(".emoji-menu").length @@ -48,10 +49,11 @@ class @AwardsHandler counter.text(parseInt(counter.text()) - 1) emojiIcon.removeClass("active") @removeMeFromAuthorList(emoji) - else if emoji =="thumbsup" || emoji == "thumbsdown" + else if emoji == "thumbsup" || emoji == "thumbsdown" emojiIcon.tooltip("destroy") counter.text(0) emojiIcon.removeClass("active") + @removeMeFromAuthorList(emoji) else emojiIcon.tooltip("destroy") emojiIcon.remove() diff --git a/app/assets/javascripts/broadcast_message.js.coffee b/app/assets/javascripts/broadcast_message.js.coffee new file mode 100644 index 0000000000..a38a329c4c --- /dev/null +++ b/app/assets/javascripts/broadcast_message.js.coffee @@ -0,0 +1,22 @@ +$ -> + $('input#broadcast_message_color').on 'input', -> + previewColor = $(@).val() + $('div.broadcast-message-preview').css('background-color', previewColor) + + $('input#broadcast_message_font').on 'input', -> + previewColor = $(@).val() + $('div.broadcast-message-preview').css('color', previewColor) + + previewPath = $('textarea#broadcast_message_message').data('preview-path') + + $('textarea#broadcast_message_message').on 'input', -> + message = $(@).val() + + if message == '' + $('.js-broadcast-message-preview').text("Your message here") + else + $.ajax( + url: previewPath + type: "POST" + data: { broadcast_message: { message: message } } + ) diff --git a/app/assets/javascripts/dashboard.js.coffee b/app/assets/javascripts/dashboard.js.coffee index 00ee503ff1..62143e66cf 100644 --- a/app/assets/javascripts/dashboard.js.coffee +++ b/app/assets/javascripts/dashboard.js.coffee @@ -1,3 +1,31 @@ -class @Dashboard - constructor: -> - new ProjectsList() +@Dashboard = + init: -> + $(".projects-list-filter").off('keyup') + this.initSearch() + + initSearch: -> + @timer = null + $(".projects-list-filter").on('keyup', -> + clearTimeout(@timer) + @timer = setTimeout(Dashboard.filterResults, 500) + ) + + filterResults: => + $('.projects-list-holder').fadeTo(250, 0.5) + + form = null + form = $("form#project-filter-form") + search = $(".projects-list-filter").val() + project_filter_url = form.attr('action') + '?' + form.serialize() + + $.ajax + type: "GET" + url: form.attr('action') + data: form.serialize() + complete: -> + $('.projects-list-holder').fadeTo(250, 1) + success: (data) -> + $('.projects-list-holder').replaceWith(data.html) + # Change url so if user reload a page - search results are saved + history.replaceState {page: project_filter_url}, document.title, project_filter_url + dataType: "json" diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 2cdf01d874..67a92d822e 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -16,6 +16,8 @@ class Dispatcher shortcut_handler = null switch page + when 'explore:projects:index', 'explore:projects:starred', 'explore:projects:trending' + Dashboard.init() when 'projects:issues:index' Issues.init() shortcut_handler = new ShortcutsNavigation() @@ -58,7 +60,7 @@ class Dispatcher shortcut_handler = new ShortcutsNavigation() MergeRequests.init() when 'dashboard:show', 'root:show' - new Dashboard() + Dashboard.init() when 'dashboard:activity' new Activities() when 'dashboard:projects:starred' @@ -86,6 +88,7 @@ class Dispatcher when 'groups:new', 'groups:edit', 'admin:groups:edit', 'admin:groups:new' new GroupAvatar() when 'projects:tree:show' + shortcut_handler = new ShortcutsNavigation() new TreeView() when 'projects:find_file:show' shortcut_handler = true diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee index c714c0fa93..b502131a99 100644 --- a/app/assets/javascripts/dropzone_input.js.coffee +++ b/app/assets/javascripts/dropzone_input.js.coffee @@ -65,8 +65,7 @@ class @DropzoneInput return success: (header, response) -> - child = $(dropzone[0]).children("textarea") - $(child).val $(child).val() + response.link.markdown + "\n" + pasteText response.link.markdown return error: (temp, errorMessage) -> @@ -128,6 +127,7 @@ class @DropzoneInput beforeSelection = $(child).val().substring 0, caretStart afterSelection = $(child).val().substring caretEnd, textEnd $(child).val beforeSelection + text + afterSelection + child.get(0).setSelectionRange caretStart + text.length, caretEnd + text.length form_textarea.trigger "input" getFilename = (e) -> diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee index 02232698bc..e52b73f94f 100644 --- a/app/assets/javascripts/issuable_context.js.coffee +++ b/app/assets/javascripts/issuable_context.js.coffee @@ -10,20 +10,10 @@ class @IssuableContext $(".issuable-sidebar .inline-update").on "change", ".js-assignee", -> $(this).submit() - $('.issuable-details').waitForImages -> - $('.issuable-affix').on 'affix.bs.affix', -> - $(@).width($(@).outerWidth()) - .on 'affixed-top.bs.affix affixed-bottom.bs.affix', -> - $(@).width('') - - $('.issuable-affix').affix offset: - top: -> - @top = ($('.issuable-affix').offset().top - 70) - bottom: -> - @bottom = $('.footer').outerHeight(true) - - $(".edit-link").click (e) -> + $(document).on "click",".edit-link", (e) -> block = $(@).parents('.block') block.find('.selectbox').show() block.find('.value').hide() block.find('.js-select2').select2("open") + + $(".right-sidebar").niceScroll() diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index cbc70cd846..d663e34871 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -50,6 +50,7 @@ class @Issue new Flash(issueFailMessage, 'alert') success: (data, textStatus, jqXHR) -> if data.saved + $(document).trigger('issuable:change'); if isClose $('a.btn-close').addClass('hidden') $('a.btn-reopen').removeClass('hidden') diff --git a/app/assets/javascripts/logo.js.coffee b/app/assets/javascripts/logo.js.coffee index a5879c8b79..35b2fbbba0 100644 --- a/app/assets/javascripts/logo.js.coffee +++ b/app/assets/javascripts/logo.js.coffee @@ -42,3 +42,9 @@ work = -> $(document).on('page:fetch', start) $(document).on('page:change', stop) + +$ -> + # Make logo clickable as part of a workaround for Safari visited + # link behaviour (See !2690). + $('#logo').on 'click', -> + $('#js-shortcuts-home').get(0).click() diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index b10e1db7f3..6f569f9e1a 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -146,6 +146,7 @@ class @MergeRequestTabs success: (data) => document.querySelector("div#diffs").innerHTML = data.html $('div#diffs .js-syntax-highlight').syntaxHighlight() + @expandViewContainer() if @diffViewType() is 'parallel' @diffsLoaded = true @scrollToElement("#diffs") @@ -177,3 +178,10 @@ class @MergeRequestTabs options = $.extend({}, defaults, options) $.ajax(options) + + # Returns diff view type + diffViewType: -> + $('.inline-parallel-buttons a.active').data('view-type') + + expandViewContainer: -> + $('.container-fluid').removeClass('container-limited') diff --git a/app/assets/javascripts/milestone.js.coffee b/app/assets/javascripts/milestone.js.coffee index d644d50b66..e6d8518bec 100644 --- a/app/assets/javascripts/milestone.js.coffee +++ b/app/assets/javascripts/milestone.js.coffee @@ -62,14 +62,24 @@ class @Milestone dataType: "json" constructor: -> + oldMouseStart = $.ui.sortable.prototype._mouseStart + $.ui.sortable.prototype._mouseStart = (event, overrideHandle, noActivation) -> + this._trigger "beforeStart", event, this._uiHash() + oldMouseStart.apply this, [event, overrideHandle, noActivation] + @bindIssuesSorting() @bindMergeRequestSorting() + @bindTabsSwitching bindIssuesSorting: -> $("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable( connectWith: ".issues-sortable-list", dropOnEmpty: true, items: "li:not(.ui-sort-disabled)", + beforeStart: (event, ui) -> + $(".issues-sortable-list").css "min-height", ui.item.outerHeight() + stop: (event, ui) -> + $(".issues-sortable-list").css "min-height", "0px" update: (event, ui) -> data = $(this).sortable("serialize") Milestone.sortIssues(data) @@ -95,10 +105,22 @@ class @Milestone ).disableSelection() bindMergeRequestSorting: -> + $('a[data-toggle="tab"]').on 'show.bs.tab', (e) -> + currentTabClass = $(e.target).data('show') + previousTabClass = $(e.relatedTarget).data('show') + + $(previousTabClass).hide() + $(currentTabClass).removeClass('hidden') + $(currentTabClass).show() + $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable( connectWith: ".merge_requests-sortable-list", dropOnEmpty: true, items: "li:not(.ui-sort-disabled)", + beforeStart: (event, ui) -> + $(".merge_requests-sortable-list").css "min-height", ui.item.outerHeight() + stop: (event, ui) -> + $(".merge_requests-sortable-list").css "min-height", "0px" update: (event, ui) -> data = $(this).sortable("serialize") Milestone.sortMergeRequests(data) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 8866d81c92..3347ab65c9 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -15,6 +15,8 @@ class @Notes @last_fetched_at = last_fetched_at @view = view @noteable_url = document.URL + @notesCountBadge ||= $(".issuable-details").find(".notes-tab .badge") + @initRefresh() @setupMainTargetNoteForm() @cleanBinding() @@ -62,6 +64,9 @@ class @Notes # fetch notes when tab becomes visible $(document).on "visibilitychange", @visibilityChange + # when issue status changes, we need to refresh data + $(document).on "issuable:change", @refresh + cleanBinding: -> $(document).off "ajax:success", ".js-main-target-form" $(document).off "ajax:success", ".js-discussion-note-form" @@ -89,7 +94,7 @@ class @Notes , 15000 refresh: -> - unless document.hidden or (@noteable_url != document.URL) + if not document.hidden and document.URL.indexOf(@noteable_url) is 0 @getContent() getContent: -> @@ -101,7 +106,10 @@ class @Notes notes = data.notes @last_fetched_at = data.last_fetched_at $.each notes, (i, note) => - @renderNote(note) + if note.discussion_with_diff_html? + @renderDiscussionNote(note) + else + @renderNote(note) ### @@ -116,19 +124,22 @@ class @Notes flash.pinTo('.header-content') return - # render note if it not present in loaded list - # or skip if rendered - if @isNewNote(note) && !note.award - @note_ids.push(note.id) - $('ul.main-notes-list'). - append(note.html). - syntaxHighlight() - @initTaskList() - if note.award awards_handler.addAwardToEmojiBar(note.note) awards_handler.scrollToAwards() + # render note if it not present in loaded list + # or skip if rendered + else if @isNewNote(note) + @note_ids.push(note.id) + + $('ul.main-notes-list') + .append(note.html) + .syntaxHighlight() + @initTaskList() + @updateNotesCount(1) + + ### Check if note does not exists on page ### @@ -144,34 +155,39 @@ class @Notes Note: for rendering inline notes use renderDiscussionNote ### renderDiscussionNote: (note) -> + return unless @isNewNote(note) + @note_ids.push(note.id) - form = $("form[rel='" + note.discussion_id + "']") + form = $("#new-discussion-note-form-#{note.discussion_id}") row = form.closest("tr") note_html = $(note.html) note_html.syntaxHighlight() # is this the first note of discussion? - if row.is(".js-temp-notes-holder") + discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']") + if discussionContainer.length is 0 # insert the note and the reply button after the temp row row.after note.discussion_html # remove the note (will be added again below) row.next().find(".note").remove() + # Before that, the container didn't exist + discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']") + # Add note to 'Changes' page discussions - $(".notes[rel='" + note.discussion_id + "']").append note_html + discussionContainer.append note_html # Init discussion on 'Discussion' page if it is merge request page - if $('body').attr('data-page').indexOf('projects:merge_request') == 0 - discussion_html = $(note.discussion_with_diff_html) - discussion_html.syntaxHighlight() - $('ul.main-notes-list').append(discussion_html) + if $('body').attr('data-page').indexOf('projects:merge_request') is 0 + $('ul.main-notes-list') + .append(note.discussion_with_diff_html) + .syntaxHighlight() else # append new note to all matching discussions - $(".notes[rel='" + note.discussion_id + "']").append note_html + discussionContainer.append note_html - # cleanup after successfully creating a diff/discussion note - @removeDiscussionNoteForm(form) + @updateNotesCount(1) ### Called in response the main target form has been successfully submitted. @@ -278,6 +294,9 @@ class @Notes addDiscussionNote: (xhr, note, status) => @renderDiscussionNote(note) + # cleanup after successfully creating a diff/discussion note + @removeDiscussionNoteForm($("#new-discussion-note-form-#{note.discussion_id}")) + ### Called in response to the edit note form being submitted @@ -320,6 +339,7 @@ class @Notes form.show() textarea = form.find("textarea") textarea.focus() + autosize(textarea) # HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?). # The textarea has the correct value, Chrome just won't show it unless we @@ -348,30 +368,32 @@ class @Notes Removes the actual note from view. Removes the whole discussion if the last note is being removed. ### - removeNote: -> - note = $(this).closest(".note") - note_id = note.attr('id') + removeNote: (e) => + noteId = $(e.currentTarget) + .closest(".note") + .attr("id") - $('.note[id="' + note_id + '"]').each -> - note = $(this) + # A same note appears in the "Discussion" and in the "Changes" tab, we have + # to remove all. Using $(".note[id='noteId']") ensure we get all the notes, + # where $("#noteId") would return only one. + $(".note[id='#{noteId}']").each (i, el) => + note = $(el) notes = note.closest(".notes") - count = notes.closest(".issuable-details").find(".notes-tab .badge") # check if this is the last note for this line if notes.find(".note").length is 1 - # for discussions - notes.closest(".discussion").remove() + # "Discussions" tab + notes.closest(".timeline-entry").remove() - # for diff lines + # "Changes" tab / commit view notes.closest("tr").remove() - # update notes count - oldNum = parseInt(count.text()) - count.text(oldNum - 1) - note.remove() + # Decrement the "Discussions" counter only once + @updateNotesCount(-1) + ### Called in response to clicking the delete attachment link @@ -411,7 +433,7 @@ class @Notes ### setupDiscussionNoteForm: (dataHolder, form) => # setup note target - form.attr "rel", dataHolder.data("discussionId") + form.attr 'id', "new-discussion-note-form-#{dataHolder.data("discussionId")}" form.find("#line_type").val dataHolder.data("lineType") form.find("#note_commit_id").val dataHolder.data("commitId") form.find("#note_line_code").val dataHolder.data("lineCode") @@ -541,3 +563,6 @@ class @Notes updateTaskList: -> $('form', this).submit() + + updateNotesCount: (updateCount) -> + @notesCountBadge.text(parseInt(@notesCountBadge.text()) + updateCount) diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index d7a658f8fa..76bc4ff42a 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -50,3 +50,19 @@ class @Project $('#notifications-button').empty().append("" + label + "") $(@).parents('ul').find('li.active').removeClass 'active' $(@).parent().addClass 'active' + + @projectSelectDropdown() + + projectSelectDropdown: -> + new ProjectSelect() + + $('.project-item-select').on 'click', (e) => + @changeProject $(e.currentTarget).val() + + $('.js-projects-dropdown-toggle').on 'click', (e) -> + e.preventDefault() + + $('.js-projects-dropdown').select2('open') + + changeProject: (url) -> + window.location = url diff --git a/app/assets/javascripts/project_select.js.coffee b/app/assets/javascripts/project_select.js.coffee index 0ae274f336..be8ab9b428 100644 --- a/app/assets/javascripts/project_select.js.coffee +++ b/app/assets/javascripts/project_select.js.coffee @@ -3,6 +3,7 @@ class @ProjectSelect $('.ajax-project-select').each (i, select) -> @groupId = $(select).data('group-id') @includeGroups = $(select).data('include-groups') + @orderBy = $(select).data('order-by') || 'id' placeholder = "Search for project" placeholder += " or group" if @includeGroups @@ -28,7 +29,7 @@ class @ProjectSelect if @groupId Api.groupProjects @groupId, query.term, projectsCallback else - Api.projects query.term, projectsCallback + Api.projects query.term, @orderBy, projectsCallback id: (project) -> project.web_url diff --git a/app/assets/javascripts/projects_list.js.coffee b/app/assets/javascripts/projects_list.js.coffee index f2887af190..a57c37e070 100644 --- a/app/assets/javascripts/projects_list.js.coffee +++ b/app/assets/javascripts/projects_list.js.coffee @@ -2,23 +2,31 @@ class @ProjectsList constructor: -> $(".projects-list .js-expand").on 'click', (e) -> e.preventDefault() - list = $(this).closest('.projects-list') - list.find("li").show() - list.find("li.bottom").hide() + $projectsList = $(this).closest('.projects-list') + ProjectsList.showPagination($projectsList) + $projectsList.find('li.bottom').hide() - $(".projects-list-filter").keyup -> - terms = $(this).val() - uiBox = $('div.projects-list-holder') - if terms == "" || terms == undefined - uiBox.find("ul.projects-list li").show() - else - uiBox.find("ul.projects-list li").each (index) -> - name = $(this).find("span.filter-title").text() + $("#filter_projects").on 'keyup', -> + ProjectsList.filter_results($("#filter_projects")) - if name.toLowerCase().search(terms.toLowerCase()) == -1 - $(this).hide() - else - $(this).show() - uiBox.find("ul.projects-list li.bottom").hide() + @showPagination: ($projectsList) -> + $projectsList.find('li').show() + $('.gl-pagination').show() + @filter_results: ($element) -> + terms = $element.val() + filterSelector = $element.data('filter-selector') || 'span.filter-title' + $projectsList = $('.projects-list') + if not terms + ProjectsList.showPagination($projectsList) + else + $projectsList.find('li').each (index) -> + $this = $(this) + name = $this.find(filterSelector).text() + + if name.toLowerCase().indexOf(terms.toLowerCase()) == -1 + $this.hide() + else + $this.show() + $('.gl-pagination').hide() diff --git a/app/assets/javascripts/shortcuts.js.coffee b/app/assets/javascripts/shortcuts.js.coffee index f141fb69c3..9c7c2474aa 100644 --- a/app/assets/javascripts/shortcuts.js.coffee +++ b/app/assets/javascripts/shortcuts.js.coffee @@ -13,8 +13,10 @@ class @Shortcuts if $('#modal-shortcuts').length > 0 $('#modal-shortcuts').modal('show') else + url = '/help/shortcuts' + url = gon.relative_url_root + url if gon.relative_url_root? $.ajax( - url: '/help/shortcuts', + url: url, dataType: 'script', success: (e) -> if location and location.length > 0 diff --git a/app/assets/javascripts/shortcuts_issuable.coffee b/app/assets/javascripts/shortcuts_issuable.coffee index bb53219468..cefa1857d7 100644 --- a/app/assets/javascripts/shortcuts_issuable.coffee +++ b/app/assets/javascripts/shortcuts_issuable.coffee @@ -5,23 +5,42 @@ class @ShortcutsIssuable extends ShortcutsNavigation constructor: (isMergeRequest) -> super() Mousetrap.bind('a', -> - $('.js-assignee').select2('open') + $('.block.assignee .edit-link').trigger('click') return false ) Mousetrap.bind('m', -> - $('.js-milestone').select2('open') + $('.block.milestone .edit-link').trigger('click') return false ) Mousetrap.bind('r', => @replyWithSelectedText() return false ) + Mousetrap.bind('j', => + @prevIssue() + return false + ) + Mousetrap.bind('k', => + @nextIssue() + return false + ) + if isMergeRequest @enabledHelp.push('.hidden-shortcut.merge_requests') else @enabledHelp.push('.hidden-shortcut.issues') + prevIssue: -> + $prevBtn = $('.prev-btn') + if not $prevBtn.hasClass('disabled') + Turbolinks.visit($prevBtn.attr('href')) + + nextIssue: -> + $nextBtn = $('.next-btn') + if not $nextBtn.hasClass('disabled') + Turbolinks.visit($nextBtn.attr('href')) + replyWithSelectedText: -> if window.getSelection selected = window.getSelection().toString() diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee index ae59480af9..cff309c597 100644 --- a/app/assets/javascripts/sidebar.js.coffee +++ b/app/assets/javascripts/sidebar.js.coffee @@ -8,4 +8,10 @@ $(document).on("click", '.toggle-nav-collapse', (e) -> $('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded") $('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left") $.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' }) + + setTimeout ( -> + niceScrollBars = $('.nicescroll').niceScroll(); + niceScrollBars.updateScrollBar(); + ), 300 + ) diff --git a/app/assets/javascripts/wikis.js.coffee b/app/assets/javascripts/wikis.js.coffee index 19420f4246..1ee827f1fa 100644 --- a/app/assets/javascripts/wikis.js.coffee +++ b/app/assets/javascripts/wikis.js.coffee @@ -2,7 +2,7 @@ class @Wikis constructor: -> - $('.build-new-wiki').bind 'click', (e) => + $('.new-wiki-page').on 'submit', (e) => $('[data-error~=slug]').addClass('hidden') field = $('#new_wiki_path') slug = @slugify(field.val()) @@ -10,6 +10,7 @@ class @Wikis if (slug.length > 0) path = field.attr('data-wikis-path') location.href = path + '/' + slug + e.preventDefault() dasherize: (value) -> value.replace(/[_\s]+/g, '-') diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 36e582d485..b7ffa3e6ff 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -24,6 +24,7 @@ &.s26 { width: 26px; height: 26px; margin-right: 8px; } &.s32 { width: 32px; height: 32px; margin-right: 10px; } &.s36 { width: 36px; height: 36px; margin-right: 10px; } + &.s40 { width: 40px; height: 40px; margin-right: 10px; } &.s46 { width: 46px; height: 46px; margin-right: 15px; } &.s48 { width: 48px; height: 48px; margin-right: 10px; } &.s60 { width: 60px; height: 60px; margin-right: 12px; } @@ -40,7 +41,8 @@ &.s16 { font-size: 12px; line-height: 1.33; } &.s24 { font-size: 14px; line-height: 1.8; } &.s26 { font-size: 20px; line-height: 1.33; } - &.s32 { font-size: 22px; line-height: 32px; } + &.s32 { font-size: 20px; line-height: 32px; } + &.s40 { font-size: 16px; line-height: 40px; } &.s60 { font-size: 32px; line-height: 60px; } &.s90 { font-size: 36px; line-height: 90px; } &.s110 { font-size: 40px; line-height: 112px; font-weight: 300; } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index d0f5d33bf4..bd89cc7dc1 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -146,6 +146,10 @@ border-bottom: 1px solid $border-color; &.oneline-block { - line-height: 42px; + line-height: 36px; + } + + > .controls { + float: right; } } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index c99292c3f8..5f193fa743 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -2,7 +2,7 @@ @include border-radius(3px); font-size: $gl-font-size; font-weight: 500; - padding: $gl-vert-padding $gl-padding; + padding: $gl-vert-padding $gl-btn-padding; &:focus, &:active { @@ -82,8 +82,7 @@ &.btn-success, &.btn-new, &.btn-create, - &.btn-save, - &.btn-green { + &.btn-save { @include btn-green; } @@ -159,7 +158,6 @@ .input-group-btn { .btn { - @include btn-gray; @include btn-middle; &:hover { @@ -186,8 +184,4 @@ border: 1px solid #c6cacf !important; background-color: #e4e7ed !important; } - - .btn-green { - @include btn-green - } } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 6ea2219073..ea56d9e12a 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -376,11 +376,11 @@ table { margin-bottom: $gl-padding; } -.new-project-item-select-holder { +.project-item-select-holder { display: inline-block; position: relative; - .new-project-item-select { + .project-item-select { position: absolute; top: 0; right: 0; diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 00cb756b37..07907e6e5a 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -36,6 +36,20 @@ } } + .filename { + &.old { + span.idiff { + background-color: #f8cbcb; + } + } + + &.new { + span.idiff { + background-color: #a6f3a6; + } + } + } + .left-options { margin-top: -3px; } @@ -144,7 +158,7 @@ } &:hover { - background: $hover; + background: $row-hover; } } } @@ -158,3 +172,15 @@ } } } + +span.idiff { + &.left { + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; + } + + &.right { + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + } +} diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 4dab806d50..d097e4d32f 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -2,11 +2,42 @@ textarea { resize: vertical; } -input[type='search'].search-text-input { - background-image: image-url("icon-search.png"); +input { + border-radius: $border-radius-base; +} + +input[type='search'] { + background-color: white; + padding-left: 10px; +} + +input[type='search'].search-input { background-repeat: no-repeat; background-position: 10px; - padding-left: 25px; + background-size: 16px; + background-position-x: 30%; + padding-left: 10px; + background-color: $gray-light; + + &.search-input[value=""] { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC'); + } + + &.search-input::-webkit-input-placeholder { + text-align: center; + } + + &.search-input:-moz-placeholder { /* Firefox 18- */ + text-align: center; + } + + &.search-input::-moz-placeholder { /* Firefox 19+ */ + text-align: center; + } + + &.search-input:-ms-input-placeholder { + text-align: center; + } } input[type='text'].danger { @@ -74,6 +105,7 @@ label { .form-control { @include box-shadow(none); + border-radius: 3px; } .form-control-inline { diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index 8d9a0aae56..12cef6f8ea 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -117,4 +117,4 @@ body { &.ui_violet { @include gitlab-theme(#9988CC, $theme-violet, #443366, #332255); } -} +} \ No newline at end of file diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index ba5e72c8c5..c2676cd1cc 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -73,7 +73,6 @@ header { .title { margin: 0; - overflow: hidden; font-size: 19px; line-height: $header-height; font-weight: normal; @@ -88,6 +87,22 @@ header { text-decoration: underline; } } + + .dropdown-toggle-caret { + position: relative; + top: -2px; + width: 12px; + line-height: 12px; + margin-left: 5px; + font-size: 10px; + text-align: center; + cursor: pointer; + } + + .project-item-select { + right: auto; + left: 0; + } } .navbar-collapse { @@ -108,16 +123,10 @@ header { .search-input { width: 220px; - background-image: image-url("icon-search.png"); - background-repeat: no-repeat; - background-position: 195px; - @include input-big; &:focus { @include box-shadow(none); outline: none; - border-color: #DDD; - background-color: #FFF; } } } @@ -132,9 +141,13 @@ header { } @media (max-width: $screen-md-max) { - .header-collapsed, .header-expanded { - @include collapsed-header; + .header-collapsed { + margin-left: $sidebar_collapsed_width; } + + .header-expanded { + margin-left: $sidebar_width; + } } @media(min-width: $screen-md-max) { diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index e93dbab0c4..08dcb563dc 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -9,7 +9,7 @@ display: block; float: left; - padding: 0 $gl-padding; + padding: 0 $gl-btn-padding; font-weight: normal; margin-right: 10px; font-size: $gl-font-size; diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss index 871b808bad..0cdcd923b3 100644 --- a/app/assets/stylesheets/framework/jquery.scss +++ b/app/assets/stylesheets/framework/jquery.scss @@ -48,8 +48,19 @@ .ui-state-hover, .ui-state-focus { - border: 1px solid $hover; - background: $hover; + border: 1px solid $row-hover; + background: $row-hover; color: #333; } } + +.ui-sortable-handle { + cursor: move; + cursor: -webkit-grab; + cursor: -moz-grab; + + &:active { + cursor: -webkit-grabbing; + cursor: -moz-grabbing; + } +} diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index c6bc6fb324..354392d5ec 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -38,7 +38,7 @@ &.smoke { background-color: $background-color; } &:hover { - background: $hover; + background: $row-hover; } &:last-child { @@ -109,7 +109,6 @@ ul.content-list { padding: 0; > li { - padding: $gl-padding 0; border-color: $table-border-color; color: $gl-gray; diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 0997dfc287..3bfac2ad9b 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -116,7 +116,7 @@ display: none; } - aside { + aside:not(.right-sidebar){ display: none; } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index c537d97fb2..d24faa897a 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -37,3 +37,102 @@ } } } + +.top-area { + @include clearfix; + + border-bottom: 1px solid #EEE; + + .nav-text { + padding-top: 16px; + padding-bottom: 11px; + display: inline-block; + width: 50%; + line-height: 28px; + + /* Small devices (phones, tablets, 768px and lower) */ + @media (max-width: $screen-sm-min) { + width: 100%; + } + } + + .nav-links { + display: inline-block; + width: 50%; + margin-bottom: 0px; + border-bottom: none; + + /* Small devices (phones, tablets, 768px and lower) */ + @media (max-width: $screen-sm-min) { + width: 100%; + } + } + + .nav-controls { + width: 50%; + display: inline-block; + float: right; + text-align: right; + padding: 11px 0; + margin-bottom: 0px; + + > .dropdown { + margin-right: $gl-padding-top; + display: inline-block; + } + + > .btn { + margin-right: $gl-padding-top; + display: inline-block; + + &:last-child { + margin-right: 0; + } + } + + > .btn-grouped { + float: none; + } + + > form { + display: inline-block; + } + + input { + height: 34px; + display: inline-block; + position: relative; + top: 1px; + margin-right: $gl-padding-top; + + /* Medium devices (desktops, 992px and up) */ + @media (min-width: $screen-md-min) { width: 200px; } + + /* Large devices (large desktops, 1200px and up) */ + @media (min-width: $screen-lg-min) { width: 250px; } + + &.input-short { + /* Medium devices (desktops, 992px and up) */ + @media (min-width: $screen-md-min) { width: 170px; } + + /* Large devices (large desktops, 1200px and up) */ + @media (min-width: $screen-lg-min) { width: 210px; } + } + } + + /* Hide on extra small devices (phones) */ + @media (max-width: $screen-xs-max) { + display: none; + } + + /* Small devices (tablets, 768px and lower) */ + @media (max-width: $screen-sm-max) { + width: 100%; + text-align: left; + + input { + width: 300px; + } + } + } +} diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss index 2cd30491bf..b6f21fd8c9 100644 --- a/app/assets/stylesheets/framework/pagination.scss +++ b/app/assets/stylesheets/framework/pagination.scss @@ -1,35 +1,11 @@ .gl-pagination { + text-align: center; border-top: 1px solid $border-color; - background-color: $background-color; - margin: -$gl-padding; + margin: 0; margin-top: 0; .pagination { padding: 0; - margin: 0; - display: block; - - li.first, - li.last, - li.next, - li.prev { - > a { - color: $link-color; - - &:hover { - color: #fff; - } - } - } - - li > a, - li > span { - border: none; - margin: 0; - @include border-radius(0 !important); - padding: 13px 19px; - border-right: 1px solid $border-color; - } } } diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index 57b9451b26..ae7bdf14c4 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -2,7 +2,13 @@ margin-bottom: $gl-padding; .panel-heading { - padding: 7px $gl-padding; + padding: $gl-vert-padding $gl-padding; + line-height: 36px; + + .controls { + margin-top: -2px; + float: right; + } } .panel-body { @@ -14,7 +20,3 @@ } } } - -.container-blank .panel .panel-heading { - line-height: 42px !important; -} diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 540d0b0316..de947c89c1 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -12,6 +12,23 @@ height: 100%; transition-duration: .3s; } + + .gitlab-text-container-link { + z-index: 1; + position: absolute; + left: 0px; + } + + #logo { + z-index: 2; + position: absolute; + width: 58px; + cursor: pointer; + } + + &.right-sidebar-expanded { + padding-right: $gutter_width; + } } .sidebar-wrapper { @@ -70,7 +87,7 @@ width: 158px; float: left; margin: 0; - margin-left: 14px; + margin-left: 50px; font-size: 19px; line-height: 41px; font-weight: normal; @@ -181,6 +198,10 @@ @mixin expanded-sidebar { padding-left: $sidebar_width; + &.right-sidebar-collapsed { + padding-right: $sidebar_collapsed_width; + } + .sidebar-wrapper { width: $sidebar_width; @@ -203,6 +224,10 @@ @mixin collapsed-sidebar { padding-left: $sidebar_collapsed_width; + &.right-sidebar-collapsed { + padding-right: $sidebar_collapsed_width; + } + .sidebar-wrapper { width: $sidebar_collapsed_width; @@ -266,26 +291,10 @@ background: #f2f6f7; } -@media (max-width: $screen-md-max) { - .page-sidebar-collapsed { - @include collapsed-sidebar; - } - - .page-sidebar-expanded { - @include collapsed-sidebar; - } - - .collapse-nav { - display: none; - } +.page-sidebar-collapsed { + @include collapsed-sidebar; } -@media(min-width: $screen-md-max) { - .page-sidebar-collapsed { - @include collapsed-sidebar; - } - - .page-sidebar-expanded { - @include expanded-sidebar; - } +.page-sidebar-expanded { + @include expanded-sidebar; } diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index 47b843e5e3..aa244fe548 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -5,13 +5,13 @@ padding: 0; .timeline-entry { - padding: $gl-padding 0; + padding: $gl-padding $gl-btn-padding; border-color: $table-border-color; color: $gl-gray; border-bottom: 1px solid $border-white-light; &:target { - background: $hover; + background: $row-hover; } &:last-child { diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index 88072606bf..3e70924487 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -114,22 +114,9 @@ * */ -.container-blank .panel .panel-heading { - font-size: 17px; - line-height: 38px; -} - .panel { box-shadow: none; - .panel-heading { - .panel-head-actions { - position: relative; - top: -5px; - float: right; - } - } - .panel-body { form, pre { margin: 0; diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index cd0621cdbf..b1b8295411 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -22,9 +22,9 @@ $brand-info: $gl-info; $brand-warning: $gl-warning; $brand-danger: $gl-danger; -$border-radius-base: 2px !default; -$border-radius-large: 2px !default; -$border-radius-small: 2px !default; +$border-radius-base: 3px !default; +$border-radius-large: 3px !default; +$border-radius-small: 3px !default; //== Scaffolding @@ -66,20 +66,20 @@ $legend-color: $text-color; //## $pagination-color: $gl-gray; -$pagination-bg: $background-color; -$pagination-border: transparent; +$pagination-bg: #fff; +$pagination-border: $border-color; -$pagination-hover-color: #fff; -$pagination-hover-bg: $brand-info; -$pagination-hover-border: transparent; +$pagination-hover-color: $gl-gray; +$pagination-hover-bg: $row-hover; +$pagination-hover-border: $border-color; -$pagination-active-color: #fff; -$pagination-active-bg: $brand-info; -$pagination-active-border: transparent; +$pagination-active-color: $blue-dark; +$pagination-active-bg: #fff; +$pagination-active-border: $border-color; -$pagination-disabled-color: #fff; -$pagination-disabled-bg: lighten($brand-info, 15%); -$pagination-disabled-border: transparent; +$pagination-disabled-color: #cdcdcd; +$pagination-disabled-bg: $background-color; +$pagination-disabled-border: $border-color; //== Form states and alerts diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 4866a17005..8d8f41287d 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -115,7 +115,7 @@ ul, ol { padding: 0; - margin: 6px 0 6px 18px !important; + margin: 6px 0 6px 28px !important; } li { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 85ecdddda7..4888854625 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -1,4 +1,4 @@ -$hover: #faf9f9; +$row-hover: #f4f8fe; $gl-text-color: #54565B; $gl-text-green: #4A2; $gl-text-red: #D12F19; @@ -12,6 +12,9 @@ $gl-font-size: 15px; $list-font-size: 15px; $sidebar_collapsed_width: 62px; $sidebar_width: 230px; +$gutter_collapsed_width: 62px; +$gutter_width: 290px; +$gutter_inner_width: 258px; $avatar_radius: 50%; $code_font_size: 13px; $code_line_height: 1.5; @@ -22,10 +25,12 @@ $header-height: 58px; $fixed-layout-width: 1280px; $gl-gray: #5a5a5a; $gl-padding: 16px; +$gl-btn-padding: 10px; $gl-vert-padding: 6px; $gl-padding-top:10px; -$gl-avatar-size: 46px; +$gl-avatar-size: 40px; $secondary-text: #7f8fa4; +$error-exclamation-point: #E62958; /* * Color schema @@ -35,11 +40,12 @@ $white-light: #FFFFFF; $white-normal: #ededed; $white-dark: #ededed; -$gray-light: #f7f7f7; -$gray-normal: #ededed; +$gray-light: #faf9f9; +$gray-normal: #f5f5f5; $gray-dark: #ededed; +$gray-darkest: #c9c9c9; -$green-light: #31AF64; +$green-light: #38ae67; $green-normal: #2FAA60; $green-dark: #2CA05B; @@ -51,7 +57,7 @@ $blue-medium-light: #3498CB; $blue-medium: #2F8EBF; $blue-medium-dark: #2D86B4; -$orange-light: #FC6443; +$orange-light: rgba(252, 109, 38, 0.80); $orange-normal: #E75E40; $orange-dark: #CE5237; @@ -63,8 +69,8 @@ $border-white-light: #F1F2F4; $border-white-normal: #D6DAE2; $border-white-dark: #C6CACF; -$border-gray-light: #d1d1d1; -$border-gray-normal: #D6DAE2; +$border-gray-light: rgba(0, 0, 0, 0.06); +$border-gray-normal: rgba(0, 0, 0, 0.10);; $border-gray-dark: #C6CACF; $border-green-light: #2FAA60; @@ -75,7 +81,7 @@ $border-blue-light: #2D9FD8; $border-blue-normal: #2897CE; $border-blue-dark: #258DC1; -$border-orange-light: #ED5C3D; +$border-orange-light: #fc6d26; $border-orange-normal: #CE5237; $border-orange-dark: #C14E35; diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss index 144852e787..a61161810a 100644 --- a/app/assets/stylesheets/pages/admin.scss +++ b/app/assets/stylesheets/pages/admin.scss @@ -55,6 +55,16 @@ @extend .alert-warning; padding: 10px; text-align: center; + + > div, p { + display: inline; + margin: 0; + + a { + color: inherit; + text-decoration: underline; + } + } } .broadcast-message-preview { diff --git a/app/assets/stylesheets/pages/appearances.scss b/app/assets/stylesheets/pages/appearances.scss new file mode 100644 index 0000000000..e2070f17c3 --- /dev/null +++ b/app/assets/stylesheets/pages/appearances.scss @@ -0,0 +1,11 @@ +.appearance-logo-preview { + max-width: 400px; + margin-bottom: 20px; +} + +.appearance-light-logo-preview { + background-color: $background-color; + max-width: 72px; + padding: 10px; + margin-bottom: 10px; +} diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss index 25a86cd0f9..8863939914 100644 --- a/app/assets/stylesheets/pages/dashboard.scss +++ b/app/assets/stylesheets/pages/dashboard.scss @@ -40,10 +40,6 @@ .avatar { @include border-radius(50%); } - - .identicon { - line-height: 46px; - } } .dash-project-access-icon { diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 529a43548c..d93b6ee673 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -12,6 +12,14 @@ .identifier { color: #5c5d5e; } + + .issue_created_ago, .author_link { + white-space: nowrap; + } + + .issue-meta { + margin-left: 65px + } } .detail-page-description { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 12b5aaff45..a7925e7954 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -56,6 +56,7 @@ width: 100%; font-family: $monospace_font; border: none; + border-collapse: separate; margin: 0px; padding: 0px; .line_holder td { diff --git a/app/assets/stylesheets/pages/emojis.scss b/app/assets/stylesheets/pages/emojis.scss index 89a94c5a78..6c721b514f 100644 --- a/app/assets/stylesheets/pages/emojis.scss +++ b/app/assets/stylesheets/pages/emojis.scss @@ -1,1272 +1,1736 @@ -/* -File is generated by https://github.com/jakesgordon/sprite-factory and midified manualy -The source: gemojione gem. -*/ - -.emoji-icon{ - background-image: image-url("emoji.png"); - background-repeat: no-repeat; -} - .emoji-0023-20E3 { background-position: 0px 0px; } -.emoji-0030-20E3 { background-position: -20px 0px; } -.emoji-0031-20E3 { background-position: -40px 0px; } -.emoji-0032-20E3 { background-position: -60px 0px; } -.emoji-0033-20E3 { background-position: -80px 0px; } -.emoji-0034-20E3 { background-position: -100px 0px; } -.emoji-0035-20E3 { background-position: -120px 0px; } -.emoji-0036-20E3 { background-position: -140px 0px; } -.emoji-0037-20E3 { background-position: -160px 0px; } -.emoji-0038-20E3 { background-position: -180px 0px; } -.emoji-0039-20E3 { background-position: -200px 0px; } -.emoji-00A9 { background-position: -220px 0px; } -.emoji-00AE { background-position: -240px 0px; } -.emoji-1F004 { background-position: -260px 0px; } -.emoji-1F0CF { background-position: -280px 0px; } -.emoji-1F170 { background-position: -300px 0px; } -.emoji-1F171 { background-position: -320px 0px; } -.emoji-1F17E { background-position: -340px 0px; } -.emoji-1F17F { background-position: -360px 0px; } -.emoji-1F18E { background-position: -380px 0px; } -.emoji-1F191 { background-position: -400px 0px; } -.emoji-1F192 { background-position: -420px 0px; } -.emoji-1F193 { background-position: -440px 0px; } -.emoji-1F194 { background-position: -460px 0px; } -.emoji-1F195 { background-position: -480px 0px; } -.emoji-1F196 { background-position: -500px 0px; } -.emoji-1F197 { background-position: -520px 0px; } -.emoji-1F198 { background-position: -540px 0px; } -.emoji-1F199 { background-position: -560px 0px; } -.emoji-1F19A { background-position: -580px 0px; } -.emoji-1F1E6-1F1E8 { background-position: -600px 0px; } -.emoji-1F1E6-1F1E9 { background-position: -620px 0px; } -.emoji-1F1E6-1F1EA { background-position: -640px 0px; } -.emoji-1F1E6-1F1EB { background-position: -660px 0px; } -.emoji-1F1E6-1F1EC { background-position: -680px 0px; } -.emoji-1F1E6-1F1EE { background-position: -700px 0px; } -.emoji-1F1E6-1F1F1 { background-position: -720px 0px; } -.emoji-1F1E6-1F1F2 { background-position: -740px 0px; } -.emoji-1F1E6-1F1F4 { background-position: -760px 0px; } -.emoji-1F1E6-1F1F7 { background-position: -780px 0px; } -.emoji-1F1E6-1F1F9 { background-position: -800px 0px; } -.emoji-1F1E6-1F1FA { background-position: -820px 0px; } -.emoji-1F1E6-1F1FC { background-position: -840px 0px; } -.emoji-1F1E6-1F1FF { background-position: -860px 0px; } -.emoji-1F1E7-1F1E6 { background-position: -880px 0px; } -.emoji-1F1E7-1F1E7 { background-position: -900px 0px; } -.emoji-1F1E7-1F1E9 { background-position: -920px 0px; } -.emoji-1F1E7-1F1EA { background-position: -940px 0px; } -.emoji-1F1E7-1F1EB { background-position: -960px 0px; } -.emoji-1F1E7-1F1EC { background-position: -980px 0px; } -.emoji-1F1E7-1F1ED { background-position: -1000px 0px; } -.emoji-1F1E7-1F1EE { background-position: -1020px 0px; } -.emoji-1F1E7-1F1EF { background-position: -1040px 0px; } -.emoji-1F1E7-1F1F2 { background-position: -1060px 0px; } -.emoji-1F1E7-1F1F3 { background-position: -1080px 0px; } -.emoji-1F1E7-1F1F4 { background-position: -1100px 0px; } -.emoji-1F1E7-1F1F7 { background-position: -1120px 0px; } -.emoji-1F1E7-1F1F8 { background-position: -1140px 0px; } -.emoji-1F1E7-1F1F9 { background-position: -1160px 0px; } -.emoji-1F1E7-1F1FC { background-position: -1180px 0px; } -.emoji-1F1E7-1F1FE { background-position: -1200px 0px; } -.emoji-1F1E7-1F1FF { background-position: -1220px 0px; } -.emoji-1F1E8-1F1E6 { background-position: -1240px 0px; } -.emoji-1F1E8-1F1E9 { background-position: -1260px 0px; } -.emoji-1F1E8-1F1EB { background-position: -1280px 0px; } -.emoji-1F1E8-1F1EC { background-position: -1300px 0px; } -.emoji-1F1E8-1F1ED { background-position: -1320px 0px; } -.emoji-1F1E8-1F1EE { background-position: -1340px 0px; } -.emoji-1F1E8-1F1F1 { background-position: -1360px 0px; } -.emoji-1F1E8-1F1F2 { background-position: -1380px 0px; } -.emoji-1F1E8-1F1F3 { background-position: -1400px 0px; } -.emoji-1F1E8-1F1F4 { background-position: -1420px 0px; } -.emoji-1F1E8-1F1F7 { background-position: -1440px 0px; } -.emoji-1F1E8-1F1FA { background-position: -1460px 0px; } -.emoji-1F1E8-1F1FB { background-position: -1480px 0px; } -.emoji-1F1E8-1F1FE { background-position: -1500px 0px; } -.emoji-1F1E8-1F1FF { background-position: -1520px 0px; } -.emoji-1F1E9-1F1EA { background-position: -1540px 0px; } -.emoji-1F1E9-1F1EF { background-position: -1560px 0px; } -.emoji-1F1E9-1F1F0 { background-position: -1580px 0px; } -.emoji-1F1E9-1F1F2 { background-position: -1600px 0px; } -.emoji-1F1E9-1F1F4 { background-position: -1620px 0px; } -.emoji-1F1E9-1F1FF { background-position: -1640px 0px; } -.emoji-1F1EA-1F1E8 { background-position: -1660px 0px; } -.emoji-1F1EA-1F1EA { background-position: -1680px 0px; } -.emoji-1F1EA-1F1EC { background-position: -1700px 0px; } -.emoji-1F1EA-1F1ED { background-position: -1720px 0px; } -.emoji-1F1EA-1F1F7 { background-position: -1740px 0px; } -.emoji-1F1EA-1F1F8 { background-position: -1760px 0px; } -.emoji-1F1EA-1F1F9 { background-position: -1780px 0px; } -.emoji-1F1EB-1F1EE { background-position: -1800px 0px; } -.emoji-1F1EB-1F1EF { background-position: -1820px 0px; } -.emoji-1F1EB-1F1F0 { background-position: -1840px 0px; } -.emoji-1F1EB-1F1F2 { background-position: -1860px 0px; } -.emoji-1F1EB-1F1F4 { background-position: -1880px 0px; } -.emoji-1F1EB-1F1F7 { background-position: -1900px 0px; } -.emoji-1F1EC-1F1E6 { background-position: -1920px 0px; } -.emoji-1F1EC-1F1E7 { background-position: -1940px 0px; } -.emoji-1F1EC-1F1E9 { background-position: -1960px 0px; } -.emoji-1F1EC-1F1EA { background-position: -1980px 0px; } -.emoji-1F1EC-1F1ED { background-position: -2000px 0px; } -.emoji-1F1EC-1F1EE { background-position: -2020px 0px; } -.emoji-1F1EC-1F1F1 { background-position: -2040px 0px; } -.emoji-1F1EC-1F1F2 { background-position: -2060px 0px; } -.emoji-1F1EC-1F1F3 { background-position: -2080px 0px; } -.emoji-1F1EC-1F1F6 { background-position: -2100px 0px; } -.emoji-1F1EC-1F1F7 { background-position: -2120px 0px; } -.emoji-1F1EC-1F1F9 { background-position: -2140px 0px; } -.emoji-1F1EC-1F1FA { background-position: -2160px 0px; } -.emoji-1F1EC-1F1FC { background-position: -2180px 0px; } -.emoji-1F1EC-1F1FE { background-position: -2200px 0px; } -.emoji-1F1ED-1F1F0 { background-position: -2220px 0px; } -.emoji-1F1ED-1F1F3 { background-position: -2240px 0px; } -.emoji-1F1ED-1F1F7 { background-position: -2260px 0px; } -.emoji-1F1ED-1F1F9 { background-position: -2280px 0px; } -.emoji-1F1ED-1F1FA { background-position: -2300px 0px; } -.emoji-1F1EE-1F1E9 { background-position: -2320px 0px; } -.emoji-1F1EE-1F1EA { background-position: -2340px 0px; } -.emoji-1F1EE-1F1F1 { background-position: -2360px 0px; } -.emoji-1F1EE-1F1F3 { background-position: -2380px 0px; } -.emoji-1F1EE-1F1F6 { background-position: -2400px 0px; } -.emoji-1F1EE-1F1F7 { background-position: -2420px 0px; } -.emoji-1F1EE-1F1F8 { background-position: -2440px 0px; } -.emoji-1F1EE-1F1F9 { background-position: -2460px 0px; } -.emoji-1F1EF-1F1EA { background-position: -2480px 0px; } -.emoji-1F1EF-1F1F2 { background-position: -2500px 0px; } -.emoji-1F1EF-1F1F4 { background-position: -2520px 0px; } -.emoji-1F1EF-1F1F5 { background-position: -2540px 0px; } -.emoji-1F1F0-1F1EA { background-position: -2560px 0px; } -.emoji-1F1F0-1F1EC { background-position: -2580px 0px; } -.emoji-1F1F0-1F1ED { background-position: -2600px 0px; } -.emoji-1F1F0-1F1EE { background-position: -2620px 0px; } -.emoji-1F1F0-1F1F2 { background-position: -2640px 0px; } -.emoji-1F1F0-1F1F3 { background-position: -2660px 0px; } -.emoji-1F1F0-1F1F5 { background-position: -2680px 0px; } -.emoji-1F1F0-1F1F7 { background-position: -2700px 0px; } -.emoji-1F1F0-1F1FC { background-position: -2720px 0px; } -.emoji-1F1F0-1F1FE { background-position: -2740px 0px; } -.emoji-1F1F0-1F1FF { background-position: -2760px 0px; } -.emoji-1F1F1-1F1E6 { background-position: -2780px 0px; } -.emoji-1F1F1-1F1E7 { background-position: -2800px 0px; } -.emoji-1F1F1-1F1E8 { background-position: -2820px 0px; } -.emoji-1F1F1-1F1EE { background-position: -2840px 0px; } -.emoji-1F1F1-1F1F0 { background-position: -2860px 0px; } -.emoji-1F1F1-1F1F7 { background-position: -2880px 0px; } -.emoji-1F1F1-1F1F8 { background-position: -2900px 0px; } -.emoji-1F1F1-1F1F9 { background-position: -2920px 0px; } -.emoji-1F1F1-1F1FA { background-position: -2940px 0px; } -.emoji-1F1F1-1F1FB { background-position: -2960px 0px; } -.emoji-1F1F1-1F1FE { background-position: -2980px 0px; } -.emoji-1F1F2-1F1E6 { background-position: -3000px 0px; } -.emoji-1F1F2-1F1E8 { background-position: -3020px 0px; } -.emoji-1F1F2-1F1E9 { background-position: -3040px 0px; } -.emoji-1F1F2-1F1EA { background-position: -3060px 0px; } -.emoji-1F1F2-1F1EC { background-position: -3080px 0px; } -.emoji-1F1F2-1F1ED { background-position: -3100px 0px; } -.emoji-1F1F2-1F1F0 { background-position: -3120px 0px; } -.emoji-1F1F2-1F1F1 { background-position: -3140px 0px; } -.emoji-1F1F2-1F1F2 { background-position: -3160px 0px; } -.emoji-1F1F2-1F1F3 { background-position: -3180px 0px; } -.emoji-1F1F2-1F1F4 { background-position: -3200px 0px; } -.emoji-1F1F2-1F1F7 { background-position: -3220px 0px; } -.emoji-1F1F2-1F1F8 { background-position: -3240px 0px; } -.emoji-1F1F2-1F1F9 { background-position: -3260px 0px; } -.emoji-1F1F2-1F1FA { background-position: -3280px 0px; } -.emoji-1F1F2-1F1FB { background-position: -3300px 0px; } -.emoji-1F1F2-1F1FC { background-position: -3320px 0px; } -.emoji-1F1F2-1F1FD { background-position: -3340px 0px; } -.emoji-1F1F2-1F1FE { background-position: -3360px 0px; } -.emoji-1F1F2-1F1FF { background-position: -3380px 0px; } -.emoji-1F1F3-1F1E6 { background-position: -3400px 0px; } -.emoji-1F1F3-1F1E8 { background-position: -3420px 0px; } -.emoji-1F1F3-1F1EA { background-position: -3440px 0px; } -.emoji-1F1F3-1F1EC { background-position: -3460px 0px; } -.emoji-1F1F3-1F1EE { background-position: -3480px 0px; } -.emoji-1F1F3-1F1F1 { background-position: -3500px 0px; } -.emoji-1F1F3-1F1F4 { background-position: -3520px 0px; } -.emoji-1F1F3-1F1F5 { background-position: -3540px 0px; } -.emoji-1F1F3-1F1F7 { background-position: -3560px 0px; } -.emoji-1F1F3-1F1FA { background-position: -3580px 0px; } -.emoji-1F1F3-1F1FF { background-position: -3600px 0px; } -.emoji-1F1F4-1F1F2 { background-position: -3620px 0px; } -.emoji-1F1F5-1F1E6 { background-position: -3640px 0px; } -.emoji-1F1F5-1F1EA { background-position: -3660px 0px; } -.emoji-1F1F5-1F1EB { background-position: -3680px 0px; } -.emoji-1F1F5-1F1EC { background-position: -3700px 0px; } -.emoji-1F1F5-1F1ED { background-position: -3720px 0px; } -.emoji-1F1F5-1F1F0 { background-position: -3740px 0px; } -.emoji-1F1F5-1F1F1 { background-position: -3760px 0px; } -.emoji-1F1F5-1F1F7 { background-position: -3780px 0px; } -.emoji-1F1F5-1F1F8 { background-position: -3800px 0px; } -.emoji-1F1F5-1F1F9 { background-position: -3820px 0px; } -.emoji-1F1F5-1F1FC { background-position: -3840px 0px; } -.emoji-1F1F5-1F1FE { background-position: -3860px 0px; } -.emoji-1F1F6-1F1E6 { background-position: -3880px 0px; } -.emoji-1F1F7-1F1F4 { background-position: -3900px 0px; } -.emoji-1F1F7-1F1F8 { background-position: -3920px 0px; } -.emoji-1F1F7-1F1FA { background-position: -3940px 0px; } -.emoji-1F1F7-1F1FC { background-position: -3960px 0px; } -.emoji-1F1F8-1F1E6 { background-position: -3980px 0px; } -.emoji-1F1F8-1F1E7 { background-position: -4000px 0px; } -.emoji-1F1F8-1F1E8 { background-position: -4020px 0px; } -.emoji-1F1F8-1F1E9 { background-position: -4040px 0px; } -.emoji-1F1F8-1F1EA { background-position: -4060px 0px; } -.emoji-1F1F8-1F1EC { background-position: -4080px 0px; } -.emoji-1F1F8-1F1ED { background-position: -4100px 0px; } -.emoji-1F1F8-1F1EE { background-position: -4120px 0px; } -.emoji-1F1F8-1F1F0 { background-position: -4140px 0px; } -.emoji-1F1F8-1F1F1 { background-position: -4160px 0px; } -.emoji-1F1F8-1F1F2 { background-position: -4180px 0px; } -.emoji-1F1F8-1F1F3 { background-position: -4200px 0px; } -.emoji-1F1F8-1F1F4 { background-position: -4220px 0px; } -.emoji-1F1F8-1F1F7 { background-position: -4240px 0px; } -.emoji-1F1F8-1F1F9 { background-position: -4260px 0px; } -.emoji-1F1F8-1F1FB { background-position: -4280px 0px; } -.emoji-1F1F8-1F1FE { background-position: -4300px 0px; } -.emoji-1F1F8-1F1FF { background-position: -4320px 0px; } -.emoji-1F1F9-1F1E9 { background-position: -4340px 0px; } -.emoji-1F1F9-1F1EC { background-position: -4360px 0px; } -.emoji-1F1F9-1F1ED { background-position: -4380px 0px; } -.emoji-1F1F9-1F1EF { background-position: -4400px 0px; } -.emoji-1F1F9-1F1F1 { background-position: -4420px 0px; } -.emoji-1F1F9-1F1F2 { background-position: -4440px 0px; } -.emoji-1F1F9-1F1F3 { background-position: -4460px 0px; } -.emoji-1F1F9-1F1F4 { background-position: -4480px 0px; } -.emoji-1F1F9-1F1F7 { background-position: -4500px 0px; } -.emoji-1F1F9-1F1F9 { background-position: -4520px 0px; } -.emoji-1F1F9-1F1FB { background-position: -4540px 0px; } -.emoji-1F1F9-1F1FC { background-position: -4560px 0px; } -.emoji-1F1F9-1F1FF { background-position: -4580px 0px; } -.emoji-1F1FA-1F1E6 { background-position: -4600px 0px; } -.emoji-1F1FA-1F1EC { background-position: -4620px 0px; } -.emoji-1F1FA-1F1F8 { background-position: -4640px 0px; } -.emoji-1F1FA-1F1FE { background-position: -4660px 0px; } -.emoji-1F1FA-1F1FF { background-position: -4680px 0px; } -.emoji-1F1FB-1F1E6 { background-position: -4700px 0px; } -.emoji-1F1FB-1F1E8 { background-position: -4720px 0px; } -.emoji-1F1FB-1F1EA { background-position: -4740px 0px; } -.emoji-1F1FB-1F1EE { background-position: -4760px 0px; } -.emoji-1F1FB-1F1F3 { background-position: -4780px 0px; } -.emoji-1F1FB-1F1FA { background-position: -4800px 0px; } -.emoji-1F1FC-1F1EB { background-position: -4820px 0px; } -.emoji-1F1FC-1F1F8 { background-position: -4840px 0px; } -.emoji-1F1FD-1F1F0 { background-position: -4860px 0px; } -.emoji-1F1FE-1F1EA { background-position: -4880px 0px; } -.emoji-1F1FF-1F1E6 { background-position: -4900px 0px; } -.emoji-1F1FF-1F1F2 { background-position: -4920px 0px; } -.emoji-1F1FF-1F1FC { background-position: -4940px 0px; } -.emoji-1F201 { background-position: -4960px 0px; } -.emoji-1F202 { background-position: -4980px 0px; } -.emoji-1F21A { background-position: -5000px 0px; } -.emoji-1F22F { background-position: -5020px 0px; } -.emoji-1F232 { background-position: -5040px 0px; } -.emoji-1F233 { background-position: -5060px 0px; } -.emoji-1F234 { background-position: -5080px 0px; } -.emoji-1F235 { background-position: -5100px 0px; } -.emoji-1F236 { background-position: -5120px 0px; } -.emoji-1F237 { background-position: -5140px 0px; } -.emoji-1F238 { background-position: -5160px 0px; } -.emoji-1F239 { background-position: -5180px 0px; } -.emoji-1F23A { background-position: -5200px 0px; } -.emoji-1F250 { background-position: -5220px 0px; } -.emoji-1F251 { background-position: -5240px 0px; } -.emoji-1F300 { background-position: -5260px 0px; } -.emoji-1F301 { background-position: -5280px 0px; } -.emoji-1F302 { background-position: -5300px 0px; } -.emoji-1F303 { background-position: -5320px 0px; } -.emoji-1F304 { background-position: -5340px 0px; } -.emoji-1F305 { background-position: -5360px 0px; } -.emoji-1F306 { background-position: -5380px 0px; } -.emoji-1F307 { background-position: -5400px 0px; } -.emoji-1F308 { background-position: -5420px 0px; } -.emoji-1F309 { background-position: -5440px 0px; } -.emoji-1F30A { background-position: -5460px 0px; } -.emoji-1F30B { background-position: -5480px 0px; } -.emoji-1F30C { background-position: -5500px 0px; } -.emoji-1F30D { background-position: -5520px 0px; } -.emoji-1F30E { background-position: -5540px 0px; } -.emoji-1F30F { background-position: -5560px 0px; } -.emoji-1F310 { background-position: -5580px 0px; } -.emoji-1F311 { background-position: -5600px 0px; } -.emoji-1F312 { background-position: -5620px 0px; } -.emoji-1F313 { background-position: -5640px 0px; } -.emoji-1F314 { background-position: -5660px 0px; } -.emoji-1F315 { background-position: -5680px 0px; } -.emoji-1F316 { background-position: -5700px 0px; } -.emoji-1F317 { background-position: -5720px 0px; } -.emoji-1F318 { background-position: -5740px 0px; } -.emoji-1F319 { background-position: -5760px 0px; } -.emoji-1F31A { background-position: -5780px 0px; } -.emoji-1F31B { background-position: -5800px 0px; } -.emoji-1F31C { background-position: -5820px 0px; } -.emoji-1F31D { background-position: -5840px 0px; } -.emoji-1F31E { background-position: -5860px 0px; } -.emoji-1F31F { background-position: -5880px 0px; } -.emoji-1F320 { background-position: -5900px 0px; } -.emoji-1F321 { background-position: -5920px 0px; } -.emoji-1F327 { background-position: -5940px 0px; } -.emoji-1F328 { background-position: -5960px 0px; } -.emoji-1F329 { background-position: -5980px 0px; } -.emoji-1F32A { background-position: -6000px 0px; } -.emoji-1F32B { background-position: -6020px 0px; } -.emoji-1F32C { background-position: -6040px 0px; } -.emoji-1F330 { background-position: -6060px 0px; } -.emoji-1F331 { background-position: -6080px 0px; } -.emoji-1F332 { background-position: -6100px 0px; } -.emoji-1F333 { background-position: -6120px 0px; } -.emoji-1F334 { background-position: -6140px 0px; } -.emoji-1F335 { background-position: -6160px 0px; } -.emoji-1F336 { background-position: -6180px 0px; } -.emoji-1F337 { background-position: -6200px 0px; } -.emoji-1F338 { background-position: -6220px 0px; } -.emoji-1F339 { background-position: -6240px 0px; } -.emoji-1F33A { background-position: -6260px 0px; } -.emoji-1F33B { background-position: -6280px 0px; } -.emoji-1F33C { background-position: -6300px 0px; } -.emoji-1F33D { background-position: -6320px 0px; } -.emoji-1F33E { background-position: -6340px 0px; } -.emoji-1F33F { background-position: -6360px 0px; } -.emoji-1F340 { background-position: -6380px 0px; } -.emoji-1F341 { background-position: -6400px 0px; } -.emoji-1F342 { background-position: -6420px 0px; } -.emoji-1F343 { background-position: -6440px 0px; } -.emoji-1F344 { background-position: -6460px 0px; } -.emoji-1F345 { background-position: -6480px 0px; } -.emoji-1F346 { background-position: -6500px 0px; } -.emoji-1F347 { background-position: -6520px 0px; } -.emoji-1F348 { background-position: -6540px 0px; } -.emoji-1F349 { background-position: -6560px 0px; } -.emoji-1F34A { background-position: -6580px 0px; } -.emoji-1F34B { background-position: -6600px 0px; } -.emoji-1F34C { background-position: -6620px 0px; } -.emoji-1F34D { background-position: -6640px 0px; } -.emoji-1F34E { background-position: -6660px 0px; } -.emoji-1F34F { background-position: -6680px 0px; } -.emoji-1F350 { background-position: -6700px 0px; } -.emoji-1F351 { background-position: -6720px 0px; } -.emoji-1F352 { background-position: -6740px 0px; } -.emoji-1F353 { background-position: -6760px 0px; } -.emoji-1F354 { background-position: -6780px 0px; } -.emoji-1F355 { background-position: -6800px 0px; } -.emoji-1F356 { background-position: -6820px 0px; } -.emoji-1F357 { background-position: -6840px 0px; } -.emoji-1F358 { background-position: -6860px 0px; } -.emoji-1F359 { background-position: -6880px 0px; } -.emoji-1F35A { background-position: -6900px 0px; } -.emoji-1F35B { background-position: -6920px 0px; } -.emoji-1F35C { background-position: -6940px 0px; } -.emoji-1F35D { background-position: -6960px 0px; } -.emoji-1F35E { background-position: -6980px 0px; } -.emoji-1F35F { background-position: -7000px 0px; } -.emoji-1F360 { background-position: -7020px 0px; } -.emoji-1F361 { background-position: -7040px 0px; } -.emoji-1F362 { background-position: -7060px 0px; } -.emoji-1F363 { background-position: -7080px 0px; } -.emoji-1F364 { background-position: -7100px 0px; } -.emoji-1F365 { background-position: -7120px 0px; } -.emoji-1F366 { background-position: -7140px 0px; } -.emoji-1F367 { background-position: -7160px 0px; } -.emoji-1F368 { background-position: -7180px 0px; } -.emoji-1F369 { background-position: -7200px 0px; } -.emoji-1F36A { background-position: -7220px 0px; } -.emoji-1F36B { background-position: -7240px 0px; } -.emoji-1F36C { background-position: -7260px 0px; } -.emoji-1F36D { background-position: -7280px 0px; } -.emoji-1F36E { background-position: -7300px 0px; } -.emoji-1F36F { background-position: -7320px 0px; } -.emoji-1F370 { background-position: -7340px 0px; } -.emoji-1F371 { background-position: -7360px 0px; } -.emoji-1F372 { background-position: -7380px 0px; } -.emoji-1F373 { background-position: -7400px 0px; } -.emoji-1F374 { background-position: -7420px 0px; } -.emoji-1F375 { background-position: -7440px 0px; } -.emoji-1F376 { background-position: -7460px 0px; } -.emoji-1F377 { background-position: -7480px 0px; } -.emoji-1F378 { background-position: -7500px 0px; } -.emoji-1F379 { background-position: -7520px 0px; } -.emoji-1F37A { background-position: -7540px 0px; } -.emoji-1F37B { background-position: -7560px 0px; } -.emoji-1F37C { background-position: -7580px 0px; } -.emoji-1F37D { background-position: -7600px 0px; } -.emoji-1F380 { background-position: -7620px 0px; } -.emoji-1F381 { background-position: -7640px 0px; } -.emoji-1F382 { background-position: -7660px 0px; } -.emoji-1F383 { background-position: -7680px 0px; } -.emoji-1F384 { background-position: -7700px 0px; } -.emoji-1F385 { background-position: -7720px 0px; } -.emoji-1F386 { background-position: -7740px 0px; } -.emoji-1F387 { background-position: -7760px 0px; } -.emoji-1F388 { background-position: -7780px 0px; } -.emoji-1F389 { background-position: -7800px 0px; } -.emoji-1F38A { background-position: -7820px 0px; } -.emoji-1F38B { background-position: -7840px 0px; } -.emoji-1F38C { background-position: -7860px 0px; } -.emoji-1F38D { background-position: -7880px 0px; } -.emoji-1F38E { background-position: -7900px 0px; } -.emoji-1F38F { background-position: -7920px 0px; } -.emoji-1F390 { background-position: -7940px 0px; } -.emoji-1F391 { background-position: -7960px 0px; } -.emoji-1F392 { background-position: -7980px 0px; } -.emoji-1F393 { background-position: -8000px 0px; } -.emoji-1F394 { background-position: -8020px 0px; } -.emoji-1F395 { background-position: -8040px 0px; } -.emoji-1F396 { background-position: -8060px 0px; } -.emoji-1F397 { background-position: -8080px 0px; } -.emoji-1F398 { background-position: -8100px 0px; } -.emoji-1F399 { background-position: -8120px 0px; } -.emoji-1F39A { background-position: -8140px 0px; } -.emoji-1F39B { background-position: -8160px 0px; } -.emoji-1F39C { background-position: -8180px 0px; } -.emoji-1F39D { background-position: -8200px 0px; } -.emoji-1F39E { background-position: -8220px 0px; } -.emoji-1F39F { background-position: -8240px 0px; } -.emoji-1F3A0 { background-position: -8260px 0px; } -.emoji-1F3A1 { background-position: -8280px 0px; } -.emoji-1F3A2 { background-position: -8300px 0px; } -.emoji-1F3A3 { background-position: -8320px 0px; } -.emoji-1F3A4 { background-position: -8340px 0px; } -.emoji-1F3A5 { background-position: -8360px 0px; } -.emoji-1F3A6 { background-position: -8380px 0px; } -.emoji-1F3A7 { background-position: -8400px 0px; } -.emoji-1F3A8 { background-position: -8420px 0px; } -.emoji-1F3A9 { background-position: -8440px 0px; } -.emoji-1F3AA { background-position: -8460px 0px; } -.emoji-1F3AB { background-position: -8480px 0px; } -.emoji-1F3AC { background-position: -8500px 0px; } -.emoji-1F3AD { background-position: -8520px 0px; } -.emoji-1F3AE { background-position: -8540px 0px; } -.emoji-1F3AF { background-position: -8560px 0px; } -.emoji-1F3B0 { background-position: -8580px 0px; } -.emoji-1F3B1 { background-position: -8600px 0px; } -.emoji-1F3B2 { background-position: -8620px 0px; } -.emoji-1F3B3 { background-position: -8640px 0px; } -.emoji-1F3B4 { background-position: -8660px 0px; } -.emoji-1F3B5 { background-position: -8680px 0px; } -.emoji-1F3B6 { background-position: -8700px 0px; } -.emoji-1F3B7 { background-position: -8720px 0px; } -.emoji-1F3B8 { background-position: -8740px 0px; } -.emoji-1F3B9 { background-position: -8760px 0px; } -.emoji-1F3BA { background-position: -8780px 0px; } -.emoji-1F3BB { background-position: -8800px 0px; } -.emoji-1F3BC { background-position: -8820px 0px; } -.emoji-1F3BD { background-position: -8840px 0px; } -.emoji-1F3BE { background-position: -8860px 0px; } -.emoji-1F3BF { background-position: -8880px 0px; } -.emoji-1F3C0 { background-position: -8900px 0px; } -.emoji-1F3C1 { background-position: -8920px 0px; } -.emoji-1F3C2 { background-position: -8940px 0px; } -.emoji-1F3C3 { background-position: -8960px 0px; } -.emoji-1F3C4 { background-position: -8980px 0px; } -.emoji-1F3C5 { background-position: -9000px 0px; } -.emoji-1F3C6 { background-position: -9020px 0px; } -.emoji-1F3C7 { background-position: -9040px 0px; } -.emoji-1F3C8 { background-position: -9060px 0px; } -.emoji-1F3C9 { background-position: -9080px 0px; } -.emoji-1F3CA { background-position: -9100px 0px; } -.emoji-1F3CB { background-position: -9120px 0px; } -.emoji-1F3CC { background-position: -9140px 0px; } -.emoji-1F3CD { background-position: -9160px 0px; } -.emoji-1F3CE { background-position: -9180px 0px; } -.emoji-1F3D4 { background-position: -9200px 0px; } -.emoji-1F3D5 { background-position: -9220px 0px; } -.emoji-1F3D6 { background-position: -9240px 0px; } -.emoji-1F3D7 { background-position: -9260px 0px; } -.emoji-1F3D8 { background-position: -9280px 0px; } -.emoji-1F3D9 { background-position: -9300px 0px; } -.emoji-1F3DA { background-position: -9320px 0px; } -.emoji-1F3DB { background-position: -9340px 0px; } -.emoji-1F3DC { background-position: -9360px 0px; } -.emoji-1F3DD { background-position: -9380px 0px; } -.emoji-1F3DE { background-position: -9400px 0px; } -.emoji-1F3DF { background-position: -9420px 0px; } -.emoji-1F3E0 { background-position: -9440px 0px; } -.emoji-1F3E1 { background-position: -9460px 0px; } -.emoji-1F3E2 { background-position: -9480px 0px; } -.emoji-1F3E3 { background-position: -9500px 0px; } -.emoji-1F3E4 { background-position: -9520px 0px; } -.emoji-1F3E5 { background-position: -9540px 0px; } -.emoji-1F3E6 { background-position: -9560px 0px; } -.emoji-1F3E7 { background-position: -9580px 0px; } -.emoji-1F3E8 { background-position: -9600px 0px; } -.emoji-1F3E9 { background-position: -9620px 0px; } -.emoji-1F3EA { background-position: -9640px 0px; } -.emoji-1F3EB { background-position: -9660px 0px; } -.emoji-1F3EC { background-position: -9680px 0px; } -.emoji-1F3ED { background-position: -9700px 0px; } -.emoji-1F3EE { background-position: -9720px 0px; } -.emoji-1F3EF { background-position: -9740px 0px; } -.emoji-1F3F0 { background-position: -9760px 0px; } -.emoji-1F3F1 { background-position: -9780px 0px; } -.emoji-1F3F2 { background-position: -9800px 0px; } -.emoji-1F3F3 { background-position: -9820px 0px; } -.emoji-1F3F4 { background-position: -9840px 0px; } -.emoji-1F3F5 { background-position: -9860px 0px; } -.emoji-1F3F6 { background-position: -9880px 0px; } -.emoji-1F3F7 { background-position: -9900px 0px; } -.emoji-1F400 { background-position: -9920px 0px; } -.emoji-1F401 { background-position: -9940px 0px; } -.emoji-1F402 { background-position: -9960px 0px; } -.emoji-1F403 { background-position: -9980px 0px; } -.emoji-1F404 { background-position: -10000px 0px; } -.emoji-1F405 { background-position: -10020px 0px; } -.emoji-1F406 { background-position: -10040px 0px; } -.emoji-1F407 { background-position: -10060px 0px; } -.emoji-1F408 { background-position: -10080px 0px; } -.emoji-1F409 { background-position: -10100px 0px; } -.emoji-1F40A { background-position: -10120px 0px; } -.emoji-1F40B { background-position: -10140px 0px; } -.emoji-1F40C { background-position: -10160px 0px; } -.emoji-1F40D { background-position: -10180px 0px; } -.emoji-1F40E { background-position: -10200px 0px; } -.emoji-1F40F { background-position: -10220px 0px; } -.emoji-1F410 { background-position: -10240px 0px; } -.emoji-1F411 { background-position: -10260px 0px; } -.emoji-1F412 { background-position: -10280px 0px; } -.emoji-1F413 { background-position: -10300px 0px; } -.emoji-1F414 { background-position: -10320px 0px; } -.emoji-1F415 { background-position: -10340px 0px; } -.emoji-1F416 { background-position: -10360px 0px; } -.emoji-1F417 { background-position: -10380px 0px; } -.emoji-1F418 { background-position: -10400px 0px; } -.emoji-1F419 { background-position: -10420px 0px; } -.emoji-1F41A { background-position: -10440px 0px; } -.emoji-1F41B { background-position: -10460px 0px; } -.emoji-1F41C { background-position: -10480px 0px; } -.emoji-1F41D { background-position: -10500px 0px; } -.emoji-1F41E { background-position: -10520px 0px; } -.emoji-1F41F { background-position: -10540px 0px; } -.emoji-1F420 { background-position: -10560px 0px; } -.emoji-1F421 { background-position: -10580px 0px; } -.emoji-1F422 { background-position: -10600px 0px; } -.emoji-1F423 { background-position: -10620px 0px; } -.emoji-1F424 { background-position: -10640px 0px; } -.emoji-1F425 { background-position: -10660px 0px; } -.emoji-1F426 { background-position: -10680px 0px; } -.emoji-1F427 { background-position: -10700px 0px; } -.emoji-1F428 { background-position: -10720px 0px; } -.emoji-1F429 { background-position: -10740px 0px; } -.emoji-1F42A { background-position: -10760px 0px; } -.emoji-1F42B { background-position: -10780px 0px; } -.emoji-1F42C { background-position: -10800px 0px; } -.emoji-1F42D { background-position: -10820px 0px; } -.emoji-1F42E { background-position: -10840px 0px; } -.emoji-1F42F { background-position: -10860px 0px; } -.emoji-1F430 { background-position: -10880px 0px; } -.emoji-1F431 { background-position: -10900px 0px; } -.emoji-1F432 { background-position: -10920px 0px; } -.emoji-1F433 { background-position: -10940px 0px; } -.emoji-1F434 { background-position: -10960px 0px; } -.emoji-1F435 { background-position: -10980px 0px; } -.emoji-1F436 { background-position: -11000px 0px; } -.emoji-1F437 { background-position: -11020px 0px; } -.emoji-1F438 { background-position: -11040px 0px; } -.emoji-1F439 { background-position: -11060px 0px; } -.emoji-1F43A { background-position: -11080px 0px; } -.emoji-1F43B { background-position: -11100px 0px; } -.emoji-1F43C { background-position: -11120px 0px; } -.emoji-1F43D { background-position: -11140px 0px; } -.emoji-1F43E { background-position: -11160px 0px; } -.emoji-1F43F { background-position: -11180px 0px; } -.emoji-1F440 { background-position: -11200px 0px; } -.emoji-1F441 { background-position: -11220px 0px; } -.emoji-1F442 { background-position: -11240px 0px; } -.emoji-1F443 { background-position: -11260px 0px; } -.emoji-1F444 { background-position: -11280px 0px; } -.emoji-1F445 { background-position: -11300px 0px; } -.emoji-1F446 { background-position: -11320px 0px; } -.emoji-1F447 { background-position: -11340px 0px; } -.emoji-1F448 { background-position: -11360px 0px; } -.emoji-1F449 { background-position: -11380px 0px; } -.emoji-1F44A { background-position: -11400px 0px; } -.emoji-1F44B { background-position: -11420px 0px; } -.emoji-1F44C { background-position: -11440px 0px; } -.emoji-1F44D { background-position: -11460px 0px; } -.emoji-1F44E { background-position: -11480px 0px; } -.emoji-1F44F { background-position: -11500px 0px; } -.emoji-1F450 { background-position: -11520px 0px; } -.emoji-1F451 { background-position: -11540px 0px; } -.emoji-1F452 { background-position: -11560px 0px; } -.emoji-1F453 { background-position: -11580px 0px; } -.emoji-1F454 { background-position: -11600px 0px; } -.emoji-1F455 { background-position: -11620px 0px; } -.emoji-1F456 { background-position: -11640px 0px; } -.emoji-1F457 { background-position: -11660px 0px; } -.emoji-1F458 { background-position: -11680px 0px; } -.emoji-1F459 { background-position: -11700px 0px; } -.emoji-1F45A { background-position: -11720px 0px; } -.emoji-1F45B { background-position: -11740px 0px; } -.emoji-1F45C { background-position: -11760px 0px; } -.emoji-1F45D { background-position: -11780px 0px; } -.emoji-1F45E { background-position: -11800px 0px; } -.emoji-1F45F { background-position: -11820px 0px; } -.emoji-1F460 { background-position: -11840px 0px; } -.emoji-1F461 { background-position: -11860px 0px; } -.emoji-1F462 { background-position: -11880px 0px; } -.emoji-1F463 { background-position: -11900px 0px; } -.emoji-1F464 { background-position: -11920px 0px; } -.emoji-1F465 { background-position: -11940px 0px; } -.emoji-1F466 { background-position: -11960px 0px; } -.emoji-1F467 { background-position: -11980px 0px; } -.emoji-1F468 { background-position: -12000px 0px; } -.emoji-1F468-1F468-1F466 { background-position: -12020px 0px; } -.emoji-1F468-1F468-1F466-1F466 { background-position: -12040px 0px; } -.emoji-1F468-1F468-1F467 { background-position: -12060px 0px; } -.emoji-1F468-1F468-1F467-1F466 { background-position: -12080px 0px; } -.emoji-1F468-1F468-1F467-1F467 { background-position: -12100px 0px; } -.emoji-1F468-1F469-1F466-1F466 { background-position: -12120px 0px; } -.emoji-1F468-1F469-1F467 { background-position: -12140px 0px; } -.emoji-1F468-1F469-1F467-1F466 { background-position: -12160px 0px; } -.emoji-1F468-1F469-1F467-1F467 { background-position: -12180px 0px; } -.emoji-1F468-2764-1F468 { background-position: -12200px 0px; } -.emoji-1F468-2764-1F48B-1F468 { background-position: -12220px 0px; } -.emoji-1F469 { background-position: -12240px 0px; } -.emoji-1F469-1F469-1F466 { background-position: -12260px 0px; } -.emoji-1F469-1F469-1F466-1F466 { background-position: -12280px 0px; } -.emoji-1F469-1F469-1F467 { background-position: -12300px 0px; } -.emoji-1F469-1F469-1F467-1F466 { background-position: -12320px 0px; } -.emoji-1F469-1F469-1F467-1F467 { background-position: -12340px 0px; } -.emoji-1F469-2764-1F469 { background-position: -12360px 0px; } -.emoji-1F469-2764-1F48B-1F469 { background-position: -12380px 0px; } -.emoji-1F46A { background-position: -12400px 0px; } -.emoji-1F46B { background-position: -12420px 0px; } -.emoji-1F46C { background-position: -12440px 0px; } -.emoji-1F46D { background-position: -12460px 0px; } -.emoji-1F46E { background-position: -12480px 0px; } -.emoji-1F46F { background-position: -12500px 0px; } -.emoji-1F470 { background-position: -12520px 0px; } -.emoji-1F471 { background-position: -12540px 0px; } -.emoji-1F472 { background-position: -12560px 0px; } -.emoji-1F473 { background-position: -12580px 0px; } -.emoji-1F474 { background-position: -12600px 0px; } -.emoji-1F475 { background-position: -12620px 0px; } -.emoji-1F476 { background-position: -12640px 0px; } -.emoji-1F477 { background-position: -12660px 0px; } -.emoji-1F478 { background-position: -12680px 0px; } -.emoji-1F479 { background-position: -12700px 0px; } -.emoji-1F47A { background-position: -12720px 0px; } -.emoji-1F47B { background-position: -12740px 0px; } -.emoji-1F47C { background-position: -12760px 0px; } -.emoji-1F47D { background-position: -12780px 0px; } -.emoji-1F47E { background-position: -12800px 0px; } -.emoji-1F47F { background-position: -12820px 0px; } -.emoji-1F480 { background-position: -12840px 0px; } -.emoji-1F481 { background-position: -12860px 0px; } -.emoji-1F482 { background-position: -12880px 0px; } -.emoji-1F483 { background-position: -12900px 0px; } -.emoji-1F484 { background-position: -12920px 0px; } -.emoji-1F485 { background-position: -12940px 0px; } -.emoji-1F486 { background-position: -12960px 0px; } -.emoji-1F487 { background-position: -12980px 0px; } -.emoji-1F488 { background-position: -13000px 0px; } -.emoji-1F489 { background-position: -13020px 0px; } -.emoji-1F48A { background-position: -13040px 0px; } -.emoji-1F48B { background-position: -13060px 0px; } -.emoji-1F48C { background-position: -13080px 0px; } -.emoji-1F48D { background-position: -13100px 0px; } -.emoji-1F48E { background-position: -13120px 0px; } -.emoji-1F48F { background-position: -13140px 0px; } -.emoji-1F490 { background-position: -13160px 0px; } -.emoji-1F491 { background-position: -13180px 0px; } -.emoji-1F492 { background-position: -13200px 0px; } -.emoji-1F493 { background-position: -13220px 0px; } -.emoji-1F494 { background-position: -13240px 0px; } -.emoji-1F495 { background-position: -13260px 0px; } -.emoji-1F496 { background-position: -13280px 0px; } -.emoji-1F497 { background-position: -13300px 0px; } -.emoji-1F498 { background-position: -13320px 0px; } -.emoji-1F499 { background-position: -13340px 0px; } -.emoji-1F49A { background-position: -13360px 0px; } -.emoji-1F49B { background-position: -13380px 0px; } -.emoji-1F49C { background-position: -13400px 0px; } -.emoji-1F49D { background-position: -13420px 0px; } -.emoji-1F49E { background-position: -13440px 0px; } -.emoji-1F49F { background-position: -13460px 0px; } -.emoji-1F4A0 { background-position: -13480px 0px; } -.emoji-1F4A1 { background-position: -13500px 0px; } -.emoji-1F4A2 { background-position: -13520px 0px; } -.emoji-1F4A3 { background-position: -13540px 0px; } -.emoji-1F4A4 { background-position: -13560px 0px; } -.emoji-1F4A5 { background-position: -13580px 0px; } -.emoji-1F4A6 { background-position: -13600px 0px; } -.emoji-1F4A7 { background-position: -13620px 0px; } -.emoji-1F4A8 { background-position: -13640px 0px; } -.emoji-1F4A9 { background-position: -13660px 0px; } -.emoji-1F4AA { background-position: -13680px 0px; } -.emoji-1F4AB { background-position: -13700px 0px; } -.emoji-1F4AC { background-position: -13720px 0px; } -.emoji-1F4AD { background-position: -13740px 0px; } -.emoji-1F4AE { background-position: -13760px 0px; } -.emoji-1F4AF { background-position: -13780px 0px; } -.emoji-1F4B0 { background-position: -13800px 0px; } -.emoji-1F4B1 { background-position: -13820px 0px; } -.emoji-1F4B2 { background-position: -13840px 0px; } -.emoji-1F4B3 { background-position: -13860px 0px; } -.emoji-1F4B4 { background-position: -13880px 0px; } -.emoji-1F4B5 { background-position: -13900px 0px; } -.emoji-1F4B6 { background-position: -13920px 0px; } -.emoji-1F4B7 { background-position: -13940px 0px; } -.emoji-1F4B8 { background-position: -13960px 0px; } -.emoji-1F4B9 { background-position: -13980px 0px; } -.emoji-1F4BA { background-position: -14000px 0px; } -.emoji-1F4BB { background-position: -14020px 0px; } -.emoji-1F4BC { background-position: -14040px 0px; } -.emoji-1F4BD { background-position: -14060px 0px; } -.emoji-1F4BE { background-position: -14080px 0px; } -.emoji-1F4BF { background-position: -14100px 0px; } -.emoji-1F4C0 { background-position: -14120px 0px; } -.emoji-1F4C1 { background-position: -14140px 0px; } -.emoji-1F4C2 { background-position: -14160px 0px; } -.emoji-1F4C3 { background-position: -14180px 0px; } -.emoji-1F4C4 { background-position: -14200px 0px; } -.emoji-1F4C5 { background-position: -14220px 0px; } -.emoji-1F4C6 { background-position: -14240px 0px; } -.emoji-1F4C7 { background-position: -14260px 0px; } -.emoji-1F4C8 { background-position: -14280px 0px; } -.emoji-1F4C9 { background-position: -14300px 0px; } -.emoji-1F4CA { background-position: -14320px 0px; } -.emoji-1F4CB { background-position: -14340px 0px; } -.emoji-1F4CC { background-position: -14360px 0px; } -.emoji-1F4CD { background-position: -14380px 0px; } -.emoji-1F4CE { background-position: -14400px 0px; } -.emoji-1F4CF { background-position: -14420px 0px; } -.emoji-1F4D0 { background-position: -14440px 0px; } -.emoji-1F4D1 { background-position: -14460px 0px; } -.emoji-1F4D2 { background-position: -14480px 0px; } -.emoji-1F4D3 { background-position: -14500px 0px; } -.emoji-1F4D4 { background-position: -14520px 0px; } -.emoji-1F4D5 { background-position: -14540px 0px; } -.emoji-1F4D6 { background-position: -14560px 0px; } -.emoji-1F4D7 { background-position: -14580px 0px; } -.emoji-1F4D8 { background-position: -14600px 0px; } -.emoji-1F4D9 { background-position: -14620px 0px; } -.emoji-1F4DA { background-position: -14640px 0px; } -.emoji-1F4DB { background-position: -14660px 0px; } -.emoji-1F4DC { background-position: -14680px 0px; } -.emoji-1F4DD { background-position: -14700px 0px; } -.emoji-1F4DE { background-position: -14720px 0px; } -.emoji-1F4DF { background-position: -14740px 0px; } -.emoji-1F4E0 { background-position: -14760px 0px; } -.emoji-1F4E1 { background-position: -14780px 0px; } -.emoji-1F4E2 { background-position: -14800px 0px; } -.emoji-1F4E3 { background-position: -14820px 0px; } -.emoji-1F4E4 { background-position: -14840px 0px; } -.emoji-1F4E5 { background-position: -14860px 0px; } -.emoji-1F4E6 { background-position: -14880px 0px; } -.emoji-1F4E7 { background-position: -14900px 0px; } -.emoji-1F4E8 { background-position: -14920px 0px; } -.emoji-1F4E9 { background-position: -14940px 0px; } -.emoji-1F4EA { background-position: -14960px 0px; } -.emoji-1F4EB { background-position: -14980px 0px; } -.emoji-1F4EC { background-position: -15000px 0px; } -.emoji-1F4ED { background-position: -15020px 0px; } -.emoji-1F4EE { background-position: -15040px 0px; } -.emoji-1F4EF { background-position: -15060px 0px; } -.emoji-1F4F0 { background-position: -15080px 0px; } -.emoji-1F4F1 { background-position: -15100px 0px; } -.emoji-1F4F2 { background-position: -15120px 0px; } -.emoji-1F4F3 { background-position: -15140px 0px; } -.emoji-1F4F4 { background-position: -15160px 0px; } -.emoji-1F4F5 { background-position: -15180px 0px; } -.emoji-1F4F6 { background-position: -15200px 0px; } -.emoji-1F4F7 { background-position: -15220px 0px; } -.emoji-1F4F8 { background-position: -15240px 0px; } -.emoji-1F4F9 { background-position: -15260px 0px; } -.emoji-1F4FA { background-position: -15280px 0px; } -.emoji-1F4FB { background-position: -15300px 0px; } -.emoji-1F4FC { background-position: -15320px 0px; } -.emoji-1F4FD { background-position: -15340px 0px; } -.emoji-1F4FE { background-position: -15360px 0px; } -.emoji-1F500 { background-position: -15380px 0px; } -.emoji-1F501 { background-position: -15400px 0px; } -.emoji-1F502 { background-position: -15420px 0px; } -.emoji-1F503 { background-position: -15440px 0px; } -.emoji-1F504 { background-position: -15460px 0px; } -.emoji-1F505 { background-position: -15480px 0px; } -.emoji-1F506 { background-position: -15500px 0px; } -.emoji-1F507 { background-position: -15520px 0px; } -.emoji-1F508 { background-position: -15540px 0px; } -.emoji-1F509 { background-position: -15560px 0px; } -.emoji-1F50A { background-position: -15580px 0px; } -.emoji-1F50B { background-position: -15600px 0px; } -.emoji-1F50C { background-position: -15620px 0px; } -.emoji-1F50D { background-position: -15640px 0px; } -.emoji-1F50E { background-position: -15660px 0px; } -.emoji-1F50F { background-position: -15680px 0px; } -.emoji-1F510 { background-position: -15700px 0px; } -.emoji-1F511 { background-position: -15720px 0px; } -.emoji-1F512 { background-position: -15740px 0px; } -.emoji-1F513 { background-position: -15760px 0px; } -.emoji-1F514 { background-position: -15780px 0px; } -.emoji-1F515 { background-position: -15800px 0px; } -.emoji-1F516 { background-position: -15820px 0px; } -.emoji-1F517 { background-position: -15840px 0px; } -.emoji-1F518 { background-position: -15860px 0px; } -.emoji-1F519 { background-position: -15880px 0px; } -.emoji-1F51A { background-position: -15900px 0px; } -.emoji-1F51B { background-position: -15920px 0px; } -.emoji-1F51C { background-position: -15940px 0px; } -.emoji-1F51D { background-position: -15960px 0px; } -.emoji-1F51E { background-position: -15980px 0px; } -.emoji-1F51F { background-position: -16000px 0px; } -.emoji-1F520 { background-position: -16020px 0px; } -.emoji-1F521 { background-position: -16040px 0px; } -.emoji-1F522 { background-position: -16060px 0px; } -.emoji-1F523 { background-position: -16080px 0px; } -.emoji-1F524 { background-position: -16100px 0px; } -.emoji-1F525 { background-position: -16120px 0px; } -.emoji-1F526 { background-position: -16140px 0px; } -.emoji-1F527 { background-position: -16160px 0px; } -.emoji-1F528 { background-position: -16180px 0px; } -.emoji-1F529 { background-position: -16200px 0px; } -.emoji-1F52A { background-position: -16220px 0px; } -.emoji-1F52B { background-position: -16240px 0px; } -.emoji-1F52C { background-position: -16260px 0px; } -.emoji-1F52D { background-position: -16280px 0px; } -.emoji-1F52E { background-position: -16300px 0px; } -.emoji-1F52F { background-position: -16320px 0px; } -.emoji-1F530 { background-position: -16340px 0px; } -.emoji-1F531 { background-position: -16360px 0px; } -.emoji-1F532 { background-position: -16380px 0px; } -.emoji-1F533 { background-position: -16400px 0px; } -.emoji-1F534 { background-position: -16420px 0px; } -.emoji-1F535 { background-position: -16440px 0px; } -.emoji-1F536 { background-position: -16460px 0px; } -.emoji-1F537 { background-position: -16480px 0px; } -.emoji-1F538 { background-position: -16500px 0px; } -.emoji-1F539 { background-position: -16520px 0px; } -.emoji-1F53A { background-position: -16540px 0px; } -.emoji-1F53B { background-position: -16560px 0px; } -.emoji-1F53C { background-position: -16580px 0px; } -.emoji-1F53D { background-position: -16600px 0px; } -.emoji-1F546 { background-position: -16620px 0px; } -.emoji-1F547 { background-position: -16640px 0px; } -.emoji-1F548 { background-position: -16660px 0px; } -.emoji-1F549 { background-position: -16680px 0px; } -.emoji-1F54A { background-position: -16700px 0px; } -.emoji-1F550 { background-position: -16720px 0px; } -.emoji-1F551 { background-position: -16740px 0px; } -.emoji-1F552 { background-position: -16760px 0px; } -.emoji-1F553 { background-position: -16780px 0px; } -.emoji-1F554 { background-position: -16800px 0px; } -.emoji-1F555 { background-position: -16820px 0px; } -.emoji-1F556 { background-position: -16840px 0px; } -.emoji-1F557 { background-position: -16860px 0px; } -.emoji-1F558 { background-position: -16880px 0px; } -.emoji-1F559 { background-position: -16900px 0px; } -.emoji-1F55A { background-position: -16920px 0px; } -.emoji-1F55B { background-position: -16940px 0px; } -.emoji-1F55C { background-position: -16960px 0px; } -.emoji-1F55D { background-position: -16980px 0px; } -.emoji-1F55E { background-position: -17000px 0px; } -.emoji-1F55F { background-position: -17020px 0px; } -.emoji-1F560 { background-position: -17040px 0px; } -.emoji-1F561 { background-position: -17060px 0px; } -.emoji-1F562 { background-position: -17080px 0px; } -.emoji-1F563 { background-position: -17100px 0px; } -.emoji-1F564 { background-position: -17120px 0px; } -.emoji-1F565 { background-position: -17140px 0px; } -.emoji-1F566 { background-position: -17160px 0px; } -.emoji-1F567 { background-position: -17180px 0px; } -.emoji-1F568 { background-position: -17200px 0px; } -.emoji-1F569 { background-position: -17220px 0px; } -.emoji-1F56A { background-position: -17240px 0px; } -.emoji-1F56B { background-position: -17260px 0px; } -.emoji-1F56C { background-position: -17280px 0px; } -.emoji-1F56D { background-position: -17300px 0px; } -.emoji-1F56E { background-position: -17320px 0px; } -.emoji-1F56F { background-position: -17340px 0px; } -.emoji-1F570 { background-position: -17360px 0px; } -.emoji-1F571 { background-position: -17380px 0px; } -.emoji-1F572 { background-position: -17400px 0px; } -.emoji-1F573 { background-position: -17420px 0px; } -.emoji-1F574 { background-position: -17440px 0px; } -.emoji-1F575 { background-position: -17460px 0px; } -.emoji-1F576 { background-position: -17480px 0px; } -.emoji-1F577 { background-position: -17500px 0px; } -.emoji-1F578 { background-position: -17520px 0px; } -.emoji-1F579 { background-position: -17540px 0px; } -.emoji-1F57B { background-position: -17560px 0px; } -.emoji-1F57E { background-position: -17580px 0px; } -.emoji-1F57F { background-position: -17600px 0px; } -.emoji-1F581 { background-position: -17620px 0px; } -.emoji-1F582 { background-position: -17640px 0px; } -.emoji-1F583 { background-position: -17660px 0px; } -.emoji-1F585 { background-position: -17680px 0px; } -.emoji-1F586 { background-position: -17700px 0px; } -.emoji-1F587 { background-position: -17720px 0px; } -.emoji-1F588 { background-position: -17740px 0px; } -.emoji-1F589 { background-position: -17760px 0px; } -.emoji-1F58A { background-position: -17780px 0px; } -.emoji-1F58B { background-position: -17800px 0px; } -.emoji-1F58C { background-position: -17820px 0px; } -.emoji-1F58D { background-position: -17840px 0px; } -.emoji-1F58E { background-position: -17860px 0px; } -.emoji-1F58F { background-position: -17880px 0px; } -.emoji-1F590 { background-position: -17900px 0px; } -.emoji-1F591 { background-position: -17920px 0px; } -.emoji-1F592 { background-position: -17940px 0px; } -.emoji-1F593 { background-position: -17960px 0px; } -.emoji-1F594 { background-position: -17980px 0px; } -.emoji-1F595 { background-position: -18000px 0px; } -.emoji-1F596 { background-position: -18020px 0px; } -.emoji-1F597 { background-position: -18040px 0px; } -.emoji-1F598 { background-position: -18060px 0px; } -.emoji-1F599 { background-position: -18080px 0px; } -.emoji-1F59E { background-position: -18100px 0px; } -.emoji-1F59F { background-position: -18120px 0px; } -.emoji-1F5A5 { background-position: -18140px 0px; } -.emoji-1F5A6 { background-position: -18160px 0px; } -.emoji-1F5A7 { background-position: -18180px 0px; } -.emoji-1F5A8 { background-position: -18200px 0px; } -.emoji-1F5A9 { background-position: -18220px 0px; } -.emoji-1F5AA { background-position: -18240px 0px; } -.emoji-1F5AB { background-position: -18260px 0px; } -.emoji-1F5AD { background-position: -18280px 0px; } -.emoji-1F5AE { background-position: -18300px 0px; } -.emoji-1F5AF { background-position: -18320px 0px; } -.emoji-1F5B2 { background-position: -18340px 0px; } -.emoji-1F5B3 { background-position: -18360px 0px; } -.emoji-1F5B4 { background-position: -18380px 0px; } -.emoji-1F5B8 { background-position: -18400px 0px; } -.emoji-1F5B9 { background-position: -18420px 0px; } -.emoji-1F5BC { background-position: -18440px 0px; } -.emoji-1F5BD { background-position: -18460px 0px; } -.emoji-1F5BE { background-position: -18480px 0px; } -.emoji-1F5C0 { background-position: -18500px 0px; } -.emoji-1F5C1 { background-position: -18520px 0px; } -.emoji-1F5C2 { background-position: -18540px 0px; } -.emoji-1F5C3 { background-position: -18560px 0px; } -.emoji-1F5C4 { background-position: -18580px 0px; } -.emoji-1F5C6 { background-position: -18600px 0px; } -.emoji-1F5C7 { background-position: -18620px 0px; } -.emoji-1F5C9 { background-position: -18640px 0px; } -.emoji-1F5CA { background-position: -18660px 0px; } -.emoji-1F5CE { background-position: -18680px 0px; } -.emoji-1F5CF { background-position: -18700px 0px; } -.emoji-1F5D0 { background-position: -18720px 0px; } -.emoji-1F5D1 { background-position: -18740px 0px; } -.emoji-1F5D2 { background-position: -18760px 0px; } -.emoji-1F5D3 { background-position: -18780px 0px; } -.emoji-1F5D4 { background-position: -18800px 0px; } -.emoji-1F5D8 { background-position: -18820px 0px; } -.emoji-1F5D9 { background-position: -18840px 0px; } -.emoji-1F5DC { background-position: -18860px 0px; } -.emoji-1F5DD { background-position: -18880px 0px; } -.emoji-1F5DE { background-position: -18900px 0px; } -.emoji-1F5E0 { background-position: -18920px 0px; } -.emoji-1F5E1 { background-position: -18940px 0px; } -.emoji-1F5E2 { background-position: -18960px 0px; } -.emoji-1F5E3 { background-position: -18980px 0px; } -.emoji-1F5E8 { background-position: -19000px 0px; } -.emoji-1F5E9 { background-position: -19020px 0px; } -.emoji-1F5EA { background-position: -19040px 0px; } -.emoji-1F5EB { background-position: -19060px 0px; } -.emoji-1F5EC { background-position: -19080px 0px; } -.emoji-1F5ED { background-position: -19100px 0px; } -.emoji-1F5EE { background-position: -19120px 0px; } -.emoji-1F5EF { background-position: -19140px 0px; } -.emoji-1F5F0 { background-position: -19160px 0px; } -.emoji-1F5F1 { background-position: -19180px 0px; } -.emoji-1F5F2 { background-position: -19200px 0px; } -.emoji-1F5F3 { background-position: -19220px 0px; } -.emoji-1F5F4 { background-position: -19240px 0px; } -.emoji-1F5F5 { background-position: -19260px 0px; } -.emoji-1F5F8 { background-position: -19280px 0px; } -.emoji-1F5F9 { background-position: -19300px 0px; } -.emoji-1F5FA { background-position: -19320px 0px; } -.emoji-1F5FB { background-position: -19340px 0px; } -.emoji-1F5FC { background-position: -19360px 0px; } -.emoji-1F5FD { background-position: -19380px 0px; } -.emoji-1F5FE { background-position: -19400px 0px; } -.emoji-1F5FF { background-position: -19420px 0px; } -.emoji-1F600 { background-position: -19440px 0px; } -.emoji-1F601 { background-position: -19460px 0px; } -.emoji-1F602 { background-position: -19480px 0px; } -.emoji-1F603 { background-position: -19500px 0px; } -.emoji-1F604 { background-position: -19520px 0px; } -.emoji-1F605 { background-position: -19540px 0px; } -.emoji-1F606 { background-position: -19560px 0px; } -.emoji-1F607 { background-position: -19580px 0px; } -.emoji-1F608 { background-position: -19600px 0px; } -.emoji-1F609 { background-position: -19620px 0px; } -.emoji-1F60A { background-position: -19640px 0px; } -.emoji-1F60B { background-position: -19660px 0px; } -.emoji-1F60C { background-position: -19680px 0px; } -.emoji-1F60D { background-position: -19700px 0px; } -.emoji-1F60E { background-position: -19720px 0px; } -.emoji-1F60F { background-position: -19740px 0px; } -.emoji-1F610 { background-position: -19760px 0px; } -.emoji-1F611 { background-position: -19780px 0px; } -.emoji-1F612 { background-position: -19800px 0px; } -.emoji-1F613 { background-position: -19820px 0px; } -.emoji-1F614 { background-position: -19840px 0px; } -.emoji-1F615 { background-position: -19860px 0px; } -.emoji-1F616 { background-position: -19880px 0px; } -.emoji-1F617 { background-position: -19900px 0px; } -.emoji-1F618 { background-position: -19920px 0px; } -.emoji-1F619 { background-position: -19940px 0px; } -.emoji-1F61A { background-position: -19960px 0px; } -.emoji-1F61B { background-position: -19980px 0px; } -.emoji-1F61C { background-position: -20000px 0px; } -.emoji-1F61D { background-position: -20020px 0px; } -.emoji-1F61E { background-position: -20040px 0px; } -.emoji-1F61F { background-position: -20060px 0px; } -.emoji-1F620 { background-position: -20080px 0px; } -.emoji-1F621 { background-position: -20100px 0px; } -.emoji-1F622 { background-position: -20120px 0px; } -.emoji-1F623 { background-position: -20140px 0px; } -.emoji-1F624 { background-position: -20160px 0px; } -.emoji-1F625 { background-position: -20180px 0px; } -.emoji-1F626 { background-position: -20200px 0px; } -.emoji-1F627 { background-position: -20220px 0px; } -.emoji-1F628 { background-position: -20240px 0px; } -.emoji-1F629 { background-position: -20260px 0px; } -.emoji-1F62A { background-position: -20280px 0px; } -.emoji-1F62B { background-position: -20300px 0px; } -.emoji-1F62C { background-position: -20320px 0px; } -.emoji-1F62D { background-position: -20340px 0px; } -.emoji-1F62E { background-position: -20360px 0px; } -.emoji-1F62F { background-position: -20380px 0px; } -.emoji-1F630 { background-position: -20400px 0px; } -.emoji-1F631 { background-position: -20420px 0px; } -.emoji-1F632 { background-position: -20440px 0px; } -.emoji-1F633 { background-position: -20460px 0px; } -.emoji-1F634 { background-position: -20480px 0px; } -.emoji-1F635 { background-position: -20500px 0px; } -.emoji-1F636 { background-position: -20520px 0px; } -.emoji-1F637 { background-position: -20540px 0px; } -.emoji-1F638 { background-position: -20560px 0px; } -.emoji-1F639 { background-position: -20580px 0px; } -.emoji-1F63A { background-position: -20600px 0px; } -.emoji-1F63B { background-position: -20620px 0px; } -.emoji-1F63C { background-position: -20640px 0px; } -.emoji-1F63D { background-position: -20660px 0px; } -.emoji-1F63E { background-position: -20680px 0px; } -.emoji-1F63F { background-position: -20700px 0px; } -.emoji-1F640 { background-position: -20720px 0px; } -.emoji-1F641 { background-position: -20740px 0px; } -.emoji-1F642 { background-position: -20760px 0px; } -.emoji-1F645 { background-position: -20780px 0px; } -.emoji-1F646 { background-position: -20800px 0px; } -.emoji-1F647 { background-position: -20820px 0px; } -.emoji-1F648 { background-position: -20840px 0px; } -.emoji-1F649 { background-position: -20860px 0px; } -.emoji-1F64A { background-position: -20880px 0px; } -.emoji-1F64B { background-position: -20900px 0px; } -.emoji-1F64C { background-position: -20920px 0px; } -.emoji-1F64D { background-position: -20940px 0px; } -.emoji-1F64E { background-position: -20960px 0px; } -.emoji-1F64F { background-position: -20980px 0px; } -.emoji-1F680 { background-position: -21000px 0px; } -.emoji-1F681 { background-position: -21020px 0px; } -.emoji-1F682 { background-position: -21040px 0px; } -.emoji-1F683 { background-position: -21060px 0px; } -.emoji-1F684 { background-position: -21080px 0px; } -.emoji-1F685 { background-position: -21100px 0px; } -.emoji-1F686 { background-position: -21120px 0px; } -.emoji-1F687 { background-position: -21140px 0px; } -.emoji-1F688 { background-position: -21160px 0px; } -.emoji-1F689 { background-position: -21180px 0px; } -.emoji-1F68A { background-position: -21200px 0px; } -.emoji-1F68B { background-position: -21220px 0px; } -.emoji-1F68C { background-position: -21240px 0px; } -.emoji-1F68D { background-position: -21260px 0px; } -.emoji-1F68E { background-position: -21280px 0px; } -.emoji-1F68F { background-position: -21300px 0px; } -.emoji-1F690 { background-position: -21320px 0px; } -.emoji-1F691 { background-position: -21340px 0px; } -.emoji-1F692 { background-position: -21360px 0px; } -.emoji-1F693 { background-position: -21380px 0px; } -.emoji-1F694 { background-position: -21400px 0px; } -.emoji-1F695 { background-position: -21420px 0px; } -.emoji-1F696 { background-position: -21440px 0px; } -.emoji-1F697 { background-position: -21460px 0px; } -.emoji-1F698 { background-position: -21480px 0px; } -.emoji-1F699 { background-position: -21500px 0px; } -.emoji-1F69A { background-position: -21520px 0px; } -.emoji-1F69B { background-position: -21540px 0px; } -.emoji-1F69C { background-position: -21560px 0px; } -.emoji-1F69D { background-position: -21580px 0px; } -.emoji-1F69E { background-position: -21600px 0px; } -.emoji-1F69F { background-position: -21620px 0px; } -.emoji-1F6A0 { background-position: -21640px 0px; } -.emoji-1F6A1 { background-position: -21660px 0px; } -.emoji-1F6A2 { background-position: -21680px 0px; } -.emoji-1F6A3 { background-position: -21700px 0px; } -.emoji-1F6A4 { background-position: -21720px 0px; } -.emoji-1F6A5 { background-position: -21740px 0px; } -.emoji-1F6A6 { background-position: -21760px 0px; } -.emoji-1F6A7 { background-position: -21780px 0px; } -.emoji-1F6A8 { background-position: -21800px 0px; } -.emoji-1F6A9 { background-position: -21820px 0px; } -.emoji-1F6AA { background-position: -21840px 0px; } -.emoji-1F6AB { background-position: -21860px 0px; } -.emoji-1F6AC { background-position: -21880px 0px; } -.emoji-1F6AD { background-position: -21900px 0px; } -.emoji-1F6AE { background-position: -21920px 0px; } -.emoji-1F6AF { background-position: -21940px 0px; } -.emoji-1F6B0 { background-position: -21960px 0px; } -.emoji-1F6B1 { background-position: -21980px 0px; } -.emoji-1F6B2 { background-position: -22000px 0px; } -.emoji-1F6B3 { background-position: -22020px 0px; } -.emoji-1F6B4 { background-position: -22040px 0px; } -.emoji-1F6B5 { background-position: -22060px 0px; } -.emoji-1F6B6 { background-position: -22080px 0px; } -.emoji-1F6B7 { background-position: -22100px 0px; } -.emoji-1F6B8 { background-position: -22120px 0px; } -.emoji-1F6B9 { background-position: -22140px 0px; } -.emoji-1F6BA { background-position: -22160px 0px; } -.emoji-1F6BB { background-position: -22180px 0px; } -.emoji-1F6BC { background-position: -22200px 0px; } -.emoji-1F6BD { background-position: -22220px 0px; } -.emoji-1F6BE { background-position: -22240px 0px; } -.emoji-1F6BF { background-position: -22260px 0px; } -.emoji-1F6C0 { background-position: -22280px 0px; } -.emoji-1F6C1 { background-position: -22300px 0px; } -.emoji-1F6C2 { background-position: -22320px 0px; } -.emoji-1F6C3 { background-position: -22340px 0px; } -.emoji-1F6C4 { background-position: -22360px 0px; } -.emoji-1F6C5 { background-position: -22380px 0px; } -.emoji-1F6C6 { background-position: -22400px 0px; } -.emoji-1F6C7 { background-position: -22420px 0px; } -.emoji-1F6C8 { background-position: -22440px 0px; } -.emoji-1F6C9 { background-position: -22460px 0px; } -.emoji-1F6CA { background-position: -22480px 0px; } -.emoji-1F6CB { background-position: -22500px 0px; } -.emoji-1F6CC { background-position: -22520px 0px; } -.emoji-1F6CD { background-position: -22540px 0px; } -.emoji-1F6CE { background-position: -22560px 0px; } -.emoji-1F6CF { background-position: -22580px 0px; } -.emoji-1F6E0 { background-position: -22600px 0px; } -.emoji-1F6E1 { background-position: -22620px 0px; } -.emoji-1F6E2 { background-position: -22640px 0px; } -.emoji-1F6E3 { background-position: -22660px 0px; } -.emoji-1F6E4 { background-position: -22680px 0px; } -.emoji-1F6E5 { background-position: -22700px 0px; } -.emoji-1F6E6 { background-position: -22720px 0px; } -.emoji-1F6E7 { background-position: -22740px 0px; } -.emoji-1F6E8 { background-position: -22760px 0px; } -.emoji-1F6E9 { background-position: -22780px 0px; } -.emoji-1F6EA { background-position: -22800px 0px; } -.emoji-1F6EB { background-position: -22820px 0px; } -.emoji-1F6EC { background-position: -22840px 0px; } -.emoji-1F6F0 { background-position: -22860px 0px; } -.emoji-1F6F1 { background-position: -22880px 0px; } -.emoji-1F6F2 { background-position: -22900px 0px; } -.emoji-1F6F3 { background-position: -22920px 0px; } -.emoji-203C { background-position: -22940px 0px; } -.emoji-2049 { background-position: -22960px 0px; } -.emoji-2122 { background-position: -22980px 0px; } -.emoji-2139 { background-position: -23000px 0px; } -.emoji-2194 { background-position: -23020px 0px; } -.emoji-2195 { background-position: -23040px 0px; } -.emoji-2196 { background-position: -23060px 0px; } -.emoji-2197 { background-position: -23080px 0px; } -.emoji-2198 { background-position: -23100px 0px; } -.emoji-2199 { background-position: -23120px 0px; } -.emoji-21A9 { background-position: -23140px 0px; } -.emoji-21AA { background-position: -23160px 0px; } -.emoji-231A { background-position: -23180px 0px; } -.emoji-231B { background-position: -23200px 0px; } -.emoji-23E9 { background-position: -23220px 0px; } -.emoji-23EA { background-position: -23240px 0px; } -.emoji-23EB { background-position: -23260px 0px; } -.emoji-23EC { background-position: -23280px 0px; } -.emoji-23F0 { background-position: -23300px 0px; } -.emoji-23F3 { background-position: -23320px 0px; } -.emoji-24C2 { background-position: -23340px 0px; } -.emoji-25AA { background-position: -23360px 0px; } -.emoji-25AB { background-position: -23380px 0px; } -.emoji-25B6 { background-position: -23400px 0px; } -.emoji-25C0 { background-position: -23420px 0px; } -.emoji-25FB { background-position: -23440px 0px; } -.emoji-25FC { background-position: -23460px 0px; } -.emoji-25FD { background-position: -23480px 0px; } -.emoji-25FE { background-position: -23500px 0px; } -.emoji-2600 { background-position: -23520px 0px; } -.emoji-2601 { background-position: -23540px 0px; } -.emoji-260E { background-position: -23560px 0px; } -.emoji-2611 { background-position: -23580px 0px; } -.emoji-2614 { background-position: -23600px 0px; } -.emoji-2615 { background-position: -23620px 0px; } -.emoji-261D { background-position: -23640px 0px; } -.emoji-263A { background-position: -23660px 0px; } -.emoji-2648 { background-position: -23680px 0px; } -.emoji-2649 { background-position: -23700px 0px; } -.emoji-264A { background-position: -23720px 0px; } -.emoji-264B { background-position: -23740px 0px; } -.emoji-264C { background-position: -23760px 0px; } -.emoji-264D { background-position: -23780px 0px; } -.emoji-264E { background-position: -23800px 0px; } -.emoji-264F { background-position: -23820px 0px; } -.emoji-2650 { background-position: -23840px 0px; } -.emoji-2651 { background-position: -23860px 0px; } -.emoji-2652 { background-position: -23880px 0px; } -.emoji-2653 { background-position: -23900px 0px; } -.emoji-2660 { background-position: -23920px 0px; } -.emoji-2663 { background-position: -23940px 0px; } -.emoji-2665 { background-position: -23960px 0px; } -.emoji-2666 { background-position: -23980px 0px; } -.emoji-2668 { background-position: -24000px 0px; } -.emoji-267B { background-position: -24020px 0px; } -.emoji-267F { background-position: -24040px 0px; } -.emoji-2693 { background-position: -24060px 0px; } -.emoji-26A0 { background-position: -24080px 0px; } -.emoji-26A1 { background-position: -24100px 0px; } -.emoji-26AA { background-position: -24120px 0px; } -.emoji-26AB { background-position: -24140px 0px; } -.emoji-26BD { background-position: -24160px 0px; } -.emoji-26BE { background-position: -24180px 0px; } -.emoji-26C4 { background-position: -24200px 0px; } -.emoji-26C5 { background-position: -24220px 0px; } -.emoji-26CE { background-position: -24240px 0px; } -.emoji-26D4 { background-position: -24260px 0px; } -.emoji-26EA { background-position: -24280px 0px; } -.emoji-26F2 { background-position: -24300px 0px; } -.emoji-26F3 { background-position: -24320px 0px; } -.emoji-26F5 { background-position: -24340px 0px; } -.emoji-26FA { background-position: -24360px 0px; } -.emoji-26FD { background-position: -24380px 0px; } -.emoji-2702 { background-position: -24400px 0px; } -.emoji-2705 { background-position: -24420px 0px; } -.emoji-2708 { background-position: -24440px 0px; } -.emoji-2709 { background-position: -24460px 0px; } -.emoji-270A { background-position: -24480px 0px; } -.emoji-270B { background-position: -24500px 0px; } -.emoji-270C { background-position: -24520px 0px; } -.emoji-270F { background-position: -24540px 0px; } -.emoji-2712 { background-position: -24560px 0px; } -.emoji-2714 { background-position: -24580px 0px; } -.emoji-2716 { background-position: -24600px 0px; } -.emoji-2728 { background-position: -24620px 0px; } -.emoji-2733 { background-position: -24640px 0px; } -.emoji-2734 { background-position: -24660px 0px; } -.emoji-2744 { background-position: -24680px 0px; } -.emoji-2747 { background-position: -24700px 0px; } -.emoji-274C { background-position: -24720px 0px; } -.emoji-274E { background-position: -24740px 0px; } -.emoji-2753 { background-position: -24760px 0px; } -.emoji-2754 { background-position: -24780px 0px; } -.emoji-2755 { background-position: -24800px 0px; } -.emoji-2757 { background-position: -24820px 0px; } -.emoji-2764 { background-position: -24840px 0px; } -.emoji-2795 { background-position: -24860px 0px; } -.emoji-2796 { background-position: -24880px 0px; } -.emoji-2797 { background-position: -24900px 0px; } -.emoji-27A1 { background-position: -24920px 0px; } -.emoji-27B0 { background-position: -24940px 0px; } -.emoji-27BF { background-position: -24960px 0px; } -.emoji-2934 { background-position: -24980px 0px; } -.emoji-2935 { background-position: -25000px 0px; } -.emoji-2B05 { background-position: -25020px 0px; } -.emoji-2B06 { background-position: -25040px 0px; } -.emoji-2B07 { background-position: -25060px 0px; } -.emoji-2B1B { background-position: -25080px 0px; } -.emoji-2B1C { background-position: -25100px 0px; } -.emoji-2B50 { background-position: -25120px 0px; } -.emoji-2B55 { background-position: -25140px 0px; } -.emoji-3030 { background-position: -25160px 0px; } -.emoji-303D { background-position: -25180px 0px; } -.emoji-3297 { background-position: -25200px 0px; } -.emoji-3299 { background-position: -25220px 0px; } \ No newline at end of file +.emoji-002A-20E3 { background-position: -20px 0px; } +.emoji-0030-20E3 { background-position: 0px -20px; } +.emoji-0031-20E3 { background-position: -20px -20px; } +.emoji-0032-20E3 { background-position: -40px 0px; } +.emoji-0033-20E3 { background-position: -40px -20px; } +.emoji-0034-20E3 { background-position: 0px -40px; } +.emoji-0035-20E3 { background-position: -20px -40px; } +.emoji-0036-20E3 { background-position: -40px -40px; } +.emoji-0037-20E3 { background-position: -60px 0px; } +.emoji-0038-20E3 { background-position: -60px -20px; } +.emoji-0039-20E3 { background-position: -60px -40px; } +.emoji-00A9 { background-position: 0px -60px; } +.emoji-00AE { background-position: -20px -60px; } +.emoji-1F004 { background-position: -40px -60px; } +.emoji-1F0CF { background-position: -60px -60px; } +.emoji-1F170 { background-position: -80px 0px; } +.emoji-1F171 { background-position: -80px -20px; } +.emoji-1F17E { background-position: -80px -40px; } +.emoji-1F17F { background-position: -80px -60px; } +.emoji-1F18E { background-position: 0px -80px; } +.emoji-1F191 { background-position: -20px -80px; } +.emoji-1F192 { background-position: -40px -80px; } +.emoji-1F193 { background-position: -60px -80px; } +.emoji-1F194 { background-position: -80px -80px; } +.emoji-1F195 { background-position: -100px 0px; } +.emoji-1F196 { background-position: -100px -20px; } +.emoji-1F197 { background-position: -100px -40px; } +.emoji-1F198 { background-position: -100px -60px; } +.emoji-1F199 { background-position: -100px -80px; } +.emoji-1F19A { background-position: 0px -100px; } +.emoji-1F1E6-1F1E8 { background-position: -20px -100px; } +.emoji-1F1E6-1F1E9 { background-position: -40px -100px; } +.emoji-1F1E6-1F1EA { background-position: -60px -100px; } +.emoji-1F1E6-1F1EB { background-position: -80px -100px; } +.emoji-1F1E6-1F1EC { background-position: -100px -100px; } +.emoji-1F1E6-1F1EE { background-position: -120px 0px; } +.emoji-1F1E6-1F1F1 { background-position: -120px -20px; } +.emoji-1F1E6-1F1F2 { background-position: -120px -40px; } +.emoji-1F1E6-1F1F4 { background-position: -120px -60px; } +.emoji-1F1E6-1F1F6 { background-position: -120px -80px; } +.emoji-1F1E6-1F1F7 { background-position: -120px -100px; } +.emoji-1F1E6-1F1F8 { background-position: 0px -120px; } +.emoji-1F1E6-1F1F9 { background-position: -20px -120px; } +.emoji-1F1E6-1F1FA { background-position: -40px -120px; } +.emoji-1F1E6-1F1FC { background-position: -60px -120px; } +.emoji-1F1E6-1F1FD { background-position: -80px -120px; } +.emoji-1F1E6-1F1FF { background-position: -100px -120px; } +.emoji-1F1E7-1F1E6 { background-position: -120px -120px; } +.emoji-1F1E7-1F1E7 { background-position: -140px 0px; } +.emoji-1F1E7-1F1E9 { background-position: -140px -20px; } +.emoji-1F1E7-1F1EA { background-position: -140px -40px; } +.emoji-1F1E7-1F1EB { background-position: -140px -60px; } +.emoji-1F1E7-1F1EC { background-position: -140px -80px; } +.emoji-1F1E7-1F1ED { background-position: -140px -100px; } +.emoji-1F1E7-1F1EE { background-position: -140px -120px; } +.emoji-1F1E7-1F1EF { background-position: 0px -140px; } +.emoji-1F1E7-1F1F1 { background-position: -20px -140px; } +.emoji-1F1E7-1F1F2 { background-position: -40px -140px; } +.emoji-1F1E7-1F1F3 { background-position: -60px -140px; } +.emoji-1F1E7-1F1F4 { background-position: -80px -140px; } +.emoji-1F1E7-1F1F6 { background-position: -100px -140px; } +.emoji-1F1E7-1F1F7 { background-position: -120px -140px; } +.emoji-1F1E7-1F1F8 { background-position: -140px -140px; } +.emoji-1F1E7-1F1F9 { background-position: -160px 0px; } +.emoji-1F1E7-1F1FB { background-position: -160px -20px; } +.emoji-1F1E7-1F1FC { background-position: -160px -40px; } +.emoji-1F1E7-1F1FE { background-position: -160px -60px; } +.emoji-1F1E7-1F1FF { background-position: -160px -80px; } +.emoji-1F1E8-1F1E6 { background-position: -160px -100px; } +.emoji-1F1E8-1F1E8 { background-position: -160px -120px; } +.emoji-1F1E8-1F1E9 { background-position: -160px -140px; } +.emoji-1F1E8-1F1EB { background-position: 0px -160px; } +.emoji-1F1E8-1F1EC { background-position: -20px -160px; } +.emoji-1F1E8-1F1ED { background-position: -40px -160px; } +.emoji-1F1E8-1F1EE { background-position: -60px -160px; } +.emoji-1F1E8-1F1F0 { background-position: -80px -160px; } +.emoji-1F1E8-1F1F1 { background-position: -100px -160px; } +.emoji-1F1E8-1F1F2 { background-position: -120px -160px; } +.emoji-1F1E8-1F1F3 { background-position: -140px -160px; } +.emoji-1F1E8-1F1F4 { background-position: -160px -160px; } +.emoji-1F1E8-1F1F5 { background-position: -180px 0px; } +.emoji-1F1E8-1F1F7 { background-position: -180px -20px; } +.emoji-1F1E8-1F1FA { background-position: -180px -40px; } +.emoji-1F1E8-1F1FB { background-position: -180px -60px; } +.emoji-1F1E8-1F1FC { background-position: -180px -80px; } +.emoji-1F1E8-1F1FD { background-position: -180px -100px; } +.emoji-1F1E8-1F1FE { background-position: -180px -120px; } +.emoji-1F1E8-1F1FF { background-position: -180px -140px; } +.emoji-1F1E9-1F1EA { background-position: -180px -160px; } +.emoji-1F1E9-1F1EC { background-position: 0px -180px; } +.emoji-1F1E9-1F1EF { background-position: -20px -180px; } +.emoji-1F1E9-1F1F0 { background-position: -40px -180px; } +.emoji-1F1E9-1F1F2 { background-position: -60px -180px; } +.emoji-1F1E9-1F1F4 { background-position: -80px -180px; } +.emoji-1F1E9-1F1FF { background-position: -100px -180px; } +.emoji-1F1EA-1F1E6 { background-position: -120px -180px; } +.emoji-1F1EA-1F1E8 { background-position: -140px -180px; } +.emoji-1F1EA-1F1EA { background-position: -160px -180px; } +.emoji-1F1EA-1F1EC { background-position: -180px -180px; } +.emoji-1F1EA-1F1ED { background-position: -200px 0px; } +.emoji-1F1EA-1F1F7 { background-position: -200px -20px; } +.emoji-1F1EA-1F1F8 { background-position: -200px -40px; } +.emoji-1F1EA-1F1F9 { background-position: -200px -60px; } +.emoji-1F1EA-1F1FA { background-position: -200px -80px; } +.emoji-1F1EB-1F1EE { background-position: -200px -100px; } +.emoji-1F1EB-1F1EF { background-position: -200px -120px; } +.emoji-1F1EB-1F1F0 { background-position: -200px -140px; } +.emoji-1F1EB-1F1F2 { background-position: -200px -160px; } +.emoji-1F1EB-1F1F4 { background-position: -200px -180px; } +.emoji-1F1EB-1F1F7 { background-position: 0px -200px; } +.emoji-1F1EC-1F1E6 { background-position: -20px -200px; } +.emoji-1F1EC-1F1E7 { background-position: -40px -200px; } +.emoji-1F1EC-1F1E9 { background-position: -60px -200px; } +.emoji-1F1EC-1F1EA { background-position: -80px -200px; } +.emoji-1F1EC-1F1EB { background-position: -100px -200px; } +.emoji-1F1EC-1F1EC { background-position: -120px -200px; } +.emoji-1F1EC-1F1ED { background-position: -140px -200px; } +.emoji-1F1EC-1F1EE { background-position: -160px -200px; } +.emoji-1F1EC-1F1F1 { background-position: -180px -200px; } +.emoji-1F1EC-1F1F2 { background-position: -200px -200px; } +.emoji-1F1EC-1F1F3 { background-position: -220px 0px; } +.emoji-1F1EC-1F1F5 { background-position: -220px -20px; } +.emoji-1F1EC-1F1F6 { background-position: -220px -40px; } +.emoji-1F1EC-1F1F7 { background-position: -220px -60px; } +.emoji-1F1EC-1F1F8 { background-position: -220px -80px; } +.emoji-1F1EC-1F1F9 { background-position: -220px -100px; } +.emoji-1F1EC-1F1FA { background-position: -220px -120px; } +.emoji-1F1EC-1F1FC { background-position: -220px -140px; } +.emoji-1F1EC-1F1FE { background-position: -220px -160px; } +.emoji-1F1ED-1F1F0 { background-position: -220px -180px; } +.emoji-1F1ED-1F1F2 { background-position: -220px -200px; } +.emoji-1F1ED-1F1F3 { background-position: 0px -220px; } +.emoji-1F1ED-1F1F7 { background-position: -20px -220px; } +.emoji-1F1ED-1F1F9 { background-position: -40px -220px; } +.emoji-1F1ED-1F1FA { background-position: -60px -220px; } +.emoji-1F1EE-1F1E8 { background-position: -80px -220px; } +.emoji-1F1EE-1F1E9 { background-position: -100px -220px; } +.emoji-1F1EE-1F1EA { background-position: -120px -220px; } +.emoji-1F1EE-1F1F1 { background-position: -140px -220px; } +.emoji-1F1EE-1F1F2 { background-position: -160px -220px; } +.emoji-1F1EE-1F1F3 { background-position: -180px -220px; } +.emoji-1F1EE-1F1F4 { background-position: -200px -220px; } +.emoji-1F1EE-1F1F6 { background-position: -220px -220px; } +.emoji-1F1EE-1F1F7 { background-position: -240px 0px; } +.emoji-1F1EE-1F1F8 { background-position: -240px -20px; } +.emoji-1F1EE-1F1F9 { background-position: -240px -40px; } +.emoji-1F1EF-1F1EA { background-position: -240px -60px; } +.emoji-1F1EF-1F1F2 { background-position: -240px -80px; } +.emoji-1F1EF-1F1F4 { background-position: -240px -100px; } +.emoji-1F1EF-1F1F5 { background-position: -240px -120px; } +.emoji-1F1F0-1F1EA { background-position: -240px -140px; } +.emoji-1F1F0-1F1EC { background-position: -240px -160px; } +.emoji-1F1F0-1F1ED { background-position: -240px -180px; } +.emoji-1F1F0-1F1EE { background-position: -240px -200px; } +.emoji-1F1F0-1F1F2 { background-position: -240px -220px; } +.emoji-1F1F0-1F1F3 { background-position: 0px -240px; } +.emoji-1F1F0-1F1F5 { background-position: -20px -240px; } +.emoji-1F1F0-1F1F7 { background-position: -40px -240px; } +.emoji-1F1F0-1F1FC { background-position: -60px -240px; } +.emoji-1F1F0-1F1FE { background-position: -80px -240px; } +.emoji-1F1F0-1F1FF { background-position: -100px -240px; } +.emoji-1F1F1-1F1E6 { background-position: -120px -240px; } +.emoji-1F1F1-1F1E7 { background-position: -140px -240px; } +.emoji-1F1F1-1F1E8 { background-position: -160px -240px; } +.emoji-1F1F1-1F1EE { background-position: -180px -240px; } +.emoji-1F1F1-1F1F0 { background-position: -200px -240px; } +.emoji-1F1F1-1F1F7 { background-position: -220px -240px; } +.emoji-1F1F1-1F1F8 { background-position: -240px -240px; } +.emoji-1F1F1-1F1F9 { background-position: -260px 0px; } +.emoji-1F1F1-1F1FA { background-position: -260px -20px; } +.emoji-1F1F1-1F1FB { background-position: -260px -40px; } +.emoji-1F1F1-1F1FE { background-position: -260px -60px; } +.emoji-1F1F2-1F1E6 { background-position: -260px -80px; } +.emoji-1F1F2-1F1E8 { background-position: -260px -100px; } +.emoji-1F1F2-1F1E9 { background-position: -260px -120px; } +.emoji-1F1F2-1F1EA { background-position: -260px -140px; } +.emoji-1F1F2-1F1EB { background-position: -260px -160px; } +.emoji-1F1F2-1F1EC { background-position: -260px -180px; } +.emoji-1F1F2-1F1ED { background-position: -260px -200px; } +.emoji-1F1F2-1F1F0 { background-position: -260px -220px; } +.emoji-1F1F2-1F1F1 { background-position: -260px -240px; } +.emoji-1F1F2-1F1F2 { background-position: 0px -260px; } +.emoji-1F1F2-1F1F3 { background-position: -20px -260px; } +.emoji-1F1F2-1F1F4 { background-position: -40px -260px; } +.emoji-1F1F2-1F1F5 { background-position: -60px -260px; } +.emoji-1F1F2-1F1F6 { background-position: -80px -260px; } +.emoji-1F1F2-1F1F7 { background-position: -100px -260px; } +.emoji-1F1F2-1F1F8 { background-position: -120px -260px; } +.emoji-1F1F2-1F1F9 { background-position: -140px -260px; } +.emoji-1F1F2-1F1FA { background-position: -160px -260px; } +.emoji-1F1F2-1F1FB { background-position: -180px -260px; } +.emoji-1F1F2-1F1FC { background-position: -200px -260px; } +.emoji-1F1F2-1F1FD { background-position: -220px -260px; } +.emoji-1F1F2-1F1FE { background-position: -240px -260px; } +.emoji-1F1F2-1F1FF { background-position: -260px -260px; } +.emoji-1F1F3-1F1E6 { background-position: -280px 0px; } +.emoji-1F1F3-1F1E8 { background-position: -280px -20px; } +.emoji-1F1F3-1F1EA { background-position: -280px -40px; } +.emoji-1F1F3-1F1EB { background-position: -280px -60px; } +.emoji-1F1F3-1F1EC { background-position: -280px -80px; } +.emoji-1F1F3-1F1EE { background-position: -280px -100px; } +.emoji-1F1F3-1F1F1 { background-position: -280px -120px; } +.emoji-1F1F3-1F1F4 { background-position: -280px -140px; } +.emoji-1F1F3-1F1F5 { background-position: -280px -160px; } +.emoji-1F1F3-1F1F7 { background-position: -280px -180px; } +.emoji-1F1F3-1F1FA { background-position: -280px -200px; } +.emoji-1F1F3-1F1FF { background-position: -280px -220px; } +.emoji-1F1F4-1F1F2 { background-position: -280px -240px; } +.emoji-1F1F5-1F1E6 { background-position: -280px -260px; } +.emoji-1F1F5-1F1EA { background-position: 0px -280px; } +.emoji-1F1F5-1F1EB { background-position: -20px -280px; } +.emoji-1F1F5-1F1EC { background-position: -40px -280px; } +.emoji-1F1F5-1F1ED { background-position: -60px -280px; } +.emoji-1F1F5-1F1F0 { background-position: -80px -280px; } +.emoji-1F1F5-1F1F1 { background-position: -100px -280px; } +.emoji-1F1F5-1F1F2 { background-position: -120px -280px; } +.emoji-1F1F5-1F1F3 { background-position: -140px -280px; } +.emoji-1F1F5-1F1F7 { background-position: -160px -280px; } +.emoji-1F1F5-1F1F8 { background-position: -180px -280px; } +.emoji-1F1F5-1F1F9 { background-position: -200px -280px; } +.emoji-1F1F5-1F1FC { background-position: -220px -280px; } +.emoji-1F1F5-1F1FE { background-position: -240px -280px; } +.emoji-1F1F6-1F1E6 { background-position: -260px -280px; } +.emoji-1F1F7-1F1EA { background-position: -280px -280px; } +.emoji-1F1F7-1F1F4 { background-position: -300px 0px; } +.emoji-1F1F7-1F1F8 { background-position: -300px -20px; } +.emoji-1F1F7-1F1FA { background-position: -300px -40px; } +.emoji-1F1F7-1F1FC { background-position: -300px -60px; } +.emoji-1F1F8-1F1E6 { background-position: -300px -80px; } +.emoji-1F1F8-1F1E7 { background-position: -300px -100px; } +.emoji-1F1F8-1F1E8 { background-position: -300px -120px; } +.emoji-1F1F8-1F1E9 { background-position: -300px -140px; } +.emoji-1F1F8-1F1EA { background-position: -300px -160px; } +.emoji-1F1F8-1F1EC { background-position: -300px -180px; } +.emoji-1F1F8-1F1ED { background-position: -300px -200px; } +.emoji-1F1F8-1F1EE { background-position: -300px -220px; } +.emoji-1F1F8-1F1EF { background-position: -300px -240px; } +.emoji-1F1F8-1F1F0 { background-position: -300px -260px; } +.emoji-1F1F8-1F1F1 { background-position: -300px -280px; } +.emoji-1F1F8-1F1F2 { background-position: 0px -300px; } +.emoji-1F1F8-1F1F3 { background-position: -20px -300px; } +.emoji-1F1F8-1F1F4 { background-position: -40px -300px; } +.emoji-1F1F8-1F1F7 { background-position: -60px -300px; } +.emoji-1F1F8-1F1F8 { background-position: -80px -300px; } +.emoji-1F1F8-1F1F9 { background-position: -100px -300px; } +.emoji-1F1F8-1F1FB { background-position: -120px -300px; } +.emoji-1F1F8-1F1FD { background-position: -140px -300px; } +.emoji-1F1F8-1F1FE { background-position: -160px -300px; } +.emoji-1F1F8-1F1FF { background-position: -180px -300px; } +.emoji-1F1F9-1F1E6 { background-position: -200px -300px; } +.emoji-1F1F9-1F1E8 { background-position: -220px -300px; } +.emoji-1F1F9-1F1E9 { background-position: -240px -300px; } +.emoji-1F1F9-1F1EB { background-position: -260px -300px; } +.emoji-1F1F9-1F1EC { background-position: -280px -300px; } +.emoji-1F1F9-1F1ED { background-position: -300px -300px; } +.emoji-1F1F9-1F1EF { background-position: -320px 0px; } +.emoji-1F1F9-1F1F0 { background-position: -320px -20px; } +.emoji-1F1F9-1F1F1 { background-position: -320px -40px; } +.emoji-1F1F9-1F1F2 { background-position: -320px -60px; } +.emoji-1F1F9-1F1F3 { background-position: -320px -80px; } +.emoji-1F1F9-1F1F4 { background-position: -320px -100px; } +.emoji-1F1F9-1F1F7 { background-position: -320px -120px; } +.emoji-1F1F9-1F1F9 { background-position: -320px -140px; } +.emoji-1F1F9-1F1FB { background-position: -320px -160px; } +.emoji-1F1F9-1F1FC { background-position: -320px -180px; } +.emoji-1F1F9-1F1FF { background-position: -320px -200px; } +.emoji-1F1FA-1F1E6 { background-position: -320px -220px; } +.emoji-1F1FA-1F1EC { background-position: -320px -240px; } +.emoji-1F1FA-1F1F2 { background-position: -320px -260px; } +.emoji-1F1FA-1F1F8 { background-position: -320px -280px; } +.emoji-1F1FA-1F1FE { background-position: -320px -300px; } +.emoji-1F1FA-1F1FF { background-position: 0px -320px; } +.emoji-1F1FB-1F1E6 { background-position: -20px -320px; } +.emoji-1F1FB-1F1E8 { background-position: -40px -320px; } +.emoji-1F1FB-1F1EA { background-position: -60px -320px; } +.emoji-1F1FB-1F1EC { background-position: -80px -320px; } +.emoji-1F1FB-1F1EE { background-position: -100px -320px; } +.emoji-1F1FB-1F1F3 { background-position: -120px -320px; } +.emoji-1F1FB-1F1FA { background-position: -140px -320px; } +.emoji-1F1FC-1F1EB { background-position: -160px -320px; } +.emoji-1F1FC-1F1F8 { background-position: -180px -320px; } +.emoji-1F1FD-1F1F0 { background-position: -200px -320px; } +.emoji-1F1FE-1F1EA { background-position: -220px -320px; } +.emoji-1F1FE-1F1F9 { background-position: -240px -320px; } +.emoji-1F1FF-1F1E6 { background-position: -260px -320px; } +.emoji-1F1FF-1F1F2 { background-position: -280px -320px; } +.emoji-1F1FF-1F1FC { background-position: -300px -320px; } +.emoji-1F201 { background-position: -320px -320px; } +.emoji-1F202 { background-position: -340px 0px; } +.emoji-1F21A { background-position: -340px -20px; } +.emoji-1F22F { background-position: -340px -40px; } +.emoji-1F232 { background-position: -340px -60px; } +.emoji-1F233 { background-position: -340px -80px; } +.emoji-1F234 { background-position: -340px -100px; } +.emoji-1F235 { background-position: -340px -120px; } +.emoji-1F236 { background-position: -340px -140px; } +.emoji-1F237 { background-position: -340px -160px; } +.emoji-1F238 { background-position: -340px -180px; } +.emoji-1F239 { background-position: -340px -200px; } +.emoji-1F23A { background-position: -340px -220px; } +.emoji-1F250 { background-position: -340px -240px; } +.emoji-1F251 { background-position: -340px -260px; } +.emoji-1F300 { background-position: -340px -280px; } +.emoji-1F301 { background-position: -340px -300px; } +.emoji-1F302 { background-position: -340px -320px; } +.emoji-1F303 { background-position: 0px -340px; } +.emoji-1F304 { background-position: -20px -340px; } +.emoji-1F305 { background-position: -40px -340px; } +.emoji-1F306 { background-position: -60px -340px; } +.emoji-1F307 { background-position: -80px -340px; } +.emoji-1F308 { background-position: -100px -340px; } +.emoji-1F309 { background-position: -120px -340px; } +.emoji-1F30A { background-position: -140px -340px; } +.emoji-1F30B { background-position: -160px -340px; } +.emoji-1F30C { background-position: -180px -340px; } +.emoji-1F30D { background-position: -200px -340px; } +.emoji-1F30E { background-position: -220px -340px; } +.emoji-1F30F { background-position: -240px -340px; } +.emoji-1F310 { background-position: -260px -340px; } +.emoji-1F311 { background-position: -280px -340px; } +.emoji-1F312 { background-position: -300px -340px; } +.emoji-1F313 { background-position: -320px -340px; } +.emoji-1F314 { background-position: -340px -340px; } +.emoji-1F315 { background-position: -360px 0px; } +.emoji-1F316 { background-position: -360px -20px; } +.emoji-1F317 { background-position: -360px -40px; } +.emoji-1F318 { background-position: -360px -60px; } +.emoji-1F319 { background-position: -360px -80px; } +.emoji-1F31A { background-position: -360px -100px; } +.emoji-1F31B { background-position: -360px -120px; } +.emoji-1F31C { background-position: -360px -140px; } +.emoji-1F31D { background-position: -360px -160px; } +.emoji-1F31E { background-position: -360px -180px; } +.emoji-1F31F { background-position: -360px -200px; } +.emoji-1F320 { background-position: -360px -220px; } +.emoji-1F321 { background-position: -360px -240px; } +.emoji-1F324 { background-position: -360px -260px; } +.emoji-1F325 { background-position: -360px -280px; } +.emoji-1F326 { background-position: -360px -300px; } +.emoji-1F327 { background-position: -360px -320px; } +.emoji-1F328 { background-position: -360px -340px; } +.emoji-1F329 { background-position: 0px -360px; } +.emoji-1F32A { background-position: -20px -360px; } +.emoji-1F32B { background-position: -40px -360px; } +.emoji-1F32C { background-position: -60px -360px; } +.emoji-1F32D { background-position: -80px -360px; } +.emoji-1F32E { background-position: -100px -360px; } +.emoji-1F32F { background-position: -120px -360px; } +.emoji-1F330 { background-position: -140px -360px; } +.emoji-1F331 { background-position: -160px -360px; } +.emoji-1F332 { background-position: -180px -360px; } +.emoji-1F333 { background-position: -200px -360px; } +.emoji-1F334 { background-position: -220px -360px; } +.emoji-1F335 { background-position: -240px -360px; } +.emoji-1F336 { background-position: -260px -360px; } +.emoji-1F337 { background-position: -280px -360px; } +.emoji-1F338 { background-position: -300px -360px; } +.emoji-1F339 { background-position: -320px -360px; } +.emoji-1F33A { background-position: -340px -360px; } +.emoji-1F33B { background-position: -360px -360px; } +.emoji-1F33C { background-position: -380px 0px; } +.emoji-1F33D { background-position: -380px -20px; } +.emoji-1F33E { background-position: -380px -40px; } +.emoji-1F33F { background-position: -380px -60px; } +.emoji-1F340 { background-position: -380px -80px; } +.emoji-1F341 { background-position: -380px -100px; } +.emoji-1F342 { background-position: -380px -120px; } +.emoji-1F343 { background-position: -380px -140px; } +.emoji-1F344 { background-position: -380px -160px; } +.emoji-1F345 { background-position: -380px -180px; } +.emoji-1F346 { background-position: -380px -200px; } +.emoji-1F347 { background-position: -380px -220px; } +.emoji-1F348 { background-position: -380px -240px; } +.emoji-1F349 { background-position: -380px -260px; } +.emoji-1F34A { background-position: -380px -280px; } +.emoji-1F34B { background-position: -380px -300px; } +.emoji-1F34C { background-position: -380px -320px; } +.emoji-1F34D { background-position: -380px -340px; } +.emoji-1F34E { background-position: -380px -360px; } +.emoji-1F34F { background-position: 0px -380px; } +.emoji-1F350 { background-position: -20px -380px; } +.emoji-1F351 { background-position: -40px -380px; } +.emoji-1F352 { background-position: -60px -380px; } +.emoji-1F353 { background-position: -80px -380px; } +.emoji-1F354 { background-position: -100px -380px; } +.emoji-1F355 { background-position: -120px -380px; } +.emoji-1F356 { background-position: -140px -380px; } +.emoji-1F357 { background-position: -160px -380px; } +.emoji-1F358 { background-position: -180px -380px; } +.emoji-1F359 { background-position: -200px -380px; } +.emoji-1F35A { background-position: -220px -380px; } +.emoji-1F35B { background-position: -240px -380px; } +.emoji-1F35C { background-position: -260px -380px; } +.emoji-1F35D { background-position: -280px -380px; } +.emoji-1F35E { background-position: -300px -380px; } +.emoji-1F35F { background-position: -320px -380px; } +.emoji-1F360 { background-position: -340px -380px; } +.emoji-1F361 { background-position: -360px -380px; } +.emoji-1F362 { background-position: -380px -380px; } +.emoji-1F363 { background-position: -400px 0px; } +.emoji-1F364 { background-position: -400px -20px; } +.emoji-1F365 { background-position: -400px -40px; } +.emoji-1F366 { background-position: -400px -60px; } +.emoji-1F367 { background-position: -400px -80px; } +.emoji-1F368 { background-position: -400px -100px; } +.emoji-1F369 { background-position: -400px -120px; } +.emoji-1F36A { background-position: -400px -140px; } +.emoji-1F36B { background-position: -400px -160px; } +.emoji-1F36C { background-position: -400px -180px; } +.emoji-1F36D { background-position: -400px -200px; } +.emoji-1F36E { background-position: -400px -220px; } +.emoji-1F36F { background-position: -400px -240px; } +.emoji-1F370 { background-position: -400px -260px; } +.emoji-1F371 { background-position: -400px -280px; } +.emoji-1F372 { background-position: -400px -300px; } +.emoji-1F373 { background-position: -400px -320px; } +.emoji-1F374 { background-position: -400px -340px; } +.emoji-1F375 { background-position: -400px -360px; } +.emoji-1F376 { background-position: -400px -380px; } +.emoji-1F377 { background-position: 0px -400px; } +.emoji-1F378 { background-position: -20px -400px; } +.emoji-1F379 { background-position: -40px -400px; } +.emoji-1F37A { background-position: -60px -400px; } +.emoji-1F37B { background-position: -80px -400px; } +.emoji-1F37C { background-position: -100px -400px; } +.emoji-1F37D { background-position: -120px -400px; } +.emoji-1F37E { background-position: -140px -400px; } +.emoji-1F37F { background-position: -160px -400px; } +.emoji-1F380 { background-position: -180px -400px; } +.emoji-1F381 { background-position: -200px -400px; } +.emoji-1F382 { background-position: -220px -400px; } +.emoji-1F383 { background-position: -240px -400px; } +.emoji-1F384 { background-position: -260px -400px; } +.emoji-1F385 { background-position: -280px -400px; } +.emoji-1F385-1F3FB { background-position: -300px -400px; } +.emoji-1F385-1F3FC { background-position: -320px -400px; } +.emoji-1F385-1F3FD { background-position: -340px -400px; } +.emoji-1F385-1F3FE { background-position: -360px -400px; } +.emoji-1F385-1F3FF { background-position: -380px -400px; } +.emoji-1F386 { background-position: -400px -400px; } +.emoji-1F387 { background-position: -420px 0px; } +.emoji-1F388 { background-position: -420px -20px; } +.emoji-1F389 { background-position: -420px -40px; } +.emoji-1F38A { background-position: -420px -60px; } +.emoji-1F38B { background-position: -420px -80px; } +.emoji-1F38C { background-position: -420px -100px; } +.emoji-1F38D { background-position: -420px -120px; } +.emoji-1F38E { background-position: -420px -140px; } +.emoji-1F38F { background-position: -420px -160px; } +.emoji-1F390 { background-position: -420px -180px; } +.emoji-1F391 { background-position: -420px -200px; } +.emoji-1F392 { background-position: -420px -220px; } +.emoji-1F393 { background-position: -420px -240px; } +.emoji-1F394 { background-position: -420px -260px; } +.emoji-1F395 { background-position: -420px -280px; } +.emoji-1F396 { background-position: -420px -300px; } +.emoji-1F397 { background-position: -420px -320px; } +.emoji-1F398 { background-position: -420px -340px; } +.emoji-1F399 { background-position: -420px -360px; } +.emoji-1F39A { background-position: -420px -380px; } +.emoji-1F39B { background-position: -420px -400px; } +.emoji-1F39C { background-position: 0px -420px; } +.emoji-1F39D { background-position: -20px -420px; } +.emoji-1F39E { background-position: -40px -420px; } +.emoji-1F39F { background-position: -60px -420px; } +.emoji-1F3A0 { background-position: -80px -420px; } +.emoji-1F3A1 { background-position: -100px -420px; } +.emoji-1F3A2 { background-position: -120px -420px; } +.emoji-1F3A3 { background-position: -140px -420px; } +.emoji-1F3A4 { background-position: -160px -420px; } +.emoji-1F3A5 { background-position: -180px -420px; } +.emoji-1F3A6 { background-position: -200px -420px; } +.emoji-1F3A7 { background-position: -220px -420px; } +.emoji-1F3A8 { background-position: -240px -420px; } +.emoji-1F3A9 { background-position: -260px -420px; } +.emoji-1F3AA { background-position: -280px -420px; } +.emoji-1F3AB { background-position: -300px -420px; } +.emoji-1F3AC { background-position: -320px -420px; } +.emoji-1F3AD { background-position: -340px -420px; } +.emoji-1F3AE { background-position: -360px -420px; } +.emoji-1F3AF { background-position: -380px -420px; } +.emoji-1F3B0 { background-position: -400px -420px; } +.emoji-1F3B1 { background-position: -420px -420px; } +.emoji-1F3B2 { background-position: -440px 0px; } +.emoji-1F3B3 { background-position: -440px -20px; } +.emoji-1F3B4 { background-position: -440px -40px; } +.emoji-1F3B5 { background-position: -440px -60px; } +.emoji-1F3B6 { background-position: -440px -80px; } +.emoji-1F3B7 { background-position: -440px -100px; } +.emoji-1F3B8 { background-position: -440px -120px; } +.emoji-1F3B9 { background-position: -440px -140px; } +.emoji-1F3BA { background-position: -440px -160px; } +.emoji-1F3BB { background-position: -440px -180px; } +.emoji-1F3BC { background-position: -440px -200px; } +.emoji-1F3BD { background-position: -440px -220px; } +.emoji-1F3BE { background-position: -440px -240px; } +.emoji-1F3BF { background-position: -440px -260px; } +.emoji-1F3C0 { background-position: -440px -280px; } +.emoji-1F3C1 { background-position: -440px -300px; } +.emoji-1F3C2 { background-position: -440px -320px; } +.emoji-1F3C3 { background-position: -440px -340px; } +.emoji-1F3C3-1F3FB { background-position: -440px -360px; } +.emoji-1F3C3-1F3FC { background-position: -440px -380px; } +.emoji-1F3C3-1F3FD { background-position: -440px -400px; } +.emoji-1F3C3-1F3FE { background-position: -440px -420px; } +.emoji-1F3C3-1F3FF { background-position: 0px -440px; } +.emoji-1F3C4 { background-position: -20px -440px; } +.emoji-1F3C4-1F3FB { background-position: -40px -440px; } +.emoji-1F3C4-1F3FC { background-position: -60px -440px; } +.emoji-1F3C4-1F3FD { background-position: -80px -440px; } +.emoji-1F3C4-1F3FE { background-position: -100px -440px; } +.emoji-1F3C4-1F3FF { background-position: -120px -440px; } +.emoji-1F3C5 { background-position: -140px -440px; } +.emoji-1F3C6 { background-position: -160px -440px; } +.emoji-1F3C7 { background-position: -180px -440px; } +.emoji-1F3C7-1F3FB { background-position: -200px -440px; } +.emoji-1F3C7-1F3FC { background-position: -220px -440px; } +.emoji-1F3C7-1F3FD { background-position: -240px -440px; } +.emoji-1F3C7-1F3FE { background-position: -260px -440px; } +.emoji-1F3C7-1F3FF { background-position: -280px -440px; } +.emoji-1F3C8 { background-position: -300px -440px; } +.emoji-1F3C9 { background-position: -320px -440px; } +.emoji-1F3CA { background-position: -340px -440px; } +.emoji-1F3CA-1F3FB { background-position: -360px -440px; } +.emoji-1F3CA-1F3FC { background-position: -380px -440px; } +.emoji-1F3CA-1F3FD { background-position: -400px -440px; } +.emoji-1F3CA-1F3FE { background-position: -420px -440px; } +.emoji-1F3CA-1F3FF { background-position: -440px -440px; } +.emoji-1F3CB { background-position: -460px 0px; } +.emoji-1F3CB-1F3FB { background-position: -460px -20px; } +.emoji-1F3CB-1F3FC { background-position: -460px -40px; } +.emoji-1F3CB-1F3FD { background-position: -460px -60px; } +.emoji-1F3CB-1F3FE { background-position: -460px -80px; } +.emoji-1F3CB-1F3FF { background-position: -460px -100px; } +.emoji-1F3CC { background-position: -460px -120px; } +.emoji-1F3CD { background-position: -460px -140px; } +.emoji-1F3CE { background-position: -460px -160px; } +.emoji-1F3CF { background-position: -460px -180px; } +.emoji-1F3D0 { background-position: -460px -200px; } +.emoji-1F3D1 { background-position: -460px -220px; } +.emoji-1F3D2 { background-position: -460px -240px; } +.emoji-1F3D3 { background-position: -460px -260px; } +.emoji-1F3D4 { background-position: -460px -280px; } +.emoji-1F3D5 { background-position: -460px -300px; } +.emoji-1F3D6 { background-position: -460px -320px; } +.emoji-1F3D7 { background-position: -460px -340px; } +.emoji-1F3D8 { background-position: -460px -360px; } +.emoji-1F3D9 { background-position: -460px -380px; } +.emoji-1F3DA { background-position: -460px -400px; } +.emoji-1F3DB { background-position: -460px -420px; } +.emoji-1F3DC { background-position: -460px -440px; } +.emoji-1F3DD { background-position: 0px -460px; } +.emoji-1F3DE { background-position: -20px -460px; } +.emoji-1F3DF { background-position: -40px -460px; } +.emoji-1F3E0 { background-position: -60px -460px; } +.emoji-1F3E1 { background-position: -80px -460px; } +.emoji-1F3E2 { background-position: -100px -460px; } +.emoji-1F3E3 { background-position: -120px -460px; } +.emoji-1F3E4 { background-position: -140px -460px; } +.emoji-1F3E5 { background-position: -160px -460px; } +.emoji-1F3E6 { background-position: -180px -460px; } +.emoji-1F3E7 { background-position: -200px -460px; } +.emoji-1F3E8 { background-position: -220px -460px; } +.emoji-1F3E9 { background-position: -240px -460px; } +.emoji-1F3EA { background-position: -260px -460px; } +.emoji-1F3EB { background-position: -280px -460px; } +.emoji-1F3EC { background-position: -300px -460px; } +.emoji-1F3ED { background-position: -320px -460px; } +.emoji-1F3EE { background-position: -340px -460px; } +.emoji-1F3EF { background-position: -360px -460px; } +.emoji-1F3F0 { background-position: -380px -460px; } +.emoji-1F3F1 { background-position: -400px -460px; } +.emoji-1F3F2 { background-position: -420px -460px; } +.emoji-1F3F3 { background-position: -440px -460px; } +.emoji-1F3F4 { background-position: -460px -460px; } +.emoji-1F3F5 { background-position: -480px 0px; } +.emoji-1F3F6 { background-position: -480px -20px; } +.emoji-1F3F7 { background-position: -480px -40px; } +.emoji-1F3F8 { background-position: -480px -60px; } +.emoji-1F3F9 { background-position: -480px -80px; } +.emoji-1F3FA { background-position: -480px -100px; } +.emoji-1F3FB { background-position: -480px -120px; } +.emoji-1F3FC { background-position: -480px -140px; } +.emoji-1F3FD { background-position: -480px -160px; } +.emoji-1F3FE { background-position: -480px -180px; } +.emoji-1F3FF { background-position: -480px -200px; } +.emoji-1F400 { background-position: -480px -220px; } +.emoji-1F401 { background-position: -480px -240px; } +.emoji-1F402 { background-position: -480px -260px; } +.emoji-1F403 { background-position: -480px -280px; } +.emoji-1F404 { background-position: -480px -300px; } +.emoji-1F405 { background-position: -480px -320px; } +.emoji-1F406 { background-position: -480px -340px; } +.emoji-1F407 { background-position: -480px -360px; } +.emoji-1F408 { background-position: -480px -380px; } +.emoji-1F409 { background-position: -480px -400px; } +.emoji-1F40A { background-position: -480px -420px; } +.emoji-1F40B { background-position: -480px -440px; } +.emoji-1F40C { background-position: -480px -460px; } +.emoji-1F40D { background-position: 0px -480px; } +.emoji-1F40E { background-position: -20px -480px; } +.emoji-1F40F { background-position: -40px -480px; } +.emoji-1F410 { background-position: -60px -480px; } +.emoji-1F411 { background-position: -80px -480px; } +.emoji-1F412 { background-position: -100px -480px; } +.emoji-1F413 { background-position: -120px -480px; } +.emoji-1F414 { background-position: -140px -480px; } +.emoji-1F415 { background-position: -160px -480px; } +.emoji-1F416 { background-position: -180px -480px; } +.emoji-1F417 { background-position: -200px -480px; } +.emoji-1F418 { background-position: -220px -480px; } +.emoji-1F419 { background-position: -240px -480px; } +.emoji-1F41A { background-position: -260px -480px; } +.emoji-1F41B { background-position: -280px -480px; } +.emoji-1F41C { background-position: -300px -480px; } +.emoji-1F41D { background-position: -320px -480px; } +.emoji-1F41E { background-position: -340px -480px; } +.emoji-1F41F { background-position: -360px -480px; } +.emoji-1F420 { background-position: -380px -480px; } +.emoji-1F421 { background-position: -400px -480px; } +.emoji-1F422 { background-position: -420px -480px; } +.emoji-1F423 { background-position: -440px -480px; } +.emoji-1F424 { background-position: -460px -480px; } +.emoji-1F425 { background-position: -480px -480px; } +.emoji-1F426 { background-position: -500px 0px; } +.emoji-1F427 { background-position: -500px -20px; } +.emoji-1F428 { background-position: -500px -40px; } +.emoji-1F429 { background-position: -500px -60px; } +.emoji-1F42A { background-position: -500px -80px; } +.emoji-1F42B { background-position: -500px -100px; } +.emoji-1F42C { background-position: -500px -120px; } +.emoji-1F42D { background-position: -500px -140px; } +.emoji-1F42E { background-position: -500px -160px; } +.emoji-1F42F { background-position: -500px -180px; } +.emoji-1F430 { background-position: -500px -200px; } +.emoji-1F431 { background-position: -500px -220px; } +.emoji-1F432 { background-position: -500px -240px; } +.emoji-1F433 { background-position: -500px -260px; } +.emoji-1F434 { background-position: -500px -280px; } +.emoji-1F435 { background-position: -500px -300px; } +.emoji-1F436 { background-position: -500px -320px; } +.emoji-1F437 { background-position: -500px -340px; } +.emoji-1F438 { background-position: -500px -360px; } +.emoji-1F439 { background-position: -500px -380px; } +.emoji-1F43A { background-position: -500px -400px; } +.emoji-1F43B { background-position: -500px -420px; } +.emoji-1F43C { background-position: -500px -440px; } +.emoji-1F43D { background-position: -500px -460px; } +.emoji-1F43E { background-position: -500px -480px; } +.emoji-1F43F { background-position: 0px -500px; } +.emoji-1F440 { background-position: -20px -500px; } +.emoji-1F441 { background-position: -40px -500px; } +.emoji-1F441-1F5E8 { background-position: -60px -500px; } +.emoji-1F442 { background-position: -80px -500px; } +.emoji-1F442-1F3FB { background-position: -100px -500px; } +.emoji-1F442-1F3FC { background-position: -120px -500px; } +.emoji-1F442-1F3FD { background-position: -140px -500px; } +.emoji-1F442-1F3FE { background-position: -160px -500px; } +.emoji-1F442-1F3FF { background-position: -180px -500px; } +.emoji-1F443 { background-position: -200px -500px; } +.emoji-1F443-1F3FB { background-position: -220px -500px; } +.emoji-1F443-1F3FC { background-position: -240px -500px; } +.emoji-1F443-1F3FD { background-position: -260px -500px; } +.emoji-1F443-1F3FE { background-position: -280px -500px; } +.emoji-1F443-1F3FF { background-position: -300px -500px; } +.emoji-1F444 { background-position: -320px -500px; } +.emoji-1F445 { background-position: -340px -500px; } +.emoji-1F446 { background-position: -360px -500px; } +.emoji-1F446-1F3FB { background-position: -380px -500px; } +.emoji-1F446-1F3FC { background-position: -400px -500px; } +.emoji-1F446-1F3FD { background-position: -420px -500px; } +.emoji-1F446-1F3FE { background-position: -440px -500px; } +.emoji-1F446-1F3FF { background-position: -460px -500px; } +.emoji-1F447 { background-position: -480px -500px; } +.emoji-1F447-1F3FB { background-position: -500px -500px; } +.emoji-1F447-1F3FC { background-position: -520px 0px; } +.emoji-1F447-1F3FD { background-position: -520px -20px; } +.emoji-1F447-1F3FE { background-position: -520px -40px; } +.emoji-1F447-1F3FF { background-position: -520px -60px; } +.emoji-1F448 { background-position: -520px -80px; } +.emoji-1F448-1F3FB { background-position: -520px -100px; } +.emoji-1F448-1F3FC { background-position: -520px -120px; } +.emoji-1F448-1F3FD { background-position: -520px -140px; } +.emoji-1F448-1F3FE { background-position: -520px -160px; } +.emoji-1F448-1F3FF { background-position: -520px -180px; } +.emoji-1F449 { background-position: -520px -200px; } +.emoji-1F449-1F3FB { background-position: -520px -220px; } +.emoji-1F449-1F3FC { background-position: -520px -240px; } +.emoji-1F449-1F3FD { background-position: -520px -260px; } +.emoji-1F449-1F3FE { background-position: -520px -280px; } +.emoji-1F449-1F3FF { background-position: -520px -300px; } +.emoji-1F44A { background-position: -520px -320px; } +.emoji-1F44A-1F3FB { background-position: -520px -340px; } +.emoji-1F44A-1F3FC { background-position: -520px -360px; } +.emoji-1F44A-1F3FD { background-position: -520px -380px; } +.emoji-1F44A-1F3FE { background-position: -520px -400px; } +.emoji-1F44A-1F3FF { background-position: -520px -420px; } +.emoji-1F44B { background-position: -520px -440px; } +.emoji-1F44B-1F3FB { background-position: -520px -460px; } +.emoji-1F44B-1F3FC { background-position: -520px -480px; } +.emoji-1F44B-1F3FD { background-position: -520px -500px; } +.emoji-1F44B-1F3FE { background-position: 0px -520px; } +.emoji-1F44B-1F3FF { background-position: -20px -520px; } +.emoji-1F44C { background-position: -40px -520px; } +.emoji-1F44C-1F3FB { background-position: -60px -520px; } +.emoji-1F44C-1F3FC { background-position: -80px -520px; } +.emoji-1F44C-1F3FD { background-position: -100px -520px; } +.emoji-1F44C-1F3FE { background-position: -120px -520px; } +.emoji-1F44C-1F3FF { background-position: -140px -520px; } +.emoji-1F44D { background-position: -160px -520px; } +.emoji-1F44D-1F3FB { background-position: -180px -520px; } +.emoji-1F44D-1F3FC { background-position: -200px -520px; } +.emoji-1F44D-1F3FD { background-position: -220px -520px; } +.emoji-1F44D-1F3FE { background-position: -240px -520px; } +.emoji-1F44D-1F3FF { background-position: -260px -520px; } +.emoji-1F44E { background-position: -280px -520px; } +.emoji-1F44E-1F3FB { background-position: -300px -520px; } +.emoji-1F44E-1F3FC { background-position: -320px -520px; } +.emoji-1F44E-1F3FD { background-position: -340px -520px; } +.emoji-1F44E-1F3FE { background-position: -360px -520px; } +.emoji-1F44E-1F3FF { background-position: -380px -520px; } +.emoji-1F44F { background-position: -400px -520px; } +.emoji-1F44F-1F3FB { background-position: -420px -520px; } +.emoji-1F44F-1F3FC { background-position: -440px -520px; } +.emoji-1F44F-1F3FD { background-position: -460px -520px; } +.emoji-1F44F-1F3FE { background-position: -480px -520px; } +.emoji-1F44F-1F3FF { background-position: -500px -520px; } +.emoji-1F450 { background-position: -520px -520px; } +.emoji-1F450-1F3FB { background-position: -540px 0px; } +.emoji-1F450-1F3FC { background-position: -540px -20px; } +.emoji-1F450-1F3FD { background-position: -540px -40px; } +.emoji-1F450-1F3FE { background-position: -540px -60px; } +.emoji-1F450-1F3FF { background-position: -540px -80px; } +.emoji-1F451 { background-position: -540px -100px; } +.emoji-1F452 { background-position: -540px -120px; } +.emoji-1F453 { background-position: -540px -140px; } +.emoji-1F454 { background-position: -540px -160px; } +.emoji-1F455 { background-position: -540px -180px; } +.emoji-1F456 { background-position: -540px -200px; } +.emoji-1F457 { background-position: -540px -220px; } +.emoji-1F458 { background-position: -540px -240px; } +.emoji-1F459 { background-position: -540px -260px; } +.emoji-1F45A { background-position: -540px -280px; } +.emoji-1F45B { background-position: -540px -300px; } +.emoji-1F45C { background-position: -540px -320px; } +.emoji-1F45D { background-position: -540px -340px; } +.emoji-1F45E { background-position: -540px -360px; } +.emoji-1F45F { background-position: -540px -380px; } +.emoji-1F460 { background-position: -540px -400px; } +.emoji-1F461 { background-position: -540px -420px; } +.emoji-1F462 { background-position: -540px -440px; } +.emoji-1F463 { background-position: -540px -460px; } +.emoji-1F464 { background-position: -540px -480px; } +.emoji-1F465 { background-position: -540px -500px; } +.emoji-1F466 { background-position: -540px -520px; } +.emoji-1F466-1F3FB { background-position: 0px -540px; } +.emoji-1F466-1F3FC { background-position: -20px -540px; } +.emoji-1F466-1F3FD { background-position: -40px -540px; } +.emoji-1F466-1F3FE { background-position: -60px -540px; } +.emoji-1F466-1F3FF { background-position: -80px -540px; } +.emoji-1F467 { background-position: -100px -540px; } +.emoji-1F467-1F3FB { background-position: -120px -540px; } +.emoji-1F467-1F3FC { background-position: -140px -540px; } +.emoji-1F467-1F3FD { background-position: -160px -540px; } +.emoji-1F467-1F3FE { background-position: -180px -540px; } +.emoji-1F467-1F3FF { background-position: -200px -540px; } +.emoji-1F468 { background-position: -220px -540px; } +.emoji-1F468-1F3FB { background-position: -240px -540px; } +.emoji-1F468-1F3FC { background-position: -260px -540px; } +.emoji-1F468-1F3FD { background-position: -280px -540px; } +.emoji-1F468-1F3FE { background-position: -300px -540px; } +.emoji-1F468-1F3FF { background-position: -320px -540px; } +.emoji-1F468-1F468-1F466 { background-position: -340px -540px; } +.emoji-1F468-1F468-1F466-1F466 { background-position: -360px -540px; } +.emoji-1F468-1F468-1F467 { background-position: -380px -540px; } +.emoji-1F468-1F468-1F467-1F466 { background-position: -400px -540px; } +.emoji-1F468-1F468-1F467-1F467 { background-position: -420px -540px; } +.emoji-1F468-1F469-1F466-1F466 { background-position: -440px -540px; } +.emoji-1F468-1F469-1F467 { background-position: -460px -540px; } +.emoji-1F468-1F469-1F467-1F466 { background-position: -480px -540px; } +.emoji-1F468-1F469-1F467-1F467 { background-position: -500px -540px; } +.emoji-1F468-2764-1F468 { background-position: -520px -540px; } +.emoji-1F468-2764-1F48B-1F468 { background-position: -540px -540px; } +.emoji-1F469 { background-position: -560px 0px; } +.emoji-1F469-1F3FB { background-position: -560px -20px; } +.emoji-1F469-1F3FC { background-position: -560px -40px; } +.emoji-1F469-1F3FD { background-position: -560px -60px; } +.emoji-1F469-1F3FE { background-position: -560px -80px; } +.emoji-1F469-1F3FF { background-position: -560px -100px; } +.emoji-1F469-1F469-1F466 { background-position: -560px -120px; } +.emoji-1F469-1F469-1F466-1F466 { background-position: -560px -140px; } +.emoji-1F469-1F469-1F467 { background-position: -560px -160px; } +.emoji-1F469-1F469-1F467-1F466 { background-position: -560px -180px; } +.emoji-1F469-1F469-1F467-1F467 { background-position: -560px -200px; } +.emoji-1F469-2764-1F469 { background-position: -560px -220px; } +.emoji-1F469-2764-1F48B-1F469 { background-position: -560px -240px; } +.emoji-1F46A { background-position: -560px -260px; } +.emoji-1F46B { background-position: -560px -280px; } +.emoji-1F46C { background-position: -560px -300px; } +.emoji-1F46D { background-position: -560px -320px; } +.emoji-1F46E { background-position: -560px -340px; } +.emoji-1F46E-1F3FB { background-position: -560px -360px; } +.emoji-1F46E-1F3FC { background-position: -560px -380px; } +.emoji-1F46E-1F3FD { background-position: -560px -400px; } +.emoji-1F46E-1F3FE { background-position: -560px -420px; } +.emoji-1F46E-1F3FF { background-position: -560px -440px; } +.emoji-1F46F { background-position: -560px -460px; } +.emoji-1F470 { background-position: -560px -480px; } +.emoji-1F470-1F3FB { background-position: -560px -500px; } +.emoji-1F470-1F3FC { background-position: -560px -520px; } +.emoji-1F470-1F3FD { background-position: -560px -540px; } +.emoji-1F470-1F3FE { background-position: 0px -560px; } +.emoji-1F470-1F3FF { background-position: -20px -560px; } +.emoji-1F471 { background-position: -40px -560px; } +.emoji-1F471-1F3FB { background-position: -60px -560px; } +.emoji-1F471-1F3FC { background-position: -80px -560px; } +.emoji-1F471-1F3FD { background-position: -100px -560px; } +.emoji-1F471-1F3FE { background-position: -120px -560px; } +.emoji-1F471-1F3FF { background-position: -140px -560px; } +.emoji-1F472 { background-position: -160px -560px; } +.emoji-1F472-1F3FB { background-position: -180px -560px; } +.emoji-1F472-1F3FC { background-position: -200px -560px; } +.emoji-1F472-1F3FD { background-position: -220px -560px; } +.emoji-1F472-1F3FE { background-position: -240px -560px; } +.emoji-1F472-1F3FF { background-position: -260px -560px; } +.emoji-1F473 { background-position: -280px -560px; } +.emoji-1F473-1F3FB { background-position: -300px -560px; } +.emoji-1F473-1F3FC { background-position: -320px -560px; } +.emoji-1F473-1F3FD { background-position: -340px -560px; } +.emoji-1F473-1F3FE { background-position: -360px -560px; } +.emoji-1F473-1F3FF { background-position: -380px -560px; } +.emoji-1F474 { background-position: -400px -560px; } +.emoji-1F474-1F3FB { background-position: -420px -560px; } +.emoji-1F474-1F3FC { background-position: -440px -560px; } +.emoji-1F474-1F3FD { background-position: -460px -560px; } +.emoji-1F474-1F3FE { background-position: -480px -560px; } +.emoji-1F474-1F3FF { background-position: -500px -560px; } +.emoji-1F475 { background-position: -520px -560px; } +.emoji-1F475-1F3FB { background-position: -540px -560px; } +.emoji-1F475-1F3FC { background-position: -560px -560px; } +.emoji-1F475-1F3FD { background-position: -580px 0px; } +.emoji-1F475-1F3FE { background-position: -580px -20px; } +.emoji-1F475-1F3FF { background-position: -580px -40px; } +.emoji-1F476 { background-position: -580px -60px; } +.emoji-1F476-1F3FB { background-position: -580px -80px; } +.emoji-1F476-1F3FC { background-position: -580px -100px; } +.emoji-1F476-1F3FD { background-position: -580px -120px; } +.emoji-1F476-1F3FE { background-position: -580px -140px; } +.emoji-1F476-1F3FF { background-position: -580px -160px; } +.emoji-1F477 { background-position: -580px -180px; } +.emoji-1F477-1F3FB { background-position: -580px -200px; } +.emoji-1F477-1F3FC { background-position: -580px -220px; } +.emoji-1F477-1F3FD { background-position: -580px -240px; } +.emoji-1F477-1F3FE { background-position: -580px -260px; } +.emoji-1F477-1F3FF { background-position: -580px -280px; } +.emoji-1F478 { background-position: -580px -300px; } +.emoji-1F478-1F3FB { background-position: -580px -320px; } +.emoji-1F478-1F3FC { background-position: -580px -340px; } +.emoji-1F478-1F3FD { background-position: -580px -360px; } +.emoji-1F478-1F3FE { background-position: -580px -380px; } +.emoji-1F478-1F3FF { background-position: -580px -400px; } +.emoji-1F479 { background-position: -580px -420px; } +.emoji-1F47A { background-position: -580px -440px; } +.emoji-1F47B { background-position: -580px -460px; } +.emoji-1F47C { background-position: -580px -480px; } +.emoji-1F47C-1F3FB { background-position: -580px -500px; } +.emoji-1F47C-1F3FC { background-position: -580px -520px; } +.emoji-1F47C-1F3FD { background-position: -580px -540px; } +.emoji-1F47C-1F3FE { background-position: -580px -560px; } +.emoji-1F47C-1F3FF { background-position: 0px -580px; } +.emoji-1F47D { background-position: -20px -580px; } +.emoji-1F47E { background-position: -40px -580px; } +.emoji-1F47F { background-position: -60px -580px; } +.emoji-1F480 { background-position: -80px -580px; } +.emoji-1F481 { background-position: -100px -580px; } +.emoji-1F481-1F3FB { background-position: -120px -580px; } +.emoji-1F481-1F3FC { background-position: -140px -580px; } +.emoji-1F481-1F3FD { background-position: -160px -580px; } +.emoji-1F481-1F3FE { background-position: -180px -580px; } +.emoji-1F481-1F3FF { background-position: -200px -580px; } +.emoji-1F482 { background-position: -220px -580px; } +.emoji-1F482-1F3FB { background-position: -240px -580px; } +.emoji-1F482-1F3FC { background-position: -260px -580px; } +.emoji-1F482-1F3FD { background-position: -280px -580px; } +.emoji-1F482-1F3FE { background-position: -300px -580px; } +.emoji-1F482-1F3FF { background-position: -320px -580px; } +.emoji-1F483 { background-position: -340px -580px; } +.emoji-1F483-1F3FB { background-position: -360px -580px; } +.emoji-1F483-1F3FC { background-position: -380px -580px; } +.emoji-1F483-1F3FD { background-position: -400px -580px; } +.emoji-1F483-1F3FE { background-position: -420px -580px; } +.emoji-1F483-1F3FF { background-position: -440px -580px; } +.emoji-1F484 { background-position: -460px -580px; } +.emoji-1F485 { background-position: -480px -580px; } +.emoji-1F485-1F3FB { background-position: -500px -580px; } +.emoji-1F485-1F3FC { background-position: -520px -580px; } +.emoji-1F485-1F3FD { background-position: -540px -580px; } +.emoji-1F485-1F3FE { background-position: -560px -580px; } +.emoji-1F485-1F3FF { background-position: -580px -580px; } +.emoji-1F486 { background-position: -600px 0px; } +.emoji-1F486-1F3FB { background-position: -600px -20px; } +.emoji-1F486-1F3FC { background-position: -600px -40px; } +.emoji-1F486-1F3FD { background-position: -600px -60px; } +.emoji-1F486-1F3FE { background-position: -600px -80px; } +.emoji-1F486-1F3FF { background-position: -600px -100px; } +.emoji-1F487 { background-position: -600px -120px; } +.emoji-1F487-1F3FB { background-position: -600px -140px; } +.emoji-1F487-1F3FC { background-position: -600px -160px; } +.emoji-1F487-1F3FD { background-position: -600px -180px; } +.emoji-1F487-1F3FE { background-position: -600px -200px; } +.emoji-1F487-1F3FF { background-position: -600px -220px; } +.emoji-1F488 { background-position: -600px -240px; } +.emoji-1F489 { background-position: -600px -260px; } +.emoji-1F48A { background-position: -600px -280px; } +.emoji-1F48B { background-position: -600px -300px; } +.emoji-1F48C { background-position: -600px -320px; } +.emoji-1F48D { background-position: -600px -340px; } +.emoji-1F48E { background-position: -600px -360px; } +.emoji-1F48F { background-position: -600px -380px; } +.emoji-1F490 { background-position: -600px -400px; } +.emoji-1F491 { background-position: -600px -420px; } +.emoji-1F492 { background-position: -600px -440px; } +.emoji-1F493 { background-position: -600px -460px; } +.emoji-1F494 { background-position: -600px -480px; } +.emoji-1F495 { background-position: -600px -500px; } +.emoji-1F496 { background-position: -600px -520px; } +.emoji-1F497 { background-position: -600px -540px; } +.emoji-1F498 { background-position: -600px -560px; } +.emoji-1F499 { background-position: -600px -580px; } +.emoji-1F49A { background-position: 0px -600px; } +.emoji-1F49B { background-position: -20px -600px; } +.emoji-1F49C { background-position: -40px -600px; } +.emoji-1F49D { background-position: -60px -600px; } +.emoji-1F49E { background-position: -80px -600px; } +.emoji-1F49F { background-position: -100px -600px; } +.emoji-1F4A0 { background-position: -120px -600px; } +.emoji-1F4A1 { background-position: -140px -600px; } +.emoji-1F4A2 { background-position: -160px -600px; } +.emoji-1F4A3 { background-position: -180px -600px; } +.emoji-1F4A4 { background-position: -200px -600px; } +.emoji-1F4A5 { background-position: -220px -600px; } +.emoji-1F4A6 { background-position: -240px -600px; } +.emoji-1F4A7 { background-position: -260px -600px; } +.emoji-1F4A8 { background-position: -280px -600px; } +.emoji-1F4A9 { background-position: -300px -600px; } +.emoji-1F4AA { background-position: -320px -600px; } +.emoji-1F4AA-1F3FB { background-position: -340px -600px; } +.emoji-1F4AA-1F3FC { background-position: -360px -600px; } +.emoji-1F4AA-1F3FD { background-position: -380px -600px; } +.emoji-1F4AA-1F3FE { background-position: -400px -600px; } +.emoji-1F4AA-1F3FF { background-position: -420px -600px; } +.emoji-1F4AB { background-position: -440px -600px; } +.emoji-1F4AC { background-position: -460px -600px; } +.emoji-1F4AD { background-position: -480px -600px; } +.emoji-1F4AE { background-position: -500px -600px; } +.emoji-1F4AF { background-position: -520px -600px; } +.emoji-1F4B0 { background-position: -540px -600px; } +.emoji-1F4B1 { background-position: -560px -600px; } +.emoji-1F4B2 { background-position: -580px -600px; } +.emoji-1F4B3 { background-position: -600px -600px; } +.emoji-1F4B4 { background-position: -620px 0px; } +.emoji-1F4B5 { background-position: -620px -20px; } +.emoji-1F4B6 { background-position: -620px -40px; } +.emoji-1F4B7 { background-position: -620px -60px; } +.emoji-1F4B8 { background-position: -620px -80px; } +.emoji-1F4B9 { background-position: -620px -100px; } +.emoji-1F4BA { background-position: -620px -120px; } +.emoji-1F4BB { background-position: -620px -140px; } +.emoji-1F4BC { background-position: -620px -160px; } +.emoji-1F4BD { background-position: -620px -180px; } +.emoji-1F4BE { background-position: -620px -200px; } +.emoji-1F4BF { background-position: -620px -220px; } +.emoji-1F4C0 { background-position: -620px -240px; } +.emoji-1F4C1 { background-position: -620px -260px; } +.emoji-1F4C2 { background-position: -620px -280px; } +.emoji-1F4C3 { background-position: -620px -300px; } +.emoji-1F4C4 { background-position: -620px -320px; } +.emoji-1F4C5 { background-position: -620px -340px; } +.emoji-1F4C6 { background-position: -620px -360px; } +.emoji-1F4C7 { background-position: -620px -380px; } +.emoji-1F4C8 { background-position: -620px -400px; } +.emoji-1F4C9 { background-position: -620px -420px; } +.emoji-1F4CA { background-position: -620px -440px; } +.emoji-1F4CB { background-position: -620px -460px; } +.emoji-1F4CC { background-position: -620px -480px; } +.emoji-1F4CD { background-position: -620px -500px; } +.emoji-1F4CE { background-position: -620px -520px; } +.emoji-1F4CF { background-position: -620px -540px; } +.emoji-1F4D0 { background-position: -620px -560px; } +.emoji-1F4D1 { background-position: -620px -580px; } +.emoji-1F4D2 { background-position: -620px -600px; } +.emoji-1F4D3 { background-position: 0px -620px; } +.emoji-1F4D4 { background-position: -20px -620px; } +.emoji-1F4D5 { background-position: -40px -620px; } +.emoji-1F4D6 { background-position: -60px -620px; } +.emoji-1F4D7 { background-position: -80px -620px; } +.emoji-1F4D8 { background-position: -100px -620px; } +.emoji-1F4D9 { background-position: -120px -620px; } +.emoji-1F4DA { background-position: -140px -620px; } +.emoji-1F4DB { background-position: -160px -620px; } +.emoji-1F4DC { background-position: -180px -620px; } +.emoji-1F4DD { background-position: -200px -620px; } +.emoji-1F4DE { background-position: -220px -620px; } +.emoji-1F4DF { background-position: -240px -620px; } +.emoji-1F4E0 { background-position: -260px -620px; } +.emoji-1F4E1 { background-position: -280px -620px; } +.emoji-1F4E2 { background-position: -300px -620px; } +.emoji-1F4E3 { background-position: -320px -620px; } +.emoji-1F4E4 { background-position: -340px -620px; } +.emoji-1F4E5 { background-position: -360px -620px; } +.emoji-1F4E6 { background-position: -380px -620px; } +.emoji-1F4E7 { background-position: -400px -620px; } +.emoji-1F4E8 { background-position: -420px -620px; } +.emoji-1F4E9 { background-position: -440px -620px; } +.emoji-1F4EA { background-position: -460px -620px; } +.emoji-1F4EB { background-position: -480px -620px; } +.emoji-1F4EC { background-position: -500px -620px; } +.emoji-1F4ED { background-position: -520px -620px; } +.emoji-1F4EE { background-position: -540px -620px; } +.emoji-1F4EF { background-position: -560px -620px; } +.emoji-1F4F0 { background-position: -580px -620px; } +.emoji-1F4F1 { background-position: -600px -620px; } +.emoji-1F4F2 { background-position: -620px -620px; } +.emoji-1F4F3 { background-position: -640px 0px; } +.emoji-1F4F4 { background-position: -640px -20px; } +.emoji-1F4F5 { background-position: -640px -40px; } +.emoji-1F4F6 { background-position: -640px -60px; } +.emoji-1F4F7 { background-position: -640px -80px; } +.emoji-1F4F8 { background-position: -640px -100px; } +.emoji-1F4F9 { background-position: -640px -120px; } +.emoji-1F4FA { background-position: -640px -140px; } +.emoji-1F4FB { background-position: -640px -160px; } +.emoji-1F4FC { background-position: -640px -180px; } +.emoji-1F4FD { background-position: -640px -200px; } +.emoji-1F4FE { background-position: -640px -220px; } +.emoji-1F4FF { background-position: -640px -240px; } +.emoji-1F500 { background-position: -640px -260px; } +.emoji-1F501 { background-position: -640px -280px; } +.emoji-1F502 { background-position: -640px -300px; } +.emoji-1F503 { background-position: -640px -320px; } +.emoji-1F504 { background-position: -640px -340px; } +.emoji-1F505 { background-position: -640px -360px; } +.emoji-1F506 { background-position: -640px -380px; } +.emoji-1F507 { background-position: -640px -400px; } +.emoji-1F508 { background-position: -640px -420px; } +.emoji-1F509 { background-position: -640px -440px; } +.emoji-1F50A { background-position: -640px -460px; } +.emoji-1F50B { background-position: -640px -480px; } +.emoji-1F50C { background-position: -640px -500px; } +.emoji-1F50D { background-position: -640px -520px; } +.emoji-1F50E { background-position: -640px -540px; } +.emoji-1F50F { background-position: -640px -560px; } +.emoji-1F510 { background-position: -640px -580px; } +.emoji-1F511 { background-position: -640px -600px; } +.emoji-1F512 { background-position: -640px -620px; } +.emoji-1F513 { background-position: 0px -640px; } +.emoji-1F514 { background-position: -20px -640px; } +.emoji-1F515 { background-position: -40px -640px; } +.emoji-1F516 { background-position: -60px -640px; } +.emoji-1F517 { background-position: -80px -640px; } +.emoji-1F518 { background-position: -100px -640px; } +.emoji-1F519 { background-position: -120px -640px; } +.emoji-1F51A { background-position: -140px -640px; } +.emoji-1F51B { background-position: -160px -640px; } +.emoji-1F51C { background-position: -180px -640px; } +.emoji-1F51D { background-position: -200px -640px; } +.emoji-1F51E { background-position: -220px -640px; } +.emoji-1F51F { background-position: -240px -640px; } +.emoji-1F520 { background-position: -260px -640px; } +.emoji-1F521 { background-position: -280px -640px; } +.emoji-1F522 { background-position: -300px -640px; } +.emoji-1F523 { background-position: -320px -640px; } +.emoji-1F524 { background-position: -340px -640px; } +.emoji-1F525 { background-position: -360px -640px; } +.emoji-1F526 { background-position: -380px -640px; } +.emoji-1F527 { background-position: -400px -640px; } +.emoji-1F528 { background-position: -420px -640px; } +.emoji-1F529 { background-position: -440px -640px; } +.emoji-1F52A { background-position: -460px -640px; } +.emoji-1F52B { background-position: -480px -640px; } +.emoji-1F52C { background-position: -500px -640px; } +.emoji-1F52D { background-position: -520px -640px; } +.emoji-1F52E { background-position: -540px -640px; } +.emoji-1F52F { background-position: -560px -640px; } +.emoji-1F530 { background-position: -580px -640px; } +.emoji-1F531 { background-position: -600px -640px; } +.emoji-1F532 { background-position: -620px -640px; } +.emoji-1F533 { background-position: -640px -640px; } +.emoji-1F534 { background-position: -660px 0px; } +.emoji-1F535 { background-position: -660px -20px; } +.emoji-1F536 { background-position: -660px -40px; } +.emoji-1F537 { background-position: -660px -60px; } +.emoji-1F538 { background-position: -660px -80px; } +.emoji-1F539 { background-position: -660px -100px; } +.emoji-1F53A { background-position: -660px -120px; } +.emoji-1F53B { background-position: -660px -140px; } +.emoji-1F53C { background-position: -660px -160px; } +.emoji-1F53D { background-position: -660px -180px; } +.emoji-1F546 { background-position: -660px -200px; } +.emoji-1F547 { background-position: -660px -220px; } +.emoji-1F548 { background-position: -660px -240px; } +.emoji-1F549 { background-position: -660px -260px; } +.emoji-1F54A { background-position: -660px -280px; } +.emoji-1F54B { background-position: -660px -300px; } +.emoji-1F54C { background-position: -660px -320px; } +.emoji-1F54D { background-position: -660px -340px; } +.emoji-1F54E { background-position: -660px -360px; } +.emoji-1F550 { background-position: -660px -380px; } +.emoji-1F551 { background-position: -660px -400px; } +.emoji-1F552 { background-position: -660px -420px; } +.emoji-1F553 { background-position: -660px -440px; } +.emoji-1F554 { background-position: -660px -460px; } +.emoji-1F555 { background-position: -660px -480px; } +.emoji-1F556 { background-position: -660px -500px; } +.emoji-1F557 { background-position: -660px -520px; } +.emoji-1F558 { background-position: -660px -540px; } +.emoji-1F559 { background-position: -660px -560px; } +.emoji-1F55A { background-position: -660px -580px; } +.emoji-1F55B { background-position: -660px -600px; } +.emoji-1F55C { background-position: -660px -620px; } +.emoji-1F55D { background-position: -660px -640px; } +.emoji-1F55E { background-position: 0px -660px; } +.emoji-1F55F { background-position: -20px -660px; } +.emoji-1F560 { background-position: -40px -660px; } +.emoji-1F561 { background-position: -60px -660px; } +.emoji-1F562 { background-position: -80px -660px; } +.emoji-1F563 { background-position: -100px -660px; } +.emoji-1F564 { background-position: -120px -660px; } +.emoji-1F565 { background-position: -140px -660px; } +.emoji-1F566 { background-position: -160px -660px; } +.emoji-1F567 { background-position: -180px -660px; } +.emoji-1F568 { background-position: -200px -660px; } +.emoji-1F569 { background-position: -220px -660px; } +.emoji-1F56A { background-position: -240px -660px; } +.emoji-1F56B { background-position: -260px -660px; } +.emoji-1F56C { background-position: -280px -660px; } +.emoji-1F56D { background-position: -300px -660px; } +.emoji-1F56E { background-position: -320px -660px; } +.emoji-1F56F { background-position: -340px -660px; } +.emoji-1F570 { background-position: -360px -660px; } +.emoji-1F571 { background-position: -380px -660px; } +.emoji-1F572 { background-position: -400px -660px; } +.emoji-1F573 { background-position: -420px -660px; } +.emoji-1F574 { background-position: -440px -660px; } +.emoji-1F575 { background-position: -460px -660px; } +.emoji-1F575-1F3FB { background-position: -480px -660px; } +.emoji-1F575-1F3FC { background-position: -500px -660px; } +.emoji-1F575-1F3FD { background-position: -520px -660px; } +.emoji-1F575-1F3FE { background-position: -540px -660px; } +.emoji-1F575-1F3FF { background-position: -560px -660px; } +.emoji-1F576 { background-position: -580px -660px; } +.emoji-1F577 { background-position: -600px -660px; } +.emoji-1F578 { background-position: -620px -660px; } +.emoji-1F579 { background-position: -640px -660px; } +.emoji-1F57B { background-position: -660px -660px; } +.emoji-1F57E { background-position: -680px 0px; } +.emoji-1F57F { background-position: -680px -20px; } +.emoji-1F581 { background-position: -680px -40px; } +.emoji-1F582 { background-position: -680px -60px; } +.emoji-1F583 { background-position: -680px -80px; } +.emoji-1F585 { background-position: -680px -100px; } +.emoji-1F586 { background-position: -680px -120px; } +.emoji-1F587 { background-position: -680px -140px; } +.emoji-1F588 { background-position: -680px -160px; } +.emoji-1F589 { background-position: -680px -180px; } +.emoji-1F58A { background-position: -680px -200px; } +.emoji-1F58B { background-position: -680px -220px; } +.emoji-1F58C { background-position: -680px -240px; } +.emoji-1F58D { background-position: -680px -260px; } +.emoji-1F58E { background-position: -680px -280px; } +.emoji-1F58F { background-position: -680px -300px; } +.emoji-1F590 { background-position: -680px -320px; } +.emoji-1F590-1F3FB { background-position: -680px -340px; } +.emoji-1F590-1F3FC { background-position: -680px -360px; } +.emoji-1F590-1F3FD { background-position: -680px -380px; } +.emoji-1F590-1F3FE { background-position: -680px -400px; } +.emoji-1F590-1F3FF { background-position: -680px -420px; } +.emoji-1F591 { background-position: -680px -440px; } +.emoji-1F592 { background-position: -680px -460px; } +.emoji-1F593 { background-position: -680px -480px; } +.emoji-1F594 { background-position: -680px -500px; } +.emoji-1F595 { background-position: -680px -520px; } +.emoji-1F595-1F3FB { background-position: -680px -540px; } +.emoji-1F595-1F3FC { background-position: -680px -560px; } +.emoji-1F595-1F3FD { background-position: -680px -580px; } +.emoji-1F595-1F3FE { background-position: -680px -600px; } +.emoji-1F595-1F3FF { background-position: -680px -620px; } +.emoji-1F596 { background-position: -680px -640px; } +.emoji-1F596-1F3FB { background-position: -680px -660px; } +.emoji-1F596-1F3FC { background-position: 0px -680px; } +.emoji-1F596-1F3FD { background-position: -20px -680px; } +.emoji-1F596-1F3FE { background-position: -40px -680px; } +.emoji-1F596-1F3FF { background-position: -60px -680px; } +.emoji-1F597 { background-position: -80px -680px; } +.emoji-1F598 { background-position: -100px -680px; } +.emoji-1F599 { background-position: -120px -680px; } +.emoji-1F59E { background-position: -140px -680px; } +.emoji-1F59F { background-position: -160px -680px; } +.emoji-1F5A5 { background-position: -180px -680px; } +.emoji-1F5A6 { background-position: -200px -680px; } +.emoji-1F5A7 { background-position: -220px -680px; } +.emoji-1F5A8 { background-position: -240px -680px; } +.emoji-1F5A9 { background-position: -260px -680px; } +.emoji-1F5AA { background-position: -280px -680px; } +.emoji-1F5AB { background-position: -300px -680px; } +.emoji-1F5AD { background-position: -320px -680px; } +.emoji-1F5AE { background-position: -340px -680px; } +.emoji-1F5AF { background-position: -360px -680px; } +.emoji-1F5B1 { background-position: -380px -680px; } +.emoji-1F5B2 { background-position: -400px -680px; } +.emoji-1F5B3 { background-position: -420px -680px; } +.emoji-1F5B4 { background-position: -440px -680px; } +.emoji-1F5B8 { background-position: -460px -680px; } +.emoji-1F5B9 { background-position: -480px -680px; } +.emoji-1F5BC { background-position: -500px -680px; } +.emoji-1F5BD { background-position: -520px -680px; } +.emoji-1F5BE { background-position: -540px -680px; } +.emoji-1F5C0 { background-position: -560px -680px; } +.emoji-1F5C1 { background-position: -580px -680px; } +.emoji-1F5C2 { background-position: -600px -680px; } +.emoji-1F5C3 { background-position: -620px -680px; } +.emoji-1F5C4 { background-position: -640px -680px; } +.emoji-1F5C6 { background-position: -660px -680px; } +.emoji-1F5C7 { background-position: -680px -680px; } +.emoji-1F5C9 { background-position: -700px 0px; } +.emoji-1F5CA { background-position: -700px -20px; } +.emoji-1F5CE { background-position: -700px -40px; } +.emoji-1F5CF { background-position: -700px -60px; } +.emoji-1F5D0 { background-position: -700px -80px; } +.emoji-1F5D1 { background-position: -700px -100px; } +.emoji-1F5D2 { background-position: -700px -120px; } +.emoji-1F5D3 { background-position: -700px -140px; } +.emoji-1F5D4 { background-position: -700px -160px; } +.emoji-1F5D8 { background-position: -700px -180px; } +.emoji-1F5D9 { background-position: -700px -200px; } +.emoji-1F5DC { background-position: -700px -220px; } +.emoji-1F5DD { background-position: -700px -240px; } +.emoji-1F5DE { background-position: -700px -260px; } +.emoji-1F5E0 { background-position: -700px -280px; } +.emoji-1F5E1 { background-position: -700px -300px; } +.emoji-1F5E2 { background-position: -700px -320px; } +.emoji-1F5E3 { background-position: -700px -340px; } +.emoji-1F5E8 { background-position: -700px -360px; } +.emoji-1F5E9 { background-position: -700px -380px; } +.emoji-1F5EA { background-position: -700px -400px; } +.emoji-1F5EB { background-position: -700px -420px; } +.emoji-1F5EC { background-position: -700px -440px; } +.emoji-1F5ED { background-position: -700px -460px; } +.emoji-1F5EE { background-position: -700px -480px; } +.emoji-1F5EF { background-position: -700px -500px; } +.emoji-1F5F0 { background-position: -700px -520px; } +.emoji-1F5F1 { background-position: -700px -540px; } +.emoji-1F5F2 { background-position: -700px -560px; } +.emoji-1F5F3 { background-position: -700px -580px; } +.emoji-1F5F4 { background-position: -700px -600px; } +.emoji-1F5F5 { background-position: -700px -620px; } +.emoji-1F5F8 { background-position: -700px -640px; } +.emoji-1F5F9 { background-position: -700px -660px; } +.emoji-1F5FA { background-position: -700px -680px; } +.emoji-1F5FB { background-position: 0px -700px; } +.emoji-1F5FC { background-position: -20px -700px; } +.emoji-1F5FD { background-position: -40px -700px; } +.emoji-1F5FE { background-position: -60px -700px; } +.emoji-1F5FF { background-position: -80px -700px; } +.emoji-1F600 { background-position: -100px -700px; } +.emoji-1F601 { background-position: -120px -700px; } +.emoji-1F602 { background-position: -140px -700px; } +.emoji-1F603 { background-position: -160px -700px; } +.emoji-1F604 { background-position: -180px -700px; } +.emoji-1F605 { background-position: -200px -700px; } +.emoji-1F606 { background-position: -220px -700px; } +.emoji-1F607 { background-position: -240px -700px; } +.emoji-1F608 { background-position: -260px -700px; } +.emoji-1F609 { background-position: -280px -700px; } +.emoji-1F60A { background-position: -300px -700px; } +.emoji-1F60B { background-position: -320px -700px; } +.emoji-1F60C { background-position: -340px -700px; } +.emoji-1F60D { background-position: -360px -700px; } +.emoji-1F60E { background-position: -380px -700px; } +.emoji-1F60F { background-position: -400px -700px; } +.emoji-1F610 { background-position: -420px -700px; } +.emoji-1F611 { background-position: -440px -700px; } +.emoji-1F612 { background-position: -460px -700px; } +.emoji-1F613 { background-position: -480px -700px; } +.emoji-1F614 { background-position: -500px -700px; } +.emoji-1F615 { background-position: -520px -700px; } +.emoji-1F616 { background-position: -540px -700px; } +.emoji-1F617 { background-position: -560px -700px; } +.emoji-1F618 { background-position: -580px -700px; } +.emoji-1F619 { background-position: -600px -700px; } +.emoji-1F61A { background-position: -620px -700px; } +.emoji-1F61B { background-position: -640px -700px; } +.emoji-1F61C { background-position: -660px -700px; } +.emoji-1F61D { background-position: -680px -700px; } +.emoji-1F61E { background-position: -700px -700px; } +.emoji-1F61F { background-position: -720px 0px; } +.emoji-1F620 { background-position: -720px -20px; } +.emoji-1F621 { background-position: -720px -40px; } +.emoji-1F622 { background-position: -720px -60px; } +.emoji-1F623 { background-position: -720px -80px; } +.emoji-1F624 { background-position: -720px -100px; } +.emoji-1F625 { background-position: -720px -120px; } +.emoji-1F626 { background-position: -720px -140px; } +.emoji-1F627 { background-position: -720px -160px; } +.emoji-1F628 { background-position: -720px -180px; } +.emoji-1F629 { background-position: -720px -200px; } +.emoji-1F62A { background-position: -720px -220px; } +.emoji-1F62B { background-position: -720px -240px; } +.emoji-1F62C { background-position: -720px -260px; } +.emoji-1F62D { background-position: -720px -280px; } +.emoji-1F62E { background-position: -720px -300px; } +.emoji-1F62F { background-position: -720px -320px; } +.emoji-1F630 { background-position: -720px -340px; } +.emoji-1F631 { background-position: -720px -360px; } +.emoji-1F632 { background-position: -720px -380px; } +.emoji-1F633 { background-position: -720px -400px; } +.emoji-1F634 { background-position: -720px -420px; } +.emoji-1F635 { background-position: -720px -440px; } +.emoji-1F636 { background-position: -720px -460px; } +.emoji-1F637 { background-position: -720px -480px; } +.emoji-1F638 { background-position: -720px -500px; } +.emoji-1F639 { background-position: -720px -520px; } +.emoji-1F63A { background-position: -720px -540px; } +.emoji-1F63B { background-position: -720px -560px; } +.emoji-1F63C { background-position: -720px -580px; } +.emoji-1F63D { background-position: -720px -600px; } +.emoji-1F63E { background-position: -720px -620px; } +.emoji-1F63F { background-position: -720px -640px; } +.emoji-1F640 { background-position: -720px -660px; } +.emoji-1F641 { background-position: -720px -680px; } +.emoji-1F642 { background-position: -720px -700px; } +.emoji-1F643 { background-position: 0px -720px; } +.emoji-1F644 { background-position: -20px -720px; } +.emoji-1F645 { background-position: -40px -720px; } +.emoji-1F645-1F3FB { background-position: -60px -720px; } +.emoji-1F645-1F3FC { background-position: -80px -720px; } +.emoji-1F645-1F3FD { background-position: -100px -720px; } +.emoji-1F645-1F3FE { background-position: -120px -720px; } +.emoji-1F645-1F3FF { background-position: -140px -720px; } +.emoji-1F646 { background-position: -160px -720px; } +.emoji-1F646-1F3FB { background-position: -180px -720px; } +.emoji-1F646-1F3FC { background-position: -200px -720px; } +.emoji-1F646-1F3FD { background-position: -220px -720px; } +.emoji-1F646-1F3FE { background-position: -240px -720px; } +.emoji-1F646-1F3FF { background-position: -260px -720px; } +.emoji-1F647 { background-position: -280px -720px; } +.emoji-1F647-1F3FB { background-position: -300px -720px; } +.emoji-1F647-1F3FC { background-position: -320px -720px; } +.emoji-1F647-1F3FD { background-position: -340px -720px; } +.emoji-1F647-1F3FE { background-position: -360px -720px; } +.emoji-1F647-1F3FF { background-position: -380px -720px; } +.emoji-1F648 { background-position: -400px -720px; } +.emoji-1F649 { background-position: -420px -720px; } +.emoji-1F64A { background-position: -440px -720px; } +.emoji-1F64B { background-position: -460px -720px; } +.emoji-1F64B-1F3FB { background-position: -480px -720px; } +.emoji-1F64B-1F3FC { background-position: -500px -720px; } +.emoji-1F64B-1F3FD { background-position: -520px -720px; } +.emoji-1F64B-1F3FE { background-position: -540px -720px; } +.emoji-1F64B-1F3FF { background-position: -560px -720px; } +.emoji-1F64C { background-position: -580px -720px; } +.emoji-1F64C-1F3FB { background-position: -600px -720px; } +.emoji-1F64C-1F3FC { background-position: -620px -720px; } +.emoji-1F64C-1F3FD { background-position: -640px -720px; } +.emoji-1F64C-1F3FE { background-position: -660px -720px; } +.emoji-1F64C-1F3FF { background-position: -680px -720px; } +.emoji-1F64D { background-position: -700px -720px; } +.emoji-1F64D-1F3FB { background-position: -720px -720px; } +.emoji-1F64D-1F3FC { background-position: -740px 0px; } +.emoji-1F64D-1F3FD { background-position: -740px -20px; } +.emoji-1F64D-1F3FE { background-position: -740px -40px; } +.emoji-1F64D-1F3FF { background-position: -740px -60px; } +.emoji-1F64E { background-position: -740px -80px; } +.emoji-1F64E-1F3FB { background-position: -740px -100px; } +.emoji-1F64E-1F3FC { background-position: -740px -120px; } +.emoji-1F64E-1F3FD { background-position: -740px -140px; } +.emoji-1F64E-1F3FE { background-position: -740px -160px; } +.emoji-1F64E-1F3FF { background-position: -740px -180px; } +.emoji-1F64F { background-position: -740px -200px; } +.emoji-1F64F-1F3FB { background-position: -740px -220px; } +.emoji-1F64F-1F3FC { background-position: -740px -240px; } +.emoji-1F64F-1F3FD { background-position: -740px -260px; } +.emoji-1F64F-1F3FE { background-position: -740px -280px; } +.emoji-1F64F-1F3FF { background-position: -740px -300px; } +.emoji-1F680 { background-position: -740px -320px; } +.emoji-1F681 { background-position: -740px -340px; } +.emoji-1F682 { background-position: -740px -360px; } +.emoji-1F683 { background-position: -740px -380px; } +.emoji-1F684 { background-position: -740px -400px; } +.emoji-1F685 { background-position: -740px -420px; } +.emoji-1F686 { background-position: -740px -440px; } +.emoji-1F687 { background-position: -740px -460px; } +.emoji-1F688 { background-position: -740px -480px; } +.emoji-1F689 { background-position: -740px -500px; } +.emoji-1F68A { background-position: -740px -520px; } +.emoji-1F68B { background-position: -740px -540px; } +.emoji-1F68C { background-position: -740px -560px; } +.emoji-1F68D { background-position: -740px -580px; } +.emoji-1F68E { background-position: -740px -600px; } +.emoji-1F68F { background-position: -740px -620px; } +.emoji-1F690 { background-position: -740px -640px; } +.emoji-1F691 { background-position: -740px -660px; } +.emoji-1F692 { background-position: -740px -680px; } +.emoji-1F693 { background-position: -740px -700px; } +.emoji-1F694 { background-position: -740px -720px; } +.emoji-1F695 { background-position: 0px -740px; } +.emoji-1F696 { background-position: -20px -740px; } +.emoji-1F697 { background-position: -40px -740px; } +.emoji-1F698 { background-position: -60px -740px; } +.emoji-1F699 { background-position: -80px -740px; } +.emoji-1F69A { background-position: -100px -740px; } +.emoji-1F69B { background-position: -120px -740px; } +.emoji-1F69C { background-position: -140px -740px; } +.emoji-1F69D { background-position: -160px -740px; } +.emoji-1F69E { background-position: -180px -740px; } +.emoji-1F69F { background-position: -200px -740px; } +.emoji-1F6A0 { background-position: -220px -740px; } +.emoji-1F6A1 { background-position: -240px -740px; } +.emoji-1F6A2 { background-position: -260px -740px; } +.emoji-1F6A3 { background-position: -280px -740px; } +.emoji-1F6A3-1F3FB { background-position: -300px -740px; } +.emoji-1F6A3-1F3FC { background-position: -320px -740px; } +.emoji-1F6A3-1F3FD { background-position: -340px -740px; } +.emoji-1F6A3-1F3FE { background-position: -360px -740px; } +.emoji-1F6A3-1F3FF { background-position: -380px -740px; } +.emoji-1F6A4 { background-position: -400px -740px; } +.emoji-1F6A5 { background-position: -420px -740px; } +.emoji-1F6A6 { background-position: -440px -740px; } +.emoji-1F6A7 { background-position: -460px -740px; } +.emoji-1F6A8 { background-position: -480px -740px; } +.emoji-1F6A9 { background-position: -500px -740px; } +.emoji-1F6AA { background-position: -520px -740px; } +.emoji-1F6AB { background-position: -540px -740px; } +.emoji-1F6AC { background-position: -560px -740px; } +.emoji-1F6AD { background-position: -580px -740px; } +.emoji-1F6AE { background-position: -600px -740px; } +.emoji-1F6AF { background-position: -620px -740px; } +.emoji-1F6B0 { background-position: -640px -740px; } +.emoji-1F6B1 { background-position: -660px -740px; } +.emoji-1F6B2 { background-position: -680px -740px; } +.emoji-1F6B3 { background-position: -700px -740px; } +.emoji-1F6B4 { background-position: -720px -740px; } +.emoji-1F6B4-1F3FB { background-position: -740px -740px; } +.emoji-1F6B4-1F3FC { background-position: -760px 0px; } +.emoji-1F6B4-1F3FD { background-position: -760px -20px; } +.emoji-1F6B4-1F3FE { background-position: -760px -40px; } +.emoji-1F6B4-1F3FF { background-position: -760px -60px; } +.emoji-1F6B5 { background-position: -760px -80px; } +.emoji-1F6B5-1F3FB { background-position: -760px -100px; } +.emoji-1F6B5-1F3FC { background-position: -760px -120px; } +.emoji-1F6B5-1F3FD { background-position: -760px -140px; } +.emoji-1F6B5-1F3FE { background-position: -760px -160px; } +.emoji-1F6B5-1F3FF { background-position: -760px -180px; } +.emoji-1F6B6 { background-position: -760px -200px; } +.emoji-1F6B6-1F3FB { background-position: -760px -220px; } +.emoji-1F6B6-1F3FC { background-position: -760px -240px; } +.emoji-1F6B6-1F3FD { background-position: -760px -260px; } +.emoji-1F6B6-1F3FE { background-position: -760px -280px; } +.emoji-1F6B6-1F3FF { background-position: -760px -300px; } +.emoji-1F6B7 { background-position: -760px -320px; } +.emoji-1F6B8 { background-position: -760px -340px; } +.emoji-1F6B9 { background-position: -760px -360px; } +.emoji-1F6BA { background-position: -760px -380px; } +.emoji-1F6BB { background-position: -760px -400px; } +.emoji-1F6BC { background-position: -760px -420px; } +.emoji-1F6BD { background-position: -760px -440px; } +.emoji-1F6BE { background-position: -760px -460px; } +.emoji-1F6BF { background-position: -760px -480px; } +.emoji-1F6C0 { background-position: -760px -500px; } +.emoji-1F6C0-1F3FB { background-position: -760px -520px; } +.emoji-1F6C0-1F3FC { background-position: -760px -540px; } +.emoji-1F6C0-1F3FD { background-position: -760px -560px; } +.emoji-1F6C0-1F3FE { background-position: -760px -580px; } +.emoji-1F6C0-1F3FF { background-position: -760px -600px; } +.emoji-1F6C1 { background-position: -760px -620px; } +.emoji-1F6C2 { background-position: -760px -640px; } +.emoji-1F6C3 { background-position: -760px -660px; } +.emoji-1F6C4 { background-position: -760px -680px; } +.emoji-1F6C5 { background-position: -760px -700px; } +.emoji-1F6C6 { background-position: -760px -720px; } +.emoji-1F6C7 { background-position: -760px -740px; } +.emoji-1F6C8 { background-position: 0px -760px; } +.emoji-1F6C9 { background-position: -20px -760px; } +.emoji-1F6CA { background-position: -40px -760px; } +.emoji-1F6CB { background-position: -60px -760px; } +.emoji-1F6CC { background-position: -80px -760px; } +.emoji-1F6CD { background-position: -100px -760px; } +.emoji-1F6CE { background-position: -120px -760px; } +.emoji-1F6CF { background-position: -140px -760px; } +.emoji-1F6D0 { background-position: -160px -760px; } +.emoji-1F6E0 { background-position: -180px -760px; } +.emoji-1F6E1 { background-position: -200px -760px; } +.emoji-1F6E2 { background-position: -220px -760px; } +.emoji-1F6E3 { background-position: -240px -760px; } +.emoji-1F6E4 { background-position: -260px -760px; } +.emoji-1F6E5 { background-position: -280px -760px; } +.emoji-1F6E6 { background-position: -300px -760px; } +.emoji-1F6E7 { background-position: -320px -760px; } +.emoji-1F6E8 { background-position: -340px -760px; } +.emoji-1F6E9 { background-position: -360px -760px; } +.emoji-1F6EA { background-position: -380px -760px; } +.emoji-1F6EB { background-position: -400px -760px; } +.emoji-1F6EC { background-position: -420px -760px; } +.emoji-1F6F0 { background-position: -440px -760px; } +.emoji-1F6F1 { background-position: -460px -760px; } +.emoji-1F6F2 { background-position: -480px -760px; } +.emoji-1F6F3 { background-position: -500px -760px; } +.emoji-1F910 { background-position: -520px -760px; } +.emoji-1F911 { background-position: -540px -760px; } +.emoji-1F912 { background-position: -560px -760px; } +.emoji-1F913 { background-position: -580px -760px; } +.emoji-1F914 { background-position: -600px -760px; } +.emoji-1F915 { background-position: -620px -760px; } +.emoji-1F916 { background-position: -640px -760px; } +.emoji-1F917 { background-position: -660px -760px; } +.emoji-1F918 { background-position: -680px -760px; } +.emoji-1F918-1F3FB { background-position: -700px -760px; } +.emoji-1F918-1F3FC { background-position: -720px -760px; } +.emoji-1F918-1F3FD { background-position: -740px -760px; } +.emoji-1F918-1F3FE { background-position: -760px -760px; } +.emoji-1F918-1F3FF { background-position: -780px 0px; } +.emoji-1F980 { background-position: -780px -20px; } +.emoji-1F981 { background-position: -780px -40px; } +.emoji-1F982 { background-position: -780px -60px; } +.emoji-1F983 { background-position: -780px -80px; } +.emoji-1F984 { background-position: -780px -100px; } +.emoji-1F9C0 { background-position: -780px -120px; } +.emoji-203C { background-position: -780px -140px; } +.emoji-2049 { background-position: -780px -160px; } +.emoji-2122 { background-position: -780px -180px; } +.emoji-2139 { background-position: -780px -200px; } +.emoji-2194 { background-position: -780px -220px; } +.emoji-2195 { background-position: -780px -240px; } +.emoji-2196 { background-position: -780px -260px; } +.emoji-2197 { background-position: -780px -280px; } +.emoji-2198 { background-position: -780px -300px; } +.emoji-2199 { background-position: -780px -320px; } +.emoji-21A9 { background-position: -780px -340px; } +.emoji-21AA { background-position: -780px -360px; } +.emoji-231A { background-position: -780px -380px; } +.emoji-231B { background-position: -780px -400px; } +.emoji-2328 { background-position: -780px -420px; } +.emoji-23E9 { background-position: -780px -440px; } +.emoji-23EA { background-position: -780px -460px; } +.emoji-23EB { background-position: -780px -480px; } +.emoji-23EC { background-position: -780px -500px; } +.emoji-23ED { background-position: -780px -520px; } +.emoji-23EE { background-position: -780px -540px; } +.emoji-23EF { background-position: -780px -560px; } +.emoji-23F0 { background-position: -780px -580px; } +.emoji-23F1 { background-position: -780px -600px; } +.emoji-23F2 { background-position: -780px -620px; } +.emoji-23F3 { background-position: -780px -640px; } +.emoji-23F8 { background-position: -780px -660px; } +.emoji-23F9 { background-position: -780px -680px; } +.emoji-23FA { background-position: -780px -700px; } +.emoji-24C2 { background-position: -780px -720px; } +.emoji-25AA { background-position: -780px -740px; } +.emoji-25AB { background-position: -780px -760px; } +.emoji-25B6 { background-position: 0px -780px; } +.emoji-25C0 { background-position: -20px -780px; } +.emoji-25FB { background-position: -40px -780px; } +.emoji-25FC { background-position: -60px -780px; } +.emoji-25FD { background-position: -80px -780px; } +.emoji-25FE { background-position: -100px -780px; } +.emoji-2600 { background-position: -120px -780px; } +.emoji-2601 { background-position: -140px -780px; } +.emoji-2602 { background-position: -160px -780px; } +.emoji-2603 { background-position: -180px -780px; } +.emoji-2604 { background-position: -200px -780px; } +.emoji-260E { background-position: -220px -780px; } +.emoji-2611 { background-position: -240px -780px; } +.emoji-2614 { background-position: -260px -780px; } +.emoji-2615 { background-position: -280px -780px; } +.emoji-2618 { background-position: -300px -780px; } +.emoji-261D { background-position: -320px -780px; } +.emoji-261D-1F3FB { background-position: -340px -780px; } +.emoji-261D-1F3FC { background-position: -360px -780px; } +.emoji-261D-1F3FD { background-position: -380px -780px; } +.emoji-261D-1F3FE { background-position: -400px -780px; } +.emoji-261D-1F3FF { background-position: -420px -780px; } +.emoji-2620 { background-position: -440px -780px; } +.emoji-2622 { background-position: -460px -780px; } +.emoji-2623 { background-position: -480px -780px; } +.emoji-2626 { background-position: -500px -780px; } +.emoji-262A { background-position: -520px -780px; } +.emoji-262E { background-position: -540px -780px; } +.emoji-262F { background-position: -560px -780px; } +.emoji-2638 { background-position: -580px -780px; } +.emoji-2639 { background-position: -600px -780px; } +.emoji-263A { background-position: -620px -780px; } +.emoji-2648 { background-position: -640px -780px; } +.emoji-2649 { background-position: -660px -780px; } +.emoji-264A { background-position: -680px -780px; } +.emoji-264B { background-position: -700px -780px; } +.emoji-264C { background-position: -720px -780px; } +.emoji-264D { background-position: -740px -780px; } +.emoji-264E { background-position: -760px -780px; } +.emoji-264F { background-position: -780px -780px; } +.emoji-2650 { background-position: -800px 0px; } +.emoji-2651 { background-position: -800px -20px; } +.emoji-2652 { background-position: -800px -40px; } +.emoji-2653 { background-position: -800px -60px; } +.emoji-2660 { background-position: -800px -80px; } +.emoji-2663 { background-position: -800px -100px; } +.emoji-2665 { background-position: -800px -120px; } +.emoji-2666 { background-position: -800px -140px; } +.emoji-2668 { background-position: -800px -160px; } +.emoji-267B { background-position: -800px -180px; } +.emoji-267F { background-position: -800px -200px; } +.emoji-2692 { background-position: -800px -220px; } +.emoji-2693 { background-position: -800px -240px; } +.emoji-2694 { background-position: -800px -260px; } +.emoji-2696 { background-position: -800px -280px; } +.emoji-2697 { background-position: -800px -300px; } +.emoji-2699 { background-position: -800px -320px; } +.emoji-269B { background-position: -800px -340px; } +.emoji-269C { background-position: -800px -360px; } +.emoji-26A0 { background-position: -800px -380px; } +.emoji-26A1 { background-position: -800px -400px; } +.emoji-26AA { background-position: -800px -420px; } +.emoji-26AB { background-position: -800px -440px; } +.emoji-26B0 { background-position: -800px -460px; } +.emoji-26B1 { background-position: -800px -480px; } +.emoji-26BD { background-position: -800px -500px; } +.emoji-26BE { background-position: -800px -520px; } +.emoji-26C4 { background-position: -800px -540px; } +.emoji-26C5 { background-position: -800px -560px; } +.emoji-26C8 { background-position: -800px -580px; } +.emoji-26CE { background-position: -800px -600px; } +.emoji-26CF { background-position: -800px -620px; } +.emoji-26D1 { background-position: -800px -640px; } +.emoji-26D3 { background-position: -800px -660px; } +.emoji-26D4 { background-position: -800px -680px; } +.emoji-26E9 { background-position: -800px -700px; } +.emoji-26EA { background-position: -800px -720px; } +.emoji-26F0 { background-position: -800px -740px; } +.emoji-26F1 { background-position: -800px -760px; } +.emoji-26F2 { background-position: -800px -780px; } +.emoji-26F3 { background-position: 0px -800px; } +.emoji-26F4 { background-position: -20px -800px; } +.emoji-26F5 { background-position: -40px -800px; } +.emoji-26F7 { background-position: -60px -800px; } +.emoji-26F8 { background-position: -80px -800px; } +.emoji-26F9 { background-position: -100px -800px; } +.emoji-26F9-1F3FB { background-position: -120px -800px; } +.emoji-26F9-1F3FC { background-position: -140px -800px; } +.emoji-26F9-1F3FD { background-position: -160px -800px; } +.emoji-26F9-1F3FE { background-position: -180px -800px; } +.emoji-26F9-1F3FF { background-position: -200px -800px; } +.emoji-26FA { background-position: -220px -800px; } +.emoji-26FD { background-position: -240px -800px; } +.emoji-2702 { background-position: -260px -800px; } +.emoji-2705 { background-position: -280px -800px; } +.emoji-2708 { background-position: -300px -800px; } +.emoji-2709 { background-position: -320px -800px; } +.emoji-270A { background-position: -340px -800px; } +.emoji-270A-1F3FB { background-position: -360px -800px; } +.emoji-270A-1F3FC { background-position: -380px -800px; } +.emoji-270A-1F3FD { background-position: -400px -800px; } +.emoji-270A-1F3FE { background-position: -420px -800px; } +.emoji-270A-1F3FF { background-position: -440px -800px; } +.emoji-270B { background-position: -460px -800px; } +.emoji-270B-1F3FB { background-position: -480px -800px; } +.emoji-270B-1F3FC { background-position: -500px -800px; } +.emoji-270B-1F3FD { background-position: -520px -800px; } +.emoji-270B-1F3FE { background-position: -540px -800px; } +.emoji-270B-1F3FF { background-position: -560px -800px; } +.emoji-270C { background-position: -580px -800px; } +.emoji-270C-1F3FB { background-position: -600px -800px; } +.emoji-270C-1F3FC { background-position: -620px -800px; } +.emoji-270C-1F3FD { background-position: -640px -800px; } +.emoji-270C-1F3FE { background-position: -660px -800px; } +.emoji-270C-1F3FF { background-position: -680px -800px; } +.emoji-270D { background-position: -700px -800px; } +.emoji-270D-1F3FB { background-position: -720px -800px; } +.emoji-270D-1F3FC { background-position: -740px -800px; } +.emoji-270D-1F3FD { background-position: -760px -800px; } +.emoji-270D-1F3FE { background-position: -780px -800px; } +.emoji-270D-1F3FF { background-position: -800px -800px; } +.emoji-270F { background-position: -820px 0px; } +.emoji-2712 { background-position: -820px -20px; } +.emoji-2714 { background-position: -820px -40px; } +.emoji-2716 { background-position: -820px -60px; } +.emoji-271D { background-position: -820px -80px; } +.emoji-2721 { background-position: -820px -100px; } +.emoji-2728 { background-position: -820px -120px; } +.emoji-2733 { background-position: -820px -140px; } +.emoji-2734 { background-position: -820px -160px; } +.emoji-2744 { background-position: -820px -180px; } +.emoji-2747 { background-position: -820px -200px; } +.emoji-274C { background-position: -820px -220px; } +.emoji-274E { background-position: -820px -240px; } +.emoji-2753 { background-position: -820px -260px; } +.emoji-2754 { background-position: -820px -280px; } +.emoji-2755 { background-position: -820px -300px; } +.emoji-2757 { background-position: -820px -320px; } +.emoji-2763 { background-position: -820px -340px; } +.emoji-2764 { background-position: -820px -360px; } +.emoji-2795 { background-position: -820px -380px; } +.emoji-2796 { background-position: -820px -400px; } +.emoji-2797 { background-position: -820px -420px; } +.emoji-27A1 { background-position: -820px -440px; } +.emoji-27B0 { background-position: -820px -460px; } +.emoji-27BF { background-position: -820px -480px; } +.emoji-2934 { background-position: -820px -500px; } +.emoji-2935 { background-position: -820px -520px; } +.emoji-2B05 { background-position: -820px -540px; } +.emoji-2B06 { background-position: -820px -560px; } +.emoji-2B07 { background-position: -820px -580px; } +.emoji-2B1B { background-position: -820px -600px; } +.emoji-2B1C { background-position: -820px -620px; } +.emoji-2B50 { background-position: -820px -640px; } +.emoji-2B55 { background-position: -820px -660px; } +.emoji-3030 { background-position: -820px -680px; } +.emoji-303D { background-position: -820px -700px; } +.emoji-3297 { background-position: -820px -720px; } +.emoji-3299 { background-position: -820px -740px; } + +.emoji-icon { + background-image: image-url('emoji.png'); + background-repeat: no-repeat; + height: 20px; + width: 20px; + + @media only screen and (-webkit-min-device-pixel-ratio: 2), + only screen and (min--moz-device-pixel-ratio: 2), + only screen and (-o-min-device-pixel-ratio: 2/1), + only screen and (min-device-pixel-ratio: 2), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 2dppx) { + background-image: image-url('emoji@2x.png'); + background-size: 840px 820px; + } +} diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 8fa15b3574..35df9a61c8 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -4,7 +4,7 @@ */ .event-item { font-size: $gl-font-size; - padding: $gl-padding 0 $gl-padding ($gl-avatar-size + 15px); + padding: $gl-padding-top 0 $gl-padding-top ($gl-avatar-size + $gl-padding-top); border-bottom: 1px solid $table-border-color; color: #7f8fa4; @@ -16,7 +16,7 @@ .event-title, .event-item-timestamp { - line-height: 44px; + line-height: 40px; } } @@ -25,7 +25,7 @@ } .avatar { - margin-left: -($gl-avatar-size + 15px); + margin-left: -($gl-avatar-size + $gl-padding-top); } .event-title { @@ -41,7 +41,6 @@ margin-right: 174px; .event-note { - margin-top: 5px; word-wrap: break-word; .md { @@ -98,8 +97,6 @@ &:last-child { border:none } .event_commits { - margin-top: 9px; - li { &.commit { background: transparent; diff --git a/app/assets/stylesheets/pages/explore.scss b/app/assets/stylesheets/pages/explore.scss index da06fe9954..9b92128624 100644 --- a/app/assets/stylesheets/pages/explore.scss +++ b/app/assets/stylesheets/pages/explore.scss @@ -6,11 +6,3 @@ font-size: 30px; } } - -.explore-trending-block { - .lead { - line-height: 32px; - font-size: 18px; - margin-top: 10px; - } -} diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 263993f59a..ec6c099df5 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -1,5 +1,15 @@ .member-search-form { float: left; + + input[type='search'] { + width: 225px; + vertical-align: bottom; + + @media (max-width: $screen-xs-max) { + width: 100px; + vertical-align: bottom; + } + } } .milestone-row { @@ -11,3 +21,21 @@ height: 42px; } } + +.group-row { + &.no-description { + .group-name { + line-height: 44px; + } + } + + .stats { + float: right; + line-height: 44px; + color: $gl-gray; + + span { + margin-right: 15px; + } + } +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 977ada0ff3..b61d1f180b 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -29,21 +29,8 @@ } } -.project-issuable-filter { - .controls { - float: right; - margin-top: 11px; - } - - .nav-links { - text-align: left; - } -} - .issuable-details { section { - border-right: 1px solid $border-white-light; - .issuable-discussion { margin-right: 1px; } @@ -73,11 +60,41 @@ .block { @include clearfix; padding: $gl-padding 0; - border-bottom: 1px solid #F0F0F0; + border-bottom: 1px solid $border-gray-light; + // This prevents the mess when resizing the sidebar + // of elements repositioning themselves.. + width: $gutter_inner_width; + // -- + + &:first-child { + padding-top: 5px; + } &:last-child { border: none; } + + span { + margin-top: 7px; + display: inline-block; + } + + .select2-container span { + margin-top: 0; + } + + .issuable-count { + + } + + .gutter-toggle { + margin-left: 20px; + padding-left: 10px; + + &:hover { + color: $gray-darkest; + } + } } .title { @@ -133,3 +150,105 @@ margin-right: 2px; } } + + +.right-sidebar { + position: fixed; + top: 58px; + bottom: 0; + right: 0; + transition: width .3s; + background: $gray-light; + padding: 10px 20px; + + &.right-sidebar-expanded { + width: $gutter_width; + + hr { + display: none; + } + + .sidebar-collapsed-icon { + display: none; + } + + .gutter-toggle { + border-left: 1px solid $border-gray-light; + } + } + + .subscribe-button { + span { + margin-top: 0; + } + } + + &.right-sidebar-collapsed { + width: $sidebar_collapsed_width; + padding-top: 0; + + hr { + margin: 0; + color: $gray-normal; + border-color: $gray-normal; + width: 62px; + margin-left: -20px + } + + .block { + width: $sidebar_collapsed_width - 1px; + margin-left: -19px; + padding: 15px 0 0 0; + border-bottom: none; + overflow: hidden; + } + + .hide-collapsed { + display: none; + } + + .gutter-toggle { + margin-left: -36px; + } + + .sidebar-collapsed-icon { + display: block; + width: 100%; + text-align: center; + padding-bottom: 10px; + color: #999999; + + span { + display: block; + margin-top: 0; + } + + .btn-clipboard { + border: none; + + &:hover { + background: transparent; + } + + i { + color: #999999; + } + } + } + } + + .btn { + background: $gray-normal; + border: 1px solid $border-gray-normal; + &:hover { + background: $gray-dark; + border: 1px solid $border-gray-dark; + } + } +} + +.detail-page-description { + small { + color: $gray-darkest; + } +} diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index ad92cc2281..8694bd654a 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -49,11 +49,6 @@ .issue-search-form { margin: 0; height: 24px; - - .issue_search { - border: 1px solid #DDD !important; - background-color: #f4f4f4; - } } form.edit-issue { @@ -70,10 +65,6 @@ form.edit-issue { width: 3em; } -.merge-request-info { - padding-left: 5px; -} - .merge-request-status { color: $gl-gray; font-size: 15px; @@ -148,4 +139,4 @@ form.edit-issue { .issue-closed-by-widget { color: $secondary-text; margin-left: 52px; -} \ No newline at end of file +} diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index d1590e42fc..1c78aafdb8 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -9,7 +9,7 @@ } } -.manage-labels-list { +.label-row { .label { padding: 9px; font-size: 14px; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 75f2ae80a9..9a2c4b83ff 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -201,3 +201,39 @@ .mr-source-target { line-height: 31px; } + +.disabled-comment-area { + padding: 16px 0; + + .disabled-profile { + width: 40px; + height: 40px; + background: $border-gray-dark; + border-radius: 20px; + display: inline-block; + margin-right: 10px; + } + + .disabled-comment { + background: $gray-light; + display: inline-block; + vertical-align: top; + height: 200px; + border-radius: 4px; + border: 1px solid $border-gray-normal; + padding-top: 90px; + text-align: center; + right: 20px; + position: absolute; + left: 70px; + margin-bottom: 20px; + + span { + color: #B2B2B2; + + a { + color: $md-link-color; + } + } + } +} diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index e80dc9e84a..d24adbf67e 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -11,3 +11,60 @@ li.milestone { height: 6px; } } + +.milestone-content { + .issues-count { + margin-right: 17px; + float: right; + width: 105px; + } + + .issue-row { + .color-label { + border-radius: 2px; + padding: 3px !important; + } + + // Issue title + span a { + color: rgba(0,0,0,0.64); + } + } +} + +.milestone-summary { + margin-bottom: 25px; + + .milestone-stat { + margin-right: 10px; + } + + .remaining-days { + color: $orange-light; + } +} + +.issues-sortable-list { + .issue-detail { + display: block; + + .issue-number{ + color: rgba(0,0,0,0.44); + margin-right: 5px; + } + .color-label { + padding: 6px 10px; + margin-right: 7px; + margin-top: 10px; + } + + .avatar { + float: none; + } + } +} + +.milestone-detail { + border-bottom: 1px solid $border-color; + padding: 20px 0; +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 13b0ed769f..542ac896f6 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -73,31 +73,26 @@ font-weight: normal; } + .visibility-icon { + display: inline-block; + margin-left: 5px; + font-size: 18px; + color: $gray; + } + p { padding: 0 $gl-padding; color: #5c5d5e; } } - .visibility-level-label { - @extend .btn; - @extend .btn-gray; - - color: $gray; - cursor: default; - - i { - color: inherit; - } - } - .project-repo-buttons { - margin-top: 12px; + margin-top: 20px; margin-bottom: 0px; .count-buttons { display: block; - margin-bottom: 12px; + margin-bottom: 20px; } .clone-row { @@ -163,7 +158,7 @@ line-height: 13px; padding: $gl-vert-padding $gl-padding; letter-spacing: .4px; - padding: 10px; + padding: 10px 14px; text-align: center; vertical-align: middle; touch-action: manipulation; @@ -281,36 +276,6 @@ margin-top: -1px; } -.top-area { - border-bottom: 1px solid #EEE; - - ul.nav-links { - display: inline-block; - width: 50%; - margin-bottom: 0px; - border-bottom: none; - } - - .projects-search-form { - width: 50%; - display: inline-block; - float: right; - padding-top: 11px; - text-align: right; - - .btn-green { - margin-left: 10px; - float: right; - } - } - - @media (max-width: $screen-xs-max) { - .projects-search-form { - padding-top: 15px; - } - } -} - .fork-namespaces { .fork-thumbnail { text-align: center; @@ -386,22 +351,6 @@ pre.light-well { border-color: #f1f1f1; } -.projects-search-form { - padding: $gl-padding 0; - padding-bottom: 0; - margin-bottom: 0px; - - input { - display: inline-block; - width: calc(100% - 151px); - } - - .btn { - display: inline-block; - width: 135px; - } -} - .git-empty { margin: 0 7px 0 7px; @@ -437,12 +386,11 @@ pre.light-well { @include basic-list; .project-row { - padding: $gl-padding 0; border-color: $table-border-color; &.no-description { .project { - line-height: 44px; + line-height: 40px; } } @@ -455,12 +403,16 @@ pre.light-well { .project-controls { float: right; color: $gl-gray; - line-height: 45px; + line-height: 40px; color: #7f8fa4; a:hover { text-decoration: none; } + + > span { + margin-left: 10px; + } } .project-description { @@ -558,3 +510,19 @@ pre.light-well { width: 101%; } } + +.cannot-be-merged, +.cannot-be-merged:hover { + color: #E62958; + margin-top: 2px; +} + +.private-forks-notice .private-fork-icon { + i:nth-child(1) { + color: #2AA056; + } + + i:nth-child(2) { + color: #FFFFFF; + } +} diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss new file mode 100644 index 0000000000..2f57f21963 --- /dev/null +++ b/app/assets/stylesheets/pages/todos.scss @@ -0,0 +1,124 @@ +/** + * Dashboard Todos + * + */ + +.navbar-nav { + li { + .badge.todos-pending-count { + background-color: #7f8fa4; + margin-top: -5px; + } + } +} + +.todos { + .panel { + border-top: none; + margin-bottom: 0; + } +} + +.todo-item { + font-size: $gl-font-size; + padding: $gl-padding-top 0 $gl-padding-top ($gl-avatar-size + $gl-padding-top); + border-bottom: 1px solid $table-border-color; + color: #7f8fa4; + + &.todo-inline { + .avatar { + position: relative; + top: -2px; + } + + .todo-title { + line-height: 40px; + } + } + + a { + color: #4c4e54; + } + + .avatar { + margin-left: -($gl-avatar-size + $gl-padding-top); + } + + .todo-title { + @include str-truncated(calc(100% - 174px)); + font-weight: 600; + + .author_name { + color: #333; + } + } + + .todo-body { + margin-right: 174px; + + .todo-note { + word-wrap: break-word; + + .md { + color: #7f8fa4; + font-size: $gl-font-size; + + p { + color: #5c5d5e; + } + } + + pre { + border: none; + background: #f9f9f9; + border-radius: 0; + color: #777; + margin: 0 20px; + overflow: hidden; + } + + .note-image-attach { + margin-top: 4px; + margin-left: 0px; + max-width: 200px; + float: none; + } + + p:last-child { + margin-bottom: 0; + } + } + + .todo-note-icon { + color: #777; + float: left; + font-size: $gl-font-size; + line-height: 16px; + margin-right: 5px; + } + } + + &:last-child { border:none } +} + +@media (max-width: $screen-xs-max) { + .todo-item { + padding-left: $gl-padding; + + .todo-title { + white-space: normal; + overflow: visible; + max-width: 100%; + } + + .avatar { + display: none; + } + + .todo-body { + margin: 0; + border-left: 2px solid #DDD; + padding-left: 10px; + } + } +} diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index c7411617cb..ef63b01060 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -21,7 +21,7 @@ &:hover { td { - background: $hover; + background: $row-hover; } cursor: pointer; } diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index cdf514197c..dfaeba41cf 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -4,8 +4,3 @@ margin-right: auto; padding-right: 7px; } - -.wiki-last-edit-by { - font-size: 80%; - font-weight: normal; -} diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb new file mode 100644 index 0000000000..26cf74e484 --- /dev/null +++ b/app/controllers/admin/appearances_controller.rb @@ -0,0 +1,57 @@ +class Admin::AppearancesController < Admin::ApplicationController + before_action :set_appearance, except: :create + + def show + end + + def preview + end + + def create + @appearance = Appearance.new(appearance_params) + + if @appearance.save + redirect_to admin_appearances_path, notice: 'Appearance was successfully created.' + else + render action: 'show' + end + end + + def update + if @appearance.update(appearance_params) + redirect_to admin_appearances_path, notice: 'Appearance was successfully updated.' + else + render action: 'show' + end + end + + def logo + @appearance.remove_logo! + + @appearance.save + + redirect_to admin_appearances_path, notice: 'Logo was succesfully removed.' + end + + def header_logos + @appearance.remove_header_logo! + @appearance.save + + redirect_to admin_appearances_path, notice: 'Header logo was succesfully removed.' + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_appearance + @appearance = Appearance.last || Appearance.new + end + + # Only allow a trusted parameter "white list" through. + def appearance_params + params.require(:appearance).permit( + :title, :description, :logo, :logo_cache, :header_logo, :header_logo_cache, + :updated_by + ) + end +end diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 094eef28a4..04a99d8c84 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -74,13 +74,14 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :metrics_timeout, :metrics_method_call_threshold, :metrics_sample_interval, - :ip_blocking_enabled, - :dnsbl_servers_list, :recaptcha_enabled, :recaptcha_site_key, :recaptcha_private_key, :sentry_enabled, :sentry_dsn, + :akismet_enabled, + :akismet_api_key, + :email_author_in_body, restricted_visibility_levels: [], import_sources: [] ) diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb index 4735b27c65..fc34292498 100644 --- a/app/controllers/admin/broadcast_messages_controller.rb +++ b/app/controllers/admin/broadcast_messages_controller.rb @@ -2,7 +2,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController before_action :finder, only: [:edit, :update, :destroy] def index - @broadcast_messages = BroadcastMessage.reorder("starts_at ASC").page(params[:page]) + @broadcast_messages = BroadcastMessage.reorder("ends_at DESC").page(params[:page]) @broadcast_message = BroadcastMessage.new end @@ -36,6 +36,10 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController end end + def preview + @message = broadcast_message_params[:message] + end + protected def finder diff --git a/app/controllers/admin/labels_controller.rb b/app/controllers/admin/labels_controller.rb index 3b070e65d0..d79ce2b10f 100644 --- a/app/controllers/admin/labels_controller.rb +++ b/app/controllers/admin/labels_controller.rb @@ -53,6 +53,6 @@ class Admin::LabelsController < Admin::ApplicationController end def label_params - params[:label].permit(:title, :color) + params[:label].permit(:title, :description, :color) end end diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb new file mode 100644 index 0000000000..377e9741e5 --- /dev/null +++ b/app/controllers/admin/spam_logs_controller.rb @@ -0,0 +1,17 @@ +class Admin::SpamLogsController < Admin::ApplicationController + def index + @spam_logs = SpamLog.order(id: :desc).page(params[:page]) + end + + def destroy + spam_log = SpamLog.find(params[:id]) + + if params[:remove_user] + spam_log.remove_user + redirect_to admin_spam_logs_path, notice: "User #{spam_log.user.username} was successfully removed." + else + spam_log.destroy + render nothing: true + end + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2d735b9059..1f55b18e0b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -25,7 +25,7 @@ class ApplicationController < ActionController::Base helper_method :abilities, :can?, :current_application_settings helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled? - helper_method :repository + helper_method :repository, :can_collaborate_with_project? rescue_from Encoding::CompatibilityError do |exception| log_exception(exception) @@ -60,6 +60,8 @@ class ApplicationController < ActionController::Base params[:authenticity_token].presence elsif params[:private_token].presence params[:private_token].presence + elsif request.headers['PRIVATE-TOKEN'].present? + request.headers['PRIVATE-TOKEN'] end user = user_token && User.find_by_authentication_token(user_token.to_s) @@ -162,7 +164,7 @@ class ApplicationController < ActionController::Base end def git_not_found! - render html: "errors/git_not_found", layout: "errors", status: 404 + render "errors/git_not_found.html", layout: "errors", status: 404 end def method_missing(method_sym, *arguments, &block) @@ -244,6 +246,8 @@ class ApplicationController < ActionController::Base def ldap_security_check if current_user && current_user.requires_ldap_check? + return unless current_user.try_obtain_ldap_lease + unless Gitlab::LDAP::Access.allowed?(current_user) sign_out current_user flash[:alert] = "Access denied for your LDAP account." @@ -275,9 +279,10 @@ class ApplicationController < ActionController::Base } end - def view_to_html_string(partial) + def view_to_html_string(partial, locals = {}) render_to_string( partial, + locals: locals, layout: false, formats: [:html] ) @@ -298,7 +303,8 @@ class ApplicationController < ActionController::Base end def set_filters_params - params[:sort] ||= 'id_desc' + set_default_sort + params[:scope] = 'all' if params[:scope].blank? params[:state] = 'opened' if params[:state].blank? @@ -405,4 +411,31 @@ class ApplicationController < ActionController::Base current_user.nil? && root_path == request.path end + + def can_collaborate_with_project?(project = nil) + project ||= @project + + can?(current_user, :push_code, project) || + (current_user && current_user.already_forked?(project)) + end + + private + + def set_default_sort + key = if is_a_listing_page_for?('issues') || is_a_listing_page_for?('merge_requests') + 'issuable_sort' + end + + cookies[key] = params[:sort] if key && params[:sort].present? + params[:sort] = cookies[key] if key + params[:sort] ||= 'id_desc' + end + + def is_a_listing_page_for?(page_type) + controller_name, action_name = params.values_at(:controller, :action) + + (controller_name == "projects/#{page_type}" && action_name == 'index') || + (controller_name == 'groups' && action_name == page_type) || + (controller_name == 'dashboard' && action_name == page_type) + end end diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb index c420b59c3a..5bb7d499cd 100644 --- a/app/controllers/ci/application_controller.rb +++ b/app/controllers/ci/application_controller.rb @@ -3,52 +3,5 @@ module Ci def self.railtie_helpers_paths "app/helpers/ci" end - - private - - def authorize_access_project! - unless can?(current_user, :read_project, project) - return page_404 - end - end - - def authorize_manage_builds! - unless can?(current_user, :manage_builds, project) - return page_404 - end - end - - def authenticate_admin! - return render_404 unless current_user.is_admin? - end - - def authorize_manage_project! - unless can?(current_user, :admin_project, project) - return page_404 - end - end - - def page_404 - render file: "#{Rails.root}/public/404.html", status: 404, layout: false - end - - def default_headers - headers['X-Frame-Options'] = 'DENY' - headers['X-XSS-Protection'] = '1; mode=block' - end - - # JSON for infinite scroll via Pager object - def pager_json(partial, count) - html = render_to_string( - partial, - layout: false, - formats: [:html] - ) - - render json: { - html: html, - count: count - } - end end end diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index 3004c2d27f..081e01a75e 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -1,9 +1,9 @@ module Ci class ProjectsController < Ci::ApplicationController - before_action :project, except: [:index] - before_action :authenticate_user!, except: [:index, :build, :badge] - before_action :authorize_access_project!, except: [:index, :badge] + before_action :project + before_action :authorize_read_project!, except: [:badge] before_action :no_cache, only: [:badge] + skip_before_action :authenticate_user!, only: [:badge] protect_from_forgery def show @@ -13,9 +13,14 @@ module Ci # Project status badge # Image with build status for sha or ref + # + # This action in DEPRECATED, this is here only for backwards compatibility + # with projects migrated from GitLab CI. + # def badge - image = Ci::ImageForBuildService.new.execute(@project, params) + return render_404 unless @project + image = Ci::ImageForBuildService.new.execute(@project, params) send_file image.path, filename: image.name, disposition: 'inline', type:"image/svg+xml" end diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index b9eb0a22f8..787416c17a 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -13,17 +13,11 @@ module CreatesCommit result = service.new(@tree_edit_project, current_user, commit_params).execute if result[:status] == :success - flash[:notice] = success_notice || "Your changes have been successfully committed." - - if create_merge_request? - success_path = new_merge_request_path - target = different_project? ? "project" : "branch" - flash[:notice] << " You can now submit a merge request to get this change into the original #{target}." - end + update_flash_notice(success_notice) respond_to do |format| - format.html { redirect_to success_path } - format.json { render json: { message: "success", filePath: success_path } } + format.html { redirect_to final_success_path(success_path) } + format.json { render json: { message: "success", filePath: final_success_path(success_path) } } end else flash[:alert] = result[:message] @@ -41,14 +35,32 @@ module CreatesCommit end def authorize_edit_tree! - return if can?(current_user, :push_code, project) - return if current_user && current_user.already_forked?(project) + return if can_collaborate_with_project? access_denied! end private + def update_flash_notice(success_notice) + flash[:notice] = success_notice || "Your changes have been successfully committed." + + if create_merge_request? + if merge_request_exists? + flash[:notice] = nil + else + target = different_project? ? "project" : "branch" + flash[:notice] << " You can now submit a merge request to get this change into the original #{target}." + end + end + end + + def final_success_path(success_path) + return success_path unless create_merge_request? + + merge_request_exists? ? existing_merge_request_path : new_merge_request_path + end + def new_merge_request_path new_namespace_project_merge_request_path( @mr_source_project.namespace, @@ -62,6 +74,19 @@ module CreatesCommit ) end + def existing_merge_request_path + namespace_project_merge_request_path(@mr_target_project.namespace, @mr_target_project, @merge_request) + end + + def merge_request_exists? + return @merge_request if defined?(@merge_request) + + @merge_request = @mr_target_project.merge_requests.opened.find_by( + source_branch: @mr_source_branch, + target_branch: @mr_target_branch + ) + end + def different_project? @mr_source_project != @mr_target_project end @@ -75,7 +100,7 @@ module CreatesCommit end def set_commit_variables - @mr_source_branch = @target_branch + @mr_source_branch ||= @target_branch if can?(current_user, :push_code, @project) # Edit file in this project @@ -89,7 +114,7 @@ module CreatesCommit else # Merge request to this project @mr_target_project = @project - @mr_target_branch = @ref + @mr_target_branch ||= @ref end else # Edit file in fork @@ -97,7 +122,7 @@ module CreatesCommit # Merge request from fork to this project @mr_source_project = @tree_edit_project @mr_target_project = @project - @mr_target_branch = @ref + @mr_target_branch ||= @ref end end end diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb index effd472194..5b09862855 100644 --- a/app/controllers/concerns/issues_action.rb +++ b/app/controllers/concerns/issues_action.rb @@ -6,6 +6,8 @@ module IssuesAction @issues = @issues.page(params[:page]).per(ApplicationController::PER_PAGE) @issues = @issues.preload(:author, :project) + @label = @issuable_finder.labels.first + respond_to do |format| format.html format.atom { render layout: false } diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb index f7a25111db..f6de696e84 100644 --- a/app/controllers/concerns/merge_requests_action.rb +++ b/app/controllers/concerns/merge_requests_action.rb @@ -5,5 +5,7 @@ module MergeRequestsAction @merge_requests = get_merge_requests_collection @merge_requests = @merge_requests.page(params[:page]).per(ApplicationController::PER_PAGE) @merge_requests = @merge_requests.preload(:author, :target_project) + + @label = @issuable_finder.labels.first end end diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index 58e9049f15..59f29473e9 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -3,7 +3,16 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController def index @projects = current_user.authorized_projects.sorted_by_activity.non_archived + @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.includes(:namespace) + + terms = params[:filter_projects] + + if terms.present? + @projects = @projects.search(terms) + end + + @projects = @projects.page(params[:page]).per(PER_PAGE) @last_push = current_user.recent_push respond_to do |format| @@ -13,6 +22,11 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController load_events render layout: false end + format.json do + render json: { + html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects }) + } + end end end @@ -20,6 +34,14 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController @projects = current_user.starred_projects @projects = @projects.includes(:namespace, :forked_from_project, :tags) @projects = @projects.sort(@sort = params[:sort]) + + terms = params[:filter_projects] + + if terms.present? + @projects = @projects.search(terms) + end + + @projects = @projects.page(params[:page]).per(PER_PAGE) @last_push = current_user.recent_push @groups = [] @@ -27,8 +49,9 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController format.html format.json do - load_events - pager_json("events/_events", @events.count) + render json: { + html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects }) + } end end end @@ -36,7 +59,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController private def load_events - @events = Event.in_projects(@projects.pluck(:id)) + @events = Event.in_projects(@projects) @events = @event_filter.apply_filter(@events).with_associations @events = @events.limit(20).offset(params[:offset] || 0) end diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb new file mode 100644 index 0000000000..43cf8fa71a --- /dev/null +++ b/app/controllers/dashboard/todos_controller.rb @@ -0,0 +1,35 @@ +class Dashboard::TodosController < Dashboard::ApplicationController + before_action :find_todos, only: [:index, :destroy_all] + + def index + @todos = @todos.page(params[:page]).per(PER_PAGE) + end + + def destroy + todo.done! + + respond_to do |format| + format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' } + format.js { render nothing: true } + end + end + + def destroy_all + @todos.each(&:done!) + + respond_to do |format| + format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' } + format.js { render nothing: true } + end + end + + private + + def todo + @todo ||= current_user.todos.find(params[:id]) + end + + def find_todos + @todos = TodosFinder.new(current_user, params).execute + end +end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 087da93508..139e40db18 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -23,14 +23,14 @@ class DashboardController < Dashboard::ApplicationController protected def load_events - project_ids = + projects = if params[:filter] == "starred" current_user.starred_projects else current_user.authorized_projects - end.pluck(:id) + end - @events = Event.in_projects(project_ids) + @events = Event.in_projects(projects) @events = @event_filter.apply_filter(@events).with_associations @events = @events.limit(20).offset(params[:offset] || 0) end diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index a5aeaed66c..317ad83500 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -6,19 +6,49 @@ class Explore::ProjectsController < Explore::ApplicationController @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? @projects = @projects.non_archived @projects = @projects.search(params[:search]) if params[:search].present? + @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present? @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE) + + respond_to do |format| + format.html + format.json do + render json: { + html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects }) + } + end + end end def trending - @trending_projects = TrendingProjectsFinder.new.execute(current_user) - @trending_projects = @trending_projects.non_archived - @trending_projects = @trending_projects.page(params[:page]).per(PER_PAGE) + @projects = TrendingProjectsFinder.new.execute(current_user) + @projects = @projects.non_archived + @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present? + @projects = @projects.page(params[:page]).per(PER_PAGE) + + respond_to do |format| + format.html + format.json do + render json: { + html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects }) + } + end + end end def starred - @starred_projects = ProjectsFinder.new.execute(current_user) - @starred_projects = @starred_projects.reorder('star_count DESC') - @starred_projects = @starred_projects.page(params[:page]).per(PER_PAGE) + @projects = ProjectsFinder.new.execute(current_user) + @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present? + @projects = @projects.reorder('star_count DESC') + @projects = @projects.page(params[:page]).per(PER_PAGE) + + respond_to do |format| + format.html + format.json do + render json: { + html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects }) + } + end + end end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index fb26a4e6fc..ca5ce1e204 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -2,18 +2,19 @@ class GroupsController < Groups::ApplicationController include IssuesAction include MergeRequestsAction - skip_before_action :authenticate_user!, only: [:show, :issues, :merge_requests] respond_to :html - before_action :group, except: [:new, :create] + + skip_before_action :authenticate_user!, only: [:index, :show, :issues, :merge_requests] + before_action :group, except: [:index, :new, :create] # Authorize - before_action :authorize_read_group!, except: [:show, :new, :create, :autocomplete] + before_action :authorize_read_group!, except: [:index, :show, :new, :create, :autocomplete] before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects] before_action :authorize_create_group!, only: [:new, :create] # Load group projects - before_action :load_projects, except: [:new, :create, :projects, :edit, :update, :autocomplete] - before_action :event_filter, only: :show + before_action :load_projects, except: [:index, :new, :create, :projects, :edit, :update, :autocomplete] + before_action :event_filter, only: [:show, :events] layout :determine_layout @@ -40,13 +41,16 @@ class GroupsController < Groups::ApplicationController def show @last_push = current_user.recent_push if current_user @projects = @projects.includes(:namespace) + @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present? + @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? respond_to do |format| format.html format.json do - load_events - pager_json("events/_events", @events.count) + render json: { + html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects }) + } end format.atom do @@ -56,6 +60,15 @@ class GroupsController < Groups::ApplicationController end end + def events + respond_to do |format| + format.json do + load_events + pager_json("events/_events", @events.count) + end + end + end + def edit end @@ -81,16 +94,13 @@ class GroupsController < Groups::ApplicationController def group @group ||= Group.find_by(path: params[:id]) + @group || render_404 end def load_projects @projects ||= ProjectsFinder.new.execute(current_user, group: group).sorted_by_activity.non_archived end - def project_ids - @projects.pluck(:id) - end - # Dont allow unauthorized access to group def authorize_read_group! unless @group and (@projects.present? or can?(current_user, :read_group, @group)) @@ -123,7 +133,7 @@ class GroupsController < Groups::ApplicationController end def load_events - @events = Event.in_projects(project_ids) + @events = Event.in_projects(@projects) @events = event_filter.apply_filter(@events).with_associations @events = @events.limit(20).offset(params[:offset] || 0) end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 4c13228fce..21135f7d60 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -1,4 +1,5 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController + include AuthenticatesWithTwoFactor protect_from_forgery except: [:kerberos, :saml, :cas3] @@ -29,14 +30,38 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController # Do additional LDAP checks for the user filter and EE features if ldap_user.allowed? - log_audit_event(@user, with: :ldap) - sign_in_and_redirect(@user) + if @user.two_factor_enabled? + prompt_for_two_factor(@user) + else + log_audit_event(@user, with: :ldap) + sign_in_and_redirect(@user) + end else flash[:alert] = "Access denied for your LDAP account." redirect_to new_user_session_path end end + def saml + if current_user + log_audit_event(current_user, with: :saml) + # Update SAML identity if data has changed. + identity = current_user.identities.find_by(extern_uid: oauth['uid'], provider: :saml) + if identity.nil? + current_user.identities.create(extern_uid: oauth['uid'], provider: :saml) + redirect_to profile_account_path, notice: 'Authentication method updated' + else + redirect_to after_sign_in_path_for(current_user) + end + else + saml_user = Gitlab::Saml::User.new(oauth) + saml_user.save + @user = saml_user.gl_user + + continue_login_process + end + end + def omniauth_error @provider = params[:provider] @error = params[:error] @@ -60,25 +85,11 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController log_audit_event(current_user, with: oauth['provider']) redirect_to profile_account_path, notice: 'Authentication method updated' else - @user = Gitlab::OAuth::User.new(oauth) - @user.save + oauth_user = Gitlab::OAuth::User.new(oauth) + oauth_user.save + @user = oauth_user.gl_user - # Only allow properly saved users to login. - if @user.persisted? && @user.valid? - log_audit_event(@user.gl_user, with: oauth['provider']) - sign_in_and_redirect(@user.gl_user) - else - error_message = - if @user.gl_user.errors.any? - @user.gl_user.errors.map do |attribute, message| - "#{attribute} #{message}" - end.join(", ") - else - '' - end - - redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return - end + continue_login_process end rescue Gitlab::OAuth::SignupDisabledError label = Gitlab::OAuth::Provider.label_for(oauth['provider']) @@ -99,6 +110,18 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController session[:service_tickets][provider] = ticket end + def continue_login_process + # Only allow properly saved users to login. + if @user.persisted? && @user.valid? + log_audit_event(@user, with: oauth['provider']) + sign_in_and_redirect(@user) + else + error_message = @user.errors.full_messages.to_sentence + + redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return + end + end + def oauth @oauth ||= request.env['omniauth.auth'] end diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 6e91d9b4ad..8f83fdd02b 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -12,11 +12,13 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController current_user.save! if current_user.changed? - if two_factor_grace_period_expired? - flash.now[:alert] = 'You must configure Two-Factor Authentication in your account.' - else - grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours - flash.now[:alert] = "You must configure Two-Factor Authentication in your account until #{l(grace_period_deadline)}." + if two_factor_authentication_required? + if two_factor_grace_period_expired? + flash.now[:alert] = 'You must enable Two-factor Authentication for your account.' + else + grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours + flash.now[:alert] = "You must enable Two-factor Authentication for your account before #{l(grace_period_deadline)}." + end end @qr_code = build_qr_code diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index dd32d50919..a326bc5821 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -28,6 +28,11 @@ class Projects::ApplicationController < ApplicationController private + def apply_diff_view_cookie! + view = params[:view] || cookies[:diff_view] + cookies.permanent[:diff_view] = params[:view] = view if view + end + def builds_enabled return render_404 unless @project.builds_enabled? end diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index f159a6d6dc..cfea126651 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -1,6 +1,6 @@ class Projects::ArtifactsController < Projects::ApplicationController layout 'project' - before_action :authorize_read_build_artifacts! + before_action :authorize_read_build! def download unless artifacts_file.file_storage? @@ -43,14 +43,4 @@ class Projects::ArtifactsController < Projects::ApplicationController def artifacts_file @artifacts_file ||= build.artifacts_file end - - def authorize_read_build_artifacts! - unless can?(current_user, :read_build_artifacts, @project) - if current_user.nil? - return authenticate_user! - else - return render_404 - end - end - end end diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb index 548f1b9ebf..f7e6bb3444 100644 --- a/app/controllers/projects/avatars_controller.rb +++ b/app/controllers/projects/avatars_controller.rb @@ -2,15 +2,13 @@ class Projects::AvatarsController < Projects::ApplicationController before_action :project def show - @blob = @project.repository.blob_at_branch('master', @project.avatar_in_git) + @blob = @repository.blob_at_branch('master', @project.avatar_in_git) if @blob headers['X-Content-Type-Options'] = 'nosniff' - send_data( - @blob.data, - type: @blob.mime_type, - disposition: 'inline', - filename: @blob.name - ) + headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob)) + headers['Content-Disposition'] = 'inline' + headers['Content-Type'] = @blob.content_type + head :ok # 'render nothing: true' messes up the Content-Type else render_404 end diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb new file mode 100644 index 0000000000..dc9c96df00 --- /dev/null +++ b/app/controllers/projects/badges_controller.rb @@ -0,0 +1,24 @@ +class Projects::BadgesController < Projects::ApplicationController + before_action :set_no_cache + + def build + respond_to do |format| + format.html { render_404 } + format.svg do + image = Ci::ImageForBuildService.new.execute(project, ref: params[:ref]) + send_file(image.path, filename: image.name, disposition: 'inline', type: 'image/svg+xml') + end + end + end + + private + + def set_no_cache + expires_now + + # Add some deprecated headers for older agents + # + response.headers['Pragma'] = 'no-cache' + response.headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT' + end +end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index bb72232edd..495a432347 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -33,6 +33,7 @@ class Projects::BlobController < Projects::ApplicationController def edit @last_commit = Gitlab::Git::Commit.last_for_path(@repository, @ref, @path).sha + blob.load_all_data!(@repository) end def update @@ -51,6 +52,7 @@ class Projects::BlobController < Projects::ApplicationController def preview @content = params[:content] + @blob.load_all_data!(@repository) diffy = Diffy::Diff.new(@blob.data, @content, diff: '-U 3', include_diff_info: true) diff_lines = diffy.diff.scan(/.*\n/)[2..-1] diff_lines = Gitlab::Diff::Parser.new.parse(diff_lines) diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 92d9699fe8..f159e169f6 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -1,9 +1,8 @@ class Projects::BuildsController < Projects::ApplicationController before_action :build, except: [:index, :cancel_all] - - before_action :authorize_manage_builds!, except: [:index, :show, :status] - - layout "project" + before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry] + before_action :authorize_update_build!, except: [:index, :show, :status] + layout 'project' def index @scope = params[:scope] @@ -23,7 +22,6 @@ class Projects::BuildsController < Projects::ApplicationController def cancel_all @project.builds.running_or_pending.each(&:cancel) - redirect_to namespace_project_builds_path(project.namespace, project) end @@ -46,18 +44,22 @@ class Projects::BuildsController < Projects::ApplicationController end build = Ci::Build.retry(@build) - redirect_to build_path(build) end + def cancel + @build.cancel + redirect_to build_path(@build) + end + def status render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha) end - def cancel - @build.cancel - - redirect_to build_path(@build) + def erase + @build.erase(erased_by: current_user) + redirect_to namespace_project_build_path(project.namespace, project, @build), + notice: "Build has been sucessfully erased!" end private @@ -69,10 +71,4 @@ class Projects::BuildsController < Projects::ApplicationController def build_path(build) namespace_project_build_path(build.project.namespace, build.project, build) end - - def authorize_manage_builds! - unless can?(current_user, :manage_builds, project) - return render_404 - end - end end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index f5a169e5aa..97d31a4229 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -2,16 +2,19 @@ # # Not to be confused with CommitsController, plural. class Projects::CommitController < Projects::ApplicationController + include CreatesCommit + # Authorize before_action :require_non_empty_project - before_action :authorize_download_code!, except: [:cancel_builds] - before_action :authorize_manage_builds!, only: [:cancel_builds] + before_action :authorize_download_code!, except: [:cancel_builds, :retry_builds] + before_action :authorize_update_build!, only: [:cancel_builds, :retry_builds] + before_action :authorize_read_commit_status!, only: [:builds] before_action :commit - before_action :authorize_manage_builds!, only: [:cancel_builds, :retry_builds] before_action :define_show_vars, only: [:show, :builds] + before_action :authorize_edit_tree!, only: [:revert] def show - return git_not_found! unless @commit + apply_diff_view_cookie! @line_notes = commit.notes.inline @note = @project.build_commit_note(commit) @@ -55,8 +58,37 @@ class Projects::CommitController < Projects::ApplicationController render layout: false end + def revert + assign_revert_commit_vars + + return render_404 if @target_branch.blank? + + create_commit(Commits::RevertService, success_notice: "The #{revert_type_title} has been successfully reverted.", + success_path: successful_revert_path, failure_path: failed_revert_path) + end + private + def revert_type_title + @commit.merged_merge_request ? 'merge request' : 'commit' + end + + def successful_revert_path + return referenced_merge_request_url if @commit.merged_merge_request + + namespace_project_commits_url(@project.namespace, @project, @target_branch) + end + + def failed_revert_path + return referenced_merge_request_url if @commit.merged_merge_request + + namespace_project_commit_url(@project.namespace, @project, params[:id]) + end + + def referenced_merge_request_url + namespace_project_merge_request_url(@project.namespace, @project, @commit.merged_merge_request) + end + def commit @commit ||= @project.commit(params[:id]) end @@ -66,6 +98,8 @@ class Projects::CommitController < Projects::ApplicationController end def define_show_vars + return git_not_found! unless commit + if params[:w].to_i == 1 @diffs = commit.diffs({ ignore_whitespace_change: true }) else @@ -78,9 +112,15 @@ class Projects::CommitController < Projects::ApplicationController @statuses = ci_commit.statuses if ci_commit end - def authorize_manage_builds! - unless can?(current_user, :manage_builds, project) - return render_404 - end + def assign_revert_commit_vars + @commit = project.commit(params[:id]) + @target_branch = params[:target_branch] + @mr_source_branch = @commit.revert_branch_name + @mr_target_branch = @target_branch + @commit_params = { + commit: @commit, + revert_type_title: revert_type_title, + create_merge_request: params[:create_merge_request].present? || different_project? + } end end diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index bf5b54c8cb..1420b96840 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -21,6 +21,9 @@ class Projects::CommitsController < Projects::ApplicationController @note_counts = project.notes.where(commit_id: @commits.map(&:id)). group(:commit_id).count + @merge_request = @project.merge_requests.opened. + find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref) + respond_to do |format| format.html format.json { pager_json("projects/commits/_commits", @commits.size) } diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 7bbe75b397..dc5d217f3e 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -4,24 +4,23 @@ class Projects::CompareController < Projects::ApplicationController # Authorize before_action :require_non_empty_project before_action :authorize_download_code! + before_action :assign_ref_vars, only: [:index, :show] + before_action :merge_request, only: [:index, :show] def index - @ref = Addressable::URI.unescape(params[:to]) end def show - base_ref = Addressable::URI.unescape(params[:from]) - @ref = head_ref = Addressable::URI.unescape(params[:to]) diff_options = { ignore_whitespace_change: true } if params[:w] == '1' compare_result = CompareService.new. - execute(@project, head_ref, @project, base_ref, diff_options) + execute(@project, @head_ref, @project, @base_ref, diff_options) if compare_result @commits = Commit.decorate(compare_result.commits, @project) @diffs = compare_result.diffs - @commit = @project.commit(head_ref) - @base_commit = @project.merge_base_commit(base_ref, head_ref) + @commit = @project.commit(@head_ref) + @base_commit = @project.merge_base_commit(@base_ref, @head_ref) @diff_refs = [@base_commit, @commit] @line_notes = [] end @@ -31,4 +30,16 @@ class Projects::CompareController < Projects::ApplicationController redirect_to namespace_project_compare_path(@project.namespace, @project, params[:from], params[:to]) end + + private + + def assign_ref_vars + @base_ref = Addressable::URI.unescape(params[:from]) + @ref = @head_ref = Addressable::URI.unescape(params[:to]) + end + + def merge_request + @merge_request ||= @project.merge_requests.opened. + find_by(source_project: @project, source_branch: @head_ref, target_branch: @base_ref) + end end diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb index 750181f0c1..a0835c9aad 100644 --- a/app/controllers/projects/forks_controller.rb +++ b/app/controllers/projects/forks_controller.rb @@ -3,6 +3,25 @@ class Projects::ForksController < Projects::ApplicationController before_action :require_non_empty_project before_action :authorize_download_code! + def index + base_query = project.forks.includes(:creator) + + @forks = if current_user + base_query.where('projects.visibility_level IN (?) OR projects.id IN (?)', + Project.public_and_internal_levels, + current_user.authorized_projects.pluck(:id)) + else + base_query.where('projects.visibility_level = ?', Project::PUBLIC) + end + + @total_forks_count = base_query.size + @private_forks_count = @total_forks_count - @forks.size + @public_forks_count = @total_forks_count - @private_forks_count + + @sort = params[:sort] || 'id_desc' + @forks = @forks.order_by(@sort).page(params[:page]).per(PER_PAGE) + end + def new @namespaces = current_user.manageable_namespaces @namespaces.delete(@project.namespace) @@ -10,7 +29,7 @@ class Projects::ForksController < Projects::ApplicationController def create namespace = Namespace.find(params[:namespace_key]) - + @forked_project = namespace.projects.find_by(path: project.path) @forked_project = nil unless @forked_project && @forked_project.forked_from_project == project @@ -23,7 +42,7 @@ class Projects::ForksController < Projects::ApplicationController if continue_params redirect_to continue_params[:to], notice: continue_params[:notice] else - redirect_to namespace_project_path(@forked_project.namespace, @forked_project), notice: "The project was successfully forked." + redirect_to namespace_project_path(@forked_project.namespace, @forked_project), notice: "The project '#{@forked_project.name}' was successfully forked." end end else diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index 07f355c35b..196996f175 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -3,6 +3,7 @@ class Projects::ImportsController < Projects::ApplicationController before_action :authorize_admin_project! before_action :require_no_repo, only: [:new, :create] before_action :redirect_if_progress, only: [:new, :create] + before_action :redirect_if_no_import, only: :show def new end @@ -63,14 +64,19 @@ class Projects::ImportsController < Projects::ApplicationController def require_no_repo if @project.repository_exists? - redirect_to(namespace_project_path(@project.namespace, @project)) + redirect_to namespace_project_path(@project.namespace, @project) end end def redirect_if_progress if @project.import_in_progress? - redirect_to namespace_project_import_path(@project.namespace, @project) && - return + redirect_to namespace_project_import_path(@project.namespace, @project) + end + end + + def redirect_if_no_import + if @project.repository_exists? && @project.no_import? + redirect_to namespace_project_path(@project.namespace, @project) end end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 6824488380..67faa1e443 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -32,6 +32,7 @@ class Projects::IssuesController < Projects::ApplicationController end @issues = @issues.page(params[:page]).per(PER_PAGE) + @label = @project.labels.find_by(title: params[:label_name]) respond_to do |format| format.html diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 86d6e3e0f6..ecac3c395e 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -69,7 +69,7 @@ class Projects::LabelsController < Projects::ApplicationController end def label_params - params.require(:label).permit(:title, :color) + params.require(:label).permit(:title, :description, :color) end def label diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index ed3050d59a..5fe2169460 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -34,6 +34,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE) @merge_requests = @merge_requests.preload(:target_project) + @label = @project.labels.find_by(title: params[:label_name]) + respond_to do |format| format.html format.json do @@ -57,6 +59,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def diffs + apply_diff_view_cookie! + @commit = @merge_request.last_commit @base_commit = @merge_request.diff_base_commit @@ -177,6 +181,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController return end + TodoService.new.merge_merge_request(merge_request, current_user) + @merge_request.update(merge_error: nil) if params[:merge_when_build_succeeds].present? && @merge_request.ci_commit && @merge_request.ci_commit.active? diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 15506bd677..21f30f278c 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -11,11 +11,12 @@ class Projects::MilestonesController < Projects::ApplicationController respond_to :html def index - @milestones = case params[:state] - when 'all'; @project.milestones.order("state, due_date DESC") - when 'closed'; @project.milestones.closed.order("due_date DESC") - else @project.milestones.active.order("due_date ASC") - end + @milestones = + case params[:state] + when 'all' then @project.milestones.reorder(due_date: :desc, title: :asc) + when 'closed' then @project.milestones.closed.reorder(due_date: :desc, title: :asc) + else @project.milestones.active.reorder(due_date: :asc, title: :asc) + end @milestones = @milestones.includes(:project) @milestones = @milestones.page(params[:page]).per(PER_PAGE) @@ -34,6 +35,7 @@ class Projects::MilestonesController < Projects::ApplicationController @issues = @milestone.issues @users = @milestone.participants.uniq @merge_requests = @milestone.merge_requests + @labels = @milestone.labels end def create diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 6f1e186d40..1b9dd56804 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -11,11 +11,9 @@ class Projects::NotesController < Projects::ApplicationController notes_json = { notes: [], last_fetched_at: current_fetched_at } @notes.each do |note| - notes_json[:notes] << { - id: note.id, - html: note_to_html(note), - valid: note.valid? - } + next if note.cross_reference_not_visible_for?(current_user) + + notes_json[:notes] << note_json(note) end render json: notes_json @@ -25,7 +23,7 @@ class Projects::NotesController < Projects::ApplicationController @note = Notes::CreateService.new(project, current_user, note_params).execute respond_to do |format| - format.json { render_note_json(@note) } + format.json { render json: note_json(@note) } format.html { redirect_back_or_default } end end @@ -34,7 +32,7 @@ class Projects::NotesController < Projects::ApplicationController @note = Notes::UpdateService.new(project, current_user, note_params).execute(note) respond_to do |format| - format.json { render_note_json(@note) } + format.json { render json: note_json(@note) } format.html { redirect_back_or_default } end end @@ -99,6 +97,8 @@ class Projects::NotesController < Projects::ApplicationController end def note_to_discussion_html(note) + return unless note.for_diff_line? + if params[:view] == 'parallel' template = "projects/notes/_diff_notes_with_reply_parallel" locals = @@ -106,7 +106,7 @@ class Projects::NotesController < Projects::ApplicationController { notes_left: [note], notes_right: [] } else { notes_left: [], notes_right: [note] } - end + end else template = "projects/notes/_diff_notes_with_reply" locals = { notes: [note] } @@ -131,9 +131,9 @@ class Projects::NotesController < Projects::ApplicationController ) end - def render_note_json(note) + def note_json(note) if note.valid? - render json: { + { valid: true, id: note.id, discussion_id: note.discussion_id, @@ -144,7 +144,7 @@ class Projects::NotesController < Projects::ApplicationController discussion_with_diff_html: note_to_discussion_with_diff_html(note) } else - render json: { + { valid: false, award: note.is_award, errors: note.errors @@ -163,8 +163,6 @@ class Projects::NotesController < Projects::ApplicationController ) end - private - def find_current_user_notes @notes = NotesFinder.new.execute(project, current_user, params) end diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index be7d5c187f..87b4d08da0 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -15,7 +15,10 @@ class Projects::RawController < Projects::ApplicationController if @blob.lfs_pointer? send_lfs_object else - stream_data + headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob)) + headers['Content-Disposition'] = 'inline' + headers['Content-Type'] = get_blob_type + head :ok # 'render nothing: true' messes up the Content-Type end else render_404 @@ -34,16 +37,6 @@ class Projects::RawController < Projects::ApplicationController end end - def stream_data - type = get_blob_type - - send_data( - @blob.data, - type: type, - disposition: 'inline' - ) - end - def send_lfs_object lfs_object = find_lfs_object diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb index e2785caa2f..bedeb4a295 100644 --- a/app/controllers/projects/runner_projects_controller.rb +++ b/app/controllers/projects/runner_projects_controller.rb @@ -1,5 +1,5 @@ class Projects::RunnerProjectsController < Projects::ApplicationController - before_action :authorize_admin_project! + before_action :authorize_admin_build! layout 'project_settings' diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb index 4993b2648a..0dd2d6a99b 100644 --- a/app/controllers/projects/runners_controller.rb +++ b/app/controllers/projects/runners_controller.rb @@ -1,6 +1,6 @@ class Projects::RunnersController < Projects::ApplicationController + before_action :authorize_admin_build! before_action :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show] - before_action :authorize_admin_project! layout 'project_settings' diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 280fe12cc7..e580487a2c 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -34,6 +34,11 @@ class Projects::TagsController < Projects::ApplicationController def destroy DeleteTagService.new(project, current_user).execute(params[:id]) - redirect_to namespace_project_tags_path(@project.namespace, @project) + respond_to do |format| + format.html do + redirect_to namespace_project_tags_path(@project.namespace, @project) + end + format.js + end end end diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb index 30adfad1da..92359745ce 100644 --- a/app/controllers/projects/triggers_controller.rb +++ b/app/controllers/projects/triggers_controller.rb @@ -1,5 +1,5 @@ class Projects::TriggersController < Projects::ApplicationController - before_action :authorize_admin_project! + before_action :authorize_admin_build! layout 'project_settings' diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index 10efafea9d..0023465457 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -1,5 +1,5 @@ class Projects::VariablesController < Projects::ApplicationController - before_action :authorize_admin_project! + before_action :authorize_admin_build! layout 'project_settings' diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 935f7d75c6..aea08ecce3 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -93,6 +93,10 @@ class ProjectsController < ApplicationController return end + if @project.pending_delete? + flash[:alert] = "Project queued for delete." + end + respond_to do |format| format.html do if @project.repository_exists? @@ -120,8 +124,8 @@ class ProjectsController < ApplicationController def destroy return access_denied! unless can?(current_user, :remove_project, @project) - ::Projects::DestroyService.new(@project, current_user, {}).execute - flash[:alert] = "Project '#{@project.name}' was deleted." + ::Projects::DestroyService.new(@project, current_user, {}).pending_delete! + flash[:alert] = "Project '#{@project.name}' will be deleted." redirect_to dashboard_projects_path rescue Projects::DestroyService::DestroyError => ex @@ -223,6 +227,7 @@ class ProjectsController < ApplicationController :issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch, :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, + :public_builds, ) end @@ -231,7 +236,7 @@ class ProjectsController < ApplicationController Emoji.emojis.map do |name, emoji| { name: name, - path: view_context.image_url("emoji/#{emoji["unicode"]}.png") + path: view_context.image_url("#{emoji["unicode"]}.png") } end end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 5efdd613e7..c48175a4c5 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -8,11 +8,6 @@ class RegistrationsController < Devise::RegistrationsController def create if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha - if Gitlab::IpCheck.new(request.remote_ip).spam? - flash[:alert] = 'Could not create an account. This IP is listed for spam.' - return render action: 'new' - end - super else flash[:alert] = "There was an error with the reCAPTCHA code below. Please re-enter the code." diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 825f85199b..44eb58e418 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -2,6 +2,8 @@ class SessionsController < Devise::SessionsController include AuthenticatesWithTwoFactor include Recaptcha::ClientHelper + skip_before_action :check_2fa_requirement, only: [:destroy] + prepend_before_action :authenticate_with_two_factor, only: [:create] prepend_before_action :store_redirect_path, only: [:new] before_action :auto_sign_in_with_provider, only: [:new] diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index 868b05929d..509f4f412c 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -55,14 +55,15 @@ class UploadsController < ApplicationController "user" => User, "project" => Project, "note" => Note, - "group" => Group + "group" => Group, + "appearance" => Appearance } upload_models[params[:model]] end def upload_mount - upload_mounts = %w(avatar attachment file) + upload_mounts = %w(avatar attachment file logo header_logo) if upload_mounts.include?(params[:mounted_as]) params[:mounted_as] diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 280228dbcc..6055b60608 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -4,8 +4,9 @@ class UsersController < ApplicationController def show @contributed_projects = contributed_projects.joined(@user).reject(&:forked?) - + @projects = PersonalProjectsFinder.new(@user).execute(current_user) + @projects = @projects.page(params[:page]).per(PER_PAGE) @groups = @user.groups.order_id_desc diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 4d56b48e3f..f7240edd61 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -81,7 +81,8 @@ class IssuableFinder elsif current_user && params[:authorized_only].presence && !current_user_related? @projects = current_user.authorized_projects.reorder(nil) else - @projects = ProjectsFinder.new.execute(current_user).reorder(nil) + @projects = ProjectsFinder.new.execute(current_user, group: group). + reorder(nil) end end @@ -118,6 +119,20 @@ class IssuableFinder labels? && params[:label_name] == Label::None.title end + def labels + return @labels if defined?(@labels) + + if labels? && !filter_by_no_label? + @labels = Label.where(title: label_names) + + if projects + @labels = @labels.where(project: projects) + end + else + @labels = Label.none + end + end + def assignee? params[:assignee_id].present? end @@ -252,8 +267,6 @@ class IssuableFinder joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{klass.name}' AND label_links.target_id = #{klass.table_name}.id"). where(label_links: { id: nil }) else - label_names = params[:label_name].split(",") - items = items.joins(:labels).where(labels: { title: label_names }) if projects @@ -265,6 +278,10 @@ class IssuableFinder items end + def label_names + params[:label_name].split(',') + end + def current_user_related? params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' end diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb new file mode 100644 index 0000000000..3ba27c4050 --- /dev/null +++ b/app/finders/todos_finder.rb @@ -0,0 +1,129 @@ +# TodosFinder +# +# Used to filter Todos by set of params +# +# Arguments: +# current_user - which user use +# params: +# action_id: integer +# author_id: integer +# project_id; integer +# state: 'pending' or 'done' +# type: 'Issue' or 'MergeRequest' +# + +class TodosFinder + NONE = '0' + + attr_accessor :current_user, :params + + def initialize(current_user, params) + @current_user = current_user + @params = params + end + + def execute + items = current_user.todos + items = by_action_id(items) + items = by_author(items) + items = by_project(items) + items = by_state(items) + items = by_type(items) + + items + end + + private + + def action_id? + action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED].include?(action_id.to_i) + end + + def action_id + params[:action_id] + end + + def author? + params[:author_id].present? + end + + def author + return @author if defined?(@author) + + @author = + if author? && params[:author_id] != NONE + User.find(params[:author_id]) + else + nil + end + end + + def project? + params[:project_id].present? + end + + def project + return @project if defined?(@project) + + if project? + @project = Project.find(params[:project_id]) + + unless Ability.abilities.allowed?(current_user, :read_project, @project) + @project = nil + end + else + @project = nil + end + + @project + end + + def type? + type.present? && ['Issue', 'MergeRequest'].include?(type) + end + + def type + params[:type] + end + + def by_action_id(items) + if action_id? + items = items.where(action: action_id) + end + + items + end + + def by_author(items) + if author? + items = items.where(author_id: author.try(:id)) + end + + items + end + + def by_project(items) + if project? + items = items.where(project: project) + end + + items + end + + def by_state(items) + case params[:state] + when 'done' + items.done + else + items.pending + end + end + + def by_type(items) + if type? + items = items.where(target_type: type) + end + + items + end +end diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index c5820bf4c5..e0abc3a286 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -1,21 +1,33 @@ module AppearancesHelper - def brand_item - nil - end - def brand_title - 'GitLab Community Edition' + if brand_item && brand_item.title + brand_item.title + else + 'GitLab Community Edition' + end end def brand_image - nil + if brand_item.logo? + image_tag brand_item.logo + else + nil + end end def brand_text - nil + markdown(brand_item.description) + end + + def brand_item + @appearance ||= Appearance.first end def brand_header_logo - render 'shared/logo.svg' + if brand_item && brand_item.header_logo? + image_tag brand_item.header_logo + else + render 'shared/logo.svg' + end end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f3a2723ee0..368969c647 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -72,7 +72,7 @@ module ApplicationHelper if user_or_email.is_a?(User) user = user_or_email else - user = User.find_by(email: user_or_email.downcase) + user = User.find_by(email: user_or_email.try(:downcase)) end if user @@ -118,12 +118,6 @@ module ApplicationHelper grouped_options_for_select(options, @ref || @project.default_branch) end - def emoji_autocomplete_source - # should be an array of strings - # so to_s can be called, because it is sufficient and to_json is too slow - Emoji.names.to_s - end - # Define whenever show last push event # with suggestion to create MR def show_last_push_widget?(event) @@ -169,18 +163,6 @@ module ApplicationHelper Gitlab.config.extra end - def search_placeholder - if @project && @project.persisted? - 'Search in this project' - elsif @snippet || @snippets || @show_snippets - 'Search snippets' - elsif @group && @group.persisted? - 'Search in this group' - else - 'Search' - end - end - # Render a `time` element with Javascript-based relative date and tooltip # # time - Time object @@ -224,8 +206,7 @@ module ApplicationHelper file_content end else - GitHub::Markup.render(file_name, file_content). - force_encoding(file_content.encoding).html_safe + other_markup(file_name, file_content) end rescue RuntimeError simple_format(file_content) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 7d6b58ee21..23693629a4 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -23,6 +23,10 @@ module ApplicationSettingsHelper current_application_settings.user_oauth_applications end + def askimet_enabled? + current_application_settings.akismet_enabled? + end + # Return a group of checkboxes that use Bootstrap's button plugin for a # toggle button effect. def restricted_level_checkboxes(help_block_id) diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index de669e529a..b4f80fd9b3 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -6,6 +6,10 @@ module AuthHelper Gitlab.config.ldap.enabled end + def omniauth_enabled? + Gitlab.config.omniauth.enabled + end + def provider_has_icon?(name) PROVIDERS_WITH_ICONS.include?(name.to_s) end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 8b689b29a4..1696792792 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -36,8 +36,7 @@ module BlobHelper notice: edit_in_new_fork_notice, notice_now: edit_in_new_fork_notice_now } - fork_path = namespace_project_fork_path(project.namespace, project, namespace_key: current_user.namespace.id, - continue: continue_params) + fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params) link_to "Edit", fork_path, class: 'btn', method: :post end @@ -62,8 +61,7 @@ module BlobHelper notice: edit_in_new_fork_notice + " Try to #{action} this file again.", notice_now: edit_in_new_fork_notice_now } - fork_path = namespace_project_fork_path(project.namespace, project, namespace_key: current_user.namespace.id, - continue: continue_params) + fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params) link_to label, fork_path, class: "btn btn-#{btn_class}", method: :post end @@ -128,4 +126,16 @@ module BlobHelper blob.size end end + + def blob_svg?(blob) + blob.language && blob.language.name == 'SVG' + end + + # SVGs can contain malicious JavaScript; only include whitelisted + # elements and attributes. Note that this whitelist is by no means complete + # and may omit some elements. + def sanitize_svg(blob) + blob.data = Loofah.scrub_fragment(blob.data, :strip).to_xml + blob + end end diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb index 1ed8c710f7..43a29c96bc 100644 --- a/app/helpers/broadcast_messages_helper.rb +++ b/app/helpers/broadcast_messages_helper.rb @@ -3,7 +3,7 @@ module BroadcastMessagesHelper return unless message.present? content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do - icon('bullhorn') << ' ' << message.message + icon('bullhorn') << ' ' << render_broadcast_message(message.message) end end @@ -31,4 +31,8 @@ module BroadcastMessagesHelper 'Pending' end end + + def render_broadcast_message(message) + Banzai.render(message, pipeline: :broadcast_message).html_safe + end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index d26f007c8e..a09e91578b 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -123,6 +123,37 @@ module CommitsHelper ) end + def revert_commit_link(commit, continue_to_path, btn_class: nil) + return unless current_user + + tooltip = "Revert this #{revert_commit_type(commit)} in a new merge request" + + if can_collaborate_with_project? + content_tag :span, 'data-toggle' => 'modal', 'data-target' => '#modal-revert-commit' do + link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class}" + end + elsif can?(current_user, :fork_project, @project) + continue_params = { + to: continue_to_path, + notice: edit_in_new_fork_notice + ' Try to revert this commit again.', + notice_now: edit_in_new_fork_notice_now + } + fork_path = namespace_project_forks_path(@project.namespace, @project, + namespace_key: current_user.namespace.id, + continue: continue_params) + + link_to 'Revert', fork_path, class: 'btn btn-grouped btn-close', method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip + end + end + + def revert_commit_type(commit) + if commit.merged_merge_request + 'merge request' + else + 'commit' + end + end + protected # Private: Returns a link to a person. If the person has a matching user and @@ -152,7 +183,7 @@ module CommitsHelper options = { class: "commit-#{options[:source]}-link has_tooltip", - data: { :'original-title' => sanitize(source_email) } + data: { 'original-title'.to_sym => sanitize(source_email) } } if user.nil? diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 62971d1e14..d76db867c5 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -1,4 +1,13 @@ module DiffHelper + def mark_inline_diffs(old_line, new_line) + old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_line, new_line).inline_diffs + + marked_old_line = Gitlab::Diff::InlineDiffMarker.new(old_line).mark(old_diffs) + marked_new_line = Gitlab::Diff::InlineDiffMarker.new(new_line).mark(new_diffs) + + [marked_old_line, marked_new_line] + end + def diff_view params[:view] == 'parallel' ? 'parallel' : 'inline' end @@ -55,12 +64,12 @@ module DiffHelper if line.blank? "  ".html_safe else - line.html_safe + line end end def line_comments - @line_comments ||= @line_notes.select(&:active?).group_by(&:line_code) + @line_comments ||= @line_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code) end def organize_comments(type_left, type_right, line_code_left, line_code_right) @@ -128,7 +137,7 @@ module DiffHelper # Always use HTML to handle case where JSON diff rendered this button params_copy.delete(:format) - link_to url_for(params_copy), id: "#{name}-diff-btn", class: (selected ? 'btn active' : 'btn') do + link_to url_for(params_copy), id: "#{name}-diff-btn", class: (selected ? 'btn active' : 'btn'), data: { view_type: name } do title end end diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb index 0d291f9a87..3648757428 100644 --- a/app/helpers/explore_helper.rb +++ b/app/helpers/explore_helper.rb @@ -10,8 +10,19 @@ module ExploreHelper options = exist_opts.merge(options) - path = explore_projects_path + path = if explore_controller? + explore_projects_path + elsif current_action?(:starred) + starred_dashboard_projects_path + else + dashboard_projects_path + end + path << "?#{options.to_param}" path end + + def explore_controller? + controller.class.name.split("::").first == "Explore" + end end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 1a22625225..89d2a64849 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -78,6 +78,21 @@ module GitlabMarkdownHelper ) end + def other_markup(file_name, text) + Gitlab::OtherMarkup.render( + file_name, + text, + project: @project, + current_user: (current_user if defined?(current_user)), + + # RelativeLinkFilter + project_wiki: @project_wiki, + requested_path: @path, + ref: @ref, + commit: @commit + ) + end + # Return the first line of +text+, up to +max_chars+, after parsing the line # as Markdown. HTML tags in the parsed output are not counted toward the # +max_chars+ limit. If the length limit falls within a tag's contents, then diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 5724d3aabe..84c6d0883b 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -7,7 +7,7 @@ module IconsHelper # font-awesome-rails gem, but should we ever use a different icon pack in the # future we won't have to change hundreds of method calls. def icon(names, options = {}) - fa_icon(names, options) + options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options) end def spinner(text = nil, visible = false) diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb new file mode 100644 index 0000000000..91a3aa371e --- /dev/null +++ b/app/helpers/issuables_helper.rb @@ -0,0 +1,37 @@ +module IssuablesHelper + + def sidebar_gutter_toggle_icon + sidebar_gutter_collapsed? ? icon('angle-double-left') : icon('angle-double-right') + end + + def sidebar_gutter_collapsed_class + "right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}" + end + + def issuables_count(issuable) + base_issuable_scope(issuable).maximum(:iid) + end + + def next_issuable_for(issuable) + base_issuable_scope(issuable).where('iid > ?', issuable.iid).last + end + + def prev_issuable_for(issuable) + base_issuable_scope(issuable).where('iid < ?', issuable.iid).first + end + + private + + def sidebar_gutter_collapsed? + cookies[:collapsed_gutter] == 'true' + end + + def base_issuable_scope(issuable) + issuable.project.send(issuable.class.table_name).send(issuable_state_scope(issuable)) + end + + def issuable_state_scope(issuable) + issuable.open? ? :opened : :closed + end + +end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 43262d579e..ae4ebc0854 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -44,14 +44,14 @@ module IssuesHelper end def bulk_update_milestone_options - milestones = project_active_milestones.to_a + milestones = @project.milestones.active.reorder(due_date: :asc, title: :asc).to_a milestones.unshift(Milestone::None) options_from_collection_for_select(milestones, 'id', 'title', params[:milestone_id]) end def milestone_options(object) - milestones = object.project.milestones.active.to_a + milestones = object.project.milestones.active.reorder(due_date: :asc, title: :asc).to_a milestones.unshift(Milestone::None) options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id) @@ -69,7 +69,7 @@ module IssuesHelper end end - def issue_button_visibility(issue, closed) + def issue_button_visibility(issue, closed) return 'hidden' if issue.closed? == closed end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index a2c3d4d2f3..1c7fcc13b4 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -7,6 +7,8 @@ module LabelsHelper # project - Project object which will be used as the context for the label's # link. If omitted, defaults to `@project`, or the label's own # project. + # type - The type of item the link will point to (:issue or + # :merge_request). If omitted, defaults to :issue. # block - An optional block that will be passed to `link_to`, forming the # body of the link element. If omitted, defaults to # `render_colored_label`. @@ -23,14 +25,19 @@ module LabelsHelper # # Force the generated link to use a provided project # link_to_label(label, project: Project.last) # + # # Force the generated link to point to merge requests instead of issues + # link_to_label(label, type: :merge_request) + # # # Customize link body with a block # link_to_label(label) { "My Custom Label Text" } # # Returns a String - def link_to_label(label, project: nil, &block) + def link_to_label(label, project: nil, type: :issue, &block) project ||= @project || label.project - link = namespace_project_issues_path(project.namespace, project, - label_name: label.name) + link = send("namespace_project_#{type.to_s.pluralize}_path", + project.namespace, + project, + label_name: label.name) if block_given? link_to link, &block @@ -83,7 +90,11 @@ module LabelsHelper end def text_color_for_bg(bg_color) - r, g, b = bg_color.slice(1,7).scan(/.{2}/).map(&:hex) + if bg_color.length == 4 + r, g, b = bg_color[1, 4].scan(/./).map { |v| (v * 2).hex } + else + r, g, b = bg_color[1, 7].scan(/.{2}/).map(&:hex) + end if (r + g + b) > 500 '#333333' diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index a42cbcff18..7de81d8dfd 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -36,4 +36,14 @@ module MilestonesHelper options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title]) end + + def milestone_remaining_days(milestone) + if milestone.expired? + content_tag(:strong, 'expired') + elsif milestone.due_date + days = milestone.remaining_days + content = content_tag(:strong, days) + content << " #{'day'.pluralize(days)} remaining" + end + end end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index e6fb8670e5..5d86bd490a 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -19,6 +19,20 @@ module NavHelper end end + def page_gutter_class + if current_path?('merge_requests#show') || + current_path?('merge_requests#diffs') || + current_path?('merge_requests#commits') || + current_path?('merge_requests#builds') || + current_path?('issues#show') + if cookies[:collapsed_gutter] == 'true' + "page-gutter right-sidebar-collapsed" + else + "page-gutter right-sidebar-expanded" + end + end + end + def nav_header_class if nav_menu_collapsed? "header-collapsed" diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 77ba612548..d6fb629b0c 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -20,6 +20,12 @@ module ProjectsHelper end end + def link_to_member_avatar(author, opts = {}) + default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" } + opts = default_opts.merge(opts) + image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar] + end + def link_to_member(project, author, opts = {}) default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" } opts = default_opts.merge(opts) @@ -40,7 +46,7 @@ module ProjectsHelper link_to(author_html, user_path(author), class: "author_link").html_safe else title = opts[:title].sub(":name", sanitize(author.name)) - link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { :'original-title' => title, container: 'body' } ).html_safe + link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { 'original-title'.to_sym => title, container: 'body' } ).html_safe end end @@ -53,14 +59,23 @@ module ProjectsHelper link_to(simple_sanitize(owner.name), user_path(owner)) end - project_link = link_to(simple_sanitize(project.name), project_path(project)) + project_link = link_to project_path(project), { class: "project-item-select-holder" } do + link_output = simple_sanitize(project.name) + + if current_user + link_output += project_select_tag :project_path, + class: "project-item-select js-projects-dropdown", + data: { include_groups: false, order_by: 'last_activity_at' } + end + + link_output + end + project_link += icon "chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle" if current_user full_title = namespace_link + ' / ' + project_link full_title += ' · '.html_safe + link_to(simple_sanitize(name), url) if name - content_tag :span do - full_title - end + full_title end def remove_project_message(project) @@ -83,10 +98,6 @@ module ProjectsHelper project_nav_tabs.include? name end - def project_active_milestones - @project.milestones.active.order("due_date, title ASC") - end - def project_for_deploy_key(deploy_key) if deploy_key.projects.include?(@project) @project @@ -116,7 +127,7 @@ module ProjectsHelper private def get_project_nav_tabs(project, current_user) - nav_tabs = [:home] + nav_tabs = [:home, :forks] if !project.empty_repo? && can?(current_user, :download_code, project) nav_tabs << [:files, :commits, :network, :graphs] @@ -126,7 +137,7 @@ module ProjectsHelper nav_tabs << :merge_requests end - if project.builds_enabled? && can?(current_user, :read_build, project) + if can?(current_user, :read_build, project) nav_tabs << :builds end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index d4f7825862..1eb790b179 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -70,7 +70,7 @@ module SearchHelper # Autocomplete results for the current user's groups def groups_autocomplete(term, limit = 5) - Group.search(term).limit(limit).map do |group| + current_user.authorized_groups.search(term).limit(limit).map do |group| { label: "group: #{search_result_sanitize(group.name)}", url: group_path(group) @@ -80,7 +80,7 @@ module SearchHelper # Autocomplete results for the current user's projects def projects_autocomplete(term, limit = 5) - ProjectsFinder.new.execute(current_user).search_by_title(term). + current_user.authorized_projects.search_by_title(term). sorted_by_stars.non_archived.limit(limit).map do |p| { label: "project: #{search_result_sanitize(p.name_with_namespace)}", diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 906cb12cd4..41ae404899 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -17,4 +17,79 @@ module SnippetsHelper snippet_path(snippet) end end + + # Get an array of line numbers surrounding a matching + # line, bounded by min/max. + # + # @returns Array of line numbers + def bounded_line_numbers(line, min, max, surrounding_lines) + lower = line - surrounding_lines > min ? line - surrounding_lines : min + upper = line + surrounding_lines < max ? line + surrounding_lines : max + (lower..upper).to_a + end + + # Returns a sorted set of lines to be included in a snippet preview. + # This ensures matching adjacent lines do not display duplicated + # surrounding code. + # + # @returns Array, unique and sorted. + def matching_lines(lined_content, surrounding_lines, query) + used_lines = [] + lined_content.each_with_index do |line, line_number| + used_lines.concat bounded_line_numbers( + line_number, + 0, + lined_content.size, + surrounding_lines + ) if line.include?(query) + end + + used_lines.uniq.sort + end + + # 'Chunkify' entire snippet. Splits the snippet data into matching lines + + # surrounding_lines() worth of unmatching lines. + # + # @returns a hash with {snippet_object, snippet_chunks:{data,start_line}} + def chunk_snippet(snippet, query, surrounding_lines = 3) + lined_content = snippet.content.split("\n") + used_lines = matching_lines(lined_content, surrounding_lines, query) + + snippet_chunk = [] + snippet_chunks = [] + snippet_start_line = 0 + last_line = -1 + + # Go through each used line, and add consecutive lines as a single chunk + # to the snippet chunk array. + used_lines.each do |line_number| + if last_line < 0 + # Start a new chunk. + snippet_start_line = line_number + snippet_chunk << lined_content[line_number] + elsif last_line == line_number - 1 + # Consecutive line, continue chunk. + snippet_chunk << lined_content[line_number] + else + # Non-consecutive line, add chunk to chunk array. + snippet_chunks << { + data: snippet_chunk.join("\n"), + start_line: snippet_start_line + 1 + } + + # Start a new chunk. + snippet_chunk = [lined_content[line_number]] + snippet_start_line = line_number + end + last_line = line_number + end + # Add final chunk to chunk array + snippet_chunks << { + data: snippet_chunk.join("\n"), + start_line: snippet_start_line + 1 + } + + # Return snippet with chunk array + { snippet_object: snippet, snippet_chunks: snippet_chunks } + end end diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 241179b021..f9026b887d 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -11,6 +11,8 @@ module SortingHelper sort_value_largest_repo => sort_title_largest_repo, sort_value_recently_signin => sort_title_recently_signin, sort_value_oldest_signin => sort_title_oldest_signin, + sort_value_downvotes => sort_title_downvotes, + sort_value_upvotes => sort_title_upvotes } end @@ -54,6 +56,14 @@ module SortingHelper 'Oldest sign in' end + def sort_title_downvotes + 'Least popular' + end + + def sort_title_upvotes + 'Most popular' + end + def sort_value_oldest_updated 'updated_asc' end @@ -93,4 +103,12 @@ module SortingHelper def sort_value_oldest_signin 'oldest_sign_in' end + + def sort_value_downvotes + 'downvotes_desc' + end + + def sort_value_upvotes + 'upvotes_desc' + end end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb new file mode 100644 index 0000000000..4b745a5b96 --- /dev/null +++ b/app/helpers/todos_helper.rb @@ -0,0 +1,87 @@ +module TodosHelper + def todos_pending_count + current_user.todos.pending.count + end + + def todos_done_count + current_user.todos.done.count + end + + def todo_action_name(todo) + case todo.action + when Todo::ASSIGNED then 'assigned you' + when Todo::MENTIONED then 'mentioned you on' + end + end + + def todo_target_link(todo) + target = todo.target_type.titleize.downcase + link_to "#{target} #{todo.target.to_reference}", todo_target_path(todo) + end + + def todo_target_path(todo) + anchor = dom_id(todo.note) if todo.note.present? + + polymorphic_path([todo.project.namespace.becomes(Namespace), + todo.project, todo.target], anchor: anchor) + end + + def todos_filter_params + { + state: params[:state], + project_id: params[:project_id], + author_id: params[:author_id], + type: params[:type], + action_id: params[:action_id], + } + end + + def todos_filter_path(options = {}) + without = options.delete(:without) + + options = todos_filter_params.merge(options) + + if without.present? + without.each do |key| + options.delete(key) + end + end + + path = request.path + path << "?#{options.to_param}" + path + end + + def todo_actions_options + actions = [ + OpenStruct.new(id: '', title: 'Any Action'), + OpenStruct.new(id: Todo::ASSIGNED, title: 'Assigned'), + OpenStruct.new(id: Todo::MENTIONED, title: 'Mentioned') + ] + + options_from_collection_for_select(actions, 'id', 'title', params[:action_id]) + end + + def todo_projects_options + projects = current_user.authorized_projects.sorted_by_activity.non_archived + projects = projects.includes(:namespace) + + projects = projects.map do |project| + OpenStruct.new(id: project.id, title: project.name_with_namespace) + end + + projects.unshift(OpenStruct.new(id: '', title: 'Any Project')) + + options_from_collection_for_select(projects, 'id', 'title', params[:project_id]) + end + + def todo_types_options + types = [ + OpenStruct.new(title: 'Any Type', name: ''), + OpenStruct.new(title: 'Issue', name: 'Issue'), + OpenStruct.new(title: 'Merge Request', name: 'MergeRequest') + ] + + options_from_collection_for_select(types, 'name', 'title', params[:type]) + end +end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 2ad7c80dae..4920ca5af6 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -56,8 +56,7 @@ module TreeHelper return false unless on_top_of_branch?(project, ref) - can?(current_user, :push_code, project) || - (current_user && current_user.already_forked?(project)) + can_collaborate_with_project?(project) end def tree_edit_branch(project = @project, ref = @ref) diff --git a/app/mailers/email_rejection_mailer.rb b/app/mailers/email_rejection_mailer.rb index 883f1c73ad..76db31a4c4 100644 --- a/app/mailers/email_rejection_mailer.rb +++ b/app/mailers/email_rejection_mailer.rb @@ -10,7 +10,7 @@ class EmailRejectionMailer < BaseMailer subject: "[Rejected] #{@original_message.subject}" } - headers['Message-ID'] = SecureRandom.hex + headers['Message-ID'] = "<#{SecureRandom.hex}@#{Gitlab.config.gitlab.host}>" headers['In-Reply-To'] = @original_message.message_id headers['References'] = @original_message.message_id diff --git a/app/mailers/emails/builds.rb b/app/mailers/emails/builds.rb index 64c1ce8cfa..2f86d1be57 100644 --- a/app/mailers/emails/builds.rb +++ b/app/mailers/emails/builds.rb @@ -3,26 +3,27 @@ module Emails def build_fail_email(build_id, to) @build = Ci::Build.find(build_id) @project = @build.project + add_project_headers - add_build_headers - headers['X-GitLab-Build-Status'] = "failed" + add_build_headers('failed') mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha)) end def build_success_email(build_id, to) @build = Ci::Build.find(build_id) @project = @build.project + add_project_headers - add_build_headers - headers['X-GitLab-Build-Status'] = "success" + add_build_headers('success') mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha)) end private - def add_build_headers + + def add_build_headers(status) headers['X-GitLab-Build-Id'] = @build.id headers['X-GitLab-Build-Ref'] = @build.ref + headers['X-GitLab-Build-Status'] = status.to_s end - end end diff --git a/app/models/ability.rb b/app/models/ability.rb index ab59a3506a..a866eadeeb 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -5,17 +5,18 @@ class Ability return [] unless user.is_a?(User) return [] if user.blocked? - case subject.class.name - when "Project" then project_abilities(user, subject) - when "Issue" then issue_abilities(user, subject) - when "Note" then note_abilities(user, subject) - when "ProjectSnippet" then project_snippet_abilities(user, subject) - when "PersonalSnippet" then personal_snippet_abilities(user, subject) - when "MergeRequest" then merge_request_abilities(user, subject) - when "Group" then group_abilities(user, subject) - when "Namespace" then namespace_abilities(user, subject) - when "GroupMember" then group_member_abilities(user, subject) - when "ProjectMember" then project_member_abilities(user, subject) + case subject + when CommitStatus then commit_status_abilities(user, subject) + when Project then project_abilities(user, subject) + when Issue then issue_abilities(user, subject) + when Note then note_abilities(user, subject) + when ProjectSnippet then project_snippet_abilities(user, subject) + when PersonalSnippet then personal_snippet_abilities(user, subject) + when MergeRequest then merge_request_abilities(user, subject) + when Group then group_abilities(user, subject) + when Namespace then namespace_abilities(user, subject) + when GroupMember then group_member_abilities(user, subject) + when ProjectMember then project_member_abilities(user, subject) else [] end.concat(global_abilities(user)) end @@ -25,6 +26,8 @@ class Ability case true when subject.is_a?(PersonalSnippet) anonymous_personal_snippet_abilities(subject) + when subject.is_a?(CommitStatus) + anonymous_commit_status_abilities(subject) when subject.is_a?(Project) || subject.respond_to?(:project) anonymous_project_abilities(subject) when subject.is_a?(Group) || subject.respond_to?(:group) @@ -52,16 +55,26 @@ class Ability :read_project_member, :read_merge_request, :read_note, - :read_build, + :read_commit_status, :download_code ] + # Allow to read builds by anonymous user if guests are allowed + rules << :read_build if project.public_builds? + rules - project_disabled_features_rules(project) else [] end end + def anonymous_commit_status_abilities(subject) + rules = anonymous_project_abilities(subject.project) + # If subject is Ci::Build which inherits from CommitStatus filter the abilities + rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build) + rules + end + def anonymous_group_abilities(subject) group = if subject.is_a?(Group) subject @@ -113,6 +126,9 @@ class Ability if project.public? || project.internal? rules.push(*public_project_rules) + + # Allow to read builds for internal projects + rules << :read_build if project.public_builds? end if project.owner == user || user.admin? @@ -134,7 +150,8 @@ class Ability def public_project_rules @public_project_rules ||= project_guest_rules + [ :download_code, - :fork_project + :fork_project, + :read_commit_status, ] end @@ -149,7 +166,6 @@ class Ability :read_project_member, :read_merge_request, :read_note, - :read_build, :create_project, :create_issue, :create_note @@ -158,24 +174,26 @@ class Ability def project_report_rules @project_report_rules ||= project_guest_rules + [ - :create_commit_status, - :read_commit_statuses, - :read_build_artifacts, :download_code, :fork_project, :create_project_snippet, :update_issue, :admin_issue, - :admin_label + :admin_label, + :read_commit_status, + :read_build, ] end def project_dev_rules @project_dev_rules ||= project_report_rules + [ :admin_merge_request, + :create_commit_status, + :update_commit_status, + :create_build, + :update_build, :create_merge_request, :create_wiki, - :manage_builds, :push_code ] end @@ -201,7 +219,9 @@ class Ability :admin_merge_request, :admin_note, :admin_wiki, - :admin_project + :admin_project, + :admin_commit_status, + :admin_build ] end @@ -240,6 +260,10 @@ class Ability rules += named_abilities('wiki') end + unless project.builds_enabled + rules += named_abilities('build') + end + rules end @@ -376,6 +400,22 @@ class Ability rules end + def commit_status_abilities(user, subject) + rules = project_abilities(user, subject.project) + # If subject is Ci::Build which inherits from CommitStatus filter the abilities + rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build) + rules + end + + def filter_build_abilities(rules) + # If we can't read build we should also not have that + # ability when looking at this in context of commit_status + %w(read create update admin).each do |rule| + rules.delete(:"#{rule}_commit_status") unless rules.include?(:"#{rule}_build") + end + rules + end + def abilities @abilities ||= begin abilities = Six.new diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb index 2bc15c60d5..cc59aa4e91 100644 --- a/app/models/abuse_report.rb +++ b/app/models/abuse_report.rb @@ -17,7 +17,7 @@ class AbuseReport < ActiveRecord::Base validates :reporter, presence: true validates :user, presence: true validates :message, presence: true - validates :user_id, uniqueness: true + validates :user_id, uniqueness: { message: 'has already been reported' } def remove_user user.block diff --git a/app/models/appearance.rb b/app/models/appearance.rb new file mode 100644 index 0000000000..4cf8dd9a8c --- /dev/null +++ b/app/models/appearance.rb @@ -0,0 +1,9 @@ +class Appearance < ActiveRecord::Base + validates :title, presence: true + validates :description, presence: true + validates :logo, file_size: { maximum: 1.megabyte } + validates :header_logo, file_size: { maximum: 1.megabyte } + + mount_uploader :logo, AttachmentUploader + mount_uploader :header_logo, AttachmentUploader +end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 2f3487b53a..269056e0e7 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -43,8 +43,7 @@ # metrics_port :integer default(8089) # sentry_enabled :boolean default(FALSE) # sentry_dsn :string -# ip_blocking_enabled :boolean default(FALSE) -# dns_blacklist_threshold :float default(0.33) +# email_author_in_body :boolean default(FALSE) # class ApplicationSetting < ActiveRecord::Base @@ -72,8 +71,8 @@ class ApplicationSetting < ActiveRecord::Base url: true validates :admin_notification_email, - allow_blank: true, - email: true + email: true, + allow_blank: true validates :two_factor_grace_period, numericality: { greater_than_or_equal_to: 0 } @@ -90,6 +89,14 @@ class ApplicationSetting < ActiveRecord::Base presence: true, if: :sentry_enabled + validates :akismet_api_key, + presence: true, + if: :akismet_enabled + + validates :max_attachment_size, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + validates_each :restricted_visibility_levels do |record, attr, value| unless value.nil? value.each do |level| @@ -145,7 +152,9 @@ class ApplicationSetting < ActiveRecord::Base shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], max_artifacts_size: Settings.artifacts['max_size'], require_two_factor_authentication: false, - two_factor_grace_period: 48 + two_factor_grace_period: 48, + recaptcha_enabled: false, + akismet_enabled: false ) end diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb index 6111963371..8a0a8a4c2a 100644 --- a/app/models/broadcast_message.rb +++ b/app/models/broadcast_message.rb @@ -26,7 +26,9 @@ class BroadcastMessage < ActiveRecord::Base default_value_for :font, '#FFFFFF' def self.current - where("ends_at > :now AND starts_at <= :now", now: Time.zone.now).last + Rails.cache.fetch("broadcast_message_current", expires_in: 1.minute) do + where("ends_at > :now AND starts_at <= :now", now: Time.zone.now).last + end end def active? diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 623edd8bc5..1227458e52 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -31,15 +31,19 @@ # artifacts_file :text # gl_project_id :integer # artifacts_metadata :text +# erased_by_id :integer +# erased_at :datetime # module Ci class Build < CommitStatus include Gitlab::Application.routes.url_helpers + LAZY_ATTRIBUTES = ['trace'] belongs_to :runner, class_name: 'Ci::Runner' belongs_to :trigger_request, class_name: 'Ci::TriggerRequest' + belongs_to :erased_by, class_name: 'User' serialize :options @@ -103,23 +107,22 @@ module Ci end state_machine :status, initial: :pending do - after_transition pending: :running do |build, transition| + after_transition pending: :running do |build| build.execute_hooks end - after_transition any => [:success, :failed, :canceled] do |build, transition| - return unless build.project + # We use around_transition to create builds for next stage as soon as possible, before the `after_*` is executed + around_transition any => [:success, :failed, :canceled] do |build, block| + block.call + build.commit.create_next_builds(build) if build.commit + end + after_transition any => [:success, :failed, :canceled] do |build| build.update_coverage - build.commit.create_next_builds(build) build.execute_hooks end end - def ignored? - failed? && allow_failure? - end - def retryable? project.builds_enabled? && commands.present? end @@ -179,6 +182,7 @@ module Ci end def update_coverage + return unless project coverage_regex = project.build_coverage_regex return unless coverage_regex coverage = extract_coverage(trace, coverage_regex) @@ -203,6 +207,10 @@ module Ci end end + def has_trace? + raw_trace.present? + end + def raw_trace if File.file?(path_to_trace) File.read(path_to_trace) @@ -330,6 +338,7 @@ module Ci end def execute_hooks + return unless project build_data = Gitlab::BuildDataBuilder.build(self) project.execute_hooks(build_data.dup, :build_hooks) project.execute_services(build_data.dup, :build_hooks) @@ -359,6 +368,33 @@ module Ci Gitlab::Ci::Build::Artifacts::Metadata.new(artifacts_metadata.path, path, **options).to_entry end + def erase(opts = {}) + return false unless erasable? + + remove_artifacts_file! + remove_artifacts_metadata! + erase_trace! + update_erased!(opts[:erased_by]) + end + + def erasable? + complete? && (artifacts? || has_trace?) + end + + def erased? + !self.erased_at.nil? + end + + private + + def erase_trace! + self.trace = nil + end + + def update_erased!(user = nil) + self.update(erased_by: user, erased_at: Time.now) + end + private def yaml_variables diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index d2a2923694..ecbd2078b1 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -205,7 +205,11 @@ module Ci end def ci_yaml_file - @ci_yaml_file ||= project.repository.blob_at(sha, '.gitlab-ci.yml').data + @ci_yaml_file ||= begin + blob = project.repository.blob_at(sha, '.gitlab-ci.yml') + blob.load_all_data!(project.repository) + blob.data + end rescue nil end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 38b20cd7fa..e725a6d468 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -22,6 +22,7 @@ module Ci extend Ci::Model LAST_CONTACT_TIME = 5.minutes.ago + AVAILABLE_SCOPES = ['specific', 'shared', 'active', 'paused', 'online'] has_many :builds, class_name: 'Ci::Build' has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' @@ -38,6 +39,11 @@ module Ci scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) } scope :ordered, ->() { order(id: :desc) } + scope :owned_or_shared, ->(project_id) do + joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id') + .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id) + end + acts_as_taggable def self.search(query) diff --git a/app/models/commit.rb b/app/models/commit.rb index 0ba7b584d9..3224f5457f 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -68,18 +68,18 @@ class Commit # Pattern used to extract commit references from text # - # The SHA can be between 6 and 40 hex characters. + # The SHA can be between 7 and 40 hex characters. # # This pattern supports cross-project references. def self.reference_pattern %r{ (?:#{Project.reference_pattern}#{reference_prefix})? - (?\h{6,40}) + (?\h{7,40}) }x end def self.link_reference_pattern - super("commit", /(?\h{6,40})/) + super("commit", /(?\h{7,40})/) end def to_reference(from_project = nil) @@ -215,6 +215,44 @@ class Commit ci_commit.try(:status) || :not_found end + def revert_branch_name + "revert-#{short_id}" + end + + def revert_description + if merged_merge_request + "This reverts merge request #{merged_merge_request.to_reference}" + else + "This reverts commit #{sha}" + end + end + + def revert_message + %Q{Revert "#{title}"\n\n#{revert_description}} + end + + def reverts_commit?(commit) + description? && description.include?(commit.revert_description) + end + + def merge_commit? + parents.size > 1 + end + + def merged_merge_request + return @merged_merge_request if defined?(@merged_merge_request) + + @merged_merge_request = project.merge_requests.find_by(merge_commit_sha: id) if merge_commit? + end + + def has_been_reverted?(current_user = nil, noteable = self) + Gitlab::ReferenceExtractor.lazily do + noteable.notes.system.flat_map do |note| + note.all_references(current_user).commits + end + end.any? { |commit_ref| commit_ref.reverts_commit?(self) } + end + private def repo_changes diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index 14e7971fa0..289dbc5728 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -32,8 +32,8 @@ class CommitRange PATTERN = /#{REF_PATTERN}\.{2,3}#{REF_PATTERN}/ # In text references, the beginning and ending refs can only be SHAs - # between 6 and 40 hex characters. - STRICT_PATTERN = /\h{6,40}\.{2,3}\h{6,40}/ + # between 7 and 40 hex characters. + STRICT_PATTERN = /\h{7,40}\.{2,3}\h{7,40}/ def self.reference_prefix '@' diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 66e0502fc0..7ef5083632 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -75,16 +75,16 @@ class CommitStatus < ActiveRecord::Base transition [:pending, :running] => :canceled end - after_transition pending: :running do |build, transition| - build.update_attributes started_at: Time.now + after_transition pending: :running do |commit_status| + commit_status.update_attributes started_at: Time.now end - after_transition any => [:success, :failed, :canceled] do |build, transition| - build.update_attributes finished_at: Time.now + after_transition any => [:success, :failed, :canceled] do |commit_status| + commit_status.update_attributes finished_at: Time.now end - after_transition [:pending, :running] => :success do |build, transition| - MergeRequests::MergeWhenBuildSucceedsService.new(build.commit.project, nil).trigger(build) + after_transition [:pending, :running] => :success do |commit_status| + MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.commit.project, nil).trigger(commit_status) end state :pending, value: 'pending' @@ -113,6 +113,10 @@ class CommitStatus < ActiveRecord::Base canceled? || success? || failed? end + def ignored? + failed? && allow_failure? + end + def duration if started_at && finished_at finished_at - started_at diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 04650a9e67..e5f089fb8a 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -69,10 +69,35 @@ module Issuable case method.to_s when 'milestone_due_asc' then order_milestone_due_asc when 'milestone_due_desc' then order_milestone_due_desc + when 'downvotes_desc' then order_downvotes_desc + when 'upvotes_desc' then order_upvotes_desc else order_by(method) end end + + def order_downvotes_desc + order_votes_desc('thumbsdown') + end + + def order_upvotes_desc + order_votes_desc('thumbsup') + end + + def order_votes_desc(award_emoji_name) + issuable_table = self.arel_table + note_table = Note.arel_table + + join_clause = issuable_table.join(note_table, Arel::Nodes::OuterJoin).on( + note_table[:noteable_id].eq(issuable_table[:id]).and( + note_table[:noteable_type].eq(self.name).and( + note_table[:is_award].eq(true).and(note_table[:note].eq(award_emoji_name)) + ) + ) + ).join_sources + + joins(join_clause).group(issuable_table[:id]).reorder("COUNT(notes.id) DESC") + end end def today? @@ -126,17 +151,17 @@ module Issuable end def to_hook_data(user) - { + hook_data = { object_kind: self.class.name.underscore, user: user.hook_attrs, - repository: { - name: project.name, - url: project.url_to_repo, - description: project.description, - homepage: project.web_url - }, - object_attributes: hook_attrs + project: project.hook_attrs, + object_attributes: hook_attrs, + # DEPRECATED + repository: project.hook_attrs.slice(:name, :url, :description, :homepage) } + hook_data.merge!(assignee: assignee.hook_attrs) if assignee + + hook_data end def label_names diff --git a/app/models/email.rb b/app/models/email.rb index 935705e2ed..b323d1edd1 100644 --- a/app/models/email.rb +++ b/app/models/email.rb @@ -15,7 +15,7 @@ class Email < ActiveRecord::Base belongs_to :user validates :user_id, presence: true - validates :email, presence: true, email: { strict_mode: true }, uniqueness: true + validates :email, presence: true, uniqueness: true, email: true validate :unique_email, if: ->(email) { email.email_changed? } before_validation :cleanup_email diff --git a/app/models/event.rb b/app/models/event.rb index 01d008035a..9a0bbf50f8 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -47,7 +47,11 @@ class Event < ActiveRecord::Base # Scopes scope :recent, -> { reorder(id: :desc) } scope :code_push, -> { where(action: PUSHED) } - scope :in_projects, ->(project_ids) { where(project_id: project_ids).recent } + + scope :in_projects, ->(projects) do + where(project_id: projects.map(&:id)).recent + end + scope :with_associations, -> { includes(project: :namespace) } scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) } @@ -64,12 +68,6 @@ class Event < ActiveRecord::Base [Event::CREATED, Event::CLOSED, Event::MERGED]) end - def latest_update_time - row = select(:updated_at, :project_id).reorder(id: :desc).take - - row ? row.updated_at : nil - end - def limit_recent(limit = 20, offset = nil) recent.limit(limit).offset(offset) end diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index 49f6c95e04..2ca79df0a2 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -31,7 +31,7 @@ class ExternalIssue # Pattern used to extract `JIRA-123` issue references from text def self.reference_pattern - %r{(?([A-Z\-]+-)\d+)} + %r{(?\b([A-Z][A-Z0-9_]+-)\d+)} end def to_reference(_from_project = nil) diff --git a/app/models/group.rb b/app/models/group.rb index 5a31b46920..76042b3e3f 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -19,7 +19,7 @@ require 'file_size_validator' class Group < Namespace include Gitlab::ConfigHelper include Referable - + has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember' alias_method :members, :group_members has_many :users, through: :group_members diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index fa18ba5dbb..fe923fafbe 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -3,11 +3,11 @@ # Table name: web_hooks # # id :integer not null, primary key -# url :string(255) +# url :string(2000) # project_id :integer # created_at :datetime # updated_at :datetime -# type :string(255) default("ProjectHook") +# type :string default("ProjectHook") # service_id :integer # push_events :boolean default(TRUE), not null # issues_events :boolean default(FALSE), not null diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb index b333a33734..80962264ba 100644 --- a/app/models/hooks/service_hook.rb +++ b/app/models/hooks/service_hook.rb @@ -3,11 +3,11 @@ # Table name: web_hooks # # id :integer not null, primary key -# url :string(255) +# url :string(2000) # project_id :integer # created_at :datetime # updated_at :datetime -# type :string(255) default("ProjectHook") +# type :string default("ProjectHook") # service_id :integer # push_events :boolean default(TRUE), not null # issues_events :boolean default(FALSE), not null diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb index d81512fae5..c147d8762a 100644 --- a/app/models/hooks/system_hook.rb +++ b/app/models/hooks/system_hook.rb @@ -3,11 +3,11 @@ # Table name: web_hooks # # id :integer not null, primary key -# url :string(255) +# url :string(2000) # project_id :integer # created_at :datetime # updated_at :datetime -# type :string(255) default("ProjectHook") +# type :string default("ProjectHook") # service_id :integer # push_events :boolean default(TRUE), not null # issues_events :boolean default(FALSE), not null diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 3bb50c63ca..7a13c3f0a3 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -3,11 +3,11 @@ # Table name: web_hooks # # id :integer not null, primary key -# url :string(255) +# url :string(2000) # project_id :integer # created_at :datetime # updated_at :datetime -# type :string(255) default("ProjectHook") +# type :string default("ProjectHook") # service_id :integer # push_events :boolean default(TRUE), not null # issues_events :boolean default(FALSE), not null diff --git a/app/models/issue.rb b/app/models/issue.rb index 7beba98460..5f58c0508f 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -38,6 +38,7 @@ class Issue < ActiveRecord::Base scope :cared, ->(user) { where(assignee_id: user) } scope :open_for, ->(user) { opened.assigned_to(user) } + scope :in_projects, ->(project_ids) { where(project_id: project_ids) } state_machine :state, initial: :opened do event :close do diff --git a/app/models/label.rb b/app/models/label.rb index 220da10a6a..07a1db4abe 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -2,13 +2,14 @@ # # Table name: labels # -# id :integer not null, primary key -# title :string(255) -# color :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# template :boolean default(FALSE) +# id :integer not null, primary key +# title :string(255) +# color :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# template :boolean default(FALSE) +# description :string(255) # class Label < ActiveRecord::Base @@ -85,6 +86,10 @@ class Label < ActiveRecord::Base issues.opened.count end + def closed_issues_count + issues.closed.count + end + def template? template end diff --git a/app/models/member.rb b/app/models/member.rb index 34efcd0088..ca08007b7e 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -39,7 +39,6 @@ class Member < ActiveRecord::Base if: :invite? }, email: { - strict_mode: true, allow_nil: true }, uniqueness: { diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 0af6064554..1543ef311d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -24,6 +24,7 @@ # merge_params :text # merge_when_build_succeeds :boolean default(FALSE), not null # merge_user_id :integer +# merge_commit_sha :string # require Rails.root.join("app/models/commit") @@ -137,6 +138,7 @@ class MergeRequest < ActiveRecord::Base scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) } scope :of_projects, ->(ids) { where(target_project_id: ids) } + scope :opened, -> { with_states(:opened, :reopened) } scope :merged, -> { with_state(:merged) } scope :closed, -> { with_state(:closed) } scope :closed_and_merged, -> { with_states(:closed, :merged) } @@ -240,7 +242,7 @@ class MergeRequest < ActiveRecord::Base return unless unchecked? can_be_merged = - project.repository.can_be_merged?(source_sha, target_branch) + !broken? && project.repository.can_be_merged?(source_sha, target_branch) if can_be_merged mark_as_mergeable @@ -258,7 +260,7 @@ class MergeRequest < ActiveRecord::Base end def work_in_progress? - !!(title =~ /\A\[?WIP\]?:? /i) + !!(title =~ /\A\[?WIP(\]|:| )/i) end def mergeable? @@ -284,7 +286,8 @@ class MergeRequest < ActiveRecord::Base def can_remove_source_branch?(current_user) !source_project.protected_branch?(source_branch) && !source_project.root_ref?(source_branch) && - Ability.abilities.allowed?(current_user, :push_code, source_project) + Ability.abilities.allowed?(current_user, :push_code, source_project) && + last_commit == source_project.commit(source_branch) end def mr_and_commit_notes @@ -346,10 +349,10 @@ class MergeRequest < ActiveRecord::Base # Return the set of issues that will be closed if this merge request is accepted. def closes_issues(current_user = self.author) if target_branch == project.default_branch - issues = commits.flat_map { |c| c.closes_issues(current_user) } - issues.push(*Gitlab::ClosingIssueExtractor.new(project, current_user). - closed_by_message(description)) - issues.uniq(&:id) + messages = commits.map(&:safe_message) << description + + Gitlab::ClosingIssueExtractor.new(project, current_user). + closed_by_message(messages.join("\n")) else [] end @@ -530,4 +533,12 @@ class MergeRequest < ActiveRecord::Base [diff_base_commit, last_commit] end + + def merge_commit + @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha + end + + def can_be_reverted?(current_user = nil) + merge_commit && !merge_commit.has_been_reverted?(current_user, self) + end end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index c9a0ad8b9b..7dc2f909b2 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -27,6 +27,7 @@ class Milestone < ActiveRecord::Base belongs_to :project has_many :issues + has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues has_many :merge_requests has_many :participants, through: :issues, source: :assignee @@ -34,7 +35,7 @@ class Milestone < ActiveRecord::Base scope :closed, -> { with_state(:closed) } scope :of_projects, ->(ids) { where(project_id: ids) } - validates :title, presence: true + validates :title, presence: true, uniqueness: { scope: :project_id } validates :project, presence: true strip_attributes :title @@ -109,6 +110,12 @@ class Milestone < ActiveRecord::Base 0 end + def remaining_days + return 0 if !due_date || expired? + + (due_date - Date.today).to_i + end + def expires_at if due_date if due_date.past? diff --git a/app/models/note.rb b/app/models/note.rb index 605caed9eb..d287e0f3c6 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -33,10 +33,12 @@ class Note < ActiveRecord::Base participant :author belongs_to :project - belongs_to :noteable, polymorphic: true + belongs_to :noteable, polymorphic: true, touch: true belongs_to :author, class_name: "User" belongs_to :updated_by, class_name: "User" + has_many :todos, dependent: :destroy + delegate :name, to: :project, prefix: true delegate :name, :email, to: :author, prefix: true @@ -375,6 +377,7 @@ class Note < ActiveRecord::Base # def set_award! return unless awards_supported? && contains_emoji_only? + self.is_award = true self.note = award_emoji_name end @@ -382,7 +385,7 @@ class Note < ActiveRecord::Base private def awards_supported? - noteable.kind_of?(Issue) || noteable.is_a?(MergeRequest) + (noteable.kind_of?(Issue) || noteable.is_a?(MergeRequest)) && !for_diff_line? end def contains_emoji_only? diff --git a/app/models/project.rb b/app/models/project.rb index 9cd2b1af28..8d98ccdf47 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -36,6 +36,7 @@ # build_coverage_regex :string # build_allow_git_fetch :boolean default(TRUE), not null # build_timeout :integer default(3600), not null +# pending_delete :boolean # require 'carrierwave/orm/activerecord' @@ -150,6 +151,7 @@ class Project < ActiveRecord::Base has_many :releases, dependent: :destroy has_many :lfs_objects_projects, dependent: :destroy has_many :lfs_objects, through: :lfs_objects_projects + has_many :todos, dependent: :destroy has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" @@ -272,6 +274,10 @@ class Project < ActiveRecord::Base query: "%#{query.try(:downcase)}%") end + def search_by_visibility(level) + where(visibility_level: Gitlab::VisibilityLevel.const_get(level.upcase)) + end + def search_by_title(query) where('projects.archived = ?', false).where('LOWER(projects.name) LIKE :query', query: "%#{query.downcase}%") end @@ -337,7 +343,7 @@ class Project < ActiveRecord::Base end def repository - @repository ||= Repository.new(path_with_namespace, nil, self) + @repository ||= Repository.new(path_with_namespace, self) end def commit(id = 'HEAD') @@ -377,6 +383,10 @@ class Project < ActiveRecord::Base external_import? || forked? end + def no_import? + import_status == 'none' + end + def external_import? import_url.present? end @@ -702,6 +712,8 @@ class Project < ActiveRecord::Base old_path_with_namespace = File.join(namespace_dir, path_was) new_path_with_namespace = File.join(namespace_dir, path) + expire_caches_before_rename(old_path_with_namespace) + if gitlab_shell.mv_repository(old_path_with_namespace, new_path_with_namespace) # If repository moved successfully we need to send update instructions to users. # However we cannot allow rollback since we moved repository @@ -730,14 +742,39 @@ class Project < ActiveRecord::Base Gitlab::UploadsTransfer.new.rename_project(path_was, path, namespace.path) end + # Expires various caches before a project is renamed. + def expire_caches_before_rename(old_path) + repo = Repository.new(old_path, self) + wiki = Repository.new("#{old_path}.wiki", self) + + if repo.exists? + repo.expire_cache + repo.expire_emptiness_caches + end + + if wiki.exists? + wiki.expire_cache + wiki.expire_emptiness_caches + end + end + def hook_attrs { name: name, - ssh_url: ssh_url_to_repo, - http_url: http_url_to_repo, + description: description, web_url: web_url, + avatar_url: avatar_url, + git_ssh_url: ssh_url_to_repo, + git_http_url: http_url_to_repo, namespace: namespace.name, - visibility_level: visibility_level + visibility_level: visibility_level, + path_with_namespace: path_with_namespace, + default_branch: default_branch, + # Backward compatibility + homepage: web_url, + url: url_to_repo, + ssh_url: ssh_url_to_repo, + http_url: http_url_to_repo } end @@ -785,6 +822,8 @@ class Project < ActiveRecord::Base def change_head(branch) # Cached divergent commit counts are based on repository head repository.expire_branch_cache + repository.expire_root_ref_cache + gitlab_shell.update_repository_head(self.path_with_namespace, branch) reload_default_branch end @@ -905,4 +944,8 @@ class Project < ActiveRecord::Base def runners_token ensure_runners_token! end + + def wiki + @wiki ||= ProjectWiki.new(self, self.owner) + end end diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index 3d7e8bbee6..e76d9eca2a 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -112,7 +112,7 @@ class PushoverService < Service priority: priority, title: "#{project.name_with_namespace}", message: message, - url: data[:repository][:homepage], + url: data[:project][:web_url], url_title: "See project #{project.name_with_namespace}" } diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 9f380a382c..9629c7e1bb 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -136,7 +136,7 @@ class ProjectTeam end def human_max_access(user_id) - Gitlab::Access.options.key max_member_access(user_id) + Gitlab::Access.options_with_owner.key(max_member_access(user_id)) end # This method assumes project and group members are eager loaded for optimal diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 8ce4749597..c96e6f0b8e 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -12,6 +12,7 @@ class ProjectWiki # Returns a string describing what went wrong after # an operation fails. attr_reader :error_message + attr_reader :project def initialize(project, user = nil) @project = project @@ -122,7 +123,7 @@ class ProjectWiki end def repository - Repository.new(path_with_namespace, default_branch, @project) + Repository.new(path_with_namespace, @project) end def default_branch diff --git a/app/models/repository.rb b/app/models/repository.rb index e9978c5e66..e050bd4525 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -15,7 +15,7 @@ class Repository Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete)) end - def initialize(path_with_namespace, default_branch = nil, project = nil) + def initialize(path_with_namespace, project) @path_with_namespace = path_with_namespace @project = project end @@ -23,13 +23,11 @@ class Repository def raw_repository return nil unless path_with_namespace - @raw_repository ||= begin - repo = Gitlab::Git::Repository.new(path_to_repo) - repo.autocrlf = :input - repo - rescue Gitlab::Git::Repository::NoRepository - nil - end + @raw_repository ||= Gitlab::Git::Repository.new(path_to_repo) + end + + def update_autocrlf_option + raw_repository.autocrlf = :input if raw_repository.autocrlf != :input end # Return absolute path to repository @@ -40,11 +38,18 @@ class Repository end def exists? - raw_repository + return false unless raw_repository + + raw_repository.rugged + true + rescue Gitlab::Git::Repository::NoRepository + false end def empty? - raw_repository.empty? + return @empty unless @empty.nil? + + @empty = cache.fetch(:empty?) { raw_repository.empty? } end # @@ -57,11 +62,15 @@ class Repository # This method return true if repository contains some content visible in project page. # def has_visible_content? - raw_repository.branch_count > 0 + return @has_visible_content unless @has_visible_content.nil? + + @has_visible_content = cache.fetch(:has_visible_content?) do + raw_repository.branch_count > 0 + end end def commit(id = 'HEAD') - return nil unless raw_repository + return nil unless exists? commit = Gitlab::Git::Commit.find(raw_repository, id) commit = Commit.new(commit, @project) if commit commit @@ -78,7 +87,8 @@ class Repository offset: offset, # --follow doesn't play well with --skip. See: # https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520 - follow: false + follow: false, + skip_merges: skip_merges } commits = Gitlab::Git::Commit.where(options) @@ -184,8 +194,11 @@ class Repository cache.fetch(:"diverging_commit_counts_#{branch.name}") do # Rugged seems to throw a `ReferenceError` when given branch_names rather # than SHA-1 hashes - number_commits_behind = commits_between(branch.target, root_ref_hash).size - number_commits_ahead = commits_between(root_ref_hash, branch.target).size + number_commits_behind = raw_repository. + count_commits_between(branch.target, root_ref_hash) + + number_commits_ahead = raw_repository. + count_commits_between(root_ref_hash, branch.target) { behind: number_commits_behind, ahead: number_commits_ahead } end @@ -196,12 +209,6 @@ class Repository readme version contribution_guide changelog license) end - def branch_cache_keys - branches.map do |branch| - :"diverging_commit_counts_#{branch.name}" - end - end - def build_cache cache_keys.each do |key| unless cache.exist?(key) @@ -226,20 +233,60 @@ class Repository @branches = nil end - def expire_cache + def expire_cache(branch_name = nil) cache_keys.each do |key| cache.expire(key) end - expire_branch_cache + expire_branch_cache(branch_name) + + # This ensures this particular cache is flushed after the first commit to a + # new repository. + expire_emptiness_caches if empty? end - def expire_branch_cache - branches.each do |branch| - cache.expire(:"diverging_commit_counts_#{branch.name}") + # Expires _all_ caches, including those that would normally only be expired + # under specific conditions. + def expire_all_caches! + expire_cache + expire_root_ref_cache + expire_emptiness_caches + expire_has_visible_content_cache + end + + def expire_branch_cache(branch_name = nil) + # When we push to the root branch we have to flush the cache for all other + # branches as their statistics are based on the commits relative to the + # root branch. + if !branch_name || branch_name == root_ref + branches.each do |branch| + cache.expire(:"diverging_commit_counts_#{branch.name}") + end + # In case a commit is pushed to a non-root branch we only have to flush the + # cache for said branch. + else + cache.expire(:"diverging_commit_counts_#{branch_name}") end end + def expire_root_ref_cache + cache.expire(:root_ref) + @root_ref = nil + end + + # Expires the cache(s) used to determine if a repository is empty or not. + def expire_emptiness_caches + cache.expire(:empty?) + @empty = nil + + expire_has_visible_content_cache + end + + def expire_has_visible_content_cache + cache.expire(:has_visible_content?) + @has_visible_content = nil + end + def rebuild_cache cache_keys.each do |key| cache.expire(key) @@ -477,7 +524,7 @@ class Repository end def root_ref - @root_ref ||= raw_repository.root_ref + @root_ref ||= cache.fetch(:root_ref) { raw_repository.root_ref } end def commit_dir(user, path, message, branch) @@ -576,6 +623,34 @@ class Repository end end + def revert(user, commit, base_branch, target_branch = nil) + source_sha = find_branch(base_branch).target + target_branch ||= base_branch + args = [commit.id, source_sha] + args << { mainline: 1 } if commit.merge_commit? + + revert_index = rugged.revert_commit(*args) + return false if revert_index.conflicts? + + tree_id = revert_index.write_tree(rugged) + return false unless diff_exists?(source_sha, tree_id) + + commit_with_hooks(user, target_branch) do |ref| + committer = user_to_committer(user) + source_sha = Rugged::Commit.create(rugged, + message: commit.revert_message, + author: committer, + committer: committer, + tree: tree_id, + parents: [rugged.lookup(source_sha)], + update_ref: ref) + end + end + + def diff_exists?(sha1, sha2) + rugged.diff(sha1, sha2).size > 0 + end + def merged_to_root_ref?(branch_name) branch_commit = commit(branch_name) root_ref_commit = commit(root_ref) @@ -588,6 +663,8 @@ class Repository end def merge_base(first_commit_id, second_commit_id) + first_commit_id = commit(first_commit_id).try(:id) || first_commit_id + second_commit_id = commit(second_commit_id).try(:id) || second_commit_id rugged.merge_base(first_commit_id, second_commit_id) rescue Rugged::ReferenceError nil @@ -600,7 +677,7 @@ class Repository def search_files(query, ref) offset = 2 - args = %W(#{Gitlab.config.git.bin_path} grep -i -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref}) + args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref}) Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/) end @@ -651,12 +728,15 @@ class Repository end def commit_with_hooks(current_user, branch) + update_autocrlf_option + oldrev = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + branch + target_branch = find_branch(branch) was_empty = empty? - unless was_empty - oldrev = find_branch(branch).target + if !was_empty && target_branch + oldrev = target_branch.target end with_tmp_ref(oldrev) do |tmp_ref| @@ -668,7 +748,7 @@ class Repository end GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do - if was_empty + if was_empty || !target_branch # Create branch rugged.references.create(ref, newrev) else @@ -683,6 +763,8 @@ class Repository end end end + + newrev end end diff --git a/app/models/spam_log.rb b/app/models/spam_log.rb new file mode 100644 index 0000000000..12df68ef83 --- /dev/null +++ b/app/models/spam_log.rb @@ -0,0 +1,10 @@ +class SpamLog < ActiveRecord::Base + belongs_to :user + + validates :user, presence: true + + def remove_user + user.block + user.destroy + end +end diff --git a/app/models/spam_report.rb b/app/models/spam_report.rb new file mode 100644 index 0000000000..cdc7321b08 --- /dev/null +++ b/app/models/spam_report.rb @@ -0,0 +1,5 @@ +class SpamReport < ActiveRecord::Base + belongs_to :user + + validates :user, presence: true +end diff --git a/app/models/todo.rb b/app/models/todo.rb new file mode 100644 index 0000000000..5f91991f78 --- /dev/null +++ b/app/models/todo.rb @@ -0,0 +1,53 @@ +# == Schema Information +# +# Table name: todos +# +# id :integer not null, primary key +# user_id :integer not null +# project_id :integer not null +# target_id :integer not null +# target_type :string not null +# author_id :integer +# note_id :integer +# action :integer not null +# state :string not null +# created_at :datetime +# updated_at :datetime +# + +class Todo < ActiveRecord::Base + ASSIGNED = 1 + MENTIONED = 2 + + belongs_to :author, class_name: "User" + belongs_to :note + belongs_to :project + belongs_to :target, polymorphic: true, touch: true + belongs_to :user + + delegate :name, :email, to: :author, prefix: true, allow_nil: true + + validates :action, :project, :target, :user, presence: true + + default_scope { reorder(id: :desc) } + + scope :pending, -> { with_state(:pending) } + scope :done, -> { with_state(:done) } + + state_machine :state, initial: :pending do + event :done do + transition [:pending, :done] => :done + end + + state :pending + state :done + end + + def body + if note.present? + note.note + else + target.title + end + end +end diff --git a/app/models/tree.rb b/app/models/tree.rb index e0e04d8859..7c4ed6e393 100644 --- a/app/models/tree.rb +++ b/app/models/tree.rb @@ -17,12 +17,20 @@ class Tree def readme return @readme if defined?(@readme) - # Take the first previewable readme, or return nil if none is available or - # we can't preview any of them - readme_tree = blobs.find do |blob| - blob.readme? && (previewable?(blob.name) || plain?(blob.name)) + available_readmes = blobs.select(&:readme?) + + previewable_readmes = available_readmes.select do |blob| + previewable?(blob.name) end + plain_readmes = available_readmes.select do |blob| + plain?(blob.name) + end + + # Prioritize previewable over plain readmes + readme_tree = previewable_readmes.first || plain_readmes.first + + # Return if we can't preview any of them if readme_tree.nil? return @readme = nil end @@ -31,6 +39,8 @@ class Tree git_repo = repository.raw_repository @readme = Gitlab::Git::Blob.find(git_repo, sha, readme_path) + @readme.load_all_data!(git_repo) + @readme end def trees diff --git a/app/models/user.rb b/app/models/user.rb index 4214f01f6a..b8d4841e65 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -138,18 +138,16 @@ class User < ActiveRecord::Base has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest" has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy has_one :abuse_report, dependent: :destroy + has_many :spam_logs, dependent: :destroy has_many :builds, dependent: :nullify, class_name: 'Ci::Build' - + has_many :todos, dependent: :destroy # # Validations # validates :name, presence: true - # Note that a 'uniqueness' and presence check is provided by devise :validatable for email. We do not need to - # duplicate that here as the validation framework will have duplicate errors in the event of a failure. - validates :email, presence: true, email: { strict_mode: true } - validates :notification_email, presence: true, email: { strict_mode: true } - validates :public_email, presence: true, email: { strict_mode: true }, allow_blank: true, uniqueness: true + validates :notification_email, presence: true, email: true + validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true validates :bio, length: { maximum: 255 }, allow_blank: true validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 } validates :username, @@ -356,11 +354,12 @@ class User < ActiveRecord::Base def disable_two_factor! update_attributes( - two_factor_enabled: false, - encrypted_otp_secret: nil, - encrypted_otp_secret_iv: nil, - encrypted_otp_secret_salt: nil, - otp_backup_codes: nil + two_factor_enabled: false, + encrypted_otp_secret: nil, + encrypted_otp_secret_iv: nil, + encrypted_otp_secret_salt: nil, + otp_grace_period_started_at: nil, + otp_backup_codes: nil ) end @@ -604,6 +603,13 @@ class User < ActiveRecord::Base end end + def try_obtain_ldap_lease + # After obtaining this lease LDAP checks will be blocked for 600 seconds + # (10 minutes) for this user. + lease = Gitlab::ExclusiveLease.new("user_ldap_check:#{id}", timeout: 600) + lease.try_obtain + end + def solo_owned_groups @solo_owned_groups ||= owned_groups.select do |group| group.owners == [self] diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 2a65f0431c..dbd70dc5a4 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -110,7 +110,7 @@ class WikiPage # Returns boolean True or False if this instance # is an old version of the page. def historical? - @page.historical? + @page.historical? && versions.first.sha != version.sha end # Returns boolean True or False if this instance diff --git a/app/services/base_service.rb b/app/services/base_service.rb index b48ca67d4d..8563633816 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -23,6 +23,10 @@ class BaseService EventCreateService.new end + def todo_service + TodoService.new + end + def log_info(message) Gitlab::AppLogger.info message end diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb index ad901f2da5..002f7ba127 100644 --- a/app/services/ci/create_builds_service.rb +++ b/app/services/ci/create_builds_service.rb @@ -34,6 +34,7 @@ module Ci build = commit.builds.create!(build_attrs) build.execute_hooks + build end end end diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb index f469b13e90..005a5c4661 100644 --- a/app/services/ci/image_for_build_service.rb +++ b/app/services/ci/image_for_build_service.rb @@ -1,28 +1,23 @@ module Ci class ImageForBuildService - def execute(project, params) - sha = params[:sha] - sha ||= - if params[:ref] - project.commit(params[:ref]).try(:sha) - end + def execute(project, opts) + sha = opts[:sha] || ref_sha(project, opts[:ref]) commit = project.ci_commits.ordered.find_by(sha: sha) image_name = image_for_commit(commit) image_path = Rails.root.join('public/ci', image_name) - - OpenStruct.new( - path: image_path, - name: image_name - ) + OpenStruct.new(path: image_path, name: image_name) end private + def ref_sha(project, ref) + project.commit(ref).try(:sha) if ref + end + def image_for_commit(commit) return 'build-unknown.svg' unless commit - 'build-' + commit.status + ".svg" end end diff --git a/app/services/commits/revert_service.rb b/app/services/commits/revert_service.rb new file mode 100644 index 0000000000..43d1c766e3 --- /dev/null +++ b/app/services/commits/revert_service.rb @@ -0,0 +1,58 @@ +module Commits + class RevertService < ::BaseService + class ValidationError < StandardError; end + class ReversionError < StandardError; end + + def execute + @source_project = params[:source_project] || @project + @target_branch = params[:target_branch] + @commit = params[:commit] + @create_merge_request = params[:create_merge_request].present? + + validate and commit + rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError, + ValidationError, ReversionError => ex + error(ex.message) + end + + def commit + revert_into = @create_merge_request ? @commit.revert_branch_name : @target_branch + + if @create_merge_request + # Temporary branch exists and contains the revert commit + return success if repository.find_branch(revert_into) + + create_target_branch + end + + unless repository.revert(current_user, @commit, revert_into) + error_msg = "Sorry, we cannot revert this #{params[:revert_type_title]} automatically. + It may have already been reverted, or a more recent commit may have updated some of its content." + raise ReversionError, error_msg + end + + success + end + + private + + def create_target_branch + result = CreateBranchService.new(@project, current_user) + .execute(@commit.revert_branch_name, @target_branch, source_project: @source_project) + + if result[:status] == :error + raise ReversionError, "There was an error creating the source branch: #{result[:message]}" + end + end + + def validate + allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch) + + unless allowed + raise_error('You are not allowed to push into this branch') + end + + true + end + end +end diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index c0e08a151f..707c2f7ff8 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -29,11 +29,7 @@ class CreateBranchService < BaseService end if new_branch - push_data = build_push_data(project, current_user, new_branch) - - project.execute_hooks(push_data.dup, :push_hooks) - project.execute_services(push_data.dup, :push_hooks) - + # GitPushService handles execution of services and hooks for branch pushes success(new_branch) else error('Invalid reference name') diff --git a/app/services/create_spam_log_service.rb b/app/services/create_spam_log_service.rb new file mode 100644 index 0000000000..59a66fde47 --- /dev/null +++ b/app/services/create_spam_log_service.rb @@ -0,0 +1,13 @@ +class CreateSpamLogService < BaseService + def initialize(project, user, params) + super(project, user, params) + end + + def execute + spam_params = params.merge({ user_id: @current_user.id, + project_id: @project.id } ) + spam_log = SpamLog.new(spam_params) + spam_log.save + spam_log + end +end diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index 004b3ce728..fae069ee4a 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -25,11 +25,7 @@ class DeleteBranchService < BaseService end if repository.rm_branch(current_user, branch_name) - push_data = build_push_data(branch) - - project.execute_hooks(push_data.dup, :push_hooks) - project.execute_services(push_data.dup, :push_hooks) - + # GitPushService handles execution of services and hooks for branch pushes success('Branch was removed') else error('Failed to remove branch') diff --git a/app/services/delete_user_service.rb b/app/services/delete_user_service.rb index e622fd5ea5..173e50c920 100644 --- a/app/services/delete_user_service.rb +++ b/app/services/delete_user_service.rb @@ -13,7 +13,7 @@ class DeleteUserService user.personal_projects.each do |project| # Skip repository removal because we remove directory with namespace # that contain all this repositories - ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute + ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete! end user.destroy diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb index d929a67629..9189de390a 100644 --- a/app/services/destroy_group_service.rb +++ b/app/services/destroy_group_service.rb @@ -9,7 +9,7 @@ class DestroyGroupService @group.projects.each do |project| # Skip repository removal because we remove directory with namespace # that contain all this repositories - ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute + ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete! end @group.destroy diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index d7ea30bc31..a1711d234f 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -1,10 +1,10 @@ -class GitPushService - attr_accessor :project, :user, :push_data, :push_commits +class GitPushService < BaseService + attr_accessor :push_data, :push_commits include Gitlab::CurrentSettings include Gitlab::Access # This method will be called after each git update - # and only if the provided user and project is present in GitLab. + # and only if the provided user and project are present in GitLab. # # All callbacks for post receive action should be placed here. # @@ -15,62 +15,67 @@ class GitPushService # 4. Executes the project's web hooks # 5. Executes the project's services # - def execute(project, user, oldrev, newrev, ref) - @project, @user = project, user + def execute + @project.repository.expire_cache(branch_name) - project.repository.expire_cache - - if push_remove_branch?(ref, newrev) + if push_remove_branch? + @project.repository.expire_has_visible_content_cache @push_commits = [] - elsif push_to_new_branch?(ref, oldrev) + elsif push_to_new_branch? + @project.repository.expire_has_visible_content_cache + # Re-find the pushed commits. - if is_default_branch?(ref) + if is_default_branch? # Initial push to the default branch. Take the full history of that branch as "newly pushed". - @push_commits = project.repository.commits(newrev) - - # Ensure HEAD points to the default branch in case it is not master - branch_name = Gitlab::Git.ref_name(ref) - project.change_head(branch_name) - - # Set protection on the default branch if configured - if (current_application_settings.default_branch_protection != PROTECTION_NONE) - developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false - project.protected_branches.create({ name: project.default_branch, developers_can_push: developers_can_push }) - end + process_default_branch else # Use the pushed commits that aren't reachable by the default branch # as a heuristic. This may include more commits than are actually pushed, but # that shouldn't matter because we check for existing cross-references later. - @push_commits = project.repository.commits_between(project.default_branch, newrev) + @push_commits = @project.repository.commits_between(@project.default_branch, params[:newrev]) # don't process commits for the initial push to the default branch - process_commit_messages(ref) + process_commit_messages end - elsif push_to_existing_branch?(ref, oldrev) + elsif push_to_existing_branch? # Collect data for this git push - @push_commits = project.repository.commits_between(oldrev, newrev) - process_commit_messages(ref) + @push_commits = @project.repository.commits_between(params[:oldrev], params[:newrev]) + process_commit_messages end - # Update merge requests that may be affected by this push. A new branch # could cause the last commit of a merge request to change. - project.update_merge_requests(oldrev, newrev, ref, @user) - - @push_data = build_push_data(oldrev, newrev, ref) - - EventCreateService.new.push(project, user, @push_data) - project.execute_hooks(@push_data.dup, :push_hooks) - project.execute_services(@push_data.dup, :push_hooks) - CreateCommitBuildsService.new.execute(project, @user, @push_data) - ProjectCacheWorker.perform_async(project.id) + update_merge_requests end protected + def update_merge_requests + @project.update_merge_requests(params[:oldrev], params[:newrev], params[:ref], current_user) + + EventCreateService.new.push(@project, current_user, build_push_data) + @project.execute_hooks(build_push_data.dup, :push_hooks) + @project.execute_services(build_push_data.dup, :push_hooks) + CreateCommitBuildsService.new.execute(@project, current_user, build_push_data) + ProjectCacheWorker.perform_async(@project.id) + end + + def process_default_branch + @push_commits = project.repository.commits(params[:newrev]) + + # Ensure HEAD points to the default branch in case it is not master + project.change_head(branch_name) + + # Set protection on the default branch if configured + if (current_application_settings.default_branch_protection != PROTECTION_NONE) + developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false + @project.protected_branches.create({ name: @project.default_branch, developers_can_push: developers_can_push }) + end + end + # Extract any GFM references from the pushed commit messages. If the configured issue-closing regex is matched, # close the referenced Issue. Create cross-reference Notes corresponding to any other referenced Mentionables. - def process_commit_messages(ref) - is_default_branch = is_default_branch?(ref) + def process_commit_messages + is_default_branch = is_default_branch? authors = Hash.new do |hash, commit| email = commit.author_email @@ -89,7 +94,7 @@ class GitPushService # Close issues if these commits were pushed to the project's default branch and the commit message matches the # closing regex. Exclude any mentioned Issues from cross-referencing even if the commits are being pushed to # a different branch. - closed_issues = commit.closes_issues(user) + closed_issues = commit.closes_issues(current_user) closed_issues.each do |issue| Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit) end @@ -99,34 +104,38 @@ class GitPushService end end - def build_push_data(oldrev, newrev, ref) - Gitlab::PushDataBuilder. - build(project, user, oldrev, newrev, ref, push_commits) + def build_push_data + @push_data ||= Gitlab::PushDataBuilder. + build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], push_commits) end - def push_to_existing_branch?(ref, oldrev) + def push_to_existing_branch? # Return if this is not a push to a branch (e.g. new commits) - Gitlab::Git.branch_ref?(ref) && !Gitlab::Git.blank_ref?(oldrev) + Gitlab::Git.branch_ref?(params[:ref]) && !Gitlab::Git.blank_ref?(params[:oldrev]) end - def push_to_new_branch?(ref, oldrev) - Gitlab::Git.branch_ref?(ref) && Gitlab::Git.blank_ref?(oldrev) + def push_to_new_branch? + Gitlab::Git.branch_ref?(params[:ref]) && Gitlab::Git.blank_ref?(params[:oldrev]) end - def push_remove_branch?(ref, newrev) - Gitlab::Git.branch_ref?(ref) && Gitlab::Git.blank_ref?(newrev) + def push_remove_branch? + Gitlab::Git.branch_ref?(params[:ref]) && Gitlab::Git.blank_ref?(params[:newrev]) end - def push_to_branch?(ref) - Gitlab::Git.branch_ref?(ref) + def push_to_branch? + Gitlab::Git.branch_ref?(params[:ref]) end - def is_default_branch?(ref) - Gitlab::Git.branch_ref?(ref) && - (Gitlab::Git.ref_name(ref) == project.default_branch || project.default_branch.nil?) + def is_default_branch? + Gitlab::Git.branch_ref?(params[:ref]) && + (Gitlab::Git.ref_name(params[:ref]) == project.default_branch || project.default_branch.nil?) end def commit_user(commit) - commit.author || user + commit.author || current_user + end + + def branch_name + @branch_name ||= Gitlab::Git.ref_name(params[:ref]) end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 2556f06e2d..ca87dca4a7 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -54,7 +54,7 @@ class IssuableBaseService < BaseService if params.present? && issuable.update_attributes(params.merge(updated_by: current_user)) issuable.reset_events_cache handle_common_system_notes(issuable, old_labels: old_labels) - handle_changes(issuable) + handle_changes(issuable, old_labels: old_labels) issuable.create_new_cross_references!(current_user) execute_hooks(issuable, 'update') end @@ -71,6 +71,19 @@ class IssuableBaseService < BaseService end end + def has_changes?(issuable, options = {}) + valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch] + + attrs_changed = valid_attrs.any? do |attr| + issuable.previous_changes.include?(attr.to_s) + end + + old_labels = options[:old_labels] + labels_changed = old_labels && issuable.labels != old_labels + + attrs_changed || labels_changed + end + def handle_common_system_notes(issuable, options = {}) if issuable.previous_changes.include?('title') create_title_change_note(issuable, issuable.previous_changes['title'].first) diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb index a1a20e4768..78254b49af 100644 --- a/app/services/issues/close_service.rb +++ b/app/services/issues/close_service.rb @@ -3,6 +3,7 @@ module Issues def execute(issue, commit = nil) if project.jira_tracker? && project.jira_service.active project.jira_service.execute(commit, issue) + todo_service.close_issue(issue, current_user) return issue end @@ -10,6 +11,7 @@ module Issues event_service.close_issue(issue, current_user) create_note(issue, commit) notification_service.close_issue(issue, current_user) + todo_service.close_issue(issue, current_user) execute_hooks(issue, 'close') end diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index bcb380d321..10787e8873 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -9,6 +9,7 @@ module Issues if issue.save issue.update_attributes(label_ids: label_params) notification_service.new_issue(issue, current_user) + todo_service.new_issue(issue, current_user) event_service.open_issue(issue, current_user) issue.create_cross_references!(current_user) execute_hooks(issue, 'open') diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index a55a04dd5e..51ef9dfe61 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -4,7 +4,16 @@ module Issues update(issue) end - def handle_changes(issue) + def handle_changes(issue, options = {}) + if has_changes?(issue, options) + todo_service.mark_pending_todos_as_done(issue, current_user) + end + + if issue.previous_changes.include?('title') || + issue.previous_changes.include?('description') + todo_service.update_issue(issue, current_user) + end + if issue.previous_changes.include?('milestone_id') create_milestone_note(issue) end @@ -12,6 +21,7 @@ module Issues if issue.previous_changes.include?('assignee_id') create_assignee_note(issue) notification_service.reassigned_issue(issue, current_user) + todo_service.reassigned_issue(issue, current_user) end end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index a9b29f9654..c0700d953d 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -56,7 +56,7 @@ module MergeRequests if commits && commits.count == 1 commit = commits.first merge_request.title = commit.title - merge_request.description = commit.description.try(:strip) + merge_request.description ||= commit.description.try(:strip) else merge_request.title = merge_request.source_branch.titleize.humanize end diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb index 47454f9f0c..27ee81fe3e 100644 --- a/app/services/merge_requests/close_service.rb +++ b/app/services/merge_requests/close_service.rb @@ -9,6 +9,7 @@ module MergeRequests event_service.close_mr(merge_request, current_user) create_note(merge_request) notification_service.close_mr(merge_request, current_user) + todo_service.close_merge_request(merge_request, current_user) execute_hooks(merge_request, 'close') end diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index 009d5a6867..33609d01f2 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -2,7 +2,7 @@ module MergeRequests class CreateService < MergeRequests::BaseService def execute # @project is used to determine whether the user can set the merge request's - # assignee, milestone and labels. Whether they can depends on their + # assignee, milestone and labels. Whether they can depends on their # permissions on the target project. source_project = @project @project = Project.find(params[:target_project_id]) if params[:target_project_id] @@ -18,6 +18,7 @@ module MergeRequests merge_request.update_attributes(label_ids: label_params) event_service.open_mr(merge_request, current_user) notification_service.new_merge_request(merge_request, current_user) + todo_service.new_merge_request(merge_request, current_user) merge_request.create_cross_references!(current_user) execute_hooks(merge_request) end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index e8bef250d8..9a58383b39 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -34,7 +34,8 @@ module MergeRequests committer: committer } - repository.merge(current_user, merge_request.source_sha, merge_request.target_branch, options) + commit_id = repository.merge(current_user, merge_request.source_sha, merge_request.target_branch, options) + merge_request.update(merge_commit_sha: commit_id) rescue StandardError => e merge_request.update(merge_error: "Something went wrong during merge") Rails.logger.error(e.message) diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb index 5cf7404a49..531bbc9b06 100644 --- a/app/services/merge_requests/merge_when_build_succeeds_service.rb +++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb @@ -19,8 +19,8 @@ module MergeRequests end # Triggers the automatic merge of merge_request once the build succeeds - def trigger(build) - merge_requests = merge_request_from(build) + def trigger(commit_status) + merge_requests = merge_request_from(commit_status) merge_requests.each do |merge_request| next unless merge_request.merge_when_build_succeeds? @@ -45,9 +45,14 @@ module MergeRequests private - def merge_request_from(build) - merge_requests = @project.origin_merge_requests.opened.where(source_branch: build.ref).to_a - merge_requests += @project.fork_merge_requests.opened.where(source_branch: build.ref).to_a + def merge_request_from(commit_status) + branches = commit_status.ref + + # This is for ref-less builds + branches ||= @project.repository.branch_names_contains(commit_status.sha) + + merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a + merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a merge_requests.uniq.select(&:source_project) end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 5ff2cc03dd..6319ad805b 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -14,7 +14,16 @@ module MergeRequests update(merge_request) end - def handle_changes(merge_request) + def handle_changes(merge_request, options = {}) + if has_changes?(merge_request, options) + todo_service.mark_pending_todos_as_done(merge_request, current_user) + end + + if merge_request.previous_changes.include?('title') || + merge_request.previous_changes.include?('description') + todo_service.update_merge_request(merge_request, current_user) + end + if merge_request.previous_changes.include?('target_branch') create_branch_change_note(merge_request, 'target', merge_request.previous_changes['target_branch'].first, @@ -28,6 +37,7 @@ module MergeRequests if merge_request.previous_changes.include?('assignee_id') create_assignee_note(merge_request) notification_service.reassigned_merge_request(merge_request, current_user) + todo_service.reassigned_merge_request(merge_request, current_user) end if merge_request.previous_changes.include?('target_branch') || diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index a8486e6a5a..2bb312bb25 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -6,27 +6,12 @@ module Notes note.system = false if note.save - notification_service.new_note(note) - - # Skip system notes, like status changes and cross-references and awards - unless note.system || note.is_award - event_service.leave_note(note, note.author) - note.create_cross_references! - execute_hooks(note) - end + # Finish the harder work in the background + NewNoteWorker.perform_in(2.seconds, note.id, params) + TodoService.new.new_note(note, current_user) end note end - - def hook_data(note) - Gitlab::NoteDataBuilder.build(note, current_user) - end - - def execute_hooks(note) - note_data = hook_data(note) - note.project.execute_hooks(note_data, :note_hooks) - note.project.execute_services(note_data, :note_hooks) - end end end diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb new file mode 100644 index 0000000000..e818f58d13 --- /dev/null +++ b/app/services/notes/post_process_service.rb @@ -0,0 +1,28 @@ +module Notes + class PostProcessService + attr_accessor :note + + def initialize(note) + @note = note + end + + def execute + # Skip system notes, like status changes and cross-references and awards + unless @note.system || @note.is_award + EventCreateService.new.leave_note(@note, @note.author) + @note.create_cross_references! + execute_note_hooks + end + end + + def hook_data + Gitlab::NoteDataBuilder.build(@note, @note.author) + end + + def execute_note_hooks + note_data = hook_data + @note.project.execute_hooks(note_data, :note_hooks) + @note.project.execute_services(note_data, :note_hooks) + end + end +end diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb index 72e2f78008..1361b1e030 100644 --- a/app/services/notes/update_service.rb +++ b/app/services/notes/update_service.rb @@ -7,6 +7,10 @@ module Notes note.create_new_cross_references!(current_user) note.reset_events_cache + if note.previous_changes.include?('note') + TodoService.new.update_note(note, current_user) + end + note end end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 28872c8925..f4dcb14285 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -6,15 +6,25 @@ module Projects DELETED_FLAG = '+deleted' + def pending_delete! + project.update_attribute(:pending_delete, true) + + ProjectDestroyWorker.perform_in(1.minute, project.id, current_user.id, params) + end + def execute return false unless can?(current_user, :remove_project, project) project.team.truncate - project.repository.expire_cache unless project.empty_repo? repo_path = project.path_with_namespace wiki_path = repo_path + '.wiki' + # Flush the cache for both repositories. This has to be done _before_ + # removing the physical repositories as some expiration code depends on + # Git data (e.g. a list of branch names). + flush_caches(project, wiki_path) + Project.transaction do project.destroy! @@ -64,5 +74,13 @@ module Projects def removal_path(path) "#{path}+#{project.id}#{DELETED_FLAG}" end + + def flush_caches(project, wiki_path) + project.repository.expire_all_caches! if project.repository.exists? + + wiki_repo = Repository.new(wiki_path, project) + + wiki_repo.expire_all_caches! if wiki_repo.exists? + end end end diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb new file mode 100644 index 0000000000..2015897dd1 --- /dev/null +++ b/app/services/projects/import_service.rb @@ -0,0 +1,67 @@ +module Projects + class ImportService < BaseService + include Gitlab::ShellAdapter + + class Error < StandardError; end + + ALLOWED_TYPES = [ + 'bitbucket', + 'fogbugz', + 'gitlab', + 'github', + 'google_code' + ] + + def execute + if unknown_url? + # In this case, we only want to import issues, not a repository. + create_repository + else + import_repository + end + + import_data + + success + rescue Error => e + error(e.message) + end + + private + + def create_repository + unless project.create_repository + raise Error, 'The repository could not be created.' + end + end + + def import_repository + begin + gitlab_shell.import_repository(project.path_with_namespace, project.import_url) + rescue Gitlab::Shell::Error => e + raise Error, e.message + end + end + + def import_data + return unless has_importer? + + unless importer.execute + raise Error, 'The remote data could not be imported.' + end + end + + def has_importer? + ALLOWED_TYPES.include?(project.import_type) + end + + def importer + class_name = "Gitlab::#{project.import_type.camelize}Import::Importer" + class_name.constantize.new(project) + end + + def unknown_url? + project.import_url == Project::UNKNOWN_IMPORT_URL + end + end +end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 1083bcec05..edced01081 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -274,12 +274,15 @@ class SystemNoteService # Check if a cross reference to a noteable from a mentioner already exists # # This method is used to prevent multiple notes being created for a mention - # when a issue is updated, for example. + # when a issue is updated, for example. The method also calls notes_for_mentioner + # to check if the mentioner is a commit, and return matches only on commit hash + # instead of project + commit, to avoid repeated mentions from forks. # # noteable - Noteable object being referenced # mentioner - Mentionable object # # Returns Boolean + def self.cross_reference_exists?(noteable, mentioner) # Initial scope should be system notes of this noteable type notes = Note.system.where(noteable_type: noteable.class) @@ -291,14 +294,20 @@ class SystemNoteService notes = notes.where(noteable_id: noteable.id) end - gfm_reference = mentioner.gfm_reference(noteable.project) - notes = notes.where(note: cross_reference_note_content(gfm_reference)) - - notes.count > 0 + notes_for_mentioner(mentioner, noteable, notes).count > 0 end private + def self.notes_for_mentioner(mentioner, noteable, notes) + if mentioner.is_a?(Commit) + notes.where('note LIKE ?', "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}") + else + gfm_reference = mentioner.gfm_reference(noteable.project) + notes.where(note: cross_reference_note_content(gfm_reference)) + end + end + def self.create_note(args = {}) Note.create(args.merge(system: true)) end diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb new file mode 100644 index 0000000000..4392e2d17f --- /dev/null +++ b/app/services/todo_service.rb @@ -0,0 +1,170 @@ +# TodoService class +# +# Used for creating todos after certain user actions +# +# Ex. +# TodoService.new.new_issue(issue, current_user) +# +class TodoService + # When create an issue we should: + # + # * create a todo for assignee if issue is assigned + # * create a todo for each mentioned user on issue + # + def new_issue(issue, current_user) + new_issuable(issue, current_user) + end + + # When update an issue we should: + # + # * mark all pending todos related to the issue for the current user as done + # + def update_issue(issue, current_user) + create_mention_todos(issue.project, issue, current_user) + end + + # When close an issue we should: + # + # * mark all pending todos related to the target for the current user as done + # + def close_issue(issue, current_user) + mark_pending_todos_as_done(issue, current_user) + end + + # When we reassign an issue we should: + # + # * create a pending todo for new assignee if issue is assigned + # + def reassigned_issue(issue, current_user) + create_assignment_todo(issue, current_user) + end + + # When create a merge request we should: + # + # * creates a pending todo for assignee if merge request is assigned + # * create a todo for each mentioned user on merge request + # + def new_merge_request(merge_request, current_user) + new_issuable(merge_request, current_user) + end + + # When update a merge request we should: + # + # * create a todo for each mentioned user on merge request + # + def update_merge_request(merge_request, current_user) + create_mention_todos(merge_request.project, merge_request, current_user) + end + + # When close a merge request we should: + # + # * mark all pending todos related to the target for the current user as done + # + def close_merge_request(merge_request, current_user) + mark_pending_todos_as_done(merge_request, current_user) + end + + # When we reassign a merge request we should: + # + # * creates a pending todo for new assignee if merge request is assigned + # + def reassigned_merge_request(merge_request, current_user) + create_assignment_todo(merge_request, current_user) + end + + # When merge a merge request we should: + # + # * mark all pending todos related to the target for the current user as done + # + def merge_merge_request(merge_request, current_user) + mark_pending_todos_as_done(merge_request, current_user) + end + + # When create a note we should: + # + # * mark all pending todos related to the noteable for the note author as done + # * create a todo for each mentioned user on note + # + def new_note(note, current_user) + handle_note(note, current_user) + end + + # When update a note we should: + # + # * mark all pending todos related to the noteable for the current user as done + # * create a todo for each new user mentioned on note + # + def update_note(note, current_user) + handle_note(note, current_user) + end + + # When marking pending todos as done we should: + # + # * mark all pending todos related to the target for the current user as done + # + def mark_pending_todos_as_done(target, user) + pending_todos(user, target.project, target).update_all(state: :done) + end + + private + + def create_todos(project, target, author, users, action, note = nil) + Array(users).each do |user| + next if pending_todos(user, project, target).exists? + + Todo.create( + project: project, + user_id: user.id, + author_id: author.id, + target_id: target.id, + target_type: target.class.name, + action: action, + note: note + ) + end + end + + def new_issuable(issuable, author) + create_assignment_todo(issuable, author) + create_mention_todos(issuable.project, issuable, author) + end + + def handle_note(note, author) + # Skip system notes, notes on commit, and notes on project snippet + return if note.system? || ['Commit', 'Snippet'].include?(note.noteable_type) + + project = note.project + target = note.noteable + + mark_pending_todos_as_done(target, author) + create_mention_todos(project, target, author, note) + end + + def create_assignment_todo(issuable, author) + if issuable.assignee && issuable.assignee != author + create_todos(issuable.project, issuable, author, issuable.assignee, Todo::ASSIGNED) + end + end + + def create_mention_todos(project, issuable, author, note = nil) + mentioned_users = filter_mentioned_users(project, note || issuable, author) + create_todos(project, issuable, author, mentioned_users, Todo::MENTIONED, note) + end + + def filter_mentioned_users(project, target, author) + mentioned_users = target.mentioned_users.select do |user| + user.can?(:read_project, project) + end + + mentioned_users.delete(author) + mentioned_users.uniq + end + + def pending_todos(user, project, target) + user.todos.pending.where( + project_id: project.id, + target_id: target.id, + target_type: target.class.name + ) + end +end diff --git a/app/validators/email_validator.rb b/app/validators/email_validator.rb index b35af10080..aab07a7ece 100644 --- a/app/validators/email_validator.rb +++ b/app/validators/email_validator.rb @@ -1,18 +1,5 @@ -# EmailValidator -# -# Based on https://github.com/balexand/email_validator -# -# Extended to use only strict mode with following allowed characters: -# ' - apostrophe -# -# See http://www.remote.org/jochen/mail/info/chars.html -# class EmailValidator < ActiveModel::EachValidator - PATTERN = /\A\s*([-a-z0-9+._']{1,64})@((?:[-a-z0-9]+\.)+[a-z]{2,})\s*\z/i.freeze - def validate_each(record, attribute, value) - unless value =~ PATTERN - record.errors.add(attribute, options[:message] || :invalid) - end + record.errors.add(attribute, :invalid) unless value =~ Devise.email_regexp end end diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml new file mode 100644 index 0000000000..6f325914d1 --- /dev/null +++ b/app/views/admin/appearances/_form.html.haml @@ -0,0 +1,58 @@ += form_for @appearance, url: admin_appearances_path, html: { class: 'form-horizontal'} do |f| + - if @appearance.errors.any? + .alert.alert-danger + - @appearance.errors.full_messages.each do |msg| + %p= msg + + %fieldset.sign-in + %legend + Sign in/Sign up pages: + .form-group + = f.label :title, class: 'control-label' + .col-sm-10 + = f.text_field :title, class: "form-control" + .form-group + = f.label :description, class: 'control-label' + .col-sm-10 + = f.text_area :description, class: "form-control", rows: 10 + .hint + Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('markdown', 'markdown'), target: '_blank'}. + .form-group + = f.label :logo, class: 'control-label' + .col-sm-10 + - if @appearance.logo? + = image_tag @appearance.logo_url, class: 'appearance-logo-preview' + - if @appearance.persisted? + %br + = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-logo" + %hr + = f.hidden_field :logo_cache + = f.file_field :logo, class: "" + .hint + Maximum file size is 1MB. Pages are optimized for a 640x360 px logo. + + %fieldset.app_logo + %legend + Navigation bar: + .form-group + = f.label :header_logo, 'Header logo', class: 'control-label' + .col-sm-10 + - if @appearance.header_logo? + = image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview' + - if @appearance.persisted? + %br + = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-logo" + %hr + = f.hidden_field :header_logo_cache + = f.file_field :header_logo, class: "" + .hint + Maximum file size is 1MB. Pages are optimized for a 72x72 px header logo + + .form-actions + = f.submit 'Save', class: 'btn btn-save' + - if @appearance.persisted? + = link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank' + + - if @appearance.updated_at + %span.pull-right + Last edit #{time_ago_with_tooltip(@appearance.updated_at)} diff --git a/app/views/admin/appearances/preview.html.haml b/app/views/admin/appearances/preview.html.haml new file mode 100644 index 0000000000..dd4a64e80b --- /dev/null +++ b/app/views/admin/appearances/preview.html.haml @@ -0,0 +1,29 @@ +- page_title "Preview | Appearance" +%h3.page-title + Appearance settings - Preview +%hr + +.ui-box + .title + Sign-in page + %div + .login-page + .container + .content + .login-title + %h1= brand_title + %hr + .container + .content + .row + .col-sm-7 + .brand-image + = brand_image + .brand_text + = brand_text + .col-sm-4 + .login-box + %h3.page-title Sign in + = text_field_tag :login, nil, class: "form-control top", placeholder: "Username or Email" + = password_field_tag :password, nil, class: "form-control bottom", placeholder: "Password" + = button_tag "Sign in", class: "btn-create btn" diff --git a/app/views/admin/appearances/show.html.haml b/app/views/admin/appearances/show.html.haml new file mode 100644 index 0000000000..089e8e4cb7 --- /dev/null +++ b/app/views/admin/appearances/show.html.haml @@ -0,0 +1,7 @@ +- page_title "Appearance" +%h3.page-title + Appearance settings +%p.light + You can modify the look and feel of GitLab here + += render 'form' diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index c4020c8273..b30dfd109e 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -14,11 +14,11 @@ .form-group.project-visibility-level-holder = f.label :default_project_visibility, class: 'control-label col-sm-2' .col-sm-10 - = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project) + = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new) .form-group.project-visibility-level-holder = f.label :default_snippet_visibility, class: 'control-label col-sm-2' .col-sm-10 - = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: PersonalSnippet) + = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: ProjectSnippet.new) .form-group = f.label :restricted_visibility_levels, class: 'control-label col-sm-2' .col-sm-10 @@ -47,6 +47,16 @@ = f.label :version_check_enabled do = f.check_box :version_check_enabled Version check enabled + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :email_author_in_body do + = f.check_box :email_author_in_body + Include author name in notification email body + .help-block + Some email servers do not support overriding the email sender name. + Enable this option to include the name of the author of the issue, + merge request or comment in the email body instead. .form-group = f.label :admin_notification_email, class: 'control-label col-sm-2' .col-sm-10 @@ -105,14 +115,14 @@ = f.check_box :signin_enabled Sign-in enabled .form-group - = f.label :two_factor_authentication, 'Two-Factor authentication', class: 'control-label col-sm-2' + = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2' .col-sm-10 .checkbox = f.label :require_two_factor_authentication do = f.check_box :require_two_factor_authentication - Require all users to setup Two-Factor authentication + Require all users to setup Two-factor authentication .form-group - = f.label :two_factor_authentication, 'Two-Factor grace period (hours)', class: 'control-label col-sm-2' + = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'control-label col-sm-2' .col-sm-10 = f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0' .help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication @@ -212,42 +222,43 @@ %fieldset %legend Spam and Anti-bot Protection - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :ip_blocking_enabled do - = f.check_box :ip_blocking_enabled - Enable IP check against blacklist at sign-up - .help-block Helps preventing accounts creation from 'known spam sources' - - .form-group - = f.label :dnsbl_servers_list, class: 'control-label col-sm-2' do - DNSBL servers list - .col-sm-10 - = f.text_field :dnsbl_servers_list, class: 'form-control' - .help-block - Please enter DNSBL servers separated with comma - .form-group .col-sm-offset-2.col-sm-10 .checkbox = f.label :recaptcha_enabled do = f.check_box :recaptcha_enabled Enable reCAPTCHA - %span.help-block#recaptcha_help_block Helps preventing bots from creating accounts + %span.help-block#recaptcha_help_block Helps prevent bots from creating accounts .form-group = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'control-label col-sm-2' .col-sm-10 = f.text_field :recaptcha_site_key, class: 'form-control' .help-block - Generate site and private keys here: - %a{ href: 'http://www.google.com/recaptcha', target: '_blank'} http://www.google.com/recaptcha + Generate site and private keys at + %a{ href: 'http://www.google.com/recaptcha', target: 'blank'} http://www.google.com/recaptcha + .form-group = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2' .col-sm-10 = f.text_field :recaptcha_private_key, class: 'form-control' + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :akismet_enabled do + = f.check_box :akismet_enabled + Enable Akismet + %span.help-block#akismet_help_block Helps prevent bots from creating issues + + .form-group + = f.label :akismet_api_key, 'Akismet API Key', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :akismet_api_key, class: 'form-control' + .help-block + Generate API key at + %a{ href: 'http://www.akismet.com', target: 'blank'} http://www.akismet.com + %fieldset %legend Error Reporting and Logging %p @@ -268,4 +279,4 @@ = f.text_field :sentry_dsn, class: 'form-control' .form-actions - = f.submit 'Save', class: 'btn btn-primary' + = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml index fa4e6335c7..e18f7b499d 100644 --- a/app/views/admin/applications/_form.html.haml +++ b/app/views/admin/applications/_form.html.haml @@ -22,5 +22,5 @@ %code= Doorkeeper.configuration.native_redirect_uri for local tests .form-actions - = f.submit 'Submit', class: "btn btn-primary wide" + = f.submit 'Submit', class: "btn btn-save wide" = link_to "Cancel", admin_applications_path, class: "btn btn-default" diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml index 953b8b6936..5c9403fa0c 100644 --- a/app/views/admin/broadcast_messages/_form.html.haml +++ b/app/views/admin/broadcast_messages/_form.html.haml @@ -1,6 +1,7 @@ .broadcast-message-preview{ style: broadcast_message_style(@broadcast_message) } = icon('bullhorn') - %span= @broadcast_message.message || "Your message here" + .js-broadcast-message-preview + = render_broadcast_message(@broadcast_message.message.presence || "Your message here") = form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-requires-input'} do |f| -if @broadcast_message.errors.any? @@ -10,7 +11,9 @@ .form-group = f.label :message, class: 'control-label' .col-sm-10 - = f.text_area :message, class: "form-control js-quick-submit", rows: 2, required: true + = f.text_area :message, class: "form-control js-quick-submit js-autosize", + required: true, + data: { preview_path: preview_admin_broadcast_messages_path } .form-group.js-toggle-colors-container .col-sm-10.col-sm-offset-2 = link_to 'Customize colors', '#', class: 'js-toggle-colors-link' diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml index 49e33698b6..c05538a393 100644 --- a/app/views/admin/broadcast_messages/index.html.haml +++ b/app/views/admin/broadcast_messages/index.html.haml @@ -34,4 +34,4 @@ = link_to icon('pencil-square-o'), edit_admin_broadcast_message_path(message), title: 'Edit', class: 'btn btn-xs' = link_to icon('times'), admin_broadcast_message_path(message), method: :delete, remote: true, title: 'Remove', class: 'js-remove-tr btn btn-xs btn-danger' - = paginate @broadcast_messages + = paginate @broadcast_messages, theme: 'gitlab' diff --git a/app/views/admin/broadcast_messages/preview.js.haml b/app/views/admin/broadcast_messages/preview.js.haml new file mode 100644 index 0000000000..fbc9453c72 --- /dev/null +++ b/app/views/admin/broadcast_messages/preview.js.haml @@ -0,0 +1 @@ +$('.js-broadcast-message-preview').html("#{j(render_broadcast_message(@message))}"); diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml index c395bd908c..34d955568f 100644 --- a/app/views/admin/builds/_build.html.haml +++ b/app/views/admin/builds/_build.html.haml @@ -4,7 +4,7 @@ = ci_status_with_icon(build.status) %td.build-link - - if build.target_url + - if can?(current_user, :read_build, project) && build.target_url = link_to build.target_url do %strong Build ##{build.id} - else @@ -60,10 +60,10 @@ %td .pull-right - - if current_user && can?(current_user, :read_build_artifacts, project) && build.artifacts? + - if can?(current_user, :read_build, project) && build.artifacts? = link_to build.artifacts_download_url, title: 'Download artifacts' do %i.fa.fa-download - - if current_user && can?(current_user, :manage_builds, build.project) + - if can?(current_user, :update_build, build.project) - if build.active? - if build.cancel_url = link_to build.cancel_url, method: :post, title: 'Cancel' do diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml index ebf2b7b60e..5931efdefe 100644 --- a/app/views/admin/builds/index.html.haml +++ b/app/views/admin/builds/index.html.haml @@ -1,9 +1,4 @@ -.project-issuable-filter - .controls - .pull-left.hidden-xs - - if @all_builds.running_or_pending.any? - = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post - +.top-area %ul.nav-links %li{class: ('active' if @scope.nil?)} = link_to admin_builds_path do @@ -20,7 +15,11 @@ Finished %span.badge.js-running-count= number_with_delimiter(@all_builds.finished.count(:id)) -.gray-content-block + .nav-controls + - if @all_builds.running_or_pending.any? + = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post + +.gray-content-block.second-block #{(@scope || 'running').capitalize} builds %ul.content-list diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index cc389c3ae0..3274ba5377 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -92,6 +92,11 @@ Rails %span.pull-right #{Rails::VERSION::STRING} + + %p + = Gitlab::Database.adapter_name + %span.pull-right + = Gitlab::Database.version %hr .row .col-sm-4 diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml index 841e6971fb..41c4389997 100644 --- a/app/views/admin/deploy_keys/index.html.haml +++ b/app/views/admin/deploy_keys/index.html.haml @@ -2,7 +2,7 @@ .panel.panel-default .panel-heading Public deploy keys (#{@deploy_keys.count}) - .panel-head-actions + .controls = link_to 'New Deploy Key', new_admin_deploy_key_path, class: "btn btn-new btn-sm" - if @deploy_keys.any? .table-holder diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index 8de2ba74a7..198026a1f7 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -21,6 +21,5 @@ - else .form-actions - = f.submit 'Save changes', class: "btn btn-primary" + = f.submit 'Save changes', class: "btn btn-save" = link_to 'Cancel', admin_group_path(@group), class: "btn btn-cancel" - diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index 3940210e19..118d3cfea0 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -17,7 +17,7 @@ .pull-right .dropdown.inline %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %span.light sort: + %span.light - if @sort.present? = sort_options_hash[@sort] - else diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index b120f4dea6..53b3cd04c6 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -37,8 +37,7 @@ - @hooks.each do |hook| %li .list-item-name - = link_to admin_hook_path(hook) do - %strong= hook.url + %strong= hook.url %p SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} .pull-right diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml index eaa94ed9e3..8c6b389bf1 100644 --- a/app/views/admin/labels/_form.html.haml +++ b/app/views/admin/labels/_form.html.haml @@ -11,6 +11,10 @@ = f.label :title, class: 'control-label' .col-sm-10 = f.text_field :title, class: "form-control", required: true + .form-group + = f.label :description, class: 'control-label' + .col-sm-10 + = f.text_field :description, class: "form-control js-quick-submit" .form-group = f.label :color, "Background color", class: 'control-label' .col-sm-10 diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml index e3ccbf6c3a..5736a30191 100644 --- a/app/views/admin/labels/_label.html.haml +++ b/app/views/admin/labels/_label.html.haml @@ -1,5 +1,7 @@ %li{id: dom_id(label)} - = render_colored_label(label) - .pull-right - = link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm' - = link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"} + .label-row + = render_colored_label(label) + = markdown(label.description, pipeline: :single_line) + .pull-right + = link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm' + = link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"} diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index d9b481404f..d39c0f4403 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -1,7 +1,7 @@ - page_title "Projects" = render 'shared/show_aside' -.row +.row.prepend-top-default %aside.col-md-3 .admin-filter = form_tag admin_namespaces_projects_path, method: :get, class: '' do @@ -47,10 +47,10 @@ .panel.panel-default .panel-heading Projects (#{@projects.total_count}) - .panel-head-actions + .controls .dropdown.inline %button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'} - %span.light sort: + %span.light - if @sort.present? = sort_options_hash[@sort] - else diff --git a/app/views/admin/spam_logs/_spam_log.html.haml b/app/views/admin/spam_logs/_spam_log.html.haml new file mode 100644 index 0000000000..8aea67f449 --- /dev/null +++ b/app/views/admin/spam_logs/_spam_log.html.haml @@ -0,0 +1,32 @@ +- user = spam_log.user +%tr + %td + = time_ago_with_tooltip(spam_log.created_at) + %td + - if user + = link_to user.name, [:admin, user] + .light.small + Joined #{time_ago_with_tooltip(user.created_at)} + - else + (removed) + %td + = spam_log.source_ip + %td + = spam_log.via_api? ? 'Y' : 'N' + %td + = spam_log.noteable_type + %td + = spam_log.title + %td + = truncate(spam_log.description, length: 100) + %td + - if user + = link_to 'Remove user', admin_spam_log_path(spam_log, remove_user: true), + data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove" + %td + - if user && !user.blocked? + = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs" + - else + .btn.btn-xs.disabled + Already Blocked + = link_to 'Remove log', [:admin, spam_log], remote: true, method: :delete, class: "btn btn-xs btn-close js-remove-tr" diff --git a/app/views/admin/spam_logs/index.html.haml b/app/views/admin/spam_logs/index.html.haml new file mode 100644 index 0000000000..0fdd5bd996 --- /dev/null +++ b/app/views/admin/spam_logs/index.html.haml @@ -0,0 +1,21 @@ +- page_title "Spam Logs" +%h3.page-title Spam Logs +%hr +- if @spam_logs.present? + .table-holder + %table.table + %thead + %tr + %th Date + %th User + %th Source IP + %th API? + %th Type + %th Title + %th Description + %th Primary Action + %th + = render @spam_logs + = paginate @spam_logs +- else + %h4 There are no Spam Logs diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index b050a4d01c..b6b1168bd3 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -32,7 +32,7 @@ .pull-right .dropdown.inline %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %span.light sort: + %span.light - if @sort.present? = sort_options_hash[@sort] - else diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml index 6ca97a692b..3d17f74b70 100644 --- a/app/views/dashboard/_groups_head.html.haml +++ b/app/views/dashboard/_groups_head.html.haml @@ -1,7 +1,13 @@ -%ul.nav-links - = nav_link(page: dashboard_groups_path) do - = link_to dashboard_groups_path, title: 'Your groups', data: {placement: 'right'} do - Your Groups - = nav_link(page: explore_groups_path) do - = link_to explore_groups_path, title: 'Explore groups', data: {placement: 'bottom'} do - Explore Groups +.top-area + %ul.nav-links + = nav_link(page: dashboard_groups_path) do + = link_to dashboard_groups_path, title: 'Your groups' do + Your Groups + = nav_link(page: explore_groups_path) do + = link_to explore_groups_path, title: 'Explore groups' do + Explore Groups + - if current_user.can_create_group? + .nav-controls + = link_to new_group_path, class: "btn btn-new" do + = icon('plus') + New Group diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 5c4b58cd68..4bc761b373 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -8,13 +8,15 @@ = nav_link(page: starred_dashboard_projects_path) do = link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do Starred Projects - = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path], html_options: { class: 'hidden-xs' }) do + = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do = link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do Explore Projects - .projects-search-form - = search_field_tag :filter_projects, nil, placeholder: 'Filter by name...', class: 'projects-list-filter form-control hidden-xs', spellcheck: false + .nav-controls + = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| + = search_field_tag :filter_projects, params[:filter_projects], placeholder: 'Filter by name...', class: 'project-filter-form-field form-control input-short projects-list-filter', spellcheck: false, id: 'project-filter-form-field' + = render 'explore/projects/dropdown' - if current_user.can_create_project? - = link_to new_project_path, class: 'btn btn-green' do - %i.fa.fa-plus + = link_to new_project_path, class: 'btn btn-new' do + = icon('plus') New Project diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index d5b7e729e7..caca91af53 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -2,15 +2,6 @@ - header_title "Groups", dashboard_groups_path = render 'dashboard/groups_head' -.gray-content-block - - if current_user.can_create_group? - %span.pull-right.hidden-xs - = link_to new_group_path, class: "btn btn-new" do - %i.fa.fa-plus - New Group - .oneline - Group members have access to all group projects. - %ul.content-list - @group_members.each do |group_member| - group = group_member.group diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index 2d3da01178..dfa5f80eef 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -4,20 +4,15 @@ - if current_user = auto_discovery_link_tag(:atom, issues_dashboard_url(format: :atom, private_token: current_user.private_token), title: "#{current_user.name} issues") -.project-issuable-filter - .controls - .pull-left - - if current_user - .hidden-xs.pull-left - = link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: 'btn' do - %i.fa.fa-rss - +.top-area + = render 'shared/issuable/nav', type: :issues + .nav-controls + - if current_user + = link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: 'btn' do + = icon('rss') = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" - = render 'shared/issuable/filter', type: :issues - -.gray-content-block.second-block - List all issues from all projects you have access to. += render 'shared/issuable/filter', type: :issues .prepend-top-default = render 'shared/issues' diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml index c5a5ec21f7..fb016599fe 100644 --- a/app/views/dashboard/merge_requests.html.haml +++ b/app/views/dashboard/merge_requests.html.haml @@ -1,14 +1,12 @@ - page_title "Merge Requests" - header_title "Merge Requests", merge_requests_dashboard_path(assignee_id: current_user.id) -.project-issuable-filter - .controls +.top-area + = render 'shared/issuable/nav', type: :merge_requests + .nav-controls = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request" - = render 'shared/issuable/filter', type: :merge_requests - -.gray-content-block.second-block - List all merge requests from all projects you have access to. += render 'shared/issuable/filter', type: :merge_requests .prepend-top-default = render 'shared/merge_requests' diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index bec1692a4d..917bfbd47e 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -1,14 +1,11 @@ - page_title "Milestones" - header_title "Milestones", dashboard_milestones_path -.project-issuable-filter - .controls - = render 'shared/new_project_item_select', path: 'milestones/new', label: "New Milestone", include_groups: true - +.top-area = render 'shared/milestones_filter' -.gray-content-block - List all milestones from all projects you have access to. + .nav-controls + = render 'shared/new_project_item_select', path: 'milestones/new', label: "New Milestone", include_groups: true .milestones %ul.content-list diff --git a/app/views/dashboard/projects/_projects.html.haml b/app/views/dashboard/projects/_projects.html.haml index cea9ffcc74..933a3edd0f 100644 --- a/app/views/dashboard/projects/_projects.html.haml +++ b/app/views/dashboard/projects/_projects.html.haml @@ -1,3 +1,6 @@ .projects-list-holder = render 'shared/projects/list', projects: @projects, ci: true + + :javascript + Dashboard.init() diff --git a/app/views/dashboard/projects/index.atom.builder b/app/views/dashboard/projects/index.atom.builder index 2e2712c514..d4daf07c6c 100644 --- a/app/views/dashboard/projects/index.atom.builder +++ b/app/views/dashboard/projects/index.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: dashboard_projects_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html" xml.id dashboard_projects_url - xml.updated @events.latest_update_time.xmlschema if @events.any? + xml.updated @events[0].updated_at.xmlschema if @events[0] @events.each do |event| event_to_atom(xml, event) diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index 53abf274bd..4565e752c1 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -10,7 +10,7 @@ - if @last_push = render "events/event_last_push", event: @last_push -- if @projects.any? +- if @projects.any? || params[:filter_projects] = render 'projects' - else = render "zero_authorized_projects" diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml new file mode 100644 index 0000000000..f38caf1826 --- /dev/null +++ b/app/views/dashboard/todos/_todo.html.haml @@ -0,0 +1,24 @@ +%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo) } + .todo-item{class: 'todo-block'} + = image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:'' + + .todo-title + %span.author-name + - if todo.author + = link_to_author(todo) + - else + (removed) + %span.todo-label + = todo_action_name(todo) + = todo_target_link(todo) + + · #{time_ago_with_tooltip(todo.created_at)} + + - if todo.pending? + .todo-actions.pull-right + = link_to 'Done', [:dashboard, todo], method: :delete, class: 'btn' + + .todo-body + .todo-note + .md + = event_note(todo.body, project: todo.project) diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml new file mode 100644 index 0000000000..946d7df393 --- /dev/null +++ b/app/views/dashboard/todos/index.html.haml @@ -0,0 +1,62 @@ +- page_title "Todos" +- header_title "Todos", dashboard_todos_path + +.top-area + %ul.nav-links + %li{class: ('active' if params[:state].blank? || params[:state] == 'pending')} + = link_to todos_filter_path(state: 'pending') do + %span + To do + %span{class: 'badge'} + = todos_pending_count + %li{class: ('active' if params[:state] == 'done')} + = link_to todos_filter_path(state: 'done') do + %span + Done + %span{class: 'badge'} + = todos_done_count + + .nav-controls + - if @todos.any?(&:pending?) + = link_to 'Mark all as done', destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn', method: :delete + +.todos-filters + .gray-content-block.second-block + = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do + .filter-item.inline + = select_tag('project_id', todo_projects_options, + class: 'select2 trigger-submit', include_blank: true, + data: {placeholder: 'Project'}) + .filter-item.inline + = users_select_tag(:author_id, selected: params[:author_id], + placeholder: 'Author', class: 'trigger-submit', any_user: "Any Author", first_user: true, current_user: true) + .filter-item.inline + = select_tag('type', todo_types_options, + class: 'select2 trigger-submit', include_blank: true, + data: {placeholder: 'Type'}) + .filter-item.inline.actions-filter + = select_tag('action_id', todo_actions_options, + class: 'select2 trigger-submit', include_blank: true, + data: {placeholder: 'Action'}) + +.prepend-top-default + - if @todos.any? + - @todos.group_by(&:project).each do |group| + .panel.panel-default.panel-small + - project = group[0] + .panel-heading + = link_to project.name_with_namespace, namespace_project_path(project.namespace, project) + + %ul.well-list.todos-list + = render group[1] + = paginate @todos, theme: "gitlab" + - else + .nothing-here-block You're all done! + +:javascript + new UsersSelect(); + + $('form.filter-form').on('submit', function (event) { + event.preventDefault(); + Turbolinks.visit(this.action + '&' + $(this).serialize()); + }); diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index dbc8eda619..d65fa60025 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -1,10 +1,10 @@ - page_title "Sign in" %div - - if signin_enabled? || ldap_enabled? + - if signin_enabled? || ldap_enabled? || crowd_enabled? = render 'devise/shared/signin_box' -# Omniauth fits between signin/ldap signin and signup and does not have a surrounding box - - if Gitlab.config.omniauth.enabled && devise_mapping.omniauthable? + - if omniauth_enabled? && devise_mapping.omniauthable? .clearfix.prepend-top-20 = render 'devise/shared/omniauth_box' @@ -14,6 +14,6 @@ = render 'devise/shared/signup_box' -# Show a message if none of the mechanisms above are enabled - - if !signin_enabled? && !ldap_enabled? && !(Gitlab.config.omniauth.enabled && devise_mapping.omniauthable?) + - if !signin_enabled? && !ldap_enabled? && !(omniauth_enabled? && devise_mapping.omniauthable?) %div No authentication methods configured. diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml index 15f9ee266c..eae80e5210 100644 --- a/app/views/doorkeeper/authorizations/new.html.haml +++ b/app/views/doorkeeper/authorizations/new.html.haml @@ -4,6 +4,15 @@ Authorize %strong.text-info= @pre_auth.client.name to use your account? + + - if current_user.admin? + .text-warning.prepend-top-20 + %p + = icon("exclamation-triangle fw") + You are an admin, which means granting access to + %strong #{@pre_auth.client.name} + will allow them to interact with GitLab as an admin as well. Proceed with caution. + - if @pre_auth.scopes #oauth-permissions %p This application will be able to: @@ -25,4 +34,4 @@ = hidden_field_tag :state, @pre_auth.state = hidden_field_tag :response_type, @pre_auth.response_type = hidden_field_tag :scope, @pre_auth.scope - = submit_tag "Deny", class: "btn btn-danger prepend-left-10" \ No newline at end of file + = submit_tag "Deny", class: "btn btn-danger prepend-left-10" diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 46432a9234..36fb2d5162 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -3,8 +3,8 @@ .event-item-timestamp #{time_ago_with_tooltip(event.created_at)} - = cache [event, current_application_settings, "v2.1"] do - = image_tag avatar_icon(event.author_email, 46), class: "avatar s46", alt:'' + = cache [event, current_application_settings, "v2.2"] do + = image_tag avatar_icon(event.author_email, 40), class: "avatar s40", alt:'' - if event.created_project? = render "events/event/created_project", event: event - elsif event.push? diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index fcb07b0408..8ffca96bb4 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -18,7 +18,7 @@ .pull-right .dropdown.inline %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} - %span.light sort: + %span.light - if @sort.present? = sort_options_hash[@sort] - else diff --git a/app/views/explore/projects/_dropdown.html.haml b/app/views/explore/projects/_dropdown.html.haml index b23a3c1e5c..87c556adc7 100644 --- a/app/views/explore/projects/_dropdown.html.haml +++ b/app/views/explore/projects/_dropdown.html.haml @@ -1,21 +1,15 @@ .dropdown.inline %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} - %span.light sort: + %span.light - if @sort.present? = sort_options_hash[@sort] - - elsif current_page?(trending_explore_projects_path) || current_page?(explore_root_path) - Trending projects - - elsif current_page?(starred_explore_projects_path) - Most stars - else - = sort_title_recently_created + = sort_title_recently_updated %b.caret %ul.dropdown-menu %li - = link_to trending_explore_projects_path do - Trending projects - = link_to starred_explore_projects_path do - Most stars + = link_to explore_projects_filter_path(sort: sort_value_name) do + = sort_title_name = link_to explore_projects_filter_path(sort: sort_value_recently_created) do = sort_title_recently_created = link_to explore_projects_filter_path(sort: sort_value_oldest_created) do @@ -24,4 +18,3 @@ = sort_title_recently_updated = link_to explore_projects_filter_path(sort: sort_value_oldest_updated) do = sort_title_oldest_updated - diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index 28b12c8dca..c248dbb695 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -1,10 +1,3 @@ -.pull-left - = form_tag explore_projects_filter_path, method: :get, class: 'form-inline form-tiny' do |f| - .form-group - = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "projects_search", spellcheck: false - .form-group - = button_tag 'Search', class: "btn" - .pull-right.hidden-sm.hidden-xs - if current_user .dropdown.inline.append-right-10 @@ -46,4 +39,3 @@ = link_to explore_projects_filter_path(tag: tag.name) do %i.fa.fa-tag = tag.name - = render 'explore/projects/dropdown' diff --git a/app/views/explore/projects/_nav.html.haml b/app/views/explore/projects/_nav.html.haml new file mode 100644 index 0000000000..614b543177 --- /dev/null +++ b/app/views/explore/projects/_nav.html.haml @@ -0,0 +1,10 @@ +%ul.nav-links + = nav_link(page: [trending_explore_projects_path, explore_root_path]) do + = link_to trending_explore_projects_path do + Trending + = nav_link(page: starred_explore_projects_path) do + = link_to starred_explore_projects_path do + Most stars + = nav_link(page: explore_projects_path) do + = link_to explore_projects_path do + All diff --git a/app/views/explore/projects/_projects.html.haml b/app/views/explore/projects/_projects.html.haml index 669079e952..999a933390 100644 --- a/app/views/explore/projects/_projects.html.haml +++ b/app/views/explore/projects/_projects.html.haml @@ -1,5 +1,5 @@ - if projects.any? - .public-projects + .projects-list-holder = render 'shared/projects/list', projects: projects - else .nothing-here-block diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml index b9a958fbe7..dca7549857 100644 --- a/app/views/explore/projects/index.html.haml +++ b/app/views/explore/projects/index.html.haml @@ -6,7 +6,10 @@ - else = render 'explore/head' -.gray-content-block.clearfix.second-block +.top-area + = render 'explore/projects/nav' + +.gray-content-block.second-block.clearfix = render 'filter' + = render 'projects', projects: @projects -= paginate @projects, theme: "gitlab" diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml index 95d46e331f..ec46175510 100644 --- a/app/views/explore/projects/starred.html.haml +++ b/app/views/explore/projects/starred.html.haml @@ -6,12 +6,5 @@ - else = render 'explore/head' -.explore-trending-block - .gray-content-block.second-block - .pull-right - = render 'explore/projects/dropdown' - .oneline - %i.fa.fa-star - See most starred projects - = render 'projects', projects: @starred_projects - = paginate @starred_projects, theme: 'gitlab' += render 'explore/projects/nav' += render 'projects', projects: @projects diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml index fa0b718e48..ec46175510 100644 --- a/app/views/explore/projects/trending.html.haml +++ b/app/views/explore/projects/trending.html.haml @@ -6,11 +6,5 @@ - else = render 'explore/head' -.explore-trending-block - .gray-content-block.second-block - .pull-right - = render 'explore/projects/dropdown' - .oneline - %i.fa.fa-comments-o - See most discussed projects for last month - = render 'projects', projects: @trending_projects += render 'explore/projects/nav' += render 'projects', projects: @projects diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index bbafc08435..209729dc7e 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -1,11 +1,12 @@ -.projects-list-holder - .projects-search-form - .input-group - = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false +.top-area + .nav-controls + = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| + - if @projects.present? + = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false - if can? current_user, :create_projects, @group - %span.input-group-btn - = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-green' do - %i.fa.fa-plus - New Project + = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do + = icon('plus') + New Project +.projects-list-holder = render 'shared/projects/list', projects: @projects, projects_limit: 20, stars: false, skip_namespace: true diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index e2f97fd933..3430f56a9c 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -1,5 +1,4 @@ - header_title group_title(@group, "Settings", edit_group_path(@group)) -- @blank_container = true .panel.panel-default.prepend-top-default .panel-heading diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index a79a0fcdc8..60234be8f8 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -1,5 +1,6 @@ - user = member.user - return unless user || member.invite? +- show_roles = local_assigns.fetch(:show_roles, true) %li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)} %span{class: ("list-item-name" if show_controls)} @@ -28,7 +29,7 @@ = link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do Resend invite - - if should_user_see_group_roles?(current_user, @group) + - if show_roles && should_user_see_group_roles?(current_user, @group) %span.pull-right %strong.member-access-level= member.human_access - if show_controls diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 6a8acc42af..6b7fd5746d 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -1,6 +1,5 @@ - page_title "Members" - header_title group_title(@group, "Members", group_group_members_path(@group)) -- @blank_container = true .group-members-page.prepend-top-default - if current_user && current_user.can?(:admin_group_member, @group) @@ -20,7 +19,7 @@ group members %small (#{@members.total_count}) - .pull-right + .controls = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do .form-group = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false } diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 90ade1e168..b0805593fd 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -4,17 +4,15 @@ - if current_user = auto_discovery_link_tag(:atom, issues_group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} issues") -.project-issuable-filter - .controls - .pull-left - - if current_user - .hidden-xs.pull-left - = link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token), class: 'btn' do - %i.fa.fa-rss - +.top-area + = render 'shared/issuable/nav', type: :issues + .nav-controls + - if current_user + = link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token), class: 'btn' do + = icon('rss') = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" - = render 'shared/issuable/filter', type: :issues += render 'shared/issuable/filter', type: :issues .gray-content-block.second-block Only issues from diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index f662f5a8c1..e1c9dd931e 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -1,11 +1,12 @@ - page_title "Merge Requests" - header_title group_title(@group, "Merge Requests", merge_requests_group_path(@group)) -.project-issuable-filter - .controls +.top-area + = render 'shared/issuable/nav', type: :merge_requests + .nav-controls = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request" - = render 'shared/issuable/filter', type: :merge_requests += render 'shared/issuable/filter', type: :merge_requests .gray-content-block.second-block Only merge requests from diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index b221d3a89a..ab307708b7 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -1,17 +1,15 @@ - page_title "Milestones" - header_title group_title(@group, "Milestones", group_milestones_path(@group)) -.project-issuable-filter - .controls - - if can?(current_user, :admin_milestones, @group) - .pull-right - %span.pull-right.hidden-xs - = link_to new_group_milestone_path(@group), class: "btn btn-new" do - = icon('plus') - New Milestone - +.top-area = render 'shared/milestones_filter' + .nav-controls + - if can?(current_user, :admin_milestones, @group) + = link_to new_group_milestone_path(@group), class: "btn btn-new" do + = icon('plus') + New Milestone + .gray-content-block Only milestones from %strong #{@group.name} diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index 9ca11ed117..dd75766121 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -6,9 +6,9 @@ %strong= @group.name projects: - if can? current_user, :admin_group, @group - .panel-head-actions + .controls = link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do - %i.fa.fa-plus + = icon('plus') New Project %ul.well-list - @projects.each do |project| diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder index 5cc0f5e1d2..c66b82bb48 100644 --- a/app/views/groups/show.atom.builder +++ b/app/views/groups/show.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: group_url(@group, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: group_url(@group), rel: "alternate", type: "text/html" xml.id group_url(@group) - xml.updated @events.latest_update_time.xmlschema if @events.any? + xml.updated @events[0].updated_at.xmlschema if @events[0] @events.each do |event| event_to_atom(xml, event) diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index ebb3df7dca..6148d8cb3d 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -32,10 +32,9 @@ %li.active = link_to "#activity", 'data-toggle' => 'tab' do Activity - - if @projects.present? - %li - = link_to "#projects", 'data-toggle' => 'tab' do - Projects + %li + = link_to "#projects", 'data-toggle' => 'tab' do + Projects - if can?(current_user, :read_group, @group) %div{ class: container_class } @@ -47,7 +46,7 @@ = render 'shared/event_filter' - .content_list + .content_list{data: {href: events_group_path}} = spinner .tab-pane#projects diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 9ee6f07b26..8e982718d2 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -40,10 +40,6 @@ %td.shortcut .key enter %td Open Selection - %tr - %td.shortcut - .key t - %td Go to finding file %tbody %tr %th @@ -161,6 +157,10 @@ .key s %td Go to snippets + %tr + %td.shortcut + .key t + %td Go to finding file .col-lg-4 %table.shortcut-mappings %tbody{ class: 'hidden-shortcut network', style: 'display:none' } diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 7b45bd0905..746386cab5 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -138,8 +138,32 @@ %h2#navs Navigation + %h4 + %code .top-area + %p Holder for top page navigation. Includes navigation, search field, sorting and button + + .example + .top-area + %ul.nav-links + %li.active + %a Open + %li + %a Closed + .nav-controls + = text_field_tag 'sample', nil, class: 'form-control' + .dropdown + %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %span Sort by name + %b.caret + %ul.dropdown-menu + %li + %a Sort by date + + = link_to 'New issue', '#', class: 'btn btn-new' + %h4 %code .nav-links + %p Only nav links without button and search .example %ul.nav-links %li.active diff --git a/app/views/kaminari/gitlab/_next_page.html.haml b/app/views/kaminari/gitlab/_next_page.html.haml index 00c5f0b6f4..c805914fc3 100644 --- a/app/views/kaminari/gitlab/_next_page.html.haml +++ b/app/views/kaminari/gitlab/_next_page.html.haml @@ -5,5 +5,9 @@ -# num_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%li.next - = link_to_unless current_page.last?, raw(t 'views.pagination.next'), url, rel: 'next', remote: remote +- if current_page.last? + %li{ class: "next disabled" } + %span= raw(t 'views.pagination.next') +- else + %li{ class: "next" } + = link_to raw(t 'views.pagination.next'), url, rel: 'next', remote: remote diff --git a/app/views/kaminari/gitlab/_paginator.html.haml b/app/views/kaminari/gitlab/_paginator.html.haml index 2f64518692..a12c53bcfe 100644 --- a/app/views/kaminari/gitlab/_paginator.html.haml +++ b/app/views/kaminari/gitlab/_paginator.html.haml @@ -10,13 +10,13 @@ %ul.pagination.clearfix - unless current_page.first? = first_page_tag unless num_pages < 5 # As kaminari will always show the first 5 pages - = prev_page_tag + = prev_page_tag - each_page do |page| - if page.left_outer? || page.right_outer? || page.inside_window? = page_tag page - elsif !page.was_truncated? = gap_tag + = next_page_tag - unless current_page.last? - = next_page_tag = last_page_tag unless num_pages < 5 diff --git a/app/views/kaminari/gitlab/_prev_page.html.haml b/app/views/kaminari/gitlab/_prev_page.html.haml index f673abdb3a..afb20455e0 100644 --- a/app/views/kaminari/gitlab/_prev_page.html.haml +++ b/app/views/kaminari/gitlab/_prev_page.html.haml @@ -5,5 +5,9 @@ -# num_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%li{class: "prev" } - = link_to_unless current_page.first?, raw(t 'views.pagination.previous'), url, rel: 'prev', remote: remote +- if current_page.first? + %li{ class: "prev disabled" } + %span= raw(t 'views.pagination.previous') +- else + %li{ class: "prev" } + = link_to raw(t 'views.pagination.previous'), url, rel: 'prev', remote: remote diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 2615998977..c799e9c588 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,9 +1,10 @@ -.page-with-sidebar{ class: page_sidebar_class } +.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" } = render "layouts/broadcast" .sidebar-wrapper.nicescroll{ class: nav_sidebar_class } .header-logo - = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home' do + %a#logo = brand_header_logo + = link_to root_path, class: 'gitlab-text-container-link', title: 'Dashboard', id: 'js-shortcuts-home' do .gitlab-text-container %h3 GitLab diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index a44f5762a6..20042e21bf 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -1,6 +1,6 @@ .search = form_tag search_path, method: :get, class: 'navbar-form pull-left' do |f| - = search_field_tag "search", nil, placeholder: search_placeholder, class: "search-input form-control", spellcheck: false + = search_field_tag "search", nil, placeholder: 'Search', class: "search-input form-control", spellcheck: false = hidden_field_tag :group_id, @group.try(:id) - if @project && @project.persisted? = hidden_field_tag :project_id, @project.id diff --git a/app/views/layouts/ci/_page.html.haml b/app/views/layouts/ci/_page.html.haml index 7e90af21b2..a13241bebe 100644 --- a/app/views/layouts/ci/_page.html.haml +++ b/app/views/layouts/ci/_page.html.haml @@ -2,8 +2,9 @@ = render "layouts/broadcast" .sidebar-wrapper.nicescroll{ class: nav_sidebar_class } .header-logo - = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home' do + %a#logo = brand_header_logo + = link_to root_path, class: 'gitlab-text-container-link', title: 'Dashboard', id: 'js-shortcuts-home' do .gitlab-text-container %h3 GitLab diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index fcb6b835a7..4781ff2350 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -21,6 +21,10 @@ %li = link_to admin_root_path, title: 'Admin Area', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('wrench fw') + %li + = link_to dashboard_todos_path, title: 'Todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + %span.badge.todos-pending-count + = todos_pending_count - if current_user.can_create_project? %li = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do @@ -39,4 +43,4 @@ = render 'shared/outdated_browser' - if @project && !@project.empty_repo? :javascript - var findFileURL = "#{namespace_project_find_file_path(@project.namespace, @project, @ref || @project.repository.root_ref)}"; \ No newline at end of file + var findFileURL = "#{namespace_project_find_file_path(@project.namespace, @project, @ref || @project.repository.root_ref)}"; diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index cffdb52cc2..280a1b9372 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -1,6 +1,6 @@ %ul.nav.nav-sidebar = nav_link(controller: :dashboard, html_options: {class: 'home'}) do - = link_to admin_root_path, title: "Stats" do + = link_to admin_root_path, title: 'Overview' do = icon('dashboard fw') %span Overview @@ -25,13 +25,13 @@ %span Deploy Keys = nav_link path: ['runners#index', 'runners#show'] do - = link_to admin_runners_path do + = link_to admin_runners_path, title: 'Runners' do = icon('cog fw') %span Runners %span.count= number_with_delimiter(Ci::Runner.count(:all)) = nav_link path: 'builds#index' do - = link_to admin_builds_path do + = link_to admin_builds_path, title: 'Builds' do = icon('link fw') %span Builds @@ -56,6 +56,11 @@ = icon('cog fw') %span Background Jobs + = nav_link(controller: :appearances) do + = link_to admin_appearances_path, title: 'Appearances' do + = icon('image') + %span + Appearance = nav_link(controller: :applications) do = link_to admin_applications_path, title: 'Applications' do @@ -82,6 +87,14 @@ Abuse Reports %span.count= number_with_delimiter(AbuseReport.count(:all)) + - if askimet_enabled? + = nav_link(controller: :spam_logs) do + = link_to admin_spam_logs_path, title: "Spam Logs" do + = icon('exclamation-triangle fw') + %span + Spam Logs + %span.count= number_with_delimiter(SpamLog.count(:all)) + = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do = link_to admin_application_settings_path, title: 'Settings' do = icon('cogs fw') diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 106abd24a5..db0cf39392 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -4,6 +4,12 @@ = icon('home fw') %span Projects + = nav_link(controller: :todos) do + = link_to dashboard_todos_path, title: 'Todos' do + = icon('bell fw') + %span + Todos + %span.count= number_with_delimiter(todos_pending_count) = nav_link(path: 'dashboard#activity') do = link_to activity_dashboard_path, class: 'shortcuts-activity', title: 'Activity' do = icon('dashboard fw') @@ -25,12 +31,12 @@ %span Issues %span.count= number_with_delimiter(current_user.assigned_issues.opened.count) - = nav_link(path: 'dashboard#merge_requests') do - = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests' do - = icon('tasks fw') - %span - Merge Requests - %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count) + = nav_link(path: 'dashboard#merge_requests') do + = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests' do + = icon('tasks fw') + %span + Merge Requests + %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count) = nav_link(controller: :snippets) do = link_to dashboard_snippets_path, title: 'Snippets' do = icon('clipboard fw') diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 270ccfd387..319974e12c 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -98,6 +98,13 @@ %span Wiki + - if project_nav_tab? :forks + = nav_link(controller: :forks, action: :index) do + = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks' do + = icon('code-fork fw') + %span + Forks + - if project_nav_tab? :snippets = nav_link(controller: :snippets) do = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do diff --git a/app/views/notify/_note_message.html.haml b/app/views/notify/_note_message.html.haml index 00cb4aa24c..12ded41fbf 100644 --- a/app/views/notify/_note_message.html.haml +++ b/app/views/notify/_note_message.html.haml @@ -1,2 +1,5 @@ +- if current_application_settings.email_author_in_body + %div + #{link_to @note.author_name, user_url(@note.author)} wrote: %div = markdown(@note.note, pipeline: :email) diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml index f4e9749e5c..81d6503731 100644 --- a/app/views/notify/build_fail_email.html.haml +++ b/app/views/notify/build_fail_email.html.haml @@ -1,9 +1,10 @@ - content_for :header do %h1{style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"} GitLab (build failed) + %h3 Project: - = link_to ci_project_url(@project) do + = link_to namespace_project_url(@project.namespace, @project) do = @project.name %p diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml index 8b004d34cc..5d247eb4cf 100644 --- a/app/views/notify/build_success_email.html.haml +++ b/app/views/notify/build_success_email.html.haml @@ -4,7 +4,7 @@ %h3 Project: - = link_to ci_project_url(@project) do + = link_to namespace_project_url(@project.namespace, @project) do = @project.name %p diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml index d3b799fca2..ad3ab2525b 100644 --- a/app/views/notify/new_issue_email.html.haml +++ b/app/views/notify/new_issue_email.html.haml @@ -1,3 +1,6 @@ +- if current_application_settings.email_author_in_body + %div + #{link_to @issue.author_name, user_url(@issue.author)} wrote: -if @issue.description = markdown(@issue.description, pipeline: :email) diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml index 90ebdfc3fe..23423e7d98 100644 --- a/app/views/notify/new_merge_request_email.html.haml +++ b/app/views/notify/new_merge_request_email.html.haml @@ -1,3 +1,6 @@ +- if current_application_settings.email_author_in_body + %div + #{link_to @merge_request.author_name, user_url(@merge_request.author)} wrote: %p.details != merge_path_description(@merge_request, '→') diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index 3dd2595f1a..f2e405b14f 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -18,7 +18,7 @@ %div %span by #{commit.author_name} %i at #{commit.committed_date.to_s(:iso8601)} - %pre.commit-message + %pre.commit-message = commit.safe_message %h4 #{pluralize @message.diffs_count, "changed file"}: diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index a42fd38de3..9fa96084f9 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -1,6 +1,5 @@ - page_title "Account" - header_title page_title, profile_account_path -- @blank_container = true - if current_user.ldap_user? .alert.alert-info @@ -32,34 +31,33 @@ - else = f.submit 'Generate', class: "btn btn-default" - - unless current_user.ldap_user? - .panel.panel-default - .panel-heading - Two-factor Authentication - .panel-body - - if current_user.two_factor_enabled? - .pull-right - = link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-close btn-sm', - data: { confirm: 'Are you sure?' } - %p.text-success - %strong - Two-factor Authentication is enabled - %p - If you lose your recovery codes you can - %strong - = succeed ',' do - = link_to 'generate new ones', codes_profile_two_factor_auth_path, method: :post, data: { confirm: 'Are you sure?' } - invalidating all previous codes. + .panel.panel-default + .panel-heading + Two-factor Authentication + .panel-body + - if current_user.two_factor_enabled? + .pull-right + = link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-close btn-sm', + data: { confirm: 'Are you sure?' } + %p.text-success + %strong + Two-factor Authentication is enabled + %p + If you lose your recovery codes you can + %strong + = succeed ',' do + = link_to 'generate new ones', codes_profile_two_factor_auth_path, method: :post, data: { confirm: 'Are you sure?' } + invalidating all previous codes. - - else - %p - Increase your account's security by enabling two-factor authentication (2FA). - %p - Each time you log in you’ll be required to provide your username and - password as usual, plus a randomly-generated code from your phone. + - else + %p + Increase your account's security by enabling two-factor authentication (2FA). + %p + Each time you log in you’ll be required to provide your username and + password as usual, plus a randomly-generated code from your phone. - .form-actions - = link_to 'Enable Two-factor Authentication', new_profile_two_factor_auth_path, class: 'btn btn-success' + .form-actions + = link_to 'Enable Two-factor Authentication', new_profile_two_factor_auth_path, class: 'btn btn-success' - if button_based_providers.any? .panel.panel-default diff --git a/app/views/profiles/applications.html.haml b/app/views/profiles/applications.html.haml index 0436c2213d..86f3582340 100644 --- a/app/views/profiles/applications.html.haml +++ b/app/views/profiles/applications.html.haml @@ -1,7 +1,7 @@ - page_title "Applications" - header_title page_title, applications_profile_path -.gray-content-block.top-block +.alert.alert-help.prepend-top-default - if user_oauth_applications? Manage applications that can use GitLab as an OAuth provider, and applications that you've authorized to use your account. diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml index 8fdba45b19..8f45f41cfe 100644 --- a/app/views/profiles/audit_log.html.haml +++ b/app/views/profiles/audit_log.html.haml @@ -1,7 +1,7 @@ - page_title "Audit Log" - header_title page_title, audit_log_profile_path -.gray-content-block.top-block +.alert.alert-help.prepend-top-default History of authentications .prepend-top-default diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index 1d140347a5..705e180471 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -1,24 +1,22 @@ - page_title "Emails" - header_title page_title, profile_emails_path -.gray-content-block.top-block - Control emails linked to your account - -%ul.prepend-top-default - %li - Your - %b Primary Email - will be used for avatar detection and web based operations, such as edits and merges. - %li - Your - %b Notification Email - will be used for account notifications. - %li - Your - %b Public Email - will be displayed on your public profile. - %li - All email addresses will be used to identify your commits. +.alert.alert-help.prepend-top-default + %ul + %li + Your + %b Primary Email + will be used for avatar detection and web based operations, such as edits and merges. + %li + Your + %b Notification Email + will be used for account notifications. + %li + Your + %b Public Email + will be displayed on your public profile. + %li + All email addresses will be used to identify your commits. .panel.panel-default .panel-heading diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml index 17a4195030..c9a6a93f54 100644 --- a/app/views/profiles/keys/index.html.haml +++ b/app/views/profiles/keys/index.html.haml @@ -1,14 +1,14 @@ - page_title "SSH Keys" - header_title page_title, profile_keys_path -.gray-content-block.top-block - .pull-right +.top-area + .nav-text + Before you can add an SSH key you need to + = link_to "generate it.", help_page_path("ssh", "README") + .nav-controls = link_to new_profile_key_path, class: "btn btn-new" do = icon('plus') Add SSH Key - .oneline - Before you can add an SSH key you need to - = link_to "generate it.", help_page_path("ssh", "README") .prepend-top-default = render 'key_table' diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 0bcadc965f..d5f61d9f0c 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -1,9 +1,6 @@ - page_title "Notifications" - header_title page_title, profile_notifications_path -.gray-content-block.top-block - These are your global notification settings. - .prepend-top-default = form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications form-horizontal global-notifications-form' } do |f| -if @user.errors.any? diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml index fab7c45c9b..ab070c09be 100644 --- a/app/views/profiles/passwords/edit.html.haml +++ b/app/views/profiles/passwords/edit.html.haml @@ -1,7 +1,7 @@ - page_title "Password" - header_title page_title, edit_profile_password_path -.gray-content-block.top-block +.alert.alert-help.prepend-top-default - if @user.password_automatically_set? Set your password. - else diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 877589dc39..1a53b4393e 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -1,8 +1,7 @@ - page_title 'Preferences' - header_title page_title, profile_preferences_path -- @blank_container = true -.alert.alert-help +.alert.alert-help.prepend-top-default These settings allow you to customize the appearance and behavior of the site. They are saved with your account and will persist to any device you use to access the site. diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index add9a00138..5051c6bf83 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -1,4 +1,4 @@ -.gray-content-block.top-block +.alert.alert-help.prepend-top-default This information will appear on your profile. - if current_user.ldap_user? Some options are unavailable for LDAP accounts diff --git a/app/views/profiles/two_factor_auths/new.html.haml b/app/views/profiles/two_factor_auths/new.html.haml index 1a5b6efce3..b2830aa083 100644 --- a/app/views/profiles/two_factor_auths/new.html.haml +++ b/app/views/profiles/two_factor_auths/new.html.haml @@ -1,6 +1,6 @@ - page_title 'Two-factor Authentication', 'Account' -%h2.page-title Two-Factor Authentication (2FA) +%h2.page-title Two-factor Authentication (2FA) %p Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code. diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 298c666499..b45df44f27 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -3,7 +3,12 @@ .project-identicon-holder = project_icon(@project, alt: '', class: 'project-avatar avatar s90') .project-home-desc - %h1= @project.name + %h1 + = @project.name + %span.visibility-icon.has_tooltip{data: { container: 'body' }, + title: "#{visibility_level_label(@project.visibility_level)} - #{project_visibility_level_description(@project.visibility_level)}"} + = visibility_level_icon(@project.visibility_level, fw: false) + - if @project.description.present? = markdown(@project.description, pipeline: :description) @@ -12,10 +17,6 @@ Forked from = link_to project_path(forked_from_project) do = forked_from_project.namespace.try(:name) - .cover-controls.left - .visibility-level-label.has_tooltip{title: project_visibility_level_description(@project.visibility_level), data: { container: 'body' } } - = visibility_level_icon(@project.visibility_level, fw: false) - = visibility_level_label(@project.visibility_level) .cover-controls - if current_user diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 3d8d88834e..f3bfe0a18b 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -35,7 +35,10 @@ - if blob.lfs_pointer? = render "download", blob: blob - elsif blob.text? - = render "text", blob: blob + - if blob_svg?(blob) + = render "image", blob: blob + - else + = render "text", blob: blob - elsif blob.image? = render "image", blob: blob - else diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml index c090f690d1..113dba5d83 100644 --- a/app/views/projects/blob/_image.html.haml +++ b/app/views/projects/blob/_image.html.haml @@ -1,2 +1,9 @@ .file-content.image_file - %img{ src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"} + - if blob_svg?(blob) + - # We need to scrub SVG but we cannot do so in the RawController: it would + - # be wrong/strange if RawController modified the data. + - blob.load_all_data!(@repository) + - blob = sanitize_svg(blob) + %img{src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"} + - else + %img{src: namespace_project_raw_path(@project.namespace, @project, @id)} diff --git a/app/views/projects/blob/_text.html.haml b/app/views/projects/blob/_text.html.haml index 906e5ccb36..d09cd73558 100644 --- a/app/views/projects/blob/_text.html.haml +++ b/app/views/projects/blob/_text.html.haml @@ -1,3 +1,4 @@ +- blob.load_all_data!(@repository) - if markup?(blob.name) .file-content.wiki = render_markup(blob.name, blob.data) diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 204def6079..7afea5a504 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -10,7 +10,7 @@   .dropdown.inline %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} - %span.light sort: + %span.light - if @sort.present? = @sort.humanize - else diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 5d18c0d803..5e3bd14565 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -1,13 +1,7 @@ - page_title "Builds" = render "header_title" -.project-issuable-filter - .controls - - if can?(current_user, :manage_builds, @project) - .pull-left.hidden-xs - - if @all_builds.running_or_pending.any? - = link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post - +.top-area %ul.nav-links %li{class: ('active' if @scope.nil?)} = link_to project_builds_path(@project) do @@ -27,6 +21,16 @@ %span.badge.js-running-count = number_with_delimiter(@all_builds.finished.count(:id)) + .nav-controls + - if can?(current_user, :update_build, @project) + - if @all_builds.running_or_pending.any? + = link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), + data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post + + = link_to ci_lint_path, class: 'btn btn-default' do + = icon('wrench') + %span CI Lint + .gray-content-block #{(@scope || 'running').capitalize} builds from this project diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index ba1fdc6f0e..8eec78a557 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -76,10 +76,16 @@ = link_to '#down-build-trace', class: 'btn' do %i.fa.fa-angle-down - %pre.trace#build-trace - %code.bash - = preserve do - = raw @build.trace_html + - if @build.erased? + .erased.alert.alert-warning + - erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by + Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)} + - else + %pre.trace#build-trace + %code.bash + = preserve do + = raw @build.trace_html + %div#down-build-trace .col-md-3 @@ -89,43 +95,60 @@ Test coverage %h1 #{@build.coverage}% - - if current_user && can?(current_user, :read_build_artifacts, @project) && @build.artifacts? - + - if can?(current_user, :read_build, @project) && @build.artifacts? .build-widget.artifacts %h4.title Build artifacts .center .btn-group{ role: :group } - = link_to "Download", @build.artifacts_download_url, class: 'btn btn-sm btn-primary' + = link_to @build.artifacts_download_url, class: 'btn btn-sm btn-primary' do + = icon('download') + Download + - if @build.artifacts_metadata? - = link_to "Browse", @build.artifacts_browse_url, class: 'btn btn-sm btn-primary' + = link_to @build.artifacts_browse_url, class: 'btn btn-sm btn-primary' do + = icon('folder-open') + Browse .build-widget %h4.title Build ##{@build.id} - - if current_user && can?(current_user, :manage_builds, @project) - .pull-right - - if @build.cancel_url - = link_to "Cancel", @build.cancel_url, class: 'btn btn-sm btn-danger', method: :post - - elsif @build.retry_url - = link_to "Retry", @build.retry_url, class: 'btn btn-sm btn-primary', method: :post + - if can?(current_user, :update_build, @project) + .center + .btn-group{ role: :group } + - if @build.cancel_url + = link_to "Cancel", @build.cancel_url, class: 'btn btn-sm btn-danger', method: :post + - elsif @build.retry_url + = link_to "Retry", @build.retry_url, class: 'btn btn-sm btn-primary', method: :post - - if @build.duration + - if @build.erasable? + = link_to erase_namespace_project_build_path(@project.namespace, @project, @build), + class: 'btn btn-sm btn-warning', method: :post, + data: { confirm: 'Are you sure you want to erase this build?' } do + = icon('eraser') + Erase + + .clearfix + - if @build.duration + %p + %span.attr-name Duration: + #{duration_in_words(@build.finished_at, @build.started_at)} %p - %span.attr-name Duration: - #{duration_in_words(@build.finished_at, @build.started_at)} - %p - %span.attr-name Created: - #{time_ago_with_tooltip(@build.created_at)} - - if @build.finished_at + %span.attr-name Created: + #{time_ago_with_tooltip(@build.created_at)} + - if @build.finished_at + %p + %span.attr-name Finished: + #{time_ago_with_tooltip(@build.finished_at)} + - if @build.erased_at + %p + %span.attr-name Erased: + #{time_ago_with_tooltip(@build.erased_at)} %p - %span.attr-name Finished: - #{time_ago_with_tooltip(@build.finished_at)} - %p - %span.attr-name Runner: - - if @build.runner && current_user && current_user.admin - = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id) - - elsif @build.runner - \##{@build.runner.id} + %span.attr-name Runner: + - if @build.runner && current_user && current_user.admin + = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id) + - elsif @build.runner + \##{@build.runner.id} - if @build.trigger_request .build-widget diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 511863d774..e7c85edff9 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -46,7 +46,7 @@ - continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master'), notice: edit_in_new_fork_notice, notice_now: edit_in_new_fork_notice_now } - - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id, + - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id, continue: continue_params) = link_to fork_path, method: :post do = icon('file fw') diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml index 329aaa0bb8..befad27666 100644 --- a/app/views/projects/commit/_builds.html.haml +++ b/app/views/projects/commit/_builds.html.haml @@ -1,6 +1,6 @@ .gray-content-block.middle-block .pull-right - - if can?(current_user, :manage_builds, @ci_commit.project) + - if can?(current_user, :update_build, @ci_commit.project) - if @ci_commit.builds.latest.failed.any?(&:retryable?) = link_to "Retry failed", retry_builds_namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), class: 'btn btn-grouped btn-primary', method: :post diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index bbe820b884..71995fcc48 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -16,6 +16,8 @@ = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-grouped" do = icon('files-o') Browse Files + - unless @commit.has_been_reverted?(current_user) + = revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id)) %div %p diff --git a/app/views/projects/commit/_revert.html.haml b/app/views/projects/commit/_revert.html.haml new file mode 100644 index 0000000000..52ca3ed5b1 --- /dev/null +++ b/app/views/projects/commit/_revert.html.haml @@ -0,0 +1,31 @@ +#modal-revert-commit.modal + .modal-dialog + .modal-content + .modal-header + %a.close{href: "#", "data-dismiss" => "modal"} × + %h3.page-title== Revert this #{revert_commit_type(commit)} + .modal-body + = form_tag revert_namespace_project_commit_path(@project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-requires-input' do + .form-group.branch + = label_tag 'target_branch', 'Revert in branch', class: 'control-label' + .col-sm-10 + = select_tag "target_branch", grouped_options_refs, class: "select2 select2-sm js-target-branch" + - if can?(current_user, :push_code, @project) + .js-create-merge-request-container + .checkbox + - nonce = SecureRandom.hex + = label_tag "create_merge_request-#{nonce}" do + = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}" + Start a new merge request with these changes + - else + = hidden_field_tag 'create_merge_request', 1 + .form-actions + = submit_tag "Revert", class: 'btn btn-create' + = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" + + - unless can?(current_user, :push_code, @project) + .inline.prepend-left-10 + = commit_in_fork_help + +:javascript + new NewCommitForm($('.js-create-dir-form')) diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 05dbe5ebea..21e186120c 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -12,3 +12,5 @@ = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @diff_refs = render "projects/notes/notes_with_form" +- if can_collaborate_with_project? + = render "projects/commit/revert", commit: @commit, title: @commit.title diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml index 2e3c956ddc..a3449d1ae0 100644 --- a/app/views/projects/commit_statuses/_commit_status.html.haml +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -1,6 +1,6 @@ %tr.commit_status %td.status - - if commit_status.target_url + - if can?(current_user, :read_commit_status, commit_status) && commit_status.target_url = link_to commit_status.target_url, class: "ci-status ci-#{commit_status.status}" do = ci_icon_for_status(commit_status.status) = commit_status.status @@ -8,14 +8,14 @@ = ci_status_with_icon(commit_status.status) %td.commit_status-link - - if commit_status.target_url + - if can?(current_user, :read_commit_status, commit_status) && commit_status.target_url = link_to commit_status.target_url do %strong ##{commit_status.id} - else %strong ##{commit_status.id} - if commit_status.show_warning? - %i.fa.fa-warning.text-warning + %i.fa.fa-warning.text-warning{data: { toggle: "tooltip" }, title: "This build is stuck, open it to know more"} - if defined?(commit_sha) && commit_sha %td @@ -66,10 +66,10 @@ %td .pull-right - - if current_user && can?(current_user, :read_build_artifacts, commit_status.project) && commit_status.artifacts_download_url + - if can?(current_user, :read_commit_status, commit_status) && commit_status.artifacts_download_url = link_to commit_status.artifacts_download_url, title: 'Download artifacts' do %i.fa.fa-download - - if current_user && can?(current_user, :manage_builds, commit_status.project) + - if can?(current_user, :update_commit_status, commit_status) - if commit_status.active? - if commit_status.cancel_url = link_to commit_status.cancel_url, method: :post, title: 'Cancel' do diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index ede64d47ab..c52cf25d40 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -11,7 +11,10 @@ = render 'shared/ref_switcher', destination: 'commits' .block-controls.hidden-xs.hidden-sm - - if create_mr_button?(@repository.root_ref, @ref) + - if @merge_request.present? + .control + = link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn' + - elsif create_mr_button?(@repository.root_ref, @ref) .control = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do = icon('plus') diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index efc25eda26..4ab81f3635 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -13,12 +13,13 @@ = text_field_tag :to, params[:to], class: "form-control", required: true   = button_tag "Compare", class: "btn btn-create commits-compare-btn" - - if create_mr_button? + - if @merge_request.present? + = link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'prepend-left-10 btn' + - elsif create_mr_button? = link_to create_mr_path, class: 'prepend-left-10 btn' do = icon("plus") Create Merge Request - :javascript var availableTags = #{@project.repository.ref_names.to_json}; diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index fc0eaef228..3ac058a3bf 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -7,16 +7,20 @@ = submodule_link(blob, @commit.id, project.repository) - else = blob_icon blob.mode, blob.name - = link_to "#diff-#{i}" do - %strong - = diff_file.new_path - - if diff_file.deleted_file - deleted - - elsif diff_file.renamed_file - renamed from - %strong - = diff_file.old_path + = link_to "#diff-#{i}" do + - if diff_file.renamed_file + - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) + %strong.filename.old + = old_path + → + %strong.filename.new + = new_path + - else + %strong + = diff_file.new_path + - if diff_file.deleted_file + deleted - if diff_file.mode_changed? %small diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml index 058b71b21f..752e92e2e6 100644 --- a/app/views/projects/diffs/_image.html.haml +++ b/app/views/projects/diffs/_image.html.haml @@ -1,9 +1,11 @@ - diff = diff_file.diff +- file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path)) +- old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.parent_id, diff.old_path)) - if diff.renamed_file || diff.new_file || diff.deleted_file .image %span.wrap .frame{class: image_diff_class(diff)} - %img{src: "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"} + %img{src: diff.deleted_file ? old_file_raw_path : file_raw_path} %p.image-info= "#{number_to_human_size file.size}" - else .image @@ -11,7 +13,7 @@ %span.wrap .frame.deleted %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.parent_id, diff.old_path))} - %img{src: "data:#{old_file.mime_type};base64,#{Base64.encode64(old_file.data)}"} + %img{src: old_file_raw_path} %p.image-info.hide %span.meta-filesize= "#{number_to_human_size old_file.size}" | @@ -23,7 +25,7 @@ %span.wrap .frame.added %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path))} - %img{src: "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"} + %img{src: file_raw_path} %p.image-info.hide %span.meta-filesize= "#{number_to_human_size file.size}" | @@ -36,10 +38,10 @@ %div.swipe.view.hide .swipe-frame .frame.deleted - %img{src: "data:#{old_file.mime_type};base64,#{Base64.encode64(old_file.data)}"} + %img{src: old_file_raw_path} .swipe-wrap .frame.added - %img{src: "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"} + %img{src: file_raw_path} %span.swipe-bar %span.top-handle %span.bottom-handle @@ -47,9 +49,9 @@ %div.onion-skin.view.hide .onion-skin-frame .frame.deleted - %img{src: "data:#{old_file.mime_type};base64,#{Base64.encode64(old_file.data)}"} + %img{src: old_file_raw_path} .frame.added - %img{src: "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"} + %img{src: file_raw_path} .controls .transparent .drag-track diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index 5e835b10e1..d75e9ef2a4 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -35,8 +35,8 @@ = render "projects/notes/diff_notes_with_reply", notes: comments, line: raw_diff_lines[index].text - if last_line > 0 - = render "projects/diffs/match_line", {line: "", - line_old: last_line, line_new: last_line, bottom: true, new_file: diff_file.new_file} + = render "projects/diffs/match_line", { line: "", + line_old: last_line, line_new: last_line, bottom: true, new_file: diff_file.new_file } - if diff_file.diff.blank? && diff_file.mode_changed? .file-mode-changed diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 8a99aceef7..f2e56081af 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -1,5 +1,3 @@ -- @blank_container = true - .project-edit-container.prepend-top-default .project-edit-errors .project-edit-content @@ -119,17 +117,18 @@ .col-sm-offset-2.col-sm-10 %p Get recent application code using the following command: .radio - = f.label :build_allow_git_fetch do + = f.label :build_allow_git_fetch_false do = f.radio_button :build_allow_git_fetch, 'false' %strong git clone %br %span.descr Slower but makes sure you have a clean dir before every build .radio - = f.label :build_allow_git_fetch do + = f.label :build_allow_git_fetch_true do = f.radio_button :build_allow_git_fetch, 'true' %strong git fetch %br %span.descr Faster + .form-group = f.label :build_timeout_in_minutes, 'Timeout', class: 'control-label' .col-sm-10 @@ -158,6 +157,13 @@ phpunit --coverage-text --colors=never (PHP) - %code ^\s*Lines:\s*\d+.\d+\% + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :public_builds do + = f.check_box :public_builds + %strong Public builds + .help-block Allow everyone to access builds for Public and Internal projects %fieldset.features %legend diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml new file mode 100644 index 0000000000..ace22625d1 --- /dev/null +++ b/app/views/projects/forks/index.html.haml @@ -0,0 +1,55 @@ +.top-area + .nav-text + - full_count_title = "#{@public_forks_count} public and #{@private_forks_count} private" + == #{pluralize(@total_forks_count, 'fork')}: #{full_count_title} + + .nav-controls + = search_field_tag :filter_projects, nil, placeholder: 'Search forks', class: 'projects-list-filter project-filter-form-field form-control input-short', + spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' } + + .dropdown + %button.dropdown-toggle.btn.sort-forks{type: 'button', 'data-toggle' => 'dropdown'} + %span.light sort: + - if @sort.present? + = sort_options_hash[@sort] + - else + = sort_title_recently_created + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right + %li + - excluded_filters = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id] + = link_to page_filter_path(sort: sort_value_recently_created, without: excluded_filters) do + = sort_title_recently_created + = link_to page_filter_path(sort: sort_value_oldest_created, without: excluded_filters) do + = sort_title_oldest_created + = link_to page_filter_path(sort: sort_value_recently_updated, without: excluded_filters) do + = sort_title_recently_updated + = link_to page_filter_path(sort: sort_value_oldest_updated, without: excluded_filters) do + = sort_title_oldest_updated + + - if current_user && can?(current_user, :fork_project, @project) + - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-new' do + = icon('code-fork fw') + Fork + - else + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-new' do + = icon('code-fork fw') + Fork + + +.projects-list-holder + - if @forks.blank? + %ul.content-list + %li + .nothing-here-block No forks to show + - else + = render 'shared/projects/list', projects: @forks, use_creator_avatar: true, + forks: true, show_last_commit_as_description: true + + - if @private_forks_count > 0 + %ul.projects-list.private-forks-notice + %li.project-row + = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon') + %strong= pluralize(@private_forks_count, 'private fork') + %span you have no access to. diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml index 8a2c027a45..edabc2d3b4 100644 --- a/app/views/projects/forks/new.html.haml +++ b/app/views/projects/forks/new.html.haml @@ -22,7 +22,7 @@ - else .fork-thumbnail - = link_to namespace_project_fork_path(@project.namespace, @project, namespace_key: namespace.id), title: "Fork here", method: "POST", class: 'has_tooltip' do + = link_to namespace_project_forks_path(@project.namespace, @project, namespace_key: namespace.id), title: "Fork here", method: "POST", class: 'has_tooltip' do = image_tag namespace_icon(namespace, 100) .caption %strong diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index f9cf4910df..654d8cd5ed 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -15,6 +15,17 @@ %li = link_to_member(@project, issue.assignee, name: false, title: "Assigned to :name") + - upvotes, downvotes = issue.upvotes, issue.downvotes + - if upvotes > 0 + %li + = icon('thumbs-up') + = upvotes + + - if downvotes > 0 + %li + = icon('thumbs-down') + = downvotes + - note_count = issue.notes.user.count - if note_count > 0 %li diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml index e0e89b764d..f34f3c0573 100644 --- a/app/views/projects/issues/_issues.html.haml +++ b/app/views/projects/issues/_issues.html.haml @@ -5,9 +5,4 @@ .nothing-here-block No issues to show - if @issues.present? - .issuable-filter-count - %span.pull-right - = number_with_delimiter(@issues.total_count) - issues for this filter - = paginate @issues, theme: "gitlab" diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index d6260ab290..fde9304c0f 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -5,22 +5,19 @@ - if current_user = auto_discovery_link_tag(:atom, namespace_project_issues_url(@project.namespace, @project, :atom, private_token: current_user.private_token), title: "#{@project.name} issues") -.project-issuable-filter - .controls - .pull-left - - if current_user - .hidden-xs.pull-left - = link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do - %i.fa.fa-rss - +.top-area + = render 'shared/issuable/nav', type: :issues + .nav-controls + - if current_user + = link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do + = icon('rss') = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project) - - if can? current_user, :create_issue, @project - = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new pull-left", title: "New Issue", id: "new_issue_link" do - %i.fa.fa-plus + = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do + = icon('plus') New Issue - = render 'shared/issuable/filter', type: :issues += render 'shared/issuable/filter', type: :issues .issues-holder = render "issues" diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 7ed898ce72..69a0e2a0c4 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -6,21 +6,6 @@ .issue .detail-page-header - .status-box{ class: "status-box-closed #{issue_button_visibility(@issue, false)}"} Closed - .status-box{ class: "status-box-open #{issue_button_visibility(@issue, true)}"} Open - %span.identifier - Issue ##{@issue.iid} - %span.creator - · - opened by #{link_to_member(@project, @issue.author, size: 24)} - · - = time_ago_with_tooltip(@issue.created_at, placement: 'bottom', html_class: 'issue_created_ago') - - if @issue.updated_at != @issue.created_at - %span - · - = icon('edit', title: 'edited') - = time_ago_with_tooltip(@issue.updated_at, placement: 'bottom', html_class: 'issue_edited_ago') - .pull-right - if can?(current_user, :create_issue, @project) = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-nr btn-grouped new-issue-link btn-success', title: 'New Issue', id: 'new_issue_link' do @@ -34,8 +19,21 @@ = icon('pencil-square-o') Edit + .pull-left + .status-box{ class: "status-box-closed #{issue_button_visibility(@issue, false)}"} Closed + .status-box{ class: "status-box-open #{issue_button_visibility(@issue, true)}"} Open + + .issue-meta + %span.identifier + Issue ##{@issue.iid} + %span.creator + · + by #{link_to_member(@project, @issue.author, size: 24)} + · + = time_ago_with_tooltip(@issue.created_at, placement: 'bottom', html_class: 'issue_created_ago') + .issue-details.issuable-details - .detail-page-description.gray-content-block.second-block + .detail-page-description.content-block %h2.title = markdown escape_once(@issue.title), pipeline: :single_line %div @@ -46,19 +44,20 @@ = markdown(@issue.description, cache_key: [@issue, "description"]) %textarea.hidden.js-task-list-field = @issue.description + - if @issue.updated_at != @issue.created_at + %small + Edited + = time_ago_with_tooltip(@issue.updated_at, placement: 'bottom', html_class: 'issue_edited_ago') - .merge-requests - = render 'merge_requests' + .merge-requests + = render 'merge_requests' - .gray-content-block.second-block.oneline-block + .content-block = render 'votes/votes_block', votable: @issue .row - %section.col-md-9 + %section.col-md-12 .issuable-discussion = render 'projects/issues/discussion' - %aside.col-md-3 - = render 'shared/issuable/sidebar', issuable: @issue - - = render 'shared/show_aside' += render 'shared/issuable/sidebar', issuable: @issue diff --git a/app/views/projects/issues/update.js.haml b/app/views/projects/issues/update.js.haml index 2f0f3fcfb0..986d8c220d 100644 --- a/app/views/projects/issues/update.js.haml +++ b/app/views/projects/issues/update.js.haml @@ -1,3 +1,3 @@ -$('.issuable-sidebar').html("#{escape_javascript(render 'shared/issuable/sidebar', issuable: @issue)}"); -$('.issuable-sidebar').parent().effect('highlight') -new Issue(); +$('aside.right-sidebar')[0].outerHTML = "#{escape_javascript(render 'shared/issuable/sidebar', issuable: @issue)}"; +$('aside.right-sidebar').effect('highlight'); +new IssuableContext(); diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml index 5ce2a7b985..d63d3a3ec2 100644 --- a/app/views/projects/labels/_form.html.haml +++ b/app/views/projects/labels/_form.html.haml @@ -11,6 +11,10 @@ = f.label :title, class: 'control-label' .col-sm-10 = f.text_field :title, class: "form-control js-quick-submit", required: true, autofocus: true + .form-group + = f.label :description, class: 'control-label' + .col-sm-10 + = f.text_field :description, class: "form-control js-quick-submit" .form-group = f.label :color, "Background color", class: 'control-label' .col-sm-10 diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index b70a9fc9fe..5b35acc66c 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -1,5 +1,6 @@ %li{id: dom_id(label)} - = link_to_label(label) + = render "shared/label_row", label: label + .pull-right %strong.append-right-20 = link_to_label(label) do diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 9081bcfe9b..cc41130a9d 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -1,13 +1,14 @@ - page_title "Labels" = render "header_title" -.gray-content-block.top-block - - if can? current_user, :admin_label, @project - = link_to new_namespace_project_label_path(@project.namespace, @project), class: "pull-right btn btn-new" do - = icon('plus') - New label - .oneline +.top-area + .nav-text Labels can be applied to issues and merge requests. + .nav-controls + - if can? current_user, :admin_label, @project + = link_to new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" do + = icon('plus') + New label .labels - if @labels.present? diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index a051729dc3..b55f6a2d32 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -24,6 +24,17 @@ %li = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name") + - upvotes, downvotes = merge_request.upvotes, merge_request.downvotes + - if upvotes > 0 + %li + = icon('thumbs-up') + = upvotes + + - if downvotes > 0 + %li + = icon('thumbs-down') + = downvotes + - note_count = merge_request.mr_and_commit_notes.user.count - if note_count > 0 %li @@ -53,7 +64,7 @@ - if merge_request.labels.any?   - merge_request.labels.each do |label| - = link_to_label(label, project: merge_request.project) + = link_to_label(label, project: merge_request.project, type: 'merge_request') - if merge_request.tasks?   %span.task-status diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml index 29d09d0a65..5473fa1916 100644 --- a/app/views/projects/merge_requests/_merge_requests.html.haml +++ b/app/views/projects/merge_requests/_merge_requests.html.haml @@ -5,10 +5,5 @@ .nothing-here-block No merge requests to show - if @merge_requests.present? - .issuable-filter-count - %span.pull-right - = number_with_delimiter(@merge_requests.total_count) - merge requests for this filter - = paginate @merge_requests, theme: "gitlab" diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 200bfa5ac4..648512e537 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -66,16 +66,13 @@ .tab-content #notes.notes.tab-pane.voting_notes - .gray-content-block.second-block.oneline-block + .content-block.oneline-block = render 'votes/votes_block', votable: @merge_request .row - %section.col-md-9 + %section.col-md-12 .issuable-discussion = render "projects/merge_requests/discussion" - %aside.col-md-3 - = render 'shared/issuable/sidebar', issuable: @merge_request - = render 'shared/show_aside' #commits.commits.tab-pane - # This tab is always loaded via AJAX @@ -87,6 +84,10 @@ .mr-loading-status = spinner += render 'shared/issuable/sidebar', issuable: @merge_request +- if @merge_request.can_be_reverted? + = render "projects/commit/revert", commit: @merge_request.merge_commit, title: @merge_request.title + :javascript var merge_request; diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 8d5d0394a8..e56a44e0a7 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -2,16 +2,19 @@ = render "header_title" = render 'projects/last_push' -.project-issuable-filter - .controls + +.top-area + = render 'shared/issuable/nav', type: :merge_requests + .nav-controls = render 'shared/issuable/search_form', path: namespace_project_merge_requests_path(@project.namespace, @project) - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - if merge_project - .pull-left.hidden-xs - = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do - %i.fa.fa-plus - New Merge Request - = render 'shared/issuable/filter', type: :merge_requests + = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do + = icon('plus') + New Merge Request + += render 'shared/issuable/filter', type: :merge_requests + .merge-requests-holder = render 'merge_requests' diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml index 7f904ec42a..a8f09f855d 100644 --- a/app/views/projects/merge_requests/show/_commits.html.haml +++ b/app/views/projects/merge_requests/show/_commits.html.haml @@ -1,4 +1,4 @@ -.gray-content-block.middle-block.oneline-block +.content-block.oneline-block = icon("sort-amount-desc") Most recent commits displayed first diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml index 877cc3d744..0dbd159298 100644 --- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml @@ -45,6 +45,10 @@ - unless @merge_request.can_be_merged_by?(current_user) %p Note that pushing to GitLab requires write access to this repository. + %p + %strong Tip: + You can also checkout merge requests locally by + %a{href: 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/workflow/merge_requests.md#checkout-merge-requests-locally', target: '_blank'} following these guidelines :javascript $(function(){ diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml index 0f81e5e891..602f787e6c 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/show/_mr_box.html.haml @@ -1,4 +1,4 @@ -.detail-page-description.gray-content-block.second-block +.detail-page-description.content-block %h2.title = markdown escape_once(@merge_request.title), pipeline: :single_line @@ -10,3 +10,8 @@ = markdown(@merge_request.description, cache_key: [@merge_request, "description"]) %textarea.hidden.js-task-list-field = @merge_request.description + + - if @merge_request.updated_at != @merge_request.created_at + %small + Edited + = time_ago_with_tooltip(@merge_request.updated_at, placement: 'bottom') diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index fc6fb2a0d4..14ea7b1778 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -5,14 +5,9 @@ Merge Request ##{@merge_request.iid} %span.creator · - opened by #{link_to_member(@project, @merge_request.author, size: 24)} + by #{link_to_member(@project, @merge_request.author, size: 24)} · = time_ago_with_tooltip(@merge_request.created_at) - - if @merge_request.updated_at != @merge_request.created_at - %span - · - = icon('edit', title: 'edited') - = time_ago_with_tooltip(@merge_request.updated_at, placement: 'bottom') .issue-btn-group.pull-right - if can?(current_user, :update_merge_request, @merge_request) diff --git a/app/views/projects/merge_requests/update.js.haml b/app/views/projects/merge_requests/update.js.haml index 93db65ddf7..9cce5660e1 100644 --- a/app/views/projects/merge_requests/update.js.haml +++ b/app/views/projects/merge_requests/update.js.haml @@ -1,3 +1,3 @@ -$('.issuable-sidebar').html("#{escape_javascript(render 'shared/issuable/sidebar', issuable: @merge_request)}"); -$('.issuable-sidebar').parent().effect('highlight') -merge_request = new MergeRequest(); +$('aside.right-sidebar')[0].outerHTML = "#{escape_javascript(render 'shared/issuable/sidebar', issuable: @merge_request)}"; +$('aside.right-sidebar').effect('highlight'); +new IssuableContext(); diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml index d1d602eecd..3abae9f0bf 100644 --- a/app/views/projects/merge_requests/widget/_merged.html.haml +++ b/app/views/projects/merge_requests/widget/_merged.html.haml @@ -8,20 +8,18 @@ #{time_ago_with_tooltip(@merge_request.merge_event.created_at)} %div - if !@merge_request.source_branch_exists? || (params[:delete_source] == 'true') - The changes were merged into - #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. - The source branch has been removed. - + %p + The changes were merged into + #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. + The source branch has been removed. + = render 'projects/merge_requests/widget/merged_buttons' - elsif @merge_request.can_remove_source_branch?(current_user) .remove_source_branch_widget %p The changes were merged into #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. You can remove the source branch now. - = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-primary btn-sm remove_source_branch" do - %i.fa.fa-times - Remove Source Branch - + = render 'projects/merge_requests/widget/merged_buttons', source_branch_exists: true .remove_source_branch_widget.failed.hide %p Failed to remove source branch '#{@merge_request.source_branch}'. diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml new file mode 100644 index 0000000000..85a3a6ba9e --- /dev/null +++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml @@ -0,0 +1,11 @@ +- source_branch_exists = local_assigns.fetch(:source_branch_exists, false) +- mr_can_be_reverted = @merge_request.can_be_reverted? + +- if source_branch_exists || mr_can_be_reverted + .btn-group + - if source_branch_exists + = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default btn-grouped btn-sm remove_source_branch" do + = icon('trash-o') + Remove Source Branch + - if mr_can_be_reverted + = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm') diff --git a/app/views/projects/milestones/_issue.html.haml b/app/views/projects/milestones/_issue.html.haml index 133d802aac..ca51b8c745 100644 --- a/app/views/projects/milestones/_issue.html.haml +++ b/app/views/projects/milestones/_issue.html.haml @@ -1,9 +1,10 @@ %li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid, 'data-url' => issue_path(issue) } - .pull-right.assignee-icon - - if issue.assignee - = image_tag avatar_icon(issue.assignee, 16), class: "avatar s16", alt: '' %span - = link_to [@project.namespace.becomes(Namespace), @project, issue] do - %span.cgray ##{issue.iid} = link_to_gfm issue.title, [@project.namespace.becomes(Namespace), @project, issue], title: issue.title - + .issue-detail + = link_to [@project.namespace.becomes(Namespace), @project, issue] do + %span.issue-number ##{issue.iid} + - issue.labels.each do |label| + = render_colored_label(label) + - if issue.assignee + = image_tag avatar_icon(issue.assignee, 16), class: "avatar s24", alt: '' diff --git a/app/views/projects/milestones/_issues.html.haml b/app/views/projects/milestones/_issues.html.haml index 6e4df75a3d..6f8a341e47 100644 --- a/app/views/projects/milestones/_issues.html.haml +++ b/app/views/projects/milestones/_issues.html.haml @@ -1,6 +1,7 @@ .panel.panel-default - .panel-heading= title + .panel-heading + = title + .pull-right= issues.size %ul{ class: "well-list issues-sortable-list", id: "issues-list-#{id}", "data-state" => id } - issues.sort_by(&:position).each do |issue| = render 'issue', issue: issue - %li.light.ui-sort-disabled Drag and drop available diff --git a/app/views/projects/milestones/_merge_requests.html.haml b/app/views/projects/milestones/_merge_requests.html.haml index 00889a5eb2..9a5a02af21 100644 --- a/app/views/projects/milestones/_merge_requests.html.haml +++ b/app/views/projects/milestones/_merge_requests.html.haml @@ -3,4 +3,3 @@ %ul{ class: "well-list merge_requests-sortable-list", id: "merge_requests-list-#{id}", "data-state" => id } - merge_requests.sort_by(&:position).each do |merge_request| = render 'merge_request', merge_request: merge_request - %li.light.ui-sort-disabled Drag and drop available diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index 114b06457a..abe567af1d 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -2,17 +2,14 @@ = render "header_title" -.project-issuable-filter - .controls - - if can?(current_user, :admin_milestone, @project) - = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "pull-right btn btn-new", title: "New Milestone" do - %i.fa.fa-plus - New Milestone - +.top-area = render 'shared/milestones_filter' -.gray-content-block - Milestone allows you to group issues and set due date for it + .nav-controls + - if can?(current_user, :admin_milestone, @project) + = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "btn btn-new", title: "New Milestone" do + = icon('plus') + New Milestone .milestones %ul.content-list diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 1142c58459..2cae1ac4e2 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -24,7 +24,7 @@ - else = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped" - = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-nr btn-remove" do + = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-nr" do = icon('trash-o') Delete @@ -32,7 +32,7 @@ = icon('pencil-square-o') Edit -.detail-page-description.gray-content-block.second-block +.detail-page-description.milestone-detail.second-block %h2.title = markdown escape_once(@milestone.title), pipeline: :single_line %div @@ -47,44 +47,53 @@ %span All issues for this milestone are closed. You may close milestone now. .context.prepend-top-default - %p.lead - Progress: - #{@milestone.closed_items_count} closed - – - #{@milestone.open_items_count} open -   - %span.light #{@milestone.percent_complete}% complete - %span.pull-right= @milestone.expires_at + .milestone-summary + %h4 Progress + %strong= @milestone.issues.count + issues: + %span.milestone-stat + %strong= @milestone.open_items_count + open and + %strong= @milestone.closed_items_count + closed + %span.milestone-stat + %strong== #{@milestone.percent_complete}% + complete + %span.milestone-stat + %span.remaining-days= milestone_remaining_days(@milestone) + %span.pull-right.tab-issues-buttons + - if can?(current_user, :create_issue, @project) + = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do + %i.fa.fa-plus + New Issue + - if can?(current_user, :read_issue, @project) + = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped" + %span.pull-right.tab-merge-requests-buttons.hidden + - if can?(current_user, :read_merge_request, @project) + = link_to 'Browse Merge Requests', namespace_project_merge_requests_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped" + = milestone_progress_bar(@milestone) %ul.nav-links.no-top.no-bottom %li.active - = link_to '#tab-issues', 'data-toggle' => 'tab' do + = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do Issues %span.badge= @issues.count %li - = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do + = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do Merge Requests %span.badge= @merge_requests.count %li = link_to '#tab-participants', 'data-toggle' => 'tab' do Participants %span.badge= @users.count + %li + = link_to '#tab-labels', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do + Labels + %span.badge= @labels.count -.tab-content +.tab-content.milestone-content .tab-pane.active#tab-issues - .gray-content-block.middle-block - .pull-right - - if can?(current_user, :create_issue, @project) - = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do - %i.fa.fa-plus - New Issue - - if can?(current_user, :read_issue, @project) - = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped" - - .oneline - All issues in this milestone - .row.prepend-top-default .col-md-4 = render('issues', title: 'Unstarted Issues (open and unassigned)', issues: @issues.opened.unassigned, id: 'unassigned') @@ -94,14 +103,6 @@ = render('issues', title: 'Completed Issues (closed)', issues: @issues.closed, id: 'closed') .tab-pane#tab-merge-requests - .gray-content-block.middle-block - .pull-right - - if can?(current_user, :read_merge_request, @project) - = link_to 'Browse Merge Requests', namespace_project_merge_requests_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped" - - .oneline - All merge requests in this milestone - .row.prepend-top-default .col-md-3 = render('merge_requests', title: 'Work in progress (open and unassigned)', merge_requests: @merge_requests.opened.unassigned, id: 'unassigned') @@ -117,10 +118,6 @@ = render 'merge_request', merge_request: merge_request .tab-pane#tab-participants - .gray-content-block.middle-block - .oneline - All participants to this milestone - %ul.bordered-list - @users.each do |user| %li @@ -129,3 +126,18 @@ %strong= truncate(user.name, lenght: 40) %br %small.cgray= user.username + + .tab-pane#tab-labels + %ul.bordered-list.manage-labels-list + - @labels.each do |label| + %li + = render_colored_label(label) + - args = [@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title, label_name: label.title] + - options = args.extract_options! + + %span.issues-count + = link_to namespace_project_issues_path(*args, options.merge(state: 'opened')) do + = pluralize label.open_issues_count, 'open issue' + %span.issues-count + = link_to namespace_project_issues_path(*args, options.merge(state: 'closed')) do + = pluralize label.closed_issues_count, 'closed issue' diff --git a/app/views/projects/notes/_diff_notes_with_reply.html.haml b/app/views/projects/notes/_diff_notes_with_reply.html.haml index c731baf0a6..11f9859a90 100644 --- a/app/views/projects/notes/_diff_notes_with_reply.html.haml +++ b/app/views/projects/notes/_diff_notes_with_reply.html.haml @@ -7,7 +7,7 @@ %i.fa.fa-comment = notes.count %td.notes_content - %ul.notes{ rel: note.discussion_id } + %ul.notes{ data: { discussion_id: note.discussion_id } } = render notes .discussion-reply-holder = link_to_reply_diff(note) diff --git a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml index c6726cbafa..bb761ed2f9 100644 --- a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml +++ b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml @@ -8,7 +8,7 @@ %i.fa.fa-comment = notes_left.count %td.notes_content.parallel.old - %ul.notes{ rel: note1.discussion_id } + %ul.notes{ data: { discussion_id: note1.discussion_id } } = render notes_left .discussion-reply-holder @@ -23,7 +23,7 @@ %i.fa.fa-comment = notes_right.count %td.notes_content.parallel.new - %ul.notes{ rel: note2.discussion_id } + %ul.notes{ data: { discussion_id: note2.discussion_id } } = render notes_right .discussion-reply-holder @@ -31,4 +31,3 @@ - else %td.notes_line.new= "" %td.notes_content.parallel.new= "" - diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 922535e5c4..e858c41283 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -1,4 +1,4 @@ -%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)], data: { discussion: note.discussion_id } } +%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)] } .timeline-entry-inner .timeline-icon %a{href: user_path(note.author)} diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index eb378b4260..910eb6cf66 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -5,6 +5,16 @@ .js-main-target-form - if can? current_user, :create_note, @project = render "projects/notes/form", view: diff_view +- else + .disabled-comment-area + .disabled-profile + .disabled-comment + %span + Please + = link_to "register",new_user_session_path + or + = link_to "login",new_user_session_path + to post a comment :javascript var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") diff --git a/app/views/projects/notes/discussions/_commit.html.haml b/app/views/projects/notes/discussions/_commit.html.haml index 6903fad4a0..3da2f2060b 100644 --- a/app/views/projects/notes/discussions/_commit.html.haml +++ b/app/views/projects/notes/discussions/_commit.html.haml @@ -20,8 +20,7 @@ = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note - else .panel.panel-default - .notes{ rel: discussion_notes.first.discussion_id } + .notes{ data: { discussion_id: discussion_notes.first.discussion_id } } = render discussion_notes .discussion-reply-holder = link_to_reply_diff(discussion_notes.first) - diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml index 1c2458fa14..c53033e367 100644 --- a/app/views/projects/project_members/_group_members.html.haml +++ b/app/views/projects/project_members/_group_members.html.haml @@ -5,7 +5,7 @@ %small (#{members.count}) - if can?(current_user, :admin_group_member, @group) - .pull-right + .controls = link_to group_group_members_path(@group), class: 'btn' do = icon('pencil-square-o') Manage group members diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index ccddab13aa..e8dce30425 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -4,7 +4,7 @@ project members %small (#{members.count}) - .pull-right + .controls = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do .form-group = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false } diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 6239a14890..0f8848a5cb 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -1,13 +1,12 @@ - page_title "Members" = render "header_title" -- @blank_container = true .project-members-page.prepend-top-default - if can?(current_user, :admin_project_member, @project) .panel.panel-default .panel-heading Add new user to project - .pull-right + .controls = link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do Import members .panel-body diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder index d676221910..9b3d3f069d 100644 --- a/app/views/projects/show.atom.builder +++ b/app/views/projects/show.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html" xml.id namespace_project_url(@project.namespace, @project) - xml.updated @events.latest_update_time.xmlschema if @events.any? + xml.updated @events[0].updated_at.xmlschema if @events[0] @events.each do |event| event_to_atom(xml, event) diff --git a/app/views/projects/tags/destroy.js.haml b/app/views/projects/tags/destroy.js.haml new file mode 100644 index 0000000000..ffeacb5a00 --- /dev/null +++ b/app/views/projects/tags/destroy.js.haml @@ -0,0 +1,3 @@ +$('.js-totaltags-count').html("#{@repository.tags.size}"); +- if @repository.tags.empty? + $('.tags').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000) diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml index 3c5edf4b03..baaa2caa6d 100644 --- a/app/views/projects/tree/_readme.html.haml +++ b/app/views/projects/tree/_readme.html.haml @@ -1,7 +1,7 @@ %article.file-holder.readme-holder .file-title = blob_icon readme.mode, readme.name - = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do + = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, @path, readme.name)) do %strong = readme.name .file-content.wiki diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 3343288ad2..3eb626e6dc 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -40,7 +40,7 @@ - continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @id), notice: edit_in_new_fork_notice, notice_now: edit_in_new_fork_notice_now } - - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id, + - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id, continue: continue_params) = link_to fork_path, method: :post do = icon('pencil fw') @@ -49,7 +49,7 @@ - continue_params = { to: request.fullpath, notice: edit_in_new_fork_notice + " Try to upload a file again.", notice_now: edit_in_new_fork_notice_now } - - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id, + - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id, continue: continue_params) = link_to fork_path, method: :post do = icon('file fw') @@ -58,7 +58,7 @@ - continue_params = { to: request.fullpath, notice: edit_in_new_fork_notice + " Try to create a new directory again.", notice_now: edit_in_new_fork_notice_now } - - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id, + - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id, continue: continue_params) = link_to fork_path, method: :post do = icon('folder fw') diff --git a/app/views/projects/variables/show.html.haml b/app/views/projects/variables/show.html.haml index e80dffc1ce..efe1e6f24c 100644 --- a/app/views/projects/variables/show.html.haml +++ b/app/views/projects/variables/show.html.haml @@ -3,9 +3,11 @@ Secret Variables %p.light - These variables will be set to environment by the runner and will be hidden in the build log. + These variables will be set to environment by the runner. %br So you can use them for passwords, secret keys or whatever you want. + %br + The value of the variable can be visible in build log if explicitly asked to do so. %hr diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml index 29bf5d62ab..2b91b7e8f6 100644 --- a/app/views/projects/wikis/_main_links.html.haml +++ b/app/views/projects/wikis/_main_links.html.haml @@ -1,12 +1,11 @@ -%span.pull-right - - if (@page && @page.persisted?) - = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn btn-grouped" do - Page History - - if can?(current_user, :create_wiki, @project) - = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn btn-grouped" do - %i.fa.fa-pencil-square-o - Edit - - if can?(current_user, :admin_wiki, @project) - = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-remove" do - = icon('trash') - Delete +- if (@page && @page.persisted?) + = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn btn-grouped" do + Page History + - if can?(current_user, :create_wiki, @project) + = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn btn-grouped" do + %i.fa.fa-pencil-square-o + Edit + - if can?(current_user, :admin_wiki, @project) + = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-remove" do + = icon('trash') + Delete diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml index 69ba301e23..a722fbc535 100644 --- a/app/views/projects/wikis/_nav.html.haml +++ b/app/views/projects/wikis/_nav.html.haml @@ -1,12 +1,4 @@ -.project-issuable-filter - .controls - - if can?(current_user, :create_wiki, @project) - = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do - %i.fa.fa-plus - New Page - - = render 'projects/wikis/new' - +.top-area %ul.nav-links = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) @@ -17,3 +9,11 @@ = nav_link(path: 'wikis#git_access') do = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do Git Access + + .nav-controls + - if can?(current_user, :create_wiki, @project) + = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do + = icon('plus') + New Page + += render 'projects/wikis/new' diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml index 53b37b1104..919daf0a7b 100644 --- a/app/views/projects/wikis/_new.html.haml +++ b/app/views/projects/wikis/_new.html.haml @@ -5,9 +5,10 @@ %a.close{href: "#", "data-dismiss" => "modal"} × %h3.page-title New Wiki Page .modal-body - .form-group - = label_tag :new_wiki_path do - %span Page slug - = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project) - .form-actions - = link_to 'Create Page', '#', class: 'build-new-wiki btn btn-create' + %form.new-wiki-page + .form-group + = label_tag :new_wiki_path do + %span Page slug + = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project), autofocus: true + .form-actions + = button_tag 'Create Page', class: 'build-new-wiki btn btn-create' diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 23f64fbbd1..4dd818c7f6 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -1,16 +1,20 @@ - page_title "Edit", @page.title.capitalize, "Wiki" = render "header_title" - = render 'nav' -.gray-content-block - .pull-right + +.top-area + .nav-text + %strong + - if @page.persisted? + = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) + - else + = @page.title.capitalize + %span.light + · + Edit Page + + .nav-controls = render 'main_links' - %h3.page-title.oneline - %span.light Edit Page - - if @page.persisted? - = link_to @page.title, namespace_project_wiki_path(@project.namespace, @project, @page) - - else - = @page.title = render 'form' diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index 4322146ce3..dcaddae2b0 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -1,11 +1,14 @@ - page_title "History", @page.title.capitalize, "Wiki" = render "header_title" - = render 'nav' -.gray-content-block - %h3.page-title - %span.light History for - = link_to @page.title, namespace_project_wiki_path(@project.namespace, @project, @page) + +.top-area + .nav-text + %strong + = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) + %span.light + · + History .table-holder %table.table diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index aae1ad69ad..92b494a513 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -2,15 +2,12 @@ = render "header_title" = render 'nav' -.gray-content-block - All pages in this wiki are listed below. - + %ul.content-list - @wiki_pages.each do |wiki_page| %li - %h4 - = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page) - %small (#{wiki_page.format}) - .pull-right - %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)} + = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page) + %small (#{wiki_page.format}) + .pull-right + %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)} = paginate @wiki_pages, theme: 'gitlab' diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 309d40f52b..067fb7f8f5 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -1,17 +1,18 @@ - page_title @page.title.capitalize, "Wiki" = render "header_title" - = render 'nav' -.gray-content-block - = render 'main_links' - %h3.page-title.oneline - = @page.title.capitalize +.top-area + .nav-text + %strong= @page.title.capitalize %span.wiki-last-edit-by · last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)} + .nav-controls + = render 'main_links' + - if @page.historical? .warning_message This is an old version of this page. diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index dcd6119971..6b77d24f50 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -1,46 +1,50 @@ +- snippet_blob = chunk_snippet(snippet_blob, @search_term) +- snippet = snippet_blob[:snippet_object] +- snippet_chunks = snippet_blob[:snippet_chunks] + .search-result-row %span - = snippet_blob[:snippet_object].title + = snippet.title by - = link_to user_snippets_path(snippet_blob[:snippet_object].author) do - = image_tag avatar_icon(snippet_blob[:snippet_object].author_email), class: "avatar avatar-inline s16", alt: '' - = snippet_blob[:snippet_object].author_name - %span.light #{time_ago_with_tooltip(snippet_blob[:snippet_object].created_at)} + = link_to user_snippets_path(snippet.author) do + = image_tag avatar_icon(snippet.author_email), class: "avatar avatar-inline s16", alt: '' + = snippet.author_name + %span.light #{time_ago_with_tooltip(snippet.created_at)} %h4.snippet-title - - snippet_path = reliable_snippet_path(snippet_blob[:snippet_object]) + - snippet_path = reliable_snippet_path(snippet) = link_to snippet_path do .file-holder .file-title %i.fa.fa-file - %strong= snippet_blob[:snippet_object].file_name - - if markup?(snippet_blob[:snippet_object].file_name) + %strong= snippet.file_name + - if markup?(snippet.file_name) .file-content.wiki - - snippet_blob[:snippet_chunks].each do |snippet| - - unless snippet[:data].empty? - = render_markup(snippet_blob[:snippet_object].file_name, snippet[:data]) + - snippet_chunks.each do |chunk| + - unless chunk[:data].empty? + = render_markup(snippet.file_name, chunk[:data]) - else .file-content.code .nothing-here-block Empty file - else .file-content.code.js-syntax-highlight .line-numbers - - snippet_blob[:snippet_chunks].each do |snippet| - - unless snippet[:data].empty? - - snippet[:data].lines.to_a.size.times do |index| - - offset = defined?(snippet[:start_line]) ? snippet[:start_line] : 1 + - snippet_chunks.each do |chunk| + - unless chunk[:data].empty? + - chunk[:data].lines.to_a.size.times do |index| + - offset = defined?(chunk[:start_line]) ? chunk[:start_line] : 1 - i = index + offset = link_to snippet_path+"#L#{i}", id: "L#{i}", rel: "#L#{i}", class: "diff-line-num" do %i.fa.fa-link = i - - unless snippet == snippet_blob[:snippet_chunks].last + - unless snippet == snippet_chunks.last %a.diff-line-num = "." %pre.code %code - - snippet_blob[:snippet_chunks].each do |snippet| - - unless snippet[:data].empty? - = snippet[:data] - - unless snippet == snippet_blob[:snippet_chunks].last + - snippet_chunks.each do |chunk| + - unless chunk[:data].empty? + = chunk[:data] + - unless chunk == snippet_chunks.last %a = "..." - else diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml index ee242c94db..57856031d6 100644 --- a/app/views/shared/_file_highlight.html.haml +++ b/app/views/shared/_file_highlight.html.haml @@ -1,7 +1,7 @@ .file-content.code.js-syntax-highlight .line-numbers - if blob.data.present? - - blob.data.lines.each_index do |index| + - blob.data.each_line.each_with_index do |_, index| - offset = defined?(first_line_number) ? first_line_number : 1 - i = index + offset -# We're not using `link_to` because it is too slow once we get to thousands of lines. diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml index 285af56ad7..627814bcfa 100644 --- a/app/views/shared/_import_form.html.haml +++ b/app/views/shared/_import_form.html.haml @@ -11,6 +11,6 @@ %li If your HTTP repository is not publicly accessible, add authentication information to the URL: https://username:password@gitlab.company.com/group/project.git. %li - The import will time out after 4 minutes. For big repositories, use a clone/push combination. + The import will time out after 15 minutes. For repositories that take longer, use a clone/push combination. %li To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}. diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml new file mode 100644 index 0000000000..8134b15d24 --- /dev/null +++ b/app/views/shared/_label_row.html.haml @@ -0,0 +1,4 @@ +%span.label-row + = link_to_label(label) + %span.prepend-left-10 + = markdown(label.description, pipeline: :single_line) diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg index 3d279ec228..b07f1c5603 100644 --- a/app/views/shared/_logo.svg +++ b/app/views/shared/_logo.svg @@ -1,21 +1,9 @@ -```, or are indented with four spaces. Only the fenced code blocks support syntax highlighting. ```no-highlight @@ -421,24 +424,24 @@ will point the link to `wikis/style` when the link is inside of a wiki markdown Here's our logo (hover to see the title text): Inline-style: - ![alt text](assets/logo-white.png) + ![alt text](img/logo.png) Reference-style: ![alt text1][logo] - [logo]: assets/logo-white.png + [logo]: img/logo.png Here's our logo: Inline-style: -![alt text](/assets/logo-white.png) +![alt text](img/logo.png) Reference-style: ![alt text][logo] -[logo]: /assets/logo-white.png +[logo]: img/logo.png ## Blockquotes @@ -585,3 +588,5 @@ By including colons in the header row, you can align the text within that column - This document leveraged heavily from the [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet). - The [Markdown Syntax Guide](https://daringfireball.net/projects/markdown/syntax) at Daring Fireball is an excellent resource for a detailed explanation of standard markdown. - [Dillinger.io](http://dillinger.io) is a handy tool for testing standard markdown. + +[rouge]: http://rouge.jneen.net/ "Rouge website" diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index 1be78ac182..168e7d143e 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -18,6 +18,9 @@ documentation](../workflow/add-user/add-user.md). |---------------------------------------|---------|------------|-------------|----------|--------| | Create new issue | ✓ | ✓ | ✓ | ✓ | ✓ | | Leave comments | ✓ | ✓ | ✓ | ✓ | ✓ | +| See a list of builds | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | +| See a build log | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | +| Download and browse build artifacts | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | | Pull project code | | ✓ | ✓ | ✓ | ✓ | | Download project | | ✓ | ✓ | ✓ | ✓ | | Create code snippets | | ✓ | ✓ | ✓ | ✓ | @@ -31,6 +34,7 @@ documentation](../workflow/add-user/add-user.md). | Remove non-protected branches | | | ✓ | ✓ | ✓ | | Add tags | | | ✓ | ✓ | ✓ | | Write a wiki | | | ✓ | ✓ | ✓ | +| Cancel and retry builds | | | ✓ | ✓ | ✓ | | Create new milestones | | | | ✓ | ✓ | | Add new team members | | | | ✓ | ✓ | | Push to protected branches | | | | ✓ | ✓ | @@ -40,12 +44,17 @@ documentation](../workflow/add-user/add-user.md). | Edit project | | | | ✓ | ✓ | | Add deploy keys to project | | | | ✓ | ✓ | | Configure project hooks | | | | ✓ | ✓ | +| Manage runners | | | | ✓ | ✓ | +| Manage build triggers | | | | ✓ | ✓ | +| Manage variables | | | | ✓ | ✓ | | Switch visibility level | | | | | ✓ | | Transfer project to another namespace | | | | | ✓ | | Remove project | | | | | ✓ | | Force push to protected branches | | | | | | | Remove protected branches | | | | | | +[^1]: If **Allow guest to access builds** is enabled in CI settings + ## Group In order for a group to appear as public and be browsable, it must contain at diff --git a/doc/profile/preferences.md b/doc/profile/preferences.md index f17bbe8f2a..073b879750 100644 --- a/doc/profile/preferences.md +++ b/doc/profile/preferences.md @@ -12,6 +12,9 @@ The default is **Charcoal**. ## Syntax highlighting theme +_GitLab uses the [rouge ruby library][rouge] for syntax highlighting. For a +list of supported languages visit the rouge website._ + Changing this setting allows the user to customize the theme used when viewing syntax highlighted code on the site. @@ -36,3 +39,5 @@ The default is **Your Projects**. It allows user to choose what content he or she want to see on project page. The default is **Readme**. + +[rouge]: http://rouge.jneen.net/ "Rouge website" diff --git a/doc/project_services/builds_emails.md b/doc/project_services/builds_emails.md new file mode 100644 index 0000000000..af0b1a287c --- /dev/null +++ b/doc/project_services/builds_emails.md @@ -0,0 +1,16 @@ +## Enabling build emails + +To receive e-mail notifications about the result status of your builds, visit +your project's **Settings > Services > Builds emails** and activate the service. + +In the _Recipients_ area, provide a list of e-mails separated by comma. + +Check the _Add pusher_ checkbox if you want the committer to also receive +e-mail notifications about each build's status. + +If you enable the _Notify only broken builds_ option, e-mail notifications will +be sent only for failed builds. + +--- + +![Builds emails service settings](img/builds_emails_service.png) diff --git a/doc/project_services/img/builds_emails_service.png b/doc/project_services/img/builds_emails_service.png new file mode 100644 index 0000000000..e604dd73ff Binary files /dev/null and b/doc/project_services/img/builds_emails_service.png differ diff --git a/doc/project_services/img/jira_add_gitlab_commit_message.png b/doc/project_services/img/jira_add_gitlab_commit_message.png new file mode 100644 index 0000000000..85e54861b3 Binary files /dev/null and b/doc/project_services/img/jira_add_gitlab_commit_message.png differ diff --git a/doc/project_services/img/jira_add_user_to_group.png b/doc/project_services/img/jira_add_user_to_group.png new file mode 100644 index 0000000000..e457643388 Binary files /dev/null and b/doc/project_services/img/jira_add_user_to_group.png differ diff --git a/doc/project_services/img/jira_create_new_group.png b/doc/project_services/img/jira_create_new_group.png new file mode 100644 index 0000000000..edaa132605 Binary files /dev/null and b/doc/project_services/img/jira_create_new_group.png differ diff --git a/doc/project_services/img/jira_create_new_group_name.png b/doc/project_services/img/jira_create_new_group_name.png new file mode 100644 index 0000000000..9e518ad784 Binary files /dev/null and b/doc/project_services/img/jira_create_new_group_name.png differ diff --git a/doc/project_services/img/jira_create_new_user.png b/doc/project_services/img/jira_create_new_user.png new file mode 100644 index 0000000000..57e433dd81 Binary files /dev/null and b/doc/project_services/img/jira_create_new_user.png differ diff --git a/doc/project_services/img/jira_group_access.png b/doc/project_services/img/jira_group_access.png new file mode 100644 index 0000000000..47716ca6d0 Binary files /dev/null and b/doc/project_services/img/jira_group_access.png differ diff --git a/doc/project_services/img/jira_issue_closed.png b/doc/project_services/img/jira_issue_closed.png new file mode 100644 index 0000000000..cabec1ae13 Binary files /dev/null and b/doc/project_services/img/jira_issue_closed.png differ diff --git a/doc/integration/img/jira_issue_reference.png b/doc/project_services/img/jira_issue_reference.png similarity index 100% rename from doc/integration/img/jira_issue_reference.png rename to doc/project_services/img/jira_issue_reference.png diff --git a/doc/project_services/img/jira_issues_workflow.png b/doc/project_services/img/jira_issues_workflow.png new file mode 100644 index 0000000000..28e17be3a8 Binary files /dev/null and b/doc/project_services/img/jira_issues_workflow.png differ diff --git a/doc/integration/img/jira_merge_request_close.png b/doc/project_services/img/jira_merge_request_close.png similarity index 100% rename from doc/integration/img/jira_merge_request_close.png rename to doc/project_services/img/jira_merge_request_close.png diff --git a/doc/integration/img/jira_project_name.png b/doc/project_services/img/jira_project_name.png similarity index 100% rename from doc/integration/img/jira_project_name.png rename to doc/project_services/img/jira_project_name.png diff --git a/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png b/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png new file mode 100644 index 0000000000..0149181dc8 Binary files /dev/null and b/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png differ diff --git a/doc/integration/img/jira_service.png b/doc/project_services/img/jira_service.png similarity index 100% rename from doc/integration/img/jira_service.png rename to doc/project_services/img/jira_service.png diff --git a/doc/integration/img/jira_service_close_issue.png b/doc/project_services/img/jira_service_close_issue.png similarity index 100% rename from doc/integration/img/jira_service_close_issue.png rename to doc/project_services/img/jira_service_close_issue.png diff --git a/doc/integration/img/jira_service_page.png b/doc/project_services/img/jira_service_page.png similarity index 100% rename from doc/integration/img/jira_service_page.png rename to doc/project_services/img/jira_service_page.png diff --git a/doc/project_services/img/jira_submit_gitlab_merge_request.png b/doc/project_services/img/jira_submit_gitlab_merge_request.png new file mode 100644 index 0000000000..e935d9362a Binary files /dev/null and b/doc/project_services/img/jira_submit_gitlab_merge_request.png differ diff --git a/doc/project_services/img/jira_user_management_link.png b/doc/project_services/img/jira_user_management_link.png new file mode 100644 index 0000000000..2745916972 Binary files /dev/null and b/doc/project_services/img/jira_user_management_link.png differ diff --git a/doc/integration/img/jira_workflow_screenshot.png b/doc/project_services/img/jira_workflow_screenshot.png similarity index 100% rename from doc/integration/img/jira_workflow_screenshot.png rename to doc/project_services/img/jira_workflow_screenshot.png diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md new file mode 100644 index 0000000000..7c12557a32 --- /dev/null +++ b/doc/project_services/jira.md @@ -0,0 +1,221 @@ +# GitLab JIRA integration + +_**Note:** +Full JIRA integration was previously exclusive to GitLab Enterprise Edition. +With [GitLab 8.3 forward][8_3_post], this feature in now [backported][jira-ce] +to GitLab Community Edition as well._ + +--- + +GitLab can be configured to interact with [JIRA Core] either using an +on-premises instance or the SaaS solution that Atlassian offers. Configuration +happens via username and password on a per-project basis. Connecting to a JIRA +server via CAS is not possible. + +Each project can be configured to connect to a different JIRA instance or, in +case you have a single JIRA instance, you can pre-fill the JIRA service +settings page in GitLab with a default template. To configure the JIRA template, +see the [Services Templates documentation][services-templates]. + +Once the GitLab project is connected to JIRA, you can reference and close the +issues in JIRA directly from GitLab's merge requests. + +## Configuration + +The configuration consists of two parts: + +- [JIRA configuration](#configuring-jira) +- [GitLab configuration](#configuring-gitlab) + +### Configuring JIRA + +First things first, we need to create a user in JIRA which will have access to +all projects that need to integrate with GitLab. + +We have split this stage in steps so it is easier to follow. + +--- + +1. Login to your JIRA instance as an administrator and under **Administration** + go to **User Management** to create a new user. + + ![JIRA user management link](img/jira_user_management_link.png) + + --- + +1. The next step is to create a new user (e.g., `gitlab`) who has write access + to projects in JIRA. Enter the user's name and a _valid_ e-mail address + since JIRA sends a verification e-mail to set-up the password. + _**Note:** JIRA creates the username automatically by using the e-mail + prefix. You can change it later if you want._ + + ![JIRA create new user](img/jira_create_new_user.png) + + --- + +1. Now, let's create a `gitlab-developers` group which will have write access + to projects in JIRA. Go to the **Groups** tab and select **Create group**. + + ![JIRA create new user](img/jira_create_new_group.png) + + --- + + Give it an optional description and hit **Create group**. + + ![JIRA create new group](img/jira_create_new_group_name.png) + + --- + +1. Give the newly-created group write access by going to + **Application access > View configuration** and adding the `gitlab-developers` + group to JIRA Core. + + ![JIRA group access](img/jira_group_access.png) + + --- + +1. Add the `gitlab` user to the `gitlab-developers` group by going to + **Users > GitLab user > Add group** and selecting the `gitlab-developers` + group from the dropdown menu. Notice that the group says _Access_ which is + what we aim for. + + ![JIRA add user to group](img/jira_add_user_to_group.png) + +--- + +The JIRA configuration is over. Write down the new JIRA username and its +password as they will be needed when configuring GitLab in the next section. + +### Configuring GitLab + +_**Note:** The currently supported JIRA versions are v6.x and v7.x. and GitLab +7.8 or higher is required._ + +--- + +Assuming you [have already configured JIRA](#configuring-jira), now it's time +to configure GitLab. + +JIRA configuration in GitLab is done via a project's +[**Services**](../project_services/project_services.md). + +To enable JIRA integration in a project, navigate to the project's +**Settings > Services > JIRA**. + +Fill in the required details on the page, as described in the table below. + +| Setting | Description | +| ------- | ----------- | +| `Description` | A name for the issue tracker (to differentiate between instances, for example). | +| `Project url` | The URL to the JIRA project which is being linked to this GitLab project. It is of the form: `https:///issues/?jql=project=`. | +| `Issues url` | The URL to the JIRA project issues overview for the project that is linked to this GitLab project. It is of the form: `https:///browse/:id`. Leave `:id` as-is, it gets replaced by GitLab at runtime. | +| `New issue url` | This is the URL to create a new issue in JIRA for the project linked to this GitLab project, and it is of the form: `https:///secure/CreateIssue.jspa` | +| `Api url` | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`. It is of the form: `https:///rest/api/2`. | +| `Username` | The username of the user created in [configuring JIRA step](#configuring-jira). | +| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | +| `JIRA issue transition` | This setting is very important to set up correctly. It is the ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot](img/jira_issues_workflow.png)). By default, this ID is set to `2` | + +After saving the configuration, your GitLab project will be able to interact +with the linked JIRA project. + +![JIRA service page](img/jira_service_page.png) + +--- + +## JIRA issues + +By now you should have [configured JIRA](#configuring-jira) and enabled the +[JIRA service in GitLab](#configuring-gitlab). If everything is set up correctly +you should be able to reference and close JIRA issues by just mentioning their +ID in GitLab commits and merge requests. + +### Referencing JIRA Issues + +If you reference a JIRA issue, e.g., `GITLAB-1`, in a commit comment, a link +which points back to JIRA is created. + +The same works for comments in merge requests as well. + +![JIRA add GitLab commit message](img/jira_add_gitlab_commit_message.png) + +--- + +The mentioning action is two-fold, so a comment with a JIRA issue in GitLab +will automatically add a comment in that particular JIRA issue with the link +back to GitLab. + + +![JIRA reference commit message](img/jira_reference_commit_message_in_jira_issue.png) + +--- + +The comment on the JIRA issue is of the form: + +> USER mentioned this issue in LINK_TO_THE_MENTION + +Where: + +| Format | Description | +| ------ | ----------- | +| `USER` | A user that mentioned the issue. This is the link to the user profile in GitLab. | +| `LINK_TO_THE_MENTION` | Link to the origin of mention with a name of the entity where JIRA issue was mentioned. Can be commit or merge request. | + +### Closing JIRA issues + +JIRA issues can be closed directly from GitLab by using trigger words in +commits and merge requests. When a commit which contains the trigger word +followed by the JIRA issue ID in the commit message is pushed, GitLab will +add a comment in the mentioned JIRA issue and immediately close it (provided +the transition ID was set up correctly). + +There are currently three trigger words, and you can use either one to achieve +the same goal: + +- `Resolves GITLAB-1` +- `Closes GITLAB-1` +- `Fixes GITLAB-1` + +where `GITLAB-1` the issue ID of the JIRA project. + +### JIRA issue closing example + +Let's say for example that we submitted a bug fix and created a merge request +in GitLab. The workflow would be something like this: + +1. Create a new branch +1. Fix the bug +1. Commit the changes and push branch to GitLab +1. Open a new merge request and reference the JIRA issue including one of the + trigger words, e.g.: `Fixes GITLAB-1`, in the description +1. Submit the merge request +1. Ask someone to review +1. Merge the merge request +1. The JIRA issue is automatically closed + +--- + +In the following screenshot you can see what the link references to the JIRA +issue look like. + +![JIRA - submit a GitLab merge request](img/jira_submit_gitlab_merge_request.png) + +--- + +Once this merge request is merged, the JIRA issue will be automatically closed +with a link to the commit that resolved the issue. + +![The GitLab integration user leaves a comment on JIRA](img/jira_issue_closed.png) + +--- + +You can see from the above image that there are four references to GitLab: + +- The first is from a comment in a specific commit +- The second is from the JIRA issue reference in the merge request description +- The third is from the actual commit that solved the issue +- And the fourth is from the commit that the merge request created + +[services-templates]: ../project_services/services_templates.md "Services templates documentation" +[JIRA Core]: https://www.atlassian.com/software/jira/core "The JIRA Core website" +[jira-ce]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2146 "MR - Backport JIRA service" +[8_3_post]: https://about.gitlab.com/2015/12/22/gitlab-8-3-released/ "GitLab 8.3 release post" diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index e340312772..3fea2cff0b 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -12,7 +12,7 @@ further configuration instructions and details. Contributions are welcome. | Assembla | Project Management Software (Source Commits Endpoint) | | [Atlassian Bamboo CI](bamboo.md) | A continuous integration and build server | | Buildkite | Continuous integration and deployments | -| Builds emails | Email the builds status to a list of recipients | +| [Builds emails](builds_emails.md) | Email the builds status to a list of recipients | | Campfire | Simple web-based real-time group chat | | Custom Issue Tracker | Custom issue tracker | | Drone CI | Continuous Integration platform built on Docker, written in Go | @@ -22,7 +22,7 @@ further configuration instructions and details. Contributions are welcome. | Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities | | [HipChat](hipchat.md) | Private group chat and IM | | [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | -| JIRA | Jira issue tracker | +| [JIRA](jira.md) | JIRA issue tracker | | JetBrains TeamCity CI | A continuous integration and build server | | PivotalTracker | Project Management Software (Source Commits Endpoint) | | Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop | diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index cdd6652b7b..f6d1234ac4 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -18,8 +18,6 @@ for two-factor authentication. If you restore a GitLab backup without restoring the database encryption key, users who have two-factor authentication enabled will lose access to your GitLab server. -If you are interested in GitLab CI backup please follow to the [CI backup documentation](https://gitlab.com/gitlab-org/gitlab-ci/blob/master/doc/raketasks/backup_restore.md)* - ``` # use this command if you've installed GitLab with the Omnibus package sudo gitlab-rake gitlab:backup:create diff --git a/doc/security/README.md b/doc/security/README.md index f34c792d00..be1abb88c3 100644 --- a/doc/security/README.md +++ b/doc/security/README.md @@ -7,4 +7,4 @@ - [Reset your root password](reset_root_password.md) - [User File Uploads](user_file_uploads.md) - [How we manage the CRIME vulnerability](crime_vulnerability.md) -- [Enforce Two-Factor authentication](two_factor_authentication.md) +- [Enforce Two-factor authentication](two_factor_authentication.md) diff --git a/doc/security/img/two_factor_authentication_settings.png b/doc/security/img/two_factor_authentication_settings.png new file mode 100644 index 0000000000..aa51ce030b Binary files /dev/null and b/doc/security/img/two_factor_authentication_settings.png differ diff --git a/doc/security/two_factor_authentication.md b/doc/security/two_factor_authentication.md index 4e25a1fdc3..8365bdb7b1 100644 --- a/doc/security/two_factor_authentication.md +++ b/doc/security/two_factor_authentication.md @@ -20,7 +20,13 @@ In the Admin area under **Settings** (`/admin/application_settings`), look for the "Sign-in Restrictions" area, where you can configure both. If you want 2FA enforcement to take effect on next login, change the grace -period to `0` +period to `0`. + +--- + +![Two factor authentication admin settings](img/two_factor_authentication_settings.png) + +--- ## Disabling 2FA for everyone @@ -28,11 +34,12 @@ There may be some special situations where you want to disable 2FA for everyone even when forced 2FA is disabled. There is a rake task for that: ``` -# use this command if you've installed GitLab with the Omnibus package +# Omnibus installations sudo gitlab-rake gitlab:two_factor:disable_for_all_users -# if you've installed GitLab from source +# Installations from source sudo -u git -H bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production ``` -**IMPORTANT: this is a permanent and irreversible action. Users will have to reactivate 2FA from scratch if they want to use it again.** +**IMPORTANT: this is a permanent and irreversible action. Users will have to + reactivate 2FA from scratch if they want to use it again.** diff --git a/doc/ssh/README.md b/doc/ssh/README.md index 77eb53427e..a1198e5878 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -5,6 +5,12 @@ An SSH key allows you to establish a secure connection between your computer and GitLab. Before generating an SSH key in your shell, check if your system already has one by running the following command: + +**Windows Command Line:** +```bash +type %userprofile%\.ssh\id_rsa.pub +``` +**GNU/Linux/Mac/PowerShell:** ```bash cat ~/.ssh/id_rsa.pub ``` @@ -25,6 +31,12 @@ press enter to use the default. If you use a different name, the key will not be used automatically. Use the command below to show your public key: + +**Windows Command Line:** +```bash +type %userprofile%\.ssh\id_rsa.pub +``` +**GNU/Linux/Mac/PowerShell:** ```bash cat ~/.ssh/id_rsa.pub ``` @@ -36,9 +48,14 @@ with your username and host. To copy your public key to the clipboard, use the code below. Depending on your OS you'll need to use a different command: -**Windows:** +**Windows Command Line:** ```bash -clip < ~/.ssh/id_rsa.pub +type %userprofile%\.ssh\id_rsa.pub | clip +``` + +**Windows PowerShell:** +```bash +cat ~/.ssh/id_rsa.pub | clip ``` **Mac:** diff --git a/doc/update/6.x-or-7.x-to-7.14.md b/doc/update/6.x-or-7.x-to-7.14.md index 4516a10208..c45fc9340e 100644 --- a/doc/update/6.x-or-7.x-to-7.14.md +++ b/doc/update/6.x-or-7.x-to-7.14.md @@ -14,6 +14,12 @@ possible to edit the label text and color. The characters `?`, `&` and `,` are no longer allowed however so those will be removed from your tags during the database migrations for GitLab 7.2. +## Stash changes + +If you [deleted the vendors folder during your original installation](https://github.com/gitlabhq/gitlabhq/issues/4883#issuecomment-31108431), [you will get an error](https://gitlab.com/gitlab-org/gitlab-ce/issues/1494) when you attempt to rebuild the assets in step 7. To avoid this, stash the changes in your GitLab working copy before starting: + + git stash + ## 0. Stop server sudo service gitlab stop diff --git a/doc/update/8.4-to-8.5.md b/doc/update/8.4-to-8.5.md new file mode 100644 index 0000000000..408a17ac34 --- /dev/null +++ b/doc/update/8.4-to-8.5.md @@ -0,0 +1,142 @@ +# From 8.4 to 8.5 + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 8-5-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 8-5-stable-ee +``` + +### 4. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch --all +sudo -u git -H git checkout v2.6.10 +``` + +### 5. Update gitlab-workhorse + +Install and compile gitlab-workhorse. This requires +[Go 1.5](https://golang.org/dl) which should already be on your system from +GitLab 8.1. + +```bash +cd /home/git/gitlab-workhorse +sudo -u git -H git fetch --all +sudo -u git -H git checkout 0.6.4 +sudo -u git -H make +``` + +### 6. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +``` + +### 7. Update configuration files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +git diff origin/8-4-stable:config/gitlab.yml.example origin/8-5-stable:config/gitlab.yml.example +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +# For HTTPS configurations +git diff origin/8-4-stable:lib/support/nginx/gitlab-ssl origin/8-5-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-4-stable:lib/support/nginx/gitlab origin/8-5-stable:lib/support/nginx/gitlab +``` + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-5-stable/lib/support/init.d/gitlab.default.example#L37 + +#### Init script + +Ensure you're still up-to-date with the latest init script changes: + + sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab + +### 8. Start application + + sudo service gitlab start + sudo service nginx restart + +### 9. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (8.4) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.3 to 8.4](8.3-to-8.4.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index 6420d65cf1..b82306bd1d 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -1,5 +1,12 @@ # Web hooks +_**Note:** +Starting from GitLab 8.5:_ + +- _the `repository` key is deprecated in favor of the `project` key_ +- _the `project.ssh_url` key is deprecated in favor of the `project.git_ssh_url` key_ +- _the `project.http_url` key is deprecated in favor of the `project.git_http_url` key_ + Project web hooks allow you to trigger an URL if new code is pushed or a new issue is created. You can configure web hooks to listen for specific events like pushes, issues or merge requests. GitLab will send a POST request with data to the web hook URL. @@ -8,8 +15,8 @@ Web hooks can be used to update an external issue tracker, trigger CI builds, up ## SSL Verification -By default, the SSL certificate of the webhook endpoint is verified based on -an internal list of Certificate Authorities, +By default, the SSL certificate of the webhook endpoint is verified based on +an internal list of Certificate Authorities, which means the certificate cannot be self-signed. You can turn this off in the web hook settings in your GitLab projects. @@ -37,8 +44,25 @@ X-Gitlab-Event: Push Hook "user_id": 4, "user_name": "John Smith", "user_email": "john@example.com", + "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", "project_id": 15, - "repository": { + "project":{ + "name":"Diaspora", + "description":"", + "web_url":"http://example.com/mike/diaspora", + "avatar_url":null, + "git_ssh_url":"git@example.com:mike/diaspora.git", + "git_http_url":"http://example.com/mike/diaspora.git", + "namespace":"Mike", + "visibility_level":0, + "path_with_namespace":"mike/diaspora", + "default_branch":"master", + "homepage":"http://example.com/mike/diaspora", + "url":"git@example.com:mike/diasporadiaspora.git", + "ssh_url":"git@example.com:mike/diaspora.git", + "http_url":"http://example.com/mike/diaspora.git" + }, + "repository":{ "name": "Diaspora", "url": "git@example.com:mike/diasporadiaspora.git", "description": "", @@ -56,7 +80,7 @@ X-Gitlab-Event: Push Hook "author": { "name": "Jordi Mallach", "email": "jordi@softcatala.org" - } + }, "added": ["CHANGELOG"], "modified": ["app/controller/application.rb"], "removed": [] @@ -76,7 +100,6 @@ X-Gitlab-Event: Push Hook } ], "total_commits_count": 4 - } ``` @@ -101,8 +124,25 @@ X-Gitlab-Event: Tag Push Hook "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7", "user_id": 1, "user_name": "John Smith", + "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", "project_id": 1, - "repository": { + "project":{ + "name":"Example", + "description":"", + "web_url":"http://example.com/jsmith/example", + "avatar_url":null, + "git_ssh_url":"git@example.com:jsmith/example.git", + "git_http_url":"http://example.com/jsmith/example.git", + "namespace":"Jsmith", + "visibility_level":0, + "path_with_namespace":"jsmith/example", + "default_branch":"master", + "homepage":"http://example.com/jsmith/example", + "url":"git@example.com:jsmith/example.git", + "ssh_url":"git@example.com:jsmith/example.git", + "http_url":"http://example.com/jsmith/example.git" + }, + "repository":{ "name": "jsmith", "url": "ssh://git@example.com/jsmith/example.git", "description": "", @@ -136,7 +176,23 @@ X-Gitlab-Event: Issue Hook "username": "root", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" }, - "repository": { + "project":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlabhq/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git", + "git_http_url":"http://example.com/gitlabhq/gitlab-test.git", + "namespace":"GitlabHQ", + "visibility_level":20, + "path_with_namespace":"gitlabhq/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlabhq/gitlab-test", + "url":"http://example.com/gitlabhq/gitlab-test.git", + "ssh_url":"git@example.com:gitlabhq/gitlab-test.git", + "http_url":"http://example.com/gitlabhq/gitlab-test.git" + }, + "repository":{ "name": "Gitlab Test", "url": "http://example.com/gitlabhq/gitlab-test.git", "description": "Aut reprehenderit ut est.", @@ -158,6 +214,11 @@ X-Gitlab-Event: Issue Hook "iid": 23, "url": "http://example.com/diaspora/issues/23", "action": "open" + }, + "assignee": { + "name": "User1", + "username": "user1", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" } } ``` @@ -193,9 +254,25 @@ X-Gitlab-Event: Note Hook "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" }, "project_id": 5, - "repository": { + "project":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlabhq/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git", + "git_http_url":"http://example.com/gitlabhq/gitlab-test.git", + "namespace":"GitlabHQ", + "visibility_level":20, + "path_with_namespace":"gitlabhq/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlabhq/gitlab-test", + "url":"http://example.com/gitlabhq/gitlab-test.git", + "ssh_url":"git@example.com:gitlabhq/gitlab-test.git", + "http_url":"http://example.com/gitlabhq/gitlab-test.git" + }, + "repository":{ "name": "Gitlab Test", - "url": "http://localhost/gitlab-org/gitlab-test.git", + "url": "http://example.com/gitlab-org/gitlab-test.git", "description": "Aut reprehenderit ut est.", "homepage": "http://example.com/gitlab-org/gitlab-test" }, @@ -256,9 +333,25 @@ X-Gitlab-Event: Note Hook "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" }, "project_id": 5, - "repository": { + "project":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" + }, + "repository":{ "name": "Gitlab Test", - "url": "http://example.com/gitlab-org/gitlab-test.git", + "url": "http://localhost/gitlab-org/gitlab-test.git", "description": "Aut reprehenderit ut est.", "homepage": "http://example.com/gitlab-org/gitlab-test" }, @@ -296,21 +389,37 @@ X-Gitlab-Event: Note Hook "description": "Et voluptas corrupti assumenda temporibus. Architecto cum animi eveniet amet asperiores. Vitae numquam voluptate est natus sit et ad id.", "position": 0, "locked_at": null, - "source": { - "name": "Gitlab Test", - "ssh_url": "git@example.com:gitlab-org/gitlab-test.git", - "http_url": "http://example.com/gitlab-org/gitlab-test.git", - "web_url": "http://example.com/gitlab-org/gitlab-test", - "namespace": "Gitlab Org", - "visibility_level": 10 + "source":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" }, "target": { - "name": "Gitlab Test", - "ssh_url": "git@example.com:gitlab-org/gitlab-test.git", - "http_url": "http://example.com/gitlab-org/gitlab-test.git", - "web_url": "http://example.com/gitlab-org/gitlab-test", - "namespace": "Gitlab Org", - "visibility_level": 10 + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" }, "last_commit": { "id": "562e173be03b8ff2efb05345d12df18815438a4b", @@ -322,7 +431,12 @@ X-Gitlab-Event: Note Hook "email": "john@example.com" } }, - "work_in_progress": false + "work_in_progress": false, + "assignee": { + "name": "User1", + "username": "user1", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + } } } ``` @@ -346,11 +460,27 @@ X-Gitlab-Event: Note Hook "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" }, "project_id": 5, - "repository": { - "name": "Gitlab Test", - "url": "http://example.com/gitlab-org/gitlab-test.git", - "description": "Aut reprehenderit ut est.", - "homepage": "http://example.com/gitlab-org/gitlab-test" + "project":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" + }, + "repository":{ + "name":"diaspora", + "url":"git@example.com:mike/diasporadiaspora.git", + "description":"", + "homepage":"http://example.com/mike/diaspora" }, "object_attributes": { "id": 1241, @@ -388,7 +518,6 @@ X-Gitlab-Event: Note Hook ### Comment on code snippet - **Request header**: ``` @@ -397,7 +526,7 @@ X-Gitlab-Event: Note Hook **Request body:** -``` +```json { "object_kind": "note", "user": { @@ -406,11 +535,27 @@ X-Gitlab-Event: Note Hook "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" }, "project_id": 5, - "repository": { - "name": "Gitlab Test", - "url": "http://example.com/gitlab-org/gitlab-test.git", - "description": "Aut reprehenderit ut est.", - "homepage": "http://example.com/gitlab-org/gitlab-test" + "project":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" + }, + "repository":{ + "name":"Gitlab Test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "description":"Aut reprehenderit ut est.", + "homepage":"http://example.com/gitlab-org/gitlab-test" }, "object_attributes": { "id": 1245, @@ -482,21 +627,37 @@ X-Gitlab-Event: Merge Request Hook "target_project_id": 14, "iid": 1, "description": "", - "source": { - "name": "awesome_project", - "ssh_url": "ssh://git@example.com/awesome_space/awesome_project.git", - "http_url": "http://example.com/awesome_space/awesome_project.git", - "web_url": "http://example.com/awesome_space/awesome_project", - "visibility_level": 20, - "namespace": "awesome_space" + "source":{ + "name":"Awesome Project", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/awesome_space/awesome_project", + "avatar_url":null, + "git_ssh_url":"git@example.com:awesome_space/awesome_project.git", + "git_http_url":"http://example.com/awesome_space/awesome_project.git", + "namespace":"Awesome Space", + "visibility_level":20, + "path_with_namespace":"awesome_space/awesome_project", + "default_branch":"master", + "homepage":"http://example.com/awesome_space/awesome_project", + "url":"http://example.com/awesome_space/awesome_project.git", + "ssh_url":"git@example.com:awesome_space/awesome_project.git", + "http_url":"http://example.com/awesome_space/awesome_project.git" }, "target": { - "name": "awesome_project", - "ssh_url": "ssh://git@example.com/awesome_space/awesome_project.git", - "http_url": "http://example.com/awesome_space/awesome_project.git", - "web_url": "http://example.com/awesome_space/awesome_project", - "visibility_level": 20, - "namespace": "awesome_space" + "name":"Awesome Project", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/awesome_space/awesome_project", + "avatar_url":null, + "git_ssh_url":"git@example.com:awesome_space/awesome_project.git", + "git_http_url":"http://example.com/awesome_space/awesome_project.git", + "namespace":"Awesome Space", + "visibility_level":20, + "path_with_namespace":"awesome_space/awesome_project", + "default_branch":"master", + "homepage":"http://example.com/awesome_space/awesome_project", + "url":"http://example.com/awesome_space/awesome_project.git", + "ssh_url":"git@example.com:awesome_space/awesome_project.git", + "http_url":"http://example.com/awesome_space/awesome_project.git" }, "last_commit": { "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", @@ -510,7 +671,12 @@ X-Gitlab-Event: Merge Request Hook }, "work_in_progress": false, "url": "http://example.com/diaspora/merge_requests/1", - "action": "open" + "action": "open", + "assignee": { + "name": "User1", + "username": "user1", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + } } } ``` diff --git a/doc/workflow/README.md b/doc/workflow/README.md index bf62ab4105..2ac32373ce 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -17,7 +17,9 @@ - [Releases](releases.md) - [Milestones](milestones.md) - [Merge Requests](merge_requests.md) +- [Revert changes](revert_changes.md) - ["Work In Progress" Merge Requests](wip_merge_requests.md) - [Merge When Build Succeeds](merge_when_build_succeeds.md) - [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md) - [Importing from SVN, GitHub, BitBucket, etc](importing/README.md) +- [Todos](todos.md) diff --git a/doc/workflow/forking/fork_button.png b/doc/workflow/forking/fork_button.png deleted file mode 100644 index def4266476..0000000000 Binary files a/doc/workflow/forking/fork_button.png and /dev/null differ diff --git a/doc/workflow/forking/groups.png b/doc/workflow/forking/groups.png deleted file mode 100644 index 3ac64b3c8e..0000000000 Binary files a/doc/workflow/forking/groups.png and /dev/null differ diff --git a/doc/workflow/forking_workflow.md b/doc/workflow/forking_workflow.md index 8edf7c6ab3..217a4a4012 100644 --- a/doc/workflow/forking_workflow.md +++ b/doc/workflow/forking_workflow.md @@ -1,36 +1,59 @@ # Project forking workflow -Forking a project to your own namespace is useful if you have no write access to the project you want to contribute -to. If you do have write access or can request it we recommend working together in the same repository since it is simpler. -See our **[GitLab Flow](https://about.gitlab.com/2014/09/29/gitlab-flow/)** article for more information about using -branches to work together. +Forking a project to your own namespace is useful if you have no write +access to the project you want to contribute to. If you do have write +access or can request it, we recommend working together in the same +repository since it is simpler. See our [GitLab Flow](gitlab_flow.md) +document more information about using branches to work together. ## Creating a fork -In order to create a fork of a project, all you need to do is click on the fork button located on the top right side -of the screen, close to the project's URL and right next to the stars button. +Forking a project is in most cases a two-step process. -![Fork button](forking/fork_button.png) -Once you do that you'll be presented with a screen where you can choose the namespace to fork to. Only namespaces -(groups and your own namespace) where you have write access to, will be shown. Click on the namespace to create your -fork there. +1. Click on the fork button located in the middle of the page or a project's + home page right next to the stars button. -![Groups view](forking/groups.png) + ![Fork button](img/forking_workflow_fork_button.png) -After the forking is done, you can start working on the newly created repository. There you will have full -[Owner](../permissions/permissions.md) access, so you can set it up as you please. + --- + +1. Once you do that, you'll be presented with a screen where you can choose + the namespace to fork to. Only namespaces (groups and your own + namespace) where you have write access to, will be shown. Click on the + namespace to create your fork there. + + ![Choose namespace](img/forking_workflow_choose_namespace.png) + + --- + + **Note:** + If the namespace you chose to fork the project to has another project with + the same path name, you will be presented with a warning that the forking + could not be completed. Try to resolve the error and repeat the forking + process. + + ![Path taken error](img/forking_workflow_path_taken_error.png) + + --- + +After the forking is done, you can start working on the newly created +repository. There, you will have full [Owner](../permissions/permissions.md) +access, so you can set it up as you please. ## Merging upstream -Once you are ready to send your code back to the main project, you need to create a merge request. Choose your forked -project's main branch as the source and the original project's main branch as the destination and create the merge request. +Once you are ready to send your code back to the main project, you need +to create a merge request. Choose your forked project's main branch as +the source and the original project's main branch as the destination and +create the [merge request](merge_requests.md). ![Selecting branches](forking/branch_select.png) -You can then assign the merge request to someone to have them review your changes. Upon pressing the 'Accept Merge Request' -button, your changes will be added to the repository and branch you're merging into. +You can then assign the merge request to someone to have them review +your changes. Upon pressing the 'Accept Merge Request' button, your +changes will be added to the repository and branch you're merging into. ![New merge request](forking/merge_request.png) - +[gitlab flow]: https://about.gitlab.com/2014/09/29/gitlab-flow/ "GitLab Flow blog post" diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index 8965e5b365..0b205ea6de 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -152,9 +152,10 @@ The name of this branch should start with the issue number, for example '15-requ When you are done or want to discuss the code you open a merge request. This is an online place to discuss the change and review the code. -Creating a branch is a manual action since you do not always want to merge a new branch you push, it could be a long-running environment or release branch. -If you create the merge request but do not assign it to anyone it is a 'work-in-process' merge request. +Opening a merge request is a manual action since you do not always want to merge a new branch you push, it could be a long-running environment or release branch. +If you open the merge request but do not assign it to anyone it is a 'Work In Progress' merge request. These are used to discuss the proposed implementation but are not ready for inclusion in the master branch yet. +_Pro tip:_ Start the title of the merge request with `[WIP]` or `WIP:` to prevent it from being merged before it's ready. When the author thinks the code is ready the merge request is assigned to reviewer. The reviewer presses the merge button when they think the code is ready for inclusion in the master branch. @@ -185,13 +186,16 @@ If you have an issue that spans across multiple repositories, the best thing is ![Vim screen showing the rebase view](rebase.png) -With git you can use an interactive rebase (rebase -i) to squash multiple commits into one and reorder them. +With git you can use an interactive rebase (`rebase -i`) to squash multiple commits into one and reorder them. +In GitLab EE and .com you can also [rebase before merge](http://doc.gitlab.com/ee/workflow/rebase_before_merge.html) from the web interface. This functionality is useful if you made a couple of commits for small changes during development and want to replace them with a single commit or if you want to make the order more logical. However you should never rebase commits you have pushed to a remote server. Somebody can have referred to the commits or cherry-picked them. When you rebase you change the identifier (SHA-1) of the commit and this is confusing. If you do that the same change will be known under multiple identifiers and this can cause much confusion. If people already reviewed your code it will be hard for them to review only the improvements you made since then if you have rebased everything into one commit. +Another reasons not to rebase is that you lose authorship information, maybe someone created a merge request, another person pushed a commit on there to improve it and a third one merged it. +In this case rebasing all the commits into one prevent the other authors from being properly attributed and sharing part of the [git blame](https://git-scm.com/docs/git-blame). People are encouraged to commit often and to frequently push to the remote repository so other people are aware what everyone is working on. This will lead to many commits per change which makes the history harder to understand. @@ -220,13 +224,11 @@ You can reuse recorded resolutions (rerere) sometimes, but without rebasing you There has to be a better way to avoid many merge commits. The way to prevent creating many merge commits is to not frequently merge master into the feature branch. -We'll discuss the three reasons to merge in master: leveraging code, solving merge conflicts and long running branches. +We'll discuss the three reasons to merge in master: leveraging code, merge conflicts, and long running branches. If you need to leverage some code that was introduced in master after you created the feature branch you can sometimes solve this by just cherry-picking a commit. If your feature branch has a merge conflict, creating a merge commit is a normal way of solving this. -You should aim to prevent merge conflicts where they are likely to occur. -One example is the CHANGELOG file where each significant change in the codebase is documented under a version header. -Instead of everyone adding their change at the bottom of the list for the current version it is better to randomly insert it in the current list for that version. -This it is likely that multiple feature branches that add to the CHANGELOG can be merged before a conflict occurs. +You can prevent some merge conflicts by using [gitattributes](http://git-scm.com/docs/gitattributes) for files that can be in a random order. +For example in GitLab our changelog file is specified in .gitattributes as `CHANGELOG merge=union` so that there are fewer merge conflicts in it. The last reason for creating merge commits is having long lived branches that you want to keep up to date with the latest state of the project. Martin Fowler, in [his article about feature branches](http://martinfowler.com/bliki/FeatureBranch.html) talks about this Continuous Integration (CI). At GitLab we are guilty of confusing CI with branch testing. Quoting Martin Fowler: "I've heard people say they are doing CI because they are running builds, perhaps using a CI server, on every branch with every commit. diff --git a/doc/workflow/img/forking_workflow_choose_namespace.png b/doc/workflow/img/forking_workflow_choose_namespace.png new file mode 100644 index 0000000000..eefe576955 Binary files /dev/null and b/doc/workflow/img/forking_workflow_choose_namespace.png differ diff --git a/doc/workflow/img/forking_workflow_fork_button.png b/doc/workflow/img/forking_workflow_fork_button.png new file mode 100644 index 0000000000..49e68d33e8 Binary files /dev/null and b/doc/workflow/img/forking_workflow_fork_button.png differ diff --git a/doc/workflow/img/forking_workflow_path_taken_error.png b/doc/workflow/img/forking_workflow_path_taken_error.png new file mode 100644 index 0000000000..7a3139506f Binary files /dev/null and b/doc/workflow/img/forking_workflow_path_taken_error.png differ diff --git a/doc/workflow/img/revert_changes_commit.png b/doc/workflow/img/revert_changes_commit.png new file mode 100644 index 0000000000..d84211e20d Binary files /dev/null and b/doc/workflow/img/revert_changes_commit.png differ diff --git a/doc/workflow/img/revert_changes_commit_modal.png b/doc/workflow/img/revert_changes_commit_modal.png new file mode 100644 index 0000000000..e94d151a2a Binary files /dev/null and b/doc/workflow/img/revert_changes_commit_modal.png differ diff --git a/doc/workflow/img/revert_changes_mr.png b/doc/workflow/img/revert_changes_mr.png new file mode 100644 index 0000000000..7adad88463 Binary files /dev/null and b/doc/workflow/img/revert_changes_mr.png differ diff --git a/doc/workflow/img/revert_changes_mr_modal.png b/doc/workflow/img/revert_changes_mr_modal.png new file mode 100644 index 0000000000..9da78f8482 Binary files /dev/null and b/doc/workflow/img/revert_changes_mr_modal.png differ diff --git a/doc/workflow/img/todos_icon.png b/doc/workflow/img/todos_icon.png new file mode 100644 index 0000000000..879b3b51c2 Binary files /dev/null and b/doc/workflow/img/todos_icon.png differ diff --git a/doc/workflow/img/todos_index.png b/doc/workflow/img/todos_index.png new file mode 100644 index 0000000000..4ee18dd128 Binary files /dev/null and b/doc/workflow/img/todos_index.png differ diff --git a/doc/workflow/img/web_editor_new_branch_dropdown.png b/doc/workflow/img/web_editor_new_branch_dropdown.png new file mode 100644 index 0000000000..009e4b05ad Binary files /dev/null and b/doc/workflow/img/web_editor_new_branch_dropdown.png differ diff --git a/doc/workflow/img/web_editor_new_branch_page.png b/doc/workflow/img/web_editor_new_branch_page.png new file mode 100644 index 0000000000..dd6cfc6e7b Binary files /dev/null and b/doc/workflow/img/web_editor_new_branch_page.png differ diff --git a/doc/workflow/img/web_editor_new_directory_dialog.png b/doc/workflow/img/web_editor_new_directory_dialog.png new file mode 100644 index 0000000000..2c76f84f39 Binary files /dev/null and b/doc/workflow/img/web_editor_new_directory_dialog.png differ diff --git a/doc/workflow/img/web_editor_new_directory_dropdown.png b/doc/workflow/img/web_editor_new_directory_dropdown.png new file mode 100644 index 0000000000..cedf46aedf Binary files /dev/null and b/doc/workflow/img/web_editor_new_directory_dropdown.png differ diff --git a/doc/workflow/img/web_editor_new_file_dropdown.png b/doc/workflow/img/web_editor_new_file_dropdown.png new file mode 100644 index 0000000000..6e884f6504 Binary files /dev/null and b/doc/workflow/img/web_editor_new_file_dropdown.png differ diff --git a/doc/workflow/img/web_editor_new_file_editor.png b/doc/workflow/img/web_editor_new_file_editor.png new file mode 100644 index 0000000000..c76473bcfa Binary files /dev/null and b/doc/workflow/img/web_editor_new_file_editor.png differ diff --git a/doc/workflow/img/web_editor_new_push_widget.png b/doc/workflow/img/web_editor_new_push_widget.png new file mode 100644 index 0000000000..a210873574 Binary files /dev/null and b/doc/workflow/img/web_editor_new_push_widget.png differ diff --git a/doc/workflow/img/web_editor_new_tag_dropdown.png b/doc/workflow/img/web_editor_new_tag_dropdown.png new file mode 100644 index 0000000000..263dd635b9 Binary files /dev/null and b/doc/workflow/img/web_editor_new_tag_dropdown.png differ diff --git a/doc/workflow/img/web_editor_new_tag_page.png b/doc/workflow/img/web_editor_new_tag_page.png new file mode 100644 index 0000000000..64d7cd11ed Binary files /dev/null and b/doc/workflow/img/web_editor_new_tag_page.png differ diff --git a/doc/workflow/img/web_editor_start_new_merge_request.png b/doc/workflow/img/web_editor_start_new_merge_request.png new file mode 100644 index 0000000000..be12a151ca Binary files /dev/null and b/doc/workflow/img/web_editor_start_new_merge_request.png differ diff --git a/doc/workflow/img/web_editor_upload_file_dialog.png b/doc/workflow/img/web_editor_upload_file_dialog.png new file mode 100644 index 0000000000..6dd2207bca Binary files /dev/null and b/doc/workflow/img/web_editor_upload_file_dialog.png differ diff --git a/doc/workflow/img/web_editor_upload_file_dropdown.png b/doc/workflow/img/web_editor_upload_file_dropdown.png new file mode 100644 index 0000000000..bf6528701b Binary files /dev/null and b/doc/workflow/img/web_editor_upload_file_dropdown.png differ diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md index 77fb7ea7cd..f693f430a4 100644 --- a/doc/workflow/importing/import_projects_from_github.md +++ b/doc/workflow/importing/import_projects_from_github.md @@ -5,11 +5,15 @@ enable the [GitHub integration][gh-import] in your GitLab instance._ At its current state, GitHub importer can import: -- the repository description -- the git repository data -- the issues -- the pull requests -- the wiki pages +- the repository description (introduced in GitLab 7.7) +- the git repository data (introduced in GitLab 7.7) +- the issues (introduced in GitLab 7.7) +- the pull requests (introduced in GitLab 8.4) +- the wiki pages (introduced in GitLab 8.4) + +It is not yet possible to import your labels, milestones and cross-repository +pull requests (those from forks). We are working on improving this in the near +future. The importer page is visible when you [create a new project][new-project]. Click on the **GitHub** link and you will be redirected to GitHub for @@ -35,12 +39,6 @@ The importer will create any new namespaces if they don't exist or in the case the namespace is taken, the project will be imported on the user's namespace. -### Note - -When you import your projects from GitHub, it is not possible to keep your -labels, milestones, and cross-repository pull requests. We are working on -improving this in the near future. - [gh-import]: ../../integration/github.md "GitHub integration" [ee-gh]: http://doc.gitlab.com/ee/integration/github.html "GitHub integration for GitLab EE" [new-project]: ../../gitlab-basics/create-project.md "How to create a new project in GitLab" diff --git a/doc/workflow/protected_branches.md b/doc/workflow/protected_branches.md index 0adf9f8e3e..fdf9a8d391 100644 --- a/doc/workflow/protected_branches.md +++ b/doc/workflow/protected_branches.md @@ -1,6 +1,6 @@ # Protected branches -Permission in GitLab are fundamentally defined around the idea of having read or write permission to the repository and branches. +Permissions in GitLab are fundamentally defined around the idea of having read or write permission to the repository and branches. To prevent people from messing with history or pushing code without review, we've created protected branches. diff --git a/doc/workflow/revert_changes.md b/doc/workflow/revert_changes.md new file mode 100644 index 0000000000..399366b0cd --- /dev/null +++ b/doc/workflow/revert_changes.md @@ -0,0 +1,64 @@ +# Reverting changes + +_**Note:** This feature was [introduced][ce-1990] in GitLab 8.5._ + +--- + +GitLab implements Git's powerful feature to [revert any commit][git-revert] +with introducing a **Revert** button in Merge Requests and commit details. + +## Reverting a Merge Request + +_**Note:** The **Revert** button will only be available for Merge Requests +created since GitLab 8.5. However, you can still revert a Merge Request +by reverting the merge commit from the list of Commits page._ + +After the Merge Request has been merged, a **Revert** button will be available +to revert the changes introduced by that Merge Request: + +![Revert Merge Request](img/revert_changes_mr.png) + +--- + +You can revert the changes directly into the selected branch or you can opt to +create a new Merge Request with the revert changes: + +![Revert Merge Request modal](img/revert_changes_mr_modal.png) + +--- + +After the Merge Request has been reverted, the **Revert** button will not be +available anymore. + +## Reverting a Commit + +You can revert a Commit from the Commit details page: + +![Revert commit](img/revert_changes_commit.png) + +--- + +Similar to reverting a Merge Request, you can opt to revert the changes +directly into the target branch or create a new Merge Request to revert the +changes: + +![Revert commit modal](img/revert_changes_commit_modal.png) + +--- + +After the Commit has been reverted, the **Revert** button will not be available +anymore. + +Please note that when reverting merge commits, the mainline will always be the +first parent. If you want to use a different mainline then you need to do that +from the command line. + +Here is a quick example to revert a merge commit using the second parent as the +mainline: + +```bash +git revert -m 2 7a39eb0 +``` + +[ce-1990]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1990 "Revert button Merge Request" +[git-revert]: https://git-scm.com/docs/git-revert "Git revert documentation" diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md new file mode 100644 index 0000000000..5f440fdafd --- /dev/null +++ b/doc/workflow/todos.md @@ -0,0 +1,73 @@ +# GitLab ToDos + +>**Note:** This feature was [introduced][ce-2817] in GitLab 8.5. + +When you log into GitLab, you normally want to see where you should spend your +time and take some action, or what you need to keep an eye on. All without the +mess of a huge pile of e-mail notifications. GitLab is where you do your work, +so being able to get started quickly is very important. + +Todos is a chronological list of to-dos that are waiting for your input, all +in a simple dashboard. + +![Todos screenshot showing a list of items to check on](img/todos_index.png) + +--- + +You can access quickly your Todos dashboard by clicking the round gray icon +next to the search bar in the upper right corner. + +![Todos icon](img/todos_icon.png) + +## What triggers a Todo + +A Todo appears in your Todos dashboard when: + +- an issue or merge request is assigned to you +- you are `@mentioned` in an issue or merge request, be it the description of + the issue/merge request or in a comment + +>**Note:** Commenting on a commit will _not_ trigger a Todo. + +## How a Todo is marked as Done + +Any action to the corresponding issue or merge request will mark your Todo as +**Done**. This action can include: + +- changing the assignee +- changing the milestone +- adding/removing a label +- commenting on the issue + +In case where you think no action is needed, you can manually mark the todo as +done by clicking the corresponding **Done** button, and it will disappear from +your Todos list. If you want to mark all your Todos as done, just click on the +**Mark all as done** button. + +--- + +In order for a Todo to be marked as done, the action must be coming from you. +So, if you close the related issue or merge the merge request yourself, and you +had a Todo for that, it will automatically get marked as done. On the other +hand, if someone else closes, merges or takes action on the issue or merge +request, your Todo will remain pending. This makes sense because you may need +to give attention to an issue even if it has been resolved. + +There is just one Todo per issue or merge request, so mentioning a user a +hundred times in an issue will only trigger one Todo. + +## Filtering your Todos + +In general, there are four kinds of filters you can use on your Todos +dashboard: + +| Filter | Description | +| ------ | ----------- | +| Project | Filter by project | +| Author | Filter by the author that triggered the Todo | +| Type | Filter by issue or merge request | +| Action | Filter by the action that triggered the Todo (Assigned or Mentioned)| + +You can choose more than one filters at the same time. + +[ce-2817]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2817 diff --git a/doc/workflow/web_editor.md b/doc/workflow/web_editor.md index 7fc8f96b9e..4a451d9895 100644 --- a/doc/workflow/web_editor.md +++ b/doc/workflow/web_editor.md @@ -1,26 +1,120 @@ # GitLab Web Editor -In GitLab you can create new files and edit existing files using our web editor. -This is especially useful if you don't have access to a command line or you just want to do a quick fix. -You can easily access the web editor, depending on the context. -Let's start from newly created project. +Sometimes it's easier to make quick changes directly from the GitLab interface +than to clone the project and use the Git command line tool. In this feature +highlight we look at how you can create a new file, directory, branch or +tag from the file browser. All of these actions are available from a single +dropdown menu. -Click on `Add a file` -to create the first file and open it in the web editor. +## Create a file -![web editor 1](web_editor/empty_project.png) +From a project's files page, click the '+' button to the right of the branch selector. +Choose **New file** from the dropdown. -Fill in a file name, some content, a commit message, branch name and press the commit button. -The file will be saved to the repository. +![New file dropdown menu](img/web_editor_new_file_dropdown.png) -![web editor 2](web_editor/new_file.png) +--- -You can edit any text file in a repository by pressing the edit button, when -viewing the file. +Enter a file name in the **File name** box. Then, add file content in the editor +area. Add a descriptive commit message and choose a branch. The branch field +will default to the branch you were viewing in the file browser. If you enter +a new branch name, a checkbox will appear allowing you to start a new merge +request after you commit the changes. -![web editor 3](web_editor/show_file.png) +When you are satisfied with your new file, click **Commit Changes** at the bottom. -Editing a file is almost the same as creating a new file, -with as addition the ability to preview your changes in a separate tab. Also you can save your change to another branch by filling out field `branch` +![Create file editor](img/web_editor_new_file_editor.png) -![web editor 3](web_editor/edit_file.png) +## Upload a file + +The ability to create a file is great when the content is text. However, this +doesn't work well for binary data such as images, PDFs or other file types. In +this case you need to upload a file. + +From a project's files page, click the '+' button to the right of the branch +selector. Choose **Upload file** from the dropdown. + +![Upload file dropdown menu](img/web_editor_upload_file_dropdown.png) + +--- + +Once the upload dialog pops up there are two ways to upload your file. Either +drag and drop a file on the pop up or use the **click to upload** link. A file +preview will appear once you have selected a file to upload. + +Enter a commit message, choose a branch, and click **Upload file** when you are +ready. + +![Upload file dialog](img/web_editor_upload_file_dialog.png) + +## Create a directory + +To keep files in the repository organized it is often helpful to create a new +directory. + +From a project's files page, click the '+' button to the right of the branch selector. +Choose **New directory** from the dropdown. + +![New directory dropdown](img/web_editor_new_directory_dropdown.png) + +--- + +In the new directory dialog enter a directory name, a commit message and choose +the target branch. Click **Create directory** to finish. + +![New directory dialog](img/web_editor_new_directory_dialog.png) + +## Create a new branch + +If you want to make changes to several files before creating a new merge +request, you can create a new branch up front. From a project's files page, +choose **New branch** from the dropdown. + +![New branch dropdown](img/web_editor_new_branch_dropdown.png) + +--- + +Enter a new **Branch name**. Optionally, change the **Create from** field +to choose which branch, tag or commit SHA this new branch will originate from. +This field will autocomplete if you start typing an existing branch or tag. +Click **Create branch** and you will be returned to the file browser on this new +branch. + +![New branch page](img/web_editor_new_branch_page.png) + +--- + +You can now make changes to any files, as needed. When you're ready to merge +the changes back to master you can use the widget at the top of the screen. +This widget only appears for a period of time after you create the branch or +modify files. + +![New push widget](img/web_editor_new_push_widget.png) + +## Create a new tag + +Tags are useful for marking major milestones such as production releases, +release candidates, and more. You can create a tag from a branch or a commit +SHA. From a project's files page, choose **New tag** from the dropdown. + +![New tag dropdown](img/web_editor_new_tag_dropdown.png) + +--- + +Give the tag a name such as `v1.0.0`. Choose the branch or SHA from which you +would like to create this new tag. You can optionally add a message and +release notes. The release notes section supports markdown format and you can +also upload an attachment. Click **Create tag** and you will be taken to the tag +list page. + +![New tag page](img/web_editor_new_tag_page.png) + +## Tips + +When creating or uploading a new file, or creating a new directory, you can +trigger a new merge request rather than committing directly to master. Enter +a new branch name in the **Target branch** field. You will notice a checkbox +appear that is labeled **Start a new merge request with these changes**. After +you commit the changes you will be taken to a new merge request form. + +![Start a new merge request with these changes](img/web_editor_start_new_merge_request.png) diff --git a/doc/workflow/web_editor/edit_file.png b/doc/workflow/web_editor/edit_file.png deleted file mode 100644 index f480c69ac3..0000000000 Binary files a/doc/workflow/web_editor/edit_file.png and /dev/null differ diff --git a/doc/workflow/web_editor/empty_project.png b/doc/workflow/web_editor/empty_project.png deleted file mode 100644 index 6a049f6bea..0000000000 Binary files a/doc/workflow/web_editor/empty_project.png and /dev/null differ diff --git a/doc/workflow/web_editor/new_file.png b/doc/workflow/web_editor/new_file.png deleted file mode 100644 index 55ebd9e025..0000000000 Binary files a/doc/workflow/web_editor/new_file.png and /dev/null differ diff --git a/doc/workflow/web_editor/show_file.png b/doc/workflow/web_editor/show_file.png deleted file mode 100644 index 9cafcb5510..0000000000 Binary files a/doc/workflow/web_editor/show_file.png and /dev/null differ diff --git a/features/admin/appearance.feature b/features/admin/appearance.feature new file mode 100644 index 0000000000..5c1dd7531c --- /dev/null +++ b/features/admin/appearance.feature @@ -0,0 +1,37 @@ +Feature: Admin Appearance + Scenario: Create new appearance + Given I sign in as an admin + And I visit admin appearance page + When submit form with new appearance + Then I should be redirected to admin appearance page + And I should see newly created appearance + + Scenario: Preview appearance + Given application has custom appearance + And I sign in as an admin + When I visit admin appearance page + And I click preview button + Then I should see a customized appearance + + Scenario: Custom sign-in page + Given application has custom appearance + When I visit login page + Then I should see a customized appearance + + Scenario: Appearance logo + Given application has custom appearance + And I sign in as an admin + And I visit admin appearance page + When I attach a logo + Then I should see a logo + And I remove the logo + Then I should see logo removed + + Scenario: Header logos + Given application has custom appearance + And I sign in as an admin + And I visit admin appearance page + When I attach header logos + Then I should see header logos + And I remove the header logos + Then I should see header logos removed diff --git a/features/admin/broadcast_messages.feature b/features/admin/broadcast_messages.feature index fd3bac77f8..4f9c651561 100644 --- a/features/admin/broadcast_messages.feature +++ b/features/admin/broadcast_messages.feature @@ -25,3 +25,9 @@ Feature: Admin Broadcast Messages When I remove an existing broadcast message Then I should be redirected to admin messages page And I should not see the removed broadcast message + + @javascript + Scenario: Live preview a customized broadcast message + When I visit admin messages page + And I enter a broadcast message with Markdown + Then I should see a live preview of the rendered broadcast message diff --git a/features/admin/spam_logs.feature b/features/admin/spam_logs.feature new file mode 100644 index 0000000000..92a5389e3a --- /dev/null +++ b/features/admin/spam_logs.feature @@ -0,0 +1,8 @@ +Feature: Admin spam logs + Background: + Given I sign in as an admin + And spam logs exist + + Scenario: Browse spam logs + When I visit spam logs page + Then I should see list of spam logs diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature index b667b587c5..c3b3577c44 100644 --- a/features/dashboard/dashboard.feature +++ b/features/dashboard/dashboard.feature @@ -41,3 +41,33 @@ Feature: Dashboard And user with name "John Doe" left project "Shop" When I visit dashboard activity page Then I should see "John Doe left project Shop" event + + @javascript + Scenario: Sorting Issues + Given I visit dashboard issues page + And I sort the list by "Oldest updated" + And I visit dashboard activity page + And I visit dashboard issues page + Then The list should be sorted by "Oldest updated" + + @javascript + Scenario: Visiting Project's issues after sorting + Given I visit dashboard issues page + And I sort the list by "Oldest updated" + And I visit project "Shop" issues page + Then The list should be sorted by "Oldest updated" + + @javascript + Scenario: Sorting Merge Requests + Given I visit dashboard merge requests page + And I sort the list by "Oldest updated" + And I visit dashboard activity page + And I visit dashboard merge requests page + Then The list should be sorted by "Oldest updated" + + @javascript + Scenario: Visiting Project's merge requests after sorting + Given I visit dashboard merge requests page + And I sort the list by "Oldest updated" + And I visit project "Shop" merge requests page + Then The list should be sorted by "Oldest updated" diff --git a/features/dashboard/todos.feature b/features/dashboard/todos.feature new file mode 100644 index 0000000000..1e7b1b50d6 --- /dev/null +++ b/features/dashboard/todos.feature @@ -0,0 +1,38 @@ +@dashboard +Feature: Dashboard Todos + Background: + Given I sign in as a user + And I own project "Shop" + And "John Doe" is a developer of project "Shop" + And "Mary Jane" is a developer of project "Shop" + And "Mary Jane" owns private project "Enterprise" + And I am a developer of project "Enterprise" + And I have todos + And I visit dashboard todos page + + @javascript + Scenario: I mark todos as done + Then I should see todos assigned to me + And I mark the todo as done + And I click on the "Done" tab + Then I should see all todos marked as done + + @javascript + Scenario: I filter by project + Given I filter by "Enterprise" + Then I should not see todos + + @javascript + Scenario: I filter by author + Given I filter by "John Doe" + Then I should not see todos related to "Mary Jane" in the list + + @javascript + Scenario: I filter by type + Given I filter by "Issue" + Then I should not see todos related to "Merge Requests" in the list + + @javascript + Scenario: I filter by action + Given I filter by "Mentioned" + Then I should not see todos related to "Assignments" in the list diff --git a/features/groups.feature b/features/groups.feature index c803e95298..55fffb012a 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -3,6 +3,10 @@ Feature: Groups Given I sign in as "John Doe" And "John Doe" is owner of group "Owned" + Scenario: I should not see a group if it does not exist + When I visit group "NonExistentGroup" page + Then page status code should be 404 + Scenario: I should have back to group button When I visit group "Owned" page Then I should see back to dashboard button diff --git a/features/login_form.feature b/features/login_form.feature deleted file mode 100644 index b4d9575448..0000000000 --- a/features/login_form.feature +++ /dev/null @@ -1,5 +0,0 @@ -Feature: Login form - Scenario: I see crowd form - Given Crowd integration enabled - When I visit sign in page - Then I should see Crowd login form \ No newline at end of file diff --git a/features/project/badges/build.feature b/features/project/badges/build.feature new file mode 100644 index 0000000000..bcf80ed620 --- /dev/null +++ b/features/project/badges/build.feature @@ -0,0 +1,27 @@ +Feature: Project Badges Build + Background: + Given I sign in as a user + And I own a project + And project has CI enabled + And project has a recent build + + Scenario: I want to see a badge for successfully built project + Given recent build is successful + When I display builds badge for a master branch + Then I should see a build success badge + + Scenario: I want to see a badge for project with failed builds + Given recent build failed + When I display builds badge for a master branch + Then I should see a build failed badge + + Scenario: I want to see a badge for project with running builds + Given recent build is successful + And project has another build that is running + When I display builds badge for a master branch + Then I should see a build running badge + + Scenario: I want to see a fresh badge on each request + Given recent build is successful + When I display builds badge for a master branch + Then I should see a badge that has not been cached diff --git a/features/project/builds/artifacts.feature b/features/project/builds/artifacts.feature index 4f68e44fd7..52dc15f2eb 100644 --- a/features/project/builds/artifacts.feature +++ b/features/project/builds/artifacts.feature @@ -7,21 +7,21 @@ Feature: Project Builds Artifacts Scenario: I download build artifacts Given recent build has artifacts available - When I visit recent build summary page + When I visit recent build details page And I click artifacts download button Then download of build artifacts archive starts Scenario: I browse build artifacts Given recent build has artifacts available And recent build has artifacts metadata available - When I visit recent build summary page + When I visit recent build details page And I click artifacts browse button Then I should see content of artifacts archive Scenario: I browse subdirectory of build artifacts Given recent build has artifacts available And recent build has artifacts metadata available - When I visit recent build summary page + When I visit recent build details page And I click artifacts browse button And I click link to subdirectory within build artifacts Then I should see content of subdirectory within artifacts archive @@ -30,7 +30,7 @@ Feature: Project Builds Artifacts Given recent build has artifacts available And recent build has artifacts metadata available And recent build artifacts contain directory with UTF-8 characters - When I visit recent build summary page + When I visit recent build details page And I click artifacts browse button And I navigate to directory with UTF-8 characters in name Then I should see content of directory with UTF-8 characters in name @@ -39,7 +39,7 @@ Feature: Project Builds Artifacts Given recent build has artifacts available And recent build has artifacts metadata available And recent build artifacts contain directory with invalid UTF-8 characters - When I visit recent build summary page + When I visit recent build details page And I click artifacts browse button And I navigate to parent directory of directory with invalid name Then I should not see directory with invalid name on the list @@ -47,7 +47,16 @@ Feature: Project Builds Artifacts Scenario: I download a single file from build artifacts Given recent build has artifacts available And recent build has artifacts metadata available - When I visit recent build summary page + When I visit recent build details page And I click artifacts browse button And I click a link to file within build artifacts Then download of a file extracted from build artifacts should start + + @javascript + Scenario: I click on a row in an artifacts table + Given recent build has artifacts available + And recent build has artifacts metadata available + When I visit recent build details page + And I click artifacts browse button + And I click a first row within build artifacts table + Then page with a coresponding path is loading diff --git a/features/project/builds/permissions.feature b/features/project/builds/permissions.feature index 1193bcd74f..3c7f72335d 100644 --- a/features/project/builds/permissions.feature +++ b/features/project/builds/permissions.feature @@ -5,6 +5,41 @@ Feature: Project Builds Permissions And project has CI enabled And project has a recent build + Scenario: I try to visit build details as guest + Given I am member of a project with a guest role + When I visit recent build details page + Then page status code should be 404 + + Scenario: I try to visit project builds page as guest + Given I am member of a project with a guest role + When I visit project builds page + Then page status code should be 404 + + Scenario: I try to visit build details of internal project without access to builds + Given The project is internal + And public access for builds is disabled + When I visit recent build details page + Then page status code should be 404 + + Scenario: I try to visit internal project builds page without access to builds + Given The project is internal + And public access for builds is disabled + When I visit project builds page + Then page status code should be 404 + + Scenario: I try to visit build details of internal project with access to builds + Given The project is internal + And public access for builds is enabled + When I visit recent build details page + Then I see details of a build + And I see build trace + + Scenario: I try to visit internal project builds page with access to builds + Given The project is internal + And public access for builds is enabled + When I visit project builds page + Then I see the build + Scenario: I try to download build artifacts as guest Given I am member of a project with a guest role And recent build has artifacts available diff --git a/features/project/builds/summary.feature b/features/project/builds/summary.feature index e90ea592aa..4f3fd194d0 100644 --- a/features/project/builds/summary.feature +++ b/features/project/builds/summary.feature @@ -5,7 +5,20 @@ Feature: Project Builds Summary And project has CI enabled And project has a recent build - Scenario: I browse build summary page - When I visit recent build summary page - Then I see summary for build + Scenario: I browse build details page + When I visit recent build details page + Then I see details of a build And I see build trace + + Scenario: I browse project builds page + When I visit project builds page + Then I see button to CI Lint + + Scenario: I erase a build + Given recent build is successful + And recent build has a build trace + When I visit recent build details page + And I click erase build button + Then recent build has been erased + And recent build summary does not have artifacts widget + And recent build summary contains information saying that build has been erased diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature index 01c1072131..a95df03835 100644 --- a/features/project/commits/commits.feature +++ b/features/project/commits/commits.feature @@ -7,6 +7,26 @@ Feature: Project Commits Scenario: I browse commits list for master branch Then I see project commits + And I should not see button to create a new merge request + Then I click the "Compare" tab + And I should not see button to create a new merge request + + Scenario: I browse commits list for feature branch without a merge request + Given I visit commits list page for feature branch + Then I see feature branch commits + And I see button to create a new merge request + Then I click the "Compare" tab + And I see button to create a new merge request + + Scenario: I browse commits list for feature branch with an open merge request + Given project have an open merge request + And I visit commits list page for feature branch + Then I see feature branch commits + And I should not see button to create a new merge request + And I should see button to the merge request + Then I click the "Compare" tab + And I should not see button to create a new merge request + And I should see button to the merge request Scenario: I browse atom feed of commits list for master branch Given I click atom feed link @@ -30,6 +50,22 @@ Feature: Project Commits And I click side-by-side diff button Then I see inline diff button + @javascript + Scenario: I compare branches without a merge request + Given I visit compare refs page + And I fill compare fields with branches + Then I see compared branches + And I see button to create a new merge request + + @javascript + Scenario: I compare branches with an open merge request + Given project have an open merge request + And I visit compare refs page + And I fill compare fields with branches + Then I see compared branches + And I should not see button to create a new merge request + And I should see button to the merge request + @javascript Scenario: I compare refs Given I visit compare refs page diff --git a/features/project/commits/revert.feature b/features/project/commits/revert.feature new file mode 100644 index 0000000000..7a2effafe0 --- /dev/null +++ b/features/project/commits/revert.feature @@ -0,0 +1,28 @@ +@project_commits +Feature: Revert Commits + Background: + Given I sign in as a user + And I own a project + And I visit my project's commits page + + Scenario: I revert a commit + Given I click on commit link + And I click on the revert button + And I revert the changes directly + Then I should see the revert commit notice + + Scenario: I revert a commit that was previously reverted + Given I click on commit link + And I click on the revert button + And I revert the changes directly + And I visit my project's commits page + And I click on commit link + And I click on the revert button + And I revert the changes directly + Then I should see a revert error + + Scenario: I revert a commit in a new merge request + Given I click on commit link + And I click on the revert button + And I revert the changes in a new merge request + Then I should see the new merge request notice diff --git a/features/project/fork.feature b/features/project/fork.feature index 37cd53ee97..ca3f2771aa 100644 --- a/features/project/fork.feature +++ b/features/project/fork.feature @@ -25,3 +25,25 @@ Feature: Project Fork Then I should see "New merge request" And I click link "New merge request" Then I should see the new merge request page for my namespace + + Scenario: Viewing forks of a Project + Given I click link "Fork" + When I fork to my namespace + And I visit the forks page of the "Shop" project + Then I should see my fork on the list + + Scenario: Viewing forks of a Project that has no repo + Given I click link "Fork" + When I fork to my namespace + And I make forked repo invalid + And I visit the forks page of the "Shop" project + Then I should see my fork on the list + + Scenario: Viewing private forks of a Project + Given There is an existent fork of the "Shop" project + And I click link "Fork" + When I fork to my namespace + And I visit the forks page of the "Shop" project + Then I should see my fork on the list + And I should not see the other fork listed + And I should see a private fork notice diff --git a/features/project/issues/award_emoji.feature b/features/project/issues/award_emoji.feature index 9a06fdc2ee..2945bb3753 100644 --- a/features/project/issues/award_emoji.feature +++ b/features/project/issues/award_emoji.feature @@ -7,8 +7,18 @@ Feature: Award Emoji And I visit "Bugfix" issue page @javascript - Scenario: I add and remove award in the issue + Scenario: I repeatedly add and remove thumbsup award in the issue + Given I click the thumbsup award Emoji + Then I have award added + Given I click the thumbsup award Emoji + Then I have no awards added + Given I click the thumbsup award Emoji + Then I have award added + + @javascript + Scenario: I add and remove custom award in the issue Given I click to emoji-picker + Then The search field is focused And I click to emoji in the picker Then I have award added And I can remove it by clicking to icon @@ -16,11 +26,13 @@ Feature: Award Emoji @javascript Scenario: I can see the list of emoji categories Given I click to emoji-picker + Then The search field is focused Then I can see the activity and food categories @javascript Scenario: I can search emoji Given I click to emoji-picker + Then The search field is focused And I search "hand" Then I see search result for "hand" diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index ab234bc750..89af58dcef 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -25,9 +25,16 @@ Feature: Project Issues Scenario: I visit issue page Given I click link "Release 0.4" Then I should see issue "Release 0.4" + And I should see "1 of 2" in the sidebar + + Scenario: I navigate between issues + Given I click link "Release 0.4" + Then I click link "Next" in the sidebar + Then I should see issue "Tweet control" + And I should see "2 of 2" in the sidebar @javascript - Scenario: I visit issue page + Scenario: I filter by author Given I add a user to project "Shop" And I click "author" dropdown Then I see current user as the first user @@ -51,6 +58,46 @@ Feature: Project Issues Then I should see comment "XML attached" And I should see an error alert section within the comment form + @javascript + Scenario: Visiting Issues after leaving a comment + Given I visit issue page "Release 0.4" + And I leave a comment like "XML attached" + And I visit project "Shop" issues page + And I sort the list by "Last updated" + Then I should see "Release 0.4" at the top + + @javascript + Scenario: Visiting Issues after being sorted the list + Given I visit project "Shop" issues page + And I sort the list by "Oldest updated" + And I visit my project's home page + And I visit project "Shop" issues page + Then The list should be sorted by "Oldest updated" + + @javascript + Scenario: Visiting Merge Requests after being sorted the list + Given I visit project "Shop" issues page + And I sort the list by "Oldest updated" + And I visit project "Shop" merge requests page + Then The list should be sorted by "Oldest updated" + + @javascript + Scenario: Visiting Merge Requests from a differente Project after sorting + Given I visit project "Shop" merge requests page + And I sort the list by "Oldest updated" + And I visit dashboard merge requests page + Then The list should be sorted by "Oldest updated" + + @javascript + Scenario: Sort issues by upvotes/downvotes + Given project "Shop" have "Bugfix" open issue + And issue "Release 0.4" have 2 upvotes and 1 downvote + And issue "Tweet control" have 1 upvote and 2 downvotes + And I sort the list by "Most popular" + Then The list should be sorted by "Most popular" + And I sort the list by "Least popular" + Then The list should be sorted by "Least popular" + @javascript Scenario: I search issue Given I fill in issue search with "Re" diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index aa9078b878..495f25f28e 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -39,6 +39,7 @@ Feature: Project Merge Requests Scenario: I visit merge request page Given I click link "Bug NS-04" Then I should see merge request "Bug NS-04" + And I should see "1 of 1" in the sidebar Scenario: I close merge request page Given I click link "Bug NS-04" @@ -75,6 +76,58 @@ Feature: Project Merge Requests And I leave a comment like "XML attached" Then I should see comment "XML attached" + @javascript + Scenario: Visiting Merge Requests after leaving a comment + Given project "Shop" have "Bug NS-05" open merge request with diffs inside + And I visit merge request page "Bug NS-04" + And I leave a comment like "XML attached" + And I visit project "Shop" merge requests page + And I sort the list by "Last updated" + Then I should see "Bug NS-04" at the top + + @javascript + Scenario: Visiting Merge Requests after being sorted the list + Given I visit project "Shop" merge requests page + And I sort the list by "Oldest updated" + And I visit my project's home page + And I visit project "Shop" merge requests page + Then The list should be sorted by "Oldest updated" + + @javascript + Scenario: Visiting Issues after being sorted the list + Given I visit project "Shop" merge requests page + And I sort the list by "Oldest updated" + And I visit project "Shop" issues page + Then The list should be sorted by "Oldest updated" + + @javascript + Scenario: Visiting Merge Requests from a differente Project after sorting + Given I visit project "Shop" merge requests page + And I sort the list by "Oldest updated" + And I visit dashboard merge requests page + Then The list should be sorted by "Oldest updated" + + @javascript + Scenario: Sort merge requests by upvotes/downvotes + Given project "Shop" have "Bug NS-05" open merge request with diffs inside + And project "Shop" have "Bug NS-06" open merge request + And merge request "Bug NS-04" have 2 upvotes and 1 downvote + And merge request "Bug NS-06" have 1 upvote and 2 downvotes + And I sort the list by "Most popular" + Then The list should be sorted by "Most popular" + And I sort the list by "Least popular" + Then The list should be sorted by "Least popular" + + @javascript + Scenario: Visiting Merge Requests after commenting on diffs + Given project "Shop" have "Bug NS-05" open merge request with diffs inside + And I visit merge request page "Bug NS-05" + And I click on the Changes tab + And I leave a comment like "Line is wrong" on diff + And I visit project "Shop" merge requests page + And I sort the list by "Last updated" + Then I should see "Bug NS-05" at the top + @javascript Scenario: I comment on a merge request diff Given project "Shop" have "Bug NS-05" open merge request with diffs inside @@ -83,6 +136,15 @@ Feature: Project Merge Requests And I leave a comment like "Line is wrong" on diff And I switch to the merge request's comments tab Then I should see a discussion has started on diff + And I should see a badge of "1" next to the discussion link + + @javascript + Scenario: I see a new comment on merge request diff from another user in the discussion tab + Given project "Shop" have "Bug NS-05" open merge request with diffs inside + And I visit merge request page "Bug NS-05" + And user "John Doe" leaves a comment like "Line is wrong" on diff + Then I should see a discussion by user "John Doe" has started on diff + And I should see a badge of "1" next to the discussion link @javascript Scenario: I edit a comment on a merge request diff @@ -100,9 +162,11 @@ Feature: Project Merge Requests And I visit merge request page "Bug NS-05" And I click on the Changes tab And I leave a comment like "Line is wrong" on diff + And I should see a badge of "1" next to the discussion link And I delete the comment "Line is wrong" on diff And I click on the Discussion tab Then I should not see any discussion + And I should see a badge of "0" next to the discussion link @javascript Scenario: I comment on a line of a commit in merge request diff --git a/features/project/merge_requests/revert.feature b/features/project/merge_requests/revert.feature new file mode 100644 index 0000000000..d767b08888 --- /dev/null +++ b/features/project/merge_requests/revert.feature @@ -0,0 +1,30 @@ +@project_merge_requests +Feature: Revert Merge Requests + Background: + Given There is an open Merge Request + And I am signed in as a developer of the project + And I am on the Merge Request detail page + And I click on Accept Merge Request + + @javascript + Scenario: I revert a merge request + Given I click on the revert button + And I revert the changes directly + Then I should see the revert merge request notice + + @javascript + Scenario: I revert a merge request that was previously reverted + Given I click on the revert button + And I revert the changes directly + And I am on the Merge Request detail page + And I click on the revert button + And I revert the changes directly + Then I should see a revert error + + @javascript + Scenario: I revert a merge request in a new merge request + Given I click on the revert button + And I am on the Merge Request detail page + And I click on the revert button + And I revert the changes in a new merge request + Then I should see the new merge request notice diff --git a/features/project/milestone.feature b/features/project/milestone.feature new file mode 100644 index 0000000000..713f0f3b97 --- /dev/null +++ b/features/project/milestone.feature @@ -0,0 +1,24 @@ +Feature: Project Milestone + Background: + Given I sign in as a user + And I own project "Shop" + And project "Shop" has labels: "bug", "feature", "enhancement" + And project "Shop" has milestone "v2.2" + And milestone has issue "Bugfix1" with labels: "bug", "feature" + And milestone has issue "Bugfix2" with labels: "bug", "enhancement" + + + @javascript + Scenario: Listing issues from issues tab + Given I visit project "Shop" milestones page + And I click link "v2.2" + Then I should see the labels "bug", "enhancement" and "feature" + And I should see the "bug" label listed only once + + @javascript + Scenario: Listing labels from labels tab + Given I visit project "Shop" milestones page + And I click link "v2.2" + And I click link "Labels" + Then I should see the list of labels + And I should see the labels "bug", "enhancement" and "feature" diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index a8c276b949..1e09dbc4c8 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -320,3 +320,13 @@ Feature: Project Source Browse Files Then I should see download link and object size And I should not see lfs pointer details And I should see buttons for allowed commands + + @javascript + Scenario: I preview an SVG file + Given I click on "Upload file" link in repo + And I upload a new SVG file + And I fill the upload file commit message + And I fill the new branch name + And I click on "Upload file" + Given I visit the SVG file + Then I can see the new rendered SVG image diff --git a/features/steps/admin/appearance.rb b/features/steps/admin/appearance.rb new file mode 100644 index 0000000000..0d1be46d11 --- /dev/null +++ b/features/steps/admin/appearance.rb @@ -0,0 +1,72 @@ +class Spinach::Features::AdminAppearance < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + + step 'submit form with new appearance' do + fill_in 'appearance_title', with: 'MyCompany' + fill_in 'appearance_description', with: 'dev server' + click_button 'Save' + end + + step 'I should be redirected to admin appearance page' do + expect(current_path).to eq admin_appearances_path + expect(page).to have_content 'Appearance settings' + end + + step 'I should see newly created appearance' do + expect(page).to have_field('appearance_title', with: 'MyCompany') + expect(page).to have_field('appearance_description', with: 'dev server') + expect(page).to have_content 'Last edit' + end + + step 'I click preview button' do + click_link "Preview" + end + + step 'application has custom appearance' do + create(:appearance) + end + + step 'I should see a customized appearance' do + expect(page).to have_content appearance.title + expect(page).to have_content appearance.description + end + + step 'I attach a logo' do + attach_file(:appearance_logo, Rails.root.join('spec', 'fixtures', 'dk.png')) + click_button 'Save' + end + + step 'I attach header logos' do + attach_file(:appearance_header_logo, Rails.root.join('spec', 'fixtures', 'dk.png')) + click_button 'Save' + end + + step 'I should see a logo' do + expect(page).to have_xpath('//img[@src="/uploads/appearance/logo/1/dk.png"]') + end + + step 'I should see header logos' do + expect(page).to have_xpath('//img[@src="/uploads/appearance/header_logo/1/dk.png"]') + end + + step 'I remove the logo' do + click_link 'Remove logo' + end + + step 'I remove the header logos' do + click_link 'Remove header logo' + end + + step 'I should see logo removed' do + expect(page).not_to have_xpath('//img[@src="/uploads/appearance/logo/1/gitlab_logo.png"]') + end + + step 'I should see header logos removed' do + expect(page).not_to have_xpath('//img[@src="/uploads/appearance/header_logo/1/header_logo_light.png"]') + end + + def appearance + Appearance.last + end +end diff --git a/features/steps/admin/broadcast_messages.rb b/features/steps/admin/broadcast_messages.rb index 6cacdf4764..af2b4a2931 100644 --- a/features/steps/admin/broadcast_messages.rb +++ b/features/steps/admin/broadcast_messages.rb @@ -19,7 +19,7 @@ class Spinach::Features::AdminBroadcastMessages < Spinach::FeatureSteps end step 'submit form with new customized broadcast message' do - fill_in 'broadcast_message_message', with: 'Application update from 4:00 CST to 5:00 CST' + fill_in 'broadcast_message_message', with: 'Application update from **4:00 CST to 5:00 CST**' fill_in 'broadcast_message_color', with: '#f2dede' fill_in 'broadcast_message_font', with: '#b94a48' select Date.today.next_year.year, from: "broadcast_message_ends_at_1i" @@ -28,6 +28,7 @@ class Spinach::Features::AdminBroadcastMessages < Spinach::FeatureSteps step 'I should see a customized broadcast message' do expect(page).to have_content 'Application update from 4:00 CST to 5:00 CST' + expect(page).to have_selector 'strong', text: '4:00 CST to 5:00 CST' expect(page).to have_selector %(div[style="background-color: #f2dede; color: #b94a48"]) end @@ -51,4 +52,15 @@ class Spinach::Features::AdminBroadcastMessages < Spinach::FeatureSteps step 'I should not see the removed broadcast message' do expect(page).not_to have_content 'Migration to new server' end + + step 'I enter a broadcast message with Markdown' do + fill_in 'broadcast_message_message', with: "Live **Markdown** previews. :tada:" + end + + step 'I should see a live preview of the rendered broadcast message' do + page.within('.broadcast-message-preview') do + expect(page).to have_selector('strong', text: 'Markdown') + expect(page).to have_selector('img.emoji') + end + end end diff --git a/features/steps/admin/spam_logs.rb b/features/steps/admin/spam_logs.rb new file mode 100644 index 0000000000..ad825fd414 --- /dev/null +++ b/features/steps/admin/spam_logs.rb @@ -0,0 +1,28 @@ +class Spinach::Features::AdminSpamLogs < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedAdmin + + step 'I should see list of spam logs' do + expect(page).to have_content('Spam Logs') + expect(page).to have_content spam_log.source_ip + expect(page).to have_content spam_log.noteable_type + expect(page).to have_content 'N' + expect(page).to have_content spam_log.title + expect(page).to have_content truncate(spam_log.description) + expect(page).to have_link('Remove user') + expect(page).to have_link('Block user') + end + + step 'spam logs exist' do + create(:spam_log) + end + + def spam_log + @spam_log ||= SpamLog.first + end + + def truncate(description) + "#{spam_log.description[0...97]}..." + end +end diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index 63f0ec2b6e..5062e34884 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -2,6 +2,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps include SharedAuthentication include SharedPaths include SharedProject + include SharedIssuable step 'I should see "New Project" link' do expect(page).to have_link "New project" diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb new file mode 100644 index 0000000000..9722a5a848 --- /dev/null +++ b/features/steps/dashboard/todos.rb @@ -0,0 +1,128 @@ +class Spinach::Features::DashboardTodos < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedProject + include SharedUser + include Select2Helper + + step '"John Doe" is a developer of project "Shop"' do + project.team << [john_doe, :developer] + end + + step 'I am a developer of project "Enterprise"' do + enterprise.team << [current_user, :developer] + end + + step '"Mary Jane" is a developer of project "Shop"' do + project.team << [john_doe, :developer] + end + + step 'I have todos' do + create(:todo, user: current_user, project: project, author: mary_jane, target: issue, action: Todo::MENTIONED) + create(:todo, user: current_user, project: project, author: john_doe, target: issue, action: Todo::ASSIGNED) + note = create(:note, author: john_doe, noteable: issue, note: "#{current_user.to_reference} Wdyt?") + create(:todo, user: current_user, project: project, author: john_doe, target: issue, action: Todo::MENTIONED, note: note) + create(:todo, user: current_user, project: project, author: john_doe, target: merge_request, action: Todo::ASSIGNED) + end + + step 'I should see todos assigned to me' do + expect(page).to have_content 'To do 4' + expect(page).to have_content 'Done 0' + + expect(page).to have_link project.name_with_namespace + should_see_todo(1, "John Doe assigned you merge request !#{merge_request.iid}", merge_request.title) + should_see_todo(2, "John Doe mentioned you on issue ##{issue.iid}", "#{current_user.to_reference} Wdyt?") + should_see_todo(3, "John Doe assigned you issue ##{issue.iid}", issue.title) + should_see_todo(4, "Mary Jane mentioned you on issue ##{issue.iid}", issue.title) + end + + step 'I mark the todo as done' do + page.within('.todo:nth-child(1)') do + click_link 'Done' + end + + expect(page).to have_content 'Todo was successfully marked as done.' + expect(page).to have_content 'To do 3' + expect(page).to have_content 'Done 1' + should_not_see_todo "John Doe assigned you merge request !#{merge_request.iid}" + end + + step 'I click on the "Done" tab' do + click_link 'Done 1' + end + + step 'I should see all todos marked as done' do + expect(page).to have_link project.name_with_namespace + should_see_todo(1, "John Doe assigned you merge request !#{merge_request.iid}", merge_request.title, false) + end + + step 'I filter by "Enterprise"' do + select2(enterprise.id, from: "#project_id") + end + + step 'I filter by "John Doe"' do + select2(john_doe.id, from: "#author_id") + end + + step 'I filter by "Issue"' do + select2('Issue', from: "#type") + end + + step 'I filter by "Mentioned"' do + select2("#{Todo::MENTIONED}", from: '#action_id') + end + + step 'I should not see todos' do + expect(page).to have_content "You're all done!" + end + + step 'I should not see todos related to "Mary Jane" in the list' do + should_not_see_todo "Mary Jane mentioned you on issue ##{issue.iid}" + end + + step 'I should not see todos related to "Merge Requests" in the list' do + should_not_see_todo "John Doe assigned you merge request !#{merge_request.iid}" + end + + step 'I should not see todos related to "Assignments" in the list' do + should_not_see_todo "John Doe assigned you merge request !#{merge_request.iid}" + should_not_see_todo "John Doe assigned you issue ##{issue.iid}" + end + + def should_see_todo(position, title, body, pending = true) + page.within(".todo:nth-child(#{position})") do + expect(page).to have_content title + expect(page).to have_content body + + if pending + expect(page).to have_link 'Done' + else + expect(page).to_not have_link 'Done' + end + end + end + + def should_not_see_todo(title) + expect(page).not_to have_content title + end + + def john_doe + @john_doe ||= user_exists("John Doe", { username: "john_doe" }) + end + + def mary_jane + @mary_jane ||= user_exists("Mary Jane", { username: "mary_jane" }) + end + + def enterprise + @enterprise ||= Project.find_by(name: 'Enterprise') + end + + def issue + @issue ||= create(:issue, assignee: current_user, project: project) + end + + def merge_request + @merge_request ||= create(:merge_request, assignee: current_user, source_project: project) + end +end diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 4c5122d1b7..1e2a78a602 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -120,6 +120,10 @@ class Spinach::Features::Groups < Spinach::FeatureSteps expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived') end + step 'I visit group "NonExistentGroup" page' do + visit group_path(-1) + end + private def assigned_to_me(key) diff --git a/features/steps/login_form.rb b/features/steps/login_form.rb deleted file mode 100644 index b9ff6ae67f..0000000000 --- a/features/steps/login_form.rb +++ /dev/null @@ -1,25 +0,0 @@ -class Spinach::Features::LoginForm < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedSnippet - include SharedUser - include SharedSearch - - step 'Crowd integration enabled' do - @providers_orig = Gitlab::OAuth::Provider.providers - @omniauth_conf_orig = Gitlab.config.omniauth.enabled - expect(Gitlab::OAuth::Provider).to receive(:providers).and_return([:crowd]) - allow_any_instance_of(ApplicationHelper).to receive(:user_omniauth_authorize_path).and_return(root_path) - expect(Gitlab.config.omniauth).to receive(:enabled).and_return(true) - end - - step 'I should see Crowd login form' do - expect(page).to have_selector '#tab-crowd form' - Gitlab::OAuth::Provider.stub(:providers).and_return(@providers_orig) - Gitlab.config.omniauth.stub(:enabled).and_return(@omniauth_conf_orig) - end - - step 'I visit sign in page' do - visit new_user_session_path - end -end diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index 0305f7e6da..6b0c1049ec 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -97,7 +97,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step "I should see a password error message" do - page.within '.alert' do + page.within '.alert-danger' do expect(page).to have_content "Password confirmation doesn't match" end end diff --git a/features/steps/project/badges/build.rb b/features/steps/project/badges/build.rb new file mode 100644 index 0000000000..47540f356e --- /dev/null +++ b/features/steps/project/badges/build.rb @@ -0,0 +1,32 @@ +class Spinach::Features::ProjectBadgesBuild < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedBuilds + include RepoHelpers + + step 'I display builds badge for a master branch' do + visit build_namespace_project_badges_path(@project.namespace, @project, ref: :master, format: :svg) + end + + step 'I should see a build success badge' do + expect_badge('success') + end + + step 'I should see a build failed badge' do + expect_badge('failed') + end + + step 'I should see a build running badge' do + expect_badge('running') + end + + step 'I should see a badge that has not been cached' do + expect(page.response_headers).to include('Cache-Control' => 'no-cache') + end + + def expect_badge(status) + svg = Nokogiri::XML.parse(page.body) + expect(page.response_headers).to include('Content-Type' => 'image/svg+xml') + expect(svg.at(%Q{text:contains("#{status}")})).to be_truthy + end +end diff --git a/features/steps/project/builds/artifacts.rb b/features/steps/project/builds/artifacts.rb index 25f2f4e837..1bdb57af9d 100644 --- a/features/steps/project/builds/artifacts.rb +++ b/features/steps/project/builds/artifacts.rb @@ -73,4 +73,14 @@ class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps expect(response_json[:archive]).to end_with('build_artifacts.zip') expect(response_json[:entry]).to eq Base64.encode64('ci_artifacts.txt') end + + step 'I click a first row within build artifacts table' do + row = first('tr[data-link]') + @row_path = row['data-link'] + row.click + end + + step 'page with a coresponding path is loading' do + expect(current_path).to eq @row_path + end end diff --git a/features/steps/project/builds/summary.rb b/features/steps/project/builds/summary.rb index 2439d48fbe..4688a0e209 100644 --- a/features/steps/project/builds/summary.rb +++ b/features/steps/project/builds/summary.rb @@ -4,11 +4,30 @@ class Spinach::Features::ProjectBuildsSummary < Spinach::FeatureSteps include SharedBuilds include RepoHelpers - step 'I see summary for build' do - expect(page).to have_content "Build ##{@build.id}" + step 'I see button to CI Lint' do + page.within('.nav-controls') do + ci_lint_tool_link = page.find_link('CI Lint') + expect(ci_lint_tool_link[:href]).to eq ci_lint_path + end end - step 'I see build trace' do - expect(page).to have_css '#build-trace' + step 'I click erase build button' do + click_link 'Erase' + end + + step 'recent build has been erased' do + expect(@build.artifacts_file.exists?).to be_falsy + expect(@build.artifacts_metadata.exists?).to be_falsy + expect(@build.trace).to be_empty + end + + step 'recent build summary does not have artifacts widget' do + expect(page).to have_no_css('.artifacts') + end + + step 'recent build summary contains information saying that build has been erased' do + page.within('.erased') do + expect(page).to have_content 'Build has been erased' + end end end diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index daf6cdaaac..f9fd733246 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -33,6 +33,13 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps expect(page).to have_content "Showing #{sample_commit.files_changed_count} changed files" end + step 'I fill compare fields with branches' do + fill_in 'from', with: 'feature' + fill_in 'to', with: 'master' + + click_button 'Compare' + end + step 'I fill compare fields with refs' do fill_in "from", with: sample_commit.parent_id fill_in "to", with: sample_commit.id @@ -56,6 +63,56 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps expect(page).to have_content "Showing 2 changed files" end + step 'I visit commits list page for feature branch' do + visit namespace_project_commits_path(@project.namespace, @project, 'feature', { limit: 5 }) + end + + step 'I see feature branch commits' do + commit = @project.repository.commit('0b4bc9a') + expect(page).to have_content(@project.name) + expect(page).to have_content(commit.message[0..12]) + expect(page).to have_content(commit.short_id) + end + + step 'project have an open merge request' do + create(:merge_request, + title: 'Feature', + source_project: @project, + source_branch: 'feature', + target_branch: 'master', + author: @project.users.first + ) + end + + step 'I click the "Compare" tab' do + click_link('Compare') + end + + step 'I fill compare fields with branches' do + fill_in 'from', with: 'master' + fill_in 'to', with: 'feature' + + click_button 'Compare' + end + + step 'I see compared branches' do + expect(page).to have_content 'Commits (1)' + expect(page).to have_content 'Showing 1 changed file with 5 additions and 0 deletions' + end + + step 'I see button to create a new merge request' do + expect(page).to have_link 'Create Merge Request' + end + + step 'I should not see button to create a new merge request' do + expect(page).to_not have_link 'Create Merge Request' + end + + step 'I should see button to the merge request' do + merge_request = MergeRequest.find_by(title: 'Feature') + expect(page).to have_link "View Open Merge Request", href: namespace_project_merge_request_path(@project.namespace, @project, merge_request) + end + step 'I see breadcrumb links' do expect(page).to have_selector('ul.breadcrumb') expect(page).to have_selector('ul.breadcrumb a', count: 4) diff --git a/features/steps/project/commits/revert.rb b/features/steps/project/commits/revert.rb new file mode 100644 index 0000000000..94a5d4e2e4 --- /dev/null +++ b/features/steps/project/commits/revert.rb @@ -0,0 +1,40 @@ +class Spinach::Features::RevertCommits < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedPaths + include SharedDiffNote + include RepoHelpers + + step 'I click on commit link' do + visit namespace_project_commit_path(@project.namespace, @project, sample_commit.id) + end + + step 'I click on the revert button' do + find("a[href='#modal-revert-commit']").click + end + + step 'I revert the changes directly' do + page.within('#modal-revert-commit') do + uncheck 'create_merge_request' + click_button 'Revert' + end + end + + step 'I should see the revert commit notice' do + page.should have_content('The commit has been successfully reverted.') + end + + step 'I should see a revert error' do + page.should have_content('Sorry, we cannot revert this commit automatically.') + end + + step 'I revert the changes in a new merge request' do + page.within('#modal-revert-commit') do + click_button 'Revert' + end + end + + step 'I should see the new merge request notice' do + page.should have_content('The commit has been successfully reverted. You can now submit a merge request to get this change into the original branch.') + end +end diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index e98bd51ca8..527f7853da 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -49,4 +49,35 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps step 'I should see the new merge request page for my namespace' do current_path.should have_content(/#{current_user.namespace.name}/i) end + + step 'I visit the forks page of the "Shop" project' do + @project = Project.where(name: 'Shop').last + visit namespace_project_forks_path(@project.namespace, @project) + end + + step 'I should see my fork on the list' do + page.within('.projects-list-holder') do + project = @user.fork_of(@project) + expect(page).to have_content("#{project.namespace.human_name} / #{project.name}") + end + end + + step 'I make forked repo invalid' do + project = @user.fork_of(@project) + project.path = 'test-crappy-path' + project.save! + end + + step 'There is an existent fork of the "Shop" project' do + user = create(:user, name: 'Mike') + @forked_project = Projects::ForkService.new(@project, user).execute + end + + step 'I should not see the other fork listed' do + expect(page).not_to have_content("#{@forked_project.namespace.human_name} / #{@forked_project.name}") + end + + step 'I should see a private fork notice' do + expect(page).to have_content("1 private fork") + end end diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index cbdce78dc0..7e4425ff66 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -43,7 +43,9 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps expect(page).to have_css("h3.page-title", text: "New Merge Request") - fill_in "merge_request_title", with: "Merge Request On Forked Project" + page.within 'form#new_merge_request' do + fill_in "merge_request_title", with: "Merge Request On Forked Project" + end end step 'I submit the merge request' do diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb index 2c2ed08655..93cf608cc6 100644 --- a/features/steps/project/issues/award_emoji.rb +++ b/features/steps/project/issues/award_emoji.rb @@ -8,6 +8,15 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps visit namespace_project_issue_path(@project.namespace, @project, @issue) end + step 'I click the thumbsup award Emoji' do + page.within '.awards' do + thumbsup = page.find('.award .emoji-1F44D') + thumbsup.click + thumbsup.hover + sleep 0.3 + end + end + step 'I click to emoji-picker' do page.within '.awards-controls' do page.find('.add-award').click @@ -37,9 +46,28 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps end step 'I have award added' do + sleep 0.2 + page.within '.awards' do expect(page).to have_selector '.award' expect(page.find('.award.active .counter')).to have_content '1' + expect(page.find('.award.active')['data-original-title']).to eq('me') + end + end + + step 'I have no awards added' do + page.within '.awards' do + expect(page).to have_selector '.award' + expect(page.all('.award').size).to eq(2) + + # Check tooltip data + page.all('.award').each do |element| + expect(element['title']).to eq("") + end + + page.all('.award .counter').each do |element| + expect(element).to have_content '0' + end end end @@ -66,4 +94,8 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps expect(page).to have_selector '[data-emoji="raised_hand"]' end end + + step 'The search field is focused' do + page.evaluate_script("document.activeElement.id").should eq "emoji_search" + end end diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index 8e8c9c5745..565bf088b4 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -54,6 +54,10 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps expect(page).to have_content "Release 0.4" end + step 'I should see issue "Tweet control"' do + expect(page).to have_content "Tweet control" + end + step 'I click link "New Issue"' do click_link "New Issue" end @@ -170,6 +174,13 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps author: project.users.first) end + step 'project "Shop" have "Bugfix" open issue' do + create(:issue, + title: "Bugfix", + project: project, + author: project.users.first) + end + step 'project "Shop" have "Release 0.3" closed issue' do create(:closed_issue, title: "Release 0.3", @@ -177,6 +188,56 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps author: project.users.first) end + step 'issue "Release 0.4" have 2 upvotes and 1 downvote' do + issue = Issue.find_by(title: 'Release 0.4') + create_list(:upvote_note, 2, project: project, noteable: issue) + create(:downvote_note, project: project, noteable: issue) + end + + step 'issue "Tweet control" have 1 upvote and 2 downvotes' do + issue = Issue.find_by(title: 'Tweet control') + create(:upvote_note, project: project, noteable: issue) + create_list(:downvote_note, 2, project: project, noteable: issue) + end + + step 'The list should be sorted by "Least popular"' do + page.within '.issues-list' do + page.within 'li.issue:nth-child(1)' do + expect(page).to have_content 'Tweet control' + expect(page).to have_content '1 2' + end + + page.within 'li.issue:nth-child(2)' do + expect(page).to have_content 'Release 0.4' + expect(page).to have_content '2 1' + end + + page.within 'li.issue:nth-child(3)' do + expect(page).to have_content 'Bugfix' + expect(page).to_not have_content '0 0' + end + end + end + + step 'The list should be sorted by "Most popular"' do + page.within '.issues-list' do + page.within 'li.issue:nth-child(1)' do + expect(page).to have_content 'Release 0.4' + expect(page).to have_content '2 1' + end + + page.within 'li.issue:nth-child(2)' do + expect(page).to have_content 'Tweet control' + expect(page).to have_content '1 2' + end + + page.within 'li.issue:nth-child(3)' do + expect(page).to have_content 'Bugfix' + expect(page).to_not have_content '0 0' + end + end + end + step 'empty project "Empty Project"' do create :empty_project, name: 'Empty Project', namespace: @user.namespace end @@ -293,7 +354,13 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps expect(page).to have_content('Yay!') end end + + step 'I should see "Release 0.4" at the top' do + expect(page.find('ul.content-list.issues-list li.issue:first-child')).to have_content("Release 0.4") + end + def filter_issue(text) fill_in 'issue_search', with: text end + end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 28f87a9bea..dde864f518 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -138,6 +138,56 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps author: project.users.first) end + step 'merge request "Bug NS-04" have 2 upvotes and 1 downvote' do + merge_request = MergeRequest.find_by(title: 'Bug NS-04') + create_list(:upvote_note, 2, project: project, noteable: merge_request) + create(:downvote_note, project: project, noteable: merge_request) + end + + step 'merge request "Bug NS-06" have 1 upvote and 2 downvotes' do + merge_request = MergeRequest.find_by(title: 'Bug NS-06') + create(:upvote_note, project: project, noteable: merge_request) + create_list(:downvote_note, 2, project: project, noteable: merge_request) + end + + step 'The list should be sorted by "Least popular"' do + page.within '.mr-list' do + page.within 'li.merge-request:nth-child(1)' do + expect(page).to have_content 'Bug NS-06' + expect(page).to have_content '1 2' + end + + page.within 'li.merge-request:nth-child(2)' do + expect(page).to have_content 'Bug NS-04' + expect(page).to have_content '2 1' + end + + page.within 'li.merge-request:nth-child(3)' do + expect(page).to have_content 'Bug NS-05' + expect(page).to_not have_content '0 0' + end + end + end + + step 'The list should be sorted by "Most popular"' do + page.within '.mr-list' do + page.within 'li.merge-request:nth-child(1)' do + expect(page).to have_content 'Bug NS-04' + expect(page).to have_content '2 1' + end + + page.within 'li.merge-request:nth-child(2)' do + expect(page).to have_content 'Bug NS-06' + expect(page).to have_content '1 2' + end + + page.within 'li.merge-request:nth-child(3)' do + expect(page).to have_content 'Bug NS-05' + expect(page).to_not have_content '0 0' + end + end + end + step 'I click on the Changes tab' do page.within '.merge-request-tabs' do click_link 'Changes' @@ -181,6 +231,15 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps leave_comment "Line is wrong" end + step 'user "John Doe" leaves a comment like "Line is wrong" on diff' do + mr = MergeRequest.find_by(title: "Bug NS-05") + create(:note_on_merge_request_diff, project: project, + noteable_id: mr.id, + author: user_exists("John Doe"), + line_code: sample_commit.line_code, + note: 'Line is wrong') + end + step 'I leave a comment like "Line is wrong" on diff in commit' do click_diff_line(sample_commit.line_code) leave_comment "Line is wrong" @@ -238,6 +297,22 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end end + step 'I should see a discussion by user "John Doe" has started on diff' do + page.within(".notes .discussion") do + page.should have_content "#{user_exists("John Doe").name} started a discussion" + page.should have_content sample_commit.line_code_path + page.should have_content "Line is wrong" + end + end + + step 'I should see a badge of "1" next to the discussion link' do + expect_discussion_badge_to_have_counter("1") + end + + step 'I should see a badge of "0" next to the discussion link' do + expect_discussion_badge_to_have_counter("0") + end + step 'I should see a discussion has started on commit diff' do page.within(".notes .discussion") do page.should have_content "#{current_user.name} started a discussion on commit" @@ -415,6 +490,14 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end end + step 'I should see "Bug NS-05" at the top' do + expect(page.find('ul.content-list.mr-list li.merge-request:first-child')).to have_content("Bug NS-05") + end + + step 'I should see "Bug NS-04" at the top' do + expect(page.find('ul.content-list.mr-list li.merge-request:first-child')).to have_content("Bug NS-04") + end + def merge_request @merge_request ||= MergeRequest.find_by!(title: "Bug NS-05") end @@ -444,4 +527,10 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps def have_visible_content (text) have_css("*", text: text, visible: true) end + + def expect_discussion_badge_to_have_counter(value) + page.within(".notes-tab .badge") do + page.should have_content value + end + end end diff --git a/features/steps/project/merge_requests/revert.rb b/features/steps/project/merge_requests/revert.rb new file mode 100644 index 0000000000..c5a4cfce6f --- /dev/null +++ b/features/steps/project/merge_requests/revert.rb @@ -0,0 +1,56 @@ +class Spinach::Features::RevertMergeRequests < Spinach::FeatureSteps + include LoginHelpers + include GitlabRoutingHelper + + step 'I click on the revert button' do + find("a[href='#modal-revert-commit']").click + end + + step 'I revert the changes directly' do + page.within('#modal-revert-commit') do + uncheck 'create_merge_request' + click_button 'Revert' + end + end + + step 'I should see the revert merge request notice' do + page.should have_content('The merge request has been successfully reverted.') + end + + step 'I should not see the revert button' do + expect(page).not_to have_selector(:xpath, "a[href='#modal-revert-commit']") + end + + step 'I am on the Merge Request detail page' do + visit merge_request_path(@merge_request) + end + + step 'I click on Accept Merge Request' do + click_button('Accept Merge Request') + end + + step 'I am signed in as a developer of the project' do + login_as(@user) + end + + step 'There is an open Merge Request' do + @user = create(:user) + @project = create(:project, :public) + @project_member = create(:project_member, user: @user, project: @project, access_level: ProjectMember::DEVELOPER) + @merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project) + end + + step 'I should see a revert error' do + page.should have_content('Sorry, we cannot revert this merge request automatically.') + end + + step 'I revert the changes in a new merge request' do + page.within('#modal-revert-commit') do + click_button 'Revert' + end + end + + step 'I should see the new merge request notice' do + page.should have_content('The merge request has been successfully reverted. You can now submit a merge request to get this change into the original branch.') + end +end diff --git a/features/steps/project/project_milestone.rb b/features/steps/project/project_milestone.rb new file mode 100644 index 0000000000..2508c09e36 --- /dev/null +++ b/features/steps/project/project_milestone.rb @@ -0,0 +1,59 @@ +class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedPaths + + step 'milestone has issue "Bugfix1" with labels: "bug", "feature"' do + project = Project.find_by(name: "Shop") + milestone = project.milestones.find_by(title: 'v2.2') + issue = create(:issue, title: "Bugfix1", project: project, milestone: milestone) + issue.labels << project.labels.find_by(title: 'bug') + issue.labels << project.labels.find_by(title: 'feature') + end + + step 'milestone has issue "Bugfix2" with labels: "bug", "enhancement"' do + project = Project.find_by(name: "Shop") + milestone = project.milestones.find_by(title: 'v2.2') + issue = create(:issue, title: "Bugfix2", project: project, milestone: milestone) + issue.labels << project.labels.find_by(title: 'bug') + issue.labels << project.labels.find_by(title: 'enhancement') + end + + step 'project "Shop" has milestone "v2.2"' do + project = Project.find_by(name: "Shop") + milestone = create(:milestone, + title: "v2.2", + project: project, + description: "# Description header" + ) + 3.times { create(:issue, project: project, milestone: milestone) } + end + + step 'I should see the list of labels' do + expect(page).to have_selector('ul.manage-labels-list') + end + + step 'I should see the labels "bug", "enhancement" and "feature"' do + page.within('#tab-issues') do + expect(page).to have_content 'bug' + expect(page).to have_content 'enhancement' + expect(page).to have_content 'feature' + end + end + + step 'I should see the "bug" label listed only once' do + page.within('#tab-labels') do + expect(page).to have_content('bug', count: 1) + end + end + + step 'I click link "v2.2"' do + click_link "v2.2" + end + + step 'I click link "Labels"' do + page.within('.nav-links') do + page.find(:xpath, "//a[@href='#tab-labels']").click + end + end +end diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index d08935aa10..51b1579167 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -52,7 +52,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I should see raw file content' do - expect(source).to eq sample_blob.data + expect(source).to eq '' # Body is filled in by gitlab-workhorse end step 'I click button "Edit"' do @@ -351,6 +351,19 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps expect(page).to have_content "You're not allowed to make changes to this project directly. A fork of this project has been created that you can make changes in, so you can submit a merge request." end + # SVG files + step 'I upload a new SVG file' do + drop_in_dropzone test_svg_file + end + + step 'I visit the SVG file' do + visit namespace_project_blob_path(@project.namespace, @project, 'new_branch_name/logo_sample.svg') + end + + step 'I can see the new rendered SVG image' do + expect(find('.file-content')).to have_css('img') + end + private def set_new_content @@ -410,4 +423,8 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps def test_image_file File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') end + + def test_svg_file + File.join(Rails.root, 'spec', 'fixtures', 'logo_sample.svg') + end end diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb index 3a4f7a6e01..2134dae168 100644 --- a/features/steps/project/source/markdown_render.rb +++ b/features/steps/project/source/markdown_render.rb @@ -238,7 +238,11 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps step 'I see new wiki page named test' do expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "test") - expect(page).to have_content "Edit Page test" + + page.within(:css, ".nav-text") do + expect(page).to have_content "Test" + expect(page).to have_content "Edit Page" + end end When 'I go back to wiki page home' do @@ -252,7 +256,11 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps step 'I see Gitlab API document' do expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "api") - expect(page).to have_content "Edit Page api" + + page.within(:css, ".nav-text") do + expect(page).to have_content "Edit" + expect(page).to have_content "Api" + end end step 'I click on Rake tasks link' do @@ -261,7 +269,11 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps step 'I see Rake tasks directory' do expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "raketasks") - expect(page).to have_content "Edit Page raketasks" + + page.within(:css, ".nav-text") do + expect(page).to have_content "Edit" + expect(page).to have_content "Rake" + end end step 'I go directory which contains README file' do diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb index d753ae1459..223b7277b5 100644 --- a/features/steps/project/wiki.rb +++ b/features/steps/project/wiki.rb @@ -120,7 +120,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps step 'I should see the new wiki page form' do expect(current_path).to match('wikis/image.jpg') expect(page).to have_content('New Wiki Page') - expect(page).to have_content('Edit Page image.jpg') + expect(page).to have_content('Edit Page') end step 'I create a New page with paths' do @@ -159,11 +159,13 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I should see the page history' do - expect(page).to have_content('History for') + page.within(:css, ".nav-text") do + expect(page).to have_content('History') + end end step 'I search for Wiki content' do - fill_in "Search in this project", with: "wiki_content" + fill_in "Search", with: "wiki_content" click_button "Search" end diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb index f88b01af84..0bd5d93b99 100644 --- a/features/steps/shared/builds.rb +++ b/features/steps/shared/builds.rb @@ -6,14 +6,30 @@ module SharedBuilds end step 'project has a recent build' do - ci_commit = create :ci_commit, project: @project, sha: sample_commit.id - @build = create :ci_build, commit: ci_commit + @ci_commit = create(:ci_commit, project: @project, sha: @project.commit.sha) + @build = create(:ci_build, commit: @ci_commit) end - step 'I visit recent build summary page' do + step 'recent build is successful' do + @build.update_column(:status, 'success') + end + + step 'recent build failed' do + @build.update_column(:status, 'failed') + end + + step 'project has another build that is running' do + create(:ci_build, commit: @ci_commit, name: 'second build', status: 'running') + end + + step 'I visit recent build details page' do visit namespace_project_build_path(@project.namespace, @project, @build) end + step 'I visit project builds page' do + visit namespace_project_builds_path(@project.namespace, @project) + end + step 'recent build has artifacts available' do artifacts = Rails.root + 'spec/fixtures/ci_build_artifacts.zip' archive = fixture_file_upload(artifacts, 'application/zip') @@ -26,6 +42,10 @@ module SharedBuilds @build.update_attributes(artifacts_metadata: gzip) end + step 'recent build has a build trace' do + @build.trace = 'build trace' + end + step 'download of build artifacts archive starts' do expect(page.response_headers['Content-Type']).to eq 'application/zip' expect(page.response_headers['Content-Transfer-Encoding']).to eq 'binary' @@ -34,4 +54,21 @@ module SharedBuilds step 'I access artifacts download page' do visit download_namespace_project_build_artifacts_path(@project.namespace, @project, @build) end + + step 'I see details of a build' do + expect(page).to have_content "Build ##{@build.id}" + end + + step 'I see build trace' do + expect(page).to have_css '#build-trace' + end + + step 'I see the build' do + page.within('.commit_status') do + expect(page).to have_content "##{@build.id}" + expect(page).to have_content @build.sha[0..7] + expect(page).to have_content @build.ref + expect(page).to have_content @build.name + end + end end diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index c6a0ae2ba3..06e6944189 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -23,7 +23,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[rel$='#{sample_commit.line_code}']") do + page.within("form[id$='#{sample_commit.line_code}']") do fill_in "note[note]", with: "Typo, please fix" find(".js-comment-button").trigger("click") sleep 0.05 @@ -33,7 +33,7 @@ module SharedDiffNote step 'I leave a diff comment in a parallel view on the left side like "Old comment"' do click_parallel_diff_line(sample_commit.line_code, 'old') - page.within("#{diff_file_selector} form[rel$='#{sample_commit.line_code}']") do + page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}']") do fill_in "note[note]", with: "Old comment" find(".js-comment-button").trigger("click") end @@ -41,7 +41,7 @@ module SharedDiffNote step 'I leave a diff comment in a parallel view on the right side like "New comment"' do click_parallel_diff_line(sample_commit.line_code, 'new') - page.within("#{diff_file_selector} form[rel$='#{sample_commit.line_code}']") do + page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}']") do fill_in "note[note]", with: "New comment" find(".js-comment-button").trigger("click") end @@ -51,7 +51,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[rel$='#{sample_commit.line_code}']") do + page.within("form[id$='#{sample_commit.line_code}']") do fill_in "note[note]", with: "Should fix it :smile:" find('.js-md-preview-button').click end @@ -62,7 +62,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.del_line_code) - page.within("form[rel$='#{sample_commit.del_line_code}']") do + page.within("form[id$='#{sample_commit.del_line_code}']") do fill_in "note[note]", with: "DRY this up" find('.js-md-preview-button').click end @@ -91,7 +91,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[rel$='#{sample_commit.line_code}']") do + page.within("form[id$='#{sample_commit.line_code}']") do fill_in 'note[note]', with: ':smile:' click_button('Add Comment') end diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb index 4c5f7488ef..ae10c6069a 100644 --- a/features/steps/shared/issuable.rb +++ b/features/steps/shared/issuable.rb @@ -106,6 +106,53 @@ module SharedIssuable edit_issuable end + step 'I sort the list by "Oldest updated"' do + find('button.dropdown-toggle.btn').click + page.within('ul.dropdown-menu.dropdown-menu-align-right li') do + click_link "Oldest updated" + end + end + + step 'I sort the list by "Least popular"' do + find('button.dropdown-toggle.btn').click + + page.within('ul.dropdown-menu.dropdown-menu-align-right li') do + click_link 'Least popular' + end + end + + step 'I sort the list by "Most popular"' do + find('button.dropdown-toggle.btn').click + + page.within('ul.dropdown-menu.dropdown-menu-align-right li') do + click_link 'Most popular' + end + end + + step 'The list should be sorted by "Oldest updated"' do + page.within('div.dropdown.inline.prepend-left-10') do + expect(page.find('button.dropdown-toggle.btn')).to have_content('Oldest updated') + end + end + + step 'I should see "1 of 1" in the sidebar' do + expect_sidebar_content('1 of 1') + end + + step 'I should see "1 of 2" in the sidebar' do + expect_sidebar_content('1 of 2') + end + + step 'I should see "2 of 2" in the sidebar' do + expect_sidebar_content('2 of 2') + end + + step 'I click link "Next" in the sidebar' do + page.within '.issuable-sidebar' do + click_link 'Next' + end + end + def create_issuable_for_project(project_name:, title:, type: :issue) project = Project.find_by(name: project_name) @@ -146,4 +193,10 @@ module SharedIssuable expect(page).to have_content("mentioned in #{issuable.class.to_s.titleize.downcase} #{issuable.to_reference(project)}") end + def expect_sidebar_content(content) + page.within '.issuable-sidebar' do + expect(page).to have_content content + end + end + end diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index 444d6726f9..eb6df61b8e 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -144,4 +144,11 @@ module SharedNote expect(page).to have_content("+1 Awesome!") end end + + step 'I sort the list by "Last updated"' do + find('button.dropdown-toggle.btn').click + page.within('ul.dropdown-menu.dropdown-menu-align-right li') do + click_link "Last updated" + end + end end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index 4264c9c6f1..4478d418d8 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -7,6 +7,10 @@ module SharedPaths visit new_project_path end + step 'I visit login page' do + visit new_user_session_path + end + # ---------------------------------------- # User # ---------------------------------------- @@ -103,6 +107,10 @@ module SharedPaths visit dashboard_groups_path end + step 'I visit dashboard todos page' do + visit dashboard_todos_path + end + step 'I should be redirected to the dashboard groups page' do expect(current_path).to eq dashboard_groups_path end @@ -183,6 +191,10 @@ module SharedPaths visit admin_groups_path end + step 'I visit admin appearance page' do + visit admin_appearances_path + end + step 'I visit admin teams page' do visit admin_teams_path end @@ -191,6 +203,10 @@ module SharedPaths visit admin_application_settings_path end + step 'I visit spam logs page' do + visit admin_spam_logs_path + end + step 'I visit applications page' do visit admin_applications_path end diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index d9c75d1223..b13e82f276 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -240,6 +240,18 @@ module SharedProject end end + step 'The project is internal' do + @project.update(visibility_level: Gitlab::VisibilityLevel::INTERNAL) + end + + step 'public access for builds is enabled' do + @project.update(public_builds: true) + end + + step 'public access for builds is disabled' do + @project.update(public_builds: false) + end + def user_owns_project(user_name:, project_name:, visibility: :private) user = user_exists(user_name, username: user_name.gsub(/\s/, '').underscore) project = Project.find_by(name: project_name) diff --git a/features/support/capybara.rb b/features/support/capybara.rb index 4156c7ec48..f33379f76c 100644 --- a/features/support/capybara.rb +++ b/features/support/capybara.rb @@ -6,11 +6,7 @@ timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 15 Capybara.javascript_driver = :poltergeist Capybara.register_driver :poltergeist do |app| - Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout) -end - -Spinach.hooks.on_tag("javascript") do - Capybara.current_driver = Capybara.javascript_driver + Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout, window_size: [1366, 768]) end Capybara.default_wait_time = timeout @@ -22,3 +18,7 @@ unless ENV['CI'] || ENV['CI_SERVER'] # Keep only the screenshots generated from the last failing test suite Capybara::Screenshot.prune_strategy = :keep_last_run end + +Spinach.hooks.before_run do + TestEnv.warm_asset_cache +end diff --git a/fixtures/emojis/aliases.json b/fixtures/emojis/aliases.json index 547ce7978b..d3831d8045 100644 --- a/fixtures/emojis/aliases.json +++ b/fixtures/emojis/aliases.json @@ -1,22 +1,35 @@ -{ +{ "northeast_pointing_airplane":"airplane_northeast", "small_airplane":"airplane_small", "up_pointing_small_airplane":"airplane_small_up", "up_pointing_airplane":"airplane_up", "left_anger_bubble":"anger_left", "right_anger_bubble":"anger_right", + "keycap_asterisk":"asterisk", + "atom_symbol":"atom", "ballot_box_with_ballot":"ballot_box", "ballot_box_with_bold_check":"ballot_box_check", "ballot_box_with_script_x":"ballot_box_x", "ballot_script_x":"ballot_x", + "person_with_ball":"basketball_player", + "person_with_ball_tone1":"basketball_player_tone1", + "person_with_ball_tone2":"basketball_player_tone2", + "person_with_ball_tone3":"basketball_player_tone3", + "person_with_ball_tone4":"basketball_player_tone4", + "person_with_ball_tone5":"basketball_player_tone5", "beach_with_umbrella":"beach", + "umbrella_on_ground":"beach_umbrella", "bellhop_bell":"bellhop", + "biohazard_sign":"biohazard", "bouquet_of_flowers":"bouquet2", + "archery":"bow_and_arrow", "bullhorn_with_sound_waves":"bullhorn_waves", "pocket calculator":"calculator", "spiral_calendar_pad":"calendar_spiral", "card_file_box":"card_box", "tape_cartridge":"cartridge", + "bottle_with_popping_cork":"champagne", + "cheese_wedge":"cheese", "city_sunrise":"city_sunset", "mantlepiece_clock":"clock", "clockwise_right_and_left_semicircle_arrows":"clockwise_arrows", @@ -30,6 +43,8 @@ "couple_with_heart_mm":"couple_mm", "couple_with_heart_ww":"couple_ww", "lower_left_crayon":"crayon", + "cricket_bat_ball":"cricket", + "latin_cross":"cross", "heavy_latin_cross":"cross_heavy", "white_latin_cross":"cross_white", "black_skull_and_crossbones":"crossbones", @@ -60,10 +75,13 @@ "al":"flag_al", "am":"flag_am", "ao":"flag_ao", + "aq":"flag_aq", "ar":"flag_ar", + "as":"flag_as", "at":"flag_at", "au":"flag_au", "aw":"flag_aw", + "ax":"flag_ax", "az":"flag_az", "ba":"flag_ba", "bb":"flag_bb", @@ -74,37 +92,47 @@ "bh":"flag_bh", "bi":"flag_bi", "bj":"flag_bj", + "bl":"flag_bl", "waving_black_flag":"flag_black", "bm":"flag_bm", "bn":"flag_bn", "bo":"flag_bo", + "bq":"flag_bq", "br":"flag_br", "bs":"flag_bs", "bt":"flag_bt", + "bv":"flag_bv", "bw":"flag_bw", "by":"flag_by", "bz":"flag_bz", "ca":"flag_ca", + "cc":"flag_cc", "congo":"flag_cd", "cf":"flag_cf", "cg":"flag_cg", "ch":"flag_ch", "ci":"flag_ci", + "ck":"flag_ck", "chile":"flag_cl", "cm":"flag_cm", "cn":"flag_cn", "co":"flag_co", + "cp":"flag_cp", "cr":"flag_cr", "cu":"flag_cu", "cv":"flag_cv", + "cw":"flag_cw", + "cx":"flag_cx", "cy":"flag_cy", "cz":"flag_cz", "de":"flag_de", + "dg":"flag_dg", "dj":"flag_dj", "dk":"flag_dk", "dm":"flag_dm", "do":"flag_do", "dz":"flag_dz", + "ea":"flag_ea", "ec":"flag_ec", "ee":"flag_ee", "eg":"flag_eg", @@ -112,6 +140,7 @@ "er":"flag_er", "es":"flag_es", "et":"flag_et", + "eu":"flag_eu", "fi":"flag_fi", "fj":"flag_fj", "fk":"flag_fk", @@ -122,26 +151,34 @@ "gb":"flag_gb", "gd":"flag_gd", "ge":"flag_ge", + "gf":"flag_gf", + "gg":"flag_gg", "gh":"flag_gh", "gi":"flag_gi", "gl":"flag_gl", "gm":"flag_gm", "gn":"flag_gn", + "gp":"flag_gp", "gq":"flag_gq", "gr":"flag_gr", + "gs":"flag_gs", "gt":"flag_gt", "gu":"flag_gu", "gw":"flag_gw", "gy":"flag_gy", "hk":"flag_hk", + "hm":"flag_hm", "hn":"flag_hn", "hr":"flag_hr", "ht":"flag_ht", "hu":"flag_hu", + "ic":"flag_ic", "indonesia":"flag_id", "ie":"flag_ie", "il":"flag_il", + "im":"flag_im", "in":"flag_in", + "io":"flag_io", "iq":"flag_iq", "ir":"flag_ir", "is":"flag_is", @@ -176,6 +213,7 @@ "mc":"flag_mc", "md":"flag_md", "me":"flag_me", + "mf":"flag_mf", "mg":"flag_mg", "mh":"flag_mh", "mk":"flag_mk", @@ -183,6 +221,8 @@ "mm":"flag_mm", "mn":"flag_mn", "mo":"flag_mo", + "mp":"flag_mp", + "mq":"flag_mq", "mr":"flag_mr", "ms":"flag_ms", "mt":"flag_mt", @@ -195,6 +235,7 @@ "na":"flag_na", "nc":"flag_nc", "ne":"flag_ne", + "nf":"flag_nf", "nigeria":"flag_ng", "ni":"flag_ni", "nl":"flag_nl", @@ -211,12 +252,15 @@ "ph":"flag_ph", "pk":"flag_pk", "pl":"flag_pl", + "pm":"flag_pm", + "pn":"flag_pn", "pr":"flag_pr", "ps":"flag_ps", "pt":"flag_pt", "pw":"flag_pw", "py":"flag_py", "qa":"flag_qa", + "re":"flag_re", "ro":"flag_ro", "rs":"flag_rs", "ru":"flag_ru", @@ -230,20 +274,27 @@ "sg":"flag_sg", "sh":"flag_sh", "si":"flag_si", + "sj":"flag_sj", "sk":"flag_sk", "sl":"flag_sl", "sm":"flag_sm", "sn":"flag_sn", "so":"flag_so", "sr":"flag_sr", + "ss":"flag_ss", "st":"flag_st", "sv":"flag_sv", + "sx":"flag_sx", "sy":"flag_sy", "sz":"flag_sz", + "ta":"flag_ta", + "tc":"flag_tc", "td":"flag_td", + "tf":"flag_tf", "tg":"flag_tg", "th":"flag_th", "tj":"flag_tj", + "tk":"flag_tk", "tl":"flag_tl", "turkmenistan":"flag_tm", "tn":"flag_tn", @@ -255,12 +306,14 @@ "tz":"flag_tz", "ua":"flag_ua", "ug":"flag_ug", + "um":"flag_um", "us":"flag_us", "uy":"flag_uy", "uz":"flag_uz", "va":"flag_va", "vc":"flag_vc", "ve":"flag_ve", + "vg":"flag_vg", "vi":"flag_vi", "vn":"flag_vn", "vu":"flag_vu", @@ -269,6 +322,7 @@ "ws":"flag_ws", "xk":"flag_xk", "ye":"flag_ye", + "yt":"flag_yt", "za":"flag_za", "zm":"flag_zm", "zw":"flag_zw", @@ -281,12 +335,24 @@ "frame_with_tiles":"frame_tiles", "frame_with_an_x":"frame_x", "anguished":"frowning", + "white_frowning_face":"frowning2", + "hammer_and_pick":"hammer_pick", "raised_hand_with_fingers_splayed":"hand_splayed", "reversed_raised_hand_with_fingers_splayed":"hand_splayed_reverse", + "raised_hand_with_fingers_splayed_tone1":"hand_splayed_tone1", + "raised_hand_with_fingers_splayed_tone2":"hand_splayed_tone2", + "raised_hand_with_fingers_splayed_tone3":"hand_splayed_tone3", + "raised_hand_with_fingers_splayed_tone4":"hand_splayed_tone4", + "raised_hand_with_fingers_splayed_tone5":"hand_splayed_tone5", "reversed_victory_hand":"hand_victory", + "face_with_head_bandage":"head_bandage", + "heavy_heart_exclamation_mark_ornament":"heart_exclamation", "heart_with_tip_on_the_left":"heart_tip", + "helmet_with_white_cross":"helmet_with_cross", "house_buildings":"homes", + "hot_dog":"hotdog", "derelict_house_building":"house_abandoned", + "hugging_face":"hugging", "circled_information_source":"info", "desert_island":"island", "up_pointing_military_airplane":"jet_up", @@ -300,16 +366,36 @@ "left_hand_telephone_receiver":"left_receiver", "man_in_business_suit_levitating":"levitate", "weight_lifter":"lifter", + "weight_lifter_tone1":"lifter_tone1", + "weight_lifter_tone2":"lifter_tone2", + "weight_lifter_tone3":"lifter_tone3", + "weight_lifter_tone4":"lifter_tone4", + "weight_lifter_tone5":"lifter_tone5", "light_mark":"light_check_mark", + "lion":"lion_face", "world_map":"map", "sports_medal":"medal", + "sign_of_the_horns":"metal", + "sign_of_the_horns_tone1":"metal_tone1", + "sign_of_the_horns_tone2":"metal_tone2", + "sign_of_the_horns_tone3":"metal_tone3", + "sign_of_the_horns_tone4":"metal_tone4", + "sign_of_the_horns_tone5":"metal_tone5", "studio_microphone":"microphone2", "reversed_hand_with_middle_finger_extended":"middle_finger", + "reversed_hand_with_middle_finger_extended_tone1":"middle_finger_tone1", + "reversed_hand_with_middle_finger_extended_tone2":"middle_finger_tone2", + "reversed_hand_with_middle_finger_extended_tone3":"middle_finger_tone3", + "reversed_hand_with_middle_finger_extended_tone4":"middle_finger_tone4", + "reversed_hand_with_middle_finger_extended_tone5":"middle_finger_tone5", + "money_mouth_face":"money_mouth", "lightning_mood_bubble":"mood_bubble_lightning", "lightning_mood":"mood_lightning", "racing_motorcycle":"motorcycle", "snow_capped_mountain":"mountain_snow", "one_button_mouse":"mouse_one", + "three_button_mouse":"mouse_three_button", + "nerd_face":"nerd", "three_networked_computers":"network", "rolled_up_newspaper":"newspaper2", "note_page":"note", @@ -319,27 +405,40 @@ "spiral_note_pad":"notepad_spiral", "oil_drum":"oil", "grandma":"older_woman", + "grandma_tone1":"older_woman_tone1", + "grandma_tone2":"older_woman_tone2", + "grandma_tone3":"older_woman_tone3", + "grandma_tone4":"older_woman_tone4", + "grandma_tone5":"older_woman_tone5", "optical_disc_icon":"optical_disk", "lower_left_paintbrush":"paintbrush", "linked_paperclips":"paperclips", "national_park":"park", + "double_vertical_bar":"pause_button", + "peace_symbol":"peace", "lower_left_ballpoint_pen":"pen_ballpoint", "lower_left_fountain_pen":"pen_fountain", "memo":"pencil", "lower_left_pencil":"pencil3", "black_pennant":"pennant_black", "white_pennant":"pennant_white", + "table_tennis":"ping_pong", "no_piracy":"piracy", + "worship_symbol":"place_of_worship", "shit":"poop", "hankey":"poop", "poo":"poop", "prohibited_sign":"prohibited", "film_projector":"projector", "racing_car":"race_car", + "radioactive_sign":"radioactive", "railroad_track":"railway_track", "right_speaker_with_one_sound_wave":"right_speaker_one", "right_speaker_with_three_sound_waves":"right_speaker_three", + "robot_face":"robot", + "face_with_rolling_eyes":"rolling_eyes", "skeleton":"skull", + "skull_and_crossbones":"skull_crossbones", "slightly_frowning_face":"slight_frown", "slightly_smiling_face":"slight_smile", "speaking_head_in_silhouette":"speaking_head", @@ -348,20 +447,53 @@ "three_speech_bubbles":"speech_three", "two_speech_bubbles":"speech_two", "sleuth_or_spy":"spy", + "sleuth_or_spy_tone1":"spy_tone1", + "sleuth_or_spy_tone2":"spy_tone2", + "sleuth_or_spy_tone3":"spy_tone3", + "sleuth_or_spy_tone4":"spy_tone4", + "sleuth_or_spy_tone5":"spy_tone5", "portable_stereo":"stereo", "black_touchtone_telephone":"telephone_black", "white_touchtone_telephone":"telephone_white", + "face_with_thermometer":"thermometer_face", + "thinking_face":"thinking", "left_thought_bubble":"thought_left", "right_thought_bubble":"thought_right", "reversed_thumbs_down_sign":"thumbs_down_reverse", "reversed_thumbs_up_sign":"thumbs_up_reverse", "-1":"thumbsdown", + "-1_tone1":"thumbsdown_tone1", + "-1_tone2":"thumbsdown_tone2", + "-1_tone3":"thumbsdown_tone3", + "-1_tone4":"thumbsdown_tone4", + "-1_tone5":"thumbsdown_tone5", "+1":"thumbsup", + "+1_tone1":"thumbsup_tone1", + "+1_tone2":"thumbsup_tone2", + "+1_tone3":"thumbsup_tone3", + "+1_tone4":"thumbsup_tone4", + "+1_tone5":"thumbsup_tone5", + "thunder_cloud_and_rain":"thunder_cloud_rain", "admission_tickets":"tickets", + "timer_clock":"timer", "hammer_and_wrench":"tools", + "next_track":"track_next", + "previous_track":"track_previous", "diesel_locomotive":"train_diesel", "triangle_with_rounded_corners":"triangle_round", "turned_ok_hand_sign":"turned_ok_hand", + "unicorn_face":"unicorn", + "upside_down_face":"upside_down", + "funeral_urn":"urn", "raised_hand_with_part_between_middle_and_ring_fingers":"vulcan", - "left_writing_hand":"writing_hand" -} \ No newline at end of file + "raised_hand_with_part_between_middle_and_ring_fingers_tone1":"vulcan_tone1", + "raised_hand_with_part_between_middle_and_ring_fingers_tone2":"vulcan_tone2", + "raised_hand_with_part_between_middle_and_ring_fingers_tone3":"vulcan_tone3", + "raised_hand_with_part_between_middle_and_ring_fingers_tone4":"vulcan_tone4", + "raised_hand_with_part_between_middle_and_ring_fingers_tone5":"vulcan_tone5", + "white_sun_behind_cloud":"white_sun_cloud", + "white_sun_behind_cloud_with_rain":"white_sun_rain_cloud", + "white_sun_with_small_cloud":"white_sun_small_cloud", + "left_writing_hand":"writing_hand", + "zipper_mouth_face":"zipper_mouth" +} diff --git a/fixtures/emojis/generate_aliases.rb b/fixtures/emojis/generate_aliases.rb new file mode 100755 index 0000000000..8838fb9a3a --- /dev/null +++ b/fixtures/emojis/generate_aliases.rb @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +require 'json' + +aliases = {} + +index_file = File.expand_path("./index.json") +index = JSON.parse(File.read(index_file)) + +index.each_pair do |key, data| + data['aliases'].each do |a| + a.tr!(':', '') + + aliases[a] = key + end +end + +puts JSON.pretty_generate(aliases, indent: ' ', space: '', space_before: '') diff --git a/fixtures/emojis/index.json b/fixtures/emojis/index.json index 60ef2399e1..7f204c1a8e 100644 --- a/fixtures/emojis/index.json +++ b/fixtures/emojis/index.json @@ -7,7 +7,21 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["numbers", "perfect", "score", "100", "percent", "a", "plus", "perfect", "school", "quiz", "score", "test", "exam"], + "keywords": [ + "numbers", + "perfect", + "score", + "100", + "percent", + "a", + "plus", + "perfect", + "school", + "quiz", + "score", + "test", + "exam" + ], "moji": "💯" }, "1234": { @@ -18,7 +32,10 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square", "numbers"], + "keywords": [ + "blue-square", + "numbers" + ], "moji": "🔢" }, "8ball": { @@ -29,7 +46,14 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["pool", "billiards", "eight ball", "pool", "pocket ball", "cue"], + "keywords": [ + "pool", + "billiards", + "eight ball", + "pool", + "pocket ball", + "cue" + ], "moji": "🎱" }, "a": { @@ -40,7 +64,11 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["alphabet", "letter", "red-square"], + "keywords": [ + "alphabet", + "letter", + "red-square" + ], "moji": "🅰" }, "ab": { @@ -51,7 +79,10 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["alphabet", "red-square"], + "keywords": [ + "alphabet", + "red-square" + ], "moji": "🆎" }, "abc": { @@ -62,7 +93,10 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["alphabet", "blue-square"], + "keywords": [ + "alphabet", + "blue-square" + ], "moji": "🔤" }, "abcd": { @@ -73,7 +107,10 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["alphabet", "blue-square"], + "keywords": [ + "alphabet", + "blue-square" + ], "moji": "🔡" }, "accept": { @@ -84,7 +121,14 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["agree", "chinese", "good", "kanji", "ok", "yes"], + "keywords": [ + "agree", + "chinese", + "good", + "kanji", + "ok", + "yes" + ], "moji": "🉑" }, "aerial_tramway": { @@ -95,18 +139,42 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["transportation", "vehicle", "aerial", "tram", "tramway", "cable", "transport"], + "keywords": [ + "transportation", + "vehicle", + "aerial", + "tram", + "tramway", + "cable", + "transport" + ], "moji": "🚡" }, "airplane": { "unicode": "2708", - "unicode_alternates": ["2708-FE0F"], + "unicode_alternates": [ + "2708-FE0F" + ], "name": "airplane", "shortname": ":airplane:", "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["flight", "transportation", "vehicle", "airplane", "plane", "airport", "travel", "airlines", "fly", "jet", "jumbo", "boeing", "airbus"], + "keywords": [ + "flight", + "transportation", + "vehicle", + "airplane", + "plane", + "airport", + "travel", + "airlines", + "fly", + "jet", + "jumbo", + "boeing", + "airbus" + ], "moji": "✈" }, "airplane_arriving": { @@ -117,7 +185,20 @@ "category": "travel_places", "aliases": [], "aliases_ascii": [], - "keywords": ["flight", "transportation", "vehicle", "plane", "airport", "travel", "airlines", "fly", "jet", "jumbo", "boeing", "airbus"] + "keywords": [ + "flight", + "transportation", + "vehicle", + "plane", + "airport", + "travel", + "airlines", + "fly", + "jet", + "jumbo", + "boeing", + "airbus" + ] }, "airplane_departure": { "unicode": "1F6EB", @@ -127,7 +208,21 @@ "category": "travel_places", "aliases": [], "aliases_ascii": [], - "keywords": ["flight", "transportation", "vehicle", "plane", "airport", "travel", "airlines", "fly", "jet", "jumbo", "boeing", "airbus", "leaving"] + "keywords": [ + "flight", + "transportation", + "vehicle", + "plane", + "airport", + "travel", + "airlines", + "fly", + "jet", + "jumbo", + "boeing", + "airbus", + "leaving" + ] }, "airplane_northeast": { "unicode": "1F6EA", @@ -135,9 +230,14 @@ "name": "northeast-pointing airplane", "shortname": ":airplane_northeast:", "category": "travel_places", - "aliases": [":northeast_pointing_airplane:"], + "aliases": [ + ":northeast_pointing_airplane:" + ], "aliases_ascii": [], - "keywords": ["plane", "travel"] + "keywords": [ + "plane", + "travel" + ] }, "airplane_small": { "unicode": "1F6E9", @@ -145,9 +245,24 @@ "name": "small airplane", "shortname": ":airplane_small:", "category": "travel_places", - "aliases": [":small_airplane:"], + "aliases": [ + ":small_airplane:" + ], "aliases_ascii": [], - "keywords": ["flight", "transportation", "vehicle", "plane", "airport", "travel", "airlines", "fly", "jet", "jumbo", "boeing", "airbus"] + "keywords": [ + "flight", + "transportation", + "vehicle", + "plane", + "airport", + "travel", + "airlines", + "fly", + "jet", + "jumbo", + "boeing", + "airbus" + ] }, "airplane_small_up": { "unicode": "1F6E8", @@ -155,9 +270,14 @@ "name": "up-pointing small airplane", "shortname": ":airplane_small_up:", "category": "travel_places", - "aliases": [":up_pointing_small_airplane:"], + "aliases": [ + ":up_pointing_small_airplane:" + ], "aliases_ascii": [], - "keywords": ["plane", "travel"] + "keywords": [ + "plane", + "travel" + ] }, "airplane_up": { "unicode": "1F6E7", @@ -165,9 +285,14 @@ "name": "up-pointing airplane", "shortname": ":airplane_up:", "category": "travel_places", - "aliases": [":up_pointing_airplane:"], + "aliases": [ + ":up_pointing_airplane:" + ], "aliases_ascii": [], - "keywords": ["plane", "travel"] + "keywords": [ + "plane", + "travel" + ] }, "alarm_clock": { "unicode": "23F0", @@ -177,9 +302,26 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["time", "wake"], + "keywords": [ + "time", + "wake" + ], "moji": "⏰" }, + "alembic": { + "unicode": "2697", + "unicode_alternates": "", + "name": "alembic", + "shortname": ":alembic:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "chemistry", + "object", + "tool" + ] + }, "alien": { "unicode": "1F47D", "unicode_alternates": [], @@ -188,7 +330,12 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["UFO", "paul", "alien", "ufo"], + "keywords": [ + "UFO", + "paul", + "alien", + "ufo" + ], "moji": "👽" }, "ambulance": { @@ -199,18 +346,50 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["911", "health", "ambulance", "emergency", "medical", "help", "assistance"], + "keywords": [ + "911", + "health", + "ambulance", + "emergency", + "medical", + "help", + "assistance" + ], "moji": "🚑" }, + "amphora": { + "unicode": "1F3FA", + "unicode_alternates": "", + "name": "amphora", + "shortname": ":amphora:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": [] + }, "anchor": { "unicode": "2693", - "unicode_alternates": ["2693-FE0F"], + "unicode_alternates": [ + "2693-FE0F" + ], "name": "anchor", "shortname": ":anchor:", "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["ferry", "ship", "anchor", "ship", "boat", "ocean", "harbor", "marina", "shipyard", "sailor", "tattoo"], + "keywords": [ + "ferry", + "ship", + "anchor", + "ship", + "boat", + "ocean", + "harbor", + "marina", + "shipyard", + "sailor", + "tattoo" + ], "moji": "⚓" }, "angel": { @@ -221,9 +400,99 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["baby", "angel", "halo", "cupid", "wings", "halo", "heaven", "wings", "jesus"], + "keywords": [ + "baby", + "angel", + "halo", + "cupid", + "wings", + "halo", + "heaven", + "wings", + "jesus" + ], "moji": "👼" }, + "angel_tone1": { + "unicode": "1F47C-1F3FB", + "unicode_alternates": "", + "name": "baby angel tone 1", + "shortname": ":angel_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "halo", + "cupid", + "heaven", + "wings", + "jesus" + ] + }, + "angel_tone2": { + "unicode": "1F47C-1F3FC", + "unicode_alternates": "", + "name": "baby angel tone 2", + "shortname": ":angel_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "halo", + "cupid", + "heaven", + "wings", + "jesus" + ] + }, + "angel_tone3": { + "unicode": "1F47C-1F3FD", + "unicode_alternates": "", + "name": "baby angel tone 3", + "shortname": ":angel_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "halo", + "cupid", + "heaven", + "wings", + "jesus" + ] + }, + "angel_tone4": { + "unicode": "1F47C-1F3FE", + "unicode_alternates": "", + "name": "baby angel tone 4", + "shortname": ":angel_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "halo", + "cupid", + "heaven", + "wings", + "jesus" + ] + }, + "angel_tone5": { + "unicode": "1F47C-1F3FF", + "unicode_alternates": "", + "name": "baby angel tone 5", + "shortname": ":angel_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "halo", + "cupid", + "heaven", + "wings", + "jesus" + ] + }, "anger": { "unicode": "1F4A2", "unicode_alternates": [], @@ -232,7 +501,11 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["anger", "angry", "mad"], + "keywords": [ + "anger", + "angry", + "mad" + ], "moji": "💢" }, "anger_left": { @@ -241,9 +514,20 @@ "name": "left anger bubble", "shortname": ":anger_left:", "category": "objects_symbols", - "aliases": [":left_anger_bubble:"], + "aliases": [ + ":left_anger_bubble:" + ], "aliases_ascii": [], - "keywords": ["speech", "balloon", "talk", "mood", "conversation", "communication", "comic", "angry"] + "keywords": [ + "speech", + "balloon", + "talk", + "mood", + "conversation", + "communication", + "comic", + "angry" + ] }, "anger_right": { "unicode": "1F5EF", @@ -251,9 +535,20 @@ "name": "right anger bubble", "shortname": ":anger_right:", "category": "objects_symbols", - "aliases": [":right_anger_bubble:"], + "aliases": [ + ":right_anger_bubble:" + ], "aliases_ascii": [], - "keywords": ["speech", "balloon", "talk", "mood", "conversation", "communication", "comic", "angry"] + "keywords": [ + "speech", + "balloon", + "talk", + "mood", + "conversation", + "communication", + "comic", + "angry" + ] }, "angry": { "unicode": "1F620", @@ -262,8 +557,22 @@ "shortname": ":angry:", "category": "emoticons", "aliases": [], - "aliases_ascii": [">:(", ">:-(", ":@"], - "keywords": ["angry", "livid", "mad", "vexed", "irritated", "annoyed", "face", "frustrated", "mad"], + "aliases_ascii": [ + ">:(", + ">:-(", + ":@" + ], + "keywords": [ + "angry", + "livid", + "mad", + "vexed", + "irritated", + "annoyed", + "face", + "frustrated", + "mad" + ], "moji": "😠" }, "anguished": { @@ -274,7 +583,17 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["face", "nervous", "stunned", "pain", "anguish", "ouch", "misery", "distress", "grief"], + "keywords": [ + "face", + "nervous", + "stunned", + "pain", + "anguish", + "ouch", + "misery", + "distress", + "grief" + ], "moji": "😧" }, "ant": { @@ -285,7 +604,14 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "insect", "ant", "queen", "insect", "team"], + "keywords": [ + "animal", + "insect", + "ant", + "queen", + "insect", + "team" + ], "moji": "🐜" }, "apple": { @@ -296,40 +622,87 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["fruit", "mac", "apple", "fruit", "electronics", "red", "doctor", "teacher", "school", "core"], + "keywords": [ + "fruit", + "mac", + "apple", + "fruit", + "electronics", + "red", + "doctor", + "teacher", + "school", + "core" + ], "moji": "🍎" }, "aquarius": { "unicode": "2652", - "unicode_alternates": ["2652-FE0F"], + "unicode_alternates": [ + "2652-FE0F" + ], "name": "aquarius", "shortname": ":aquarius:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["aquarius", "water", "bearer", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "purple-square", "sign", "zodiac", "horoscope"], + "keywords": [ + "aquarius", + "water", + "bearer", + "astrology", + "greek", + "constellation", + "stars", + "zodiac", + "sign", + "purple-square", + "sign", + "zodiac", + "horoscope" + ], "moji": "♒" }, "aries": { "unicode": "2648", - "unicode_alternates": ["2648-FE0F"], + "unicode_alternates": [ + "2648-FE0F" + ], "name": "aries", "shortname": ":aries:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["aries", "ram", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "purple-square", "sign", "zodiac", "horoscope"], + "keywords": [ + "aries", + "ram", + "astrology", + "greek", + "constellation", + "stars", + "zodiac", + "sign", + "purple-square", + "sign", + "zodiac", + "horoscope" + ], "moji": "♈" }, "arrow_backward": { "unicode": "25C0", - "unicode_alternates": ["25C0-FE0F"], + "unicode_alternates": [ + "25C0-FE0F" + ], "name": "black left-pointing triangle", "shortname": ":arrow_backward:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["arrow", "blue-square"], + "keywords": [ + "arrow", + "blue-square" + ], "moji": "◀" }, "arrow_double_down": { @@ -340,7 +713,10 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["arrow", "blue-square"], + "keywords": [ + "arrow", + "blue-square" + ], "moji": "⏬" }, "arrow_double_up": { @@ -351,18 +727,26 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["arrow", "blue-square"], + "keywords": [ + "arrow", + "blue-square" + ], "moji": "⏫" }, "arrow_down": { "unicode": "2B07", - "unicode_alternates": ["2B07-FE0F"], + "unicode_alternates": [ + "2B07-FE0F" + ], "name": "downwards black arrow", "shortname": ":arrow_down:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["arrow", "blue-square"], + "keywords": [ + "arrow", + "blue-square" + ], "moji": "⬇" }, "arrow_down_small": { @@ -373,117 +757,168 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["arrow", "blue-square"], + "keywords": [ + "arrow", + "blue-square" + ], "moji": "🔽" }, "arrow_forward": { "unicode": "25B6", - "unicode_alternates": ["25B6-FE0F"], + "unicode_alternates": [ + "25B6-FE0F" + ], "name": "black right-pointing triangle", "shortname": ":arrow_forward:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["arrow", "blue-square"], + "keywords": [ + "arrow", + "blue-square" + ], "moji": "▶" }, "arrow_heading_down": { "unicode": "2935", - "unicode_alternates": ["2935-FE0F"], + "unicode_alternates": [ + "2935-FE0F" + ], "name": "arrow pointing rightwards then curving downwards", "shortname": ":arrow_heading_down:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["arrow", "blue-square"], + "keywords": [ + "arrow", + "blue-square" + ], "moji": "⤵" }, "arrow_heading_up": { "unicode": "2934", - "unicode_alternates": ["2934-FE0F"], + "unicode_alternates": [ + "2934-FE0F" + ], "name": "arrow pointing rightwards then curving upwards", "shortname": ":arrow_heading_up:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["arrow", "blue-square"], + "keywords": [ + "arrow", + "blue-square" + ], "moji": "⤴" }, "arrow_left": { "unicode": "2B05", - "unicode_alternates": ["2B05-FE0F"], + "unicode_alternates": [ + "2B05-FE0F" + ], "name": "leftwards black arrow", "shortname": ":arrow_left:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["arrow", "blue-square", "previous"], + "keywords": [ + "arrow", + "blue-square", + "previous" + ], "moji": "⬅" }, "arrow_lower_left": { "unicode": "2199", - "unicode_alternates": ["2199-FE0F"], + "unicode_alternates": [ + "2199-FE0F" + ], "name": "south west arrow", "shortname": ":arrow_lower_left:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["arrow", "blue-square"], + "keywords": [ + "arrow", + "blue-square" + ], "moji": "↙" }, "arrow_lower_right": { "unicode": "2198", - "unicode_alternates": ["2198-FE0F"], + "unicode_alternates": [ + "2198-FE0F" + ], "name": "south east arrow", "shortname": ":arrow_lower_right:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["arrow", "blue-square"], + "keywords": [ + "arrow", + "blue-square" + ], "moji": "↘" }, "arrow_right": { "unicode": "27A1", - "unicode_alternates": ["27A1-FE0F"], + "unicode_alternates": [ + "27A1-FE0F" + ], "name": "black rightwards arrow", "shortname": ":arrow_right:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square", "next"], + "keywords": [ + "blue-square", + "next" + ], "moji": "➡" }, "arrow_right_hook": { "unicode": "21AA", - "unicode_alternates": ["21AA-FE0F"], + "unicode_alternates": [ + "21AA-FE0F" + ], "name": "rightwards arrow with hook", "shortname": ":arrow_right_hook:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square"], + "keywords": [ + "blue-square" + ], "moji": "↪" }, "arrow_up": { "unicode": "2B06", - "unicode_alternates": ["2B06-FE0F"], + "unicode_alternates": [ + "2B06-FE0F" + ], "name": "upwards black arrow", "shortname": ":arrow_up:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square"], + "keywords": [ + "blue-square" + ], "moji": "⬆" }, "arrow_up_down": { "unicode": "2195", - "unicode_alternates": ["2195-FE0F"], + "unicode_alternates": [ + "2195-FE0F" + ], "name": "up down arrow", "shortname": ":arrow_up_down:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square"], + "keywords": [ + "blue-square" + ], "moji": "↕" }, "arrow_up_small": { @@ -494,29 +929,39 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square"], + "keywords": [ + "blue-square" + ], "moji": "🔼" }, "arrow_upper_left": { "unicode": "2196", - "unicode_alternates": ["2196-FE0F"], + "unicode_alternates": [ + "2196-FE0F" + ], "name": "north west arrow", "shortname": ":arrow_upper_left:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square"], + "keywords": [ + "blue-square" + ], "moji": "↖" }, "arrow_upper_right": { "unicode": "2197", - "unicode_alternates": ["2197-FE0F"], + "unicode_alternates": [ + "2197-FE0F" + ], "name": "north east arrow", "shortname": ":arrow_upper_right:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square"], + "keywords": [ + "blue-square" + ], "moji": "↗" }, "arrows_clockwise": { @@ -527,7 +972,9 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["sync"], + "keywords": [ + "sync" + ], "moji": "🔃" }, "arrows_counterclockwise": { @@ -538,7 +985,10 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square", "sync"], + "keywords": [ + "blue-square", + "sync" + ], "moji": "🔄" }, "art": { @@ -549,7 +999,20 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["design", "draw", "paint", "artist", "palette", "art", "colors", "paint", "draw", "brush", "pastels", "oils"], + "keywords": [ + "design", + "draw", + "paint", + "artist", + "palette", + "art", + "colors", + "paint", + "draw", + "brush", + "pastels", + "oils" + ], "moji": "🎨" }, "articulated_lorry": { @@ -560,7 +1023,16 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["cars", "transportation", "vehicle", "truck", "delivery", "semi", "lorry", "articulated"], + "keywords": [ + "cars", + "transportation", + "vehicle", + "truck", + "delivery", + "semi", + "lorry", + "articulated" + ], "moji": "🚛" }, "ascending_notes": { @@ -571,7 +1043,28 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["score", "music", "sound", "tone"] + "keywords": [ + "score", + "music", + "sound", + "tone" + ] + }, + "asterisk": { + "unicode": "002A-20E3", + "unicode_alternates": "002a-fe0f-20e3", + "name": "keycap asterisk", + "shortname": ":asterisk:", + "category": "symbols", + "aliases": [ + ":keycap_asterisk:" + ], + "aliases_ascii": [], + "keywords": [ + "*", + "star", + "symbol" + ] }, "astonished": { "unicode": "1F632", @@ -581,7 +1074,13 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["face", "xox", "shocked", "surprise", "astonished"], + "keywords": [ + "face", + "xox", + "shocked", + "surprise", + "astonished" + ], "moji": "😲" }, "athletic_shoe": { @@ -592,7 +1091,10 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["shoes", "sports"], + "keywords": [ + "shoes", + "sports" + ], "moji": "👟" }, "atm": { @@ -603,9 +1105,38 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["atm", "cash", "withdrawal", "money", "deposit", "financial", "bank", "adam", "payday", "bank", "blue-square", "cash", "money", "payment"], + "keywords": [ + "atm", + "cash", + "withdrawal", + "money", + "deposit", + "financial", + "bank", + "adam", + "payday", + "bank", + "blue-square", + "cash", + "money", + "payment" + ], "moji": "🏧" }, + "atom": { + "unicode": "269B", + "unicode_alternates": "", + "name": "atom symbol", + "shortname": ":atom:", + "category": "symbols", + "aliases": [ + ":atom_symbol:" + ], + "aliases_ascii": [], + "keywords": [ + "atheist" + ] + }, "b": { "unicode": "1F171", "unicode_alternates": [], @@ -614,7 +1145,11 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["alphabet", "letter", "red-square"], + "keywords": [ + "alphabet", + "letter", + "red-square" + ], "moji": "🅱" }, "baby": { @@ -625,7 +1160,11 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["boy", "child", "infant"], + "keywords": [ + "boy", + "child", + "infant" + ], "moji": "👶" }, "baby_bottle": { @@ -636,7 +1175,17 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["container", "food", "baby", "bottle", "milk", "mother", "nipple", "newborn", "formula"], + "keywords": [ + "container", + "food", + "baby", + "bottle", + "milk", + "mother", + "nipple", + "newborn", + "formula" + ], "moji": "🍼" }, "baby_chick": { @@ -647,7 +1196,17 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "chicken", "chick", "baby", "bird", "chicken", "young", "woman", "cute"], + "keywords": [ + "animal", + "chicken", + "chick", + "baby", + "bird", + "chicken", + "young", + "woman", + "cute" + ], "moji": "🐤" }, "baby_symbol": { @@ -658,9 +1217,89 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["child", "orange-square", "baby", "crawl", "newborn", "human", "diaper", "small", "babe"], + "keywords": [ + "child", + "orange-square", + "baby", + "crawl", + "newborn", + "human", + "diaper", + "small", + "babe" + ], "moji": "🚼" }, + "baby_tone1": { + "unicode": "1F476-1F3FB", + "unicode_alternates": "", + "name": "baby tone 1", + "shortname": ":baby_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "child", + "infant", + "toddler" + ] + }, + "baby_tone2": { + "unicode": "1F476-1F3FC", + "unicode_alternates": "", + "name": "baby tone 2", + "shortname": ":baby_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "child", + "infant", + "toddler" + ] + }, + "baby_tone3": { + "unicode": "1F476-1F3FD", + "unicode_alternates": "", + "name": "baby tone 3", + "shortname": ":baby_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "child", + "infant", + "toddler" + ] + }, + "baby_tone4": { + "unicode": "1F476-1F3FE", + "unicode_alternates": "", + "name": "baby tone 4", + "shortname": ":baby_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "child", + "infant", + "toddler" + ] + }, + "baby_tone5": { + "unicode": "1F476-1F3FF", + "unicode_alternates": "", + "name": "baby tone 5", + "shortname": ":baby_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "child", + "infant", + "toddler" + ] + }, "back": { "unicode": "1F519", "unicode_alternates": [], @@ -669,9 +1308,21 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["arrow"], + "keywords": [ + "arrow" + ], "moji": "🔙" }, + "badminton": { + "unicode": "1F3F8", + "unicode_alternates": "", + "name": "badminton racquet", + "shortname": ":badminton:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [] + }, "baggage_claim": { "unicode": "1F6C4", "unicode_alternates": [], @@ -680,7 +1331,15 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["airport", "blue-square", "transport", "bag", "baggage", "luggage", "travel"], + "keywords": [ + "airport", + "blue-square", + "transport", + "bag", + "baggage", + "luggage", + "travel" + ], "moji": "🛄" }, "balloon": { @@ -691,7 +1350,17 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["celebration", "party", "balloon", "birthday", "celebration", "helium", "gas", "children", "float"], + "keywords": [ + "celebration", + "party", + "balloon", + "birthday", + "celebration", + "helium", + "gas", + "children", + "float" + ], "moji": "🎈" }, "ballot_box": { @@ -700,9 +1369,13 @@ "name": "ballot box with ballot", "shortname": ":ballot_box:", "category": "objects_symbols", - "aliases": [":ballot_box_with_ballot:"], + "aliases": [ + ":ballot_box_with_ballot:" + ], "aliases_ascii": [], - "keywords": ["vote"] + "keywords": [ + "vote" + ] }, "ballot_box_check": { "unicode": "1F5F9", @@ -710,19 +1383,29 @@ "name": "ballot box with bold check", "shortname": ":ballot_box_check:", "category": "objects_symbols", - "aliases": [":ballot_box_with_bold_check:"], + "aliases": [ + ":ballot_box_with_bold_check:" + ], "aliases_ascii": [], - "keywords": ["mark", "vote"] + "keywords": [ + "mark", + "vote" + ] }, "ballot_box_with_check": { "unicode": "2611", - "unicode_alternates": ["2611-FE0F"], + "unicode_alternates": [ + "2611-FE0F" + ], "name": "ballot box with check", "shortname": ":ballot_box_with_check:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["agree", "ok"], + "keywords": [ + "agree", + "ok" + ], "moji": "☑" }, "ballot_box_x": { @@ -731,9 +1414,14 @@ "name": "ballot box with script x", "shortname": ":ballot_box_x:", "category": "objects_symbols", - "aliases": [":ballot_box_with_script_x:"], + "aliases": [ + ":ballot_box_with_script_x:" + ], "aliases_ascii": [], - "keywords": ["mark", "vote"] + "keywords": [ + "mark", + "vote" + ] }, "ballot_x": { "unicode": "1F5F4", @@ -741,9 +1429,14 @@ "name": "ballot script x", "shortname": ":ballot_x:", "category": "objects_symbols", - "aliases": [":ballot_script_x:"], + "aliases": [ + ":ballot_script_x:" + ], "aliases_ascii": [], - "keywords": ["mark", "vote"] + "keywords": [ + "mark", + "vote" + ] }, "bamboo": { "unicode": "1F38D", @@ -753,7 +1446,25 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["nature", "plant", "vegetable", "pine", "bamboo", "decoration", "new", "years", "spirits", "harvest", "prosperity", "longevity", "fortune", "luck", "welcome", "farming", "agriculture"], + "keywords": [ + "nature", + "plant", + "vegetable", + "pine", + "bamboo", + "decoration", + "new", + "years", + "spirits", + "harvest", + "prosperity", + "longevity", + "fortune", + "luck", + "welcome", + "farming", + "agriculture" + ], "moji": "🎍" }, "banana": { @@ -764,18 +1475,29 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "fruit", "banana", "peel", "bunch"], + "keywords": [ + "food", + "fruit", + "banana", + "peel", + "bunch" + ], "moji": "🍌" }, "bangbang": { "unicode": "203C", - "unicode_alternates": ["203C-FE0F"], + "unicode_alternates": [ + "203C-FE0F" + ], "name": "double exclamation mark", "shortname": ":bangbang:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["exclamation", "surprise"], + "keywords": [ + "exclamation", + "surprise" + ], "moji": "‼" }, "bank": { @@ -786,7 +1508,9 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["building"], + "keywords": [ + "building" + ], "moji": "🏦" }, "bar_chart": { @@ -797,7 +1521,11 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["graph", "presentation", "stats"], + "keywords": [ + "graph", + "presentation", + "stats" + ], "moji": "📊" }, "barber": { @@ -808,18 +1536,28 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["hair", "salon", "style"], + "keywords": [ + "hair", + "salon", + "style" + ], "moji": "💈" }, "baseball": { "unicode": "26BE", - "unicode_alternates": ["26BE-FE0F"], + "unicode_alternates": [ + "26BE-FE0F" + ], "name": "baseball", "shortname": ":baseball:", "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["MLB", "balls", "sports"], + "keywords": [ + "MLB", + "balls", + "sports" + ], "moji": "⚾" }, "basketball": { @@ -830,9 +1568,95 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["NBA", "balls", "sports", "basketball", "bball", "dribble", "hoop", "net", "swish", "rip city"], + "keywords": [ + "NBA", + "balls", + "sports", + "basketball", + "bball", + "dribble", + "hoop", + "net", + "swish", + "rip city" + ], "moji": "🏀" }, + "basketball_player": { + "unicode": "26F9", + "unicode_alternates": "", + "name": "person with ball", + "shortname": ":basketball_player:", + "category": "activity", + "aliases": [ + ":person_with_ball:" + ], + "aliases_ascii": [], + "keywords": [ + "sport", + "travel" + ] + }, + "basketball_player_tone1": { + "unicode": "26F9-1F3FB", + "unicode_alternates": "", + "name": "person with ball tone 1", + "shortname": ":basketball_player_tone1:", + "category": "activity", + "aliases": [ + ":person_with_ball_tone1:" + ], + "aliases_ascii": [], + "keywords": [] + }, + "basketball_player_tone2": { + "unicode": "26F9-1F3FC", + "unicode_alternates": "", + "name": "person with ball tone 2", + "shortname": ":basketball_player_tone2:", + "category": "activity", + "aliases": [ + ":person_with_ball_tone2:" + ], + "aliases_ascii": [], + "keywords": [] + }, + "basketball_player_tone3": { + "unicode": "26F9-1F3FD", + "unicode_alternates": "", + "name": "person with ball tone 3", + "shortname": ":basketball_player_tone3:", + "category": "activity", + "aliases": [ + ":person_with_ball_tone3:" + ], + "aliases_ascii": [], + "keywords": [] + }, + "basketball_player_tone4": { + "unicode": "26F9-1F3FE", + "unicode_alternates": "", + "name": "person with ball tone 4", + "shortname": ":basketball_player_tone4:", + "category": "activity", + "aliases": [ + ":person_with_ball_tone4:" + ], + "aliases_ascii": [], + "keywords": [] + }, + "basketball_player_tone5": { + "unicode": "26F9-1F3FF", + "unicode_alternates": "", + "name": "person with ball tone 5", + "shortname": ":basketball_player_tone5:", + "category": "activity", + "aliases": [ + ":person_with_ball_tone5:" + ], + "aliases_ascii": [], + "keywords": [] + }, "bath": { "unicode": "1F6C0", "unicode_alternates": [], @@ -841,9 +1665,140 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["clean", "shower", "bath", "tub", "basin", "wash", "bubble", "soak", "bathroom", "soap", "water", "clean", "shampoo", "lather", "water"], + "keywords": [ + "clean", + "shower", + "bath", + "tub", + "basin", + "wash", + "bubble", + "soak", + "bathroom", + "soap", + "water", + "clean", + "shampoo", + "lather", + "water" + ], "moji": "🛀" }, + "bath_tone1": { + "unicode": "1F6C0-1F3FB", + "unicode_alternates": "", + "name": "bath tone 1", + "shortname": ":bath_tone1:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "shower", + "tub", + "basin", + "wash", + "bubble", + "soak", + "bathroom", + "soap", + "water", + "clean", + "shampoo", + "lather" + ] + }, + "bath_tone2": { + "unicode": "1F6C0-1F3FC", + "unicode_alternates": "", + "name": "bath tone 2", + "shortname": ":bath_tone2:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "shower", + "tub", + "basin", + "wash", + "bubble", + "soak", + "bathroom", + "soap", + "water", + "clean", + "shampoo", + "lather" + ] + }, + "bath_tone3": { + "unicode": "1F6C0-1F3FD", + "unicode_alternates": "", + "name": "bath tone 3", + "shortname": ":bath_tone3:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "shower", + "tub", + "basin", + "wash", + "bubble", + "soak", + "bathroom", + "soap", + "water", + "clean", + "shampoo", + "lather" + ] + }, + "bath_tone4": { + "unicode": "1F6C0-1F3FE", + "unicode_alternates": "", + "name": "bath tone 4", + "shortname": ":bath_tone4:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "shower", + "tub", + "basin", + "wash", + "bubble", + "soak", + "bathroom", + "soap", + "water", + "clean", + "shampoo", + "lather" + ] + }, + "bath_tone5": { + "unicode": "1F6C0-1F3FF", + "unicode_alternates": "", + "name": "bath tone 5", + "shortname": ":bath_tone5:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "shower", + "tub", + "basin", + "wash", + "bubble", + "soak", + "bathroom", + "soap", + "water", + "clean", + "shampoo", + "lather" + ] + }, "bathtub": { "unicode": "1F6C1", "unicode_alternates": [], @@ -852,7 +1807,23 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["clean", "shower", "bath", "tub", "basin", "wash", "bubble", "soak", "bathroom", "soap", "water", "clean", "shampoo", "lather", "water"], + "keywords": [ + "clean", + "shower", + "bath", + "tub", + "basin", + "wash", + "bubble", + "soak", + "bathroom", + "soap", + "water", + "clean", + "shampoo", + "lather", + "water" + ], "moji": "🛁" }, "battery": { @@ -863,7 +1834,11 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["energy", "power", "sustain"], + "keywords": [ + "energy", + "power", + "sustain" + ], "moji": "🔋" }, "beach": { @@ -872,9 +1847,38 @@ "name": "beach with umbrella", "shortname": ":beach:", "category": "travel_places", - "aliases": [":beach_with_umbrella:"], + "aliases": [ + ":beach_with_umbrella:" + ], "aliases_ascii": [], - "keywords": ["sand", "sun", "surf", "vacation", "relaxation", "tanning", "tan", "swimming"] + "keywords": [ + "sand", + "sun", + "surf", + "vacation", + "relaxation", + "tanning", + "tan", + "swimming" + ] + }, + "beach_umbrella": { + "unicode": "26F1", + "unicode_alternates": "", + "name": "umbrella on ground", + "shortname": ":beach_umbrella:", + "category": "objects", + "aliases": [ + ":umbrella_on_ground:" + ], + "aliases_ascii": [], + "keywords": [ + "nature", + "rain", + "sun", + "travel", + "weather" + ] }, "bear": { "unicode": "1F43B", @@ -884,7 +1888,10 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature"], + "keywords": [ + "animal", + "nature" + ], "moji": "🐻" }, "bed": { @@ -895,7 +1902,15 @@ "category": "travel_places", "aliases": [], "aliases_ascii": [], - "keywords": ["sleep", "sex", "queen", "full", "twin", "king", "mattress"] + "keywords": [ + "sleep", + "sex", + "queen", + "full", + "twin", + "king", + "mattress" + ] }, "bee": { "unicode": "1F41D", @@ -905,7 +1920,20 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "insect", "bee", "queen", "buzz", "flower", "pollen", "sting", "honey", "hive", "bumble", "pollination"], + "keywords": [ + "animal", + "insect", + "bee", + "queen", + "buzz", + "flower", + "pollen", + "sting", + "honey", + "hive", + "bumble", + "pollination" + ], "moji": "🐝" }, "beer": { @@ -916,7 +1944,26 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["beverage", "drink", "drunk", "party", "pub", "relax", "beer", "hops", "mug", "barley", "malt", "yeast", "portland", "oregon", "brewery", "micro", "pint", "boot"], + "keywords": [ + "beverage", + "drink", + "drunk", + "party", + "pub", + "relax", + "beer", + "hops", + "mug", + "barley", + "malt", + "yeast", + "portland", + "oregon", + "brewery", + "micro", + "pint", + "boot" + ], "moji": "🍺" }, "beers": { @@ -927,7 +1974,25 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["beverage", "drink", "drunk", "party", "pub", "relax", "beer", "beers", "cheers", "mug", "toast", "celebrate", "pub", "bar", "jolly", "hops", "clink"], + "keywords": [ + "beverage", + "drink", + "drunk", + "party", + "pub", + "relax", + "beer", + "beers", + "cheers", + "mug", + "toast", + "celebrate", + "pub", + "bar", + "jolly", + "hops", + "clink" + ], "moji": "🍻" }, "beetle": { @@ -938,7 +2003,19 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["insect", "nature", "lady", "bug", "ladybug", "ladybird", "beetle", "cow", "lady cow", "insect", "endearment"], + "keywords": [ + "insect", + "nature", + "lady", + "bug", + "ladybug", + "ladybird", + "beetle", + "cow", + "lady cow", + "insect", + "endearment" + ], "moji": "🐞" }, "beginner": { @@ -949,7 +2026,10 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["badge", "shield"], + "keywords": [ + "badge", + "shield" + ], "moji": "🔰" }, "bell": { @@ -960,7 +2040,13 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["chime", "christmas", "notification", "sound", "xmas"], + "keywords": [ + "chime", + "christmas", + "notification", + "sound", + "xmas" + ], "moji": "🔔" }, "bellhop": { @@ -969,9 +2055,15 @@ "name": "bellhop bell", "shortname": ":bellhop:", "category": "travel_places", - "aliases": [":bellhop_bell:"], + "aliases": [ + ":bellhop_bell:" + ], "aliases_ascii": [], - "keywords": ["hotel", "porter", "ding"] + "keywords": [ + "hotel", + "porter", + "ding" + ] }, "bento": { "unicode": "1F371", @@ -981,7 +2073,19 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["box", "food", "japanese", "bento", "japanese", "rice", "meal", "box", "obento", "convenient", "lunchbox"], + "keywords": [ + "box", + "food", + "japanese", + "bento", + "japanese", + "rice", + "meal", + "box", + "obento", + "convenient", + "lunchbox" + ], "moji": "🍱" }, "bicyclist": { @@ -992,9 +2096,115 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["bike", "exercise", "hipster", "sports", "bicyclist", "road", "bike", "pedal", "bicycle", "transportation"], + "keywords": [ + "bike", + "exercise", + "hipster", + "sports", + "bicyclist", + "road", + "bike", + "pedal", + "bicycle", + "transportation" + ], "moji": "🚴" }, + "bicyclist_tone1": { + "unicode": "1F6B4-1F3FB", + "unicode_alternates": "", + "name": "bicyclist tone 1", + "shortname": ":bicyclist_tone1:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "bike", + "exercise", + "hipster", + "sport", + "road", + "pedal", + "bicycle", + "transportation" + ] + }, + "bicyclist_tone2": { + "unicode": "1F6B4-1F3FC", + "unicode_alternates": "", + "name": "bicyclist tone 2", + "shortname": ":bicyclist_tone2:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "bike", + "exercise", + "hipster", + "sport", + "road", + "pedal", + "bicycle", + "transportation" + ] + }, + "bicyclist_tone3": { + "unicode": "1F6B4-1F3FD", + "unicode_alternates": "", + "name": "bicyclist tone 3", + "shortname": ":bicyclist_tone3:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "bike", + "exercise", + "hipster", + "sport", + "road", + "pedal", + "bicycle", + "transportation" + ] + }, + "bicyclist_tone4": { + "unicode": "1F6B4-1F3FE", + "unicode_alternates": "", + "name": "bicyclist tone 4", + "shortname": ":bicyclist_tone4:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "bike", + "exercise", + "hipster", + "sport", + "road", + "pedal", + "bicycle", + "transportation" + ] + }, + "bicyclist_tone5": { + "unicode": "1F6B4-1F3FF", + "unicode_alternates": "", + "name": "bicyclist tone 5", + "shortname": ":bicyclist_tone5:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "bike", + "exercise", + "hipster", + "sport", + "road", + "pedal", + "bicycle", + "transportation" + ] + }, "bike": { "unicode": "1F6B2", "unicode_alternates": [], @@ -1003,7 +2213,16 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["bicycle", "exercise", "hipster", "sports", "bike", "pedal", "bicycle", "transportation"], + "keywords": [ + "bicycle", + "exercise", + "hipster", + "sports", + "bike", + "pedal", + "bicycle", + "transportation" + ], "moji": "🚲" }, "bikini": { @@ -1014,9 +2233,30 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["beach", "fashion", "female", "girl", "swimming", "woman"], + "keywords": [ + "beach", + "fashion", + "female", + "girl", + "swimming", + "woman" + ], "moji": "👙" }, + "biohazard": { + "unicode": "2623", + "unicode_alternates": "", + "name": "biohazard sign", + "shortname": ":biohazard:", + "category": "symbols", + "aliases": [ + ":biohazard_sign:" + ], + "aliases_ascii": [], + "keywords": [ + "symbol" + ] + }, "bird": { "unicode": "1F426", "unicode_alternates": [], @@ -1025,7 +2265,12 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "fly", "nature", "tweet"], + "keywords": [ + "animal", + "fly", + "nature", + "tweet" + ], "moji": "🐦" }, "birthday": { @@ -1036,18 +2281,31 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["cake", "party", "birthday", "birth", "cake", "dessert", "wish", "celebrate"], + "keywords": [ + "cake", + "party", + "birthday", + "birth", + "cake", + "dessert", + "wish", + "celebrate" + ], "moji": "🎂" }, "black_circle": { "unicode": "26AB", - "unicode_alternates": ["26AB-FE0F"], + "unicode_alternates": [ + "26AB-FE0F" + ], "name": "medium black circle", "shortname": ":black_circle:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["shape"], + "keywords": [ + "shape" + ], "moji": "⚫" }, "black_joker": { @@ -1058,23 +2316,33 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["cards", "game", "poker"], + "keywords": [ + "cards", + "game", + "poker" + ], "moji": "🃏" }, "black_large_square": { "unicode": "2B1B", - "unicode_alternates": ["2B1B-FE0F"], + "unicode_alternates": [ + "2B1B-FE0F" + ], "name": "black large square", "shortname": ":black_large_square:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["shape"], + "keywords": [ + "shape" + ], "moji": "⬛" }, "black_medium_small_square": { "unicode": "25FE", - "unicode_alternates": ["25FE-FE0F"], + "unicode_alternates": [ + "25FE-FE0F" + ], "name": "black medium small square", "shortname": ":black_medium_small_square:", "category": "other", @@ -1085,29 +2353,40 @@ }, "black_medium_square": { "unicode": "25FC", - "unicode_alternates": ["25FC-FE0F"], + "unicode_alternates": [ + "25FC-FE0F" + ], "name": "black medium square", "shortname": ":black_medium_square:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["shape"], + "keywords": [ + "shape" + ], "moji": "◼" }, "black_nib": { "unicode": "2712", - "unicode_alternates": ["2712-FE0F"], + "unicode_alternates": [ + "2712-FE0F" + ], "name": "black nib", "shortname": ":black_nib:", "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["pen", "stationery"], + "keywords": [ + "pen", + "stationery" + ], "moji": "✒" }, "black_small_square": { "unicode": "25AA", - "unicode_alternates": ["25AA-FE0F"], + "unicode_alternates": [ + "25AA-FE0F" + ], "name": "black small square", "shortname": ":black_small_square:", "category": "other", @@ -1124,7 +2403,9 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["frame"], + "keywords": [ + "frame" + ], "moji": "🔲" }, "blossom": { @@ -1135,7 +2416,14 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["flowers", "nature", "yellow", "blossom", "daisy", "flower"], + "keywords": [ + "flowers", + "nature", + "yellow", + "blossom", + "daisy", + "flower" + ], "moji": "🌼" }, "blowfish": { @@ -1146,7 +2434,19 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "nature", "ocean", "sea", "blowfish", "pufferfish", "puffer", "ballonfish", "toadfish", "fugu fish", "sushi"], + "keywords": [ + "food", + "nature", + "ocean", + "sea", + "blowfish", + "pufferfish", + "puffer", + "ballonfish", + "toadfish", + "fugu fish", + "sushi" + ], "moji": "🐡" }, "blue_book": { @@ -1157,7 +2457,11 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["knowledge", "library", "read"], + "keywords": [ + "knowledge", + "library", + "read" + ], "moji": "📘" }, "blue_car": { @@ -1168,7 +2472,13 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["car", "suv", "car", "wagon", "automobile"], + "keywords": [ + "car", + "suv", + "car", + "wagon", + "automobile" + ], "moji": "🚙" }, "blue_heart": { @@ -1179,7 +2489,19 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["affection", "like", "love", "valentines", "blue", "heart", "love", "stability", "truth", "loyalty", "trust"], + "keywords": [ + "affection", + "like", + "love", + "valentines", + "blue", + "heart", + "love", + "stability", + "truth", + "loyalty", + "trust" + ], "moji": "💙" }, "blush": { @@ -1190,7 +2512,18 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["crush", "embarrassed", "face", "flushed", "happy", "shy", "smile", "smiling", "smile", "smiley"], + "keywords": [ + "crush", + "embarrassed", + "face", + "flushed", + "happy", + "shy", + "smile", + "smiling", + "smile", + "smiley" + ], "moji": "😊" }, "boar": { @@ -1201,7 +2534,10 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature"], + "keywords": [ + "animal", + "nature" + ], "moji": "🐗" }, "bomb": { @@ -1212,7 +2548,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["boom", "explode"], + "keywords": [ + "boom", + "explode" + ], "moji": "💣" }, "book": { @@ -1223,7 +2562,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["library", "literature"], + "keywords": [ + "library", + "literature" + ], "moji": "📖" }, "book2": { @@ -1234,7 +2576,13 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["library", "literature", "novel", "reading", "story"] + "keywords": [ + "library", + "literature", + "novel", + "reading", + "story" + ] }, "bookmark": { "unicode": "1F516", @@ -1244,7 +2592,9 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["favorite"], + "keywords": [ + "favorite" + ], "moji": "🔖" }, "bookmark_tabs": { @@ -1255,7 +2605,9 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["favorite"], + "keywords": [ + "favorite" + ], "moji": "📑" }, "books": { @@ -1266,7 +2618,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["library", "literature"], + "keywords": [ + "library", + "literature" + ], "moji": "📚" }, "boom": { @@ -1277,7 +2632,18 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["bomb", "explode", "explosion", "boom", "bang", "collision", "fire", "emphasis", "wow", "bam"], + "keywords": [ + "bomb", + "explode", + "explosion", + "boom", + "bang", + "collision", + "fire", + "emphasis", + "wow", + "bam" + ], "moji": "💥" }, "boot": { @@ -1288,7 +2654,10 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["fashion", "shoes"], + "keywords": [ + "fashion", + "shoes" + ], "moji": "👢" }, "bouquet": { @@ -1299,7 +2668,10 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["flowers", "nature"], + "keywords": [ + "flowers", + "nature" + ], "moji": "💐" }, "bouquet2": { @@ -1308,9 +2680,16 @@ "name": "bouquet of flowers", "shortname": ":bouquet2:", "category": "celebration", - "aliases": [":bouquet_of_flowers:"], + "aliases": [ + ":bouquet_of_flowers:" + ], "aliases_ascii": [], - "keywords": ["nature", "marriage", "wedding", "bride"] + "keywords": [ + "nature", + "marriage", + "wedding", + "bride" + ] }, "bow": { "unicode": "1F647", @@ -1320,9 +2699,120 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["boy", "male", "man", "sorry", "bow", "respect", "curtsy", "bend"], + "keywords": [ + "boy", + "male", + "man", + "sorry", + "bow", + "respect", + "curtsy", + "bend" + ], "moji": "🙇" }, + "bow_and_arrow": { + "unicode": "1F3F9", + "unicode_alternates": "", + "name": "bow and arrow", + "shortname": ":bow_and_arrow:", + "category": "activity", + "aliases": [ + ":archery:" + ], + "aliases_ascii": [], + "keywords": [] + }, + "bow_tone1": { + "unicode": "1F647-1F3FB", + "unicode_alternates": "", + "name": "person bowing deeply tone 1", + "shortname": ":bow_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "boy", + "male", + "man", + "sorry", + "bow", + "respect", + "bend" + ] + }, + "bow_tone2": { + "unicode": "1F647-1F3FC", + "unicode_alternates": "", + "name": "person bowing deeply tone 2", + "shortname": ":bow_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "boy", + "male", + "man", + "sorry", + "bow", + "respect", + "bend" + ] + }, + "bow_tone3": { + "unicode": "1F647-1F3FD", + "unicode_alternates": "", + "name": "person bowing deeply tone 3", + "shortname": ":bow_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "boy", + "male", + "man", + "sorry", + "bow", + "respect", + "bend" + ] + }, + "bow_tone4": { + "unicode": "1F647-1F3FE", + "unicode_alternates": "", + "name": "person bowing deeply tone 4", + "shortname": ":bow_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "boy", + "male", + "man", + "sorry", + "bow", + "respect", + "bend" + ] + }, + "bow_tone5": { + "unicode": "1F647-1F3FF", + "unicode_alternates": "", + "name": "person bowing deeply tone 5", + "shortname": ":bow_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "boy", + "male", + "man", + "sorry", + "bow", + "respect", + "bend" + ] + }, "bowling": { "unicode": "1F3B3", "unicode_alternates": [], @@ -1331,7 +2821,18 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["fun", "play", "sports", "bowl", "bowling", "ball", "pin", "strike", "spare", "game"], + "keywords": [ + "fun", + "play", + "sports", + "bowl", + "bowling", + "ball", + "pin", + "strike", + "spare", + "game" + ], "moji": "🎳" }, "boy": { @@ -1342,9 +2843,83 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["guy", "male", "man"], + "keywords": [ + "guy", + "male", + "man" + ], "moji": "👦" }, + "boy_tone1": { + "unicode": "1F466-1F3FB", + "unicode_alternates": "", + "name": "boy tone 1", + "shortname": ":boy_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "male", + "kid", + "child" + ] + }, + "boy_tone2": { + "unicode": "1F466-1F3FC", + "unicode_alternates": "", + "name": "boy tone 2", + "shortname": ":boy_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "male", + "kid", + "child" + ] + }, + "boy_tone3": { + "unicode": "1F466-1F3FD", + "unicode_alternates": "", + "name": "boy tone 3", + "shortname": ":boy_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "male", + "kid", + "child" + ] + }, + "boy_tone4": { + "unicode": "1F466-1F3FE", + "unicode_alternates": "", + "name": "boy tone 4", + "shortname": ":boy_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "male", + "kid", + "child" + ] + }, + "boy_tone5": { + "unicode": "1F466-1F3FF", + "unicode_alternates": "", + "name": "boy tone 5", + "shortname": ":boy_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "male", + "kid", + "child" + ] + }, "boys_symbol": { "unicode": "1F6C9", "unicode_alternates": [], @@ -1353,7 +2928,10 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["male", "child"] + "keywords": [ + "male", + "child" + ] }, "bread": { "unicode": "1F35E", @@ -1363,7 +2941,15 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["breakfast", "food", "toast", "wheat", "bread", "loaf", "yeast"], + "keywords": [ + "breakfast", + "food", + "toast", + "wheat", + "bread", + "loaf", + "yeast" + ], "moji": "🍞" }, "bride_with_veil": { @@ -1374,9 +2960,121 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["couple", "marriage", "wedding", "bride", "wedding", "planning", "veil", "gown", "dress", "engagement", "white"], + "keywords": [ + "couple", + "marriage", + "wedding", + "bride", + "wedding", + "planning", + "veil", + "gown", + "dress", + "engagement", + "white" + ], "moji": "👰" }, + "bride_with_veil_tone1": { + "unicode": "1F470-1F3FB", + "unicode_alternates": "", + "name": "bride with veil tone 1", + "shortname": ":bride_with_veil_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "couple", + "marriage", + "wedding", + "wedding", + "planning", + "gown", + "dress", + "engagement", + "white" + ] + }, + "bride_with_veil_tone2": { + "unicode": "1F470-1F3FC", + "unicode_alternates": "", + "name": "bride with veil tone 2", + "shortname": ":bride_with_veil_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "couple", + "marriage", + "wedding", + "wedding", + "planning", + "gown", + "dress", + "engagement", + "white" + ] + }, + "bride_with_veil_tone3": { + "unicode": "1F470-1F3FD", + "unicode_alternates": "", + "name": "bride with veil tone 3", + "shortname": ":bride_with_veil_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "couple", + "marriage", + "wedding", + "wedding", + "planning", + "gown", + "dress", + "engagement", + "white" + ] + }, + "bride_with_veil_tone4": { + "unicode": "1F470-1F3FE", + "unicode_alternates": "", + "name": "bride with veil tone 4", + "shortname": ":bride_with_veil_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "couple", + "marriage", + "wedding", + "wedding", + "planning", + "gown", + "dress", + "engagement", + "white" + ] + }, + "bride_with_veil_tone5": { + "unicode": "1F470-1F3FF", + "unicode_alternates": "", + "name": "bride with veil tone 5", + "shortname": ":bride_with_veil_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "couple", + "marriage", + "wedding", + "wedding", + "planning", + "gown", + "dress", + "engagement", + "white" + ] + }, "bridge_at_night": { "unicode": "1F309", "unicode_alternates": [], @@ -1385,7 +3083,18 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["photo", "sanfrancisco", "bridge", "night", "water", "road", "evening", "suspension", "golden", "gate"], + "keywords": [ + "photo", + "sanfrancisco", + "bridge", + "night", + "water", + "road", + "evening", + "suspension", + "golden", + "gate" + ], "moji": "🌉" }, "briefcase": { @@ -1396,7 +3105,11 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["business", "documents", "work"], + "keywords": [ + "business", + "documents", + "work" + ], "moji": "💼" }, "broken_heart": { @@ -1406,8 +3119,13 @@ "shortname": ":broken_heart:", "category": "emoticons", "aliases": [], - "aliases_ascii": [":\\", ">:/", ":-/", ":-.", ":/", ":\\", "=/", "=\\", ":L", "=L"], - "keywords": ["confused", "confuse", "daze", "perplex", "puzzle", "indifference", "skeptical", "undecided", "uneasy", "hesitant"], + "aliases_ascii": [ + ">:\\", + ">:/", + ":-/", + ":-.", + ":/", + ":\\", + "=/", + "=\\", + ":L", + "=L" + ], + "keywords": [ + "confused", + "confuse", + "daze", + "perplex", + "puzzle", + "indifference", + "skeptical", + "undecided", + "uneasy", + "hesitant" + ], "moji": "😕" }, "congratulations": { "unicode": "3297", - "unicode_alternates": ["3297-FE0F"], + "unicode_alternates": [ + "3297-FE0F" + ], "name": "circled ideograph congratulation", "shortname": ":congratulations:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["chinese", "japanese", "kanji"], + "keywords": [ + "chinese", + "japanese", + "kanji" + ], "moji": "㊗" }, "construction": { @@ -2495,9 +5053,29 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["caution", "progress", "wip"], + "keywords": [ + "caution", + "progress", + "wip" + ], "moji": "🚧" }, + "construction_site": { + "unicode": "1F3D7", + "unicode_alternates": "", + "name": "building construction", + "shortname": ":construction_site:", + "category": "travel", + "aliases": [ + ":building_construction:" + ], + "aliases_ascii": [], + "keywords": [ + "site", + "work", + "place" + ] + }, "construction_worker": { "unicode": "1F477", "unicode_alternates": [], @@ -2506,9 +5084,89 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["human", "male", "man", "wip"], + "keywords": [ + "human", + "male", + "man", + "wip" + ], "moji": "👷" }, + "construction_worker_tone1": { + "unicode": "1F477-1F3FB", + "unicode_alternates": "", + "name": "construction worker tone 1", + "shortname": ":construction_worker_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "human", + "male", + "man", + "wip" + ] + }, + "construction_worker_tone2": { + "unicode": "1F477-1F3FC", + "unicode_alternates": "", + "name": "construction worker tone 2", + "shortname": ":construction_worker_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "human", + "male", + "man", + "wip" + ] + }, + "construction_worker_tone3": { + "unicode": "1F477-1F3FD", + "unicode_alternates": "", + "name": "construction worker tone 3", + "shortname": ":construction_worker_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "human", + "male", + "man", + "wip" + ] + }, + "construction_worker_tone4": { + "unicode": "1F477-1F3FE", + "unicode_alternates": "", + "name": "construction worker tone 4", + "shortname": ":construction_worker_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "human", + "male", + "man", + "wip" + ] + }, + "construction_worker_tone5": { + "unicode": "1F477-1F3FF", + "unicode_alternates": "", + "name": "construction worker tone 5", + "shortname": ":construction_worker_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "human", + "male", + "man", + "wip" + ] + }, "control_knobs": { "unicode": "1F39B", "unicode_alternates": [], @@ -2517,7 +5175,9 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["dial"] + "keywords": [ + "dial" + ] }, "contruction_site": { "unicode": "1F3D7", @@ -2525,9 +5185,14 @@ "name": "building construction", "shortname": ":contruction_site:", "category": "travel_places", - "aliases": [":building_construction:"], + "aliases": [ + ":building_construction:" + ], "aliases_ascii": [], - "keywords": ["site", "work"] + "keywords": [ + "site", + "work" + ] }, "convenience_store": { "unicode": "1F3EA", @@ -2537,7 +5202,9 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["building"], + "keywords": [ + "building" + ], "moji": "🏪" }, "cookie": { @@ -2548,7 +5215,17 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["chocolate", "food", "oreo", "snack", "cookie", "dessert", "biscuit", "sweet", "chocolate"], + "keywords": [ + "chocolate", + "food", + "oreo", + "snack", + "cookie", + "dessert", + "biscuit", + "sweet", + "chocolate" + ], "moji": "🍪" }, "cool": { @@ -2559,7 +5236,10 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square", "words"], + "keywords": [ + "blue-square", + "words" + ], "moji": "🆒" }, "cop": { @@ -2570,9 +5250,95 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["arrest", "enforcement", "law", "man", "police"], + "keywords": [ + "arrest", + "enforcement", + "law", + "man", + "police" + ], "moji": "👮" }, + "cop_tone1": { + "unicode": "1F46E-1F3FB", + "unicode_alternates": "", + "name": "police officer tone 1", + "shortname": ":cop_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "arrest", + "enforcement", + "law", + "man", + "cop" + ] + }, + "cop_tone2": { + "unicode": "1F46E-1F3FC", + "unicode_alternates": "", + "name": "police officer tone 2", + "shortname": ":cop_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "arrest", + "enforcement", + "law", + "man", + "cop" + ] + }, + "cop_tone3": { + "unicode": "1F46E-1F3FD", + "unicode_alternates": "", + "name": "police officer tone 3", + "shortname": ":cop_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "arrest", + "enforcement", + "law", + "man", + "cop" + ] + }, + "cop_tone4": { + "unicode": "1F46E-1F3FE", + "unicode_alternates": "", + "name": "police officer tone 4", + "shortname": ":cop_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "arrest", + "enforcement", + "law", + "man", + "cop" + ] + }, + "cop_tone5": { + "unicode": "1F46E-1F3FF", + "unicode_alternates": "", + "name": "police officer tone 5", + "shortname": ":cop_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "arrest", + "enforcement", + "law", + "man", + "cop" + ] + }, "copyright": { "moji": "©", "unicode": "00A9", @@ -2582,7 +5348,10 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["ip", "license"] + "keywords": [ + "ip", + "license" + ] }, "corn": { "unicode": "1F33D", @@ -2592,7 +5361,22 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "plant", "vegetable", "corn", "maize", "food", "iowa", "kernel", "popcorn", "husk", "yellow", "stalk", "cob", "ear"], + "keywords": [ + "food", + "plant", + "vegetable", + "corn", + "maize", + "food", + "iowa", + "kernel", + "popcorn", + "husk", + "yellow", + "stalk", + "cob", + "ear" + ], "moji": "🌽" }, "couch": { @@ -2601,9 +5385,20 @@ "name": "couch and lamp", "shortname": ":couch:", "category": "travel_places", - "aliases": [":couch_and_lamp:"], + "aliases": [ + ":couch_and_lamp:" + ], "aliases_ascii": [], - "keywords": ["lounge", "sectional", "sofa", "loveseat", "leather", "microfiber", "sit", "relax"] + "keywords": [ + "lounge", + "sectional", + "sofa", + "loveseat", + "leather", + "microfiber", + "sit", + "relax" + ] }, "couple": { "unicode": "1F46B", @@ -2613,18 +5408,40 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["affection", "date", "dating", "human", "like", "love", "marriage", "people", "valentines"], + "keywords": [ + "affection", + "date", + "dating", + "human", + "like", + "love", + "marriage", + "people", + "valentines" + ], "moji": "👫" }, "couple_mm": { "unicode": "1F468-2764-1F468", - "unicode_alternates": ["1F468-200D-2764-FE0F-200D-1F468"], + "unicode_alternates": [ + "1F468-200D-2764-FE0F-200D-1F468" + ], "name": "couple (man,man)", "shortname": ":couple_mm:", "category": "people", - "aliases": [":couple_with_heart_mm:"], + "aliases": [ + ":couple_with_heart_mm:" + ], "aliases_ascii": [], - "keywords": ["affection", "dating", "human", "like", "love", "marriage", "valentines"] + "keywords": [ + "affection", + "dating", + "human", + "like", + "love", + "marriage", + "valentines" + ] }, "couple_with_heart": { "unicode": "1F491", @@ -2634,18 +5451,38 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["affection", "dating", "human", "like", "love", "marriage", "valentines"], + "keywords": [ + "affection", + "dating", + "human", + "like", + "love", + "marriage", + "valentines" + ], "moji": "💑" }, "couple_ww": { "unicode": "1F469-2764-1F469", - "unicode_alternates": ["1F469-200D-2764-FE0F-200D-1F469"], + "unicode_alternates": [ + "1F469-200D-2764-FE0F-200D-1F469" + ], "name": "couple (woman,woman)", "shortname": ":couple_ww:", "category": "people", - "aliases": [":couple_with_heart_ww:"], + "aliases": [ + ":couple_with_heart_ww:" + ], "aliases_ascii": [], - "keywords": ["affection", "dating", "human", "like", "love", "marriage", "valentines"] + "keywords": [ + "affection", + "dating", + "human", + "like", + "love", + "marriage", + "valentines" + ] }, "couplekiss": { "unicode": "1F48F", @@ -2655,7 +5492,13 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["dating", "like", "love", "marriage", "valentines"], + "keywords": [ + "dating", + "like", + "love", + "marriage", + "valentines" + ], "moji": "💏" }, "cow": { @@ -2666,7 +5509,11 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "beef", "ox"], + "keywords": [ + "animal", + "beef", + "ox" + ], "moji": "🐮" }, "cow2": { @@ -2677,18 +5524,46 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "beef", "nature", "ox", "cow", "milk", "dairy", "beef", "bessie", "moo"], + "keywords": [ + "animal", + "beef", + "nature", + "ox", + "cow", + "milk", + "dairy", + "beef", + "bessie", + "moo" + ], "moji": "🐄" }, + "crab": { + "unicode": "1F980", + "unicode_alternates": "", + "name": "crab", + "shortname": ":crab:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": [] + }, "crayon": { "unicode": "1F58D", "unicode_alternates": [], "name": "lower left crayon", "shortname": ":crayon:", "category": "objects_symbols", - "aliases": [":lower_left_crayon:"], + "aliases": [ + ":lower_left_crayon:" + ], "aliases_ascii": [], - "keywords": ["write", "draw", "color", "wax"] + "keywords": [ + "write", + "draw", + "color", + "wax" + ] }, "credit_card": { "unicode": "1F4B3", @@ -2698,7 +5573,23 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["bill", "dollar", "money", "pay", "payment", "credit", "card", "loan", "purchase", "shopping", "mastercard", "visa", "american express", "wallet", "signature"], + "keywords": [ + "bill", + "dollar", + "money", + "pay", + "payment", + "credit", + "card", + "loan", + "purchase", + "shopping", + "mastercard", + "visa", + "american express", + "wallet", + "signature" + ], "moji": "💳" }, "crescent_moon": { @@ -2709,9 +5600,30 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["night", "moon", "crescent", "waxing", "sky", "night", "cheese", "phase"], + "keywords": [ + "night", + "moon", + "crescent", + "waxing", + "sky", + "night", + "cheese", + "phase" + ], "moji": "🌙" }, + "cricket": { + "unicode": "1F3CF", + "unicode_alternates": "", + "name": "cricket bat and ball", + "shortname": ":cricket:", + "category": "activity", + "aliases": [ + ":cricket_bat_ball:" + ], + "aliases_ascii": [], + "keywords": [] + }, "crocodile": { "unicode": "1F40A", "unicode_alternates": [], @@ -2720,18 +5632,47 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature", "crocodile", "croc", "alligator", "gator", "cranky"], + "keywords": [ + "animal", + "nature", + "crocodile", + "croc", + "alligator", + "gator", + "cranky" + ], "moji": "🐊" }, + "cross": { + "unicode": "271D", + "unicode_alternates": "", + "name": "latin cross", + "shortname": ":cross:", + "category": "symbols", + "aliases": [ + ":latin_cross:" + ], + "aliases_ascii": [], + "keywords": [ + "religion", + "symbol", + "christian" + ] + }, "cross_heavy": { "unicode": "1F547", "unicode_alternates": [], "name": "heavy latin cross", "shortname": ":cross_heavy:", "category": "objects_symbols", - "aliases": [":heavy_latin_cross:"], + "aliases": [ + ":heavy_latin_cross:" + ], "aliases_ascii": [], - "keywords": ["religion", "symbol"] + "keywords": [ + "religion", + "symbol" + ] }, "cross_white": { "unicode": "1F546", @@ -2739,9 +5680,14 @@ "name": "white latin cross", "shortname": ":cross_white:", "category": "objects_symbols", - "aliases": [":white_latin_cross:"], + "aliases": [ + ":white_latin_cross:" + ], "aliases_ascii": [], - "keywords": ["religion", "symbol"] + "keywords": [ + "religion", + "symbol" + ] }, "crossbones": { "unicode": "1F571", @@ -2749,9 +5695,15 @@ "name": "black skull and crossbones", "shortname": ":crossbones:", "category": "objects_symbols", - "aliases": [":black_skull_and_crossbones:"], + "aliases": [ + ":black_skull_and_crossbones:" + ], "aliases_ascii": [], - "keywords": ["poison", "danger", "death"] + "keywords": [ + "poison", + "danger", + "death" + ] }, "crossed_flags": { "unicode": "1F38C", @@ -2761,9 +5713,24 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["japan"], + "keywords": [ + "japan" + ], "moji": "🎌" }, + "crossed_swords": { + "unicode": "2694", + "unicode_alternates": "", + "name": "crossed swords", + "shortname": ":crossed_swords:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "object", + "weapon" + ] + }, "crown": { "unicode": "1F451", "unicode_alternates": [], @@ -2772,7 +5739,12 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["king", "kod", "leader", "royalty"], + "keywords": [ + "king", + "kod", + "leader", + "royalty" + ], "moji": "👑" }, "cruise_ship": { @@ -2781,9 +5753,15 @@ "name": "passenger ship", "shortname": ":cruise_ship:", "category": "travel_places", - "aliases": [":passenger_ship:"], + "aliases": [ + ":passenger_ship:" + ], "aliases_ascii": [], - "keywords": ["titanic", "transportation", "boat"] + "keywords": [ + "titanic", + "transportation", + "boat" + ] }, "cry": { "unicode": "1F622", @@ -2792,8 +5770,21 @@ "shortname": ":cry:", "category": "emoticons", "aliases": [], - "aliases_ascii": [":'(", ":'-(", ";(", ";-("], - "keywords": ["face", "sad", "sad", "cry", "tear", "weep", "tears"], + "aliases_ascii": [ + ":'(", + ":'-(", + ";(", + ";-(" + ], + "keywords": [ + "face", + "sad", + "sad", + "cry", + "tear", + "weep", + "tears" + ], "moji": "😢" }, "crying_cat_face": { @@ -2804,7 +5795,22 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "cats", "sad", "tears", "weep", "cry", "cat", "sob", "tears", "sad", "melancholy", "morn", "somber", "hurt"], + "keywords": [ + "animal", + "cats", + "sad", + "tears", + "weep", + "cry", + "cat", + "sob", + "tears", + "sad", + "melancholy", + "morn", + "somber", + "hurt" + ], "moji": "😿" }, "crystal_ball": { @@ -2815,7 +5821,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["disco", "party"], + "keywords": [ + "disco", + "party" + ], "moji": "🔮" }, "cupid": { @@ -2826,7 +5835,13 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["affection", "heart", "like", "love", "valentines"], + "keywords": [ + "affection", + "heart", + "like", + "love", + "valentines" + ], "moji": "💘" }, "curly_loop": { @@ -2837,7 +5852,9 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["scribble"], + "keywords": [ + "scribble" + ], "moji": "➰" }, "currency_exchange": { @@ -2848,7 +5865,11 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["dollar", "money", "travel"], + "keywords": [ + "dollar", + "money", + "travel" + ], "moji": "💱" }, "curry": { @@ -2859,7 +5880,17 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "hot", "indian", "spicy", "curry", "spice", "flavor", "food", "meal"], + "keywords": [ + "food", + "hot", + "indian", + "spicy", + "curry", + "spice", + "flavor", + "food", + "meal" + ], "moji": "🍛" }, "custard": { @@ -2870,7 +5901,18 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["desert", "food", "custard", "cream", "rich", "butter", "dessert", "crème", "brûlée", "french"], + "keywords": [ + "desert", + "food", + "custard", + "cream", + "rich", + "butter", + "dessert", + "crème", + "brûlée", + "french" + ], "moji": "🍮" }, "customs": { @@ -2881,7 +5923,17 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["border", "passport", "customs", "travel", "foreign", "goods", "check", "authority", "government"], + "keywords": [ + "border", + "passport", + "customs", + "travel", + "foreign", + "goods", + "check", + "authority", + "government" + ], "moji": "🛃" }, "cyclone": { @@ -2893,7 +5945,17 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["blue", "cloud", "swirl", "weather", "cyclone", "hurricane", "typhoon", "storm", "ocean"] + "keywords": [ + "blue", + "cloud", + "swirl", + "weather", + "cyclone", + "hurricane", + "typhoon", + "storm", + "ocean" + ] }, "dagger": { "unicode": "1F5E1", @@ -2901,9 +5963,14 @@ "name": "dagger knife", "shortname": ":dagger:", "category": "objects_symbols", - "aliases": [":dagger_knife:"], + "aliases": [ + ":dagger_knife:" + ], "aliases_ascii": [], - "keywords": ["blade", "knife"] + "keywords": [ + "blade", + "knife" + ] }, "dancer": { "unicode": "1F483", @@ -2913,9 +5980,145 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["female", "fun", "girl", "woman", "dance", "dancer", "dress", "fancy", "boogy", "party", "celebrate", "ballet", "tango", "cha cha", "music"], + "keywords": [ + "female", + "fun", + "girl", + "woman", + "dance", + "dancer", + "dress", + "fancy", + "boogy", + "party", + "celebrate", + "ballet", + "tango", + "cha cha", + "music" + ], "moji": "💃" }, + "dancer_tone1": { + "unicode": "1F483-1F3FB", + "unicode_alternates": "", + "name": "dancer tone 1", + "shortname": ":dancer_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "fun", + "girl", + "woman", + "dress", + "fancy", + "boogy", + "party", + "celebrate", + "ballet", + "tango", + "cha cha", + "music" + ] + }, + "dancer_tone2": { + "unicode": "1F483-1F3FC", + "unicode_alternates": "", + "name": "dancer tone 2", + "shortname": ":dancer_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "fun", + "girl", + "woman", + "dress", + "fancy", + "boogy", + "party", + "celebrate", + "ballet", + "tango", + "cha cha", + "music" + ] + }, + "dancer_tone3": { + "unicode": "1F483-1F3FD", + "unicode_alternates": "", + "name": "dancer tone 3", + "shortname": ":dancer_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "fun", + "girl", + "woman", + "dress", + "fancy", + "boogy", + "party", + "celebrate", + "ballet", + "tango", + "cha cha", + "music" + ] + }, + "dancer_tone4": { + "unicode": "1F483-1F3FE", + "unicode_alternates": "", + "name": "dancer tone 4", + "shortname": ":dancer_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "fun", + "girl", + "woman", + "dress", + "fancy", + "boogy", + "party", + "celebrate", + "ballet", + "tango", + "cha cha", + "music" + ] + }, + "dancer_tone5": { + "unicode": "1F483-1F3FF", + "unicode_alternates": "", + "name": "dancer tone 5", + "shortname": ":dancer_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "fun", + "girl", + "woman", + "dress", + "fancy", + "boogy", + "party", + "celebrate", + "ballet", + "tango", + "cha cha", + "music" + ] + }, "dancers": { "unicode": "1F46F", "unicode_alternates": [], @@ -2924,7 +6127,19 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["bunny", "female", "girls", "women", "dancing", "dancers", "showgirl", "playboy", "costume", "bunny", "cancan"], + "keywords": [ + "bunny", + "female", + "girls", + "women", + "dancing", + "dancers", + "showgirl", + "playboy", + "costume", + "bunny", + "cancan" + ], "moji": "👯" }, "dango": { @@ -2935,7 +6150,15 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "dango", "japanese", "dumpling", "mochi", "balls", "skewer"], + "keywords": [ + "food", + "dango", + "japanese", + "dumpling", + "mochi", + "balls", + "skewer" + ], "moji": "🍡" }, "dark_sunglasses": { @@ -2946,7 +6169,10 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["shades", "eyes"] + "keywords": [ + "shades", + "eyes" + ] }, "dart": { "unicode": "1F3AF", @@ -2956,7 +6182,19 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["bar", "game", "direct", "hit", "bullseye", "dart", "archery", "game", "fletching", "arrow", "sport"], + "keywords": [ + "bar", + "game", + "direct", + "hit", + "bullseye", + "dart", + "archery", + "game", + "fletching", + "arrow", + "sport" + ], "moji": "🎯" }, "dash": { @@ -2967,7 +6205,12 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["air", "fast", "shoo", "wind"], + "keywords": [ + "air", + "fast", + "shoo", + "wind" + ], "moji": "💨" }, "date": { @@ -2978,7 +6221,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["calendar", "schedule"], + "keywords": [ + "calendar", + "schedule" + ], "moji": "📅" }, "deciduous_tree": { @@ -2989,7 +6235,15 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["nature", "plant", "deciduous", "tree", "leaves", "fall", "color"], + "keywords": [ + "nature", + "plant", + "deciduous", + "tree", + "leaves", + "fall", + "color" + ], "moji": "🌳" }, "department_store": { @@ -3000,7 +6254,16 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["building", "mall", "shopping", "department", "store", "retail", "sale", "merchandise"], + "keywords": [ + "building", + "mall", + "shopping", + "department", + "store", + "retail", + "sale", + "merchandise" + ], "moji": "🏬" }, "descending_notes": { @@ -3011,7 +6274,12 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["score", "music", "sound", "tone"] + "keywords": [ + "score", + "music", + "sound", + "tone" + ] }, "desert": { "unicode": "1F3DC", @@ -3021,7 +6289,14 @@ "category": "travel_places", "aliases": [], "aliases_ascii": [], - "keywords": ["hot", "dry", "sandy", "cactus", "sunny", "barren"] + "keywords": [ + "hot", + "dry", + "sandy", + "cactus", + "sunny", + "barren" + ] }, "desktop": { "unicode": "1F5A5", @@ -3029,9 +6304,13 @@ "name": "desktop computer", "shortname": ":desktop:", "category": "objects_symbols", - "aliases": [":desktop_computer:"], + "aliases": [ + ":desktop_computer:" + ], "aliases_ascii": [], - "keywords": ["cpu"] + "keywords": [ + "cpu" + ] }, "desktop_window": { "unicode": "1F5D4", @@ -3041,7 +6320,9 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["computer"] + "keywords": [ + "computer" + ] }, "diamond_shape_with_a_dot_inside": { "unicode": "1F4A0", @@ -3051,18 +6332,31 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["diamond", "cute", "cuteness", "kawaii", "japanese", "glyph", "adorable"], + "keywords": [ + "diamond", + "cute", + "cuteness", + "kawaii", + "japanese", + "glyph", + "adorable" + ], "moji": "💠" }, "diamonds": { "unicode": "2666", - "unicode_alternates": ["2666-FE0F"], + "unicode_alternates": [ + "2666-FE0F" + ], "name": "black diamond suit", "shortname": ":diamonds:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["cards", "poker"], + "keywords": [ + "cards", + "poker" + ], "moji": "♦" }, "disappointed": { @@ -3072,8 +6366,24 @@ "shortname": ":disappointed:", "category": "emoticons", "aliases": [], - "aliases_ascii": [">:[", ":-(", ":(", ":-[", ":[", "=("], - "keywords": ["disappointed", "disappoint", "frown", "depressed", "discouraged", "face", "sad", "upset"], + "aliases_ascii": [ + ">:[", + ":-(", + ":(", + ":-[", + ":[", + "=(" + ], + "keywords": [ + "disappointed", + "disappoint", + "frown", + "depressed", + "discouraged", + "face", + "sad", + "upset" + ], "moji": "😞" }, "disappointed_relieved": { @@ -3084,7 +6394,14 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["face", "nervous", "phew", "sweat", "disappoint", "relief"], + "keywords": [ + "face", + "nervous", + "phew", + "sweat", + "disappoint", + "relief" + ], "moji": "😥" }, "dividers": { @@ -3093,9 +6410,14 @@ "name": "card index dividers", "shortname": ":dividers:", "category": "objects_symbols", - "aliases": [":card_index_dividers:"], + "aliases": [ + ":card_index_dividers:" + ], "aliases_ascii": [], - "keywords": ["stationery", "rolodex"] + "keywords": [ + "stationery", + "rolodex" + ] }, "dizzy": { "unicode": "1F4AB", @@ -3105,7 +6427,18 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["shoot", "sparkle", "star", "dizzy", "drunk", "sick", "intoxicated", "squeans", "starburst", "star"], + "keywords": [ + "shoot", + "sparkle", + "star", + "dizzy", + "drunk", + "sick", + "intoxicated", + "squeans", + "starburst", + "star" + ], "moji": "💫" }, "dizzy_face": { @@ -3115,8 +6448,23 @@ "shortname": ":dizzy_face:", "category": "emoticons", "aliases": [], - "aliases_ascii": ["#-)", "#)", "%-)", "%)", "X)", "X-)"], - "keywords": ["dizzy", "drunk", "inebriated", "face", "spent", "unconscious", "xox"], + "aliases_ascii": [ + "#-)", + "#)", + "%-)", + "%)", + "X)", + "X-)" + ], + "keywords": [ + "dizzy", + "drunk", + "inebriated", + "face", + "spent", + "unconscious", + "xox" + ], "moji": "😵" }, "do_not_litter": { @@ -3127,7 +6475,17 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["bin", "garbage", "trash", "litter", "garbage", "waste", "no", "can", "trash"], + "keywords": [ + "bin", + "garbage", + "trash", + "litter", + "garbage", + "waste", + "no", + "can", + "trash" + ], "moji": "🚯" }, "document": { @@ -3138,7 +6496,9 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["page"] + "keywords": [ + "page" + ] }, "document_text": { "unicode": "1F5B9", @@ -3146,9 +6506,13 @@ "name": "document with text", "shortname": ":document_text:", "category": "objects_symbols", - "aliases": [":document_with_text:"], + "aliases": [ + ":document_with_text:" + ], "aliases_ascii": [], - "keywords": ["page"] + "keywords": [ + "page" + ] }, "dog": { "unicode": "1F436", @@ -3158,7 +6522,12 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "friend", "nature", "woof"], + "keywords": [ + "animal", + "friend", + "nature", + "woof" + ], "moji": "🐶" }, "dog2": { @@ -3169,7 +6538,20 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "doge", "friend", "nature", "pet", "dog", "puppy", "pet", "friend", "woof", "bark", "fido"], + "keywords": [ + "animal", + "doge", + "friend", + "nature", + "pet", + "dog", + "puppy", + "pet", + "friend", + "woof", + "bark", + "fido" + ], "moji": "🐕" }, "dollar": { @@ -3180,7 +6562,21 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["bill", "currency", "money", "dollar", "united states", "canada", "australia", "banknote", "money", "currency", "paper", "cash", "bills"], + "keywords": [ + "bill", + "currency", + "money", + "dollar", + "united states", + "canada", + "australia", + "banknote", + "money", + "currency", + "paper", + "cash", + "bills" + ], "moji": "💵" }, "dolls": { @@ -3191,7 +6587,23 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["japanese", "kimono", "toy", "dolls", "japan", "japanese", "day", "girls", "emperor", "empress", "pray", "blessing", "imperial", "family", "royal"], + "keywords": [ + "japanese", + "kimono", + "toy", + "dolls", + "japan", + "japanese", + "day", + "girls", + "emperor", + "empress", + "pray", + "blessing", + "imperial", + "family", + "royal" + ], "moji": "🎎" }, "dolphin": { @@ -3202,7 +6614,15 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "fins", "fish", "flipper", "nature", "ocean", "sea"], + "keywords": [ + "animal", + "fins", + "fish", + "flipper", + "nature", + "ocean", + "sea" + ], "moji": "🐬" }, "door": { @@ -3213,7 +6633,17 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["entry", "exit", "house", "door", "doorway", "entrance", "enter", "exit", "entry"], + "keywords": [ + "entry", + "exit", + "house", + "door", + "doorway", + "entrance", + "enter", + "exit", + "entry" + ], "moji": "🚪" }, "doughnut": { @@ -3224,7 +6654,21 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["desert", "food", "snack", "sweet", "doughnut", "donut", "pastry", "fried", "dessert", "breakfast", "police", "homer", "sweet"], + "keywords": [ + "desert", + "food", + "snack", + "sweet", + "doughnut", + "donut", + "pastry", + "fried", + "dessert", + "breakfast", + "police", + "homer", + "sweet" + ], "moji": "🍩" }, "dove": { @@ -3233,9 +6677,14 @@ "name": "dove of peace", "shortname": ":dove:", "category": "objects_symbols", - "aliases": [":dove_of_peace:"], + "aliases": [ + ":dove_of_peace:" + ], "aliases_ascii": [], - "keywords": ["symbol", "bird"] + "keywords": [ + "symbol", + "bird" + ] }, "dragon": { "unicode": "1F409", @@ -3245,7 +6694,17 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "chinese", "green", "myth", "nature", "dragon", "fire", "legendary", "myth"], + "keywords": [ + "animal", + "chinese", + "green", + "myth", + "nature", + "dragon", + "fire", + "legendary", + "myth" + ], "moji": "🐉" }, "dragon_face": { @@ -3256,7 +6715,18 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "chinese", "green", "myth", "nature", "dragon", "head", "fire", "legendary", "myth"], + "keywords": [ + "animal", + "chinese", + "green", + "myth", + "nature", + "dragon", + "head", + "fire", + "legendary", + "myth" + ], "moji": "🐲" }, "dress": { @@ -3267,7 +6737,10 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["clothes", "fashion"], + "keywords": [ + "clothes", + "fashion" + ], "moji": "👗" }, "dromedary_camel": { @@ -3278,7 +6751,22 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "desert", "hot", "dromedary", "camel", "hump", "desert", "middle east", "heat", "hot", "water", "hump day", "wednesday", "sex"], + "keywords": [ + "animal", + "desert", + "hot", + "dromedary", + "camel", + "hump", + "desert", + "middle east", + "heat", + "hot", + "water", + "hump day", + "wednesday", + "sex" + ], "moji": "🐪" }, "droplet": { @@ -3289,7 +6777,23 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["drip", "faucet", "water", "drop", "droplet", "h20", "water", "aqua", "tear", "sweat", "rain", "moisture", "wet", "moist", "spit"], + "keywords": [ + "drip", + "faucet", + "water", + "drop", + "droplet", + "h20", + "water", + "aqua", + "tear", + "sweat", + "rain", + "moisture", + "wet", + "moist", + "spit" + ], "moji": "💧" }, "dvd": { @@ -3300,7 +6804,11 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["cd", "disc", "disk"], + "keywords": [ + "cd", + "disc", + "disk" + ], "moji": "📀" }, "e-mail": { @@ -3309,9 +6817,14 @@ "name": "e-mail symbol", "shortname": ":e-mail:", "category": "objects", - "aliases": [":email:"], + "aliases": [ + ":email:" + ], "aliases_ascii": [], - "keywords": ["communication", "inbox"], + "keywords": [ + "communication", + "inbox" + ], "moji": "📧" }, "ear": { @@ -3322,7 +6835,12 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["face", "hear", "listen", "sound"], + "keywords": [ + "face", + "hear", + "listen", + "sound" + ], "moji": "👂" }, "ear_of_rice": { @@ -3333,9 +6851,87 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["nature", "plant", "ear", "rice", "food", "plant", "seed"], + "keywords": [ + "nature", + "plant", + "ear", + "rice", + "food", + "plant", + "seed" + ], "moji": "🌾" }, + "ear_tone1": { + "unicode": "1F442-1F3FB", + "unicode_alternates": "", + "name": "ear tone 1", + "shortname": ":ear_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "hear", + "listen", + "sound" + ] + }, + "ear_tone2": { + "unicode": "1F442-1F3FC", + "unicode_alternates": "", + "name": "ear tone 2", + "shortname": ":ear_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "hear", + "listen", + "sound" + ] + }, + "ear_tone3": { + "unicode": "1F442-1F3FD", + "unicode_alternates": "", + "name": "ear tone 3", + "shortname": ":ear_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "hear", + "listen", + "sound" + ] + }, + "ear_tone4": { + "unicode": "1F442-1F3FE", + "unicode_alternates": "", + "name": "ear tone 4", + "shortname": ":ear_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "hear", + "listen", + "sound" + ] + }, + "ear_tone5": { + "unicode": "1F442-1F3FF", + "unicode_alternates": "", + "name": "ear tone 5", + "shortname": ":ear_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "hear", + "listen", + "sound" + ] + }, "earth_africa": { "unicode": "1F30D", "unicode_alternates": [], @@ -3344,7 +6940,18 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["globe", "international", "world", "earth", "globe", "space", "planet", "africa", "europe", "home"], + "keywords": [ + "globe", + "international", + "world", + "earth", + "globe", + "space", + "planet", + "africa", + "europe", + "home" + ], "moji": "🌍" }, "earth_americas": { @@ -3355,7 +6962,21 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["USA", "globe", "international", "world", "earth", "globe", "space", "planet", "north", "south", "america", "americas", "home"], + "keywords": [ + "USA", + "globe", + "international", + "world", + "earth", + "globe", + "space", + "planet", + "north", + "south", + "america", + "americas", + "home" + ], "moji": "🌎" }, "earth_asia": { @@ -3366,7 +6987,19 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["east", "globe", "international", "world", "earth", "globe", "space", "planet", "asia", "australia", "home"], + "keywords": [ + "east", + "globe", + "international", + "world", + "earth", + "globe", + "space", + "planet", + "asia", + "australia", + "home" + ], "moji": "🌏" }, "egg": { @@ -3377,7 +7010,18 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["breakfast", "food", "egg", "fry", "pan", "flat", "cook", "frying", "cooking", "utensil"], + "keywords": [ + "breakfast", + "food", + "egg", + "fry", + "pan", + "flat", + "cook", + "frying", + "cooking", + "utensil" + ], "moji": "🍳" }, "eggplant": { @@ -3388,23 +7032,41 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["aubergine", "food", "nature", "vegetable", "eggplant", "aubergine", "fruit", "purple", "penis"], + "keywords": [ + "aubergine", + "food", + "nature", + "vegetable", + "eggplant", + "aubergine", + "fruit", + "purple", + "penis" + ], "moji": "🍆" }, "eight": { "moji": "8️⃣", "unicode": "0038-20E3", - "unicode_alternates": ["0038-FE0F-20E3"], + "unicode_alternates": [ + "0038-FE0F-20E3" + ], "name": "digit eight", "shortname": ":eight:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["8", "blue-square", "numbers"] + "keywords": [ + "8", + "blue-square", + "numbers" + ] }, "eight_pointed_black_star": { "unicode": "2734", - "unicode_alternates": ["2734-FE0F"], + "unicode_alternates": [ + "2734-FE0F" + ], "name": "eight pointed black star", "shortname": ":eight_pointed_black_star:", "category": "other", @@ -3415,13 +7077,19 @@ }, "eight_spoked_asterisk": { "unicode": "2733", - "unicode_alternates": ["2733-FE0F"], + "unicode_alternates": [ + "2733-FE0F" + ], "name": "eight spoked asterisk", "shortname": ":eight_spoked_asterisk:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["green-square", "sparkle", "star"], + "keywords": [ + "green-square", + "sparkle", + "star" + ], "moji": "✳" }, "electric_plug": { @@ -3432,7 +7100,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["charger", "power"], + "keywords": [ + "charger", + "power" + ], "moji": "🔌" }, "elephant": { @@ -3443,7 +7114,12 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature", "nose", "thailand"], + "keywords": [ + "animal", + "nature", + "nose", + "thailand" + ], "moji": "🐘" }, "end": { @@ -3454,18 +7130,28 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["arrow", "words"], + "keywords": [ + "arrow", + "words" + ], "moji": "🔚" }, "envelope": { "unicode": "2709", - "unicode_alternates": ["2709-FE0F"], + "unicode_alternates": [ + "2709-FE0F" + ], "name": "envelope", "shortname": ":envelope:", "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["communication", "letter", "mail", "postal"], + "keywords": [ + "communication", + "letter", + "mail", + "postal" + ], "moji": "✉" }, "envelope_back": { @@ -3474,9 +7160,16 @@ "name": "back of envelope", "shortname": ":envelope_back:", "category": "objects_symbols", - "aliases": [":back_of_envelope:"], + "aliases": [ + ":back_of_envelope:" + ], "aliases_ascii": [], - "keywords": ["communication", "letter", "mail", "postal"] + "keywords": [ + "communication", + "letter", + "mail", + "postal" + ] }, "envelope_flying": { "unicode": "1F585", @@ -3484,9 +7177,16 @@ "name": "flying envelope", "shortname": ":envelope_flying:", "category": "objects_symbols", - "aliases": [":flying_envelope:"], + "aliases": [ + ":flying_envelope:" + ], "aliases_ascii": [], - "keywords": ["communication", "letter", "mail", "postal"] + "keywords": [ + "communication", + "letter", + "mail", + "postal" + ] }, "envelope_stamped": { "unicode": "1F583", @@ -3494,9 +7194,16 @@ "name": "stamped envelope", "shortname": ":envelope_stamped:", "category": "objects_symbols", - "aliases": [":stamped_envelope:"], + "aliases": [ + ":stamped_envelope:" + ], "aliases_ascii": [], - "keywords": ["communication", "letter", "mail", "postal"] + "keywords": [ + "communication", + "letter", + "mail", + "postal" + ] }, "envelope_stamped_pen": { "unicode": "1F586", @@ -3504,9 +7211,16 @@ "name": "pen over stamped envelope", "shortname": ":envelope_stamped_pen:", "category": "objects_symbols", - "aliases": [":pen_over_stamped_envelope:"], + "aliases": [ + ":pen_over_stamped_envelope:" + ], "aliases_ascii": [], - "keywords": ["communication", "letter", "mail", "postal"] + "keywords": [ + "communication", + "letter", + "mail", + "postal" + ] }, "envelope_with_arrow": { "unicode": "1F4E9", @@ -3516,7 +7230,9 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["email"], + "keywords": [ + "email" + ], "moji": "📩" }, "euro": { @@ -3527,7 +7243,19 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["currency", "dollar", "money", "euro", "europe", "banknote", "money", "currency", "paper", "cash", "bills"], + "keywords": [ + "currency", + "dollar", + "money", + "euro", + "europe", + "banknote", + "money", + "currency", + "paper", + "cash", + "bills" + ], "moji": "💶" }, "european_castle": { @@ -3538,7 +7266,29 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["building", "history", "royalty", "castle", "european", "residence", "royalty", "disneyland", "disney", "fort", "fortified", "moat", "tower", "princess", "prince", "lord", "king", "queen", "fortress", "nobel", "stronghold"], + "keywords": [ + "building", + "history", + "royalty", + "castle", + "european", + "residence", + "royalty", + "disneyland", + "disney", + "fort", + "fortified", + "moat", + "tower", + "princess", + "prince", + "lord", + "king", + "queen", + "fortress", + "nobel", + "stronghold" + ], "moji": "🏰" }, "european_post_office": { @@ -3549,7 +7299,9 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["building"], + "keywords": [ + "building" + ], "moji": "🏤" }, "evergreen_tree": { @@ -3560,18 +7312,29 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["nature", "plant", "evergreen", "tree", "needles", "christmas"], + "keywords": [ + "nature", + "plant", + "evergreen", + "tree", + "needles", + "christmas" + ], "moji": "🌲" }, "exclamation": { "unicode": "2757", - "unicode_alternates": ["2757-FE0F"], + "unicode_alternates": [ + "2757-FE0F" + ], "name": "heavy exclamation mark symbol", "shortname": ":exclamation:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["surprise"], + "keywords": [ + "surprise" + ], "moji": "❗" }, "expressionless": { @@ -3581,8 +7344,20 @@ "shortname": ":expressionless:", "category": "emoticons", "aliases": [], - "aliases_ascii": ["-_-", "-__-", "-___-"], - "keywords": ["expressionless", "blank", "void", "vapid", "without expression", "face", "indifferent"], + "aliases_ascii": [ + "-_-", + "-__-", + "-___-" + ], + "keywords": [ + "expressionless", + "blank", + "void", + "vapid", + "without expression", + "face", + "indifferent" + ], "moji": "😑" }, "eye": { @@ -3593,7 +7368,21 @@ "category": "people", "aliases": [], "aliases_ascii": [], - "keywords": ["look", "peek", "watch"] + "keywords": [ + "look", + "peek", + "watch" + ] + }, + "eye_in_speech_bubble": { + "unicode": "1F441-1F5E8", + "unicode_alternates": "1f441-200d-1f5e8", + "name": "eye in speech bubble", + "shortname": ":eye_in_speech_bubble:", + "category": "symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": [] }, "eyeglasses": { "unicode": "1F453", @@ -3603,7 +7392,24 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["accessories", "eyesight", "fashion", "eyeglasses", "spectacles", "eye", "sight", "nearsightedness", "myopia", "farsightedness", "hyperopia", "frames", "vision", "see", "blurry", "contacts"], + "keywords": [ + "accessories", + "eyesight", + "fashion", + "eyeglasses", + "spectacles", + "eye", + "sight", + "nearsightedness", + "myopia", + "farsightedness", + "hyperopia", + "frames", + "vision", + "see", + "blurry", + "contacts" + ], "moji": "👓" }, "eyes": { @@ -3614,7 +7420,12 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["look", "peek", "stalk", "watch"], + "keywords": [ + "look", + "peek", + "stalk", + "watch" + ], "moji": "👀" }, "factory": { @@ -3625,7 +7436,9 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["building"], + "keywords": [ + "building" + ], "moji": "🏭" }, "fallen_leaf": { @@ -3636,7 +7449,17 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["leaves", "nature", "plant", "vegetable", "leaf", "fall", "color", "deciduous", "autumn"], + "keywords": [ + "leaves", + "nature", + "plant", + "vegetable", + "leaf", + "fall", + "color", + "deciduous", + "autumn" + ], "moji": "🍂" }, "family": { @@ -3647,148 +7470,359 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["child", "dad", "father", "home", "mom", "mother", "parents", "family", "mother", "father", "child", "girl", "boy", "group", "unit"], + "keywords": [ + "child", + "dad", + "father", + "home", + "mom", + "mother", + "parents", + "family", + "mother", + "father", + "child", + "girl", + "boy", + "group", + "unit" + ], "moji": "👪" }, "family_mmb": { "unicode": "1F468-1F468-1F466", - "unicode_alternates": ["1F468-200D-1F468-200D-1F466"], + "unicode_alternates": [ + "1F468-200D-1F468-200D-1F466" + ], "name": "family (man,man,boy)", "shortname": ":family_mmb:", "category": "people", "aliases": [], "aliases_ascii": [], - "keywords": ["child", "dad", "father", "parents", "group", "unit", "gay", "homosexual", "man", "boy"] + "keywords": [ + "child", + "dad", + "father", + "parents", + "group", + "unit", + "gay", + "homosexual", + "man", + "boy" + ] }, "family_mmbb": { "unicode": "1F468-1F468-1F466-1F466", - "unicode_alternates": ["1F468-200D-1F468-200D-1F466-200D-1F466"], + "unicode_alternates": [ + "1F468-200D-1F468-200D-1F466-200D-1F466" + ], "name": "family (man,man,boy,boy)", "shortname": ":family_mmbb:", "category": "people", "aliases": [], "aliases_ascii": [], - "keywords": ["children", "dad", "father", "parents", "group", "unit", "gay", "homosexual", "man", "boy"] + "keywords": [ + "children", + "dad", + "father", + "parents", + "group", + "unit", + "gay", + "homosexual", + "man", + "boy" + ] }, "family_mmg": { "unicode": "1F468-1F468-1F467", - "unicode_alternates": ["1F468-200D-1F468-200D-1F467"], + "unicode_alternates": [ + "1F468-200D-1F468-200D-1F467" + ], "name": "family (man,man,girl)", "shortname": ":family_mmg:", "category": "people", "aliases": [], "aliases_ascii": [], - "keywords": ["child", "dad", "father", "parents", "group", "unit", "gay", "homosexual", "man", "girl"] + "keywords": [ + "child", + "dad", + "father", + "parents", + "group", + "unit", + "gay", + "homosexual", + "man", + "girl" + ] }, "family_mmgb": { "unicode": "1F468-1F468-1F467-1F466", - "unicode_alternates": ["1F468-200D-1F468-200D-1F467-200D-1F466"], + "unicode_alternates": [ + "1F468-200D-1F468-200D-1F467-200D-1F466" + ], "name": "family (man,man,girl,boy)", "shortname": ":family_mmgb:", "category": "people", "aliases": [], "aliases_ascii": [], - "keywords": ["children", "dad", "father", "parents", "group", "unit", "gay", "homosexual", "man", "girl", "boy"] + "keywords": [ + "children", + "dad", + "father", + "parents", + "group", + "unit", + "gay", + "homosexual", + "man", + "girl", + "boy" + ] }, "family_mmgg": { "unicode": "1F468-1F468-1F467-1F467", - "unicode_alternates": ["1F468-200D-1F468-200D-1F467-200D-1F467"], + "unicode_alternates": [ + "1F468-200D-1F468-200D-1F467-200D-1F467" + ], "name": "family (man,man,girl,girl)", "shortname": ":family_mmgg:", "category": "people", "aliases": [], "aliases_ascii": [], - "keywords": ["children", "dad", "father", "parents", "group", "unit", "gay", "homosexual", "man", "girl"] + "keywords": [ + "children", + "dad", + "father", + "parents", + "group", + "unit", + "gay", + "homosexual", + "man", + "girl" + ] }, "family_mwbb": { "unicode": "1F468-1F469-1F466-1F466", - "unicode_alternates": ["1F468-200D-1F469-200D-1F466-200D-1F466"], + "unicode_alternates": [ + "1F468-200D-1F469-200D-1F466-200D-1F466" + ], "name": "family (man,woman,boy,boy)", "shortname": ":family_mwbb:", "category": "people", "aliases": [], "aliases_ascii": [], - "keywords": ["dad", "father", "mom", "mother", "parents", "children", "boy", "group", "unit", "man", "woman"] + "keywords": [ + "dad", + "father", + "mom", + "mother", + "parents", + "children", + "boy", + "group", + "unit", + "man", + "woman" + ] }, "family_mwg": { "unicode": "1F468-1F469-1F467", - "unicode_alternates": ["1F468-200D-1F469-200D-1F467"], + "unicode_alternates": [ + "1F468-200D-1F469-200D-1F467" + ], "name": "family (man,woman,girl)", "shortname": ":family_mwg:", "category": "people", "aliases": [], "aliases_ascii": [], - "keywords": ["child", "dad", "father", "mom", "mother", "parents", "girl", "boy", "group", "unit", "man", "woman"] + "keywords": [ + "child", + "dad", + "father", + "mom", + "mother", + "parents", + "girl", + "boy", + "group", + "unit", + "man", + "woman" + ] }, "family_mwgb": { "unicode": "1F468-1F469-1F467-1F466", - "unicode_alternates": ["1F468-200D-1F469-200D-1F467-200D-1F466"], + "unicode_alternates": [ + "1F468-200D-1F469-200D-1F467-200D-1F466" + ], "name": "family (man,woman,girl,boy)", "shortname": ":family_mwgb:", "category": "people", "aliases": [], "aliases_ascii": [], - "keywords": ["dad", "father", "mom", "mother", "parents", "children", "girl", "boy", "group", "unit", "man", "woman"] + "keywords": [ + "dad", + "father", + "mom", + "mother", + "parents", + "children", + "girl", + "boy", + "group", + "unit", + "man", + "woman" + ] }, "family_mwgg": { "unicode": "1F468-1F469-1F467-1F467", - "unicode_alternates": ["1F468-200D-1F469-200D-1F467-200D-1F467"], + "unicode_alternates": [ + "1F468-200D-1F469-200D-1F467-200D-1F467" + ], "name": "family (man,woman,girl,girl)", "shortname": ":family_mwgg:", "category": "people", "aliases": [], "aliases_ascii": [], - "keywords": ["dad", "father", "mom", "mother", "parents", "children", "girl", "group", "unit", "man", "woman"] + "keywords": [ + "dad", + "father", + "mom", + "mother", + "parents", + "children", + "girl", + "group", + "unit", + "man", + "woman" + ] }, "family_wwb": { "unicode": "1F469-1F469-1F466", - "unicode_alternates": ["1F469-200D-1F469-200D-1F466"], + "unicode_alternates": [ + "1F469-200D-1F469-200D-1F466" + ], "name": "family (woman,woman,boy)", "shortname": ":family_wwb:", "category": "people", "aliases": [], "aliases_ascii": [], - "keywords": ["mom", "mother", "parents", "child", "boy", "group", "unit", "gay", "lesbian", "homosexual", "woman"] + "keywords": [ + "mom", + "mother", + "parents", + "child", + "boy", + "group", + "unit", + "gay", + "lesbian", + "homosexual", + "woman" + ] }, "family_wwbb": { "unicode": "1F469-1F469-1F466-1F466", - "unicode_alternates": ["1F469-200D-1F469-200D-1F466-200D-1F466"], + "unicode_alternates": [ + "1F469-200D-1F469-200D-1F466-200D-1F466" + ], "name": "family (woman,woman,boy,boy)", "shortname": ":family_wwbb:", "category": "people", "aliases": [], "aliases_ascii": [], - "keywords": ["mom", "mother", "parents", "children", "group", "unit", "gay", "lesbian", "homosexual", "woman", "boy"] + "keywords": [ + "mom", + "mother", + "parents", + "children", + "group", + "unit", + "gay", + "lesbian", + "homosexual", + "woman", + "boy" + ] }, "family_wwg": { "unicode": "1F469-1F469-1F467", - "unicode_alternates": ["1F469-200D-1F469-200D-1F467"], + "unicode_alternates": [ + "1F469-200D-1F469-200D-1F467" + ], "name": "family (woman,woman,girl)", "shortname": ":family_wwg:", "category": "people", "aliases": [], "aliases_ascii": [], - "keywords": ["mom", "mother", "parents", "child", "woman", "girl", "group", "unit", "gay", "lesbian", "homosexual"] + "keywords": [ + "mom", + "mother", + "parents", + "child", + "woman", + "girl", + "group", + "unit", + "gay", + "lesbian", + "homosexual" + ] }, "family_wwgb": { "unicode": "1F469-1F469-1F467-1F466", - "unicode_alternates": ["1F469-200D-1F469-200D-1F467-200D-1F466"], + "unicode_alternates": [ + "1F469-200D-1F469-200D-1F467-200D-1F466" + ], "name": "family (woman,woman,girl,boy)", "shortname": ":family_wwgb:", "category": "people", "aliases": [], "aliases_ascii": [], - "keywords": ["mom", "mother", "parents", "children", "group", "unit", "gay", "lesbian", "homosexual", "woman", "girl", "boy"] + "keywords": [ + "mom", + "mother", + "parents", + "children", + "group", + "unit", + "gay", + "lesbian", + "homosexual", + "woman", + "girl", + "boy" + ] }, "family_wwgg": { "unicode": "1F469-1F469-1F467-1F467", - "unicode_alternates": ["1F469-200D-1F469-200D-1F467-200D-1F467"], + "unicode_alternates": [ + "1F469-200D-1F469-200D-1F467-200D-1F467" + ], "name": "family (woman,woman,girl,girl)", "shortname": ":family_wwgg:", "category": "people", "aliases": [], "aliases_ascii": [], - "keywords": ["mom", "mother", "parents", "children", "group", "unit", "gay", "lesbian", "homosexual", "woman", "girl"] + "keywords": [ + "mom", + "mother", + "parents", + "children", + "group", + "unit", + "gay", + "lesbian", + "homosexual", + "woman", + "girl" + ] }, "fast_forward": { "unicode": "23E9", @@ -3798,7 +7832,9 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square"], + "keywords": [ + "blue-square" + ], "moji": "⏩" }, "fax": { @@ -3809,7 +7845,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["communication", "technology"], + "keywords": [ + "communication", + "technology" + ], "moji": "📠" }, "fearful": { @@ -3820,7 +7859,17 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["face", "nervous", "oops", "scared", "terrified", "fear", "fearful", "scared", "frightened"], + "keywords": [ + "face", + "nervous", + "oops", + "scared", + "terrified", + "fear", + "fearful", + "scared", + "frightened" + ], "moji": "😨" }, "feet": { @@ -3831,7 +7880,29 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "cat", "dog", "footprints", "paw", "pet", "tracking", "paw", "prints", "mark", "imprints", "footsteps", "animal", "lion", "bear", "dog", "cat", "raccoon", "critter", "feet", "pawsteps"], + "keywords": [ + "animal", + "cat", + "dog", + "footprints", + "paw", + "pet", + "tracking", + "paw", + "prints", + "mark", + "imprints", + "footsteps", + "animal", + "lion", + "bear", + "dog", + "cat", + "raccoon", + "critter", + "feet", + "pawsteps" + ], "moji": "🐾" }, "ferris_wheel": { @@ -3842,9 +7913,44 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["carnival", "londoneye", "photo", "farris", "wheel", "amusement", "park", "fair", "ride", "entertainment"], + "keywords": [ + "carnival", + "londoneye", + "photo", + "farris", + "wheel", + "amusement", + "park", + "fair", + "ride", + "entertainment" + ], "moji": "🎡" }, + "ferry": { + "unicode": "26F4", + "unicode_alternates": "", + "name": "ferry", + "shortname": ":ferry:", + "category": "travel", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "boat", + "place", + "travel" + ] + }, + "field_hockey": { + "unicode": "1F3D1", + "unicode_alternates": "", + "name": "field hockey stick and ball", + "shortname": ":field_hockey:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [] + }, "file_cabinet": { "unicode": "1F5C4", "unicode_alternates": [], @@ -3853,7 +7959,12 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["folders", "office", "documents", "storage"] + "keywords": [ + "folders", + "office", + "documents", + "storage" + ] }, "file_folder": { "unicode": "1F4C1", @@ -3863,7 +7974,9 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["documents"], + "keywords": [ + "documents" + ], "moji": "📁" }, "film_frames": { @@ -3874,7 +7987,14 @@ "category": "activity", "aliases": [], "aliases_ascii": [], - "keywords": ["movie", "record", "8mm", "16mm", "reel", "celluloid"] + "keywords": [ + "movie", + "record", + "8mm", + "16mm", + "reel", + "celluloid" + ] }, "finger_pointing_down": { "unicode": "1F597", @@ -3882,9 +8002,15 @@ "name": "white down pointing left hand index", "shortname": ":finger_pointing_down:", "category": "people", - "aliases": [":white_down_pointing_left_hand_index:"], + "aliases": [ + ":white_down_pointing_left_hand_index:" + ], "aliases_ascii": [], - "keywords": ["direction", "finger", "hand"] + "keywords": [ + "direction", + "finger", + "hand" + ] }, "finger_pointing_down2": { "unicode": "1F59F", @@ -3892,9 +8018,15 @@ "name": "sideways white down pointing index", "shortname": ":finger_pointing_down2:", "category": "people", - "aliases": [":sideways_white_down_pointing_index:"], + "aliases": [ + ":sideways_white_down_pointing_index:" + ], "aliases_ascii": [], - "keywords": ["direction", "finger", "hand"] + "keywords": [ + "direction", + "finger", + "hand" + ] }, "finger_pointing_left": { "unicode": "1F598", @@ -3902,9 +8034,15 @@ "name": "sideways white left pointing index", "shortname": ":finger_pointing_left:", "category": "people", - "aliases": [":sideways_white_left_pointing_index:"], + "aliases": [ + ":sideways_white_left_pointing_index:" + ], "aliases_ascii": [], - "keywords": ["direction", "finger", "hand"] + "keywords": [ + "direction", + "finger", + "hand" + ] }, "finger_pointing_right": { "unicode": "1F599", @@ -3912,9 +8050,15 @@ "name": "sideways white right pointing index", "shortname": ":finger_pointing_right:", "category": "people", - "aliases": [":sideways_white_right_pointing_index:"], + "aliases": [ + ":sideways_white_right_pointing_index:" + ], "aliases_ascii": [], - "keywords": ["direction", "finger", "hand"] + "keywords": [ + "direction", + "finger", + "hand" + ] }, "finger_pointing_up": { "unicode": "1F59E", @@ -3922,9 +8066,15 @@ "name": "sideways white up pointing index", "shortname": ":finger_pointing_up:", "category": "people", - "aliases": [":sideways_white_up_pointing_index:"], + "aliases": [ + ":sideways_white_up_pointing_index:" + ], "aliases_ascii": [], - "keywords": ["direction", "finger", "hand"] + "keywords": [ + "direction", + "finger", + "hand" + ] }, "fire": { "unicode": "1F525", @@ -3932,9 +8082,15 @@ "name": "fire", "shortname": ":fire:", "category": "emoticons", - "aliases": [":flame:"], + "aliases": [ + ":flame:" + ], "aliases_ascii": [], - "keywords": ["cook", "hot", "flame"], + "keywords": [ + "cook", + "hot", + "flame" + ], "moji": "🔥" }, "fire_engine": { @@ -3945,7 +8101,17 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["cars", "transportation", "vehicle", "fire", "fighter", "engine", "truck", "emergency", "medical"], + "keywords": [ + "cars", + "transportation", + "vehicle", + "fire", + "fighter", + "engine", + "truck", + "emergency", + "medical" + ], "moji": "🚒" }, "fire_engine_oncoming": { @@ -3954,9 +8120,17 @@ "name": "oncoming fire engine", "shortname": ":fire_engine_oncoming:", "category": "travel_places", - "aliases": [":oncoming_fire_engine:"], + "aliases": [ + ":oncoming_fire_engine:" + ], "aliases_ascii": [], - "keywords": ["transportation", "vehicle", "fighter", "truck", "emergency"] + "keywords": [ + "transportation", + "vehicle", + "fighter", + "truck", + "emergency" + ] }, "fireworks": { "unicode": "1F386", @@ -3966,7 +8140,22 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["carnival", "congratulations", "festival", "photo", "fireworks", "independence", "celebration", "explosion", "july", "4th", "rocket", "sky", "idea", "excitement"], + "keywords": [ + "carnival", + "congratulations", + "festival", + "photo", + "fireworks", + "independence", + "celebration", + "explosion", + "july", + "4th", + "rocket", + "sky", + "idea", + "excitement" + ], "moji": "🎆" }, "first_quarter_moon": { @@ -3977,7 +8166,16 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["nature", "moon", "quarter", "first", "sky", "night", "cheese", "phase"], + "keywords": [ + "nature", + "moon", + "quarter", + "first", + "sky", + "night", + "cheese", + "phase" + ], "moji": "🌓" }, "first_quarter_moon_with_face": { @@ -3988,7 +8186,18 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["nature", "moon", "first", "quarter", "anthropomorphic", "face", "sky", "night", "cheese", "phase"], + "keywords": [ + "nature", + "moon", + "first", + "quarter", + "anthropomorphic", + "face", + "sky", + "night", + "cheese", + "phase" + ], "moji": "🌛" }, "fish": { @@ -3999,7 +8208,11 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "food", "nature"], + "keywords": [ + "animal", + "food", + "nature" + ], "moji": "🐟" }, "fish_cake": { @@ -4010,7 +8223,16 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "fish", "cake", "kamboko", "swirl", "ramen", "noodles", "naruto"], + "keywords": [ + "food", + "fish", + "cake", + "kamboko", + "swirl", + "ramen", + "noodles", + "naruto" + ], "moji": "🍥" }, "fishing_pole_and_fish": { @@ -4021,7 +8243,13 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "hobby", "fish", "fishing", "pole"], + "keywords": [ + "food", + "hobby", + "fish", + "fishing", + "pole" + ], "moji": "🎣" }, "fist": { @@ -4032,19 +8260,99 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["fingers", "grasp", "hand"], + "keywords": [ + "fingers", + "grasp", + "hand" + ], "moji": "✊" }, + "fist_tone1": { + "unicode": "270A-1F3FB", + "unicode_alternates": "", + "name": "raised fist tone 1", + "shortname": ":fist_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "fingers", + "grasp", + "hand" + ] + }, + "fist_tone2": { + "unicode": "270A-1F3FC", + "unicode_alternates": "", + "name": "raised fist tone 2", + "shortname": ":fist_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "fingers", + "grasp", + "hand" + ] + }, + "fist_tone3": { + "unicode": "270A-1F3FD", + "unicode_alternates": "", + "name": "raised fist tone 3", + "shortname": ":fist_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "fingers", + "grasp", + "hand" + ] + }, + "fist_tone4": { + "unicode": "270A-1F3FE", + "unicode_alternates": "", + "name": "raised fist tone 4", + "shortname": ":fist_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "fingers", + "grasp", + "hand" + ] + }, + "fist_tone5": { + "unicode": "270A-1F3FF", + "unicode_alternates": "", + "name": "raised fist tone 5", + "shortname": ":fist_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "fingers", + "grasp", + "hand" + ] + }, "five": { "moji": "5️⃣", "unicode": "0035-20E3", - "unicode_alternates": ["0035-FE0F-20E3"], + "unicode_alternates": [ + "0035-FE0F-20E3" + ], "name": "digit five", "shortname": ":five:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square", "numbers", "prime"] + "keywords": [ + "blue-square", + "numbers", + "prime" + ] }, "flag_ac": { "unicode": "1F1E6-1F1E8", @@ -4052,9 +8360,15 @@ "name": "ascension", "shortname": ":flag_ac:", "category": "flags", - "aliases": [":ac:"], + "aliases": [ + ":ac:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ac"] + "keywords": [ + "country", + "nation", + "ac" + ] }, "flag_ad": { "unicode": "1F1E6-1F1E9", @@ -4062,9 +8376,15 @@ "name": "andorra", "shortname": ":flag_ad:", "category": "flags", - "aliases": [":ad:"], + "aliases": [ + ":ad:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ad"] + "keywords": [ + "country", + "nation", + "ad" + ] }, "flag_ae": { "unicode": "1F1E6-1F1EA", @@ -4072,9 +8392,15 @@ "name": "the united arab emirates", "shortname": ":flag_ae:", "category": "flags", - "aliases": [":ae:"], + "aliases": [ + ":ae:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ae"] + "keywords": [ + "country", + "nation", + "ae" + ] }, "flag_af": { "unicode": "1F1E6-1F1EB", @@ -4082,9 +8408,16 @@ "name": "afghanistan", "shortname": ":flag_af:", "category": "flags", - "aliases": [":af:"], + "aliases": [ + ":af:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "afghanestan", "af"] + "keywords": [ + "country", + "nation", + "afghanestan", + "af" + ] }, "flag_ag": { "unicode": "1F1E6-1F1EC", @@ -4092,9 +8425,15 @@ "name": "antigua and barbuda", "shortname": ":flag_ag:", "category": "flags", - "aliases": [":ag:"], + "aliases": [ + ":ag:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ag"] + "keywords": [ + "country", + "nation", + "ag" + ] }, "flag_ai": { "unicode": "1F1E6-1F1EE", @@ -4102,9 +8441,15 @@ "name": "anguilla", "shortname": ":flag_ai:", "category": "flags", - "aliases": [":ai:"], + "aliases": [ + ":ai:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ai"] + "keywords": [ + "country", + "nation", + "ai" + ] }, "flag_al": { "unicode": "1F1E6-1F1F1", @@ -4112,9 +8457,16 @@ "name": "albania", "shortname": ":flag_al:", "category": "flags", - "aliases": [":al:"], + "aliases": [ + ":al:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "shqiperia", "al"] + "keywords": [ + "country", + "nation", + "shqiperia", + "al" + ] }, "flag_am": { "unicode": "1F1E6-1F1F2", @@ -4122,9 +8474,16 @@ "name": "armenia", "shortname": ":flag_am:", "category": "flags", - "aliases": [":am:"], + "aliases": [ + ":am:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "hayastan", "am"] + "keywords": [ + "country", + "nation", + "hayastan", + "am" + ] }, "flag_ao": { "unicode": "1F1E6-1F1F4", @@ -4132,9 +8491,27 @@ "name": "angola", "shortname": ":flag_ao:", "category": "flags", - "aliases": [":ao:"], + "aliases": [ + ":ao:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ao"] + "keywords": [ + "country", + "nation", + "ao" + ] + }, + "flag_aq": { + "unicode": "1F1E6-1F1F6", + "unicode_alternates": "", + "name": "antarctica", + "shortname": ":flag_aq:", + "category": "flags", + "aliases": [ + ":aq:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_ar": { "unicode": "1F1E6-1F1F7", @@ -4142,9 +8519,27 @@ "name": "argentina", "shortname": ":flag_ar:", "category": "flags", - "aliases": [":ar:"], + "aliases": [ + ":ar:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ar"] + "keywords": [ + "country", + "nation", + "ar" + ] + }, + "flag_as": { + "unicode": "1F1E6-1F1F8", + "unicode_alternates": "", + "name": "american samoa", + "shortname": ":flag_as:", + "category": "flags", + "aliases": [ + ":as:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_at": { "unicode": "1F1E6-1F1F9", @@ -4152,9 +8547,17 @@ "name": "austria", "shortname": ":flag_at:", "category": "flags", - "aliases": [":at:"], + "aliases": [ + ":at:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "österreich", "osterreich", "at"] + "keywords": [ + "country", + "nation", + "österreich", + "osterreich", + "at" + ] }, "flag_au": { "unicode": "1F1E6-1F1FA", @@ -4162,9 +8565,15 @@ "name": "australia", "shortname": ":flag_au:", "category": "flags", - "aliases": [":au:"], + "aliases": [ + ":au:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "au"] + "keywords": [ + "country", + "nation", + "au" + ] }, "flag_aw": { "unicode": "1F1E6-1F1FC", @@ -4172,9 +8581,27 @@ "name": "aruba", "shortname": ":flag_aw:", "category": "flags", - "aliases": [":aw:"], + "aliases": [ + ":aw:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "aw"] + "keywords": [ + "country", + "nation", + "aw" + ] + }, + "flag_ax": { + "unicode": "1F1E6-1F1FD", + "unicode_alternates": "", + "name": "åland islands", + "shortname": ":flag_ax:", + "category": "flags", + "aliases": [ + ":ax:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_az": { "unicode": "1F1E6-1F1FF", @@ -4182,9 +8609,16 @@ "name": "azerbaijan", "shortname": ":flag_az:", "category": "flags", - "aliases": [":az:"], + "aliases": [ + ":az:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "azarbaycan", "az"] + "keywords": [ + "country", + "nation", + "azarbaycan", + "az" + ] }, "flag_ba": { "unicode": "1F1E7-1F1E6", @@ -4192,9 +8626,16 @@ "name": "bosnia and herzegovina", "shortname": ":flag_ba:", "category": "flags", - "aliases": [":ba:"], + "aliases": [ + ":ba:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "bosna i hercegovina", "ba"] + "keywords": [ + "country", + "nation", + "bosna i hercegovina", + "ba" + ] }, "flag_bb": { "unicode": "1F1E7-1F1E7", @@ -4202,9 +8643,15 @@ "name": "barbados", "shortname": ":flag_bb:", "category": "flags", - "aliases": [":bb:"], + "aliases": [ + ":bb:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "bb"] + "keywords": [ + "country", + "nation", + "bb" + ] }, "flag_bd": { "unicode": "1F1E7-1F1E9", @@ -4212,9 +8659,15 @@ "name": "bangladesh", "shortname": ":flag_bd:", "category": "flags", - "aliases": [":bd:"], + "aliases": [ + ":bd:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "bd"] + "keywords": [ + "country", + "nation", + "bd" + ] }, "flag_be": { "unicode": "1F1E7-1F1EA", @@ -4222,9 +8675,17 @@ "name": "belgium", "shortname": ":flag_be:", "category": "flags", - "aliases": [":be:"], + "aliases": [ + ":be:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "belgique", "belgie", "be"] + "keywords": [ + "country", + "nation", + "belgique", + "belgie", + "be" + ] }, "flag_bf": { "unicode": "1F1E7-1F1EB", @@ -4232,9 +8693,15 @@ "name": "burkina faso", "shortname": ":flag_bf:", "category": "flags", - "aliases": [":bf:"], + "aliases": [ + ":bf:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "bf"] + "keywords": [ + "country", + "nation", + "bf" + ] }, "flag_bg": { "unicode": "1F1E7-1F1EC", @@ -4242,9 +8709,15 @@ "name": "bulgaria", "shortname": ":flag_bg:", "category": "flags", - "aliases": [":bg:"], + "aliases": [ + ":bg:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "bg"] + "keywords": [ + "country", + "nation", + "bg" + ] }, "flag_bh": { "unicode": "1F1E7-1F1ED", @@ -4252,9 +8725,16 @@ "name": "bahrain", "shortname": ":flag_bh:", "category": "flags", - "aliases": [":bh:"], + "aliases": [ + ":bh:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "al bahrayn", "bh"] + "keywords": [ + "country", + "nation", + "al bahrayn", + "bh" + ] }, "flag_bi": { "unicode": "1F1E7-1F1EE", @@ -4262,9 +8742,15 @@ "name": "burundi", "shortname": ":flag_bi:", "category": "flags", - "aliases": [":bi:"], + "aliases": [ + ":bi:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "bi"] + "keywords": [ + "country", + "nation", + "bi" + ] }, "flag_bj": { "unicode": "1F1E7-1F1EF", @@ -4272,9 +8758,27 @@ "name": "benin", "shortname": ":flag_bj:", "category": "flags", - "aliases": [":bj:"], + "aliases": [ + ":bj:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "bj"] + "keywords": [ + "country", + "nation", + "bj" + ] + }, + "flag_bl": { + "unicode": "1F1E7-1F1F1", + "unicode_alternates": "", + "name": "saint barthélemy", + "shortname": ":flag_bl:", + "category": "flags", + "aliases": [ + ":bl:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_black": { "unicode": "1F3F4", @@ -4282,9 +8786,14 @@ "name": "waving black flag", "shortname": ":flag_black:", "category": "objects_symbols", - "aliases": [":waving_black_flag:"], + "aliases": [ + ":waving_black_flag:" + ], "aliases_ascii": [], - "keywords": ["symbol", "signal"] + "keywords": [ + "symbol", + "signal" + ] }, "flag_bm": { "unicode": "1F1E7-1F1F2", @@ -4292,9 +8801,15 @@ "name": "bermuda", "shortname": ":flag_bm:", "category": "flags", - "aliases": [":bm:"], + "aliases": [ + ":bm:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "bm"] + "keywords": [ + "country", + "nation", + "bm" + ] }, "flag_bn": { "unicode": "1F1E7-1F1F3", @@ -4302,9 +8817,15 @@ "name": "brunei", "shortname": ":flag_bn:", "category": "flags", - "aliases": [":bn:"], + "aliases": [ + ":bn:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "bn"] + "keywords": [ + "country", + "nation", + "bn" + ] }, "flag_bo": { "unicode": "1F1E7-1F1F4", @@ -4312,9 +8833,27 @@ "name": "bolivia", "shortname": ":flag_bo:", "category": "flags", - "aliases": [":bo:"], + "aliases": [ + ":bo:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "bo"] + "keywords": [ + "country", + "nation", + "bo" + ] + }, + "flag_bq": { + "unicode": "1F1E7-1F1F6", + "unicode_alternates": "", + "name": "caribbean netherlands", + "shortname": ":flag_bq:", + "category": "flags", + "aliases": [ + ":bq:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_br": { "unicode": "1F1E7-1F1F7", @@ -4322,9 +8861,16 @@ "name": "brazil", "shortname": ":flag_br:", "category": "flags", - "aliases": [":br:"], + "aliases": [ + ":br:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "brasil", "br"] + "keywords": [ + "country", + "nation", + "brasil", + "br" + ] }, "flag_bs": { "unicode": "1F1E7-1F1F8", @@ -4332,9 +8878,15 @@ "name": "the bahamas", "shortname": ":flag_bs:", "category": "flags", - "aliases": [":bs:"], + "aliases": [ + ":bs:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "bs"] + "keywords": [ + "country", + "nation", + "bs" + ] }, "flag_bt": { "unicode": "1F1E7-1F1F9", @@ -4342,9 +8894,27 @@ "name": "bhutan", "shortname": ":flag_bt:", "category": "flags", - "aliases": [":bt:"], + "aliases": [ + ":bt:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "bt"] + "keywords": [ + "country", + "nation", + "bt" + ] + }, + "flag_bv": { + "unicode": "1F1E7-1F1FB", + "unicode_alternates": "", + "name": "bouvet island", + "shortname": ":flag_bv:", + "category": "flags", + "aliases": [ + ":bv:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_bw": { "unicode": "1F1E7-1F1FC", @@ -4352,9 +8922,15 @@ "name": "botswana", "shortname": ":flag_bw:", "category": "flags", - "aliases": [":bw:"], + "aliases": [ + ":bw:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "bw"] + "keywords": [ + "country", + "nation", + "bw" + ] }, "flag_by": { "unicode": "1F1E7-1F1FE", @@ -4362,9 +8938,16 @@ "name": "belarus", "shortname": ":flag_by:", "category": "flags", - "aliases": [":by:"], + "aliases": [ + ":by:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "byelarus", "by"] + "keywords": [ + "country", + "nation", + "byelarus", + "by" + ] }, "flag_bz": { "unicode": "1F1E7-1F1FF", @@ -4372,9 +8955,15 @@ "name": "belize", "shortname": ":flag_bz:", "category": "flags", - "aliases": [":bz:"], + "aliases": [ + ":bz:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "bz"] + "keywords": [ + "country", + "nation", + "bz" + ] }, "flag_ca": { "unicode": "1F1E8-1F1E6", @@ -4382,9 +8971,27 @@ "name": "canada", "shortname": ":flag_ca:", "category": "flags", - "aliases": [":ca:"], + "aliases": [ + ":ca:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ca"] + "keywords": [ + "country", + "nation", + "ca" + ] + }, + "flag_cc": { + "unicode": "1F1E8-1F1E8", + "unicode_alternates": "", + "name": "cocos (keeling) islands", + "shortname": ":flag_cc:", + "category": "flags", + "aliases": [ + ":cc:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_cd": { "unicode": "1F1E8-1F1E9", @@ -4392,9 +8999,17 @@ "name": "the democratic republic of the congo", "shortname": ":flag_cd:", "category": "flags", - "aliases": [":congo:"], + "aliases": [ + ":congo:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "république démocratique du congo", "republique democratique du congo", "cd"] + "keywords": [ + "country", + "nation", + "république démocratique du congo", + "republique democratique du congo", + "cd" + ] }, "flag_cf": { "unicode": "1F1E8-1F1EB", @@ -4402,9 +9017,15 @@ "name": "central african republic", "shortname": ":flag_cf:", "category": "flags", - "aliases": [":cf:"], + "aliases": [ + ":cf:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "cf"] + "keywords": [ + "country", + "nation", + "cf" + ] }, "flag_cg": { "unicode": "1F1E8-1F1EC", @@ -4412,9 +9033,15 @@ "name": "the republic of the congo", "shortname": ":flag_cg:", "category": "flags", - "aliases": [":cg:"], + "aliases": [ + ":cg:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "cg"] + "keywords": [ + "country", + "nation", + "cg" + ] }, "flag_ch": { "unicode": "1F1E8-1F1ED", @@ -4422,9 +9049,15 @@ "name": "switzerland", "shortname": ":flag_ch:", "category": "flags", - "aliases": [":ch:"], + "aliases": [ + ":ch:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "swiss"] + "keywords": [ + "country", + "nation", + "swiss" + ] }, "flag_ci": { "unicode": "1F1E8-1F1EE", @@ -4432,9 +9065,27 @@ "name": "cote d'ivoire", "shortname": ":flag_ci:", "category": "flags", - "aliases": [":ci:"], + "aliases": [ + ":ci:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ci"] + "keywords": [ + "country", + "nation", + "ci" + ] + }, + "flag_ck": { + "unicode": "1F1E8-1F1F0", + "unicode_alternates": "", + "name": "cook islands", + "shortname": ":flag_ck:", + "category": "flags", + "aliases": [ + ":ck:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_cl": { "unicode": "1F1E8-1F1F1", @@ -4442,9 +9093,15 @@ "name": "chile", "shortname": ":flag_cl:", "category": "flags", - "aliases": [":chile:"], + "aliases": [ + ":chile:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "cl"] + "keywords": [ + "country", + "nation", + "cl" + ] }, "flag_cm": { "unicode": "1F1E8-1F1F2", @@ -4452,9 +9109,15 @@ "name": "cameroon", "shortname": ":flag_cm:", "category": "flags", - "aliases": [":cm:"], + "aliases": [ + ":cm:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "cm"] + "keywords": [ + "country", + "nation", + "cm" + ] }, "flag_cn": { "unicode": "1F1E8-1F1F3", @@ -4462,9 +9125,18 @@ "name": "china", "shortname": ":flag_cn:", "category": "flags", - "aliases": [":cn:"], + "aliases": [ + ":cn:" + ], "aliases_ascii": [], - "keywords": ["chinese", "prc", "zhong guo", "country", "nation", "cn"] + "keywords": [ + "chinese", + "prc", + "zhong guo", + "country", + "nation", + "cn" + ] }, "flag_co": { "unicode": "1F1E8-1F1F4", @@ -4472,9 +9144,27 @@ "name": "colombia", "shortname": ":flag_co:", "category": "flags", - "aliases": [":co:"], + "aliases": [ + ":co:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "co"] + "keywords": [ + "country", + "nation", + "co" + ] + }, + "flag_cp": { + "unicode": "1F1E8-1F1F5", + "unicode_alternates": "", + "name": "clipperton island", + "shortname": ":flag_cp:", + "category": "flags", + "aliases": [ + ":cp:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_cr": { "unicode": "1F1E8-1F1F7", @@ -4482,9 +9172,15 @@ "name": "costa rica", "shortname": ":flag_cr:", "category": "flags", - "aliases": [":cr:"], + "aliases": [ + ":cr:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "cr"] + "keywords": [ + "country", + "nation", + "cr" + ] }, "flag_cu": { "unicode": "1F1E8-1F1FA", @@ -4492,9 +9188,15 @@ "name": "cuba", "shortname": ":flag_cu:", "category": "flags", - "aliases": [":cu:"], + "aliases": [ + ":cu:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "cu"] + "keywords": [ + "country", + "nation", + "cu" + ] }, "flag_cv": { "unicode": "1F1E8-1F1FB", @@ -4502,9 +9204,40 @@ "name": "cape verde", "shortname": ":flag_cv:", "category": "flags", - "aliases": [":cv:"], + "aliases": [ + ":cv:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "cabo verde", "cv"] + "keywords": [ + "country", + "nation", + "cabo verde", + "cv" + ] + }, + "flag_cw": { + "unicode": "1F1E8-1F1FC", + "unicode_alternates": "", + "name": "curaçao", + "shortname": ":flag_cw:", + "category": "flags", + "aliases": [ + ":cw:" + ], + "aliases_ascii": [], + "keywords": [] + }, + "flag_cx": { + "unicode": "1F1E8-1F1FD", + "unicode_alternates": "", + "name": "christmas island", + "shortname": ":flag_cx:", + "category": "flags", + "aliases": [ + ":cx:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_cy": { "unicode": "1F1E8-1F1FE", @@ -4512,9 +9245,17 @@ "name": "cyprus", "shortname": ":flag_cy:", "category": "flags", - "aliases": [":cy:"], + "aliases": [ + ":cy:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "kibris", "kypros", "cy"] + "keywords": [ + "country", + "nation", + "kibris", + "kypros", + "cy" + ] }, "flag_cz": { "unicode": "1F1E8-1F1FF", @@ -4522,9 +9263,16 @@ "name": "the czech republic", "shortname": ":flag_cz:", "category": "flags", - "aliases": [":cz:"], + "aliases": [ + ":cz:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ceska republika", "cz"] + "keywords": [ + "country", + "nation", + "ceska republika", + "cz" + ] }, "flag_de": { "unicode": "1F1E9-1F1EA", @@ -4532,9 +9280,29 @@ "name": "germany", "shortname": ":flag_de:", "category": "flags", - "aliases": [":de:"], + "aliases": [ + ":de:" + ], "aliases_ascii": [], - "keywords": ["german", "nation", "deutschland", "country", "de"] + "keywords": [ + "german", + "nation", + "deutschland", + "country", + "de" + ] + }, + "flag_dg": { + "unicode": "1F1E9-1F1EC", + "unicode_alternates": "", + "name": "diego garcia", + "shortname": ":flag_dg:", + "category": "flags", + "aliases": [ + ":dg:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_dj": { "unicode": "1F1E9-1F1EF", @@ -4542,9 +9310,15 @@ "name": "djibouti", "shortname": ":flag_dj:", "category": "flags", - "aliases": [":dj:"], + "aliases": [ + ":dj:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "dj"] + "keywords": [ + "country", + "nation", + "dj" + ] }, "flag_dk": { "unicode": "1F1E9-1F1F0", @@ -4552,9 +9326,16 @@ "name": "denmark", "shortname": ":flag_dk:", "category": "flags", - "aliases": [":dk:"], + "aliases": [ + ":dk:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "danmark", "dk"] + "keywords": [ + "country", + "nation", + "danmark", + "dk" + ] }, "flag_dm": { "unicode": "1F1E9-1F1F2", @@ -4562,9 +9343,15 @@ "name": "dominica", "shortname": ":flag_dm:", "category": "flags", - "aliases": [":dm:"], + "aliases": [ + ":dm:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "dm"] + "keywords": [ + "country", + "nation", + "dm" + ] }, "flag_do": { "unicode": "1F1E9-1F1F4", @@ -4572,9 +9359,15 @@ "name": "the dominican republic", "shortname": ":flag_do:", "category": "flags", - "aliases": [":do:"], + "aliases": [ + ":do:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "do"] + "keywords": [ + "country", + "nation", + "do" + ] }, "flag_dz": { "unicode": "1F1E9-1F1FF", @@ -4582,9 +9375,29 @@ "name": "algeria", "shortname": ":flag_dz:", "category": "flags", - "aliases": [":dz:"], + "aliases": [ + ":dz:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "al jaza'ir", "al jazair", "dz"] + "keywords": [ + "country", + "nation", + "al jaza'ir", + "al jazair", + "dz" + ] + }, + "flag_ea": { + "unicode": "1F1EA-1F1E6", + "unicode_alternates": "", + "name": "ceuta, melilla", + "shortname": ":flag_ea:", + "category": "flags", + "aliases": [ + ":ea:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_ec": { "unicode": "1F1EA-1F1E8", @@ -4592,9 +9405,15 @@ "name": "ecuador", "shortname": ":flag_ec:", "category": "flags", - "aliases": [":ec:"], + "aliases": [ + ":ec:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ec"] + "keywords": [ + "country", + "nation", + "ec" + ] }, "flag_ee": { "unicode": "1F1EA-1F1EA", @@ -4602,9 +9421,16 @@ "name": "estonia", "shortname": ":flag_ee:", "category": "flags", - "aliases": [":ee:"], + "aliases": [ + ":ee:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "eesti vabariik", "ee"] + "keywords": [ + "country", + "nation", + "eesti vabariik", + "ee" + ] }, "flag_eg": { "unicode": "1F1EA-1F1EC", @@ -4612,9 +9438,16 @@ "name": "egypt", "shortname": ":flag_eg:", "category": "flags", - "aliases": [":eg:"], + "aliases": [ + ":eg:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "misr", "eg"] + "keywords": [ + "country", + "nation", + "misr", + "eg" + ] }, "flag_eh": { "unicode": "1F1EA-1F1ED", @@ -4622,9 +9455,18 @@ "name": "western sahara", "shortname": ":flag_eh:", "category": "flags", - "aliases": [":eh:"], + "aliases": [ + ":eh:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "aṣ-Ṣaḥrā’ al-gharbīyah", "sahra", "gharbiyah", "eh"] + "keywords": [ + "country", + "nation", + "aṣ-Ṣaḥrā’ al-gharbīyah", + "sahra", + "gharbiyah", + "eh" + ] }, "flag_er": { "unicode": "1F1EA-1F1F7", @@ -4632,9 +9474,16 @@ "name": "eritrea", "shortname": ":flag_er:", "category": "flags", - "aliases": [":er:"], + "aliases": [ + ":er:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "hagere ertra", "er"] + "keywords": [ + "country", + "nation", + "hagere ertra", + "er" + ] }, "flag_es": { "unicode": "1F1EA-1F1F8", @@ -4642,9 +9491,17 @@ "name": "spain", "shortname": ":flag_es:", "category": "flags", - "aliases": [":es:"], + "aliases": [ + ":es:" + ], "aliases_ascii": [], - "keywords": ["nation", "españa", "country", "espana", "es"] + "keywords": [ + "nation", + "españa", + "country", + "espana", + "es" + ] }, "flag_et": { "unicode": "1F1EA-1F1F9", @@ -4652,9 +9509,29 @@ "name": "ethiopia", "shortname": ":flag_et:", "category": "flags", - "aliases": [":et:"], + "aliases": [ + ":et:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ityop'iya", "ityopiya", "et"] + "keywords": [ + "country", + "nation", + "ityop'iya", + "ityopiya", + "et" + ] + }, + "flag_eu": { + "unicode": "1F1EA-1F1FA", + "unicode_alternates": "", + "name": "european union", + "shortname": ":flag_eu:", + "category": "flags", + "aliases": [ + ":eu:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_fi": { "unicode": "1F1EB-1F1EE", @@ -4662,9 +9539,16 @@ "name": "finland", "shortname": ":flag_fi:", "category": "flags", - "aliases": [":fi:"], + "aliases": [ + ":fi:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "suomen tasavalta", "fi"] + "keywords": [ + "country", + "nation", + "suomen tasavalta", + "fi" + ] }, "flag_fj": { "unicode": "1F1EB-1F1EF", @@ -4672,9 +9556,15 @@ "name": "fiji", "shortname": ":flag_fj:", "category": "flags", - "aliases": [":fj:"], + "aliases": [ + ":fj:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "fj"] + "keywords": [ + "country", + "nation", + "fj" + ] }, "flag_fk": { "unicode": "1F1EB-1F1F0", @@ -4682,9 +9572,16 @@ "name": "falkland islands", "shortname": ":flag_fk:", "category": "flags", - "aliases": [":fk:"], + "aliases": [ + ":fk:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "islas malvinas", "fk"] + "keywords": [ + "country", + "nation", + "islas malvinas", + "fk" + ] }, "flag_fm": { "unicode": "1F1EB-1F1F2", @@ -4692,9 +9589,15 @@ "name": "micronesia", "shortname": ":flag_fm:", "category": "flags", - "aliases": [":fm:"], + "aliases": [ + ":fm:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "fm"] + "keywords": [ + "country", + "nation", + "fm" + ] }, "flag_fo": { "unicode": "1F1EB-1F1F4", @@ -4702,9 +9605,16 @@ "name": "faroe islands", "shortname": ":flag_fo:", "category": "flags", - "aliases": [":fo:"], + "aliases": [ + ":fo:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "foroyar", "fo"] + "keywords": [ + "country", + "nation", + "foroyar", + "fo" + ] }, "flag_fr": { "unicode": "1F1EB-1F1F7", @@ -4712,9 +9622,16 @@ "name": "france", "shortname": ":flag_fr:", "category": "flags", - "aliases": [":fr:"], + "aliases": [ + ":fr:" + ], "aliases_ascii": [], - "keywords": ["french", "nation", "country", "fr"] + "keywords": [ + "french", + "nation", + "country", + "fr" + ] }, "flag_ga": { "unicode": "1F1EC-1F1E6", @@ -4722,9 +9639,15 @@ "name": "gabon", "shortname": ":flag_ga:", "category": "flags", - "aliases": [":ga:"], + "aliases": [ + ":ga:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ga"] + "keywords": [ + "country", + "nation", + "ga" + ] }, "flag_gb": { "unicode": "1F1EC-1F1E7", @@ -4732,9 +9655,19 @@ "name": "great britain", "shortname": ":flag_gb:", "category": "flags", - "aliases": [":gb:"], + "aliases": [ + ":gb:" + ], "aliases_ascii": [], - "keywords": ["UK", "gb", "britsh", "nation", "united kingdom", "england", "country"] + "keywords": [ + "UK", + "gb", + "britsh", + "nation", + "united kingdom", + "england", + "country" + ] }, "flag_gd": { "unicode": "1F1EC-1F1E9", @@ -4742,9 +9675,15 @@ "name": "grenada", "shortname": ":flag_gd:", "category": "flags", - "aliases": [":gd:"], + "aliases": [ + ":gd:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "gd"] + "keywords": [ + "country", + "nation", + "gd" + ] }, "flag_ge": { "unicode": "1F1EC-1F1EA", @@ -4752,9 +9691,41 @@ "name": "georgia", "shortname": ":flag_ge:", "category": "flags", - "aliases": [":ge:"], + "aliases": [ + ":ge:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "sak'art'velo", "sakartvelo", "ge"] + "keywords": [ + "country", + "nation", + "sak'art'velo", + "sakartvelo", + "ge" + ] + }, + "flag_gf": { + "unicode": "1F1EC-1F1EB", + "unicode_alternates": "", + "name": "french guiana", + "shortname": ":flag_gf:", + "category": "flags", + "aliases": [ + ":gf:" + ], + "aliases_ascii": [], + "keywords": [] + }, + "flag_gg": { + "unicode": "1F1EC-1F1EC", + "unicode_alternates": "", + "name": "guernsey", + "shortname": ":flag_gg:", + "category": "flags", + "aliases": [ + ":gg:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_gh": { "unicode": "1F1EC-1F1ED", @@ -4762,9 +9733,15 @@ "name": "ghana", "shortname": ":flag_gh:", "category": "flags", - "aliases": [":gh:"], + "aliases": [ + ":gh:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "gh"] + "keywords": [ + "country", + "nation", + "gh" + ] }, "flag_gi": { "unicode": "1F1EC-1F1EE", @@ -4772,9 +9749,15 @@ "name": "gibraltar", "shortname": ":flag_gi:", "category": "flags", - "aliases": [":gi:"], + "aliases": [ + ":gi:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "gi"] + "keywords": [ + "country", + "nation", + "gi" + ] }, "flag_gl": { "unicode": "1F1EC-1F1F1", @@ -4782,9 +9765,16 @@ "name": "greenland", "shortname": ":flag_gl:", "category": "flags", - "aliases": [":gl:"], + "aliases": [ + ":gl:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "kalaallit nunaat", "gl"] + "keywords": [ + "country", + "nation", + "kalaallit nunaat", + "gl" + ] }, "flag_gm": { "unicode": "1F1EC-1F1F2", @@ -4792,9 +9782,15 @@ "name": "the gambia", "shortname": ":flag_gm:", "category": "flags", - "aliases": [":gm:"], + "aliases": [ + ":gm:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "gm"] + "keywords": [ + "country", + "nation", + "gm" + ] }, "flag_gn": { "unicode": "1F1EC-1F1F3", @@ -4802,9 +9798,28 @@ "name": "guinea", "shortname": ":flag_gn:", "category": "flags", - "aliases": [":gn:"], + "aliases": [ + ":gn:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "guinee", "gn"] + "keywords": [ + "country", + "nation", + "guinee", + "gn" + ] + }, + "flag_gp": { + "unicode": "1F1EC-1F1F5", + "unicode_alternates": "", + "name": "guadeloupe", + "shortname": ":flag_gp:", + "category": "flags", + "aliases": [ + ":gp:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_gq": { "unicode": "1F1EC-1F1F6", @@ -4812,9 +9827,16 @@ "name": "equatorial guinea", "shortname": ":flag_gq:", "category": "flags", - "aliases": [":gq:"], + "aliases": [ + ":gq:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "guinea ecuatorial", "gq"] + "keywords": [ + "country", + "nation", + "guinea ecuatorial", + "gq" + ] }, "flag_gr": { "unicode": "1F1EC-1F1F7", @@ -4822,9 +9844,29 @@ "name": "greece", "shortname": ":flag_gr:", "category": "flags", - "aliases": [":gr:"], + "aliases": [ + ":gr:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ellas", "ellada", "gr"] + "keywords": [ + "country", + "nation", + "ellas", + "ellada", + "gr" + ] + }, + "flag_gs": { + "unicode": "1F1EC-1F1F8", + "unicode_alternates": "", + "name": "south georgia", + "shortname": ":flag_gs:", + "category": "flags", + "aliases": [ + ":gs:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_gt": { "unicode": "1F1EC-1F1F9", @@ -4832,9 +9874,15 @@ "name": "guatemala", "shortname": ":flag_gt:", "category": "flags", - "aliases": [":gt:"], + "aliases": [ + ":gt:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "gt"] + "keywords": [ + "country", + "nation", + "gt" + ] }, "flag_gu": { "unicode": "1F1EC-1F1FA", @@ -4842,9 +9890,15 @@ "name": "guam", "shortname": ":flag_gu:", "category": "flags", - "aliases": [":gu:"], + "aliases": [ + ":gu:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "gu"] + "keywords": [ + "country", + "nation", + "gu" + ] }, "flag_gw": { "unicode": "1F1EC-1F1FC", @@ -4852,9 +9906,17 @@ "name": "guinea-bissau", "shortname": ":flag_gw:", "category": "flags", - "aliases": [":gw:"], + "aliases": [ + ":gw:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "guine-bissau", "guine bissau", "gw"] + "keywords": [ + "country", + "nation", + "guine-bissau", + "guine bissau", + "gw" + ] }, "flag_gy": { "unicode": "1F1EC-1F1FE", @@ -4862,9 +9924,15 @@ "name": "guyana", "shortname": ":flag_gy:", "category": "flags", - "aliases": [":gy:"], + "aliases": [ + ":gy:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "gy"] + "keywords": [ + "country", + "nation", + "gy" + ] }, "flag_hk": { "unicode": "1F1ED-1F1F0", @@ -4872,9 +9940,28 @@ "name": "hong kong", "shortname": ":flag_hk:", "category": "flags", - "aliases": [":hk:"], + "aliases": [ + ":hk:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "xianggang", "hk"] + "keywords": [ + "country", + "nation", + "xianggang", + "hk" + ] + }, + "flag_hm": { + "unicode": "1F1ED-1F1F2", + "unicode_alternates": "", + "name": "heard island and mcdonald islands", + "shortname": ":flag_hm:", + "category": "flags", + "aliases": [ + ":hm:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_hn": { "unicode": "1F1ED-1F1F3", @@ -4882,9 +9969,15 @@ "name": "honduras", "shortname": ":flag_hn:", "category": "flags", - "aliases": [":hn:"], + "aliases": [ + ":hn:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "hn"] + "keywords": [ + "country", + "nation", + "hn" + ] }, "flag_hr": { "unicode": "1F1ED-1F1F7", @@ -4892,9 +9985,16 @@ "name": "croatia", "shortname": ":flag_hr:", "category": "flags", - "aliases": [":hr:"], + "aliases": [ + ":hr:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "hrvatska", "hr"] + "keywords": [ + "country", + "nation", + "hrvatska", + "hr" + ] }, "flag_ht": { "unicode": "1F1ED-1F1F9", @@ -4902,9 +10002,15 @@ "name": "haiti", "shortname": ":flag_ht:", "category": "flags", - "aliases": [":ht:"], + "aliases": [ + ":ht:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ht"] + "keywords": [ + "country", + "nation", + "ht" + ] }, "flag_hu": { "unicode": "1F1ED-1F1FA", @@ -4912,9 +10018,28 @@ "name": "hungary", "shortname": ":flag_hu:", "category": "flags", - "aliases": [":hu:"], + "aliases": [ + ":hu:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "magyarorszag", "hu"] + "keywords": [ + "country", + "nation", + "magyarorszag", + "hu" + ] + }, + "flag_ic": { + "unicode": "1F1EE-1F1E8", + "unicode_alternates": "", + "name": "canary islands", + "shortname": ":flag_ic:", + "category": "flags", + "aliases": [ + ":ic:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_id": { "unicode": "1F1EE-1F1E9", @@ -4922,9 +10047,15 @@ "name": "indonesia", "shortname": ":flag_id:", "category": "flags", - "aliases": [":indonesia:"], + "aliases": [ + ":indonesia:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "id"] + "keywords": [ + "country", + "nation", + "id" + ] }, "flag_ie": { "unicode": "1F1EE-1F1EA", @@ -4932,9 +10063,17 @@ "name": "ireland", "shortname": ":flag_ie:", "category": "flags", - "aliases": [":ie:"], + "aliases": [ + ":ie:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "éire", "eire", "ie"] + "keywords": [ + "country", + "nation", + "éire", + "eire", + "ie" + ] }, "flag_il": { "unicode": "1F1EE-1F1F1", @@ -4942,9 +10081,29 @@ "name": "israel", "shortname": ":flag_il:", "category": "flags", - "aliases": [":il:"], + "aliases": [ + ":il:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "yisra'el", "yisrael", "il"] + "keywords": [ + "country", + "nation", + "yisra'el", + "yisrael", + "il" + ] + }, + "flag_im": { + "unicode": "1F1EE-1F1F2", + "unicode_alternates": "", + "name": "isle of man", + "shortname": ":flag_im:", + "category": "flags", + "aliases": [ + ":im:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_in": { "unicode": "1F1EE-1F1F3", @@ -4952,9 +10111,28 @@ "name": "india", "shortname": ":flag_in:", "category": "flags", - "aliases": [":in:"], + "aliases": [ + ":in:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "bharat", "in"] + "keywords": [ + "country", + "nation", + "bharat", + "in" + ] + }, + "flag_io": { + "unicode": "1F1EE-1F1F4", + "unicode_alternates": "", + "name": "british indian ocean territory", + "shortname": ":flag_io:", + "category": "flags", + "aliases": [ + ":io:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_iq": { "unicode": "1F1EE-1F1F6", @@ -4962,9 +10140,15 @@ "name": "iraq", "shortname": ":flag_iq:", "category": "flags", - "aliases": [":iq:"], + "aliases": [ + ":iq:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "iq"] + "keywords": [ + "country", + "nation", + "iq" + ] }, "flag_ir": { "unicode": "1F1EE-1F1F7", @@ -4972,9 +10156,15 @@ "name": "iran", "shortname": ":flag_ir:", "category": "flags", - "aliases": [":ir:"], + "aliases": [ + ":ir:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ir"] + "keywords": [ + "country", + "nation", + "ir" + ] }, "flag_is": { "unicode": "1F1EE-1F1F8", @@ -4982,9 +10172,16 @@ "name": "iceland", "shortname": ":flag_is:", "category": "flags", - "aliases": [":is:"], + "aliases": [ + ":is:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "lyoveldio island", "is"] + "keywords": [ + "country", + "nation", + "lyoveldio island", + "is" + ] }, "flag_it": { "unicode": "1F1EE-1F1F9", @@ -4992,9 +10189,16 @@ "name": "italy", "shortname": ":flag_it:", "category": "flags", - "aliases": [":it:"], + "aliases": [ + ":it:" + ], "aliases_ascii": [], - "keywords": ["italia", "country", "nation", "it"] + "keywords": [ + "italia", + "country", + "nation", + "it" + ] }, "flag_je": { "unicode": "1F1EF-1F1EA", @@ -5002,9 +10206,15 @@ "name": "jersey", "shortname": ":flag_je:", "category": "flags", - "aliases": [":je:"], + "aliases": [ + ":je:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "je"] + "keywords": [ + "country", + "nation", + "je" + ] }, "flag_jm": { "unicode": "1F1EF-1F1F2", @@ -5012,9 +10222,15 @@ "name": "jamaica", "shortname": ":flag_jm:", "category": "flags", - "aliases": [":jm:"], + "aliases": [ + ":jm:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "jm"] + "keywords": [ + "country", + "nation", + "jm" + ] }, "flag_jo": { "unicode": "1F1EF-1F1F4", @@ -5022,9 +10238,16 @@ "name": "jordan", "shortname": ":flag_jo:", "category": "flags", - "aliases": [":jo:"], + "aliases": [ + ":jo:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "al urdun", "jo"] + "keywords": [ + "country", + "nation", + "al urdun", + "jo" + ] }, "flag_jp": { "unicode": "1F1EF-1F1F5", @@ -5032,9 +10255,16 @@ "name": "japan", "shortname": ":flag_jp:", "category": "flags", - "aliases": [":jp:"], + "aliases": [ + ":jp:" + ], "aliases_ascii": [], - "keywords": ["nation", "nippon", "country", "jp"] + "keywords": [ + "nation", + "nippon", + "country", + "jp" + ] }, "flag_ke": { "unicode": "1F1F0-1F1EA", @@ -5042,9 +10272,15 @@ "name": "kenya", "shortname": ":flag_ke:", "category": "flags", - "aliases": [":ke:"], + "aliases": [ + ":ke:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ke"] + "keywords": [ + "country", + "nation", + "ke" + ] }, "flag_kg": { "unicode": "1F1F0-1F1EC", @@ -5052,9 +10288,16 @@ "name": "kyrgyzstan", "shortname": ":flag_kg:", "category": "flags", - "aliases": [":kg:"], + "aliases": [ + ":kg:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "kyrgyz respublikasy", "kg"] + "keywords": [ + "country", + "nation", + "kyrgyz respublikasy", + "kg" + ] }, "flag_kh": { "unicode": "1F1F0-1F1ED", @@ -5062,9 +10305,16 @@ "name": "cambodia", "shortname": ":flag_kh:", "category": "flags", - "aliases": [":kh:"], + "aliases": [ + ":kh:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "kampuchea", "kh"] + "keywords": [ + "country", + "nation", + "kampuchea", + "kh" + ] }, "flag_ki": { "unicode": "1F1F0-1F1EE", @@ -5072,9 +10322,17 @@ "name": "kiribati", "shortname": ":flag_ki:", "category": "flags", - "aliases": [":ki:"], + "aliases": [ + ":ki:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "kiribati", "kiribas", "ki"] + "keywords": [ + "country", + "nation", + "kiribati", + "kiribas", + "ki" + ] }, "flag_km": { "unicode": "1F1F0-1F1F2", @@ -5082,9 +10340,15 @@ "name": "the comoros", "shortname": ":flag_km:", "category": "flags", - "aliases": [":km:"], + "aliases": [ + ":km:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "km"] + "keywords": [ + "country", + "nation", + "km" + ] }, "flag_kn": { "unicode": "1F1F0-1F1F3", @@ -5092,9 +10356,15 @@ "name": "saint kitts and nevis", "shortname": ":flag_kn:", "category": "flags", - "aliases": [":kn:"], + "aliases": [ + ":kn:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "kn"] + "keywords": [ + "country", + "nation", + "kn" + ] }, "flag_kp": { "unicode": "1F1F0-1F1F5", @@ -5102,9 +10372,15 @@ "name": "north korea", "shortname": ":flag_kp:", "category": "flags", - "aliases": [":kp:"], + "aliases": [ + ":kp:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "kp"] + "keywords": [ + "country", + "nation", + "kp" + ] }, "flag_kr": { "unicode": "1F1F0-1F1F7", @@ -5112,9 +10388,16 @@ "name": "korea", "shortname": ":flag_kr:", "category": "flags", - "aliases": [":kr:"], + "aliases": [ + ":kr:" + ], "aliases_ascii": [], - "keywords": ["nation", "country", "south korea", "kr"] + "keywords": [ + "nation", + "country", + "south korea", + "kr" + ] }, "flag_kw": { "unicode": "1F1F0-1F1FC", @@ -5122,9 +10405,16 @@ "name": "kuwait", "shortname": ":flag_kw:", "category": "flags", - "aliases": [":kw:"], + "aliases": [ + ":kw:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "al kuwayt", "kw"] + "keywords": [ + "country", + "nation", + "al kuwayt", + "kw" + ] }, "flag_ky": { "unicode": "1F1F0-1F1FE", @@ -5132,9 +10422,15 @@ "name": "cayman islands", "shortname": ":flag_ky:", "category": "flags", - "aliases": [":ky:"], + "aliases": [ + ":ky:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ky"] + "keywords": [ + "country", + "nation", + "ky" + ] }, "flag_kz": { "unicode": "1F1F0-1F1FF", @@ -5142,9 +10438,16 @@ "name": "kazakhstan", "shortname": ":flag_kz:", "category": "flags", - "aliases": [":kz:"], + "aliases": [ + ":kz:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "qazaqstan", "kz"] + "keywords": [ + "country", + "nation", + "qazaqstan", + "kz" + ] }, "flag_la": { "unicode": "1F1F1-1F1E6", @@ -5152,9 +10455,15 @@ "name": "laos", "shortname": ":flag_la:", "category": "flags", - "aliases": [":la:"], + "aliases": [ + ":la:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "la"] + "keywords": [ + "country", + "nation", + "la" + ] }, "flag_lb": { "unicode": "1F1F1-1F1E7", @@ -5162,9 +10471,16 @@ "name": "lebanon", "shortname": ":flag_lb:", "category": "flags", - "aliases": [":lb:"], + "aliases": [ + ":lb:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "lubnan", "lb"] + "keywords": [ + "country", + "nation", + "lubnan", + "lb" + ] }, "flag_lc": { "unicode": "1F1F1-1F1E8", @@ -5172,9 +10488,15 @@ "name": "saint lucia", "shortname": ":flag_lc:", "category": "flags", - "aliases": [":lc:"], + "aliases": [ + ":lc:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "lc"] + "keywords": [ + "country", + "nation", + "lc" + ] }, "flag_li": { "unicode": "1F1F1-1F1EE", @@ -5182,9 +10504,15 @@ "name": "liechtenstein", "shortname": ":flag_li:", "category": "flags", - "aliases": [":li:"], + "aliases": [ + ":li:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "li"] + "keywords": [ + "country", + "nation", + "li" + ] }, "flag_lk": { "unicode": "1F1F1-1F1F0", @@ -5192,9 +10520,15 @@ "name": "sri lanka", "shortname": ":flag_lk:", "category": "flags", - "aliases": [":lk:"], + "aliases": [ + ":lk:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "lk"] + "keywords": [ + "country", + "nation", + "lk" + ] }, "flag_lr": { "unicode": "1F1F1-1F1F7", @@ -5202,9 +10536,15 @@ "name": "liberia", "shortname": ":flag_lr:", "category": "flags", - "aliases": [":lr:"], + "aliases": [ + ":lr:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "lr"] + "keywords": [ + "country", + "nation", + "lr" + ] }, "flag_ls": { "unicode": "1F1F1-1F1F8", @@ -5212,9 +10552,15 @@ "name": "lesotho", "shortname": ":flag_ls:", "category": "flags", - "aliases": [":ls:"], + "aliases": [ + ":ls:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ls"] + "keywords": [ + "country", + "nation", + "ls" + ] }, "flag_lt": { "unicode": "1F1F1-1F1F9", @@ -5222,9 +10568,16 @@ "name": "lithuania", "shortname": ":flag_lt:", "category": "flags", - "aliases": [":lt:"], + "aliases": [ + ":lt:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "lietuva", "lt"] + "keywords": [ + "country", + "nation", + "lietuva", + "lt" + ] }, "flag_lu": { "unicode": "1F1F1-1F1FA", @@ -5232,9 +10585,17 @@ "name": "luxembourg", "shortname": ":flag_lu:", "category": "flags", - "aliases": [":lu:"], + "aliases": [ + ":lu:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "luxembourg", "letzebuerg", "lu"] + "keywords": [ + "country", + "nation", + "luxembourg", + "letzebuerg", + "lu" + ] }, "flag_lv": { "unicode": "1F1F1-1F1FB", @@ -5242,9 +10603,16 @@ "name": "latvia", "shortname": ":flag_lv:", "category": "flags", - "aliases": [":lv:"], + "aliases": [ + ":lv:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "latvija", "lv"] + "keywords": [ + "country", + "nation", + "latvija", + "lv" + ] }, "flag_ly": { "unicode": "1F1F1-1F1FE", @@ -5252,9 +10620,16 @@ "name": "libya", "shortname": ":flag_ly:", "category": "flags", - "aliases": [":ly:"], + "aliases": [ + ":ly:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "libiyah", "ly"] + "keywords": [ + "country", + "nation", + "libiyah", + "ly" + ] }, "flag_ma": { "unicode": "1F1F2-1F1E6", @@ -5262,9 +10637,16 @@ "name": "morocco", "shortname": ":flag_ma:", "category": "flags", - "aliases": [":ma:"], + "aliases": [ + ":ma:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "al maghrib", "ma"] + "keywords": [ + "country", + "nation", + "al maghrib", + "ma" + ] }, "flag_mc": { "unicode": "1F1F2-1F1E8", @@ -5272,9 +10654,15 @@ "name": "monaco", "shortname": ":flag_mc:", "category": "flags", - "aliases": [":mc:"], + "aliases": [ + ":mc:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "mc"] + "keywords": [ + "country", + "nation", + "mc" + ] }, "flag_md": { "unicode": "1F1F2-1F1E9", @@ -5282,9 +10670,15 @@ "name": "moldova", "shortname": ":flag_md:", "category": "flags", - "aliases": [":md:"], + "aliases": [ + ":md:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "md"] + "keywords": [ + "country", + "nation", + "md" + ] }, "flag_me": { "unicode": "1F1F2-1F1EA", @@ -5292,9 +10686,28 @@ "name": "montenegro", "shortname": ":flag_me:", "category": "flags", - "aliases": [":me:"], + "aliases": [ + ":me:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "crna gora", "me"] + "keywords": [ + "country", + "nation", + "crna gora", + "me" + ] + }, + "flag_mf": { + "unicode": "1F1F2-1F1EB", + "unicode_alternates": "", + "name": "saint martin", + "shortname": ":flag_mf:", + "category": "flags", + "aliases": [ + ":mf:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_mg": { "unicode": "1F1F2-1F1EC", @@ -5302,9 +10715,15 @@ "name": "madagascar", "shortname": ":flag_mg:", "category": "flags", - "aliases": [":mg:"], + "aliases": [ + ":mg:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "mg"] + "keywords": [ + "country", + "nation", + "mg" + ] }, "flag_mh": { "unicode": "1F1F2-1F1ED", @@ -5312,9 +10731,15 @@ "name": "the marshall islands", "shortname": ":flag_mh:", "category": "flags", - "aliases": [":mh:"], + "aliases": [ + ":mh:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "mh"] + "keywords": [ + "country", + "nation", + "mh" + ] }, "flag_mk": { "unicode": "1F1F2-1F1F0", @@ -5322,9 +10747,15 @@ "name": "macedonia", "shortname": ":flag_mk:", "category": "flags", - "aliases": [":mk:"], + "aliases": [ + ":mk:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "mk"] + "keywords": [ + "country", + "nation", + "mk" + ] }, "flag_ml": { "unicode": "1F1F2-1F1F1", @@ -5332,9 +10763,15 @@ "name": "mali", "shortname": ":flag_ml:", "category": "flags", - "aliases": [":ml:"], + "aliases": [ + ":ml:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ml"] + "keywords": [ + "country", + "nation", + "ml" + ] }, "flag_mm": { "unicode": "1F1F2-1F1F2", @@ -5342,9 +10779,16 @@ "name": "myanmar", "shortname": ":flag_mm:", "category": "flags", - "aliases": [":mm:"], + "aliases": [ + ":mm:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "myanma naingngandaw", "mm"] + "keywords": [ + "country", + "nation", + "myanma naingngandaw", + "mm" + ] }, "flag_mn": { "unicode": "1F1F2-1F1F3", @@ -5352,9 +10796,16 @@ "name": "mongolia", "shortname": ":flag_mn:", "category": "flags", - "aliases": [":mn:"], + "aliases": [ + ":mn:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "mongol uls", "mn"] + "keywords": [ + "country", + "nation", + "mongol uls", + "mn" + ] }, "flag_mo": { "unicode": "1F1F2-1F1F4", @@ -5362,9 +10813,40 @@ "name": "macau", "shortname": ":flag_mo:", "category": "flags", - "aliases": [":mo:"], + "aliases": [ + ":mo:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "aomen", "mo"] + "keywords": [ + "country", + "nation", + "aomen", + "mo" + ] + }, + "flag_mp": { + "unicode": "1F1F2-1F1F5", + "unicode_alternates": "", + "name": "northern mariana islands", + "shortname": ":flag_mp:", + "category": "flags", + "aliases": [ + ":mp:" + ], + "aliases_ascii": [], + "keywords": [] + }, + "flag_mq": { + "unicode": "1F1F2-1F1F6", + "unicode_alternates": "", + "name": "martinique", + "shortname": ":flag_mq:", + "category": "flags", + "aliases": [ + ":mq:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_mr": { "unicode": "1F1F2-1F1F7", @@ -5372,9 +10854,16 @@ "name": "mauritania", "shortname": ":flag_mr:", "category": "flags", - "aliases": [":mr:"], + "aliases": [ + ":mr:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "muritaniyah", "mr"] + "keywords": [ + "country", + "nation", + "muritaniyah", + "mr" + ] }, "flag_ms": { "unicode": "1F1F2-1F1F8", @@ -5382,9 +10871,15 @@ "name": "montserrat", "shortname": ":flag_ms:", "category": "flags", - "aliases": [":ms:"], + "aliases": [ + ":ms:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ms"] + "keywords": [ + "country", + "nation", + "ms" + ] }, "flag_mt": { "unicode": "1F1F2-1F1F9", @@ -5392,9 +10887,15 @@ "name": "malta", "shortname": ":flag_mt:", "category": "flags", - "aliases": [":mt:"], + "aliases": [ + ":mt:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "mt"] + "keywords": [ + "country", + "nation", + "mt" + ] }, "flag_mu": { "unicode": "1F1F2-1F1FA", @@ -5402,9 +10903,15 @@ "name": "mauritius", "shortname": ":flag_mu:", "category": "flags", - "aliases": [":mu:"], + "aliases": [ + ":mu:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "mu"] + "keywords": [ + "country", + "nation", + "mu" + ] }, "flag_mv": { "unicode": "1F1F2-1F1FB", @@ -5412,9 +10919,16 @@ "name": "maldives", "shortname": ":flag_mv:", "category": "flags", - "aliases": [":mv:"], + "aliases": [ + ":mv:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "dhivehi raajje", "mv"] + "keywords": [ + "country", + "nation", + "dhivehi raajje", + "mv" + ] }, "flag_mw": { "unicode": "1F1F2-1F1FC", @@ -5422,9 +10936,15 @@ "name": "malawi", "shortname": ":flag_mw:", "category": "flags", - "aliases": [":mw:"], + "aliases": [ + ":mw:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "mw"] + "keywords": [ + "country", + "nation", + "mw" + ] }, "flag_mx": { "unicode": "1F1F2-1F1FD", @@ -5432,9 +10952,15 @@ "name": "mexico", "shortname": ":flag_mx:", "category": "flags", - "aliases": [":mx:"], + "aliases": [ + ":mx:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "mx"] + "keywords": [ + "country", + "nation", + "mx" + ] }, "flag_my": { "unicode": "1F1F2-1F1FE", @@ -5442,9 +10968,15 @@ "name": "malaysia", "shortname": ":flag_my:", "category": "flags", - "aliases": [":my:"], + "aliases": [ + ":my:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "my"] + "keywords": [ + "country", + "nation", + "my" + ] }, "flag_mz": { "unicode": "1F1F2-1F1FF", @@ -5452,9 +10984,16 @@ "name": "mozambique", "shortname": ":flag_mz:", "category": "flags", - "aliases": [":mz:"], + "aliases": [ + ":mz:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "mocambique", "mz"] + "keywords": [ + "country", + "nation", + "mocambique", + "mz" + ] }, "flag_na": { "unicode": "1F1F3-1F1E6", @@ -5462,9 +11001,15 @@ "name": "namibia", "shortname": ":flag_na:", "category": "flags", - "aliases": [":na:"], + "aliases": [ + ":na:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "na"] + "keywords": [ + "country", + "nation", + "na" + ] }, "flag_nc": { "unicode": "1F1F3-1F1E8", @@ -5472,9 +11017,18 @@ "name": "new caledonia", "shortname": ":flag_nc:", "category": "flags", - "aliases": [":nc:"], + "aliases": [ + ":nc:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "nouvelle", "calédonie", "caledonie", "nc"] + "keywords": [ + "country", + "nation", + "nouvelle", + "calédonie", + "caledonie", + "nc" + ] }, "flag_ne": { "unicode": "1F1F3-1F1EA", @@ -5482,9 +11036,27 @@ "name": "niger", "shortname": ":flag_ne:", "category": "flags", - "aliases": [":ne:"], + "aliases": [ + ":ne:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ne"] + "keywords": [ + "country", + "nation", + "ne" + ] + }, + "flag_nf": { + "unicode": "1F1F3-1F1EB", + "unicode_alternates": "", + "name": "norfolk island", + "shortname": ":flag_nf:", + "category": "flags", + "aliases": [ + ":nf:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_ng": { "unicode": "1F1F3-1F1EC", @@ -5492,9 +11064,15 @@ "name": "nigeria", "shortname": ":flag_ng:", "category": "flags", - "aliases": [":nigeria:"], + "aliases": [ + ":nigeria:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ng"] + "keywords": [ + "country", + "nation", + "ng" + ] }, "flag_ni": { "unicode": "1F1F3-1F1EE", @@ -5502,9 +11080,15 @@ "name": "nicaragua", "shortname": ":flag_ni:", "category": "flags", - "aliases": [":ni:"], + "aliases": [ + ":ni:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ni"] + "keywords": [ + "country", + "nation", + "ni" + ] }, "flag_nl": { "unicode": "1F1F3-1F1F1", @@ -5512,9 +11096,17 @@ "name": "the netherlands", "shortname": ":flag_nl:", "category": "flags", - "aliases": [":nl:"], + "aliases": [ + ":nl:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "nederland", "holland", "nl"] + "keywords": [ + "country", + "nation", + "nederland", + "holland", + "nl" + ] }, "flag_no": { "unicode": "1F1F3-1F1F4", @@ -5522,9 +11114,16 @@ "name": "norway", "shortname": ":flag_no:", "category": "flags", - "aliases": [":no:"], + "aliases": [ + ":no:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "norge", "no"] + "keywords": [ + "country", + "nation", + "norge", + "no" + ] }, "flag_np": { "unicode": "1F1F3-1F1F5", @@ -5532,9 +11131,15 @@ "name": "nepal", "shortname": ":flag_np:", "category": "flags", - "aliases": [":np:"], + "aliases": [ + ":np:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "np"] + "keywords": [ + "country", + "nation", + "np" + ] }, "flag_nr": { "unicode": "1F1F3-1F1F7", @@ -5542,9 +11147,15 @@ "name": "nauru", "shortname": ":flag_nr:", "category": "flags", - "aliases": [":nr:"], + "aliases": [ + ":nr:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "nr"] + "keywords": [ + "country", + "nation", + "nr" + ] }, "flag_nu": { "unicode": "1F1F3-1F1FA", @@ -5552,9 +11163,15 @@ "name": "niue", "shortname": ":flag_nu:", "category": "flags", - "aliases": [":nu:"], + "aliases": [ + ":nu:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "nu"] + "keywords": [ + "country", + "nation", + "nu" + ] }, "flag_nz": { "unicode": "1F1F3-1F1FF", @@ -5562,9 +11179,16 @@ "name": "new zealand", "shortname": ":flag_nz:", "category": "flags", - "aliases": [":nz:"], + "aliases": [ + ":nz:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "aotearoa", "nz"] + "keywords": [ + "country", + "nation", + "aotearoa", + "nz" + ] }, "flag_om": { "unicode": "1F1F4-1F1F2", @@ -5572,9 +11196,16 @@ "name": "oman", "shortname": ":flag_om:", "category": "flags", - "aliases": [":om:"], + "aliases": [ + ":om:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "saltanat uman", "om"] + "keywords": [ + "country", + "nation", + "saltanat uman", + "om" + ] }, "flag_pa": { "unicode": "1F1F5-1F1E6", @@ -5582,9 +11213,15 @@ "name": "panama", "shortname": ":flag_pa:", "category": "flags", - "aliases": [":pa:"], + "aliases": [ + ":pa:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "pa"] + "keywords": [ + "country", + "nation", + "pa" + ] }, "flag_pe": { "unicode": "1F1F5-1F1EA", @@ -5592,9 +11229,15 @@ "name": "peru", "shortname": ":flag_pe:", "category": "flags", - "aliases": [":pe:"], + "aliases": [ + ":pe:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "pe"] + "keywords": [ + "country", + "nation", + "pe" + ] }, "flag_pf": { "unicode": "1F1F5-1F1EB", @@ -5602,9 +11245,17 @@ "name": "french polynesia", "shortname": ":flag_pf:", "category": "flags", - "aliases": [":pf:"], + "aliases": [ + ":pf:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "polynésie française", "polynesie francaise", "pf"] + "keywords": [ + "country", + "nation", + "polynésie française", + "polynesie francaise", + "pf" + ] }, "flag_pg": { "unicode": "1F1F5-1F1EC", @@ -5612,9 +11263,16 @@ "name": "papua new guinea", "shortname": ":flag_pg:", "category": "flags", - "aliases": [":pg:"], + "aliases": [ + ":pg:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "papua niu gini", "pg"] + "keywords": [ + "country", + "nation", + "papua niu gini", + "pg" + ] }, "flag_ph": { "unicode": "1F1F5-1F1ED", @@ -5622,9 +11280,16 @@ "name": "the philippines", "shortname": ":flag_ph:", "category": "flags", - "aliases": [":ph:"], + "aliases": [ + ":ph:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "pilipinas", "ph"] + "keywords": [ + "country", + "nation", + "pilipinas", + "ph" + ] }, "flag_pk": { "unicode": "1F1F5-1F1F0", @@ -5632,9 +11297,15 @@ "name": "pakistan", "shortname": ":flag_pk:", "category": "flags", - "aliases": [":pk:"], + "aliases": [ + ":pk:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "pk"] + "keywords": [ + "country", + "nation", + "pk" + ] }, "flag_pl": { "unicode": "1F1F5-1F1F1", @@ -5642,9 +11313,40 @@ "name": "poland", "shortname": ":flag_pl:", "category": "flags", - "aliases": [":pl:"], + "aliases": [ + ":pl:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "polska", "pl"] + "keywords": [ + "country", + "nation", + "polska", + "pl" + ] + }, + "flag_pm": { + "unicode": "1F1F5-1F1F2", + "unicode_alternates": "", + "name": "saint pierre and miquelon", + "shortname": ":flag_pm:", + "category": "flags", + "aliases": [ + ":pm:" + ], + "aliases_ascii": [], + "keywords": [] + }, + "flag_pn": { + "unicode": "1F1F5-1F1F3", + "unicode_alternates": "", + "name": "pitcairn", + "shortname": ":flag_pn:", + "category": "flags", + "aliases": [ + ":pn:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_pr": { "unicode": "1F1F5-1F1F7", @@ -5652,9 +11354,15 @@ "name": "puerto rico", "shortname": ":flag_pr:", "category": "flags", - "aliases": [":pr:"], + "aliases": [ + ":pr:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "pr"] + "keywords": [ + "country", + "nation", + "pr" + ] }, "flag_ps": { "unicode": "1F1F5-1F1F8", @@ -5662,9 +11370,15 @@ "name": "palestinian authority", "shortname": ":flag_ps:", "category": "flags", - "aliases": [":ps:"], + "aliases": [ + ":ps:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ps"] + "keywords": [ + "country", + "nation", + "ps" + ] }, "flag_pt": { "unicode": "1F1F5-1F1F9", @@ -5672,9 +11386,15 @@ "name": "portugal", "shortname": ":flag_pt:", "category": "flags", - "aliases": [":pt:"], + "aliases": [ + ":pt:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "pt"] + "keywords": [ + "country", + "nation", + "pt" + ] }, "flag_pw": { "unicode": "1F1F5-1F1FC", @@ -5682,9 +11402,16 @@ "name": "palau", "shortname": ":flag_pw:", "category": "flags", - "aliases": [":pw:"], + "aliases": [ + ":pw:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "belau", "pw"] + "keywords": [ + "country", + "nation", + "belau", + "pw" + ] }, "flag_py": { "unicode": "1F1F5-1F1FE", @@ -5692,9 +11419,15 @@ "name": "paraguay", "shortname": ":flag_py:", "category": "flags", - "aliases": [":py:"], + "aliases": [ + ":py:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "py"] + "keywords": [ + "country", + "nation", + "py" + ] }, "flag_qa": { "unicode": "1F1F6-1F1E6", @@ -5702,9 +11435,28 @@ "name": "qatar", "shortname": ":flag_qa:", "category": "flags", - "aliases": [":qa:"], + "aliases": [ + ":qa:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "dawlat qatar", "qa"] + "keywords": [ + "country", + "nation", + "dawlat qatar", + "qa" + ] + }, + "flag_re": { + "unicode": "1F1F7-1F1EA", + "unicode_alternates": "", + "name": "réunion", + "shortname": ":flag_re:", + "category": "flags", + "aliases": [ + ":re:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_ro": { "unicode": "1F1F7-1F1F4", @@ -5712,9 +11464,15 @@ "name": "romania", "shortname": ":flag_ro:", "category": "flags", - "aliases": [":ro:"], + "aliases": [ + ":ro:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ro"] + "keywords": [ + "country", + "nation", + "ro" + ] }, "flag_rs": { "unicode": "1F1F7-1F1F8", @@ -5722,9 +11480,16 @@ "name": "serbia", "shortname": ":flag_rs:", "category": "flags", - "aliases": [":rs:"], + "aliases": [ + ":rs:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "srbija", "rs"] + "keywords": [ + "country", + "nation", + "srbija", + "rs" + ] }, "flag_ru": { "unicode": "1F1F7-1F1FA", @@ -5732,9 +11497,16 @@ "name": "russia", "shortname": ":flag_ru:", "category": "flags", - "aliases": [":ru:"], + "aliases": [ + ":ru:" + ], "aliases_ascii": [], - "keywords": ["nation", "russian", "country", "ru"] + "keywords": [ + "nation", + "russian", + "country", + "ru" + ] }, "flag_rw": { "unicode": "1F1F7-1F1FC", @@ -5742,9 +11514,15 @@ "name": "rwanda", "shortname": ":flag_rw:", "category": "flags", - "aliases": [":rw:"], + "aliases": [ + ":rw:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "rw"] + "keywords": [ + "country", + "nation", + "rw" + ] }, "flag_sa": { "unicode": "1F1F8-1F1E6", @@ -5752,9 +11530,17 @@ "name": "saudi arabia", "shortname": ":flag_sa:", "category": "flags", - "aliases": [":saudiarabia:", ":saudi:"], + "aliases": [ + ":saudiarabia:", + ":saudi:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "al arabiyah as suudiyah", "sa"] + "keywords": [ + "country", + "nation", + "al arabiyah as suudiyah", + "sa" + ] }, "flag_sb": { "unicode": "1F1F8-1F1E7", @@ -5762,9 +11548,15 @@ "name": "the solomon islands", "shortname": ":flag_sb:", "category": "flags", - "aliases": [":sb:"], + "aliases": [ + ":sb:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "sb"] + "keywords": [ + "country", + "nation", + "sb" + ] }, "flag_sc": { "unicode": "1F1F8-1F1E8", @@ -5772,9 +11564,16 @@ "name": "the seychelles", "shortname": ":flag_sc:", "category": "flags", - "aliases": [":sc:"], + "aliases": [ + ":sc:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "seychelles", "sc"] + "keywords": [ + "country", + "nation", + "seychelles", + "sc" + ] }, "flag_sd": { "unicode": "1F1F8-1F1E9", @@ -5782,9 +11581,16 @@ "name": "sudan", "shortname": ":flag_sd:", "category": "flags", - "aliases": [":sd:"], + "aliases": [ + ":sd:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "as-sudan", "sd"] + "keywords": [ + "country", + "nation", + "as-sudan", + "sd" + ] }, "flag_se": { "unicode": "1F1F8-1F1EA", @@ -5792,9 +11598,16 @@ "name": "sweden", "shortname": ":flag_se:", "category": "flags", - "aliases": [":se:"], + "aliases": [ + ":se:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "sverige", "se"] + "keywords": [ + "country", + "nation", + "sverige", + "se" + ] }, "flag_sg": { "unicode": "1F1F8-1F1EC", @@ -5802,9 +11615,15 @@ "name": "singapore", "shortname": ":flag_sg:", "category": "flags", - "aliases": [":sg:"], + "aliases": [ + ":sg:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "sg"] + "keywords": [ + "country", + "nation", + "sg" + ] }, "flag_sh": { "unicode": "1F1F8-1F1ED", @@ -5812,9 +11631,15 @@ "name": "saint helena", "shortname": ":flag_sh:", "category": "flags", - "aliases": [":sh:"], + "aliases": [ + ":sh:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "sh"] + "keywords": [ + "country", + "nation", + "sh" + ] }, "flag_si": { "unicode": "1F1F8-1F1EE", @@ -5822,9 +11647,28 @@ "name": "slovenia", "shortname": ":flag_si:", "category": "flags", - "aliases": [":si:"], + "aliases": [ + ":si:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "slovenija", "si"] + "keywords": [ + "country", + "nation", + "slovenija", + "si" + ] + }, + "flag_sj": { + "unicode": "1F1F8-1F1EF", + "unicode_alternates": "", + "name": "svalbard and jan mayen", + "shortname": ":flag_sj:", + "category": "flags", + "aliases": [ + ":sj:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_sk": { "unicode": "1F1F8-1F1F0", @@ -5832,9 +11676,15 @@ "name": "slovakia", "shortname": ":flag_sk:", "category": "flags", - "aliases": [":sk:"], + "aliases": [ + ":sk:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "sk"] + "keywords": [ + "country", + "nation", + "sk" + ] }, "flag_sl": { "unicode": "1F1F8-1F1F1", @@ -5842,9 +11692,15 @@ "name": "sierra leone", "shortname": ":flag_sl:", "category": "flags", - "aliases": [":sl:"], + "aliases": [ + ":sl:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "sl"] + "keywords": [ + "country", + "nation", + "sl" + ] }, "flag_sm": { "unicode": "1F1F8-1F1F2", @@ -5852,9 +11708,15 @@ "name": "san marino", "shortname": ":flag_sm:", "category": "flags", - "aliases": [":sm:"], + "aliases": [ + ":sm:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "sm"] + "keywords": [ + "country", + "nation", + "sm" + ] }, "flag_sn": { "unicode": "1F1F8-1F1F3", @@ -5862,9 +11724,15 @@ "name": "senegal", "shortname": ":flag_sn:", "category": "flags", - "aliases": [":sn:"], + "aliases": [ + ":sn:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "sn"] + "keywords": [ + "country", + "nation", + "sn" + ] }, "flag_so": { "unicode": "1F1F8-1F1F4", @@ -5872,9 +11740,15 @@ "name": "somalia", "shortname": ":flag_so:", "category": "flags", - "aliases": [":so:"], + "aliases": [ + ":so:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "so"] + "keywords": [ + "country", + "nation", + "so" + ] }, "flag_sr": { "unicode": "1F1F8-1F1F7", @@ -5882,9 +11756,27 @@ "name": "suriname", "shortname": ":flag_sr:", "category": "flags", - "aliases": [":sr:"], + "aliases": [ + ":sr:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "sr"] + "keywords": [ + "country", + "nation", + "sr" + ] + }, + "flag_ss": { + "unicode": "1F1F8-1F1F8", + "unicode_alternates": "", + "name": "south sudan", + "shortname": ":flag_ss:", + "category": "flags", + "aliases": [ + ":ss:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_st": { "unicode": "1F1F8-1F1F9", @@ -5892,9 +11784,16 @@ "name": "sao tome and principe", "shortname": ":flag_st:", "category": "flags", - "aliases": [":st:"], + "aliases": [ + ":st:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "sao tome e principe", "st"] + "keywords": [ + "country", + "nation", + "sao tome e principe", + "st" + ] }, "flag_sv": { "unicode": "1F1F8-1F1FB", @@ -5902,9 +11801,27 @@ "name": "el salvador", "shortname": ":flag_sv:", "category": "flags", - "aliases": [":sv:"], + "aliases": [ + ":sv:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "sv"] + "keywords": [ + "country", + "nation", + "sv" + ] + }, + "flag_sx": { + "unicode": "1F1F8-1F1FD", + "unicode_alternates": "", + "name": "sint maarten", + "shortname": ":flag_sx:", + "category": "flags", + "aliases": [ + ":sx:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_sy": { "unicode": "1F1F8-1F1FE", @@ -5912,9 +11829,15 @@ "name": "syria", "shortname": ":flag_sy:", "category": "flags", - "aliases": [":sy:"], + "aliases": [ + ":sy:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "sy"] + "keywords": [ + "country", + "nation", + "sy" + ] }, "flag_sz": { "unicode": "1F1F8-1F1FF", @@ -5922,9 +11845,39 @@ "name": "swaziland", "shortname": ":flag_sz:", "category": "flags", - "aliases": [":sz:"], + "aliases": [ + ":sz:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "sz"] + "keywords": [ + "country", + "nation", + "sz" + ] + }, + "flag_ta": { + "unicode": "1F1F9-1F1E6", + "unicode_alternates": "", + "name": "tristan da cunha", + "shortname": ":flag_ta:", + "category": "flags", + "aliases": [ + ":ta:" + ], + "aliases_ascii": [], + "keywords": [] + }, + "flag_tc": { + "unicode": "1F1F9-1F1E8", + "unicode_alternates": "", + "name": "turks and caicos islands", + "shortname": ":flag_tc:", + "category": "flags", + "aliases": [ + ":tc:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_td": { "unicode": "1F1F9-1F1E9", @@ -5932,9 +11885,28 @@ "name": "chad", "shortname": ":flag_td:", "category": "flags", - "aliases": [":td:"], + "aliases": [ + ":td:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "tchad", "td"] + "keywords": [ + "country", + "nation", + "tchad", + "td" + ] + }, + "flag_tf": { + "unicode": "1F1F9-1F1EB", + "unicode_alternates": "", + "name": "french southern territories", + "shortname": ":flag_tf:", + "category": "flags", + "aliases": [ + ":tf:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_tg": { "unicode": "1F1F9-1F1EC", @@ -5942,9 +11914,16 @@ "name": "togo", "shortname": ":flag_tg:", "category": "flags", - "aliases": [":tg:"], + "aliases": [ + ":tg:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "republique togolaise", "tg"] + "keywords": [ + "country", + "nation", + "republique togolaise", + "tg" + ] }, "flag_th": { "unicode": "1F1F9-1F1ED", @@ -5952,9 +11931,16 @@ "name": "thailand", "shortname": ":flag_th:", "category": "flags", - "aliases": [":th:"], + "aliases": [ + ":th:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "prathet thai", "th"] + "keywords": [ + "country", + "nation", + "prathet thai", + "th" + ] }, "flag_tj": { "unicode": "1F1F9-1F1EF", @@ -5962,9 +11948,28 @@ "name": "tajikistan", "shortname": ":flag_tj:", "category": "flags", - "aliases": [":tj:"], + "aliases": [ + ":tj:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "jumhurii tojikiston", "tj"] + "keywords": [ + "country", + "nation", + "jumhurii tojikiston", + "tj" + ] + }, + "flag_tk": { + "unicode": "1F1F9-1F1F0", + "unicode_alternates": "", + "name": "tokelau", + "shortname": ":flag_tk:", + "category": "flags", + "aliases": [ + ":tk:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_tl": { "unicode": "1F1F9-1F1F1", @@ -5972,9 +11977,15 @@ "name": "east timor", "shortname": ":flag_tl:", "category": "flags", - "aliases": [":tl:"], + "aliases": [ + ":tl:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "tl"] + "keywords": [ + "country", + "nation", + "tl" + ] }, "flag_tm": { "unicode": "1F1F9-1F1F2", @@ -5982,9 +11993,15 @@ "name": "turkmenistan", "shortname": ":flag_tm:", "category": "flags", - "aliases": [":turkmenistan:"], + "aliases": [ + ":turkmenistan:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "tm"] + "keywords": [ + "country", + "nation", + "tm" + ] }, "flag_tn": { "unicode": "1F1F9-1F1F3", @@ -5992,9 +12009,16 @@ "name": "tunisia", "shortname": ":flag_tn:", "category": "flags", - "aliases": [":tn:"], + "aliases": [ + ":tn:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "tunis", "tn"] + "keywords": [ + "country", + "nation", + "tunis", + "tn" + ] }, "flag_to": { "unicode": "1F1F9-1F1F4", @@ -6002,9 +12026,15 @@ "name": "tonga", "shortname": ":flag_to:", "category": "flags", - "aliases": [":to:"], + "aliases": [ + ":to:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "to"] + "keywords": [ + "country", + "nation", + "to" + ] }, "flag_tr": { "unicode": "1F1F9-1F1F7", @@ -6012,9 +12042,15 @@ "name": "turkey", "shortname": ":flag_tr:", "category": "flags", - "aliases": [":tr:"], + "aliases": [ + ":tr:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "turkiye"] + "keywords": [ + "country", + "nation", + "turkiye" + ] }, "flag_tt": { "unicode": "1F1F9-1F1F9", @@ -6022,9 +12058,15 @@ "name": "trinidad and tobago", "shortname": ":flag_tt:", "category": "flags", - "aliases": [":tt:"], + "aliases": [ + ":tt:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "tt"] + "keywords": [ + "country", + "nation", + "tt" + ] }, "flag_tv": { "unicode": "1F1F9-1F1FB", @@ -6032,9 +12074,15 @@ "name": "tuvalu", "shortname": ":flag_tv:", "category": "flags", - "aliases": [":tuvalu:"], + "aliases": [ + ":tuvalu:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "tv"] + "keywords": [ + "country", + "nation", + "tv" + ] }, "flag_tw": { "unicode": "1F1F9-1F1FC", @@ -6042,9 +12090,16 @@ "name": "the republic of china", "shortname": ":flag_tw:", "category": "flags", - "aliases": [":tw:"], + "aliases": [ + ":tw:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "taiwan", "tw"] + "keywords": [ + "country", + "nation", + "taiwan", + "tw" + ] }, "flag_tz": { "unicode": "1F1F9-1F1FF", @@ -6052,9 +12107,15 @@ "name": "tanzania", "shortname": ":flag_tz:", "category": "flags", - "aliases": [":tz:"], + "aliases": [ + ":tz:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "tz"] + "keywords": [ + "country", + "nation", + "tz" + ] }, "flag_ua": { "unicode": "1F1FA-1F1E6", @@ -6062,9 +12123,16 @@ "name": "ukraine", "shortname": ":flag_ua:", "category": "flags", - "aliases": [":ua:"], + "aliases": [ + ":ua:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ukrayina", "ua"] + "keywords": [ + "country", + "nation", + "ukrayina", + "ua" + ] }, "flag_ug": { "unicode": "1F1FA-1F1EC", @@ -6072,9 +12140,27 @@ "name": "uganda", "shortname": ":flag_ug:", "category": "flags", - "aliases": [":ug:"], + "aliases": [ + ":ug:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ug"] + "keywords": [ + "country", + "nation", + "ug" + ] + }, + "flag_um": { + "unicode": "1F1FA-1F1F2", + "unicode_alternates": "", + "name": "united states minor outlying islands", + "shortname": ":flag_um:", + "category": "flags", + "aliases": [ + ":um:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_us": { "unicode": "1F1FA-1F1F8", @@ -6082,9 +12168,20 @@ "name": "united states", "shortname": ":flag_us:", "category": "flags", - "aliases": [":us:"], + "aliases": [ + ":us:" + ], "aliases_ascii": [], - "keywords": ["american", "country", "nation", "usa", "united states of america", "america", "old glory", "us"] + "keywords": [ + "american", + "country", + "nation", + "usa", + "united states of america", + "america", + "old glory", + "us" + ] }, "flag_uy": { "unicode": "1F1FA-1F1FE", @@ -6092,9 +12189,15 @@ "name": "uruguay", "shortname": ":flag_uy:", "category": "flags", - "aliases": [":uy:"], + "aliases": [ + ":uy:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "uy"] + "keywords": [ + "country", + "nation", + "uy" + ] }, "flag_uz": { "unicode": "1F1FA-1F1FF", @@ -6102,9 +12205,16 @@ "name": "uzbekistan", "shortname": ":flag_uz:", "category": "flags", - "aliases": [":uz:"], + "aliases": [ + ":uz:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "uzbekiston respublikasi", "uz"] + "keywords": [ + "country", + "nation", + "uzbekiston respublikasi", + "uz" + ] }, "flag_va": { "unicode": "1F1FB-1F1E6", @@ -6112,9 +12222,15 @@ "name": "the vatican city", "shortname": ":flag_va:", "category": "flags", - "aliases": [":va:"], + "aliases": [ + ":va:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "va"] + "keywords": [ + "country", + "nation", + "va" + ] }, "flag_vc": { "unicode": "1F1FB-1F1E8", @@ -6122,9 +12238,15 @@ "name": "saint vincent and the grenadines", "shortname": ":flag_vc:", "category": "flags", - "aliases": [":vc:"], + "aliases": [ + ":vc:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "vc"] + "keywords": [ + "country", + "nation", + "vc" + ] }, "flag_ve": { "unicode": "1F1FB-1F1EA", @@ -6132,9 +12254,27 @@ "name": "venezuela", "shortname": ":flag_ve:", "category": "flags", - "aliases": [":ve:"], + "aliases": [ + ":ve:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "ve"] + "keywords": [ + "country", + "nation", + "ve" + ] + }, + "flag_vg": { + "unicode": "1F1FB-1F1EC", + "unicode_alternates": "", + "name": "british virgin islands", + "shortname": ":flag_vg:", + "category": "flags", + "aliases": [ + ":vg:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_vi": { "unicode": "1F1FB-1F1EE", @@ -6142,9 +12282,15 @@ "name": "u.s. virgin islands", "shortname": ":flag_vi:", "category": "flags", - "aliases": [":vi:"], + "aliases": [ + ":vi:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "vi"] + "keywords": [ + "country", + "nation", + "vi" + ] }, "flag_vn": { "unicode": "1F1FB-1F1F3", @@ -6152,9 +12298,16 @@ "name": "vietnam", "shortname": ":flag_vn:", "category": "flags", - "aliases": [":vn:"], + "aliases": [ + ":vn:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "viet nam", "vn"] + "keywords": [ + "country", + "nation", + "viet nam", + "vn" + ] }, "flag_vu": { "unicode": "1F1FB-1F1FA", @@ -6162,9 +12315,15 @@ "name": "vanuatu", "shortname": ":flag_vu:", "category": "flags", - "aliases": [":vu:"], + "aliases": [ + ":vu:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "vu"] + "keywords": [ + "country", + "nation", + "vu" + ] }, "flag_wf": { "unicode": "1F1FC-1F1EB", @@ -6172,9 +12331,15 @@ "name": "wallis and futuna", "shortname": ":flag_wf:", "category": "flags", - "aliases": [":wf:"], + "aliases": [ + ":wf:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "wf"] + "keywords": [ + "country", + "nation", + "wf" + ] }, "flag_white": { "unicode": "1F3F3", @@ -6182,9 +12347,14 @@ "name": "waving white flag", "shortname": ":flag_white:", "category": "objects_symbols", - "aliases": [":waving_white_flag:"], + "aliases": [ + ":waving_white_flag:" + ], "aliases_ascii": [], - "keywords": ["symbol", "signal"] + "keywords": [ + "symbol", + "signal" + ] }, "flag_ws": { "unicode": "1F1FC-1F1F8", @@ -6192,9 +12362,16 @@ "name": "samoa", "shortname": ":flag_ws:", "category": "flags", - "aliases": [":ws:"], + "aliases": [ + ":ws:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "american samoa", "ws"] + "keywords": [ + "country", + "nation", + "american samoa", + "ws" + ] }, "flag_xk": { "unicode": "1F1FD-1F1F0", @@ -6202,9 +12379,15 @@ "name": "kosovo", "shortname": ":flag_xk:", "category": "flags", - "aliases": [":xk:"], + "aliases": [ + ":xk:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "xk"] + "keywords": [ + "country", + "nation", + "xk" + ] }, "flag_ye": { "unicode": "1F1FE-1F1EA", @@ -6212,9 +12395,28 @@ "name": "yemen", "shortname": ":flag_ye:", "category": "flags", - "aliases": [":ye:"], + "aliases": [ + ":ye:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "al yaman", "ye"] + "keywords": [ + "country", + "nation", + "al yaman", + "ye" + ] + }, + "flag_yt": { + "unicode": "1F1FE-1F1F9", + "unicode_alternates": "", + "name": "mayotte", + "shortname": ":flag_yt:", + "category": "flags", + "aliases": [ + ":yt:" + ], + "aliases_ascii": [], + "keywords": [] }, "flag_za": { "unicode": "1F1FF-1F1E6", @@ -6222,9 +12424,14 @@ "name": "south africa", "shortname": ":flag_za:", "category": "flags", - "aliases": [":za:"], + "aliases": [ + ":za:" + ], "aliases_ascii": [], - "keywords": ["country", "nation"] + "keywords": [ + "country", + "nation" + ] }, "flag_zm": { "unicode": "1F1FF-1F1F2", @@ -6232,9 +12439,15 @@ "name": "zambia", "shortname": ":flag_zm:", "category": "flags", - "aliases": [":zm:"], + "aliases": [ + ":zm:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "zm"] + "keywords": [ + "country", + "nation", + "zm" + ] }, "flag_zw": { "unicode": "1F1FF-1F1FC", @@ -6242,9 +12455,15 @@ "name": "zimbabwe", "shortname": ":flag_zw:", "category": "flags", - "aliases": [":zw:"], + "aliases": [ + ":zw:" + ], "aliases_ascii": [], - "keywords": ["country", "nation", "zw"] + "keywords": [ + "country", + "nation", + "zw" + ] }, "flags": { "unicode": "1F38F", @@ -6254,7 +12473,23 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["banner", "carp", "fish", "japanese", "koinobori", "children", "kids", "boys", "celebration", "happiness", "carp", "streamers", "japanese", "holiday", "flags"], + "keywords": [ + "banner", + "carp", + "fish", + "japanese", + "koinobori", + "children", + "kids", + "boys", + "celebration", + "happiness", + "carp", + "streamers", + "japanese", + "holiday", + "flags" + ], "moji": "🎏" }, "flashlight": { @@ -6265,18 +12500,36 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["dark"], + "keywords": [ + "dark" + ], "moji": "🔦" }, + "fleur-de-lis": { + "unicode": "269C", + "unicode_alternates": "", + "name": "fleur-de-lis", + "shortname": ":fleur-de-lis:", + "category": "symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "symbol" + ] + }, "flip_phone": { "unicode": "1F581", "unicode_alternates": [], "name": "clamshell mobile phone", "shortname": ":flip_phone:", "category": "objects_symbols", - "aliases": [":clamshell_mobile_phone:"], + "aliases": [ + ":clamshell_mobile_phone:" + ], "aliases_ascii": [], - "keywords": ["cellphone"] + "keywords": [ + "cellphone" + ] }, "floppy_black": { "unicode": "1F5AA", @@ -6284,9 +12537,20 @@ "name": "black hard shell floppy disk", "shortname": ":floppy_black:", "category": "objects_symbols", - "aliases": [":black_hard_shell_floppy_disk:"], + "aliases": [ + ":black_hard_shell_floppy_disk:" + ], "aliases_ascii": [], - "keywords": ["oldschool", "save", "technology", "storage", "information", "computer", "drive", "megabyte"] + "keywords": [ + "oldschool", + "save", + "technology", + "storage", + "information", + "computer", + "drive", + "megabyte" + ] }, "floppy_disk": { "unicode": "1F4BE", @@ -6296,7 +12560,18 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["oldschool", "save", "technology", "floppy", "disk", "storage", "information", "computer", "drive", "megabyte"], + "keywords": [ + "oldschool", + "save", + "technology", + "floppy", + "disk", + "storage", + "information", + "computer", + "drive", + "megabyte" + ], "moji": "💾" }, "floppy_white": { @@ -6305,9 +12580,20 @@ "name": "white hard shell floppy disk", "shortname": ":floppy_white:", "category": "objects_symbols", - "aliases": [":white_hard_shell_floppy_disk:"], + "aliases": [ + ":white_hard_shell_floppy_disk:" + ], "aliases_ascii": [], - "keywords": ["oldschool", "save", "technology", "storage", "information", "computer", "drive", "megabyte"] + "keywords": [ + "oldschool", + "save", + "technology", + "storage", + "information", + "computer", + "drive", + "megabyte" + ] }, "flower_playing_cards": { "unicode": "1F3B4", @@ -6317,7 +12603,15 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["playing", "card", "flower", "game", "august", "moon", "special"], + "keywords": [ + "playing", + "card", + "flower", + "game", + "august", + "moon", + "special" + ], "moji": "🎴" }, "flushed": { @@ -6327,8 +12621,21 @@ "shortname": ":flushed:", "category": "emoticons", "aliases": [], - "aliases_ascii": [":$", "=$"], - "keywords": ["blush", "face", "flattered", "flush", "blush", "red", "pink", "cheeks", "shy"], + "aliases_ascii": [ + ":$", + "=$" + ], + "keywords": [ + "blush", + "face", + "flattered", + "flush", + "blush", + "red", + "pink", + "cheeks", + "shy" + ], "moji": "😳" }, "fog": { @@ -6339,7 +12646,12 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["weather", "damp", "cloud", "hazy"] + "keywords": [ + "weather", + "damp", + "cloud", + "hazy" + ] }, "foggy": { "unicode": "1F301", @@ -6349,7 +12661,14 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["mountain", "photo", "bridge", "weather", "fog", "foggy"], + "keywords": [ + "mountain", + "photo", + "bridge", + "weather", + "fog", + "foggy" + ], "moji": "🌁" }, "folder": { @@ -6360,7 +12679,9 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["documents"] + "keywords": [ + "documents" + ] }, "folder_open": { "unicode": "1F5C1", @@ -6368,9 +12689,14 @@ "name": "open folder", "shortname": ":folder_open:", "category": "objects_symbols", - "aliases": [":open_folder:"], + "aliases": [ + ":open_folder:" + ], "aliases_ascii": [], - "keywords": ["documents", "load"] + "keywords": [ + "documents", + "load" + ] }, "football": { "unicode": "1F3C8", @@ -6380,7 +12706,16 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["NFL", "balls", "sports", "football", "ball", "sport", "america", "american"], + "keywords": [ + "NFL", + "balls", + "sports", + "football", + "ball", + "sport", + "america", + "american" + ], "moji": "🏈" }, "footprints": { @@ -6391,7 +12726,9 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["feet"], + "keywords": [ + "feet" + ], "moji": "👣" }, "fork_and_knife": { @@ -6402,7 +12739,16 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["cutlery", "kitchen", "fork", "knife", "restaurant", "meal", "food", "eat"], + "keywords": [ + "cutlery", + "kitchen", + "fork", + "knife", + "restaurant", + "meal", + "food", + "eat" + ], "moji": "🍴" }, "fork_knife_plate": { @@ -6411,31 +12757,51 @@ "name": "fork and knife with plate", "shortname": ":fork_knife_plate:", "category": "travel_places", - "aliases": [":fork_and_knife_with_plate:"], + "aliases": [ + ":fork_and_knife_with_plate:" + ], "aliases_ascii": [], - "keywords": ["meal", "food", "breakfast", "lunch", "dinner", "utensils", "setting"] + "keywords": [ + "meal", + "food", + "breakfast", + "lunch", + "dinner", + "utensils", + "setting" + ] }, "fountain": { "unicode": "26F2", - "unicode_alternates": ["26F2-FE0F"], + "unicode_alternates": [ + "26F2-FE0F" + ], "name": "fountain", "shortname": ":fountain:", "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["photo"], + "keywords": [ + "photo" + ], "moji": "⛲" }, "four": { "moji": "4️⃣", "unicode": "0034-20E3", - "unicode_alternates": ["0034-FE0F-20E3"], + "unicode_alternates": [ + "0034-FE0F-20E3" + ], "name": "digit four", "shortname": ":four:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["4", "blue-square", "numbers"] + "keywords": [ + "4", + "blue-square", + "numbers" + ] }, "four_leaf_clover": { "unicode": "1F340", @@ -6445,7 +12811,20 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["lucky", "nature", "plant", "vegetable", "clover", "four", "leaf", "luck", "irish", "saint", "patrick", "green"], + "keywords": [ + "lucky", + "nature", + "plant", + "vegetable", + "clover", + "four", + "leaf", + "luck", + "irish", + "saint", + "patrick", + "green" + ], "moji": "🍀" }, "frame_photo": { @@ -6454,9 +12833,13 @@ "name": "frame with picture", "shortname": ":frame_photo:", "category": "objects_symbols", - "aliases": [":frame_with_picture:"], + "aliases": [ + ":frame_with_picture:" + ], "aliases_ascii": [], - "keywords": ["photo"] + "keywords": [ + "photo" + ] }, "frame_tiles": { "unicode": "1F5BD", @@ -6464,9 +12847,14 @@ "name": "frame with tiles", "shortname": ":frame_tiles:", "category": "objects_symbols", - "aliases": [":frame_with_tiles:"], + "aliases": [ + ":frame_with_tiles:" + ], "aliases_ascii": [], - "keywords": ["photo", "painting"] + "keywords": [ + "photo", + "painting" + ] }, "frame_x": { "unicode": "1F5BE", @@ -6474,9 +12862,14 @@ "name": "frame with an x", "shortname": ":frame_x:", "category": "objects_symbols", - "aliases": [":frame_with_an_x:"], + "aliases": [ + ":frame_with_an_x:" + ], "aliases_ascii": [], - "keywords": ["photo", "painting"] + "keywords": [ + "photo", + "painting" + ] }, "free": { "unicode": "1F193", @@ -6486,7 +12879,10 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square", "words"], + "keywords": [ + "blue-square", + "words" + ], "moji": "🆓" }, "fried_shrimp": { @@ -6497,7 +12893,15 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "food", "shrimp", "fried", "seafood", "small", "fish"], + "keywords": [ + "animal", + "food", + "shrimp", + "fried", + "seafood", + "small", + "fish" + ], "moji": "🍤" }, "fries": { @@ -6508,7 +12912,16 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["chips", "food", "fries", "french", "potato", "fry", "russet", "idaho"], + "keywords": [ + "chips", + "food", + "fries", + "french", + "potato", + "fry", + "russet", + "idaho" + ], "moji": "🍟" }, "frog": { @@ -6519,7 +12932,10 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature"], + "keywords": [ + "animal", + "nature" + ], "moji": "🐸" }, "frowning": { @@ -6528,20 +12944,50 @@ "name": "frowning face with open mouth", "shortname": ":frowning:", "category": "emoticons", - "aliases": [":anguished:"], + "aliases": [ + ":anguished:" + ], "aliases_ascii": [], - "keywords": ["aw", "face", "frown", "sad", "pout", "sulk", "glower"], + "keywords": [ + "aw", + "face", + "frown", + "sad", + "pout", + "sulk", + "glower" + ], "moji": "😦" }, + "frowning2": { + "unicode": "2639", + "unicode_alternates": "", + "name": "white frowning face", + "shortname": ":frowning2:", + "category": "people", + "aliases": [ + ":white_frowning_face:" + ], + "aliases_ascii": [], + "keywords": [ + "frown", + "person" + ] + }, "fuelpump": { "unicode": "26FD", - "unicode_alternates": ["26FD-FE0F"], + "unicode_alternates": [ + "26FD-FE0F" + ], "name": "fuel pump", "shortname": ":fuelpump:", "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["gas station", "petroleum"], + "keywords": [ + "gas station", + "petroleum" + ], "moji": "⛽" }, "full_moon": { @@ -6552,7 +12998,20 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["nature", "yellow", "moon", "full", "sky", "night", "cheese", "phase", "monster", "spooky", "werewolves", "twilight"], + "keywords": [ + "nature", + "yellow", + "moon", + "full", + "sky", + "night", + "cheese", + "phase", + "monster", + "spooky", + "werewolves", + "twilight" + ], "moji": "🌕" }, "full_moon_with_face": { @@ -6563,7 +13022,20 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["night", "moon", "full", "anthropomorphic", "face", "sky", "night", "cheese", "phase", "spooky", "werewolves", "monsters"], + "keywords": [ + "night", + "moon", + "full", + "anthropomorphic", + "face", + "sky", + "night", + "cheese", + "phase", + "spooky", + "werewolves", + "monsters" + ], "moji": "🌝" }, "game_die": { @@ -6574,9 +13046,30 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["dice", "game", "die", "dice", "craps", "gamble", "play"], + "keywords": [ + "dice", + "game", + "die", + "dice", + "craps", + "gamble", + "play" + ], "moji": "🎲" }, + "gear": { + "unicode": "2699", + "unicode_alternates": "", + "name": "gear", + "shortname": ":gear:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "object", + "tool" + ] + }, "gem": { "unicode": "1F48E", "unicode_alternates": [], @@ -6585,18 +13078,35 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["blue", "ruby"], + "keywords": [ + "blue", + "ruby" + ], "moji": "💎" }, "gemini": { "unicode": "264A", - "unicode_alternates": ["264A-FE0F"], + "unicode_alternates": [ + "264A-FE0F" + ], "name": "gemini", "shortname": ":gemini:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["gemini", "twins", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "sign", "zodiac", "horoscope"], + "keywords": [ + "gemini", + "twins", + "astrology", + "greek", + "constellation", + "stars", + "zodiac", + "sign", + "sign", + "zodiac", + "horoscope" + ], "moji": "♊" }, "ghost": { @@ -6607,7 +13117,9 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["halloween"], + "keywords": [ + "halloween" + ], "moji": "👻" }, "gift": { @@ -6618,7 +13130,18 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["birthday", "christmas", "present", "xmas", "gift", "present", "wrap", "package", "birthday", "wedding"], + "keywords": [ + "birthday", + "christmas", + "present", + "xmas", + "gift", + "present", + "wrap", + "package", + "birthday", + "wedding" + ], "moji": "🎁" }, "gift_heart": { @@ -6629,7 +13152,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["love", "valentines"], + "keywords": [ + "love", + "valentines" + ], "moji": "💝" }, "girl": { @@ -6640,9 +13166,82 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["female", "woman"], + "keywords": [ + "female", + "woman" + ], "moji": "👧" }, + "girl_tone1": { + "unicode": "1F467-1F3FB", + "unicode_alternates": "", + "name": "girl tone 1", + "shortname": ":girl_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "kid", + "child" + ] + }, + "girl_tone2": { + "unicode": "1F467-1F3FC", + "unicode_alternates": "", + "name": "girl tone 2", + "shortname": ":girl_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "kid", + "child" + ] + }, + "girl_tone3": { + "unicode": "1F467-1F3FD", + "unicode_alternates": "", + "name": "girl tone 3", + "shortname": ":girl_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "kid", + "child" + ] + }, + "girl_tone4": { + "unicode": "1F467-1F3FE", + "unicode_alternates": "", + "name": "girl tone 4", + "shortname": ":girl_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "kid", + "child" + ] + }, + "girl_tone5": { + "unicode": "1F467-1F3FF", + "unicode_alternates": "", + "name": "girl tone 5", + "shortname": ":girl_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "kid", + "child" + ] + }, "girls_symbol": { "unicode": "1F6CA", "unicode_alternates": [], @@ -6651,7 +13250,10 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["female", "child"] + "keywords": [ + "female", + "child" + ] }, "globe_with_meridians": { "unicode": "1F310", @@ -6661,7 +13263,17 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["earth", "international", "world", "earth", "meridian", "globe", "space", "planet", "home"], + "keywords": [ + "earth", + "international", + "world", + "earth", + "meridian", + "globe", + "space", + "planet", + "home" + ], "moji": "🌐" }, "goat": { @@ -6672,18 +13284,31 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature", "goat", "sheep", "kid", "billy", "livestock"], + "keywords": [ + "animal", + "nature", + "goat", + "sheep", + "kid", + "billy", + "livestock" + ], "moji": "🐐" }, "golf": { "unicode": "26F3", - "unicode_alternates": ["26F3-FE0F"], + "unicode_alternates": [ + "26F3-FE0F" + ], "name": "flag in hole", "shortname": ":golf:", "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["business", "sports"], + "keywords": [ + "business", + "sports" + ], "moji": "⛳" }, "golfer": { @@ -6694,7 +13319,13 @@ "category": "activity", "aliases": [], "aliases_ascii": [], - "keywords": ["sport", "par", "birdie", "eagle", "mulligan"] + "keywords": [ + "sport", + "par", + "birdie", + "eagle", + "mulligan" + ] }, "grapes": { "unicode": "1F347", @@ -6704,7 +13335,16 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "fruit", "grapes", "wine", "vinegar", "fruit", "cluster", "vine"], + "keywords": [ + "food", + "fruit", + "grapes", + "wine", + "vinegar", + "fruit", + "cluster", + "vine" + ], "moji": "🍇" }, "green_apple": { @@ -6715,7 +13355,17 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["fruit", "nature", "apple", "fruit", "green", "pie", "granny", "smith", "core"], + "keywords": [ + "fruit", + "nature", + "apple", + "fruit", + "green", + "pie", + "granny", + "smith", + "core" + ], "moji": "🍏" }, "green_book": { @@ -6726,7 +13376,11 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["knowledge", "library", "read"], + "keywords": [ + "knowledge", + "library", + "read" + ], "moji": "📗" }, "green_heart": { @@ -6737,7 +13391,22 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["affection", "like", "love", "valentines", "green", "heart", "love", "nature", "rebirth", "reborn", "jealous", "clingy", "envious", "possessive"], + "keywords": [ + "affection", + "like", + "love", + "valentines", + "green", + "heart", + "love", + "nature", + "rebirth", + "reborn", + "jealous", + "clingy", + "envious", + "possessive" + ], "moji": "💚" }, "grey_exclamation": { @@ -6748,7 +13417,9 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["surprise"], + "keywords": [ + "surprise" + ], "moji": "❕" }, "grey_question": { @@ -6759,7 +13430,9 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["doubts"], + "keywords": [ + "doubts" + ], "moji": "❔" }, "grimacing": { @@ -6770,7 +13443,14 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["face", "grimace", "teeth", "grimace", "disapprove", "pain"], + "keywords": [ + "face", + "grimace", + "teeth", + "grimace", + "disapprove", + "pain" + ], "moji": "😬" }, "grin": { @@ -6781,7 +13461,17 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["face", "happy", "joy", "smile", "grin", "grinning", "smiling", "smile", "smiley"], + "keywords": [ + "face", + "happy", + "joy", + "smile", + "grin", + "grinning", + "smiling", + "smile", + "smiley" + ], "moji": "😁" }, "grinning": { @@ -6792,7 +13482,17 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["face", "happy", "joy", "smile", "grin", "grinning", "smiling", "smile", "smiley"], + "keywords": [ + "face", + "happy", + "joy", + "smile", + "grin", + "grinning", + "smiling", + "smile", + "smiley" + ], "moji": "🕧" }, "guardsman": { @@ -6803,9 +13503,138 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["british", "gb", "male", "man", "uk", "guardsman", "guard", "bearskin", "hat", "british", "queen", "ceremonial", "military"], + "keywords": [ + "british", + "gb", + "male", + "man", + "uk", + "guardsman", + "guard", + "bearskin", + "hat", + "british", + "queen", + "ceremonial", + "military" + ], "moji": "💂" }, + "guardsman_tone1": { + "unicode": "1F482-1F3FB", + "unicode_alternates": "", + "name": "guardsman tone 1", + "shortname": ":guardsman_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "british", + "gb", + "male", + "man", + "uk", + "guard", + "bearskin", + "hat", + "british", + "queen", + "ceremonial", + "military" + ] + }, + "guardsman_tone2": { + "unicode": "1F482-1F3FC", + "unicode_alternates": "", + "name": "guardsman tone 2", + "shortname": ":guardsman_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "british", + "gb", + "male", + "man", + "uk", + "guard", + "bearskin", + "hat", + "british", + "queen", + "ceremonial", + "military" + ] + }, + "guardsman_tone3": { + "unicode": "1F482-1F3FD", + "unicode_alternates": "", + "name": "guardsman tone 3", + "shortname": ":guardsman_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "british", + "gb", + "male", + "man", + "uk", + "guard", + "bearskin", + "hat", + "british", + "queen", + "ceremonial", + "military" + ] + }, + "guardsman_tone4": { + "unicode": "1F482-1F3FE", + "unicode_alternates": "", + "name": "guardsman tone 4", + "shortname": ":guardsman_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "british", + "gb", + "male", + "man", + "uk", + "guard", + "bearskin", + "hat", + "british", + "queen", + "ceremonial", + "military" + ] + }, + "guardsman_tone5": { + "unicode": "1F482-1F3FF", + "unicode_alternates": "", + "name": "guardsman tone 5", + "shortname": ":guardsman_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "british", + "gb", + "male", + "man", + "uk", + "guard", + "bearskin", + "hat", + "british", + "queen", + "ceremonial", + "military" + ] + }, "guitar": { "unicode": "1F3B8", "unicode_alternates": [], @@ -6814,7 +13643,18 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["instrument", "music", "guitar", "string", "music", "instrument", "jam", "rock", "acoustic", "electric"], + "keywords": [ + "instrument", + "music", + "guitar", + "string", + "music", + "instrument", + "jam", + "rock", + "acoustic", + "electric" + ], "moji": "🎸" }, "gun": { @@ -6825,7 +13665,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["violence", "weapon"], + "keywords": [ + "violence", + "weapon" + ], "moji": "🔫" }, "haircut": { @@ -6836,9 +13679,83 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["female", "girl", "woman"], + "keywords": [ + "female", + "girl", + "woman" + ], "moji": "💇" }, + "haircut_tone1": { + "unicode": "1F487-1F3FB", + "unicode_alternates": "", + "name": "haircut tone 1", + "shortname": ":haircut_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman" + ] + }, + "haircut_tone2": { + "unicode": "1F487-1F3FC", + "unicode_alternates": "", + "name": "haircut tone 2", + "shortname": ":haircut_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman" + ] + }, + "haircut_tone3": { + "unicode": "1F487-1F3FD", + "unicode_alternates": "", + "name": "haircut tone 3", + "shortname": ":haircut_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman" + ] + }, + "haircut_tone4": { + "unicode": "1F487-1F3FE", + "unicode_alternates": "", + "name": "haircut tone 4", + "shortname": ":haircut_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman" + ] + }, + "haircut_tone5": { + "unicode": "1F487-1F3FF", + "unicode_alternates": "", + "name": "haircut tone 5", + "shortname": ":haircut_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman" + ] + }, "hamburger": { "unicode": "1F354", "unicode_alternates": [], @@ -6847,7 +13764,15 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "meat", "hamburger", "burger", "meat", "cow", "beef"], + "keywords": [ + "food", + "meat", + "hamburger", + "burger", + "meat", + "cow", + "beef" + ], "moji": "🍔" }, "hammer": { @@ -6858,9 +13783,31 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["done", "judge", "law", "ruling", "tools", "verdict"], + "keywords": [ + "done", + "judge", + "law", + "ruling", + "tools", + "verdict" + ], "moji": "🔨" }, + "hammer_pick": { + "unicode": "2692", + "unicode_alternates": "", + "name": "hammer and pick", + "shortname": ":hammer_pick:", + "category": "objects", + "aliases": [ + ":hammer_and_pick:" + ], + "aliases_ascii": [], + "keywords": [ + "object", + "tool" + ] + }, "hamster": { "unicode": "1F439", "unicode_alternates": [], @@ -6869,7 +13816,10 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature"], + "keywords": [ + "animal", + "nature" + ], "moji": "🐹" }, "hand_splayed": { @@ -6878,9 +13828,16 @@ "name": "raised hand with fingers splayed", "shortname": ":hand_splayed:", "category": "people", - "aliases": [":raised_hand_with_fingers_splayed:"], + "aliases": [ + ":raised_hand_with_fingers_splayed:" + ], "aliases_ascii": [], - "keywords": ["hi", "five", "stop", "halt"] + "keywords": [ + "hi", + "five", + "stop", + "halt" + ] }, "hand_splayed_reverse": { "unicode": "1F591", @@ -6888,9 +13845,101 @@ "name": "reversed raised hand with fingers splayed", "shortname": ":hand_splayed_reverse:", "category": "people", - "aliases": [":reversed_raised_hand_with_fingers_splayed:"], + "aliases": [ + ":reversed_raised_hand_with_fingers_splayed:" + ], "aliases_ascii": [], - "keywords": ["hi", "five", "stop", "halt"] + "keywords": [ + "hi", + "five", + "stop", + "halt" + ] + }, + "hand_splayed_tone1": { + "unicode": "1F590-1F3FB", + "unicode_alternates": "", + "name": "raised hand with fingers splayed tone 1", + "shortname": ":hand_splayed_tone1:", + "category": "people", + "aliases": [ + ":raised_hand_with_fingers_splayed_tone1:" + ], + "aliases_ascii": [], + "keywords": [ + "hi", + "five", + "stop", + "halt" + ] + }, + "hand_splayed_tone2": { + "unicode": "1F590-1F3FC", + "unicode_alternates": "", + "name": "raised hand with fingers splayed tone 2", + "shortname": ":hand_splayed_tone2:", + "category": "people", + "aliases": [ + ":raised_hand_with_fingers_splayed_tone2:" + ], + "aliases_ascii": [], + "keywords": [ + "hi", + "five", + "stop", + "halt" + ] + }, + "hand_splayed_tone3": { + "unicode": "1F590-1F3FD", + "unicode_alternates": "", + "name": "raised hand with fingers splayed tone 3", + "shortname": ":hand_splayed_tone3:", + "category": "people", + "aliases": [ + ":raised_hand_with_fingers_splayed_tone3:" + ], + "aliases_ascii": [], + "keywords": [ + "hi", + "five", + "stop", + "halt" + ] + }, + "hand_splayed_tone4": { + "unicode": "1F590-1F3FE", + "unicode_alternates": "", + "name": "raised hand with fingers splayed tone 4", + "shortname": ":hand_splayed_tone4:", + "category": "people", + "aliases": [ + ":raised_hand_with_fingers_splayed_tone4:" + ], + "aliases_ascii": [], + "keywords": [ + "hi", + "five", + "stop", + "halt" + ] + }, + "hand_splayed_tone5": { + "unicode": "1F590-1F3FF", + "unicode_alternates": "", + "name": "raised hand with fingers splayed tone 5", + "shortname": ":hand_splayed_tone5:", + "category": "people", + "aliases": [ + ":raised_hand_with_fingers_splayed_tone5:" + ], + "aliases_ascii": [], + "keywords": [ + "hi", + "five", + "stop", + "halt" + ] }, "hand_victory": { "unicode": "1F594", @@ -6898,9 +13947,13 @@ "name": "reversed victory hand", "shortname": ":hand_victory:", "category": "people", - "aliases": [":reversed_victory_hand:"], + "aliases": [ + ":reversed_victory_hand:" + ], "aliases_ascii": [], - "keywords": ["fu"] + "keywords": [ + "fu" + ] }, "handbag": { "unicode": "1F45C", @@ -6910,7 +13963,12 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["accessories", "accessory", "bag", "fashion"], + "keywords": [ + "accessories", + "accessory", + "bag", + "fashion" + ], "moji": "👜" }, "hard_disk": { @@ -6921,18 +13979,32 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["save", "technology", "storage", "information", "computer", "drive", "megabyte", "gigabyte", "hd"] + "keywords": [ + "save", + "technology", + "storage", + "information", + "computer", + "drive", + "megabyte", + "gigabyte", + "hd" + ] }, "hash": { "moji": "#⃣", "unicode": "0023-20E3", - "unicode_alternates": ["0023-FE0F-20E3"], + "unicode_alternates": [ + "0023-FE0F-20E3" + ], "name": "number sign", "shortname": ":hash:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["symbol"] + "keywords": [ + "symbol" + ] }, "hatched_chick": { "unicode": "1F425", @@ -6942,7 +14014,17 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["baby", "chicken", "chick", "baby", "bird", "chicken", "young", "woman", "cute"], + "keywords": [ + "baby", + "chicken", + "chick", + "baby", + "bird", + "chicken", + "young", + "woman", + "cute" + ], "moji": "🐥" }, "hatching_chick": { @@ -6953,9 +14035,33 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["born", "chicken", "egg", "chick", "egg", "baby", "bird", "chicken", "young", "woman", "cute"], + "keywords": [ + "born", + "chicken", + "egg", + "chick", + "egg", + "baby", + "bird", + "chicken", + "young", + "woman", + "cute" + ], "moji": "🐣" }, + "head_bandage": { + "unicode": "1F915", + "unicode_alternates": "", + "name": "face with head-bandage", + "shortname": ":head_bandage:", + "category": "people", + "aliases": [ + ":face_with_head_bandage:" + ], + "aliases_ascii": [], + "keywords": [] + }, "headphones": { "unicode": "1F3A7", "unicode_alternates": [], @@ -6964,7 +14070,19 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["gadgets", "music", "score", "headphone", "sound", "music", "ears", "beats", "buds", "audio", "listen"], + "keywords": [ + "gadgets", + "music", + "score", + "headphone", + "sound", + "music", + "ears", + "beats", + "buds", + "audio", + "listen" + ], "moji": "🎧" }, "hear_no_evil": { @@ -6975,19 +14093,47 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "monkey", "monkey", "ears", "hear", "sound", "kikazaru"], + "keywords": [ + "animal", + "monkey", + "monkey", + "ears", + "hear", + "sound", + "kikazaru" + ], "moji": "🙉" }, "heart": { "moji": "❤", "unicode": "2764", - "unicode_alternates": ["2764-FE0F"], + "unicode_alternates": [ + "2764-FE0F" + ], "name": "heavy black heart", "shortname": ":heart:", "category": "emoticons", "aliases": [], - "aliases_ascii": ["<3"], - "keywords": ["like", "love", "red", "pink", "black", "heart", "love", "passion", "romance", "intense", "desire", "death", "evil", "cold", "valentines"] + "aliases_ascii": [ + "<3" + ], + "keywords": [ + "like", + "love", + "red", + "pink", + "black", + "heart", + "love", + "passion", + "romance", + "intense", + "desire", + "death", + "evil", + "cold", + "valentines" + ] }, "heart_decoration": { "unicode": "1F49F", @@ -6997,9 +14143,29 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["like", "love", "purple-square"], + "keywords": [ + "like", + "love", + "purple-square" + ], "moji": "💟" }, + "heart_exclamation": { + "unicode": "2763", + "unicode_alternates": "", + "name": "heavy heart exclamation mark ornament", + "shortname": ":heart_exclamation:", + "category": "symbols", + "aliases": [ + ":heavy_heart_exclamation_mark_ornament:" + ], + "aliases_ascii": [], + "keywords": [ + "emotion", + "punctuation", + "symbol" + ] + }, "heart_eyes": { "unicode": "1F60D", "unicode_alternates": [], @@ -7008,7 +14174,22 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["affection", "crush", "face", "infatuation", "like", "love", "valentines", "smiling", "heart", "lovestruck", "love", "flirt", "smile", "heart-shaped"], + "keywords": [ + "affection", + "crush", + "face", + "infatuation", + "like", + "love", + "valentines", + "smiling", + "heart", + "lovestruck", + "love", + "flirt", + "smile", + "heart-shaped" + ], "moji": "😍" }, "heart_eyes_cat": { @@ -7019,7 +14200,17 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["affection", "animal", "cats", "like", "love", "valentines", "lovestruck", "love", "heart"], + "keywords": [ + "affection", + "animal", + "cats", + "like", + "love", + "valentines", + "lovestruck", + "love", + "heart" + ], "moji": "😻" }, "heart_tip": { @@ -7028,9 +14219,16 @@ "name": "heart with tip on the left", "shortname": ":heart_tip:", "category": "celebration", - "aliases": [":heart_with_tip_on_the_left:"], + "aliases": [ + ":heart_with_tip_on_the_left:" + ], "aliases_ascii": [], - "keywords": ["affection", "like", "love", "valentines"] + "keywords": [ + "affection", + "like", + "love", + "valentines" + ] }, "heartbeat": { "unicode": "1F493", @@ -7040,7 +14238,12 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["affection", "like", "love", "valentines"], + "keywords": [ + "affection", + "like", + "love", + "valentines" + ], "moji": "💓" }, "heartpulse": { @@ -7051,29 +14254,44 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["affection", "like", "love", "valentines"], + "keywords": [ + "affection", + "like", + "love", + "valentines" + ], "moji": "💗" }, "hearts": { "unicode": "2665", - "unicode_alternates": ["2665-FE0F"], + "unicode_alternates": [ + "2665-FE0F" + ], "name": "black heart suit", "shortname": ":hearts:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["cards", "poker"], + "keywords": [ + "cards", + "poker" + ], "moji": "♥" }, "heavy_check_mark": { "unicode": "2714", - "unicode_alternates": ["2714-FE0F"], + "unicode_alternates": [ + "2714-FE0F" + ], "name": "heavy check mark", "shortname": ":heavy_check_mark:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["nike", "ok"], + "keywords": [ + "nike", + "ok" + ], "moji": "✔" }, "heavy_division_sign": { @@ -7084,7 +14302,11 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["calculation", "divide", "math"], + "keywords": [ + "calculation", + "divide", + "math" + ], "moji": "➗" }, "heavy_dollar_sign": { @@ -7095,7 +14317,18 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["currency", "money", "payment", "dollar", "currency", "money", "cash", "sale", "purchase", "value"], + "keywords": [ + "currency", + "money", + "payment", + "dollar", + "currency", + "money", + "cash", + "sale", + "purchase", + "value" + ], "moji": "💲" }, "heavy_minus_sign": { @@ -7106,18 +14339,26 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["calculation", "math"], + "keywords": [ + "calculation", + "math" + ], "moji": "➖" }, "heavy_multiplication_x": { "unicode": "2716", - "unicode_alternates": ["2716-FE0F"], + "unicode_alternates": [ + "2716-FE0F" + ], "name": "heavy multiplication x", "shortname": ":heavy_multiplication_x:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["calculation", "math"], + "keywords": [ + "calculation", + "math" + ], "moji": "✖" }, "heavy_plus_sign": { @@ -7128,7 +14369,10 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["calculation", "math"], + "keywords": [ + "calculation", + "math" + ], "moji": "➕" }, "helicopter": { @@ -7139,9 +14383,33 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["transportation", "vehicle", "helicopter", "helo", "gyro", "gyrocopter"], + "keywords": [ + "transportation", + "vehicle", + "helicopter", + "helo", + "gyro", + "gyrocopter" + ], "moji": "🚁" }, + "helmet_with_cross": { + "unicode": "26D1", + "unicode_alternates": "", + "name": "helmet with white cross", + "shortname": ":helmet_with_cross:", + "category": "people", + "aliases": [ + ":helmet_with_white_cross:" + ], + "aliases_ascii": [], + "keywords": [ + "aid", + "face", + "hat", + "person" + ] + }, "herb": { "unicode": "1F33F", "unicode_alternates": [], @@ -7150,7 +14418,19 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["grass", "lawn", "medicine", "plant", "vegetable", "weed", "herb", "spice", "plant", "cook", "cooking"], + "keywords": [ + "grass", + "lawn", + "medicine", + "plant", + "vegetable", + "weed", + "herb", + "spice", + "plant", + "cook", + "cooking" + ], "moji": "🌿" }, "hibiscus": { @@ -7161,7 +14441,14 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["flowers", "plant", "vegetable", "hibiscus", "flower", "warm"], + "keywords": [ + "flowers", + "plant", + "vegetable", + "hibiscus", + "flower", + "warm" + ], "moji": "🌺" }, "high_brightness": { @@ -7172,7 +14459,11 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["light", "summer", "sun"], + "keywords": [ + "light", + "summer", + "sun" + ], "moji": "🔆" }, "high_heel": { @@ -7183,9 +14474,23 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["fashion", "female", "shoes"], + "keywords": [ + "fashion", + "female", + "shoes" + ], "moji": "👠" }, + "hockey": { + "unicode": "1F3D2", + "unicode_alternates": "", + "name": "ice hockey stick and puck", + "shortname": ":hockey:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [] + }, "hole": { "unicode": "1F573", "unicode_alternates": [], @@ -7194,7 +14499,10 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["pit", "well"] + "keywords": [ + "pit", + "well" + ] }, "homes": { "unicode": "1F3D8", @@ -7202,9 +14510,19 @@ "name": "house buildings", "shortname": ":homes:", "category": "travel_places", - "aliases": [":house_buildings:"], + "aliases": [ + ":house_buildings:" + ], "aliases_ascii": [], - "keywords": ["home", "residence", "dwelling", "mansion", "bungalow", "ranch", "craftsman"] + "keywords": [ + "home", + "residence", + "dwelling", + "mansion", + "bungalow", + "ranch", + "craftsman" + ] }, "honey_pot": { "unicode": "1F36F", @@ -7214,7 +14532,15 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["bees", "sweet", "honey", "pot", "bees", "pooh", "bear"], + "keywords": [ + "bees", + "sweet", + "honey", + "pot", + "bees", + "pooh", + "bear" + ], "moji": "🍯" }, "horse": { @@ -7225,7 +14551,10 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "brown"], + "keywords": [ + "animal", + "brown" + ], "moji": "🐴" }, "horse_racing": { @@ -7236,9 +14565,103 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "betting", "competition", "horse", "race", "racing", "jockey", "triple crown"], + "keywords": [ + "animal", + "betting", + "competition", + "horse", + "race", + "racing", + "jockey", + "triple crown" + ], "moji": "🏇" }, + "horse_racing_tone1": { + "unicode": "1F3C7-1F3FB", + "unicode_alternates": "", + "name": "horse racing tone 1", + "shortname": ":horse_racing_tone1:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "animal", + "betting", + "competition", + "race", + "jockey", + "triple crown" + ] + }, + "horse_racing_tone2": { + "unicode": "1F3C7-1F3FC", + "unicode_alternates": "", + "name": "horse racing tone 2", + "shortname": ":horse_racing_tone2:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "animal", + "betting", + "competition", + "race", + "jockey", + "triple crown" + ] + }, + "horse_racing_tone3": { + "unicode": "1F3C7-1F3FD", + "unicode_alternates": "", + "name": "horse racing tone 3", + "shortname": ":horse_racing_tone3:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "animal", + "betting", + "competition", + "race", + "jockey", + "triple crown" + ] + }, + "horse_racing_tone4": { + "unicode": "1F3C7-1F3FE", + "unicode_alternates": "", + "name": "horse racing tone 4", + "shortname": ":horse_racing_tone4:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "animal", + "betting", + "competition", + "race", + "jockey", + "triple crown" + ] + }, + "horse_racing_tone5": { + "unicode": "1F3C7-1F3FF", + "unicode_alternates": "", + "name": "horse racing tone 5", + "shortname": ":horse_racing_tone5:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "animal", + "betting", + "competition", + "race", + "jockey", + "triple crown" + ] + }, "hospital": { "unicode": "1F3E5", "unicode_alternates": [], @@ -7247,7 +14670,12 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["building", "doctor", "health", "surgery"], + "keywords": [ + "building", + "doctor", + "health", + "surgery" + ], "moji": "🏥" }, "hot_pepper": { @@ -7258,7 +14686,27 @@ "category": "food_drink", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "nature", "spicy", "chili", "cayenne", "habanero", "jalapeno"] + "keywords": [ + "food", + "nature", + "spicy", + "chili", + "cayenne", + "habanero", + "jalapeno" + ] + }, + "hotdog": { + "unicode": "1F32D", + "unicode_alternates": "", + "name": "hot dog", + "shortname": ":hotdog:", + "category": "foods", + "aliases": [ + ":hot_dog:" + ], + "aliases_ascii": [], + "keywords": [] }, "hotel": { "unicode": "1F3E8", @@ -7268,29 +14716,50 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["accomodation", "building", "checkin", "whotel", "hotel", "motel", "holiday inn", "hospital"], + "keywords": [ + "accomodation", + "building", + "checkin", + "whotel", + "hotel", + "motel", + "holiday inn", + "hospital" + ], "moji": "🏨" }, "hotsprings": { "unicode": "2668", - "unicode_alternates": ["2668-FE0F"], + "unicode_alternates": [ + "2668-FE0F" + ], "name": "hot springs", "shortname": ":hotsprings:", "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["bath", "relax", "warm"], + "keywords": [ + "bath", + "relax", + "warm" + ], "moji": "♨" }, "hourglass": { "unicode": "231B", - "unicode_alternates": ["231B-FE0F"], + "unicode_alternates": [ + "231B-FE0F" + ], "name": "hourglass", "shortname": ":hourglass:", "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["clock", "oldschool", "time"], + "keywords": [ + "clock", + "oldschool", + "time" + ], "moji": "⌛" }, "hourglass_flowing_sand": { @@ -7301,7 +14770,11 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["countdown", "oldschool", "time"], + "keywords": [ + "countdown", + "oldschool", + "time" + ], "moji": "⏳" }, "house": { @@ -7312,7 +14785,18 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["building", "home", "house", "home", "residence", "dwelling", "mansion", "bungalow", "ranch", "craftsman"], + "keywords": [ + "building", + "home", + "house", + "home", + "residence", + "dwelling", + "mansion", + "bungalow", + "ranch", + "craftsman" + ], "moji": "🏠" }, "house_abandoned": { @@ -7321,9 +14805,24 @@ "name": "derelict house building", "shortname": ":house_abandoned:", "category": "travel_places", - "aliases": [":derelict_house_building:"], + "aliases": [ + ":derelict_house_building:" + ], "aliases_ascii": [], - "keywords": ["home", "residence", "dwelling", "mansion", "bungalow", "ranch", "craftsman", "boarded", "abandoned", "vacant", "run down", "shoddy"] + "keywords": [ + "home", + "residence", + "dwelling", + "mansion", + "bungalow", + "ranch", + "craftsman", + "boarded", + "abandoned", + "vacant", + "run down", + "shoddy" + ] }, "house_with_garden": { "unicode": "1F3E1", @@ -7333,9 +14832,25 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["home", "nature", "plant"], + "keywords": [ + "home", + "nature", + "plant" + ], "moji": "🏡" }, + "hugging": { + "unicode": "1F917", + "unicode_alternates": "", + "name": "hugging face", + "shortname": ":hugging:", + "category": "people", + "aliases": [ + ":hugging_face:" + ], + "aliases_ascii": [], + "keywords": [] + }, "hushed": { "unicode": "1F62F", "unicode_alternates": [], @@ -7344,7 +14859,14 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["face", "woo", "quiet", "hush", "whisper", "silent"], + "keywords": [ + "face", + "woo", + "quiet", + "hush", + "whisper", + "silent" + ], "moji": "😯" }, "ice_cream": { @@ -7355,9 +14877,37 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["desert", "food", "hot", "icecream", "ice", "cream", "dairy", "dessert", "cold", "soft", "serve", "cone", "waffle"], + "keywords": [ + "desert", + "food", + "hot", + "icecream", + "ice", + "cream", + "dairy", + "dessert", + "cold", + "soft", + "serve", + "cone", + "waffle" + ], "moji": "🍨" }, + "ice_skate": { + "unicode": "26F8", + "unicode_alternates": "", + "name": "ice skate", + "shortname": ":ice_skate:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "place", + "sport", + "travel" + ] + }, "icecream": { "unicode": "1F366", "unicode_alternates": [], @@ -7366,9 +14916,39 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["desert", "food", "hot", "icecream", "ice", "cream", "dairy", "dessert", "cold", "soft", "serve", "cone", "yogurt"], + "keywords": [ + "desert", + "food", + "hot", + "icecream", + "ice", + "cream", + "dairy", + "dessert", + "cold", + "soft", + "serve", + "cone", + "yogurt" + ], "moji": "🍦" }, + "id": { + "unicode": "1F194", + "unicode_alternates": "", + "name": "squared id", + "shortname": ":id:", + "category": "symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "purple-square", + "identification", + "identity", + "symbol", + "word" + ] + }, "ideograph_advantage": { "unicode": "1F250", "unicode_alternates": [], @@ -7377,7 +14957,12 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["chinese", "get", "kanji", "obtain"], + "keywords": [ + "chinese", + "get", + "kanji", + "obtain" + ], "moji": "🉐" }, "imp": { @@ -7388,7 +14973,14 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["angry", "devil", "evil", "horns", "cute", "devil"], + "keywords": [ + "angry", + "devil", + "evil", + "horns", + "cute", + "devil" + ], "moji": "👿" }, "inbox_tray": { @@ -7399,7 +14991,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["documents", "email"], + "keywords": [ + "documents", + "email" + ], "moji": "📥" }, "incoming_envelope": { @@ -7410,7 +15005,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["email", "inbox"], + "keywords": [ + "email", + "inbox" + ], "moji": "📨" }, "info": { @@ -7419,9 +15017,13 @@ "name": "circled information source", "shortname": ":info:", "category": "objects_symbols", - "aliases": [":circled_information_source:"], + "aliases": [ + ":circled_information_source:" + ], "aliases_ascii": [], - "keywords": ["icon"] + "keywords": [ + "icon" + ] }, "information_desk_person": { "unicode": "1F481", @@ -7431,18 +15033,147 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["female", "girl", "human", "woman", "information", "help", "question", "answer", "sassy", "unimpressed", "attitude", "snarky"], + "keywords": [ + "female", + "girl", + "human", + "woman", + "information", + "help", + "question", + "answer", + "sassy", + "unimpressed", + "attitude", + "snarky" + ], "moji": "💁" }, + "information_desk_person_tone1": { + "unicode": "1F481-1F3FB", + "unicode_alternates": "", + "name": "information desk person tone 1", + "shortname": ":information_desk_person_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "human", + "woman", + "help", + "question", + "answer", + "sassy", + "unimpressed", + "attitude", + "snarky" + ] + }, + "information_desk_person_tone2": { + "unicode": "1F481-1F3FC", + "unicode_alternates": "", + "name": "information desk person tone 2", + "shortname": ":information_desk_person_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "human", + "woman", + "help", + "question", + "answer", + "sassy", + "unimpressed", + "attitude", + "snarky" + ] + }, + "information_desk_person_tone3": { + "unicode": "1F481-1F3FD", + "unicode_alternates": "", + "name": "information desk person tone 3", + "shortname": ":information_desk_person_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "human", + "woman", + "help", + "question", + "answer", + "sassy", + "unimpressed", + "attitude", + "snarky" + ] + }, + "information_desk_person_tone4": { + "unicode": "1F481-1F3FE", + "unicode_alternates": "", + "name": "information desk person tone 4", + "shortname": ":information_desk_person_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "human", + "woman", + "help", + "question", + "answer", + "sassy", + "unimpressed", + "attitude", + "snarky" + ] + }, + "information_desk_person_tone5": { + "unicode": "1F481-1F3FF", + "unicode_alternates": "", + "name": "information desk person tone 5", + "shortname": ":information_desk_person_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "human", + "woman", + "help", + "question", + "answer", + "sassy", + "unimpressed", + "attitude", + "snarky" + ] + }, "information_source": { "unicode": "2139", - "unicode_alternates": ["2139-FE0F"], + "unicode_alternates": [ + "2139-FE0F" + ], "name": "information source", "shortname": ":information_source:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["alphabet", "blue-square", "letter"], + "keywords": [ + "alphabet", + "blue-square", + "letter" + ], "moji": "ℹ" }, "innocent": { @@ -7452,19 +15183,49 @@ "shortname": ":innocent:", "category": "emoticons", "aliases": [], - "aliases_ascii": ["O:-)", "0:-3", "0:3", "0:-)", "0:)", "0;^)", "O:-)", "O:)", "O;-)", "O=)", "0;-)", "O:-3", "O:3"], - "keywords": ["angel", "face", "halo", "halo", "angel", "innocent", "ring", "circle", "heaven"], + "aliases_ascii": [ + "O:-)", + "0:-3", + "0:3", + "0:-)", + "0:)", + "0;^)", + "O:-)", + "O:)", + "O;-)", + "O=)", + "0;-)", + "O:-3", + "O:3" + ], + "keywords": [ + "angel", + "face", + "halo", + "halo", + "angel", + "innocent", + "ring", + "circle", + "heaven" + ], "moji": "😇" }, "interrobang": { "unicode": "2049", - "unicode_alternates": ["2049-FE0F"], + "unicode_alternates": [ + "2049-FE0F" + ], "name": "exclamation question mark", "shortname": ":interrobang:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["punctuation", "surprise", "wat"], + "keywords": [ + "punctuation", + "surprise", + "wat" + ], "moji": "⁉" }, "iphone": { @@ -7475,7 +15236,12 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["apple", "dial", "gadgets", "technology"], + "keywords": [ + "apple", + "dial", + "gadgets", + "technology" + ], "moji": "📱" }, "island": { @@ -7484,9 +15250,15 @@ "name": "desert island", "shortname": ":island:", "category": "travel_places", - "aliases": [":desert_island:"], + "aliases": [ + ":desert_island:" + ], "aliases_ascii": [], - "keywords": ["land", "solitude", "alone"] + "keywords": [ + "land", + "solitude", + "alone" + ] }, "izakaya_lantern": { "unicode": "1F3EE", @@ -7496,7 +15268,17 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["light", "izakaya", "lantern", "stay", "drink", "alcohol", "bar", "sake", "restaurant"], + "keywords": [ + "light", + "izakaya", + "lantern", + "stay", + "drink", + "alcohol", + "bar", + "sake", + "restaurant" + ], "moji": "🏮" }, "jack_o_lantern": { @@ -7507,7 +15289,24 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["halloween", "jack-o-lantern", "pumpkin", "halloween", "holiday", "carve", "autumn", "fall", "october", "saints", "costume", "spooky", "horror", "scary", "scared", "dead"], + "keywords": [ + "halloween", + "jack-o-lantern", + "pumpkin", + "halloween", + "holiday", + "carve", + "autumn", + "fall", + "october", + "saints", + "costume", + "spooky", + "horror", + "scary", + "scared", + "dead" + ], "moji": "🎃" }, "japan": { @@ -7518,7 +15317,9 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["nation"], + "keywords": [ + "nation" + ], "moji": "🗾" }, "japanese_castle": { @@ -7529,7 +15330,17 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["building", "photo", "castle", "japanese", "residence", "royalty", "fort", "fortified", "fortress"], + "keywords": [ + "building", + "photo", + "castle", + "japanese", + "residence", + "royalty", + "fort", + "fortified", + "fortress" + ], "moji": "🏯" }, "japanese_goblin": { @@ -7540,7 +15351,24 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["evil", "mask", "red", "japanese", "tengu", "supernatural", "avian", "demon", "goblin", "mask", "theater", "nose", "frown", "mustache", "anger", "frustration"], + "keywords": [ + "evil", + "mask", + "red", + "japanese", + "tengu", + "supernatural", + "avian", + "demon", + "goblin", + "mask", + "theater", + "nose", + "frown", + "mustache", + "anger", + "frustration" + ], "moji": "👺" }, "japanese_ogre": { @@ -7551,7 +15379,21 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["monster", "japanese", "oni", "demon", "troll", "ogre", "folklore", "monster", "devil", "mask", "theater", "horns", "teeth"], + "keywords": [ + "monster", + "japanese", + "oni", + "demon", + "troll", + "ogre", + "folklore", + "monster", + "devil", + "mask", + "theater", + "horns", + "teeth" + ], "moji": "👹" }, "jeans": { @@ -7562,7 +15404,19 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["fashion", "shopping", "jeans", "pants", "blue", "denim", "levi's", "levi", "designer", "work", "skinny"], + "keywords": [ + "fashion", + "shopping", + "jeans", + "pants", + "blue", + "denim", + "levi's", + "levi", + "designer", + "work", + "skinny" + ], "moji": "👖" }, "jet_up": { @@ -7571,9 +15425,13 @@ "name": "up-pointing military airplane", "shortname": ":jet_up:", "category": "travel_places", - "aliases": [":up_pointing_military_airplane:"], + "aliases": [ + ":up_pointing_military_airplane:" + ], "aliases_ascii": [], - "keywords": ["jet"] + "keywords": [ + "jet" + ] }, "joy": { "unicode": "1F602", @@ -7582,8 +15440,22 @@ "shortname": ":joy:", "category": "emoticons", "aliases": [], - "aliases_ascii": [":')", ":'-)"], - "keywords": ["cry", "face", "haha", "happy", "tears", "tears", "cry", "joy", "happy", "weep"], + "aliases_ascii": [ + ":')", + ":'-)" + ], + "keywords": [ + "cry", + "face", + "haha", + "happy", + "tears", + "tears", + "cry", + "joy", + "happy", + "weep" + ], "moji": "😂" }, "joy_cat": { @@ -7594,7 +15466,17 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "cats", "haha", "happy", "tears", "happy", "tears", "cry", "joy"], + "keywords": [ + "animal", + "cats", + "haha", + "happy", + "tears", + "happy", + "tears", + "cry", + "joy" + ], "moji": "😹" }, "joystick": { @@ -7605,7 +15487,21 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["games", "atari", "controller"] + "keywords": [ + "games", + "atari", + "controller" + ] + }, + "kaaba": { + "unicode": "1F54B", + "unicode_alternates": "", + "name": "kaaba", + "shortname": ":kaaba:", + "category": "travel", + "aliases": [], + "aliases_ascii": [], + "keywords": [] }, "key": { "unicode": "1F511", @@ -7615,7 +15511,11 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["door", "lock", "password"], + "keywords": [ + "door", + "lock", + "password" + ], "moji": "🔑" }, "key2": { @@ -7624,9 +15524,16 @@ "name": "old key", "shortname": ":key2:", "category": "objects_symbols", - "aliases": [":old_key:"], + "aliases": [ + ":old_key:" + ], "aliases_ascii": [], - "keywords": ["door", "lock", "password", "skeleton"] + "keywords": [ + "door", + "lock", + "password", + "skeleton" + ] }, "keyboard": { "unicode": "1F5AE", @@ -7634,9 +15541,16 @@ "name": "wired keyboard", "shortname": ":keyboard:", "category": "objects_symbols", - "aliases": [":wired_keyboard:"], + "aliases": [ + ":wired_keyboard:" + ], "aliases_ascii": [], - "keywords": ["typing", "keys", "input", "device"] + "keywords": [ + "typing", + "keys", + "input", + "device" + ] }, "keyboard_mouse": { "unicode": "1F5A6", @@ -7644,9 +15558,15 @@ "name": "keyboard and mouse", "shortname": ":keyboard_mouse:", "category": "objects_symbols", - "aliases": [":keyboard_and_mouse:"], + "aliases": [ + ":keyboard_and_mouse:" + ], "aliases_ascii": [], - "keywords": ["computer", "input", "desktop"] + "keywords": [ + "computer", + "input", + "desktop" + ] }, "keyboard_with_jacks": { "unicode": "1F398", @@ -7654,9 +15574,15 @@ "name": "musical keyboard with jacks", "shortname": ":keyboard_with_jacks:", "category": "objects_symbols", - "aliases": [":musical_keyboard_with_jacks:"], + "aliases": [ + ":musical_keyboard_with_jacks:" + ], "aliases_ascii": [], - "keywords": ["music", "instrument", "midi"] + "keywords": [ + "music", + "instrument", + "midi" + ] }, "keycap_ten": { "unicode": "1F51F", @@ -7666,7 +15592,11 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["10", "blue-square", "numbers"], + "keywords": [ + "10", + "blue-square", + "numbers" + ], "moji": "🔟" }, "kimono": { @@ -7677,7 +15607,13 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["dress", "fashion", "female", "japanese", "women"], + "keywords": [ + "dress", + "fashion", + "female", + "japanese", + "women" + ], "moji": "👘" }, "kiss": { @@ -7688,28 +15624,57 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["affection", "face", "like", "lips", "love", "valentines"], + "keywords": [ + "affection", + "face", + "like", + "lips", + "love", + "valentines" + ], "moji": "💋" }, "kiss_mm": { "unicode": "1F468-2764-1F48B-1F468", - "unicode_alternates": ["1F468-200D-2764-FE0F-200D-1F48B-200D-1F468"], + "unicode_alternates": [ + "1F468-200D-2764-FE0F-200D-1F48B-200D-1F468" + ], "name": "kiss (man,man)", "shortname": ":kiss_mm:", "category": "people", - "aliases": [":couplekiss_mm:"], + "aliases": [ + ":couplekiss_mm:" + ], "aliases_ascii": [], - "keywords": ["dating", "like", "love", "marriage", "valentines", "couple"] + "keywords": [ + "dating", + "like", + "love", + "marriage", + "valentines", + "couple" + ] }, "kiss_ww": { "unicode": "1F469-2764-1F48B-1F469", - "unicode_alternates": ["1F469-200D-2764-FE0F-200D-1F48B-200D-1F469"], + "unicode_alternates": [ + "1F469-200D-2764-FE0F-200D-1F48B-200D-1F469" + ], "name": "kiss (woman,woman)", "shortname": ":kiss_ww:", "category": "people", - "aliases": [":couplekiss_ww:"], + "aliases": [ + ":couplekiss_ww:" + ], "aliases_ascii": [], - "keywords": ["dating", "like", "love", "marriage", "valentines", "couple"] + "keywords": [ + "dating", + "like", + "love", + "marriage", + "valentines", + "couple" + ] }, "kissing": { "unicode": "1F617", @@ -7719,7 +15684,19 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["3", "face", "infatuation", "like", "love", "valentines", "kissing", "kiss", "pucker", "lips", "smooch"], + "keywords": [ + "3", + "face", + "infatuation", + "like", + "love", + "valentines", + "kissing", + "kiss", + "pucker", + "lips", + "smooch" + ], "moji": "😗" }, "kissing_cat": { @@ -7730,7 +15707,15 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "cats", "passion", "kiss", "puckered", "heart", "love"], + "keywords": [ + "animal", + "cats", + "passion", + "kiss", + "puckered", + "heart", + "love" + ], "moji": "😽" }, "kissing_closed_eyes": { @@ -7741,7 +15726,21 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["affection", "face", "infatuation", "like", "love", "valentines", "kissing", "kiss", "passion", "puckered", "heart", "love", "smooch"], + "keywords": [ + "affection", + "face", + "infatuation", + "like", + "love", + "valentines", + "kissing", + "kiss", + "passion", + "puckered", + "heart", + "love", + "smooch" + ], "moji": "😚" }, "kissing_heart": { @@ -7751,8 +15750,25 @@ "shortname": ":kissing_heart:", "category": "emoticons", "aliases": [], - "aliases_ascii": [":*", ":-*", "=*", ":^*"], - "keywords": ["affection", "face", "infatuation", "kiss", "blowing kiss", "heart", "love", "lips", "like", "love", "valentines"], + "aliases_ascii": [ + ":*", + ":-*", + "=*", + ":^*" + ], + "keywords": [ + "affection", + "face", + "infatuation", + "kiss", + "blowing kiss", + "heart", + "love", + "lips", + "like", + "love", + "valentines" + ], "moji": "😘" }, "kissing_smiling_eyes": { @@ -7763,7 +15779,18 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["affection", "face", "infatuation", "valentines", "kissing", "kiss", "smile", "pucker", "lips", "smooch"], + "keywords": [ + "affection", + "face", + "infatuation", + "valentines", + "kissing", + "kiss", + "smile", + "pucker", + "lips", + "smooch" + ], "moji": "😙" }, "knife": { @@ -7785,7 +15812,10 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature"], + "keywords": [ + "animal", + "nature" + ], "moji": "🐨" }, "koko": { @@ -7796,7 +15826,13 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square", "destination", "here", "japanese", "katakana"], + "keywords": [ + "blue-square", + "destination", + "here", + "japanese", + "katakana" + ], "moji": "🈁" }, "label": { @@ -7807,7 +15843,9 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["tag"] + "keywords": [ + "tag" + ] }, "large_blue_circle": { "unicode": "1F535", @@ -7828,7 +15866,9 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["shape"], + "keywords": [ + "shape" + ], "moji": "🔷" }, "large_orange_diamond": { @@ -7839,7 +15879,9 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["shape"], + "keywords": [ + "shape" + ], "moji": "🔶" }, "last_quarter_moon": { @@ -7850,7 +15892,16 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["nature", "moon", "last", "quarter", "sky", "night", "cheese", "phase"], + "keywords": [ + "nature", + "moon", + "last", + "quarter", + "sky", + "night", + "cheese", + "phase" + ], "moji": "🌗" }, "last_quarter_moon_with_face": { @@ -7861,7 +15912,18 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["nature", "moon", "last", "quarter", "anthropomorphic", "face", "sky", "night", "cheese", "phase"], + "keywords": [ + "nature", + "moon", + "last", + "quarter", + "anthropomorphic", + "face", + "sky", + "night", + "cheese", + "phase" + ], "moji": "🌜" }, "laughing": { @@ -7870,9 +15932,23 @@ "name": "smiling face with open mouth and tightly-closed ey", "shortname": ":laughing:", "category": "emoticons", - "aliases": [":satisfied:"], - "aliases_ascii": [">:)", ">;)", ">:-)", ">=)"], - "keywords": ["happy", "joy", "lol", "smiling", "laughing", "laugh"], + "aliases": [ + ":satisfied:" + ], + "aliases_ascii": [ + ">:)", + ">;)", + ">:-)", + ">=)" + ], + "keywords": [ + "happy", + "joy", + "lol", + "smiling", + "laughing", + "laugh" + ], "moji": "😆" }, "leaves": { @@ -7883,7 +15959,19 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["grass", "lawn", "nature", "plant", "tree", "vegetable", "leaves", "leaf", "wind", "float", "fluttering"], + "keywords": [ + "grass", + "lawn", + "nature", + "plant", + "tree", + "vegetable", + "leaves", + "leaf", + "wind", + "float", + "fluttering" + ], "moji": "🍃" }, "ledger": { @@ -7894,7 +15982,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["notes", "paper"], + "keywords": [ + "notes", + "paper" + ], "moji": "📒" }, "left_luggage": { @@ -7905,7 +15996,14 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square", "travel", "bag", "baggage", "luggage", "travel"], + "keywords": [ + "blue-square", + "travel", + "bag", + "baggage", + "luggage", + "travel" + ], "moji": "🛅" }, "left_receiver": { @@ -7914,24 +16012,36 @@ "name": "left hand telephone receiver", "shortname": ":left_receiver:", "category": "objects_symbols", - "aliases": [":left_hand_telephone_receiver:"], + "aliases": [ + ":left_hand_telephone_receiver:" + ], "aliases_ascii": [], - "keywords": ["communication", "dial", "technology"] + "keywords": [ + "communication", + "dial", + "technology" + ] }, "left_right_arrow": { "unicode": "2194", - "unicode_alternates": ["2194-FE0F"], + "unicode_alternates": [ + "2194-FE0F" + ], "name": "left right arrow", "shortname": ":left_right_arrow:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["shape"], + "keywords": [ + "shape" + ], "moji": "↔" }, "leftwards_arrow_with_hook": { "unicode": "21A9", - "unicode_alternates": ["21A9-FE0F"], + "unicode_alternates": [ + "21A9-FE0F" + ], "name": "leftwards arrow with hook", "shortname": ":leftwards_arrow_with_hook:", "category": "other", @@ -7948,18 +16058,39 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["fruit", "nature", "lemon", "yellow", "citrus"], + "keywords": [ + "fruit", + "nature", + "lemon", + "yellow", + "citrus" + ], "moji": "🍋" }, "leo": { "unicode": "264C", - "unicode_alternates": ["264C-FE0F"], + "unicode_alternates": [ + "264C-FE0F" + ], "name": "leo", "shortname": ":leo:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["leo", "lion", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "purple-square", "sign", "zodiac", "horoscope"], + "keywords": [ + "leo", + "lion", + "astrology", + "greek", + "constellation", + "stars", + "zodiac", + "sign", + "purple-square", + "sign", + "zodiac", + "horoscope" + ], "moji": "♌" }, "leopard": { @@ -7970,7 +16101,15 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature", "leopard", "cat", "spot", "spotted", "sexy"], + "keywords": [ + "animal", + "nature", + "leopard", + "cat", + "spot", + "spotted", + "sexy" + ], "moji": "🐆" }, "level_slider": { @@ -7981,7 +16120,9 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["controls"] + "keywords": [ + "controls" + ] }, "levitate": { "unicode": "1F574", @@ -7989,19 +16130,39 @@ "name": "man in business suit levitating", "shortname": ":levitate:", "category": "people", - "aliases": [":man_in_business_suit_levitating:"], + "aliases": [ + ":man_in_business_suit_levitating:" + ], "aliases_ascii": [], - "keywords": ["hover", "exclamation"] + "keywords": [ + "hover", + "exclamation" + ] }, "libra": { "unicode": "264E", - "unicode_alternates": ["264E-FE0F"], + "unicode_alternates": [ + "264E-FE0F" + ], "name": "libra", "shortname": ":libra:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["libra", "scales", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "purple-square", "sign", "zodiac", "horoscope"], + "keywords": [ + "libra", + "scales", + "astrology", + "greek", + "constellation", + "stars", + "zodiac", + "sign", + "purple-square", + "sign", + "zodiac", + "horoscope" + ], "moji": "♎" }, "lifter": { @@ -8010,9 +16171,101 @@ "name": "weight lifter", "shortname": ":lifter:", "category": "activity", - "aliases": [":weight_lifter:"], + "aliases": [ + ":weight_lifter:" + ], "aliases_ascii": [], - "keywords": ["bench", "press", "squats", "deadlift"] + "keywords": [ + "bench", + "press", + "squats", + "deadlift" + ] + }, + "lifter_tone1": { + "unicode": "1F3CB-1F3FB", + "unicode_alternates": "", + "name": "weight lifter tone 1", + "shortname": ":lifter_tone1:", + "category": "activity", + "aliases": [ + ":weight_lifter_tone1:" + ], + "aliases_ascii": [], + "keywords": [ + "bench", + "press", + "squats", + "deadlift" + ] + }, + "lifter_tone2": { + "unicode": "1F3CB-1F3FC", + "unicode_alternates": "", + "name": "weight lifter tone 2", + "shortname": ":lifter_tone2:", + "category": "activity", + "aliases": [ + ":weight_lifter_tone2:" + ], + "aliases_ascii": [], + "keywords": [ + "bench", + "press", + "squats", + "deadlift" + ] + }, + "lifter_tone3": { + "unicode": "1F3CB-1F3FD", + "unicode_alternates": "", + "name": "weight lifter tone 3", + "shortname": ":lifter_tone3:", + "category": "activity", + "aliases": [ + ":weight_lifter_tone3:" + ], + "aliases_ascii": [], + "keywords": [ + "bench", + "press", + "squats", + "deadlift" + ] + }, + "lifter_tone4": { + "unicode": "1F3CB-1F3FE", + "unicode_alternates": "", + "name": "weight lifter tone 4", + "shortname": ":lifter_tone4:", + "category": "activity", + "aliases": [ + ":weight_lifter_tone4:" + ], + "aliases_ascii": [], + "keywords": [ + "bench", + "press", + "squats", + "deadlift" + ] + }, + "lifter_tone5": { + "unicode": "1F3CB-1F3FF", + "unicode_alternates": "", + "name": "weight lifter tone 5", + "shortname": ":lifter_tone5:", + "category": "activity", + "aliases": [ + ":weight_lifter_tone5:" + ], + "aliases_ascii": [], + "keywords": [ + "bench", + "press", + "squats", + "deadlift" + ] }, "light_check_mark": { "unicode": "1F5F8", @@ -8020,9 +16273,13 @@ "name": "light check mark", "shortname": ":light_check_mark:", "category": "objects_symbols", - "aliases": [":light_mark:"], + "aliases": [ + ":light_mark:" + ], "aliases_ascii": [], - "keywords": ["vote"] + "keywords": [ + "vote" + ] }, "light_rail": { "unicode": "1F688", @@ -8032,7 +16289,13 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["transportation", "vehicle", "train", "rail", "light"], + "keywords": [ + "transportation", + "vehicle", + "train", + "rail", + "light" + ], "moji": "🚈" }, "link": { @@ -8043,9 +16306,24 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["rings", "url"], + "keywords": [ + "rings", + "url" + ], "moji": "🔗" }, + "lion_face": { + "unicode": "1F981", + "unicode_alternates": "", + "name": "lion face", + "shortname": ":lion_face:", + "category": "nature", + "aliases": [ + ":lion:" + ], + "aliases_ascii": [], + "keywords": [] + }, "lips": { "unicode": "1F444", "unicode_alternates": [], @@ -8054,7 +16332,10 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["kiss", "mouth"], + "keywords": [ + "kiss", + "mouth" + ], "moji": "👄" }, "lips2": { @@ -8065,7 +16346,10 @@ "category": "people", "aliases": [], "aliases_ascii": [], - "keywords": ["kiss", "mouth"] + "keywords": [ + "kiss", + "mouth" + ] }, "lipstick": { "unicode": "1F484", @@ -8075,7 +16359,11 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["fashion", "female", "girl"], + "keywords": [ + "fashion", + "female", + "girl" + ], "moji": "💄" }, "lock": { @@ -8086,7 +16374,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["password", "security"], + "keywords": [ + "password", + "security" + ], "moji": "🔒" }, "lock_with_ink_pen": { @@ -8097,7 +16388,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["secret", "security"], + "keywords": [ + "secret", + "security" + ], "moji": "🔏" }, "lollipop": { @@ -8108,7 +16402,18 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["candy", "food", "snack", "sweet", "lollipop", "stick", "lick", "sweet", "sugar", "candy"], + "keywords": [ + "candy", + "food", + "snack", + "sweet", + "lollipop", + "stick", + "lick", + "sweet", + "sugar", + "candy" + ], "moji": "🍭" }, "loop": { @@ -8119,7 +16424,9 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["curly"], + "keywords": [ + "curly" + ], "moji": "➿" }, "loud_sound": { @@ -8141,7 +16448,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["sound", "volume"], + "keywords": [ + "sound", + "volume" + ], "moji": "📢" }, "love_hotel": { @@ -8152,7 +16462,22 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["affection", "dating", "like", "love", "hotel", "love", "sex", "romance", "leisure", "adultery", "prostitution", "hospital", "birth", "happy"], + "keywords": [ + "affection", + "dating", + "like", + "love", + "hotel", + "love", + "sex", + "romance", + "leisure", + "adultery", + "prostitution", + "hospital", + "birth", + "happy" + ], "moji": "🏩" }, "love_letter": { @@ -8163,7 +16488,17 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["affection", "email", "envelope", "like", "valentines", "love", "letter", "kiss", "heart"], + "keywords": [ + "affection", + "email", + "envelope", + "like", + "valentines", + "love", + "letter", + "kiss", + "heart" + ], "moji": "💌" }, "low_brightness": { @@ -8174,18 +16509,27 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["summer", "sun"], + "keywords": [ + "summer", + "sun" + ], "moji": "🔅" }, "m": { "unicode": "24C2", - "unicode_alternates": ["24C2-FE0F"], + "unicode_alternates": [ + "24C2-FE0F" + ], "name": "circled latin capital letter m", "shortname": ":m:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["alphabet", "blue-circle", "letter"], + "keywords": [ + "alphabet", + "blue-circle", + "letter" + ], "moji": "Ⓜ" }, "mag": { @@ -8196,7 +16540,14 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["search", "zoom", "detective", "investigator", "detail", "details"], + "keywords": [ + "search", + "zoom", + "detective", + "investigator", + "detail", + "details" + ], "moji": "🔍" }, "mag_right": { @@ -8207,18 +16558,31 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["search", "zoom", "detective", "investigator", "detail", "details"], + "keywords": [ + "search", + "zoom", + "detective", + "investigator", + "detail", + "details" + ], "moji": "🔎" }, "mahjong": { "unicode": "1F004", - "unicode_alternates": ["1F004-FE0F"], + "unicode_alternates": [ + "1F004-FE0F" + ], "name": "mahjong tile red dragon", "shortname": ":mahjong:", "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["chinese", "game", "kanji"], + "keywords": [ + "chinese", + "game", + "kanji" + ], "moji": "🀄" }, "mailbox": { @@ -8229,7 +16593,11 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["communication", "email", "inbox"], + "keywords": [ + "communication", + "email", + "inbox" + ], "moji": "📫" }, "mailbox_closed": { @@ -8240,7 +16608,11 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["communication", "email", "inbox"], + "keywords": [ + "communication", + "email", + "inbox" + ], "moji": "📪" }, "mailbox_with_mail": { @@ -8251,7 +16623,11 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["communication", "email", "inbox"], + "keywords": [ + "communication", + "email", + "inbox" + ], "moji": "📬" }, "mailbox_with_no_mail": { @@ -8262,7 +16638,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["email", "inbox"], + "keywords": [ + "email", + "inbox" + ], "moji": "📭" }, "man": { @@ -8273,9 +16652,95 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["classy", "dad", "father", "guy", "mustashe"], + "keywords": [ + "classy", + "dad", + "father", + "guy", + "mustashe" + ], "moji": "👨" }, + "man_tone1": { + "unicode": "1F468-1F3FB", + "unicode_alternates": "", + "name": "man tone 1", + "shortname": ":man_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "classy", + "dad", + "father", + "guy", + "mustache" + ] + }, + "man_tone2": { + "unicode": "1F468-1F3FC", + "unicode_alternates": "", + "name": "man tone 2", + "shortname": ":man_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "classy", + "dad", + "father", + "guy", + "mustache" + ] + }, + "man_tone3": { + "unicode": "1F468-1F3FD", + "unicode_alternates": "", + "name": "man tone 3", + "shortname": ":man_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "classy", + "dad", + "father", + "guy", + "mustache" + ] + }, + "man_tone4": { + "unicode": "1F468-1F3FE", + "unicode_alternates": "", + "name": "man tone 4", + "shortname": ":man_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "classy", + "dad", + "father", + "guy", + "mustache" + ] + }, + "man_tone5": { + "unicode": "1F468-1F3FF", + "unicode_alternates": "", + "name": "man tone 5", + "shortname": ":man_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "classy", + "dad", + "father", + "guy", + "mustache" + ] + }, "man_with_gua_pi_mao": { "unicode": "1F472", "unicode_alternates": [], @@ -8284,9 +16749,101 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["boy", "male", "skullcap", "chinese", "asian", "qing"], + "keywords": [ + "boy", + "male", + "skullcap", + "chinese", + "asian", + "qing" + ], "moji": "👲" }, + "man_with_gua_pi_mao_tone1": { + "unicode": "1F472-1F3FB", + "unicode_alternates": "", + "name": "man with gua pi mao tone 1", + "shortname": ":man_with_gua_pi_mao_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "boy", + "male", + "skullcap", + "chinese", + "asian", + "qing" + ] + }, + "man_with_gua_pi_mao_tone2": { + "unicode": "1F472-1F3FC", + "unicode_alternates": "", + "name": "man with gua pi mao tone 2", + "shortname": ":man_with_gua_pi_mao_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "boy", + "male", + "skullcap", + "chinese", + "asian", + "qing" + ] + }, + "man_with_gua_pi_mao_tone3": { + "unicode": "1F472-1F3FD", + "unicode_alternates": "", + "name": "man with gua pi mao tone 3", + "shortname": ":man_with_gua_pi_mao_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "boy", + "male", + "skullcap", + "chinese", + "asian", + "qing" + ] + }, + "man_with_gua_pi_mao_tone4": { + "unicode": "1F472-1F3FE", + "unicode_alternates": "", + "name": "man with gua pi mao tone 4", + "shortname": ":man_with_gua_pi_mao_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "boy", + "male", + "skullcap", + "chinese", + "asian", + "qing" + ] + }, + "man_with_gua_pi_mao_tone5": { + "unicode": "1F472-1F3FF", + "unicode_alternates": "", + "name": "man with gua pi mao tone 5", + "shortname": ":man_with_gua_pi_mao_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "boy", + "male", + "skullcap", + "chinese", + "asian", + "qing" + ] + }, "man_with_turban": { "unicode": "1F473", "unicode_alternates": [], @@ -8295,9 +16852,120 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["male", "turban", "headdress", "headwear", "pagri", "india", "indian", "mummy", "wisdom", "peace"], + "keywords": [ + "male", + "turban", + "headdress", + "headwear", + "pagri", + "india", + "indian", + "mummy", + "wisdom", + "peace" + ], "moji": "👳" }, + "man_with_turban_tone1": { + "unicode": "1F473-1F3FB", + "unicode_alternates": "", + "name": "man with turban tone 1", + "shortname": ":man_with_turban_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "male", + "headdress", + "headwear", + "pagri", + "india", + "indian", + "mummy", + "wisdom", + "peace" + ] + }, + "man_with_turban_tone2": { + "unicode": "1F473-1F3FC", + "unicode_alternates": "", + "name": "man with turban tone 2", + "shortname": ":man_with_turban_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "male", + "headdress", + "headwear", + "pagri", + "india", + "indian", + "mummy", + "wisdom", + "peace" + ] + }, + "man_with_turban_tone3": { + "unicode": "1F473-1F3FD", + "unicode_alternates": "", + "name": "man with turban tone 3", + "shortname": ":man_with_turban_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "male", + "headdress", + "headwear", + "pagri", + "india", + "indian", + "mummy", + "wisdom", + "peace" + ] + }, + "man_with_turban_tone4": { + "unicode": "1F473-1F3FE", + "unicode_alternates": "", + "name": "man with turban tone 4", + "shortname": ":man_with_turban_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "male", + "headdress", + "headwear", + "pagri", + "india", + "indian", + "mummy", + "wisdom", + "peace" + ] + }, + "man_with_turban_tone5": { + "unicode": "1F473-1F3FF", + "unicode_alternates": "", + "name": "man with turban tone 5", + "shortname": ":man_with_turban_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "male", + "headdress", + "headwear", + "pagri", + "india", + "indian", + "mummy", + "wisdom", + "peace" + ] + }, "mans_shoe": { "unicode": "1F45E", "unicode_alternates": [], @@ -8306,7 +16974,10 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["fashion", "male"], + "keywords": [ + "fashion", + "male" + ], "moji": "👞" }, "map": { @@ -8315,9 +16986,15 @@ "name": "world map", "shortname": ":map:", "category": "travel_places", - "aliases": [":world_map:"], + "aliases": [ + ":world_map:" + ], "aliases_ascii": [], - "keywords": ["atlas", "earth", "cartography"] + "keywords": [ + "atlas", + "earth", + "cartography" + ] }, "maple_leaf": { "unicode": "1F341", @@ -8327,7 +17004,17 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["canada", "nature", "plant", "vegetable", "maple", "leaf", "syrup", "canada", "tree"], + "keywords": [ + "canada", + "nature", + "plant", + "vegetable", + "maple", + "leaf", + "syrup", + "canada", + "tree" + ], "moji": "🍁" }, "mask": { @@ -8338,7 +17025,16 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["face", "ill", "sick", "sick", "virus", "flu", "medical", "mask"], + "keywords": [ + "face", + "ill", + "sick", + "sick", + "virus", + "flu", + "medical", + "mask" + ], "moji": "😷" }, "massage": { @@ -8349,9 +17045,83 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["female", "girl", "woman"], + "keywords": [ + "female", + "girl", + "woman" + ], "moji": "💆" }, + "massage_tone1": { + "unicode": "1F486-1F3FB", + "unicode_alternates": "", + "name": "face massage tone 1", + "shortname": ":massage_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman" + ] + }, + "massage_tone2": { + "unicode": "1F486-1F3FC", + "unicode_alternates": "", + "name": "face massage tone 2", + "shortname": ":massage_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman" + ] + }, + "massage_tone3": { + "unicode": "1F486-1F3FD", + "unicode_alternates": "", + "name": "face massage tone 3", + "shortname": ":massage_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman" + ] + }, + "massage_tone4": { + "unicode": "1F486-1F3FE", + "unicode_alternates": "", + "name": "face massage tone 4", + "shortname": ":massage_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman" + ] + }, + "massage_tone5": { + "unicode": "1F486-1F3FF", + "unicode_alternates": "", + "name": "face massage tone 5", + "shortname": ":massage_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman" + ] + }, "meat_on_bone": { "unicode": "1F356", "unicode_alternates": [], @@ -8360,7 +17130,14 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "good", "meat", "bone", "animal", "cooked"], + "keywords": [ + "food", + "good", + "meat", + "bone", + "animal", + "cooked" + ], "moji": "🍖" }, "medal": { @@ -8369,9 +17146,22 @@ "name": "sports medal", "shortname": ":medal:", "category": "activity", - "aliases": [":sports_medal:"], + "aliases": [ + ":sports_medal:" + ], "aliases_ascii": [], - "keywords": ["award", "ceremony", "contest", "ftw", "place", "win", "first", "show", "reward", "achievement"] + "keywords": [ + "award", + "ceremony", + "contest", + "ftw", + "place", + "win", + "first", + "show", + "reward", + "achievement" + ] }, "mega": { "unicode": "1F4E3", @@ -8381,7 +17171,11 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["sound", "speaker", "volume"], + "keywords": [ + "sound", + "speaker", + "volume" + ], "moji": "📣" }, "melon": { @@ -8392,9 +17186,26 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "fruit", "nature", "melon", "cantaloupe", "honeydew"], + "keywords": [ + "food", + "fruit", + "nature", + "melon", + "cantaloupe", + "honeydew" + ], "moji": "🍈" }, + "menorah": { + "unicode": "1F54E", + "unicode_alternates": "", + "name": "menorah with nine branches", + "shortname": ":menorah:", + "category": "symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": [] + }, "mens": { "unicode": "1F6B9", "unicode_alternates": [], @@ -8403,9 +17214,122 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["restroom", "toilet", "wc", "men", "bathroom", "restroom", "sign", "boy", "male", "avatar"], + "keywords": [ + "restroom", + "toilet", + "wc", + "men", + "bathroom", + "restroom", + "sign", + "boy", + "male", + "avatar" + ], "moji": "🚹" }, + "metal": { + "unicode": "1F918", + "unicode_alternates": "", + "name": "sign of the horns", + "shortname": ":metal:", + "category": "people", + "aliases": [ + ":sign_of_the_horns:" + ], + "aliases_ascii": [], + "keywords": [ + "band", + "concert", + "fingers", + "rocknroll" + ] + }, + "metal_tone1": { + "unicode": "1F918-1F3FB", + "unicode_alternates": "", + "name": "sign of the horns tone 1", + "shortname": ":metal_tone1:", + "category": "people", + "aliases": [ + ":sign_of_the_horns_tone1:" + ], + "aliases_ascii": [], + "keywords": [ + "band", + "concert", + "fingers", + "rocknroll" + ] + }, + "metal_tone2": { + "unicode": "1F918-1F3FC", + "unicode_alternates": "", + "name": "sign of the horns tone 2", + "shortname": ":metal_tone2:", + "category": "people", + "aliases": [ + ":sign_of_the_horns_tone2:" + ], + "aliases_ascii": [], + "keywords": [ + "band", + "concert", + "fingers", + "rocknroll" + ] + }, + "metal_tone3": { + "unicode": "1F918-1F3FD", + "unicode_alternates": "", + "name": "sign of the horns tone 3", + "shortname": ":metal_tone3:", + "category": "people", + "aliases": [ + ":sign_of_the_horns_tone3:" + ], + "aliases_ascii": [], + "keywords": [ + "band", + "concert", + "fingers", + "rocknroll" + ] + }, + "metal_tone4": { + "unicode": "1F918-1F3FE", + "unicode_alternates": "", + "name": "sign of the horns tone 4", + "shortname": ":metal_tone4:", + "category": "people", + "aliases": [ + ":sign_of_the_horns_tone4:" + ], + "aliases_ascii": [], + "keywords": [ + "band", + "concert", + "fingers", + "rocknroll" + ] + }, + "metal_tone5": { + "unicode": "1F918-1F3FF", + "unicode_alternates": "", + "name": "sign of the horns tone 5", + "shortname": ":metal_tone5:", + "category": "people", + "aliases": [ + ":sign_of_the_horns_tone5:" + ], + "aliases_ascii": [], + "keywords": [ + "band", + "concert", + "fingers", + "rocknroll" + ] + }, "metro": { "unicode": "1F687", "unicode_alternates": [], @@ -8414,7 +17338,17 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square", "mrt", "transportation", "tube", "underground", "metro", "subway", "underground", "train"], + "keywords": [ + "blue-square", + "mrt", + "transportation", + "tube", + "underground", + "metro", + "subway", + "underground", + "train" + ], "moji": "🚇" }, "microphone": { @@ -8425,7 +17359,17 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["PA", "music", "sound", "microphone", "mic", "audio", "sound", "voice", "karaoke"], + "keywords": [ + "PA", + "music", + "sound", + "microphone", + "mic", + "audio", + "sound", + "voice", + "karaoke" + ], "moji": "🎤" }, "microphone2": { @@ -8434,9 +17378,15 @@ "name": "studio microphone", "shortname": ":microphone2:", "category": "objects_symbols", - "aliases": [":studio_microphone:"], + "aliases": [ + ":studio_microphone:" + ], "aliases_ascii": [], - "keywords": ["mic", "audio", "recording"] + "keywords": [ + "mic", + "audio", + "recording" + ] }, "microscope": { "unicode": "1F52C", @@ -8446,7 +17396,11 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["experiment", "laboratory", "zoomin"], + "keywords": [ + "experiment", + "laboratory", + "zoomin" + ], "moji": "🔬" }, "middle_finger": { @@ -8455,9 +17409,83 @@ "name": "reversed hand with middle finger extended", "shortname": ":middle_finger:", "category": "people", - "aliases": [":reversed_hand_with_middle_finger_extended:"], + "aliases": [ + ":reversed_hand_with_middle_finger_extended:" + ], "aliases_ascii": [], - "keywords": ["fu"] + "keywords": [ + "fu" + ] + }, + "middle_finger_tone1": { + "unicode": "1F595-1F3FB", + "unicode_alternates": "", + "name": "reversed hand with middle finger extended tone 1", + "shortname": ":middle_finger_tone1:", + "category": "people", + "aliases": [ + ":reversed_hand_with_middle_finger_extended_tone1:" + ], + "aliases_ascii": [], + "keywords": [ + "fu" + ] + }, + "middle_finger_tone2": { + "unicode": "1F595-1F3FC", + "unicode_alternates": "", + "name": "reversed hand with middle finger extended tone 2", + "shortname": ":middle_finger_tone2:", + "category": "people", + "aliases": [ + ":reversed_hand_with_middle_finger_extended_tone2:" + ], + "aliases_ascii": [], + "keywords": [ + "fu" + ] + }, + "middle_finger_tone3": { + "unicode": "1F595-1F3FD", + "unicode_alternates": "", + "name": "reversed hand with middle finger extended tone 3", + "shortname": ":middle_finger_tone3:", + "category": "people", + "aliases": [ + ":reversed_hand_with_middle_finger_extended_tone3:" + ], + "aliases_ascii": [], + "keywords": [ + "fu" + ] + }, + "middle_finger_tone4": { + "unicode": "1F595-1F3FE", + "unicode_alternates": "", + "name": "reversed hand with middle finger extended tone 4", + "shortname": ":middle_finger_tone4:", + "category": "people", + "aliases": [ + ":reversed_hand_with_middle_finger_extended_tone4:" + ], + "aliases_ascii": [], + "keywords": [ + "fu" + ] + }, + "middle_finger_tone5": { + "unicode": "1F595-1F3FF", + "unicode_alternates": "", + "name": "reversed hand with middle finger extended tone 5", + "shortname": ":middle_finger_tone5:", + "category": "people", + "aliases": [ + ":reversed_hand_with_middle_finger_extended_tone5:" + ], + "aliases_ascii": [], + "keywords": [ + "fu" + ] }, "military_medal": { "unicode": "1F396", @@ -8467,7 +17495,13 @@ "category": "celebration", "aliases": [], "aliases_ascii": [], - "keywords": ["honor", "acknowledgment", "purple heart", "heroism", "veteran"] + "keywords": [ + "honor", + "acknowledgment", + "purple heart", + "heroism", + "veteran" + ] }, "milky_way": { "unicode": "1F30C", @@ -8477,7 +17511,17 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["photo", "space", "milky", "galaxy", "star", "stars", "planets", "space", "sky"], + "keywords": [ + "photo", + "space", + "milky", + "galaxy", + "star", + "stars", + "planets", + "space", + "sky" + ], "moji": "🌌" }, "minibus": { @@ -8488,7 +17532,15 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["car", "transportation", "vehicle", "bus", "city", "transport", "transportation"], + "keywords": [ + "car", + "transportation", + "vehicle", + "bus", + "city", + "transport", + "transportation" + ], "moji": "🚐" }, "minidisc": { @@ -8499,7 +17551,13 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["data", "disc", "disk", "record", "technology"], + "keywords": [ + "data", + "disc", + "disk", + "record", + "technology" + ], "moji": "💽" }, "mobile_phone_off": { @@ -8510,9 +17568,23 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["mute"], + "keywords": [ + "mute" + ], "moji": "📴" }, + "money_mouth": { + "unicode": "1F911", + "unicode_alternates": "", + "name": "money-mouth face", + "shortname": ":money_mouth:", + "category": "people", + "aliases": [ + ":money_mouth_face:" + ], + "aliases_ascii": [], + "keywords": [] + }, "money_with_wings": { "unicode": "1F4B8", "unicode_alternates": [], @@ -8521,7 +17593,22 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["bills", "dollar", "payment", "money", "wings", "easy", "spend", "work", "lost", "blown", "burned", "gift", "cash", "dollar"], + "keywords": [ + "bills", + "dollar", + "payment", + "money", + "wings", + "easy", + "spend", + "work", + "lost", + "blown", + "burned", + "gift", + "cash", + "dollar" + ], "moji": "💸" }, "moneybag": { @@ -8532,7 +17619,11 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["coins", "dollar", "payment"], + "keywords": [ + "coins", + "dollar", + "payment" + ], "moji": "💰" }, "monkey": { @@ -8543,7 +17634,14 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature", "monkey", "primate", "banana", "silly"], + "keywords": [ + "animal", + "nature", + "monkey", + "primate", + "banana", + "silly" + ], "moji": "🐒" }, "monkey_face": { @@ -8554,7 +17652,10 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature"], + "keywords": [ + "animal", + "nature" + ], "moji": "🐵" }, "monorail": { @@ -8565,7 +17666,14 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["transportation", "vehicle", "train", "mono", "rail", "transport"], + "keywords": [ + "transportation", + "vehicle", + "train", + "mono", + "rail", + "transport" + ], "moji": "🚝" }, "mood_bubble": { @@ -8576,7 +17684,13 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["balloon", "conversation", "communication", "comic", "feeling"] + "keywords": [ + "balloon", + "conversation", + "communication", + "comic", + "feeling" + ] }, "mood_bubble_lightning": { "unicode": "1F5F1", @@ -8584,9 +17698,17 @@ "name": "lightning mood bubble", "shortname": ":mood_bubble_lightning:", "category": "objects_symbols", - "aliases": [":lightning_mood_bubble:"], + "aliases": [ + ":lightning_mood_bubble:" + ], "aliases_ascii": [], - "keywords": ["balloon", "conversation", "communication", "comic", "feeling"] + "keywords": [ + "balloon", + "conversation", + "communication", + "comic", + "feeling" + ] }, "mood_lightning": { "unicode": "1F5F2", @@ -8594,9 +17716,15 @@ "name": "lightning mood", "shortname": ":mood_lightning:", "category": "objects_symbols", - "aliases": [":lightning_mood:"], + "aliases": [ + ":lightning_mood:" + ], "aliases_ascii": [], - "keywords": ["zap", "electric", "current"] + "keywords": [ + "zap", + "electric", + "current" + ] }, "mortar_board": { "unicode": "1F393", @@ -8606,9 +17734,35 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["cap", "college", "degree", "graduation", "hat", "school", "university", "graduation", "cap", "mortarboard", "academic", "education", "ceremony", "square", "tassel"], + "keywords": [ + "cap", + "college", + "degree", + "graduation", + "hat", + "school", + "university", + "graduation", + "cap", + "mortarboard", + "academic", + "education", + "ceremony", + "square", + "tassel" + ], "moji": "🎓" }, + "mosque": { + "unicode": "1F54C", + "unicode_alternates": "", + "name": "mosque", + "shortname": ":mosque:", + "category": "travel", + "aliases": [], + "aliases_ascii": [], + "keywords": [] + }, "motorboat": { "unicode": "1F6E5", "unicode_alternates": [], @@ -8617,7 +17771,13 @@ "category": "travel_places", "aliases": [], "aliases_ascii": [], - "keywords": ["transportation", "vehicle", "boat", "speedboat", "powerboat"] + "keywords": [ + "transportation", + "vehicle", + "boat", + "speedboat", + "powerboat" + ] }, "motorcycle": { "unicode": "1F3CD", @@ -8625,9 +17785,14 @@ "name": "racing motorcycle", "shortname": ":motorcycle:", "category": "activity", - "aliases": [":racing_motorcycle:"], + "aliases": [ + ":racing_motorcycle:" + ], "aliases_ascii": [], - "keywords": ["bike", "speed"] + "keywords": [ + "bike", + "speed" + ] }, "motorway": { "unicode": "1F6E3", @@ -8637,7 +17802,13 @@ "category": "travel_places", "aliases": [], "aliases_ascii": [], - "keywords": ["road", "highway", "freeway", "traffic", "travel"] + "keywords": [ + "road", + "highway", + "freeway", + "traffic", + "travel" + ] }, "mount_fuji": { "unicode": "1F5FB", @@ -8647,9 +17818,26 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["japan", "mountain", "nature", "photo"], + "keywords": [ + "japan", + "mountain", + "nature", + "photo" + ], "moji": "🗻" }, + "mountain": { + "unicode": "26F0", + "unicode_alternates": "", + "name": "mountain", + "shortname": ":mountain:", + "category": "travel", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "place" + ] + }, "mountain_bicyclist": { "unicode": "1F6B5", "unicode_alternates": [], @@ -8658,9 +17846,104 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["human", "sports", "transportation", "bicyclist", "mountain", "bike", "pedal", "bicycle", "transportation"], + "keywords": [ + "human", + "sports", + "transportation", + "bicyclist", + "mountain", + "bike", + "pedal", + "bicycle", + "transportation" + ], "moji": "🚵" }, + "mountain_bicyclist_tone1": { + "unicode": "1F6B5-1F3FB", + "unicode_alternates": "", + "name": "mountain bicyclist tone 1", + "shortname": ":mountain_bicyclist_tone1:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "sport", + "transportation", + "bike", + "pedal", + "bicycle", + "transportation" + ] + }, + "mountain_bicyclist_tone2": { + "unicode": "1F6B5-1F3FC", + "unicode_alternates": "", + "name": "mountain bicyclist tone 2", + "shortname": ":mountain_bicyclist_tone2:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "sport", + "transportation", + "bike", + "pedal", + "bicycle", + "transportation" + ] + }, + "mountain_bicyclist_tone3": { + "unicode": "1F6B5-1F3FD", + "unicode_alternates": "", + "name": "mountain bicyclist tone 3", + "shortname": ":mountain_bicyclist_tone3:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "sport", + "transportation", + "bike", + "pedal", + "bicycle", + "transportation" + ] + }, + "mountain_bicyclist_tone4": { + "unicode": "1F6B5-1F3FE", + "unicode_alternates": "", + "name": "mountain bicyclist tone 4", + "shortname": ":mountain_bicyclist_tone4:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "sport", + "transportation", + "bike", + "pedal", + "bicycle", + "transportation" + ] + }, + "mountain_bicyclist_tone5": { + "unicode": "1F6B5-1F3FF", + "unicode_alternates": "", + "name": "mountain bicyclist tone 5", + "shortname": ":mountain_bicyclist_tone5:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "sport", + "transportation", + "bike", + "pedal", + "bicycle", + "transportation" + ] + }, "mountain_cableway": { "unicode": "1F6A0", "unicode_alternates": [], @@ -8669,7 +17952,15 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["transportation", "vehicle", "mountain", "cable", "rail", "train", "railway"], + "keywords": [ + "transportation", + "vehicle", + "mountain", + "cable", + "rail", + "train", + "railway" + ], "moji": "🚠" }, "mountain_railway": { @@ -8680,7 +17971,14 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["transportation", "mountain", "railway", "rail", "train", "transport"], + "keywords": [ + "transportation", + "mountain", + "railway", + "rail", + "train", + "transport" + ], "moji": "🚞" }, "mountain_snow": { @@ -8689,9 +17987,16 @@ "name": "snow capped mountain", "shortname": ":mountain_snow:", "category": "travel_places", - "aliases": [":snow_capped_mountain:"], + "aliases": [ + ":snow_capped_mountain:" + ], "aliases_ascii": [], - "keywords": ["cold", "elevation", "hiking", "peak"] + "keywords": [ + "cold", + "elevation", + "hiking", + "peak" + ] }, "mouse": { "unicode": "1F42D", @@ -8701,7 +18006,10 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature"], + "keywords": [ + "animal", + "nature" + ], "moji": "🐭" }, "mouse2": { @@ -8712,7 +18020,13 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature", "mouse", "mice", "rodent"], + "keywords": [ + "animal", + "nature", + "mouse", + "mice", + "rodent" + ], "moji": "🐁" }, "mouse_one": { @@ -8721,9 +18035,32 @@ "name": "one button mouse", "shortname": ":mouse_one:", "category": "objects_symbols", - "aliases": [":one_button_mouse:"], + "aliases": [ + ":one_button_mouse:" + ], "aliases_ascii": [], - "keywords": ["computer", "input", "device"] + "keywords": [ + "computer", + "input", + "device" + ] + }, + "mouse_three_button": { + "unicode": "1F5B1", + "unicode_alternates": "", + "name": "three button mouse", + "shortname": ":mouse_three_button:", + "category": "objects", + "aliases": [ + ":three_button_mouse:" + ], + "aliases_ascii": [], + "keywords": [ + "3", + "computer", + "object", + "office" + ] }, "movie_camera": { "unicode": "1F3A5", @@ -8733,7 +18070,16 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["film", "record", "movie", "camera", "camcorder", "video", "motion", "picture"], + "keywords": [ + "film", + "record", + "movie", + "camera", + "camcorder", + "video", + "motion", + "picture" + ], "moji": "🎥" }, "moyai": { @@ -8744,7 +18090,10 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["island", "stone"], + "keywords": [ + "island", + "stone" + ], "moji": "🗿" }, "muscle": { @@ -8755,9 +18104,101 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["arm", "flex", "hand", "strong", "muscle", "bicep"], + "keywords": [ + "arm", + "flex", + "hand", + "strong", + "muscle", + "bicep" + ], "moji": "💪" }, + "muscle_tone1": { + "unicode": "1F4AA-1F3FB", + "unicode_alternates": "", + "name": "flexed biceps tone 1", + "shortname": ":muscle_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "arm", + "flex", + "hand", + "strong", + "muscle", + "bicep" + ] + }, + "muscle_tone2": { + "unicode": "1F4AA-1F3FC", + "unicode_alternates": "", + "name": "flexed biceps tone 2", + "shortname": ":muscle_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "arm", + "flex", + "hand", + "strong", + "muscle", + "bicep" + ] + }, + "muscle_tone3": { + "unicode": "1F4AA-1F3FD", + "unicode_alternates": "", + "name": "flexed biceps tone 3", + "shortname": ":muscle_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "arm", + "flex", + "hand", + "strong", + "muscle", + "bicep" + ] + }, + "muscle_tone4": { + "unicode": "1F4AA-1F3FE", + "unicode_alternates": "", + "name": "flexed biceps tone 4", + "shortname": ":muscle_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "arm", + "flex", + "hand", + "strong", + "muscle", + "bicep" + ] + }, + "muscle_tone5": { + "unicode": "1F4AA-1F3FF", + "unicode_alternates": "", + "name": "flexed biceps tone 5", + "shortname": ":muscle_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "arm", + "flex", + "hand", + "strong", + "muscle", + "bicep" + ] + }, "mushroom": { "unicode": "1F344", "unicode_alternates": [], @@ -8766,7 +18207,14 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["plant", "vegetable", "mushroom", "fungi", "food", "fungus"], + "keywords": [ + "plant", + "vegetable", + "mushroom", + "fungi", + "food", + "fungus" + ], "moji": "🍄" }, "musical_keyboard": { @@ -8777,7 +18225,16 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["instrument", "piano", "music", "keyboard", "piano", "organ", "instrument", "electric"], + "keywords": [ + "instrument", + "piano", + "music", + "keyboard", + "piano", + "organ", + "instrument", + "electric" + ], "moji": "🎹" }, "musical_note": { @@ -8788,7 +18245,14 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["score", "musical", "music", "note", "music", "sound"], + "keywords": [ + "score", + "musical", + "music", + "note", + "music", + "sound" + ], "moji": "🎵" }, "musical_score": { @@ -8799,7 +18263,17 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["clef", "treble", "music", "musical", "score", "clef", "g-clef", "stave", "staff"], + "keywords": [ + "clef", + "treble", + "music", + "musical", + "score", + "clef", + "g-clef", + "stave", + "staff" + ], "moji": "🎼" }, "mute": { @@ -8810,7 +18284,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["sound", "volume"], + "keywords": [ + "sound", + "volume" + ], "moji": "🔇" }, "nail_care": { @@ -8821,9 +18298,77 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["beauty", "manicure"], + "keywords": [ + "beauty", + "manicure" + ], "moji": "💅" }, + "nail_care_tone1": { + "unicode": "1F485-1F3FB", + "unicode_alternates": "", + "name": "nail polish tone 1", + "shortname": ":nail_care_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "beauty", + "manicure" + ] + }, + "nail_care_tone2": { + "unicode": "1F485-1F3FC", + "unicode_alternates": "", + "name": "nail polish tone 2", + "shortname": ":nail_care_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "beauty", + "manicure" + ] + }, + "nail_care_tone3": { + "unicode": "1F485-1F3FD", + "unicode_alternates": "", + "name": "nail polish tone 3", + "shortname": ":nail_care_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "beauty", + "manicure" + ] + }, + "nail_care_tone4": { + "unicode": "1F485-1F3FE", + "unicode_alternates": "", + "name": "nail polish tone 4", + "shortname": ":nail_care_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "beauty", + "manicure" + ] + }, + "nail_care_tone5": { + "unicode": "1F485-1F3FF", + "unicode_alternates": "", + "name": "nail polish tone 5", + "shortname": ":nail_care_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "beauty", + "manicure" + ] + }, "name_badge": { "unicode": "1F4DB", "unicode_alternates": [], @@ -8832,7 +18377,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["fire", "forbid"], + "keywords": [ + "fire", + "forbid" + ], "moji": "📛" }, "necktie": { @@ -8843,7 +18391,13 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["cloth", "fashion", "formal", "shirt", "suitup"], + "keywords": [ + "cloth", + "fashion", + "formal", + "shirt", + "suitup" + ], "moji": "👔" }, "negative_squared_cross_mark": { @@ -8854,18 +18408,42 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["deny", "green-square", "no", "x"], + "keywords": [ + "deny", + "green-square", + "no", + "x" + ], "moji": "❎" }, + "nerd": { + "unicode": "1F913", + "unicode_alternates": "", + "name": "nerd face", + "shortname": ":nerd:", + "category": "people", + "aliases": [ + ":nerd_face:" + ], + "aliases_ascii": [], + "keywords": [] + }, "network": { "unicode": "1F5A7", "unicode_alternates": [], "name": "three networked computers", "shortname": ":network:", "category": "objects_symbols", - "aliases": [":three_networked_computers:"], + "aliases": [ + ":three_networked_computers:" + ], "aliases_ascii": [], - "keywords": ["lan", "wan", "network", "technology"] + "keywords": [ + "lan", + "wan", + "network", + "technology" + ] }, "neutral_face": { "unicode": "1F610", @@ -8875,7 +18453,14 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["face", "indifference", "neutral", "objective", "impartial", "blank"], + "keywords": [ + "face", + "indifference", + "neutral", + "objective", + "impartial", + "blank" + ], "moji": "😐" }, "new": { @@ -8886,7 +18471,9 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square"], + "keywords": [ + "blue-square" + ], "moji": "🆕" }, "new_moon": { @@ -8897,7 +18484,15 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["nature", "moon", "new", "sky", "night", "cheese", "phase"], + "keywords": [ + "nature", + "moon", + "new", + "sky", + "night", + "cheese", + "phase" + ], "moji": "🌑" }, "new_moon_with_face": { @@ -8908,7 +18503,17 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["nature", "moon", "new", "anthropomorphic", "face", "sky", "night", "cheese", "phase"], + "keywords": [ + "nature", + "moon", + "new", + "anthropomorphic", + "face", + "sky", + "night", + "cheese", + "phase" + ], "moji": "🌚" }, "newspaper": { @@ -8919,7 +18524,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["headline", "press"], + "keywords": [ + "headline", + "press" + ], "moji": "📰" }, "newspaper2": { @@ -8928,9 +18536,29 @@ "name": "rolled-up newspaper", "shortname": ":newspaper2:", "category": "objects_symbols", - "aliases": [":rolled_up_newspaper:"], + "aliases": [ + ":rolled_up_newspaper:" + ], "aliases_ascii": [], - "keywords": ["headline", "press"] + "keywords": [ + "headline", + "press" + ] + }, + "ng": { + "unicode": "1F196", + "unicode_alternates": "", + "name": "squared ng", + "shortname": ":ng:", + "category": "symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "blue-square", + "no good", + "symbol", + "word" + ] }, "night_with_stars": { "unicode": "1F303", @@ -8940,19 +18568,33 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["night", "star", "cloudless", "evening", "planets", "space", "sky"], + "keywords": [ + "night", + "star", + "cloudless", + "evening", + "planets", + "space", + "sky" + ], "moji": "🌃" }, "nine": { "moji": "9️⃣", "unicode": "0039-20E3", - "unicode_alternates": ["0039-FE0F-20E3"], + "unicode_alternates": [ + "0039-FE0F-20E3" + ], "name": "digit nine", "shortname": ":nine:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["9", "blue-square", "numbers"] + "keywords": [ + "9", + "blue-square", + "numbers" + ] }, "no_bell": { "unicode": "1F515", @@ -8962,7 +18604,11 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["mute", "sound", "volume"], + "keywords": [ + "mute", + "sound", + "volume" + ], "moji": "🔕" }, "no_bicycles": { @@ -8973,18 +18619,33 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["cyclist", "prohibited", "bicycle", "bike pedal", "no"], + "keywords": [ + "cyclist", + "prohibited", + "bicycle", + "bike pedal", + "no" + ], "moji": "🚳" }, "no_entry": { "unicode": "26D4", - "unicode_alternates": ["26D4-FE0F"], + "unicode_alternates": [ + "26D4-FE0F" + ], "name": "no entry", "shortname": ":no_entry:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["bad", "denied", "limit", "privacy", "security", "stop"], + "keywords": [ + "bad", + "denied", + "limit", + "privacy", + "security", + "stop" + ], "moji": "⛔" }, "no_entry_sign": { @@ -8995,7 +18656,16 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["denied", "disallow", "forbid", "limit", "stop", "no", "stop", "entry"], + "keywords": [ + "denied", + "disallow", + "forbid", + "limit", + "stop", + "no", + "stop", + "entry" + ], "moji": "🚫" }, "no_good": { @@ -9006,9 +18676,128 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["female", "girl", "woman", "no", "stop", "nope", "don't", "not"], + "keywords": [ + "female", + "girl", + "woman", + "no", + "stop", + "nope", + "don't", + "not" + ], "moji": "🙅" }, + "no_good_tone1": { + "unicode": "1F645-1F3FB", + "unicode_alternates": "", + "name": "face with no good gesture tone 1", + "shortname": ":no_good_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman", + "stop", + "nope", + "don't", + "not", + "forbidden", + "hand", + "person", + "prohibited" + ] + }, + "no_good_tone2": { + "unicode": "1F645-1F3FC", + "unicode_alternates": "", + "name": "face with no good gesture tone 2", + "shortname": ":no_good_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman", + "stop", + "nope", + "don't", + "not", + "forbidden", + "hand", + "person", + "prohibited" + ] + }, + "no_good_tone3": { + "unicode": "1F645-1F3FD", + "unicode_alternates": "", + "name": "face with no good gesture tone 3", + "shortname": ":no_good_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman", + "stop", + "nope", + "don't", + "not", + "forbidden", + "hand", + "person", + "prohibited" + ] + }, + "no_good_tone4": { + "unicode": "1F645-1F3FE", + "unicode_alternates": "", + "name": "face with no good gesture tone 4", + "shortname": ":no_good_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman", + "stop", + "nope", + "don't", + "not", + "forbidden", + "hand", + "person", + "prohibited" + ] + }, + "no_good_tone5": { + "unicode": "1F645-1F3FF", + "unicode_alternates": "", + "name": "face with no good gesture tone 5", + "shortname": ":no_good_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman", + "stop", + "nope", + "don't", + "not", + "forbidden", + "hand", + "person", + "prohibited" + ] + }, "no_mobile_phones": { "unicode": "1F4F5", "unicode_alternates": [], @@ -9017,7 +18806,10 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["iphone", "mute"], + "keywords": [ + "iphone", + "mute" + ], "moji": "📵" }, "no_mouth": { @@ -9027,8 +18819,24 @@ "shortname": ":no_mouth:", "category": "emoticons", "aliases": [], - "aliases_ascii": [":-X", ":X", ":-#", ":#", "=X", "=x", ":x", ":-x", "=#"], - "keywords": ["face", "hellokitty", "mouth", "silent", "vapid"], + "aliases_ascii": [ + ":-X", + ":X", + ":-#", + ":#", + "=X", + "=x", + ":x", + ":-x", + "=#" + ], + "keywords": [ + "face", + "hellokitty", + "mouth", + "silent", + "vapid" + ], "moji": "😶" }, "no_pedestrians": { @@ -9039,7 +18847,18 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["crossing", "rules", "walking", "no", "walk", "pedestrian", "stroll", "stride", "foot", "feet"], + "keywords": [ + "crossing", + "rules", + "walking", + "no", + "walk", + "pedestrian", + "stroll", + "stride", + "foot", + "feet" + ], "moji": "🚷" }, "no_smoking": { @@ -9050,7 +18869,18 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["cigarette", "no", "smoking", "cigarette", "smoke", "cancer", "lungs", "inhale", "tar", "nicotine"], + "keywords": [ + "cigarette", + "no", + "smoking", + "cigarette", + "smoke", + "cancer", + "lungs", + "inhale", + "tar", + "nicotine" + ], "moji": "🚭" }, "non-potable_water": { @@ -9061,7 +18891,18 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["drink", "faucet", "tap", "non-potable", "water", "not drinkable", "dirty", "gross", "aqua", "h20"], + "keywords": [ + "drink", + "faucet", + "tap", + "non-potable", + "water", + "not drinkable", + "dirty", + "gross", + "aqua", + "h20" + ], "moji": "🚱" }, "nose": { @@ -9072,18 +18913,91 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["smell", "sniff"], + "keywords": [ + "smell", + "sniff" + ], "moji": "👃" }, + "nose_tone1": { + "unicode": "1F443-1F3FB", + "unicode_alternates": "", + "name": "nose tone 1", + "shortname": ":nose_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "smell", + "sniff" + ] + }, + "nose_tone2": { + "unicode": "1F443-1F3FC", + "unicode_alternates": "", + "name": "nose tone 2", + "shortname": ":nose_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "smell", + "sniff" + ] + }, + "nose_tone3": { + "unicode": "1F443-1F3FD", + "unicode_alternates": "", + "name": "nose tone 3", + "shortname": ":nose_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "smell", + "sniff" + ] + }, + "nose_tone4": { + "unicode": "1F443-1F3FE", + "unicode_alternates": "", + "name": "nose tone 4", + "shortname": ":nose_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "smell", + "sniff" + ] + }, + "nose_tone5": { + "unicode": "1F443-1F3FF", + "unicode_alternates": "", + "name": "nose tone 5", + "shortname": ":nose_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "smell", + "sniff" + ] + }, "note": { "unicode": "1F5C9", "unicode_alternates": [], "name": "note page", "shortname": ":note:", "category": "objects_symbols", - "aliases": [":note_page:"], + "aliases": [ + ":note_page:" + ], "aliases_ascii": [], - "keywords": ["stationery", "post-it"] + "keywords": [ + "stationery", + "post-it" + ] }, "note_empty": { "unicode": "1F5C6", @@ -9091,9 +19005,14 @@ "name": "empty note page", "shortname": ":note_empty:", "category": "objects_symbols", - "aliases": [":empty_note_page:"], + "aliases": [ + ":empty_note_page:" + ], "aliases_ascii": [], - "keywords": ["stationery", "post-it"] + "keywords": [ + "stationery", + "post-it" + ] }, "notebook": { "unicode": "1F4D3", @@ -9103,7 +19022,12 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["notes", "paper", "record", "stationery"], + "keywords": [ + "notes", + "paper", + "record", + "stationery" + ], "moji": "📓" }, "notebook_with_decorative_cover": { @@ -9114,7 +19038,12 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["classroom", "notes", "paper", "record"], + "keywords": [ + "classroom", + "notes", + "paper", + "record" + ], "moji": "📔" }, "notepad": { @@ -9123,9 +19052,14 @@ "name": "note pad", "shortname": ":notepad:", "category": "objects_symbols", - "aliases": [":note_pad:"], + "aliases": [ + ":note_pad:" + ], "aliases_ascii": [], - "keywords": ["stationery", "post-it"] + "keywords": [ + "stationery", + "post-it" + ] }, "notepad_empty": { "unicode": "1F5C7", @@ -9133,9 +19067,14 @@ "name": "empty note pad", "shortname": ":notepad_empty:", "category": "objects_symbols", - "aliases": [":empty_note_pad:"], + "aliases": [ + ":empty_note_pad:" + ], "aliases_ascii": [], - "keywords": ["stationery", "post-it"] + "keywords": [ + "stationery", + "post-it" + ] }, "notepad_spiral": { "unicode": "1F5D2", @@ -9143,9 +19082,13 @@ "name": "spiral note pad", "shortname": ":notepad_spiral:", "category": "objects_symbols", - "aliases": [":spiral_note_pad:"], + "aliases": [ + ":spiral_note_pad:" + ], "aliases_ascii": [], - "keywords": ["stationery"] + "keywords": [ + "stationery" + ] }, "notes": { "unicode": "1F3B6", @@ -9155,7 +19098,16 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["music", "score", "musical", "music", "notes", "music", "sound", "melody"], + "keywords": [ + "music", + "score", + "musical", + "music", + "notes", + "music", + "sound", + "melody" + ], "moji": "🎶" }, "nut_and_bolt": { @@ -9166,18 +19118,26 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["handy", "tools"], + "keywords": [ + "handy", + "tools" + ], "moji": "🔩" }, "o": { "unicode": "2B55", - "unicode_alternates": ["2B55-FE0F"], + "unicode_alternates": [ + "2B55-FE0F" + ], "name": "heavy large circle", "shortname": ":o:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["circle", "round"], + "keywords": [ + "circle", + "round" + ], "moji": "⭕" }, "o2": { @@ -9188,7 +19148,11 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["alphabet", "letter", "red-square"], + "keywords": [ + "alphabet", + "letter", + "red-square" + ], "moji": "🅾" }, "ocean": { @@ -9199,7 +19163,16 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["sea", "water", "wave", "ocean", "wave", "surf", "beach", "tide"], + "keywords": [ + "sea", + "water", + "wave", + "ocean", + "wave", + "surf", + "beach", + "tide" + ], "moji": "🌊" }, "octopus": { @@ -9210,7 +19183,12 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "creature", "ocean", "sea"], + "keywords": [ + "animal", + "creature", + "ocean", + "sea" + ], "moji": "🐙" }, "oden": { @@ -9221,7 +19199,14 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "japanese", "oden", "seafood", "casserole", "stew"], + "keywords": [ + "food", + "japanese", + "oden", + "seafood", + "casserole", + "stew" + ], "moji": "🍢" }, "office": { @@ -9232,7 +19217,11 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["building", "bureau", "work"], + "keywords": [ + "building", + "bureau", + "work" + ], "moji": "🏢" }, "oil": { @@ -9241,9 +19230,13 @@ "name": "oil drum", "shortname": ":oil:", "category": "objects_symbols", - "aliases": [":oil_drum:"], + "aliases": [ + ":oil_drum:" + ], "aliases_ascii": [], - "keywords": ["petroleum"] + "keywords": [ + "petroleum" + ] }, "ok": { "unicode": "1F197", @@ -9253,7 +19246,12 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["agree", "blue-square", "good", "yes"], + "keywords": [ + "agree", + "blue-square", + "good", + "yes" + ], "moji": "🆗" }, "ok_hand": { @@ -9264,9 +19262,126 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["fingers", "limbs", "perfect", "okay", "ok", "smoke", "smoking", "marijuana", "joint", "pot", "420"], + "keywords": [ + "fingers", + "limbs", + "perfect", + "okay", + "ok", + "smoke", + "smoking", + "marijuana", + "joint", + "pot", + "420" + ], "moji": "👌" }, + "ok_hand_tone1": { + "unicode": "1F44C-1F3FB", + "unicode_alternates": "", + "name": "ok hand sign tone 1", + "shortname": ":ok_hand_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "fingers", + "limbs", + "perfect", + "okay", + "smoke", + "smoking", + "marijuana", + "joint", + "pot", + "420" + ] + }, + "ok_hand_tone2": { + "unicode": "1F44C-1F3FC", + "unicode_alternates": "", + "name": "ok hand sign tone 2", + "shortname": ":ok_hand_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "fingers", + "limbs", + "perfect", + "okay", + "smoke", + "smoking", + "marijuana", + "joint", + "pot", + "420" + ] + }, + "ok_hand_tone3": { + "unicode": "1F44C-1F3FD", + "unicode_alternates": "", + "name": "ok hand sign tone 3", + "shortname": ":ok_hand_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "fingers", + "limbs", + "perfect", + "okay", + "smoke", + "smoking", + "marijuana", + "joint", + "pot", + "420" + ] + }, + "ok_hand_tone4": { + "unicode": "1F44C-1F3FE", + "unicode_alternates": "", + "name": "ok hand sign tone 4", + "shortname": ":ok_hand_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "fingers", + "limbs", + "perfect", + "okay", + "smoke", + "smoking", + "marijuana", + "joint", + "pot", + "420" + ] + }, + "ok_hand_tone5": { + "unicode": "1F44C-1F3FF", + "unicode_alternates": "", + "name": "ok hand sign tone 5", + "shortname": ":ok_hand_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "fingers", + "limbs", + "perfect", + "okay", + "smoke", + "smoking", + "marijuana", + "joint", + "pot", + "420" + ] + }, "ok_woman": { "unicode": "1F646", "unicode_alternates": [], @@ -9274,10 +19389,120 @@ "shortname": ":ok_woman:", "category": "emoticons", "aliases": [], - "aliases_ascii": ["*\\0/*", "\\0/", "*\\O/*", "\\O/"], - "keywords": ["female", "girl", "human", "pink", "women", "yes", "ok", "okay", "accept"], + "aliases_ascii": [ + "*\\0/*", + "\\0/", + "*\\O/*", + "\\O/" + ], + "keywords": [ + "female", + "girl", + "human", + "pink", + "women", + "yes", + "ok", + "okay", + "accept" + ], "moji": "🙆" }, + "ok_woman_tone1": { + "unicode": "1F646-1F3FB", + "unicode_alternates": "", + "name": "face with ok gesture tone1", + "shortname": ":ok_woman_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "human", + "pink", + "women", + "yes", + "okay", + "accept" + ] + }, + "ok_woman_tone2": { + "unicode": "1F646-1F3FC", + "unicode_alternates": "", + "name": "face with ok gesture tone2", + "shortname": ":ok_woman_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "human", + "pink", + "women", + "yes", + "okay", + "accept" + ] + }, + "ok_woman_tone3": { + "unicode": "1F646-1F3FD", + "unicode_alternates": "", + "name": "face with ok gesture tone3", + "shortname": ":ok_woman_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "human", + "pink", + "women", + "yes", + "okay", + "accept" + ] + }, + "ok_woman_tone4": { + "unicode": "1F646-1F3FE", + "unicode_alternates": "", + "name": "face with ok gesture tone4", + "shortname": ":ok_woman_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "human", + "pink", + "women", + "yes", + "okay", + "accept" + ] + }, + "ok_woman_tone5": { + "unicode": "1F646-1F3FF", + "unicode_alternates": "", + "name": "face with ok gesture tone5", + "shortname": ":ok_woman_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "human", + "pink", + "women", + "yes", + "okay", + "accept" + ] + }, "older_man": { "unicode": "1F474", "unicode_alternates": [], @@ -9286,20 +19511,197 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["human", "male", "men"], + "keywords": [ + "human", + "male", + "men" + ], "moji": "👴" }, + "older_man_tone1": { + "unicode": "1F474-1F3FB", + "unicode_alternates": "", + "name": "older man tone 1", + "shortname": ":older_man_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "male", + "men", + "grandpa", + "grandfather" + ] + }, + "older_man_tone2": { + "unicode": "1F474-1F3FC", + "unicode_alternates": "", + "name": "older man tone 2", + "shortname": ":older_man_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "male", + "men", + "grandpa", + "grandfather" + ] + }, + "older_man_tone3": { + "unicode": "1F474-1F3FD", + "unicode_alternates": "", + "name": "older man tone 3", + "shortname": ":older_man_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "male", + "men", + "grandpa", + "grandfather" + ] + }, + "older_man_tone4": { + "unicode": "1F474-1F3FE", + "unicode_alternates": "", + "name": "older man tone 4", + "shortname": ":older_man_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "male", + "men", + "grandpa", + "grandfather" + ] + }, + "older_man_tone5": { + "unicode": "1F474-1F3FF", + "unicode_alternates": "", + "name": "older man tone 5", + "shortname": ":older_man_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "male", + "men", + "grandpa", + "grandfather" + ] + }, "older_woman": { "unicode": "1F475", "unicode_alternates": [], "name": "older woman", "shortname": ":older_woman:", "category": "emoticons", - "aliases": [":grandma:"], + "aliases": [ + ":grandma:" + ], "aliases_ascii": [], - "keywords": ["female", "girl", "women", "grandma", "grandmother"], + "keywords": [ + "female", + "girl", + "women", + "grandma", + "grandmother" + ], "moji": "👵" }, + "older_woman_tone1": { + "unicode": "1F475-1F3FB", + "unicode_alternates": "", + "name": "older woman tone 1", + "shortname": ":older_woman_tone1:", + "category": "people", + "aliases": [ + ":grandma_tone1:" + ], + "aliases_ascii": [], + "keywords": [ + "female", + "women", + "lady", + "grandma", + "grandmother" + ] + }, + "older_woman_tone2": { + "unicode": "1F475-1F3FC", + "unicode_alternates": "", + "name": "older woman tone 2", + "shortname": ":older_woman_tone2:", + "category": "people", + "aliases": [ + ":grandma_tone2:" + ], + "aliases_ascii": [], + "keywords": [ + "female", + "women", + "lady", + "grandma", + "grandmother" + ] + }, + "older_woman_tone3": { + "unicode": "1F475-1F3FD", + "unicode_alternates": "", + "name": "older woman tone 3", + "shortname": ":older_woman_tone3:", + "category": "people", + "aliases": [ + ":grandma_tone3:" + ], + "aliases_ascii": [], + "keywords": [ + "female", + "women", + "lady", + "grandma", + "grandmother" + ] + }, + "older_woman_tone4": { + "unicode": "1F475-1F3FE", + "unicode_alternates": "", + "name": "older woman tone 4", + "shortname": ":older_woman_tone4:", + "category": "people", + "aliases": [ + ":grandma_tone4:" + ], + "aliases_ascii": [], + "keywords": [ + "female", + "women", + "lady", + "grandma", + "grandmother" + ] + }, + "older_woman_tone5": { + "unicode": "1F475-1F3FF", + "unicode_alternates": "", + "name": "older woman tone 5", + "shortname": ":older_woman_tone5:", + "category": "people", + "aliases": [ + ":grandma_tone5:" + ], + "aliases_ascii": [], + "keywords": [ + "female", + "women", + "lady", + "grandma", + "grandmother" + ] + }, "om_symbol": { "unicode": "1F549", "unicode_alternates": [], @@ -9308,7 +19710,16 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["hinduism", "sound", "spiritual", "icon", "dharmic", "buddhism", "jainism", "meditate"] + "keywords": [ + "hinduism", + "sound", + "spiritual", + "icon", + "dharmic", + "buddhism", + "jainism", + "meditate" + ] }, "on": { "unicode": "1F51B", @@ -9318,7 +19729,10 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["arrow", "words"], + "keywords": [ + "arrow", + "words" + ], "moji": "🔛" }, "oncoming_automobile": { @@ -9329,7 +19743,14 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["car", "transportation", "vehicle", "sedan", "car", "automobile"], + "keywords": [ + "car", + "transportation", + "vehicle", + "sedan", + "car", + "automobile" + ], "moji": "🚘" }, "oncoming_bus": { @@ -9340,7 +19761,15 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["transportation", "vehicle", "bus", "school", "city", "transportation", "public"], + "keywords": [ + "transportation", + "vehicle", + "bus", + "school", + "city", + "transportation", + "public" + ], "moji": "🚍" }, "oncoming_police_car": { @@ -9351,7 +19780,19 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["enforcement", "law", "vehicle", "police", "car", "emergency", "ticket", "citation", "crime", "help", "officer"], + "keywords": [ + "enforcement", + "law", + "vehicle", + "police", + "car", + "emergency", + "ticket", + "citation", + "crime", + "help", + "officer" + ], "moji": "🚔" }, "oncoming_taxi": { @@ -9362,19 +19803,35 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["cars", "uber", "vehicle", "taxi", "car", "automobile", "city", "transport", "service"], + "keywords": [ + "cars", + "uber", + "vehicle", + "taxi", + "car", + "automobile", + "city", + "transport", + "service" + ], "moji": "🚖" }, "one": { "moji": "1️⃣", "unicode": "0031-20E3", - "unicode_alternates": ["0031-FE0F-20E3"], + "unicode_alternates": [ + "0031-FE0F-20E3" + ], "name": "digit one", "shortname": ":one:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["1", "blue-square", "numbers"] + "keywords": [ + "1", + "blue-square", + "numbers" + ] }, "open_file_folder": { "unicode": "1F4C2", @@ -9384,7 +19841,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["documents", "load"], + "keywords": [ + "documents", + "load" + ], "moji": "📂" }, "open_hands": { @@ -9395,9 +19855,77 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["butterfly", "fingers"], + "keywords": [ + "butterfly", + "fingers" + ], "moji": "👐" }, + "open_hands_tone1": { + "unicode": "1F450-1F3FB", + "unicode_alternates": "", + "name": "open hands sign tone 1", + "shortname": ":open_hands_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "butterfly", + "fingers" + ] + }, + "open_hands_tone2": { + "unicode": "1F450-1F3FC", + "unicode_alternates": "", + "name": "open hands sign tone 2", + "shortname": ":open_hands_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "butterfly", + "fingers" + ] + }, + "open_hands_tone3": { + "unicode": "1F450-1F3FD", + "unicode_alternates": "", + "name": "open hands sign tone 3", + "shortname": ":open_hands_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "butterfly", + "fingers" + ] + }, + "open_hands_tone4": { + "unicode": "1F450-1F3FE", + "unicode_alternates": "", + "name": "open hands sign tone 4", + "shortname": ":open_hands_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "butterfly", + "fingers" + ] + }, + "open_hands_tone5": { + "unicode": "1F450-1F3FF", + "unicode_alternates": "", + "name": "open hands sign tone 5", + "shortname": ":open_hands_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "butterfly", + "fingers" + ] + }, "open_mouth": { "unicode": "1F62E", "unicode_alternates": [], @@ -9405,8 +19933,24 @@ "shortname": ":open_mouth:", "category": "emoticons", "aliases": [], - "aliases_ascii": [":-O", ":O", ":-o", ":o", "O_O", ">:O"], - "keywords": ["face", "impressed", "mouth", "open", "jaw", "gapping", "surprise", "wow"], + "aliases_ascii": [ + ":-O", + ":O", + ":-o", + ":o", + "O_O", + ">:O" + ], + "keywords": [ + "face", + "impressed", + "mouth", + "open", + "jaw", + "gapping", + "surprise", + "wow" + ], "moji": "😮" }, "ophiuchus": { @@ -9417,7 +19961,19 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["ophiuchus", "serpent", "snake", "astrology", "greek", "constellation", "stars", "zodiac", "purple-square", "sign", "horoscope"], + "keywords": [ + "ophiuchus", + "serpent", + "snake", + "astrology", + "greek", + "constellation", + "stars", + "zodiac", + "purple-square", + "sign", + "horoscope" + ], "moji": "⛎" }, "optical_disk": { @@ -9426,9 +19982,17 @@ "name": "optical disc icon", "shortname": ":optical_disk:", "category": "objects_symbols", - "aliases": [":optical_disc_icon:"], + "aliases": [ + ":optical_disc_icon:" + ], "aliases_ascii": [], - "keywords": ["cd", "dvd", "disc", "disk", "technology"] + "keywords": [ + "cd", + "dvd", + "disc", + "disk", + "technology" + ] }, "orange_book": { "unicode": "1F4D9", @@ -9438,9 +20002,27 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["knowledge", "library", "read"], + "keywords": [ + "knowledge", + "library", + "read" + ], "moji": "📙" }, + "orthodox_cross": { + "unicode": "2626", + "unicode_alternates": "", + "name": "orthodox cross", + "shortname": ":orthodox_cross:", + "category": "symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "christian", + "religion", + "symbol" + ] + }, "outbox_tray": { "unicode": "1F4E4", "unicode_alternates": [], @@ -9449,7 +20031,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["email", "inbox"], + "keywords": [ + "email", + "inbox" + ], "moji": "📤" }, "ox": { @@ -9460,7 +20045,11 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "beef", "cow"], + "keywords": [ + "animal", + "beef", + "cow" + ], "moji": "🐂" }, "package": { @@ -9471,7 +20060,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["gift", "mail"], + "keywords": [ + "gift", + "mail" + ], "moji": "📦" }, "page": { @@ -9482,7 +20074,9 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["document"] + "keywords": [ + "document" + ] }, "page_facing_up": { "unicode": "1F4C4", @@ -9492,7 +20086,9 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["documents"], + "keywords": [ + "documents" + ], "moji": "📄" }, "page_with_curl": { @@ -9503,7 +20099,9 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["documents"], + "keywords": [ + "documents" + ], "moji": "📃" }, "pager": { @@ -9514,7 +20112,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["bbcall", "oldschool"], + "keywords": [ + "bbcall", + "oldschool" + ], "moji": "📟" }, "pages": { @@ -9525,7 +20126,9 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["documents"] + "keywords": [ + "documents" + ] }, "paintbrush": { "unicode": "1F58C", @@ -9533,9 +20136,15 @@ "name": "lower left paintbrush", "shortname": ":paintbrush:", "category": "objects_symbols", - "aliases": [":lower_left_paintbrush:"], + "aliases": [ + ":lower_left_paintbrush:" + ], "aliases_ascii": [], - "keywords": ["brush", "art", "painting"] + "keywords": [ + "brush", + "art", + "painting" + ] }, "palm_tree": { "unicode": "1F334", @@ -9545,7 +20154,17 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["nature", "plant", "vegetable", "palm", "tree", "coconuts", "fronds", "warm", "tropical"], + "keywords": [ + "nature", + "plant", + "vegetable", + "palm", + "tree", + "coconuts", + "fronds", + "warm", + "tropical" + ], "moji": "🌴" }, "panda_face": { @@ -9556,7 +20175,22 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature", "panda", "bear", "face", "cub", "cute", "endearment", "friendship", "love", "bamboo", "china", "black", "white"], + "keywords": [ + "animal", + "nature", + "panda", + "bear", + "face", + "cub", + "cute", + "endearment", + "friendship", + "love", + "bamboo", + "china", + "black", + "white" + ], "moji": "🐼" }, "paperclip": { @@ -9567,7 +20201,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["documents", "stationery"], + "keywords": [ + "documents", + "stationery" + ], "moji": "📎" }, "paperclips": { @@ -9576,9 +20213,14 @@ "name": "linked paperclips", "shortname": ":paperclips:", "category": "objects_symbols", - "aliases": [":linked_paperclips:"], + "aliases": [ + ":linked_paperclips:" + ], "aliases_ascii": [], - "keywords": ["documents", "stationery"] + "keywords": [ + "documents", + "stationery" + ] }, "park": { "unicode": "1F3DE", @@ -9586,41 +20228,77 @@ "name": "national park", "shortname": ":park:", "category": "travel_places", - "aliases": [":national_park:"], + "aliases": [ + ":national_park:" + ], "aliases_ascii": [], - "keywords": ["woods", "nature", "wildlife", "forest", "wilderness", "national"] + "keywords": [ + "woods", + "nature", + "wildlife", + "forest", + "wilderness", + "national" + ] }, "parking": { "unicode": "1F17F", - "unicode_alternates": ["1F17F-FE0F"], + "unicode_alternates": [ + "1F17F-FE0F" + ], "name": "negative squared latin capital letter p", "shortname": ":parking:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["alphabet", "blue-square", "cars", "letter"], + "keywords": [ + "alphabet", + "blue-square", + "cars", + "letter" + ], "moji": "🅿" }, "part_alternation_mark": { "unicode": "303D", - "unicode_alternates": ["303D-FE0F"], + "unicode_alternates": [ + "303D-FE0F" + ], "name": "part alternation mark", "shortname": ":part_alternation_mark:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["graph", "sing", "song", "vocal", "music", "karaoke", "cue", "letter", "m", "japanese"], + "keywords": [ + "graph", + "sing", + "song", + "vocal", + "music", + "karaoke", + "cue", + "letter", + "m", + "japanese" + ], "moji": "〽" }, "partly_sunny": { "unicode": "26C5", - "unicode_alternates": ["26C5-FE0F"], + "unicode_alternates": [ + "26C5-FE0F" + ], "name": "sun behind cloud", "shortname": ":partly_sunny:", "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["cloud", "morning", "nature", "weather"], + "keywords": [ + "cloud", + "morning", + "nature", + "weather" + ], "moji": "⛅" }, "passport_control": { @@ -9631,9 +20309,48 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square", "custom", "passport", "official", "travel", "control", "foreign", "identification"], + "keywords": [ + "blue-square", + "custom", + "passport", + "official", + "travel", + "control", + "foreign", + "identification" + ], "moji": "🛂" }, + "pause_button": { + "unicode": "23F8", + "unicode_alternates": "", + "name": "double vertical bar", + "shortname": ":pause_button:", + "category": "symbols", + "aliases": [ + ":double_vertical_bar:" + ], + "aliases_ascii": [], + "keywords": [ + "pause", + "sound", + "symbol" + ] + }, + "peace": { + "unicode": "262E", + "unicode_alternates": "", + "name": "peace symbol", + "shortname": ":peace:", + "category": "symbols", + "aliases": [ + ":peace_symbol:" + ], + "aliases_ascii": [], + "keywords": [ + "sign" + ] + }, "peach": { "unicode": "1F351", "unicode_alternates": [], @@ -9642,7 +20359,15 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "fruit", "nature", "peach", "fruit", "juicy", "pit"], + "keywords": [ + "food", + "fruit", + "nature", + "peach", + "fruit", + "juicy", + "pit" + ], "moji": "🍑" }, "pear": { @@ -9653,7 +20378,13 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["fruit", "nature", "pear", "fruit", "shape"], + "keywords": [ + "fruit", + "nature", + "pear", + "fruit", + "shape" + ], "moji": "🍐" }, "pen_ballpoint": { @@ -9662,9 +20393,15 @@ "name": "lower left ballpoint pen", "shortname": ":pen_ballpoint:", "category": "objects_symbols", - "aliases": [":lower_left_ballpoint_pen:"], + "aliases": [ + ":lower_left_ballpoint_pen:" + ], "aliases_ascii": [], - "keywords": ["write", "bic", "ink"] + "keywords": [ + "write", + "bic", + "ink" + ] }, "pen_fountain": { "unicode": "1F58B", @@ -9672,9 +20409,15 @@ "name": "lower left fountain pen", "shortname": ":pen_fountain:", "category": "objects_symbols", - "aliases": [":lower_left_fountain_pen:"], + "aliases": [ + ":lower_left_fountain_pen:" + ], "aliases_ascii": [], - "keywords": ["write", "calligraphy", "ink"] + "keywords": [ + "write", + "calligraphy", + "ink" + ] }, "pencil": { "unicode": "1F4DD", @@ -9682,20 +20425,33 @@ "name": "memo", "shortname": ":pencil:", "category": "objects", - "aliases": [":memo:"], + "aliases": [ + ":memo:" + ], "aliases_ascii": [], - "keywords": ["documents", "paper", "station", "write"], + "keywords": [ + "documents", + "paper", + "station", + "write" + ], "moji": "📝" }, "pencil2": { "unicode": "270F", - "unicode_alternates": ["270F-FE0F"], + "unicode_alternates": [ + "270F-FE0F" + ], "name": "pencil", "shortname": ":pencil2:", "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["paper", "stationery", "write"], + "keywords": [ + "paper", + "stationery", + "write" + ], "moji": "✏" }, "pencil3": { @@ -9704,9 +20460,15 @@ "name": "lower left pencil", "shortname": ":pencil3:", "category": "objects_symbols", - "aliases": [":lower_left_pencil:"], + "aliases": [ + ":lower_left_pencil:" + ], "aliases_ascii": [], - "keywords": ["paper", "stationery", "write"] + "keywords": [ + "paper", + "stationery", + "write" + ] }, "penguin": { "unicode": "1F427", @@ -9716,7 +20478,10 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature"], + "keywords": [ + "animal", + "nature" + ], "moji": "🐧" }, "pennant_black": { @@ -9725,9 +20490,14 @@ "name": "black pennant", "shortname": ":pennant_black:", "category": "objects_symbols", - "aliases": [":black_pennant:"], + "aliases": [ + ":black_pennant:" + ], "aliases_ascii": [], - "keywords": ["flag", "athletics"] + "keywords": [ + "flag", + "athletics" + ] }, "pennant_white": { "unicode": "1F3F1", @@ -9735,9 +20505,14 @@ "name": "white pennant", "shortname": ":pennant_white:", "category": "objects_symbols", - "aliases": [":white_pennant:"], + "aliases": [ + ":white_pennant:" + ], "aliases_ascii": [], - "keywords": ["flag", "athletics"] + "keywords": [ + "flag", + "athletics" + ] }, "pensive": { "unicode": "1F614", @@ -9747,7 +20522,18 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["face", "okay", "sad", "pensive", "thoughtful", "think", "reflective", "wistful", "meditate", "serious"], + "keywords": [ + "face", + "okay", + "sad", + "pensive", + "thoughtful", + "think", + "reflective", + "wistful", + "meditate", + "serious" + ], "moji": "😔" }, "performing_arts": { @@ -9758,7 +20544,19 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["acting", "drama", "theater", "performing", "arts", "performance", "entertainment", "acting", "story", "mask", "masks"], + "keywords": [ + "acting", + "drama", + "theater", + "performing", + "arts", + "performance", + "entertainment", + "acting", + "story", + "mask", + "masks" + ], "moji": "🎭" }, "persevere": { @@ -9768,8 +20566,17 @@ "shortname": ":persevere:", "category": "emoticons", "aliases": [], - "aliases_ascii": [">.<"], - "keywords": ["endure", "persevere", "face", "no", "sick", "upset"], + "aliases_ascii": [ + ">.<" + ], + "keywords": [ + "endure", + "persevere", + "face", + "no", + "sick", + "upset" + ], "moji": "😣" }, "person_frowning": { @@ -9780,9 +20587,107 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["female", "girl", "woman", "dejected", "rejected", "sad", "frown"], + "keywords": [ + "female", + "girl", + "woman", + "dejected", + "rejected", + "sad", + "frown" + ], "moji": "🙍" }, + "person_frowning_tone1": { + "unicode": "1F64D-1F3FB", + "unicode_alternates": "", + "name": "person frowning tone 1", + "shortname": ":person_frowning_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman", + "dejected", + "rejected", + "sad", + "frown" + ] + }, + "person_frowning_tone2": { + "unicode": "1F64D-1F3FC", + "unicode_alternates": "", + "name": "person frowning tone 2", + "shortname": ":person_frowning_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman", + "dejected", + "rejected", + "sad", + "frown" + ] + }, + "person_frowning_tone3": { + "unicode": "1F64D-1F3FD", + "unicode_alternates": "", + "name": "person frowning tone 3", + "shortname": ":person_frowning_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman", + "dejected", + "rejected", + "sad", + "frown" + ] + }, + "person_frowning_tone4": { + "unicode": "1F64D-1F3FE", + "unicode_alternates": "", + "name": "person frowning tone 4", + "shortname": ":person_frowning_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman", + "dejected", + "rejected", + "sad", + "frown" + ] + }, + "person_frowning_tone5": { + "unicode": "1F64D-1F3FF", + "unicode_alternates": "", + "name": "person frowning tone 5", + "shortname": ":person_frowning_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman", + "dejected", + "rejected", + "sad", + "frown" + ] + }, "person_with_blond_hair": { "unicode": "1F471", "unicode_alternates": [], @@ -9791,9 +20696,107 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["male", "man", "blonde", "young", "western", "westerner", "occidental"], + "keywords": [ + "male", + "man", + "blonde", + "young", + "western", + "westerner", + "occidental" + ], "moji": "👱" }, + "person_with_blond_hair_tone1": { + "unicode": "1F471-1F3FB", + "unicode_alternates": "", + "name": "person with blond hair tone 1", + "shortname": ":person_with_blond_hair_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "male", + "man", + "blonde", + "young", + "western", + "westerner", + "occidental" + ] + }, + "person_with_blond_hair_tone2": { + "unicode": "1F471-1F3FC", + "unicode_alternates": "", + "name": "person with blond hair tone 2", + "shortname": ":person_with_blond_hair_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "male", + "man", + "blonde", + "young", + "western", + "westerner", + "occidental" + ] + }, + "person_with_blond_hair_tone3": { + "unicode": "1F471-1F3FD", + "unicode_alternates": "", + "name": "person with blond hair tone 3", + "shortname": ":person_with_blond_hair_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "male", + "man", + "blonde", + "young", + "western", + "westerner", + "occidental" + ] + }, + "person_with_blond_hair_tone4": { + "unicode": "1F471-1F3FE", + "unicode_alternates": "", + "name": "person with blond hair tone 4", + "shortname": ":person_with_blond_hair_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "male", + "man", + "blonde", + "young", + "western", + "westerner", + "occidental" + ] + }, + "person_with_blond_hair_tone5": { + "unicode": "1F471-1F3FF", + "unicode_alternates": "", + "name": "person with blond hair tone 5", + "shortname": ":person_with_blond_hair_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "male", + "man", + "blonde", + "young", + "western", + "westerner", + "occidental" + ] + }, "person_with_pouting_face": { "unicode": "1F64E", "unicode_alternates": [], @@ -9802,9 +20805,121 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["female", "girl", "woman", "pout", "sexy", "cute", "annoyed"], + "keywords": [ + "female", + "girl", + "woman", + "pout", + "sexy", + "cute", + "annoyed" + ], "moji": "🙎" }, + "person_with_pouting_face_tone1": { + "unicode": "1F64E-1F3FB", + "unicode_alternates": "", + "name": "person with pouting face tone1", + "shortname": ":person_with_pouting_face_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman", + "pout", + "sexy", + "cute", + "annoyed" + ] + }, + "person_with_pouting_face_tone2": { + "unicode": "1F64E-1F3FC", + "unicode_alternates": "", + "name": "person with pouting face tone2", + "shortname": ":person_with_pouting_face_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman", + "pout", + "sexy", + "cute", + "annoyed" + ] + }, + "person_with_pouting_face_tone3": { + "unicode": "1F64E-1F3FD", + "unicode_alternates": "", + "name": "person with pouting face tone3", + "shortname": ":person_with_pouting_face_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman", + "pout", + "sexy", + "cute", + "annoyed" + ] + }, + "person_with_pouting_face_tone4": { + "unicode": "1F64E-1F3FE", + "unicode_alternates": "", + "name": "person with pouting face tone4", + "shortname": ":person_with_pouting_face_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman", + "pout", + "sexy", + "cute", + "annoyed" + ] + }, + "person_with_pouting_face_tone5": { + "unicode": "1F64E-1F3FF", + "unicode_alternates": "", + "name": "person with pouting face tone5", + "shortname": ":person_with_pouting_face_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman", + "pout", + "sexy", + "cute", + "annoyed" + ] + }, + "pick": { + "unicode": "26CF", + "unicode_alternates": "", + "name": "pick", + "shortname": ":pick:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "mining", + "object", + "tool" + ] + }, "pig": { "unicode": "1F437", "unicode_alternates": [], @@ -9813,7 +20928,10 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "oink"], + "keywords": [ + "animal", + "oink" + ], "moji": "🐷" }, "pig2": { @@ -9824,7 +20942,21 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature", "pig", "piggy", "pork", "ham", "hog", "bacon", "oink", "slop", "livestock", "greed", "greedy"], + "keywords": [ + "animal", + "nature", + "pig", + "piggy", + "pork", + "ham", + "hog", + "bacon", + "oink", + "slop", + "livestock", + "greed", + "greedy" + ], "moji": "🐖" }, "pig_nose": { @@ -9835,7 +20967,20 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "oink", "pig", "nose", "snout", "food", "eat", "cute", "oink", "pink", "smell", "truffle"], + "keywords": [ + "animal", + "oink", + "pig", + "nose", + "snout", + "food", + "eat", + "cute", + "oink", + "pink", + "smell", + "truffle" + ], "moji": "🐽" }, "pill": { @@ -9846,7 +20991,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["health", "medicine"], + "keywords": [ + "health", + "medicine" + ], "moji": "💊" }, "pineapple": { @@ -9857,28 +21005,68 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "fruit", "nature", "pineapple", "pina", "tropical", "flower"], + "keywords": [ + "food", + "fruit", + "nature", + "pineapple", + "pina", + "tropical", + "flower" + ], "moji": "🍍" }, + "ping_pong": { + "unicode": "1F3D3", + "unicode_alternates": "", + "name": "table tennis paddle and ball", + "shortname": ":ping_pong:", + "category": "activity", + "aliases": [ + ":table_tennis:" + ], + "aliases_ascii": [], + "keywords": [] + }, "piracy": { "unicode": "1F572", "unicode_alternates": [], "name": "no piracy", "shortname": ":piracy:", "category": "objects_symbols", - "aliases": [":no_piracy:"], + "aliases": [ + ":no_piracy:" + ], "aliases_ascii": [], - "keywords": ["theft", "rule"] + "keywords": [ + "theft", + "rule" + ] }, "pisces": { "unicode": "2653", - "unicode_alternates": ["2653-FE0F"], + "unicode_alternates": [ + "2653-FE0F" + ], "name": "pisces", "shortname": ":pisces:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["pisces", "fish", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "purple-square", "sign", "zodiac", "horoscope"], + "keywords": [ + "pisces", + "fish", + "astrology", + "greek", + "constellation", + "stars", + "zodiac", + "sign", + "purple-square", + "sign", + "zodiac", + "horoscope" + ], "moji": "♓" }, "pizza": { @@ -9889,9 +21077,48 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "party", "pizza", "pie", "new york", "italian", "italy", "slice", "peperoni"], + "keywords": [ + "food", + "party", + "pizza", + "pie", + "new york", + "italian", + "italy", + "slice", + "peperoni" + ], "moji": "🍕" }, + "place_of_worship": { + "unicode": "1F6D0", + "unicode_alternates": "", + "name": "place of worship", + "shortname": ":place_of_worship:", + "category": "symbols", + "aliases": [ + ":worship_symbol:" + ], + "aliases_ascii": [], + "keywords": [] + }, + "play_pause": { + "unicode": "23EF", + "unicode_alternates": "", + "name": "black right-pointing double triangle with double vertical bar", + "shortname": ":play_pause:", + "category": "symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "arrow", + "pause", + "play", + "right", + "sound", + "symbol" + ] + }, "point_down": { "unicode": "1F447", "unicode_alternates": [], @@ -9900,9 +21127,83 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["direction", "fingers", "hand"], + "keywords": [ + "direction", + "fingers", + "hand" + ], "moji": "👇" }, + "point_down_tone1": { + "unicode": "1F447-1F3FB", + "unicode_alternates": "", + "name": "white down pointing backhand index tone 1", + "shortname": ":point_down_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand" + ] + }, + "point_down_tone2": { + "unicode": "1F447-1F3FC", + "unicode_alternates": "", + "name": "white down pointing backhand index tone 2", + "shortname": ":point_down_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand" + ] + }, + "point_down_tone3": { + "unicode": "1F447-1F3FD", + "unicode_alternates": "", + "name": "white down pointing backhand index tone 3", + "shortname": ":point_down_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand" + ] + }, + "point_down_tone4": { + "unicode": "1F447-1F3FE", + "unicode_alternates": "", + "name": "white down pointing backhand index tone 4", + "shortname": ":point_down_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand" + ] + }, + "point_down_tone5": { + "unicode": "1F447-1F3FF", + "unicode_alternates": "", + "name": "white down pointing backhand index tone 5", + "shortname": ":point_down_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand" + ] + }, "point_left": { "unicode": "1F448", "unicode_alternates": [], @@ -9911,9 +21212,83 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["direction", "fingers", "hand"], + "keywords": [ + "direction", + "fingers", + "hand" + ], "moji": "👈" }, + "point_left_tone1": { + "unicode": "1F448-1F3FB", + "unicode_alternates": "", + "name": "white left pointing backhand index tone 1", + "shortname": ":point_left_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand" + ] + }, + "point_left_tone2": { + "unicode": "1F448-1F3FC", + "unicode_alternates": "", + "name": "white left pointing backhand index tone 2", + "shortname": ":point_left_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand" + ] + }, + "point_left_tone3": { + "unicode": "1F448-1F3FD", + "unicode_alternates": "", + "name": "white left pointing backhand index tone 3", + "shortname": ":point_left_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand" + ] + }, + "point_left_tone4": { + "unicode": "1F448-1F3FE", + "unicode_alternates": "", + "name": "white left pointing backhand index tone 4", + "shortname": ":point_left_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand" + ] + }, + "point_left_tone5": { + "unicode": "1F448-1F3FF", + "unicode_alternates": "", + "name": "white left pointing backhand index tone 5", + "shortname": ":point_left_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand" + ] + }, "point_right": { "unicode": "1F449", "unicode_alternates": [], @@ -9922,18 +21297,98 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["direction", "fingers", "hand"], + "keywords": [ + "direction", + "fingers", + "hand" + ], "moji": "👉" }, + "point_right_tone1": { + "unicode": "1F449-1F3FB", + "unicode_alternates": "", + "name": "white right pointing backhand index tone 1", + "shortname": ":point_right_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand" + ] + }, + "point_right_tone2": { + "unicode": "1F449-1F3FC", + "unicode_alternates": "", + "name": "white right pointing backhand index tone 2", + "shortname": ":point_right_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand" + ] + }, + "point_right_tone3": { + "unicode": "1F449-1F3FD", + "unicode_alternates": "", + "name": "white right pointing backhand index tone 3", + "shortname": ":point_right_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand" + ] + }, + "point_right_tone4": { + "unicode": "1F449-1F3FE", + "unicode_alternates": "", + "name": "white right pointing backhand index tone 4", + "shortname": ":point_right_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand" + ] + }, + "point_right_tone5": { + "unicode": "1F449-1F3FF", + "unicode_alternates": "", + "name": "white right pointing backhand index tone 5", + "shortname": ":point_right_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand" + ] + }, "point_up": { "unicode": "261D", - "unicode_alternates": ["261D-FE0F"], + "unicode_alternates": [ + "261D-FE0F" + ], "name": "white up pointing index", "shortname": ":point_up:", "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["direction", "fingers", "hand"], + "keywords": [ + "direction", + "fingers", + "hand" + ], "moji": "☝" }, "point_up_2": { @@ -9944,9 +21399,163 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["direction", "fingers", "hand"], + "keywords": [ + "direction", + "fingers", + "hand" + ], "moji": "👆" }, + "point_up_2_tone1": { + "unicode": "1F446-1F3FB", + "unicode_alternates": "", + "name": "white up pointing backhand index tone 1", + "shortname": ":point_up_2_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand", + "one" + ] + }, + "point_up_2_tone2": { + "unicode": "1F446-1F3FC", + "unicode_alternates": "", + "name": "white up pointing backhand index tone 2", + "shortname": ":point_up_2_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand", + "one" + ] + }, + "point_up_2_tone3": { + "unicode": "1F446-1F3FD", + "unicode_alternates": "", + "name": "white up pointing backhand index tone 3", + "shortname": ":point_up_2_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand", + "one" + ] + }, + "point_up_2_tone4": { + "unicode": "1F446-1F3FE", + "unicode_alternates": "", + "name": "white up pointing backhand index tone 4", + "shortname": ":point_up_2_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand", + "one" + ] + }, + "point_up_2_tone5": { + "unicode": "1F446-1F3FF", + "unicode_alternates": "", + "name": "white up pointing backhand index tone 5", + "shortname": ":point_up_2_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand", + "one" + ] + }, + "point_up_tone1": { + "unicode": "261D-1F3FB", + "unicode_alternates": "", + "name": "white up pointing index tone 1", + "shortname": ":point_up_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand", + "one" + ] + }, + "point_up_tone2": { + "unicode": "261D-1F3FC", + "unicode_alternates": "", + "name": "white up pointing index tone 2", + "shortname": ":point_up_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand", + "one" + ] + }, + "point_up_tone3": { + "unicode": "261D-1F3FD", + "unicode_alternates": "", + "name": "white up pointing index tone 3", + "shortname": ":point_up_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand", + "one" + ] + }, + "point_up_tone4": { + "unicode": "261D-1F3FE", + "unicode_alternates": "", + "name": "white up pointing index tone 4", + "shortname": ":point_up_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand", + "one" + ] + }, + "point_up_tone5": { + "unicode": "261D-1F3FF", + "unicode_alternates": "", + "name": "white up pointing index tone 5", + "shortname": ":point_up_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "direction", + "finger", + "hand", + "one" + ] + }, "police_car": { "unicode": "1F693", "unicode_alternates": [], @@ -9955,7 +21564,21 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["cars", "enforcement", "law", "transportation", "vehicle", "police", "car", "emergency", "ticket", "citation", "crime", "help", "officer"], + "keywords": [ + "cars", + "enforcement", + "law", + "transportation", + "vehicle", + "police", + "car", + "emergency", + "ticket", + "citation", + "crime", + "help", + "officer" + ], "moji": "🚓" }, "poodle": { @@ -9966,7 +21589,18 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["101", "animal", "dog", "nature", "poodle", "dog", "clip", "showy", "sophisticated", "vain"], + "keywords": [ + "101", + "animal", + "dog", + "nature", + "poodle", + "dog", + "clip", + "showy", + "sophisticated", + "vain" + ], "moji": "🐩" }, "poop": { @@ -9975,11 +21609,31 @@ "name": "pile of poo", "shortname": ":poop:", "category": "emoticons", - "aliases": [":shit:", ":hankey:", ":poo:"], + "aliases": [ + ":shit:", + ":hankey:", + ":poo:" + ], "aliases_ascii": [], - "keywords": ["poop", "shit", "shitface", "turd", "poo"], + "keywords": [ + "poop", + "shit", + "shitface", + "turd", + "poo" + ], "moji": "💩" }, + "popcorn": { + "unicode": "1F37F", + "unicode_alternates": "", + "name": "popcorn", + "shortname": ":popcorn:", + "category": "foods", + "aliases": [], + "aliases_ascii": [], + "keywords": [] + }, "post_office": { "unicode": "1F3E3", "unicode_alternates": [], @@ -9988,7 +21642,11 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["building", "communication", "email"], + "keywords": [ + "building", + "communication", + "email" + ], "moji": "🏣" }, "postal_horn": { @@ -9999,7 +21657,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["instrument", "music"], + "keywords": [ + "instrument", + "music" + ], "moji": "📯" }, "postbox": { @@ -10010,7 +21671,11 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["email", "envelope", "letter"], + "keywords": [ + "email", + "envelope", + "letter" + ], "moji": "📮" }, "potable_water": { @@ -10021,7 +21686,21 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square", "cleaning", "faucet", "liquid", "restroom", "potable", "water", "drinkable", "pure", "clear", "clean", "aqua", "h20"], + "keywords": [ + "blue-square", + "cleaning", + "faucet", + "liquid", + "restroom", + "potable", + "water", + "drinkable", + "pure", + "clear", + "clean", + "aqua", + "h20" + ], "moji": "🚰" }, "pouch": { @@ -10032,7 +21711,16 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["accessories", "bag", "pouch", "bag", "cosmetic", "packing", "grandma", "makeup"], + "keywords": [ + "accessories", + "bag", + "pouch", + "bag", + "cosmetic", + "packing", + "grandma", + "makeup" + ], "moji": "👝" }, "poultry_leg": { @@ -10043,7 +21731,14 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "meat", "poultry", "leg", "chicken", "fried"], + "keywords": [ + "food", + "meat", + "poultry", + "leg", + "chicken", + "fried" + ], "moji": "🍗" }, "pound": { @@ -10054,7 +21749,24 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["bills", "british", "currency", "england", "money", "sterling", "uk", "pound", "britain", "british", "banknote", "money", "currency", "paper", "cash", "bills"], + "keywords": [ + "bills", + "british", + "currency", + "england", + "money", + "sterling", + "uk", + "pound", + "britain", + "british", + "banknote", + "money", + "currency", + "paper", + "cash", + "bills" + ], "moji": "💷" }, "pouting_cat": { @@ -10065,7 +21777,15 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "cats", "pout", "annoyed", "miffed", "glower", "frown"], + "keywords": [ + "animal", + "cats", + "pout", + "annoyed", + "miffed", + "glower", + "frown" + ], "moji": "😾" }, "pray": { @@ -10076,9 +21796,136 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["highfive", "hope", "namaste", "please", "wish", "pray", "high five", "hands", "sorrow", "regret", "sorry"], + "keywords": [ + "highfive", + "hope", + "namaste", + "please", + "wish", + "pray", + "high five", + "hands", + "sorrow", + "regret", + "sorry" + ], "moji": "🙏" }, + "pray_tone1": { + "unicode": "1F64F-1F3FB", + "unicode_alternates": "", + "name": "person with folded hands tone 1", + "shortname": ":pray_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "highfive", + "hope", + "namaste", + "please", + "wish", + "pray", + "high five", + "sorrow", + "regret", + "sorry" + ] + }, + "pray_tone2": { + "unicode": "1F64F-1F3FC", + "unicode_alternates": "", + "name": "person with folded hands tone 2", + "shortname": ":pray_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "highfive", + "hope", + "namaste", + "please", + "wish", + "pray", + "high five", + "sorrow", + "regret", + "sorry" + ] + }, + "pray_tone3": { + "unicode": "1F64F-1F3FD", + "unicode_alternates": "", + "name": "person with folded hands tone 3", + "shortname": ":pray_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "highfive", + "hope", + "namaste", + "please", + "wish", + "pray", + "high five", + "sorrow", + "regret", + "sorry" + ] + }, + "pray_tone4": { + "unicode": "1F64F-1F3FE", + "unicode_alternates": "", + "name": "person with folded hands tone 4", + "shortname": ":pray_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "highfive", + "hope", + "namaste", + "please", + "wish", + "pray", + "high five", + "sorrow", + "regret", + "sorry" + ] + }, + "pray_tone5": { + "unicode": "1F64F-1F3FF", + "unicode_alternates": "", + "name": "person with folded hands tone 5", + "shortname": ":pray_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "highfive", + "hope", + "namaste", + "please", + "wish", + "pray", + "high five", + "sorrow", + "regret", + "sorry" + ] + }, + "prayer_beads": { + "unicode": "1F4FF", + "unicode_alternates": "", + "name": "prayer beads", + "shortname": ":prayer_beads:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": [] + }, "princess": { "unicode": "1F478", "unicode_alternates": [], @@ -10087,9 +21934,138 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["blond", "crown", "female", "girl", "woman", "princess", "royal", "royalty", "king", "queen", "daughter", "disney", "high-maintenance"], + "keywords": [ + "blond", + "crown", + "female", + "girl", + "woman", + "princess", + "royal", + "royalty", + "king", + "queen", + "daughter", + "disney", + "high-maintenance" + ], "moji": "👸" }, + "princess_tone1": { + "unicode": "1F478-1F3FB", + "unicode_alternates": "", + "name": "princess tone 1", + "shortname": ":princess_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "blond", + "crown", + "female", + "girl", + "woman", + "royal", + "royalty", + "king", + "queen", + "daughter", + "disney", + "high-maintenance" + ] + }, + "princess_tone2": { + "unicode": "1F478-1F3FC", + "unicode_alternates": "", + "name": "princess tone 2", + "shortname": ":princess_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "blond", + "crown", + "female", + "girl", + "woman", + "royal", + "royalty", + "king", + "queen", + "daughter", + "disney", + "high-maintenance" + ] + }, + "princess_tone3": { + "unicode": "1F478-1F3FD", + "unicode_alternates": "", + "name": "princess tone 3", + "shortname": ":princess_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "blond", + "crown", + "female", + "girl", + "woman", + "royal", + "royalty", + "king", + "queen", + "daughter", + "disney", + "high-maintenance" + ] + }, + "princess_tone4": { + "unicode": "1F478-1F3FE", + "unicode_alternates": "", + "name": "princess tone 4", + "shortname": ":princess_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "blond", + "crown", + "female", + "girl", + "woman", + "royal", + "royalty", + "king", + "queen", + "daughter", + "disney", + "high-maintenance" + ] + }, + "princess_tone5": { + "unicode": "1F478-1F3FF", + "unicode_alternates": "", + "name": "princess tone 5", + "shortname": ":princess_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "blond", + "crown", + "female", + "girl", + "woman", + "royal", + "royalty", + "king", + "queen", + "daughter", + "disney", + "high-maintenance" + ] + }, "printer": { "unicode": "1F5A8", "unicode_alternates": [], @@ -10098,7 +22074,12 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["hardcopy", "paper", "inkjet", "laser"] + "keywords": [ + "hardcopy", + "paper", + "inkjet", + "laser" + ] }, "prohibited": { "unicode": "1F6C7", @@ -10106,9 +22087,19 @@ "name": "prohibited sign", "shortname": ":prohibited:", "category": "objects_symbols", - "aliases": [":prohibited_sign:"], + "aliases": [ + ":prohibited_sign:" + ], "aliases_ascii": [], - "keywords": ["no", "not", "denied", "disallow", "forbid", "limit", "stop"] + "keywords": [ + "no", + "not", + "denied", + "disallow", + "forbid", + "limit", + "stop" + ] }, "projector": { "unicode": "1F4FD", @@ -10116,9 +22107,18 @@ "name": "film projector", "shortname": ":projector:", "category": "objects_symbols", - "aliases": [":film_projector:"], + "aliases": [ + ":film_projector:" + ], "aliases_ascii": [], - "keywords": ["movie", "video", "motion", "picture", "8mm", "16mm"] + "keywords": [ + "movie", + "video", + "motion", + "picture", + "8mm", + "16mm" + ] }, "punch": { "unicode": "1F44A", @@ -10128,9 +22128,77 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["fist", "hand"], + "keywords": [ + "fist", + "hand" + ], "moji": "👊" }, + "punch_tone1": { + "unicode": "1F44A-1F3FB", + "unicode_alternates": "", + "name": "fisted hand sign tone 1", + "shortname": ":punch_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "fist", + "punch" + ] + }, + "punch_tone2": { + "unicode": "1F44A-1F3FC", + "unicode_alternates": "", + "name": "fisted hand sign tone 2", + "shortname": ":punch_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "fist", + "punch" + ] + }, + "punch_tone3": { + "unicode": "1F44A-1F3FD", + "unicode_alternates": "", + "name": "fisted hand sign tone 3", + "shortname": ":punch_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "fist", + "punch" + ] + }, + "punch_tone4": { + "unicode": "1F44A-1F3FE", + "unicode_alternates": "", + "name": "fisted hand sign tone 4", + "shortname": ":punch_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "fist", + "punch" + ] + }, + "punch_tone5": { + "unicode": "1F44A-1F3FF", + "unicode_alternates": "", + "name": "fisted hand sign tone 5", + "shortname": ":punch_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "fist", + "punch" + ] + }, "purple_heart": { "unicode": "1F49C", "unicode_alternates": [], @@ -10139,7 +22207,25 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["affection", "like", "love", "valentines", "purple", "violet", "heart", "love", "sensitive", "understanding", "compassionate", "compassion", "duty", "honor", "royalty", "veteran", "sacrifice"], + "keywords": [ + "affection", + "like", + "love", + "valentines", + "purple", + "violet", + "heart", + "love", + "sensitive", + "understanding", + "compassionate", + "compassion", + "duty", + "honor", + "royalty", + "veteran", + "sacrifice" + ], "moji": "💜" }, "purse": { @@ -10150,7 +22236,20 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["accessories", "fashion", "money", "purse", "clutch", "bag", "handbag", "coin bag", "accessory", "money", "ladies", "shopping"], + "keywords": [ + "accessories", + "fashion", + "money", + "purse", + "clutch", + "bag", + "handbag", + "coin bag", + "accessory", + "money", + "ladies", + "shopping" + ], "moji": "👛" }, "pushpin": { @@ -10161,7 +22260,9 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["stationery"], + "keywords": [ + "stationery" + ], "moji": "📌" }, "pushpin_black": { @@ -10172,7 +22273,9 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["stationery"] + "keywords": [ + "stationery" + ] }, "put_litter_in_its_place": { "unicode": "1F6AE", @@ -10182,7 +22285,15 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square", "litter", "waste", "trash", "garbage", "receptacle", "can"], + "keywords": [ + "blue-square", + "litter", + "waste", + "trash", + "garbage", + "receptacle", + "can" + ], "moji": "🚮" }, "question": { @@ -10193,7 +22304,10 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["confused", "doubt"], + "keywords": [ + "confused", + "doubt" + ], "moji": "❓" }, "rabbit": { @@ -10204,7 +22318,10 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature"], + "keywords": [ + "animal", + "nature" + ], "moji": "🐰" }, "rabbit2": { @@ -10215,7 +22332,15 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature", "rabbit", "bunny", "easter", "reproduction", "prolific"], + "keywords": [ + "animal", + "nature", + "rabbit", + "bunny", + "easter", + "reproduction", + "prolific" + ], "moji": "🐇" }, "race_car": { @@ -10224,9 +22349,18 @@ "name": "racing car", "shortname": ":race_car:", "category": "activity", - "aliases": [":racing_car:"], + "aliases": [ + ":racing_car:" + ], "aliases_ascii": [], - "keywords": ["formula 1", "race", "stock", "nascar", "speed", "drive"] + "keywords": [ + "formula 1", + "race", + "stock", + "nascar", + "speed", + "drive" + ] }, "racehorse": { "unicode": "1F40E", @@ -10236,7 +22370,29 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "gamble", "horse", "powerful", "draft", "calvary", "cowboy", "cowgirl", "mounted", "race", "ride", "gallop", "trot", "colt", "filly", "mare", "stallion", "gelding", "yearling", "thoroughbred", "pony"], + "keywords": [ + "animal", + "gamble", + "horse", + "powerful", + "draft", + "calvary", + "cowboy", + "cowgirl", + "mounted", + "race", + "ride", + "gallop", + "trot", + "colt", + "filly", + "mare", + "stallion", + "gelding", + "yearling", + "thoroughbred", + "pony" + ], "moji": "🐎" }, "radio": { @@ -10247,7 +22403,12 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["communication", "music", "podcast", "program"], + "keywords": [ + "communication", + "music", + "podcast", + "program" + ], "moji": "📻" }, "radio_button": { @@ -10258,9 +22419,25 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["input"], + "keywords": [ + "input" + ], "moji": "🔘" }, + "radioactive": { + "unicode": "2622", + "unicode_alternates": "", + "name": "radioactive sign", + "shortname": ":radioactive:", + "category": "symbols", + "aliases": [ + ":radioactive_sign:" + ], + "aliases_ascii": [], + "keywords": [ + "symbol" + ] + }, "rage": { "unicode": "1F621", "unicode_alternates": [], @@ -10269,7 +22446,16 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["angry", "despise", "hate", "mad", "pout", "anger", "rage", "irate"], + "keywords": [ + "angry", + "despise", + "hate", + "mad", + "pout", + "anger", + "rage", + "irate" + ], "moji": "😡" }, "railway_car": { @@ -10280,7 +22466,15 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["transportation", "vehicle", "railway", "rail", "car", "coach", "train"], + "keywords": [ + "transportation", + "vehicle", + "railway", + "rail", + "car", + "coach", + "train" + ], "moji": "🚃" }, "railway_track": { @@ -10289,9 +22483,17 @@ "name": "railway track", "shortname": ":railway_track:", "category": "travel_places", - "aliases": [":railroad_track:"], + "aliases": [ + ":railroad_track:" + ], "aliases_ascii": [], - "keywords": ["train", "trolley", "subway", "locomotive", "transit"] + "keywords": [ + "train", + "trolley", + "subway", + "locomotive", + "transit" + ] }, "rainbow": { "unicode": "1F308", @@ -10301,7 +22503,21 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["happy", "nature", "photo", "sky", "unicorn", "rainbow", "color", "pride", "diversity", "spectrum", "refract", "leprechaun", "gold"], + "keywords": [ + "happy", + "nature", + "photo", + "sky", + "unicorn", + "rainbow", + "color", + "pride", + "diversity", + "spectrum", + "refract", + "leprechaun", + "gold" + ], "moji": "🌈" }, "raised_hand": { @@ -10312,9 +22528,83 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["female", "girl", "woman"], + "keywords": [ + "female", + "girl", + "woman" + ], "moji": "✋" }, + "raised_hand_tone1": { + "unicode": "270B-1F3FB", + "unicode_alternates": "", + "name": "raised hand tone 1", + "shortname": ":raised_hand_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman" + ] + }, + "raised_hand_tone2": { + "unicode": "270B-1F3FC", + "unicode_alternates": "", + "name": "raised hand tone 2", + "shortname": ":raised_hand_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman" + ] + }, + "raised_hand_tone3": { + "unicode": "270B-1F3FD", + "unicode_alternates": "", + "name": "raised hand tone 3", + "shortname": ":raised_hand_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman" + ] + }, + "raised_hand_tone4": { + "unicode": "270B-1F3FE", + "unicode_alternates": "", + "name": "raised hand tone 4", + "shortname": ":raised_hand_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman" + ] + }, + "raised_hand_tone5": { + "unicode": "270B-1F3FF", + "unicode_alternates": "", + "name": "raised hand tone 5", + "shortname": ":raised_hand_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman" + ] + }, "raised_hands": { "unicode": "1F64C", "unicode_alternates": [], @@ -10323,9 +22613,106 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["gesture", "hooray", "winning", "woot", "yay", "banzai"], + "keywords": [ + "gesture", + "hooray", + "winning", + "woot", + "yay", + "banzai" + ], "moji": "🙌" }, + "raised_hands_tone1": { + "unicode": "1F64C-1F3FB", + "unicode_alternates": "", + "name": "person raising both hands in celebration tone 1", + "shortname": ":raised_hands_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "gesture", + "hooray", + "winning", + "woot", + "yay", + "banzai", + "raised" + ] + }, + "raised_hands_tone2": { + "unicode": "1F64C-1F3FC", + "unicode_alternates": "", + "name": "person raising both hands in celebration tone 2", + "shortname": ":raised_hands_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "gesture", + "hooray", + "winning", + "woot", + "yay", + "banzai", + "raised" + ] + }, + "raised_hands_tone3": { + "unicode": "1F64C-1F3FD", + "unicode_alternates": "", + "name": "person raising both hands in celebration tone 3", + "shortname": ":raised_hands_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "gesture", + "hooray", + "winning", + "woot", + "yay", + "banzai", + "raised" + ] + }, + "raised_hands_tone4": { + "unicode": "1F64C-1F3FE", + "unicode_alternates": "", + "name": "person raising both hands in celebration tone 4", + "shortname": ":raised_hands_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "gesture", + "hooray", + "winning", + "woot", + "yay", + "banzai", + "raised" + ] + }, + "raised_hands_tone5": { + "unicode": "1F64C-1F3FF", + "unicode_alternates": "", + "name": "person raising both hands in celebration tone 5", + "shortname": ":raised_hands_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "gesture", + "hooray", + "winning", + "woot", + "yay", + "banzai", + "raised" + ] + }, "raising_hand": { "unicode": "1F64B", "unicode_alternates": [], @@ -10334,9 +22721,108 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["female", "girl", "woman", "hand", "raise", "notice", "attention", "answer"], + "keywords": [ + "female", + "girl", + "woman", + "hand", + "raise", + "notice", + "attention", + "answer" + ], "moji": "🙋" }, + "raising_hand_tone1": { + "unicode": "1F64B-1F3FB", + "unicode_alternates": "", + "name": "happy person raising one hand tone1", + "shortname": ":raising_hand_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman", + "raise", + "notice", + "attention", + "answer" + ] + }, + "raising_hand_tone2": { + "unicode": "1F64B-1F3FC", + "unicode_alternates": "", + "name": "happy person raising one hand tone2", + "shortname": ":raising_hand_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman", + "raise", + "notice", + "attention", + "answer" + ] + }, + "raising_hand_tone3": { + "unicode": "1F64B-1F3FD", + "unicode_alternates": "", + "name": "happy person raising one hand tone3", + "shortname": ":raising_hand_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman", + "raise", + "notice", + "attention", + "answer" + ] + }, + "raising_hand_tone4": { + "unicode": "1F64B-1F3FE", + "unicode_alternates": "", + "name": "happy person raising one hand tone4", + "shortname": ":raising_hand_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman", + "raise", + "notice", + "attention", + "answer" + ] + }, + "raising_hand_tone5": { + "unicode": "1F64B-1F3FF", + "unicode_alternates": "", + "name": "happy person raising one hand tone5", + "shortname": ":raising_hand_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "woman", + "raise", + "notice", + "attention", + "answer" + ] + }, "ram": { "unicode": "1F40F", "unicode_alternates": [], @@ -10345,7 +22831,16 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature", "sheep", "ram", "sheep", "male", "horn", "horns"], + "keywords": [ + "animal", + "nature", + "sheep", + "ram", + "sheep", + "male", + "horn", + "horns" + ], "moji": "🐏" }, "ramen": { @@ -10356,7 +22851,17 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["chipsticks", "food", "japanese", "noodle", "ramen", "noodles", "bowl", "steaming", "soup"], + "keywords": [ + "chipsticks", + "food", + "japanese", + "noodle", + "ramen", + "noodles", + "bowl", + "steaming", + "soup" + ], "moji": "🍜" }, "rat": { @@ -10367,18 +22872,45 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "mouse", "rat", "rodent", "crooked", "snitch"], + "keywords": [ + "animal", + "mouse", + "rat", + "rodent", + "crooked", + "snitch" + ], "moji": "🐀" }, + "record_button": { + "unicode": "23FA", + "unicode_alternates": "", + "name": "black circle for record", + "shortname": ":record_button:", + "category": "symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "sound", + "symbol" + ] + }, "recycle": { "unicode": "267B", - "unicode_alternates": ["267B-FE0F"], + "unicode_alternates": [ + "267B-FE0F" + ], "name": "black universal recycling symbol", "shortname": ":recycle:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["arrow", "environment", "garbage", "trash"], + "keywords": [ + "arrow", + "environment", + "garbage", + "trash" + ], "moji": "♻" }, "red_car": { @@ -10389,7 +22921,10 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["transportation", "vehicle"], + "keywords": [ + "transportation", + "vehicle" + ], "moji": "🚗" }, "red_circle": { @@ -10400,7 +22935,9 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["shape"], + "keywords": [ + "shape" + ], "moji": "🔴" }, "registered": { @@ -10412,17 +22949,28 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["alphabet", "circle"] + "keywords": [ + "alphabet", + "circle" + ] }, "relaxed": { "unicode": "263A", - "unicode_alternates": ["263A-FE0F"], + "unicode_alternates": [ + "263A-FE0F" + ], "name": "white smiling face", "shortname": ":relaxed:", "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["blush", "face", "happiness", "massage", "smile"], + "keywords": [ + "blush", + "face", + "happiness", + "massage", + "smile" + ], "moji": "☺" }, "relieved": { @@ -10433,7 +22981,17 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["face", "happiness", "massage", "phew", "relaxed", "relieved", "satisfied", "phew", "relief"], + "keywords": [ + "face", + "happiness", + "massage", + "phew", + "relaxed", + "relieved", + "satisfied", + "phew", + "relief" + ], "moji": "😌" }, "reminder_ribbon": { @@ -10444,7 +23002,9 @@ "category": "celebration", "aliases": [], "aliases_ascii": [], - "keywords": ["awareness"] + "keywords": [ + "awareness" + ] }, "repeat": { "unicode": "1F501", @@ -10454,7 +23014,10 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["loop", "record"], + "keywords": [ + "loop", + "record" + ], "moji": "🔁" }, "repeat_one": { @@ -10465,7 +23028,10 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square", "loop"], + "keywords": [ + "blue-square", + "loop" + ], "moji": "🔂" }, "restroom": { @@ -10476,7 +23042,17 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square", "woman", "man", "unisex", "bathroom", "restroom", "sign", "shared", "toilet"], + "keywords": [ + "blue-square", + "woman", + "man", + "unisex", + "bathroom", + "restroom", + "sign", + "shared", + "toilet" + ], "moji": "🚻" }, "revolving_hearts": { @@ -10487,7 +23063,19 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["affection", "like", "love", "valentines", "heart", "hearts", "revolving", "moving", "circle", "multiple", "lovers"], + "keywords": [ + "affection", + "like", + "love", + "valentines", + "heart", + "hearts", + "revolving", + "moving", + "circle", + "multiple", + "lovers" + ], "moji": "💞" }, "rewind": { @@ -10498,7 +23086,10 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square", "play"], + "keywords": [ + "blue-square", + "play" + ], "moji": "⏪" }, "ribbon": { @@ -10509,7 +23100,16 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["bowtie", "decoration", "girl", "pink", "ribbon", "lace", "wrap", "decorate"], + "keywords": [ + "bowtie", + "decoration", + "girl", + "pink", + "ribbon", + "lace", + "wrap", + "decorate" + ], "moji": "🎀" }, "rice": { @@ -10520,7 +23120,14 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "rice", "white", "grain", "food", "bowl"], + "keywords": [ + "food", + "rice", + "white", + "grain", + "food", + "bowl" + ], "moji": "🍚" }, "rice_ball": { @@ -10531,7 +23138,16 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "japanese", "rice", "ball", "white", "nori", "seaweed", "japanese"], + "keywords": [ + "food", + "japanese", + "rice", + "ball", + "white", + "nori", + "seaweed", + "japanese" + ], "moji": "🍙" }, "rice_cracker": { @@ -10542,7 +23158,15 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "japanese", "rice", "cracker", "seaweed", "food", "japanese"], + "keywords": [ + "food", + "japanese", + "rice", + "cracker", + "seaweed", + "food", + "japanese" + ], "moji": "🍘" }, "rice_scene": { @@ -10553,7 +23177,18 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["photo", "moon", "viewing", "observing", "otsukimi", "tsukimi", "rice", "scene", "festival", "autumn"], + "keywords": [ + "photo", + "moon", + "viewing", + "observing", + "otsukimi", + "tsukimi", + "rice", + "scene", + "festival", + "autumn" + ], "moji": "🎑" }, "right_speaker": { @@ -10564,7 +23199,13 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["sound", "listen", "hear", "noise", "volume"] + "keywords": [ + "sound", + "listen", + "hear", + "noise", + "volume" + ] }, "right_speaker_one": { "unicode": "1F569", @@ -10572,9 +23213,14 @@ "name": "right speaker with one sound wave", "shortname": ":right_speaker_one:", "category": "objects_symbols", - "aliases": [":right_speaker_with_one_sound_wave:"], + "aliases": [ + ":right_speaker_with_one_sound_wave:" + ], "aliases_ascii": [], - "keywords": ["low", "volume"] + "keywords": [ + "low", + "volume" + ] }, "right_speaker_three": { "unicode": "1F56A", @@ -10582,9 +23228,15 @@ "name": "right speaker with three sound waves", "shortname": ":right_speaker_three:", "category": "objects_symbols", - "aliases": [":right_speaker_with_three_sound_waves:"], + "aliases": [ + ":right_speaker_with_three_sound_waves:" + ], "aliases_ascii": [], - "keywords": ["loud", "high", "volume"] + "keywords": [ + "loud", + "high", + "volume" + ] }, "ring": { "unicode": "1F48D", @@ -10594,7 +23246,12 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["marriage", "propose", "valentines", "wedding"], + "keywords": [ + "marriage", + "propose", + "valentines", + "wedding" + ], "moji": "💍" }, "ringing_bell": { @@ -10605,7 +23262,25 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["alert", "ding", "volume", "sound", "chime"] + "keywords": [ + "alert", + "ding", + "volume", + "sound", + "chime" + ] + }, + "robot": { + "unicode": "1F916", + "unicode_alternates": "", + "name": "robot face", + "shortname": ":robot:", + "category": "people", + "aliases": [ + ":robot_face:" + ], + "aliases_ascii": [], + "keywords": [] }, "rocket": { "unicode": "1F680", @@ -10615,7 +23290,16 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["launch", "ship", "staffmode", "rocket", "space", "spacecraft", "astronaut", "cosmonaut"], + "keywords": [ + "launch", + "ship", + "staffmode", + "rocket", + "space", + "spacecraft", + "astronaut", + "cosmonaut" + ], "moji": "🚀" }, "roller_coaster": { @@ -10626,9 +23310,34 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["carnival", "fun", "photo", "play", "playground", "roller", "coaster", "amusement", "park", "fair", "ride", "entertainment"], + "keywords": [ + "carnival", + "fun", + "photo", + "play", + "playground", + "roller", + "coaster", + "amusement", + "park", + "fair", + "ride", + "entertainment" + ], "moji": "🎢" }, + "rolling_eyes": { + "unicode": "1F644", + "unicode_alternates": "", + "name": "face with rolling eyes", + "shortname": ":rolling_eyes:", + "category": "people", + "aliases": [ + ":face_with_rolling_eyes:" + ], + "aliases_ascii": [], + "keywords": [] + }, "rooster": { "unicode": "1F413", "unicode_alternates": [], @@ -10637,7 +23346,17 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "chicken", "nature", "rooster", "cockerel", "cock", "male", "cock-a-doodle-doo", "crowing"], + "keywords": [ + "animal", + "chicken", + "nature", + "rooster", + "cockerel", + "cock", + "male", + "cock-a-doodle-doo", + "crowing" + ], "moji": "🐓" }, "rose": { @@ -10648,7 +23367,18 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["flowers", "love", "valentines", "rose", "fragrant", "flower", "thorns", "love", "petals", "romance"], + "keywords": [ + "flowers", + "love", + "valentines", + "rose", + "fragrant", + "flower", + "thorns", + "love", + "petals", + "romance" + ], "moji": "🌹" }, "rosette": { @@ -10659,7 +23389,9 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["flower"] + "keywords": [ + "flower" + ] }, "rosette_black": { "unicode": "1F3F6", @@ -10669,7 +23401,9 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["flower"] + "keywords": [ + "flower" + ] }, "rotating_light": { "unicode": "1F6A8", @@ -10679,7 +23413,15 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["911", "ambulance", "emergency", "police", "light", "police", "emergency"], + "keywords": [ + "911", + "ambulance", + "emergency", + "police", + "light", + "police", + "emergency" + ], "moji": "🚨" }, "round_pushpin": { @@ -10690,7 +23432,9 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["stationery"], + "keywords": [ + "stationery" + ], "moji": "📍" }, "rowboat": { @@ -10701,9 +23445,108 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["hobby", "ship", "sports", "water", "boat", "row", "oar", "paddle"], + "keywords": [ + "hobby", + "ship", + "sports", + "water", + "boat", + "row", + "oar", + "paddle" + ], "moji": "🚣" }, + "rowboat_tone1": { + "unicode": "1F6A3-1F3FB", + "unicode_alternates": "", + "name": "rowboat tone 1", + "shortname": ":rowboat_tone1:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "hobby", + "ship", + "water", + "boat", + "row", + "oar", + "paddle" + ] + }, + "rowboat_tone2": { + "unicode": "1F6A3-1F3FC", + "unicode_alternates": "", + "name": "rowboat tone 2", + "shortname": ":rowboat_tone2:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "hobby", + "ship", + "water", + "boat", + "row", + "oar", + "paddle" + ] + }, + "rowboat_tone3": { + "unicode": "1F6A3-1F3FD", + "unicode_alternates": "", + "name": "rowboat tone 3", + "shortname": ":rowboat_tone3:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "hobby", + "ship", + "water", + "boat", + "row", + "oar", + "paddle" + ] + }, + "rowboat_tone4": { + "unicode": "1F6A3-1F3FE", + "unicode_alternates": "", + "name": "rowboat tone 4", + "shortname": ":rowboat_tone4:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "hobby", + "ship", + "water", + "boat", + "row", + "oar", + "paddle" + ] + }, + "rowboat_tone5": { + "unicode": "1F6A3-1F3FF", + "unicode_alternates": "", + "name": "rowboat tone 5", + "shortname": ":rowboat_tone5:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "hobby", + "ship", + "water", + "boat", + "row", + "oar", + "paddle" + ] + }, "rugby_football": { "unicode": "1F3C9", "unicode_alternates": [], @@ -10712,7 +23555,15 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["sports", "rugby", "football", "ball", "sport", "team", "england"], + "keywords": [ + "sports", + "rugby", + "football", + "ball", + "sport", + "team", + "england" + ], "moji": "🏉" }, "runner": { @@ -10723,9 +23574,115 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["exercise", "man", "walking", "run", "runner", "jog", "exercise", "sprint", "race", "dash"], + "keywords": [ + "exercise", + "man", + "walking", + "run", + "runner", + "jog", + "exercise", + "sprint", + "race", + "dash" + ], "moji": "🏃" }, + "runner_tone1": { + "unicode": "1F3C3-1F3FB", + "unicode_alternates": "", + "name": "runner tone 1", + "shortname": ":runner_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "exercise", + "man", + "run", + "jog", + "sprint", + "race", + "dash", + "marathon" + ] + }, + "runner_tone2": { + "unicode": "1F3C3-1F3FC", + "unicode_alternates": "", + "name": "runner tone 2", + "shortname": ":runner_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "exercise", + "man", + "run", + "jog", + "sprint", + "race", + "dash", + "marathon" + ] + }, + "runner_tone3": { + "unicode": "1F3C3-1F3FD", + "unicode_alternates": "", + "name": "runner tone 3", + "shortname": ":runner_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "exercise", + "man", + "run", + "jog", + "sprint", + "race", + "dash", + "marathon" + ] + }, + "runner_tone4": { + "unicode": "1F3C3-1F3FE", + "unicode_alternates": "", + "name": "runner tone 4", + "shortname": ":runner_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "exercise", + "man", + "run", + "jog", + "sprint", + "race", + "dash", + "marathon" + ] + }, + "runner_tone5": { + "unicode": "1F3C3-1F3FF", + "unicode_alternates": "", + "name": "runner tone 5", + "shortname": ":runner_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "exercise", + "man", + "run", + "jog", + "sprint", + "race", + "dash", + "marathon" + ] + }, "running_shirt_with_sash": { "unicode": "1F3BD", "unicode_alternates": [], @@ -10734,29 +23691,73 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["pageant", "play", "running", "run", "shirt", "cloths", "compete", "sports"], + "keywords": [ + "pageant", + "play", + "running", + "run", + "shirt", + "cloths", + "compete", + "sports" + ], "moji": "🎽" }, + "sa": { + "unicode": "1F202", + "unicode_alternates": "", + "name": "squared katakana sa", + "shortname": ":sa:", + "category": "symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "blue-square", + "japanese", + "symbol", + "word" + ] + }, "sagittarius": { "unicode": "2650", - "unicode_alternates": ["2650-FE0F"], + "unicode_alternates": [ + "2650-FE0F" + ], "name": "sagittarius", "shortname": ":sagittarius:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["sagittarius", "centaur", "archer", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "sign", "zodiac", "horoscope"], + "keywords": [ + "sagittarius", + "centaur", + "archer", + "astrology", + "greek", + "constellation", + "stars", + "zodiac", + "sign", + "sign", + "zodiac", + "horoscope" + ], "moji": "♐" }, "sailboat": { "unicode": "26F5", - "unicode_alternates": ["26F5-FE0F"], + "unicode_alternates": [ + "26F5-FE0F" + ], "name": "sailboat", "shortname": ":sailboat:", "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["ship", "transportation"], + "keywords": [ + "ship", + "transportation" + ], "moji": "⛵" }, "sake": { @@ -10767,7 +23768,19 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["beverage", "drink", "drunk", "wine", "sake", "wine", "rice", "ferment", "alcohol", "japanese", "drink"], + "keywords": [ + "beverage", + "drink", + "drunk", + "wine", + "sake", + "wine", + "rice", + "ferment", + "alcohol", + "japanese", + "drink" + ], "moji": "🍶" }, "sandal": { @@ -10778,7 +23791,10 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["fashion", "shoes"], + "keywords": [ + "fashion", + "shoes" + ], "moji": "👡" }, "santa": { @@ -10789,9 +23805,159 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["christmas", "father christmas", "festival", "male", "man", "xmas", "santa", "saint nick", "jolly", "ho ho ho", "north pole", "presents", "gifts", "naughty", "nice", "sleigh", "father", "christmas", "holiday"], + "keywords": [ + "christmas", + "father christmas", + "festival", + "male", + "man", + "xmas", + "santa", + "saint nick", + "jolly", + "ho ho ho", + "north pole", + "presents", + "gifts", + "naughty", + "nice", + "sleigh", + "father", + "christmas", + "holiday" + ], "moji": "🎅" }, + "santa_tone1": { + "unicode": "1F385-1F3FB", + "unicode_alternates": "", + "name": "father christmas tone 1", + "shortname": ":santa_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "festival", + "male", + "man", + "xmas", + "santa", + "saint nick", + "jolly", + "ho ho ho", + "north pole", + "presents", + "gifts", + "naughty", + "nice", + "sleigh", + "holiday" + ] + }, + "santa_tone2": { + "unicode": "1F385-1F3FC", + "unicode_alternates": "", + "name": "father christmas tone 2", + "shortname": ":santa_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "festival", + "male", + "man", + "xmas", + "santa", + "saint nick", + "jolly", + "ho ho ho", + "north pole", + "presents", + "gifts", + "naughty", + "nice", + "sleigh", + "holiday" + ] + }, + "santa_tone3": { + "unicode": "1F385-1F3FD", + "unicode_alternates": "", + "name": "father christmas tone 3", + "shortname": ":santa_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "festival", + "male", + "man", + "xmas", + "santa", + "saint nick", + "jolly", + "ho ho ho", + "north pole", + "presents", + "gifts", + "naughty", + "nice", + "sleigh", + "holiday" + ] + }, + "santa_tone4": { + "unicode": "1F385-1F3FE", + "unicode_alternates": "", + "name": "father christmas tone 4", + "shortname": ":santa_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "festival", + "male", + "man", + "xmas", + "santa", + "saint nick", + "jolly", + "ho ho ho", + "north pole", + "presents", + "gifts", + "naughty", + "nice", + "sleigh", + "holiday" + ] + }, + "santa_tone5": { + "unicode": "1F385-1F3FF", + "unicode_alternates": "", + "name": "father christmas tone 5", + "shortname": ":santa_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "festival", + "male", + "man", + "xmas", + "santa", + "saint nick", + "jolly", + "ho ho ho", + "north pole", + "presents", + "gifts", + "naughty", + "nice", + "sleigh", + "holiday" + ] + }, "satellite": { "unicode": "1F4E1", "unicode_alternates": [], @@ -10800,7 +23966,9 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["communication"], + "keywords": [ + "communication" + ], "moji": "📡" }, "satellite_orbital": { @@ -10811,7 +23979,11 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["communication", "orbital", "space"] + "keywords": [ + "communication", + "orbital", + "space" + ] }, "saxophone": { "unicode": "1F3B7", @@ -10821,9 +23993,35 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["instrument", "music", "saxophone", "sax", "music", "instrument", "woodwind"], + "keywords": [ + "instrument", + "music", + "saxophone", + "sax", + "music", + "instrument", + "woodwind" + ], "moji": "🎷" }, + "scales": { + "unicode": "2696", + "unicode_alternates": "", + "name": "scales", + "shortname": ":scales:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "balance", + "justice", + "libra", + "object", + "tool", + "weight", + "zodiac" + ] + }, "school": { "unicode": "1F3EB", "unicode_alternates": [], @@ -10832,7 +24030,17 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["building", "school", "university", "elementary", "middle", "high", "college", "teach", "education"], + "keywords": [ + "building", + "school", + "university", + "elementary", + "middle", + "high", + "college", + "teach", + "education" + ], "moji": "🏫" }, "school_satchel": { @@ -10843,29 +24051,74 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["bag", "education", "student", "school", "satchel", "backpack", "bag", "packing", "pack", "hike", "education", "adventure", "travel", "sightsee"], + "keywords": [ + "bag", + "education", + "student", + "school", + "satchel", + "backpack", + "bag", + "packing", + "pack", + "hike", + "education", + "adventure", + "travel", + "sightsee" + ], "moji": "🎒" }, "scissors": { "unicode": "2702", - "unicode_alternates": ["2702-FE0F"], + "unicode_alternates": [ + "2702-FE0F" + ], "name": "black scissors", "shortname": ":scissors:", "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["cut", "stationery"], + "keywords": [ + "cut", + "stationery" + ], "moji": "✂" }, + "scorpion": { + "unicode": "1F982", + "unicode_alternates": "", + "name": "scorpion", + "shortname": ":scorpion:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": [] + }, "scorpius": { "unicode": "264F", - "unicode_alternates": ["264F-FE0F"], + "unicode_alternates": [ + "264F-FE0F" + ], "name": "scorpius", "shortname": ":scorpius:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["scorpius", "scorpion", "scorpio", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "sign", "zodiac", "horoscope"], + "keywords": [ + "scorpius", + "scorpion", + "scorpio", + "astrology", + "greek", + "constellation", + "stars", + "zodiac", + "sign", + "sign", + "zodiac", + "horoscope" + ], "moji": "♏" }, "scream": { @@ -10876,7 +24129,14 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["face", "munch", "scream", "painting", "artist", "alien"], + "keywords": [ + "face", + "munch", + "scream", + "painting", + "artist", + "alien" + ], "moji": "😱" }, "scream_cat": { @@ -10887,7 +24147,22 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "cats", "munch", "weary", "sleepy", "tired", "tiredness", "study", "finals", "school", "exhausted", "scream", "painting", "artist"], + "keywords": [ + "animal", + "cats", + "munch", + "weary", + "sleepy", + "tired", + "tiredness", + "study", + "finals", + "school", + "exhausted", + "scream", + "painting", + "artist" + ], "moji": "🙀" }, "scroll": { @@ -10898,7 +24173,9 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["documents"], + "keywords": [ + "documents" + ], "moji": "📜" }, "seat": { @@ -10909,18 +24186,24 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["sit"], + "keywords": [ + "sit" + ], "moji": "💺" }, "secret": { "unicode": "3299", - "unicode_alternates": ["3299-FE0F"], + "unicode_alternates": [ + "3299-FE0F" + ], "name": "circled ideograph secret", "shortname": ":secret:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["privacy"], + "keywords": [ + "privacy" + ], "moji": "㊙" }, "see_no_evil": { @@ -10931,7 +24214,17 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "monkey", "nature", "monkey", "see", "eyes", "vision", "sight", "mizaru"], + "keywords": [ + "animal", + "monkey", + "nature", + "monkey", + "see", + "eyes", + "vision", + "sight", + "mizaru" + ], "moji": "🙈" }, "seedling": { @@ -10942,19 +24235,49 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["grass", "lawn", "nature", "plant", "seedling", "plant", "new", "start", "grow"], + "keywords": [ + "grass", + "lawn", + "nature", + "plant", + "seedling", + "plant", + "new", + "start", + "grow" + ], "moji": "🌱" }, "seven": { "moji": "7️⃣", "unicode": "0037-20E3", - "unicode_alternates": ["0037-FE0F-20E3"], + "unicode_alternates": [ + "0037-FE0F-20E3" + ], "name": "digit seven", "shortname": ":seven:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["7", "blue-square", "numbers", "prime"] + "keywords": [ + "7", + "blue-square", + "numbers", + "prime" + ] + }, + "shamrock": { + "unicode": "2618", + "unicode_alternates": "", + "name": "shamrock", + "shortname": ":shamrock:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "nature", + "plant" + ] }, "shaved_ice": { "unicode": "1F367", @@ -10964,7 +24287,16 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["desert", "hot", "shaved", "ice", "dessert", "treat", "syrup", "flavoring"], + "keywords": [ + "desert", + "hot", + "shaved", + "ice", + "dessert", + "treat", + "syrup", + "flavoring" + ], "moji": "🍧" }, "sheep": { @@ -10975,7 +24307,17 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature", "sheep", "wool", "flock", "follower", "ewe", "female", "lamb"], + "keywords": [ + "animal", + "nature", + "sheep", + "wool", + "flock", + "follower", + "ewe", + "female", + "lamb" + ], "moji": "🐑" }, "shell": { @@ -10986,7 +24328,17 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["beach", "nature", "sea", "shell", "spiral", "beach", "sand", "crab", "nautilus"], + "keywords": [ + "beach", + "nature", + "sea", + "shell", + "spiral", + "beach", + "sand", + "crab", + "nautilus" + ], "moji": "🐚" }, "shield": { @@ -10997,7 +24349,26 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["interstate", "route", "sign", "highway", "interstate"] + "keywords": [ + "interstate", + "route", + "sign", + "highway", + "interstate" + ] + }, + "shinto_shrine": { + "unicode": "26E9", + "unicode_alternates": "", + "name": "shinto shrine", + "shortname": ":shinto_shrine:", + "category": "travel", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "religion", + "symbol" + ] }, "ship": { "unicode": "1F6A2", @@ -11007,7 +24378,13 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["titanic", "transportation", "ferry", "ship", "boat"], + "keywords": [ + "titanic", + "transportation", + "ferry", + "ship", + "boat" + ], "moji": "🚢" }, "shirt": { @@ -11018,7 +24395,10 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["cloth", "fashion"], + "keywords": [ + "cloth", + "fashion" + ], "moji": "👕" }, "shopping_bags": { @@ -11029,7 +24409,13 @@ "category": "travel_places", "aliases": [], "aliases_ascii": [], - "keywords": ["purchase", "mall", "buy", "store", "shop"] + "keywords": [ + "purchase", + "mall", + "buy", + "store", + "shop" + ] }, "shower": { "unicode": "1F6BF", @@ -11039,7 +24425,18 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["bath", "clean", "wash", "bathroom", "shower", "soap", "water", "clean", "shampoo", "lather"], + "keywords": [ + "bath", + "clean", + "wash", + "bathroom", + "shower", + "soap", + "water", + "clean", + "shampoo", + "lather" + ], "moji": "🚿" }, "signal_strength": { @@ -11050,19 +24447,27 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square"], + "keywords": [ + "blue-square" + ], "moji": "📶" }, "six": { "moji": "6️⃣", "unicode": "0036-20E3", - "unicode_alternates": ["0036-FE0F-20E3"], + "unicode_alternates": [ + "0036-FE0F-20E3" + ], "name": "digit six", "shortname": ":six:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["6", "blue-square", "numbers"] + "keywords": [ + "6", + "blue-square", + "numbers" + ] }, "six_pointed_star": { "unicode": "1F52F", @@ -11072,7 +24477,9 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["purple-square"], + "keywords": [ + "purple-square" + ], "moji": "🔯" }, "ski": { @@ -11083,20 +24490,75 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["cold", "sports", "winter", "ski", "downhill", "cross-country", "poles", "snow", "winter", "mountain", "alpine", "powder", "slalom", "freestyle"], + "keywords": [ + "cold", + "sports", + "winter", + "ski", + "downhill", + "cross-country", + "poles", + "snow", + "winter", + "mountain", + "alpine", + "powder", + "slalom", + "freestyle" + ], "moji": "🎿" }, + "skier": { + "unicode": "26F7", + "unicode_alternates": "", + "name": "skier", + "shortname": ":skier:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "person", + "ski", + "snow", + "sport", + "travel" + ] + }, "skull": { "unicode": "1F480", "unicode_alternates": [], "name": "skull", "shortname": ":skull:", "category": "emoticons", - "aliases": [":skeleton:"], + "aliases": [ + ":skeleton:" + ], "aliases_ascii": [], - "keywords": ["dead", "skeleton", "dying"], + "keywords": [ + "dead", + "skeleton", + "dying" + ], "moji": "💀" }, + "skull_crossbones": { + "unicode": "2620", + "unicode_alternates": "", + "name": "skull and crossbones", + "shortname": ":skull_crossbones:", + "category": "objects", + "aliases": [ + ":skull_and_crossbones:" + ], + "aliases_ascii": [], + "keywords": [ + "body", + "death", + "face", + "monster", + "person" + ] + }, "sleeping": { "unicode": "1F634", "unicode_alternates": [], @@ -11105,7 +24567,15 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["face", "sleepy", "tired", "sleep", "sleepy", "sleeping", "snore"], + "keywords": [ + "face", + "sleepy", + "tired", + "sleep", + "sleepy", + "sleeping", + "snore" + ], "moji": "😴" }, "sleeping_accommodation": { @@ -11116,7 +24586,11 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["hotel", "motel", "rest"] + "keywords": [ + "hotel", + "motel", + "rest" + ] }, "sleepy": { "unicode": "1F62A", @@ -11126,7 +24600,14 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["face", "rest", "tired", "sleepy", "tired", "exhausted"], + "keywords": [ + "face", + "rest", + "tired", + "sleepy", + "tired", + "exhausted" + ], "moji": "😪" }, "slight_frown": { @@ -11135,9 +24616,16 @@ "name": "slightly frowning face", "shortname": ":slight_frown:", "category": "people", - "aliases": [":slightly_frowning_face:"], + "aliases": [ + ":slightly_frowning_face:" + ], "aliases_ascii": [], - "keywords": ["slight", "frown", "unhappy", "disappointed"] + "keywords": [ + "slight", + "frown", + "unhappy", + "disappointed" + ] }, "slight_smile": { "unicode": "1F642", @@ -11145,9 +24633,15 @@ "name": "slightly smiling face", "shortname": ":slight_smile:", "category": "people", - "aliases": [":slightly_smiling_face:"], + "aliases": [ + ":slightly_smiling_face:" + ], "aliases_ascii": [], - "keywords": ["slight", "smile", "happy"] + "keywords": [ + "slight", + "smile", + "happy" + ] }, "slot_machine": { "unicode": "1F3B0", @@ -11157,7 +24651,17 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["bet", "gamble", "vegas", "slot", "machine", "gamble", "one-armed bandit", "slots", "luck"], + "keywords": [ + "bet", + "gamble", + "vegas", + "slot", + "machine", + "gamble", + "one-armed bandit", + "slots", + "luck" + ], "moji": "🎰" }, "small_blue_diamond": { @@ -11168,7 +24672,9 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["shape"], + "keywords": [ + "shape" + ], "moji": "🔹" }, "small_orange_diamond": { @@ -11179,7 +24685,9 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["shape"], + "keywords": [ + "shape" + ], "moji": "🔸" }, "small_red_triangle": { @@ -11190,7 +24698,9 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["shape"], + "keywords": [ + "shape" + ], "moji": "🔺" }, "small_red_triangle_down": { @@ -11201,7 +24711,9 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["shape"], + "keywords": [ + "shape" + ], "moji": "🔻" }, "smile": { @@ -11211,8 +24723,24 @@ "shortname": ":smile:", "category": "emoticons", "aliases": [], - "aliases_ascii": [":)", ":-)", "=]", "=)", ":]"], - "keywords": ["face", "funny", "haha", "happy", "joy", "laugh", "smile", "smiley", "smiling"], + "aliases_ascii": [ + ":)", + ":-)", + "=]", + "=)", + ":]" + ], + "keywords": [ + "face", + "funny", + "haha", + "happy", + "joy", + "laugh", + "smile", + "smiley", + "smiling" + ], "moji": "😄" }, "smile_cat": { @@ -11223,7 +24751,14 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "cats", "cat", "smile", "grin", "grinning"], + "keywords": [ + "animal", + "cats", + "cat", + "smile", + "grin", + "grinning" + ], "moji": "😸" }, "smiley": { @@ -11233,8 +24768,20 @@ "shortname": ":smiley:", "category": "emoticons", "aliases": [], - "aliases_ascii": [":D", ":-D", "=D"], - "keywords": ["face", "haha", "happy", "joy", "smiling", "smile", "smiley"], + "aliases_ascii": [ + ":D", + ":-D", + "=D" + ], + "keywords": [ + "face", + "haha", + "happy", + "joy", + "smiling", + "smile", + "smiley" + ], "moji": "😃" }, "smiley_cat": { @@ -11245,7 +24792,15 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "cats", "happy", "smile", "smiley", "cat", "happy"], + "keywords": [ + "animal", + "cats", + "happy", + "smile", + "smiley", + "cat", + "happy" + ], "moji": "😺" }, "smiling_imp": { @@ -11256,7 +24811,14 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["devil", "horns", "horns", "devil", "impish", "trouble"], + "keywords": [ + "devil", + "horns", + "horns", + "devil", + "impish", + "trouble" + ], "moji": "😈" }, "smirk": { @@ -11267,7 +24829,18 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["mean", "prank", "smile", "smug", "smirking", "smirk", "smug", "smile", "half-smile", "conceited"], + "keywords": [ + "mean", + "prank", + "smile", + "smug", + "smirking", + "smirk", + "smug", + "smile", + "half-smile", + "conceited" + ], "moji": "😏" }, "smirk_cat": { @@ -11278,7 +24851,15 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "cats", "smirk", "smirking", "wry", "confident", "confidence"], + "keywords": [ + "animal", + "cats", + "smirk", + "smirking", + "wry", + "confident", + "confidence" + ], "moji": "😼" }, "smoking": { @@ -11289,7 +24870,19 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["cigarette", "kills", "tobacco", "smoking", "cigarette", "smoke", "cancer", "lungs", "inhale", "tar", "nicotine"], + "keywords": [ + "cigarette", + "kills", + "tobacco", + "smoking", + "cigarette", + "smoke", + "cancer", + "lungs", + "inhale", + "tar", + "nicotine" + ], "moji": "🚬" }, "snail": { @@ -11300,7 +24893,16 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "shell", "slow", "snail", "slow", "escargot", "french", "appetizer"], + "keywords": [ + "animal", + "shell", + "slow", + "snail", + "slow", + "escargot", + "french", + "appetizer" + ], "moji": "🐌" }, "snake": { @@ -11311,7 +24913,10 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "evil"], + "keywords": [ + "animal", + "evil" + ], "moji": "🐍" }, "snowboarder": { @@ -11322,31 +24927,89 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["sports", "winter", "snow", "boarding", "sports", "freestyle", "halfpipe", "board", "mountain", "alpine", "winter"], + "keywords": [ + "sports", + "winter", + "snow", + "boarding", + "sports", + "freestyle", + "halfpipe", + "board", + "mountain", + "alpine", + "winter" + ], "moji": "🏂" }, "snowflake": { "unicode": "2744", - "unicode_alternates": ["2744-FE0F"], + "unicode_alternates": [ + "2744-FE0F" + ], "name": "snowflake", "shortname": ":snowflake:", "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["christmas", "cold", "season", "weather", "winter", "xmas", "snowflake", "snow", "frozen", "droplet", "ice", "crystal", "cold", "chilly", "winter", "unique", "special", "below zero", "elsa"], + "keywords": [ + "christmas", + "cold", + "season", + "weather", + "winter", + "xmas", + "snowflake", + "snow", + "frozen", + "droplet", + "ice", + "crystal", + "cold", + "chilly", + "winter", + "unique", + "special", + "below zero", + "elsa" + ], "moji": "❄" }, "snowman": { "unicode": "26C4", - "unicode_alternates": ["26C4-FE0F"], + "unicode_alternates": [ + "26C4-FE0F" + ], "name": "snowman without snow", "shortname": ":snowman:", "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["christmas", "cold", "season", "weather", "winter", "xmas"], + "keywords": [ + "christmas", + "cold", + "season", + "weather", + "winter", + "xmas" + ], "moji": "⛄" }, + "snowman2": { + "unicode": "2603", + "unicode_alternates": "", + "name": "snowman", + "shortname": ":snowman2:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "cold", + "nature", + "snow", + "weather" + ] + }, "sob": { "unicode": "1F62D", "unicode_alternates": [], @@ -11355,18 +25018,41 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["cry", "face", "sad", "tears", "upset", "cry", "sob", "tears", "sad", "melancholy", "morn", "somber", "hurt"], + "keywords": [ + "cry", + "face", + "sad", + "tears", + "upset", + "cry", + "sob", + "tears", + "sad", + "melancholy", + "morn", + "somber", + "hurt" + ], "moji": "😭" }, "soccer": { "unicode": "26BD", - "unicode_alternates": ["26BD-FE0F"], + "unicode_alternates": [ + "26BD-FE0F" + ], "name": "soccer ball", "shortname": ":soccer:", "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["balls", "fifa", "football", "sports", "european", "football"], + "keywords": [ + "balls", + "fifa", + "football", + "sports", + "european", + "football" + ], "moji": "⚽" }, "soon": { @@ -11377,7 +25063,10 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["arrow", "words"], + "keywords": [ + "arrow", + "words" + ], "moji": "🔜" }, "sos": { @@ -11388,7 +25077,12 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["emergency", "help", "red-square", "words"], + "keywords": [ + "emergency", + "help", + "red-square", + "words" + ], "moji": "🆘" }, "sound": { @@ -11399,7 +25093,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["speaker", "volume"], + "keywords": [ + "speaker", + "volume" + ], "moji": "🔉" }, "space_invader": { @@ -11410,18 +25107,26 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["arcade", "game"], + "keywords": [ + "arcade", + "game" + ], "moji": "👾" }, "spades": { "unicode": "2660", - "unicode_alternates": ["2660-FE0F"], + "unicode_alternates": [ + "2660-FE0F" + ], "name": "black spade suit", "shortname": ":spades:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["cards", "poker"], + "keywords": [ + "cards", + "poker" + ], "moji": "♠" }, "spaghetti": { @@ -11432,18 +25137,32 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "italian", "noodle", "spaghetti", "noodles", "tomato", "sauce", "italian"], + "keywords": [ + "food", + "italian", + "noodle", + "spaghetti", + "noodles", + "tomato", + "sauce", + "italian" + ], "moji": "🍝" }, "sparkle": { "unicode": "2747", - "unicode_alternates": ["2747-FE0F"], + "unicode_alternates": [ + "2747-FE0F" + ], "name": "sparkle", "shortname": ":sparkle:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["green-square", "stars"], + "keywords": [ + "green-square", + "stars" + ], "moji": "❇" }, "sparkler": { @@ -11454,7 +25173,11 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["night", "shine", "stars"], + "keywords": [ + "night", + "shine", + "stars" + ], "moji": "🎇" }, "sparkles": { @@ -11465,7 +25188,12 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["cool", "shine", "shiny", "stars"], + "keywords": [ + "cool", + "shine", + "shiny", + "stars" + ], "moji": "✨" }, "sparkling_heart": { @@ -11476,7 +25204,12 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["affection", "like", "love", "valentines"], + "keywords": [ + "affection", + "like", + "love", + "valentines" + ], "moji": "💖" }, "speak_no_evil": { @@ -11487,7 +25220,19 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "monkey", "monkey", "mouth", "talk", "say", "words", "verbal", "verbalize", "oral", "iwazaru"], + "keywords": [ + "animal", + "monkey", + "monkey", + "mouth", + "talk", + "say", + "words", + "verbal", + "verbalize", + "oral", + "iwazaru" + ], "moji": "🙊" }, "speaker": { @@ -11498,7 +25243,12 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["sound", "listen", "hear", "noise"] + "keywords": [ + "sound", + "listen", + "hear", + "noise" + ] }, "speaking_head": { "unicode": "1F5E3", @@ -11506,9 +25256,13 @@ "name": "speaking head in silhouette", "shortname": ":speaking_head:", "category": "objects_symbols", - "aliases": [":speaking_head_in_silhouette:"], + "aliases": [ + ":speaking_head_in_silhouette:" + ], "aliases_ascii": [], - "keywords": ["talk"] + "keywords": [ + "talk" + ] }, "speech_balloon": { "unicode": "1F4AC", @@ -11518,7 +25272,17 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["bubble", "words", "speech", "balloon", "talk", "conversation", "communication", "comic", "dialogue"], + "keywords": [ + "bubble", + "words", + "speech", + "balloon", + "talk", + "conversation", + "communication", + "comic", + "dialogue" + ], "moji": "💬" }, "speech_left": { @@ -11527,9 +25291,19 @@ "name": "left speech bubble", "shortname": ":speech_left:", "category": "objects_symbols", - "aliases": [":left_speech_bubble:"], + "aliases": [ + ":left_speech_bubble:" + ], "aliases_ascii": [], - "keywords": ["balloon", "words", "talk", "conversation", "communication", "comic", "dialogue"] + "keywords": [ + "balloon", + "words", + "talk", + "conversation", + "communication", + "comic", + "dialogue" + ] }, "speech_right": { "unicode": "1F5E9", @@ -11537,9 +25311,19 @@ "name": "right speech bubble", "shortname": ":speech_right:", "category": "objects_symbols", - "aliases": [":right_speech_bubble:"], + "aliases": [ + ":right_speech_bubble:" + ], "aliases_ascii": [], - "keywords": ["balloon", "words", "talk", "conversation", "communication", "comic", "dialogue"] + "keywords": [ + "balloon", + "words", + "talk", + "conversation", + "communication", + "comic", + "dialogue" + ] }, "speech_three": { "unicode": "1F5EB", @@ -11547,9 +25331,19 @@ "name": "three speech bubbles", "shortname": ":speech_three:", "category": "objects_symbols", - "aliases": [":three_speech_bubbles:"], + "aliases": [ + ":three_speech_bubbles:" + ], "aliases_ascii": [], - "keywords": ["balloon", "words", "talk", "conversation", "communication", "comic", "dialogue"] + "keywords": [ + "balloon", + "words", + "talk", + "conversation", + "communication", + "comic", + "dialogue" + ] }, "speech_two": { "unicode": "1F5EA", @@ -11557,9 +25351,19 @@ "name": "two speech bubbles", "shortname": ":speech_two:", "category": "objects_symbols", - "aliases": [":two_speech_bubbles:"], + "aliases": [ + ":two_speech_bubbles:" + ], "aliases_ascii": [], - "keywords": ["balloon", "words", "talk", "conversation", "communication", "comic", "dialogue"] + "keywords": [ + "balloon", + "words", + "talk", + "conversation", + "communication", + "comic", + "dialogue" + ] }, "speedboat": { "unicode": "1F6A4", @@ -11569,7 +25373,16 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["ship", "transportation", "vehicle", "motor", "speed", "ski", "power", "boat"], + "keywords": [ + "ship", + "transportation", + "vehicle", + "motor", + "speed", + "ski", + "power", + "boat" + ], "moji": "🚤" }, "spider": { @@ -11580,7 +25393,10 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["arachnid", "eight-legged"] + "keywords": [ + "arachnid", + "eight-legged" + ] }, "spider_web": { "unicode": "1F578", @@ -11590,7 +25406,9 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["cobweb"] + "keywords": [ + "cobweb" + ] }, "spy": { "unicode": "1F575", @@ -11598,9 +25416,100 @@ "name": "sleuth or spy", "shortname": ":spy:", "category": "people", - "aliases": [":sleuth_or_spy:"], + "aliases": [ + ":sleuth_or_spy:" + ], "aliases_ascii": [], - "keywords": ["pi", "undercover", "investigator"] + "keywords": [ + "pi", + "undercover", + "investigator" + ] + }, + "spy_tone1": { + "unicode": "1F575-1F3FB", + "unicode_alternates": "", + "name": "sleuth or spy tone 1", + "shortname": ":spy_tone1:", + "category": "people", + "aliases": [ + ":sleuth_or_spy_tone1:" + ], + "aliases_ascii": [], + "keywords": [ + "pi", + "undercover", + "investigator", + "person" + ] + }, + "spy_tone2": { + "unicode": "1F575-1F3FC", + "unicode_alternates": "", + "name": "sleuth or spy tone 2", + "shortname": ":spy_tone2:", + "category": "people", + "aliases": [ + ":sleuth_or_spy_tone2:" + ], + "aliases_ascii": [], + "keywords": [ + "pi", + "undercover", + "investigator", + "person" + ] + }, + "spy_tone3": { + "unicode": "1F575-1F3FD", + "unicode_alternates": "", + "name": "sleuth or spy tone 3", + "shortname": ":spy_tone3:", + "category": "people", + "aliases": [ + ":sleuth_or_spy_tone3:" + ], + "aliases_ascii": [], + "keywords": [ + "pi", + "undercover", + "investigator", + "person" + ] + }, + "spy_tone4": { + "unicode": "1F575-1F3FE", + "unicode_alternates": "", + "name": "sleuth or spy tone 4", + "shortname": ":spy_tone4:", + "category": "people", + "aliases": [ + ":sleuth_or_spy_tone4:" + ], + "aliases_ascii": [], + "keywords": [ + "pi", + "undercover", + "investigator", + "person" + ] + }, + "spy_tone5": { + "unicode": "1F575-1F3FF", + "unicode_alternates": "", + "name": "sleuth or spy tone 5", + "shortname": ":spy_tone5:", + "category": "people", + "aliases": [ + ":sleuth_or_spy_tone5:" + ], + "aliases_ascii": [], + "keywords": [ + "pi", + "undercover", + "investigator", + "person" + ] }, "stadium": { "unicode": "1F3DF", @@ -11610,17 +25519,28 @@ "category": "travel_places", "aliases": [], "aliases_ascii": [], - "keywords": ["sport", "event", "concert", "convention", "game"] + "keywords": [ + "sport", + "event", + "concert", + "convention", + "game" + ] }, "star": { "unicode": "2B50", - "unicode_alternates": ["2B50-FE0F"], + "unicode_alternates": [ + "2B50-FE0F" + ], "name": "white medium star", "shortname": ":star:", "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["night", "yellow"], + "keywords": [ + "night", + "yellow" + ], "moji": "⭐" }, "star2": { @@ -11631,9 +25551,48 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["night", "sparkle", "glow", "glowing", "star", "five", "points", "classic"], + "keywords": [ + "night", + "sparkle", + "glow", + "glowing", + "star", + "five", + "points", + "classic" + ], "moji": "🌟" }, + "star_and_crescent": { + "unicode": "262A", + "unicode_alternates": "", + "name": "star and crescent", + "shortname": ":star_and_crescent:", + "category": "symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "islam", + "muslim", + "religion", + "symbol" + ] + }, + "star_of_david": { + "unicode": "2721", + "unicode_alternates": "", + "name": "star of david", + "shortname": ":star_of_david:", + "category": "symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "jew", + "jewish", + "religion", + "symbol" + ] + }, "stars": { "unicode": "1F320", "unicode_alternates": [], @@ -11642,7 +25601,17 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["night", "photo", "shooting", "shoot", "star", "sky", "night", "comet", "meteoroid"], + "keywords": [ + "night", + "photo", + "shooting", + "shoot", + "star", + "sky", + "night", + "comet", + "meteoroid" + ], "moji": "🌠" }, "station": { @@ -11653,7 +25622,14 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["public", "transportation", "vehicle", "station", "train", "subway"], + "keywords": [ + "public", + "transportation", + "vehicle", + "station", + "train", + "subway" + ], "moji": "🚉" }, "statue_of_liberty": { @@ -11664,7 +25640,10 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["american", "newyork"], + "keywords": [ + "american", + "newyork" + ], "moji": "🗽" }, "steam_locomotive": { @@ -11675,7 +25654,15 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["train", "transportation", "vehicle", "locomotive", "steam", "train", "engine"], + "keywords": [ + "train", + "transportation", + "vehicle", + "locomotive", + "steam", + "train", + "engine" + ], "moji": "🚂" }, "stereo": { @@ -11684,9 +25671,17 @@ "name": "portable stereo", "shortname": ":stereo:", "category": "objects_symbols", - "aliases": [":portable_stereo:"], + "aliases": [ + ":portable_stereo:" + ], "aliases_ascii": [], - "keywords": ["communication", "music", "program", "boom", "box"] + "keywords": [ + "communication", + "music", + "program", + "boom", + "box" + ] }, "stew": { "unicode": "1F372", @@ -11696,7 +25691,16 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "meat", "stew", "hearty", "soup", "thick", "hot", "pot"], + "keywords": [ + "food", + "meat", + "stew", + "hearty", + "soup", + "thick", + "hot", + "pot" + ], "moji": "🍲" }, "stock_chart": { @@ -11707,7 +25711,39 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["graph", "presentation", "stats", "business"] + "keywords": [ + "graph", + "presentation", + "stats", + "business" + ] + }, + "stop_button": { + "unicode": "23F9", + "unicode_alternates": "", + "name": "black square for stop", + "shortname": ":stop_button:", + "category": "symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "sound", + "symbol" + ] + }, + "stopwatch": { + "unicode": "23F1", + "unicode_alternates": "", + "name": "stopwatch", + "shortname": ":stopwatch:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "clock", + "object", + "time" + ] }, "straight_ruler": { "unicode": "1F4CF", @@ -11717,7 +25753,9 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["stationery"], + "keywords": [ + "stationery" + ], "moji": "📏" }, "strawberry": { @@ -11728,7 +25766,15 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "fruit", "nature", "strawberry", "short", "cake", "berry"], + "keywords": [ + "food", + "fruit", + "nature", + "strawberry", + "short", + "cake", + "berry" + ], "moji": "🍓" }, "stuck_out_tongue": { @@ -11738,8 +25784,32 @@ "shortname": ":stuck_out_tongue:", "category": "emoticons", "aliases": [], - "aliases_ascii": [":P", ":-P", "=P", ":-p", ":p", "=p", ":-Þ", ":Þ", ":þ", ":-þ", ":-b", ":b", "d:"], - "keywords": ["childish", "face", "mischievous", "playful", "prank", "tongue", "silly", "playful", "cheeky"], + "aliases_ascii": [ + ":P", + ":-P", + "=P", + ":-p", + ":p", + "=p", + ":-Þ", + ":Þ", + ":þ", + ":-þ", + ":-b", + ":b", + "d:" + ], + "keywords": [ + "childish", + "face", + "mischievous", + "playful", + "prank", + "tongue", + "silly", + "playful", + "cheeky" + ], "moji": "😛" }, "stuck_out_tongue_closed_eyes": { @@ -11750,7 +25820,17 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["face", "mischievous", "playful", "prank", "tongue", "kidding", "silly", "playful", "ecstatic"], + "keywords": [ + "face", + "mischievous", + "playful", + "prank", + "tongue", + "kidding", + "silly", + "playful", + "ecstatic" + ], "moji": "😝" }, "stuck_out_tongue_winking_eye": { @@ -11760,8 +25840,25 @@ "shortname": ":stuck_out_tongue_winking_eye:", "category": "emoticons", "aliases": [], - "aliases_ascii": [">:P", "X-P", "x-p"], - "keywords": ["childish", "face", "mischievous", "playful", "prank", "tongue", "wink", "winking", "kidding", "silly", "playful", "crazy"], + "aliases_ascii": [ + ">:P", + "X-P", + "x-p" + ], + "keywords": [ + "childish", + "face", + "mischievous", + "playful", + "prank", + "tongue", + "wink", + "winking", + "kidding", + "silly", + "playful", + "crazy" + ], "moji": "😜" }, "sun_with_face": { @@ -11772,7 +25869,13 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["morning", "sun", "anthropomorphic", "face", "sky"], + "keywords": [ + "morning", + "sun", + "anthropomorphic", + "face", + "sky" + ], "moji": "🌞" }, "sunflower": { @@ -11783,7 +25886,15 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["nature", "plant", "sunflower", "sun", "flower", "seeds", "yellow"], + "keywords": [ + "nature", + "plant", + "sunflower", + "sun", + "flower", + "seeds", + "yellow" + ], "moji": "🌻" }, "sunglasses": { @@ -11793,19 +25904,41 @@ "shortname": ":sunglasses:", "category": "emoticons", "aliases": [], - "aliases_ascii": ["B-)", "B)", "8)", "8-)", "B-D", "8-D"], - "keywords": ["cool", "face", "smiling", "sunglasses", "sun", "glasses", "sunny", "cool", "smooth"], + "aliases_ascii": [ + "B-)", + "B)", + "8)", + "8-)", + "B-D", + "8-D" + ], + "keywords": [ + "cool", + "face", + "smiling", + "sunglasses", + "sun", + "glasses", + "sunny", + "cool", + "smooth" + ], "moji": "😎" }, "sunny": { "unicode": "2600", - "unicode_alternates": ["2600-FE0F"], + "unicode_alternates": [ + "2600-FE0F" + ], "name": "black sun with rays", "shortname": ":sunny:", "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["brightness", "weather"] + "keywords": [ + "brightness", + "weather" + ] }, "sunrise": { "unicode": "1F305", @@ -11815,7 +25948,17 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["morning", "photo", "vacation", "view", "sunrise", "sun", "morning", "color", "sky"], + "keywords": [ + "morning", + "photo", + "vacation", + "view", + "sunrise", + "sun", + "morning", + "color", + "sky" + ], "moji": "🌅" }, "sunrise_over_mountains": { @@ -11826,7 +25969,18 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["photo", "vacation", "view", "sunrise", "sun", "morning", "mountain", "rural", "color", "sky"], + "keywords": [ + "photo", + "vacation", + "view", + "sunrise", + "sun", + "morning", + "mountain", + "rural", + "color", + "sky" + ], "moji": "🌄" }, "surfer": { @@ -11837,9 +25991,114 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["ocean", "sea", "sports", "surfer", "surf", "wave", "ocean", "ride", "swell"], + "keywords": [ + "ocean", + "sea", + "sports", + "surfer", + "surf", + "wave", + "ocean", + "ride", + "swell" + ], "moji": "🏄" }, + "surfer_tone1": { + "unicode": "1F3C4-1F3FB", + "unicode_alternates": "", + "name": "surfer tone 1", + "shortname": ":surfer_tone1:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "ocean", + "sea", + "sport", + "surf", + "wave", + "ocean", + "ride", + "swell" + ] + }, + "surfer_tone2": { + "unicode": "1F3C4-1F3FC", + "unicode_alternates": "", + "name": "surfer tone 2", + "shortname": ":surfer_tone2:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "ocean", + "sea", + "sport", + "surf", + "wave", + "ocean", + "ride", + "swell" + ] + }, + "surfer_tone3": { + "unicode": "1F3C4-1F3FD", + "unicode_alternates": "", + "name": "surfer tone 3", + "shortname": ":surfer_tone3:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "ocean", + "sea", + "sport", + "surf", + "wave", + "ocean", + "ride", + "swell" + ] + }, + "surfer_tone4": { + "unicode": "1F3C4-1F3FE", + "unicode_alternates": "", + "name": "surfer tone 4", + "shortname": ":surfer_tone4:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "ocean", + "sea", + "sport", + "surf", + "wave", + "ocean", + "ride", + "swell" + ] + }, + "surfer_tone5": { + "unicode": "1F3C4-1F3FF", + "unicode_alternates": "", + "name": "surfer tone 5", + "shortname": ":surfer_tone5:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "ocean", + "sea", + "sport", + "surf", + "wave", + "ocean", + "ride", + "swell" + ] + }, "sushi": { "unicode": "1F363", "unicode_alternates": [], @@ -11848,7 +26107,15 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "japanese", "sushi", "fish", "raw", "nigiri", "japanese"], + "keywords": [ + "food", + "japanese", + "sushi", + "fish", + "raw", + "nigiri", + "japanese" + ], "moji": "🍣" }, "suspension_railway": { @@ -11859,7 +26126,15 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["transportation", "vehicle", "suspension", "railway", "rail", "train", "transportation"], + "keywords": [ + "transportation", + "vehicle", + "suspension", + "railway", + "rail", + "train", + "transportation" + ], "moji": "🚟" }, "sweat": { @@ -11869,8 +26144,22 @@ "shortname": ":sweat:", "category": "emoticons", "aliases": [], - "aliases_ascii": ["':(", "':-(", "'=("], - "keywords": ["cold", "sweat", "sick", "anxious", "worried", "clammy", "diaphoresis", "face", "hot"], + "aliases_ascii": [ + "':(", + "':-(", + "'=(" + ], + "keywords": [ + "cold", + "sweat", + "sick", + "anxious", + "worried", + "clammy", + "diaphoresis", + "face", + "hot" + ], "moji": "😓" }, "sweat_drops": { @@ -11881,7 +26170,9 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["water"], + "keywords": [ + "water" + ], "moji": "💦" }, "sweat_smile": { @@ -11891,8 +26182,23 @@ "shortname": ":sweat_smile:", "category": "emoticons", "aliases": [], - "aliases_ascii": ["':)", "':-)", "'=)", "':D", "':-D", "'=D"], - "keywords": ["face", "happy", "hot", "smiling", "cold", "sweat", "perspiration"], + "aliases_ascii": [ + "':)", + "':-)", + "'=)", + "':D", + "':-D", + "'=D" + ], + "keywords": [ + "face", + "happy", + "hot", + "smiling", + "cold", + "sweat", + "perspiration" + ], "moji": "😅" }, "sweet_potato": { @@ -11903,7 +26209,15 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "nature", "sweet", "potato", "potassium", "roasted", "roast"], + "keywords": [ + "food", + "nature", + "sweet", + "potato", + "potassium", + "roasted", + "roast" + ], "moji": "🍠" }, "swimmer": { @@ -11914,9 +26228,120 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["sports", "swimmer", "swim", "water", "pool", "laps", "freestyle", "butterfly", "breaststroke", "backstroke"], + "keywords": [ + "sports", + "swimmer", + "swim", + "water", + "pool", + "laps", + "freestyle", + "butterfly", + "breaststroke", + "backstroke" + ], "moji": "🏊" }, + "swimmer_tone1": { + "unicode": "1F3CA-1F3FB", + "unicode_alternates": "", + "name": "swimmer tone 1", + "shortname": ":swimmer_tone1:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "sport", + "swim", + "water", + "pool", + "laps", + "freestyle", + "butterfly", + "breaststroke", + "backstroke" + ] + }, + "swimmer_tone2": { + "unicode": "1F3CA-1F3FC", + "unicode_alternates": "", + "name": "swimmer tone 2", + "shortname": ":swimmer_tone2:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "sport", + "swim", + "water", + "pool", + "laps", + "freestyle", + "butterfly", + "breaststroke", + "backstroke" + ] + }, + "swimmer_tone3": { + "unicode": "1F3CA-1F3FD", + "unicode_alternates": "", + "name": "swimmer tone 3", + "shortname": ":swimmer_tone3:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "sport", + "swim", + "water", + "pool", + "laps", + "freestyle", + "butterfly", + "breaststroke", + "backstroke" + ] + }, + "swimmer_tone4": { + "unicode": "1F3CA-1F3FE", + "unicode_alternates": "", + "name": "swimmer tone 4", + "shortname": ":swimmer_tone4:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "sport", + "swim", + "water", + "pool", + "laps", + "freestyle", + "butterfly", + "breaststroke", + "backstroke" + ] + }, + "swimmer_tone5": { + "unicode": "1F3CA-1F3FF", + "unicode_alternates": "", + "name": "swimmer tone 5", + "shortname": ":swimmer_tone5:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "sport", + "swim", + "water", + "pool", + "laps", + "freestyle", + "butterfly", + "breaststroke", + "backstroke" + ] + }, "symbols": { "unicode": "1F523", "unicode_alternates": [], @@ -11925,9 +26350,21 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square"], + "keywords": [ + "blue-square" + ], "moji": "🔣" }, + "synagogue": { + "unicode": "1F54D", + "unicode_alternates": "", + "name": "synagogue", + "shortname": ":synagogue:", + "category": "travel", + "aliases": [], + "aliases_ascii": [], + "keywords": [] + }, "syringe": { "unicode": "1F489", "unicode_alternates": [], @@ -11936,9 +26373,26 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["blood", "drugs", "health", "hospital", "medicine", "needle"], + "keywords": [ + "blood", + "drugs", + "health", + "hospital", + "medicine", + "needle" + ], "moji": "💉" }, + "taco": { + "unicode": "1F32E", + "unicode_alternates": "", + "name": "taco", + "shortname": ":taco:", + "category": "foods", + "aliases": [], + "aliases_ascii": [], + "keywords": [] + }, "tada": { "unicode": "1F389", "unicode_alternates": [], @@ -11947,7 +26401,18 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["contulations", "party", "party", "popper", "tada", "celebration", "victory", "announcement", "climax", "congratulations"], + "keywords": [ + "contulations", + "party", + "party", + "popper", + "tada", + "celebration", + "victory", + "announcement", + "climax", + "congratulations" + ], "moji": "🎉" }, "tanabata_tree": { @@ -11958,7 +26423,16 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["nature", "plant", "tanabata", "tree", "festival", "star", "wish", "holiday"], + "keywords": [ + "nature", + "plant", + "tanabata", + "tree", + "festival", + "star", + "wish", + "holiday" + ], "moji": "🎋" }, "tangerine": { @@ -11969,18 +26443,40 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "fruit", "nature", "tangerine", "citrus", "orange"], + "keywords": [ + "food", + "fruit", + "nature", + "tangerine", + "citrus", + "orange" + ], "moji": "🍊" }, "taurus": { "unicode": "2649", - "unicode_alternates": ["2649-FE0F"], + "unicode_alternates": [ + "2649-FE0F" + ], "name": "taurus", "shortname": ":taurus:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["purple-square", "sign", "taurus", "bull", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "zodiac", "horoscope"], + "keywords": [ + "purple-square", + "sign", + "taurus", + "bull", + "astrology", + "greek", + "constellation", + "stars", + "zodiac", + "sign", + "zodiac", + "horoscope" + ], "moji": "♉" }, "taxi": { @@ -11991,7 +26487,18 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["cars", "transportation", "uber", "vehicle", "taxi", "car", "automobile", "city", "transport", "service"], + "keywords": [ + "cars", + "transportation", + "uber", + "vehicle", + "taxi", + "car", + "automobile", + "city", + "transport", + "service" + ], "moji": "🚕" }, "tea": { @@ -12002,18 +26509,36 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["bowl", "breakfast", "british", "drink", "green", "tea", "leaf", "drink", "teacup", "hot", "beverage"], + "keywords": [ + "bowl", + "breakfast", + "british", + "drink", + "green", + "tea", + "leaf", + "drink", + "teacup", + "hot", + "beverage" + ], "moji": "🍵" }, "telephone": { "unicode": "260E", - "unicode_alternates": ["260E-FE0F"], + "unicode_alternates": [ + "260E-FE0F" + ], "name": "black telephone", "shortname": ":telephone:", "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["communication", "dial", "technology"], + "keywords": [ + "communication", + "dial", + "technology" + ], "moji": "☎" }, "telephone_black": { @@ -12022,9 +26547,15 @@ "name": "black touchtone telephone", "shortname": ":telephone_black:", "category": "objects_symbols", - "aliases": [":black_touchtone_telephone:"], + "aliases": [ + ":black_touchtone_telephone:" + ], "aliases_ascii": [], - "keywords": ["communication", "dial", "technology"] + "keywords": [ + "communication", + "dial", + "technology" + ] }, "telephone_receiver": { "unicode": "1F4DE", @@ -12034,7 +26565,11 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["communication", "dial", "technology"], + "keywords": [ + "communication", + "dial", + "technology" + ], "moji": "📞" }, "telephone_white": { @@ -12043,9 +26578,15 @@ "name": "white touchtone telephone", "shortname": ":telephone_white:", "category": "objects_symbols", - "aliases": [":white_touchtone_telephone:"], + "aliases": [ + ":white_touchtone_telephone:" + ], "aliases_ascii": [], - "keywords": ["communication", "dial", "technology"] + "keywords": [ + "communication", + "dial", + "technology" + ] }, "telescope": { "unicode": "1F52D", @@ -12055,9 +26596,28 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["space", "stars"], + "keywords": [ + "space", + "stars" + ], "moji": "🔭" }, + "ten": { + "unicode": "1F51F", + "unicode_alternates": "", + "name": "keycap ten", + "shortname": ":ten:", + "category": "symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "10", + "blue-square", + "numbers", + "symbol", + "word" + ] + }, "tennis": { "unicode": "1F3BE", "unicode_alternates": [], @@ -12066,18 +26626,36 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["balls", "green", "sports", "tennis", "racket", "racquet", "ball", "game", "net", "court", "love"], + "keywords": [ + "balls", + "green", + "sports", + "tennis", + "racket", + "racquet", + "ball", + "game", + "net", + "court", + "love" + ], "moji": "🎾" }, "tent": { "unicode": "26FA", - "unicode_alternates": ["26FA-FE0F"], + "unicode_alternates": [ + "26FA-FE0F" + ], "name": "tent", "shortname": ":tent:", "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["camp", "outdoors", "photo"], + "keywords": [ + "camp", + "outdoors", + "photo" + ], "moji": "⛺" }, "thermometer": { @@ -12088,7 +26666,33 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["temperature"] + "keywords": [ + "temperature" + ] + }, + "thermometer_face": { + "unicode": "1F912", + "unicode_alternates": "", + "name": "face with thermometer", + "shortname": ":thermometer_face:", + "category": "people", + "aliases": [ + ":face_with_thermometer:" + ], + "aliases_ascii": [], + "keywords": [] + }, + "thinking": { + "unicode": "1F914", + "unicode_alternates": "", + "name": "thinking face", + "shortname": ":thinking:", + "category": "people", + "aliases": [ + ":thinking_face:" + ], + "aliases_ascii": [], + "keywords": [] }, "thought_balloon": { "unicode": "1F4AD", @@ -12098,7 +26702,17 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["bubble", "cloud", "speech", "thought", "balloon", "comic", "think", "day dream", "wonder"], + "keywords": [ + "bubble", + "cloud", + "speech", + "thought", + "balloon", + "comic", + "think", + "day dream", + "wonder" + ], "moji": "💭" }, "thought_left": { @@ -12107,9 +26721,18 @@ "name": "left thought bubble", "shortname": ":thought_left:", "category": "objects_symbols", - "aliases": [":left_thought_bubble:"], + "aliases": [ + ":left_thought_bubble:" + ], "aliases_ascii": [], - "keywords": ["balloon", "cloud", "comic", "think", "day dream", "wonder"] + "keywords": [ + "balloon", + "cloud", + "comic", + "think", + "day dream", + "wonder" + ] }, "thought_right": { "unicode": "1F5ED", @@ -12117,20 +26740,36 @@ "name": "right thought bubble", "shortname": ":thought_right:", "category": "objects_symbols", - "aliases": [":right_thought_bubble:"], + "aliases": [ + ":right_thought_bubble:" + ], "aliases_ascii": [], - "keywords": ["balloon", "cloud", "comic", "think", "day dream", "wonder"] + "keywords": [ + "balloon", + "cloud", + "comic", + "think", + "day dream", + "wonder" + ] }, "three": { "moji": "3️⃣", "unicode": "0033-20E3", - "unicode_alternates": ["0033-FE0F-20E3"], + "unicode_alternates": [ + "0033-FE0F-20E3" + ], "name": "digit three", "shortname": ":three:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["3", "blue-square", "numbers", "prime"] + "keywords": [ + "3", + "blue-square", + "numbers", + "prime" + ] }, "thumbs_down_reverse": { "unicode": "1F593", @@ -12138,9 +26777,15 @@ "name": "reversed thumbs down sign", "shortname": ":thumbs_down_reverse:", "category": "people", - "aliases": [":reversed_thumbs_down_sign:"], + "aliases": [ + ":reversed_thumbs_down_sign:" + ], "aliases_ascii": [], - "keywords": ["hand", "no", "-1"] + "keywords": [ + "hand", + "no", + "-1" + ] }, "thumbs_up_reverse": { "unicode": "1F592", @@ -12148,9 +26793,17 @@ "name": "reversed thumbs up sign", "shortname": ":thumbs_up_reverse:", "category": "people", - "aliases": [":reversed_thumbs_up_sign:"], + "aliases": [ + ":reversed_thumbs_up_sign:" + ], "aliases_ascii": [], - "keywords": ["cool", "hand", "like", "yes", "+1"] + "keywords": [ + "cool", + "hand", + "like", + "yes", + "+1" + ] }, "thumbsdown": { "unicode": "1F44E", @@ -12158,22 +26811,219 @@ "name": "thumbs down sign", "shortname": ":thumbsdown:", "category": "emoticons", - "aliases": [":-1:"], + "aliases": [ + ":-1:" + ], "aliases_ascii": [], - "keywords": ["hand", "no"], + "keywords": [ + "hand", + "no" + ], "moji": "👎" }, + "thumbsdown_tone1": { + "unicode": "1F44E-1F3FB", + "unicode_alternates": "", + "name": "thumbs down sign tone 1", + "shortname": ":thumbsdown_tone1:", + "category": "people", + "aliases": [ + ":-1_tone1:" + ], + "aliases_ascii": [], + "keywords": [ + "hand", + "no", + "-1" + ] + }, + "thumbsdown_tone2": { + "unicode": "1F44E-1F3FC", + "unicode_alternates": "", + "name": "thumbs down sign tone 2", + "shortname": ":thumbsdown_tone2:", + "category": "people", + "aliases": [ + ":-1_tone2:" + ], + "aliases_ascii": [], + "keywords": [ + "hand", + "no", + "-1" + ] + }, + "thumbsdown_tone3": { + "unicode": "1F44E-1F3FD", + "unicode_alternates": "", + "name": "thumbs down sign tone 3", + "shortname": ":thumbsdown_tone3:", + "category": "people", + "aliases": [ + ":-1_tone3:" + ], + "aliases_ascii": [], + "keywords": [ + "hand", + "no", + "-1" + ] + }, + "thumbsdown_tone4": { + "unicode": "1F44E-1F3FE", + "unicode_alternates": "", + "name": "thumbs down sign tone 4", + "shortname": ":thumbsdown_tone4:", + "category": "people", + "aliases": [ + ":-1_tone4:" + ], + "aliases_ascii": [], + "keywords": [ + "hand", + "no", + "-1" + ] + }, + "thumbsdown_tone5": { + "unicode": "1F44E-1F3FF", + "unicode_alternates": "", + "name": "thumbs down sign tone 5", + "shortname": ":thumbsdown_tone5:", + "category": "people", + "aliases": [ + ":-1_tone5:" + ], + "aliases_ascii": [], + "keywords": [ + "hand", + "no", + "-1" + ] + }, "thumbsup": { "unicode": "1F44D", "unicode_alternates": [], "name": "thumbs up sign", "shortname": ":thumbsup:", "category": "emoticons", - "aliases": [":+1:"], + "aliases": [ + ":+1:" + ], "aliases_ascii": [], - "keywords": ["cool", "hand", "like", "yes"], + "keywords": [ + "cool", + "hand", + "like", + "yes" + ], "moji": "👍" }, + "thumbsup_tone1": { + "unicode": "1F44D-1F3FB", + "unicode_alternates": "", + "name": "thumbs up sign tone 1", + "shortname": ":thumbsup_tone1:", + "category": "people", + "aliases": [ + ":+1_tone1:" + ], + "aliases_ascii": [], + "keywords": [ + "cool", + "hand", + "like", + "yes", + "+1" + ] + }, + "thumbsup_tone2": { + "unicode": "1F44D-1F3FC", + "unicode_alternates": "", + "name": "thumbs up sign tone 2", + "shortname": ":thumbsup_tone2:", + "category": "people", + "aliases": [ + ":+1_tone2:" + ], + "aliases_ascii": [], + "keywords": [ + "cool", + "hand", + "like", + "yes", + "+1" + ] + }, + "thumbsup_tone3": { + "unicode": "1F44D-1F3FD", + "unicode_alternates": "", + "name": "thumbs up sign tone 3", + "shortname": ":thumbsup_tone3:", + "category": "people", + "aliases": [ + ":+1_tone3:" + ], + "aliases_ascii": [], + "keywords": [ + "cool", + "hand", + "like", + "yes", + "+1" + ] + }, + "thumbsup_tone4": { + "unicode": "1F44D-1F3FE", + "unicode_alternates": "", + "name": "thumbs up sign tone 4", + "shortname": ":thumbsup_tone4:", + "category": "people", + "aliases": [ + ":+1_tone4:" + ], + "aliases_ascii": [], + "keywords": [ + "cool", + "hand", + "like", + "yes", + "+1" + ] + }, + "thumbsup_tone5": { + "unicode": "1F44D-1F3FF", + "unicode_alternates": "", + "name": "thumbs up sign tone 5", + "shortname": ":thumbsup_tone5:", + "category": "people", + "aliases": [ + ":+1_tone5:" + ], + "aliases_ascii": [], + "keywords": [ + "cool", + "hand", + "like", + "yes", + "+1" + ] + }, + "thunder_cloud_rain": { + "unicode": "26C8", + "unicode_alternates": "", + "name": "thunder cloud and rain", + "shortname": ":thunder_cloud_rain:", + "category": "nature", + "aliases": [ + ":thunder_cloud_and_rain:" + ], + "aliases_ascii": [], + "keywords": [ + "nature", + "weather" + ] + }, "ticket": { "unicode": "1F3AB", "unicode_alternates": [], @@ -12182,7 +27032,18 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["concert", "event", "pass", "ticket", "show", "entertainment", "stub", "admission", "proof", "purchase"], + "keywords": [ + "concert", + "event", + "pass", + "ticket", + "show", + "entertainment", + "stub", + "admission", + "proof", + "purchase" + ], "moji": "🎫" }, "tickets": { @@ -12191,9 +27052,20 @@ "name": "admission tickets", "shortname": ":tickets:", "category": "activity", - "aliases": [":admission_tickets:"], + "aliases": [ + ":admission_tickets:" + ], "aliases_ascii": [], - "keywords": ["concert", "event", "pass", "show", "entertainment", "stub", "proof", "purchase"] + "keywords": [ + "concert", + "event", + "pass", + "show", + "entertainment", + "stub", + "proof", + "purchase" + ] }, "tiger": { "unicode": "1F42F", @@ -12203,7 +27075,9 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal"], + "keywords": [ + "animal" + ], "moji": "🐯" }, "tiger2": { @@ -12214,9 +27088,33 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature", "tiger", "cat", "striped", "tony", "tigger", "hobs"], + "keywords": [ + "animal", + "nature", + "tiger", + "cat", + "striped", + "tony", + "tigger", + "hobs" + ], "moji": "🐅" }, + "timer": { + "unicode": "23F2", + "unicode_alternates": "", + "name": "timer clock", + "shortname": ":timer:", + "category": "objects", + "aliases": [ + ":timer_clock:" + ], + "aliases_ascii": [], + "keywords": [ + "object", + "time" + ] + }, "tired_face": { "unicode": "1F62B", "unicode_alternates": [], @@ -12225,9 +27123,34 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["face", "frustrated", "sick", "upset", "whine", "exhausted", "sleepy", "tired"], + "keywords": [ + "face", + "frustrated", + "sick", + "upset", + "whine", + "exhausted", + "sleepy", + "tired" + ], "moji": "😫" }, + "tm": { + "unicode": "2122", + "unicode_alternates": "2122-fe0f", + "name": "trade mark sign", + "shortname": ":tm:", + "category": "symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "brand", + "trademark", + "symbol", + "tm", + "word" + ] + }, "toilet": { "unicode": "1F6BD", "unicode_alternates": [], @@ -12236,7 +27159,17 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["restroom", "wc", "toilet", "bathroom", "throne", "porcelain", "waste", "flush", "plumbing"], + "keywords": [ + "restroom", + "wc", + "toilet", + "bathroom", + "throne", + "porcelain", + "waste", + "flush", + "plumbing" + ], "moji": "🚽" }, "tokyo_tower": { @@ -12247,7 +27180,10 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["japan", "photo"], + "keywords": [ + "japan", + "photo" + ], "moji": "🗼" }, "tomato": { @@ -12258,9 +27194,68 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "fruit", "nature", "vegetable", "tomato", "fruit", "sauce", "italian"], + "keywords": [ + "food", + "fruit", + "nature", + "vegetable", + "tomato", + "fruit", + "sauce", + "italian" + ], "moji": "🍅" }, + "tone1": { + "unicode": "1F3FB", + "unicode_alternates": "", + "name": "emoji modifier Fitzpatrick type-1-2", + "shortname": ":tone1:", + "category": "modifier", + "aliases": [], + "aliases_ascii": [], + "keywords": [] + }, + "tone2": { + "unicode": "1F3FC", + "unicode_alternates": "", + "name": "emoji modifier Fitzpatrick type-3", + "shortname": ":tone2:", + "category": "modifier", + "aliases": [], + "aliases_ascii": [], + "keywords": [] + }, + "tone3": { + "unicode": "1F3FD", + "unicode_alternates": "", + "name": "emoji modifier Fitzpatrick type-4", + "shortname": ":tone3:", + "category": "modifier", + "aliases": [], + "aliases_ascii": [], + "keywords": [] + }, + "tone4": { + "unicode": "1F3FE", + "unicode_alternates": "", + "name": "emoji modifier Fitzpatrick type-5", + "shortname": ":tone4:", + "category": "modifier", + "aliases": [], + "aliases_ascii": [], + "keywords": [] + }, + "tone5": { + "unicode": "1F3FF", + "unicode_alternates": "", + "name": "emoji modifier Fitzpatrick type-6", + "shortname": ":tone5:", + "category": "modifier", + "aliases": [], + "aliases_ascii": [], + "keywords": [] + }, "tongue": { "unicode": "1F445", "unicode_alternates": [], @@ -12269,7 +27264,25 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["mouth", "playful", "tongue", "mouth", "taste", "buds", "food", "silly", "playful", "tease", "kiss", "french kiss", "lick", "tasty", "playfulness", "silliness", "intimacy"], + "keywords": [ + "mouth", + "playful", + "tongue", + "mouth", + "taste", + "buds", + "food", + "silly", + "playful", + "tease", + "kiss", + "french kiss", + "lick", + "tasty", + "playfulness", + "silliness", + "intimacy" + ], "moji": "👅" }, "tools": { @@ -12278,9 +27291,13 @@ "name": "hammer and wrench", "shortname": ":tools:", "category": "objects_symbols", - "aliases": [":hammer_and_wrench:"], + "aliases": [ + ":hammer_and_wrench:" + ], "aliases_ascii": [], - "keywords": ["tools"] + "keywords": [ + "tools" + ] }, "top": { "unicode": "1F51D", @@ -12290,7 +27307,10 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square", "words"], + "keywords": [ + "blue-square", + "words" + ], "moji": "🔝" }, "tophat": { @@ -12301,9 +27321,63 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["classy", "gentleman", "magic", "top", "hat", "cap", "beaver", "high", "tall", "stove", "pipe", "chimney", "topper", "london", "period piece", "magic", "magician"], + "keywords": [ + "classy", + "gentleman", + "magic", + "top", + "hat", + "cap", + "beaver", + "high", + "tall", + "stove", + "pipe", + "chimney", + "topper", + "london", + "period piece", + "magic", + "magician" + ], "moji": "🎩" }, + "track_next": { + "unicode": "23ED", + "unicode_alternates": "", + "name": "black right-pointing double triangle with vertical bar", + "shortname": ":track_next:", + "category": "symbols", + "aliases": [ + ":next_track:" + ], + "aliases_ascii": [], + "keywords": [ + "arrow", + "next scene", + "next track", + "sound", + "symbol" + ] + }, + "track_previous": { + "unicode": "23EE", + "unicode_alternates": "", + "name": "black left-pointing double triangle with vertical bar", + "shortname": ":track_previous:", + "category": "symbols", + "aliases": [ + ":previous_track:" + ], + "aliases_ascii": [], + "keywords": [ + "arrow", + "previous scene", + "previous track", + "sound", + "symbol" + ] + }, "trackball": { "unicode": "1F5B2", "unicode_alternates": [], @@ -12312,7 +27386,11 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["input", "device", "gadget"] + "keywords": [ + "input", + "device", + "gadget" + ] }, "tractor": { "unicode": "1F69C", @@ -12322,7 +27400,17 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["agriculture", "car", "farming", "vehicle", "tractor", "farm", "construction", "machine", "digger"], + "keywords": [ + "agriculture", + "car", + "farming", + "vehicle", + "tractor", + "farm", + "construction", + "machine", + "digger" + ], "moji": "🚜" }, "traffic_light": { @@ -12333,7 +27421,16 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["traffic", "transportation", "traffic", "light", "stop", "go", "yield", "horizontal"], + "keywords": [ + "traffic", + "transportation", + "traffic", + "light", + "stop", + "go", + "yield", + "horizontal" + ], "moji": "🚥" }, "train": { @@ -12344,7 +27441,10 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["tram", "rail"] + "keywords": [ + "tram", + "rail" + ] }, "train2": { "unicode": "1F686", @@ -12354,7 +27454,13 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["transportation", "vehicle", "train", "locomotive", "rail"], + "keywords": [ + "transportation", + "vehicle", + "train", + "locomotive", + "rail" + ], "moji": "🚆" }, "train_diesel": { @@ -12363,9 +27469,16 @@ "name": "diesel locomotive", "shortname": ":train_diesel:", "category": "travel_places", - "aliases": [":diesel_locomotive:"], + "aliases": [ + ":diesel_locomotive:" + ], "aliases_ascii": [], - "keywords": ["train", "transportation", "engine", "rail"] + "keywords": [ + "train", + "transportation", + "engine", + "rail" + ] }, "tram": { "unicode": "1F68A", @@ -12375,7 +27488,13 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["transportation", "vehicle", "tram", "transportation", "transport"], + "keywords": [ + "transportation", + "vehicle", + "tram", + "transportation", + "transport" + ], "moji": "🚊" }, "triangle_round": { @@ -12384,9 +27503,15 @@ "name": "triangle with rounded corners", "shortname": ":triangle_round:", "category": "objects_symbols", - "aliases": [":triangle_with_rounded_corners:"], + "aliases": [ + ":triangle_with_rounded_corners:" + ], "aliases_ascii": [], - "keywords": ["caution", "warning", "alert"] + "keywords": [ + "caution", + "warning", + "alert" + ] }, "triangular_flag_on_post": { "unicode": "1F6A9", @@ -12396,7 +27521,14 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["triangle", "triangular", "flag", "golf", "post", "flagpole"], + "keywords": [ + "triangle", + "triangular", + "flag", + "golf", + "post", + "flagpole" + ], "moji": "🚩" }, "triangular_ruler": { @@ -12407,7 +27539,12 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["architect", "math", "sketch", "stationery"], + "keywords": [ + "architect", + "math", + "sketch", + "stationery" + ], "moji": "📐" }, "trident": { @@ -12418,7 +27555,10 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["spear", "weapon"], + "keywords": [ + "spear", + "weapon" + ], "moji": "🔱" }, "triumph": { @@ -12429,7 +27569,14 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["face", "gas", "phew", "triumph", "steam", "breath"], + "keywords": [ + "face", + "gas", + "phew", + "triumph", + "steam", + "breath" + ], "moji": "😤" }, "trolleybus": { @@ -12440,7 +27587,16 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["bart", "transportation", "vehicle", "trolley", "bus", "city", "transport", "transportation"], + "keywords": [ + "bart", + "transportation", + "vehicle", + "trolley", + "bus", + "city", + "transport", + "transportation" + ], "moji": "🚎" }, "trophy": { @@ -12451,7 +27607,22 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["award", "ceremony", "contest", "ftw", "place", "win", "trophy", "first", "show", "place", "win", "reward", "achievement", "medal"], + "keywords": [ + "award", + "ceremony", + "contest", + "ftw", + "place", + "win", + "trophy", + "first", + "show", + "place", + "win", + "reward", + "achievement", + "medal" + ], "moji": "🏆" }, "tropical_drink": { @@ -12462,7 +27633,17 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["beverage", "tropical", "drink", "mixed", "pineapple", "coconut", "pina", "fruit", "umbrella"], + "keywords": [ + "beverage", + "tropical", + "drink", + "mixed", + "pineapple", + "coconut", + "pina", + "fruit", + "umbrella" + ], "moji": "🍹" }, "tropical_fish": { @@ -12473,7 +27654,10 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "swim"], + "keywords": [ + "animal", + "swim" + ], "moji": "🐠" }, "truck": { @@ -12484,7 +27668,13 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["cars", "transportation", "truck", "delivery", "package"], + "keywords": [ + "cars", + "transportation", + "truck", + "delivery", + "package" + ], "moji": "🚚" }, "trumpet": { @@ -12495,7 +27685,14 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["brass", "music", "trumpet", "brass", "music", "instrument"], + "keywords": [ + "brass", + "music", + "trumpet", + "brass", + "music", + "instrument" + ], "moji": "🎺" }, "tulip": { @@ -12506,18 +27703,42 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["flowers", "nature", "plant", "tulip", "flower", "bulb", "spring", "easter"], + "keywords": [ + "flowers", + "nature", + "plant", + "tulip", + "flower", + "bulb", + "spring", + "easter" + ], "moji": "🌷" }, + "turkey": { + "unicode": "1F983", + "unicode_alternates": "", + "name": "turkey", + "shortname": ":turkey:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": [] + }, "turned_ok_hand": { "unicode": "1F58F", "unicode_alternates": [], "name": "turned ok hand sign", "shortname": ":turned_ok_hand:", "category": "people", - "aliases": [":turned_ok_hand_sign:"], + "aliases": [ + ":turned_ok_hand_sign:" + ], "aliases_ascii": [], - "keywords": ["perfect", "okay"] + "keywords": [ + "perfect", + "okay" + ] }, "turtle": { "unicode": "1F422", @@ -12527,9 +27748,39 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "slow", "turtle", "shell", "tortoise", "chelonian", "reptile", "slow", "snap", "steady"], + "keywords": [ + "animal", + "slow", + "turtle", + "shell", + "tortoise", + "chelonian", + "reptile", + "slow", + "snap", + "steady" + ], "moji": "🐢" }, + "tv": { + "unicode": "1F4FA", + "unicode_alternates": "", + "name": "television", + "shortname": ":tv:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "oldschool", + "program", + "show", + "technology", + "tv", + "entertainment", + "object", + "video" + ] + }, "twisted_rightwards_arrows": { "unicode": "1F500", "unicode_alternates": [], @@ -12538,19 +27789,28 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square"], + "keywords": [ + "blue-square" + ], "moji": "🔀" }, "two": { "moji": "2️⃣", "unicode": "0032-20E3", - "unicode_alternates": ["0032-FE0F-20E3"], + "unicode_alternates": [ + "0032-FE0F-20E3" + ], "name": "digit two", "shortname": ":two:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["2", "blue-square", "numbers", "prime"] + "keywords": [ + "2", + "blue-square", + "numbers", + "prime" + ] }, "two_hearts": { "unicode": "1F495", @@ -12560,7 +27820,17 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["affection", "like", "love", "valentines", "heart", "hearts", "two", "love", "emotion"], + "keywords": [ + "affection", + "like", + "love", + "valentines", + "heart", + "hearts", + "two", + "love", + "emotion" + ], "moji": "💕" }, "two_men_holding_hands": { @@ -12571,7 +27841,21 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["bromance", "couple", "friends", "like", "love", "men", "gay", "homosexual", "friends", "hands", "holding", "team", "unity"], + "keywords": [ + "bromance", + "couple", + "friends", + "like", + "love", + "men", + "gay", + "homosexual", + "friends", + "hands", + "holding", + "team", + "unity" + ], "moji": "👬" }, "two_women_holding_hands": { @@ -12582,7 +27866,24 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["couple", "female", "friends", "like", "love", "women", "hands", "girlfriends", "friends", "sisters", "mother", "daughter", "gay", "homosexual", "couple", "unity"], + "keywords": [ + "couple", + "female", + "friends", + "like", + "love", + "women", + "hands", + "girlfriends", + "friends", + "sisters", + "mother", + "daughter", + "gay", + "homosexual", + "couple", + "unity" + ], "moji": "👭" }, "u5272": { @@ -12593,7 +27894,13 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["chinese", "cut", "divide", "kanji", "pink"], + "keywords": [ + "chinese", + "cut", + "divide", + "kanji", + "pink" + ], "moji": "🈹" }, "u5408": { @@ -12604,7 +27911,12 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["chinese", "japanese", "join", "kanji"], + "keywords": [ + "chinese", + "japanese", + "join", + "kanji" + ], "moji": "🈴" }, "u55b6": { @@ -12615,18 +27927,28 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["japanese", "opening hours"], + "keywords": [ + "japanese", + "opening hours" + ], "moji": "🈺" }, "u6307": { "unicode": "1F22F", - "unicode_alternates": ["1F22F-FE0F"], + "unicode_alternates": [ + "1F22F-FE0F" + ], "name": "squared cjk unified ideograph-6307", "shortname": ":u6307:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["chinese", "green-square", "kanji", "point"], + "keywords": [ + "chinese", + "green-square", + "kanji", + "point" + ], "moji": "🈯" }, "u6708": { @@ -12637,7 +27959,13 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["chinese", "japanese", "kanji", "moon", "orange-square"], + "keywords": [ + "chinese", + "japanese", + "kanji", + "moon", + "orange-square" + ], "moji": "🈷" }, "u6709": { @@ -12648,7 +27976,12 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["chinese", "have", "kanji", "orange-square"], + "keywords": [ + "chinese", + "have", + "kanji", + "orange-square" + ], "moji": "🈶" }, "u6e80": { @@ -12659,18 +27992,33 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["chinese", "full", "japanese", "kanji", "red-square"], + "keywords": [ + "chinese", + "full", + "japanese", + "kanji", + "red-square" + ], "moji": "🈵" }, "u7121": { "unicode": "1F21A", - "unicode_alternates": ["1F21A-FE0F"], + "unicode_alternates": [ + "1F21A-FE0F" + ], "name": "squared cjk unified ideograph-7121", "shortname": ":u7121:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["chinese", "japanese", "kanji", "no", "nothing", "orange-square"], + "keywords": [ + "chinese", + "japanese", + "kanji", + "no", + "nothing", + "orange-square" + ], "moji": "🈚" }, "u7533": { @@ -12681,7 +28029,11 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["chinese", "japanese", "kanji"], + "keywords": [ + "chinese", + "japanese", + "kanji" + ], "moji": "🈸" }, "u7981": { @@ -12692,7 +28044,14 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["chinese", "forbidden", "japanese", "kanji", "limit", "restricted"], + "keywords": [ + "chinese", + "forbidden", + "japanese", + "kanji", + "limit", + "restricted" + ], "moji": "🈲" }, "u7a7a": { @@ -12703,20 +28062,45 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["chinese", "empty", "japanese", "kanji"], + "keywords": [ + "chinese", + "empty", + "japanese", + "kanji" + ], "moji": "🈳" }, "umbrella": { "unicode": "2614", - "unicode_alternates": ["2614-FE0F"], + "unicode_alternates": [ + "2614-FE0F" + ], "name": "umbrella with rain drops", "shortname": ":umbrella:", "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["rain", "weather"], + "keywords": [ + "rain", + "weather" + ], "moji": "☔" }, + "umbrella2": { + "unicode": "2602", + "unicode_alternates": "", + "name": "umbrella", + "shortname": ":umbrella2:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "clothing", + "nature", + "rain", + "weather" + ] + }, "unamused": { "unicode": "1F612", "unicode_alternates": [], @@ -12725,7 +28109,19 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["bored", "face", "indifference", "serious", "straight face", "unamused", "not amused", "depressed", "unhappy", "disapprove", "lame"], + "keywords": [ + "bored", + "face", + "indifference", + "serious", + "straight face", + "unamused", + "not amused", + "depressed", + "unhappy", + "disapprove", + "lame" + ], "moji": "😒" }, "underage": { @@ -12736,9 +28132,26 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["18", "drink", "night", "pub"], + "keywords": [ + "18", + "drink", + "night", + "pub" + ], "moji": "🔞" }, + "unicorn": { + "unicode": "1F984", + "unicode_alternates": "", + "name": "unicorn face", + "shortname": ":unicorn:", + "category": "nature", + "aliases": [ + ":unicorn_face:" + ], + "aliases_ascii": [], + "keywords": [] + }, "unlock": { "unicode": "1F513", "unicode_alternates": [], @@ -12747,7 +28160,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["privacy", "security"], + "keywords": [ + "privacy", + "security" + ], "moji": "🔓" }, "up": { @@ -12758,20 +28174,138 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square"], + "keywords": [ + "blue-square" + ], "moji": "🆙" }, + "upside_down": { + "unicode": "1F643", + "unicode_alternates": "", + "name": "upside-down face", + "shortname": ":upside_down:", + "category": "people", + "aliases": [ + ":upside_down_face:" + ], + "aliases_ascii": [], + "keywords": [] + }, + "urn": { + "unicode": "26B1", + "unicode_alternates": "", + "name": "funeral urn", + "shortname": ":urn:", + "category": "objects", + "aliases": [ + ":funeral_urn:" + ], + "aliases_ascii": [], + "keywords": [ + "death", + "object" + ] + }, "v": { "unicode": "270C", - "unicode_alternates": ["270C-FE0F"], + "unicode_alternates": [ + "270C-FE0F" + ], "name": "victory hand", "shortname": ":v:", "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["fingers", "hand", "ohyeah", "peace", "two", "victory"], + "keywords": [ + "fingers", + "hand", + "ohyeah", + "peace", + "two", + "victory" + ], "moji": "✌" }, + "v_tone1": { + "unicode": "270C-1F3FB", + "unicode_alternates": "", + "name": "victory hand tone 1", + "shortname": ":v_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "fingers", + "ohyeah", + "peace", + "two", + "v" + ] + }, + "v_tone2": { + "unicode": "270C-1F3FC", + "unicode_alternates": "", + "name": "victory hand tone 2", + "shortname": ":v_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "fingers", + "ohyeah", + "peace", + "two", + "v" + ] + }, + "v_tone3": { + "unicode": "270C-1F3FD", + "unicode_alternates": "", + "name": "victory hand tone 3", + "shortname": ":v_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "fingers", + "ohyeah", + "peace", + "two", + "v" + ] + }, + "v_tone4": { + "unicode": "270C-1F3FE", + "unicode_alternates": "", + "name": "victory hand tone 4", + "shortname": ":v_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "fingers", + "ohyeah", + "peace", + "two", + "v" + ] + }, + "v_tone5": { + "unicode": "270C-1F3FF", + "unicode_alternates": "", + "name": "victory hand tone 5", + "shortname": ":v_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "fingers", + "ohyeah", + "peace", + "two", + "v" + ] + }, "vertical_traffic_light": { "unicode": "1F6A6", "unicode_alternates": [], @@ -12780,7 +28314,15 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["transportation", "traffic", "light", "stop", "go", "yield", "vertical"], + "keywords": [ + "transportation", + "traffic", + "light", + "stop", + "go", + "yield", + "vertical" + ], "moji": "🚦" }, "vhs": { @@ -12791,7 +28333,11 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["oldschool", "record", "video"], + "keywords": [ + "oldschool", + "record", + "video" + ], "moji": "📼" }, "vibration_mode": { @@ -12802,7 +28348,10 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["orange-square", "phone"], + "keywords": [ + "orange-square", + "phone" + ], "moji": "📳" }, "video_camera": { @@ -12813,7 +28362,10 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["film", "record"], + "keywords": [ + "film", + "record" + ], "moji": "📹" }, "video_game": { @@ -12824,7 +28376,19 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["PS4", "console", "controller", "play", "video", "game", "console", "controller", "nintendo", "xbox", "playstation"], + "keywords": [ + "PS4", + "console", + "controller", + "play", + "video", + "game", + "console", + "controller", + "nintendo", + "xbox", + "playstation" + ], "moji": "🎮" }, "violin": { @@ -12835,18 +28399,39 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["instrument", "music", "violin", "fiddle", "music", "instrument"], + "keywords": [ + "instrument", + "music", + "violin", + "fiddle", + "music", + "instrument" + ], "moji": "🎻" }, "virgo": { "unicode": "264D", - "unicode_alternates": ["264D-FE0F"], + "unicode_alternates": [ + "264D-FE0F" + ], "name": "virgo", "shortname": ":virgo:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["sign", "virgo", "maiden", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "zodiac", "horoscope"], + "keywords": [ + "sign", + "virgo", + "maiden", + "astrology", + "greek", + "constellation", + "stars", + "zodiac", + "sign", + "zodiac", + "horoscope" + ], "moji": "♍" }, "volcano": { @@ -12857,9 +28442,27 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["nature", "photo", "volcano", "lava", "magma", "hot", "explode"], + "keywords": [ + "nature", + "photo", + "volcano", + "lava", + "magma", + "hot", + "explode" + ], "moji": "🌋" }, + "volleyball": { + "unicode": "1F3D0", + "unicode_alternates": "", + "name": "volleyball", + "shortname": ":volleyball:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": [] + }, "vs": { "unicode": "1F19A", "unicode_alternates": [], @@ -12868,7 +28471,10 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["orange-square", "words"], + "keywords": [ + "orange-square", + "words" + ], "moji": "🆚" }, "vulcan": { @@ -12877,9 +28483,113 @@ "name": "raised hand with part between middle and ring fingers", "shortname": ":vulcan:", "category": "people", - "aliases": [":raised_hand_with_part_between_middle_and_ring_fingers:"], + "aliases": [ + ":raised_hand_with_part_between_middle_and_ring_fingers:" + ], "aliases_ascii": [], - "keywords": ["vulcan", "spock", "leonard", "nimoy", "star trek", "live long"] + "keywords": [ + "vulcan", + "spock", + "leonard", + "nimoy", + "star trek", + "live long" + ] + }, + "vulcan_tone1": { + "unicode": "1F596-1F3FB", + "unicode_alternates": "", + "name": "raised hand with part between middle and ring fingers tone 1", + "shortname": ":vulcan_tone1:", + "category": "people", + "aliases": [ + ":raised_hand_with_part_between_middle_and_ring_fingers_tone1:" + ], + "aliases_ascii": [], + "keywords": [ + "vulcan", + "spock", + "leonard", + "nimoy", + "star trek", + "live long" + ] + }, + "vulcan_tone2": { + "unicode": "1F596-1F3FC", + "unicode_alternates": "", + "name": "raised hand with part between middle and ring fingers tone 2", + "shortname": ":vulcan_tone2:", + "category": "people", + "aliases": [ + ":raised_hand_with_part_between_middle_and_ring_fingers_tone2:" + ], + "aliases_ascii": [], + "keywords": [ + "vulcan", + "spock", + "leonard", + "nimoy", + "star trek", + "live long" + ] + }, + "vulcan_tone3": { + "unicode": "1F596-1F3FD", + "unicode_alternates": "", + "name": "raised hand with part between middle and ring fingers tone 3", + "shortname": ":vulcan_tone3:", + "category": "people", + "aliases": [ + ":raised_hand_with_part_between_middle_and_ring_fingers_tone3:" + ], + "aliases_ascii": [], + "keywords": [ + "vulcan", + "spock", + "leonard", + "nimoy", + "star trek", + "live long" + ] + }, + "vulcan_tone4": { + "unicode": "1F596-1F3FE", + "unicode_alternates": "", + "name": "raised hand with part between middle and ring fingers tone 4", + "shortname": ":vulcan_tone4:", + "category": "people", + "aliases": [ + ":raised_hand_with_part_between_middle_and_ring_fingers_tone4:" + ], + "aliases_ascii": [], + "keywords": [ + "vulcan", + "spock", + "leonard", + "nimoy", + "star trek", + "live long" + ] + }, + "vulcan_tone5": { + "unicode": "1F596-1F3FF", + "unicode_alternates": "", + "name": "raised hand with part between middle and ring fingers tone 5", + "shortname": ":vulcan_tone5:", + "category": "people", + "aliases": [ + ":raised_hand_with_part_between_middle_and_ring_fingers_tone5:" + ], + "aliases_ascii": [], + "keywords": [ + "vulcan", + "spock", + "leonard", + "nimoy", + "star trek", + "live long" + ] }, "walking": { "unicode": "1F6B6", @@ -12889,9 +28599,103 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["human", "man", "walk", "pedestrian", "stroll", "stride", "foot", "feet"], + "keywords": [ + "human", + "man", + "walk", + "pedestrian", + "stroll", + "stride", + "foot", + "feet" + ], "moji": "🚶" }, + "walking_tone1": { + "unicode": "1F6B6-1F3FB", + "unicode_alternates": "", + "name": "pedestrian tone 1", + "shortname": ":walking_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "man", + "walk", + "stroll", + "stride", + "hiking", + "hike" + ] + }, + "walking_tone2": { + "unicode": "1F6B6-1F3FC", + "unicode_alternates": "", + "name": "pedestrian tone 2", + "shortname": ":walking_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "man", + "walk", + "stroll", + "stride", + "hiking", + "hike" + ] + }, + "walking_tone3": { + "unicode": "1F6B6-1F3FD", + "unicode_alternates": "", + "name": "pedestrian tone 3", + "shortname": ":walking_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "man", + "walk", + "stroll", + "stride", + "hiking", + "hike" + ] + }, + "walking_tone4": { + "unicode": "1F6B6-1F3FE", + "unicode_alternates": "", + "name": "pedestrian tone 4", + "shortname": ":walking_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "man", + "walk", + "stroll", + "stride", + "hiking", + "hike" + ] + }, + "walking_tone5": { + "unicode": "1F6B6-1F3FF", + "unicode_alternates": "", + "name": "pedestrian tone 5", + "shortname": ":walking_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "man", + "walk", + "stroll", + "stride", + "hiking", + "hike" + ] + }, "waning_crescent_moon": { "unicode": "1F318", "unicode_alternates": [], @@ -12900,7 +28704,16 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["nature", "moon", "crescent", "waning", "sky", "night", "cheese", "phase"], + "keywords": [ + "nature", + "moon", + "crescent", + "waning", + "sky", + "night", + "cheese", + "phase" + ], "moji": "🌘" }, "waning_gibbous_moon": { @@ -12911,18 +28724,32 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["nature", "moon", "waning", "gibbous", "sky", "night", "cheese", "phase"], + "keywords": [ + "nature", + "moon", + "waning", + "gibbous", + "sky", + "night", + "cheese", + "phase" + ], "moji": "🌖" }, "warning": { "unicode": "26A0", - "unicode_alternates": ["26A0-FE0F"], + "unicode_alternates": [ + "26A0-FE0F" + ], "name": "warning sign", "shortname": ":warning:", "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["exclamation", "wip"], + "keywords": [ + "exclamation", + "wip" + ], "moji": "⚠" }, "wastebasket": { @@ -12933,17 +28760,26 @@ "category": "objects_symbols", "aliases": [], "aliases_ascii": [], - "keywords": ["trash", "garbage", "dispose"] + "keywords": [ + "trash", + "garbage", + "dispose" + ] }, "watch": { "unicode": "231A", - "unicode_alternates": ["231A-FE0F"], + "unicode_alternates": [ + "231A-FE0F" + ], "name": "watch", "shortname": ":watch:", "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["accessories", "time"], + "keywords": [ + "accessories", + "time" + ], "moji": "⌚" }, "water_buffalo": { @@ -12954,7 +28790,18 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "cow", "nature", "ox", "water", "buffalo", "asia", "bovine", "milk", "dairy"], + "keywords": [ + "animal", + "cow", + "nature", + "ox", + "water", + "buffalo", + "asia", + "bovine", + "milk", + "dairy" + ], "moji": "🐃" }, "watermelon": { @@ -12965,7 +28812,15 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["food", "fruit", "melon", "watermelon", "summer", "fruit", "large"], + "keywords": [ + "food", + "fruit", + "melon", + "watermelon", + "summer", + "fruit", + "large" + ], "moji": "🍉" }, "wave": { @@ -12976,9 +28831,100 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["farewell", "gesture", "goodbye", "hands", "solong"], + "keywords": [ + "farewell", + "gesture", + "goodbye", + "hands", + "solong" + ], "moji": "👋" }, + "wave_tone1": { + "unicode": "1F44B-1F3FB", + "unicode_alternates": "", + "name": "waving hand sign tone 1", + "shortname": ":wave_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "farewell", + "gesture", + "goodbye", + "solong", + "hi", + "wave" + ] + }, + "wave_tone2": { + "unicode": "1F44B-1F3FC", + "unicode_alternates": "", + "name": "waving hand sign tone 2", + "shortname": ":wave_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "farewell", + "gesture", + "goodbye", + "solong", + "hi", + "wave" + ] + }, + "wave_tone3": { + "unicode": "1F44B-1F3FD", + "unicode_alternates": "", + "name": "waving hand sign tone 3", + "shortname": ":wave_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "farewell", + "gesture", + "goodbye", + "solong", + "hi", + "wave" + ] + }, + "wave_tone4": { + "unicode": "1F44B-1F3FE", + "unicode_alternates": "", + "name": "waving hand sign tone 4", + "shortname": ":wave_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "farewell", + "gesture", + "goodbye", + "solong", + "hi", + "wave" + ] + }, + "wave_tone5": { + "unicode": "1F44B-1F3FF", + "unicode_alternates": "", + "name": "waving hand sign tone 5", + "shortname": ":wave_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "farewell", + "gesture", + "goodbye", + "solong", + "hi", + "wave" + ] + }, "wavy_dash": { "unicode": "3030", "unicode_alternates": [], @@ -12987,7 +28933,10 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["draw", "line"], + "keywords": [ + "draw", + "line" + ], "moji": "〰" }, "waxing_crescent_moon": { @@ -12998,7 +28947,15 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["nature", "moon", "waxing", "sky", "night", "cheese", "phase"], + "keywords": [ + "nature", + "moon", + "waxing", + "sky", + "night", + "cheese", + "phase" + ], "moji": "🌒" }, "waxing_gibbous_moon": { @@ -13009,7 +28966,9 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["nature"], + "keywords": [ + "nature" + ], "moji": "🌔" }, "wc": { @@ -13020,7 +28979,20 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square", "restroom", "toilet", "water", "closet", "toilet", "bathroom", "throne", "porcelain", "waste", "flush", "plumbing"], + "keywords": [ + "blue-square", + "restroom", + "toilet", + "water", + "closet", + "toilet", + "bathroom", + "throne", + "porcelain", + "waste", + "flush", + "plumbing" + ], "moji": "🚾" }, "weary": { @@ -13031,7 +29003,21 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["face", "frustrated", "sad", "sleepy", "tired", "weary", "sleepy", "tired", "tiredness", "study", "finals", "school", "exhausted"], + "keywords": [ + "face", + "frustrated", + "sad", + "sleepy", + "tired", + "weary", + "sleepy", + "tired", + "tiredness", + "study", + "finals", + "school", + "exhausted" + ], "moji": "😩" }, "wedding": { @@ -13042,7 +29028,15 @@ "category": "places", "aliases": [], "aliases_ascii": [], - "keywords": ["affection", "bride", "couple", "groom", "like", "love", "marriage"], + "keywords": [ + "affection", + "bride", + "couple", + "groom", + "like", + "love", + "marriage" + ], "moji": "💒" }, "whale": { @@ -13053,7 +29047,12 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature", "ocean", "sea"], + "keywords": [ + "animal", + "nature", + "ocean", + "sea" + ], "moji": "🐳" }, "whale2": { @@ -13064,18 +29063,48 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature", "ocean", "sea", "whale", "blubber", "bloated", "fat", "large", "massive"], + "keywords": [ + "animal", + "nature", + "ocean", + "sea", + "whale", + "blubber", + "bloated", + "fat", + "large", + "massive" + ], "moji": "🐋" }, + "wheel_of_dharma": { + "unicode": "2638", + "unicode_alternates": "", + "name": "wheel of dharma", + "shortname": ":wheel_of_dharma:", + "category": "symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "buddhist", + "religion", + "symbol" + ] + }, "wheelchair": { "unicode": "267F", - "unicode_alternates": ["267F-FE0F"], + "unicode_alternates": [ + "267F-FE0F" + ], "name": "wheelchair symbol", "shortname": ":wheelchair:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square", "disabled"], + "keywords": [ + "blue-square", + "disabled" + ], "moji": "♿" }, "white_check_mark": { @@ -13086,18 +29115,26 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["agree", "green-square", "ok"], + "keywords": [ + "agree", + "green-square", + "ok" + ], "moji": "✅" }, "white_circle": { "unicode": "26AA", - "unicode_alternates": ["26AA-FE0F"], + "unicode_alternates": [ + "26AA-FE0F" + ], "name": "medium white circle", "shortname": ":white_circle:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["shape"], + "keywords": [ + "shape" + ], "moji": "⚪" }, "white_flower": { @@ -13108,51 +29145,81 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["japanese", "white", "flower", "teacher", "school", "grade", "score", "brilliance", "intelligence", "homework", "student", "assignment", "praise"], + "keywords": [ + "japanese", + "white", + "flower", + "teacher", + "school", + "grade", + "score", + "brilliance", + "intelligence", + "homework", + "student", + "assignment", + "praise" + ], "moji": "💮" }, "white_large_square": { "unicode": "2B1C", - "unicode_alternates": ["2B1C-FE0F"], + "unicode_alternates": [ + "2B1C-FE0F" + ], "name": "white large square", "shortname": ":white_large_square:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["shape"], + "keywords": [ + "shape" + ], "moji": "⬜" }, "white_medium_small_square": { "unicode": "25FD", - "unicode_alternates": ["25FD-FE0F"], + "unicode_alternates": [ + "25FD-FE0F" + ], "name": "white medium small square", "shortname": ":white_medium_small_square:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["shape"], + "keywords": [ + "shape" + ], "moji": "◽" }, "white_medium_square": { "unicode": "25FB", - "unicode_alternates": ["25FB-FE0F"], + "unicode_alternates": [ + "25FB-FE0F" + ], "name": "white medium square", "shortname": ":white_medium_square:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["shape"], + "keywords": [ + "shape" + ], "moji": "◻" }, "white_small_square": { "unicode": "25AB", - "unicode_alternates": ["25AB-FE0F"], + "unicode_alternates": [ + "25AB-FE0F" + ], "name": "white small square", "shortname": ":white_small_square:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["shape"], + "keywords": [ + "shape" + ], "moji": "▫" }, "white_square_button": { @@ -13163,9 +29230,56 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["shape"], + "keywords": [ + "shape" + ], "moji": "🔳" }, + "white_sun_cloud": { + "unicode": "1F325", + "unicode_alternates": "", + "name": "white sun behind cloud", + "shortname": ":white_sun_cloud:", + "category": "nature", + "aliases": [ + ":white_sun_behind_cloud:" + ], + "aliases_ascii": [], + "keywords": [ + "nature", + "weather" + ] + }, + "white_sun_rain_cloud": { + "unicode": "1F326", + "unicode_alternates": "", + "name": "white sun behind cloud with rain", + "shortname": ":white_sun_rain_cloud:", + "category": "nature", + "aliases": [ + ":white_sun_behind_cloud_with_rain:" + ], + "aliases_ascii": [], + "keywords": [ + "nature", + "weather" + ] + }, + "white_sun_small_cloud": { + "unicode": "1F324", + "unicode_alternates": "", + "name": "white sun with small cloud", + "shortname": ":white_sun_small_cloud:", + "category": "nature", + "aliases": [ + ":white_sun_with_small_cloud:" + ], + "aliases_ascii": [], + "keywords": [ + "nature", + "weather" + ] + }, "wind_blowing_face": { "unicode": "1F32C", "unicode_alternates": [], @@ -13174,7 +29288,10 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["mother", "nature"] + "keywords": [ + "mother", + "nature" + ] }, "wind_chime": { "unicode": "1F390", @@ -13184,7 +29301,21 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["ding", "nature", "wind", "chime", "bell", "fūrin", "instrument", "music", "spirits", "soothing", "protective", "spiritual", "sound"], + "keywords": [ + "ding", + "nature", + "wind", + "chime", + "bell", + "fūrin", + "instrument", + "music", + "spirits", + "soothing", + "protective", + "spiritual", + "sound" + ], "moji": "🎐" }, "wine_glass": { @@ -13195,7 +29326,20 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["alcohol", "beverage", "booze", "bottle", "drink", "drunk", "fermented", "glass", "grapes", "tasting", "wine", "winery"], + "keywords": [ + "alcohol", + "beverage", + "booze", + "bottle", + "drink", + "drunk", + "fermented", + "glass", + "grapes", + "tasting", + "wine", + "winery" + ], "moji": "🍷" }, "wink": { @@ -13205,8 +29349,26 @@ "shortname": ":wink:", "category": "emoticons", "aliases": [], - "aliases_ascii": [";)", ";-)", "*-)", "*)", ";-]", ";]", ";D", ";^)"], - "keywords": ["face", "happy", "mischievous", "secret", "wink", "winking", "friendly", "joke"], + "aliases_ascii": [ + ";)", + ";-)", + "*-)", + "*)", + ";-]", + ";]", + ";D", + ";^)" + ], + "keywords": [ + "face", + "happy", + "mischievous", + "secret", + "wink", + "winking", + "friendly", + "joke" + ], "moji": "😉" }, "wolf": { @@ -13217,7 +29379,10 @@ "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["animal", "nature"], + "keywords": [ + "animal", + "nature" + ], "moji": "🐺" }, "woman": { @@ -13228,9 +29393,82 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["female", "girls"], + "keywords": [ + "female", + "girls" + ], "moji": "👩" }, + "woman_tone1": { + "unicode": "1F469-1F3FB", + "unicode_alternates": "", + "name": "woman tone 1", + "shortname": ":woman_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "lady" + ] + }, + "woman_tone2": { + "unicode": "1F469-1F3FC", + "unicode_alternates": "", + "name": "woman tone 2", + "shortname": ":woman_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "lady" + ] + }, + "woman_tone3": { + "unicode": "1F469-1F3FD", + "unicode_alternates": "", + "name": "woman tone 3", + "shortname": ":woman_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "lady" + ] + }, + "woman_tone4": { + "unicode": "1F469-1F3FE", + "unicode_alternates": "", + "name": "woman tone 4", + "shortname": ":woman_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "lady" + ] + }, + "woman_tone5": { + "unicode": "1F469-1F3FF", + "unicode_alternates": "", + "name": "woman tone 5", + "shortname": ":woman_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "female", + "girl", + "lady" + ] + }, "womans_clothes": { "unicode": "1F45A", "unicode_alternates": [], @@ -13239,7 +29477,21 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["fashion", "woman", "clothing", "clothes", "blouse", "shirt", "wardrobe", "breasts", "cleavage", "shopping", "shop", "dressing", "dressed"], + "keywords": [ + "fashion", + "woman", + "clothing", + "clothes", + "blouse", + "shirt", + "wardrobe", + "breasts", + "cleavage", + "shopping", + "shop", + "dressing", + "dressed" + ], "moji": "👚" }, "womans_hat": { @@ -13250,7 +29502,11 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["accessories", "fashion", "female"], + "keywords": [ + "accessories", + "fashion", + "female" + ], "moji": "👒" }, "womens": { @@ -13261,7 +29517,16 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["purple-square", "woman", "bathroom", "restroom", "sign", "girl", "female", "avatar"], + "keywords": [ + "purple-square", + "woman", + "bathroom", + "restroom", + "sign", + "girl", + "female", + "avatar" + ], "moji": "🚺" }, "worried": { @@ -13272,7 +29537,16 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["concern", "face", "nervous", "worried", "anxious", "distressed", "nervous", "tense"], + "keywords": [ + "concern", + "face", + "nervous", + "worried", + "anxious", + "distressed", + "nervous", + "tense" + ], "moji": "😟" }, "wrench": { @@ -13283,7 +29557,11 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["diy", "ikea", "tools"], + "keywords": [ + "diy", + "ikea", + "tools" + ], "moji": "🔧" }, "writing_hand": { @@ -13292,9 +29570,91 @@ "name": "left writing hand", "shortname": ":writing_hand:", "category": "people", - "aliases": [":left_writing_hand:"], + "aliases": [ + ":left_writing_hand:" + ], "aliases_ascii": [], - "keywords": ["write", "sign", "signature", "draw"] + "keywords": [ + "write", + "sign", + "signature", + "draw" + ] + }, + "writing_hand_tone1": { + "unicode": "270D-1F3FB", + "unicode_alternates": "", + "name": "writing hand tone 1", + "shortname": ":writing_hand_tone1:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "write", + "sign", + "signature", + "draw" + ] + }, + "writing_hand_tone2": { + "unicode": "270D-1F3FC", + "unicode_alternates": "", + "name": "writing hand tone 2", + "shortname": ":writing_hand_tone2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "write", + "sign", + "signature", + "draw" + ] + }, + "writing_hand_tone3": { + "unicode": "270D-1F3FD", + "unicode_alternates": "", + "name": "writing hand tone 3", + "shortname": ":writing_hand_tone3:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "write", + "sign", + "signature", + "draw" + ] + }, + "writing_hand_tone4": { + "unicode": "270D-1F3FE", + "unicode_alternates": "", + "name": "writing hand tone 4", + "shortname": ":writing_hand_tone4:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "write", + "sign", + "signature", + "draw" + ] + }, + "writing_hand_tone5": { + "unicode": "270D-1F3FF", + "unicode_alternates": "", + "name": "writing hand tone 5", + "shortname": ":writing_hand_tone5:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "write", + "sign", + "signature", + "draw" + ] }, "x": { "unicode": "274C", @@ -13304,7 +29664,11 @@ "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["delete", "no", "remove"], + "keywords": [ + "delete", + "no", + "remove" + ], "moji": "❌" }, "yellow_heart": { @@ -13315,7 +29679,25 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["affection", "like", "love", "valentines", "yellow", "gold", "heart", "love", "friendship", "happy", "happiness", "trust", "compassionate", "respectful", "honest", "caring", "selfless"], + "keywords": [ + "affection", + "like", + "love", + "valentines", + "yellow", + "gold", + "heart", + "love", + "friendship", + "happy", + "happiness", + "trust", + "compassionate", + "respectful", + "honest", + "caring", + "selfless" + ], "moji": "💛" }, "yen": { @@ -13326,9 +29708,39 @@ "category": "objects", "aliases": [], "aliases_ascii": [], - "keywords": ["currency", "dollar", "japanese", "money", "yen", "japan", "japanese", "banknote", "money", "currency", "paper", "cash", "bill"], + "keywords": [ + "currency", + "dollar", + "japanese", + "money", + "yen", + "japan", + "japanese", + "banknote", + "money", + "currency", + "paper", + "cash", + "bill" + ], "moji": "💴" }, + "yin_yang": { + "unicode": "262F", + "unicode_alternates": "", + "name": "yin yang", + "shortname": ":yin_yang:", + "category": "symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": [ + "religion", + "sign", + "symbol", + "tao", + "taoist" + ] + }, "yum": { "unicode": "1F60B", "unicode_alternates": [], @@ -13337,30 +29749,68 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["face", "happy", "joy", "smile", "tongue", "delicious", "savoring", "food", "eat", "yummy", "yum", "tasty", "savory"], + "keywords": [ + "face", + "happy", + "joy", + "smile", + "tongue", + "delicious", + "savoring", + "food", + "eat", + "yummy", + "yum", + "tasty", + "savory" + ], "moji": "😋" }, "zap": { "unicode": "26A1", - "unicode_alternates": ["26A1-FE0F"], + "unicode_alternates": [ + "26A1-FE0F" + ], "name": "high voltage sign", "shortname": ":zap:", "category": "nature", "aliases": [], "aliases_ascii": [], - "keywords": ["lightning bolt", "thunder", "weather"], + "keywords": [ + "lightning bolt", + "thunder", + "weather" + ], "moji": "⚡" }, "zero": { "moji": "0️⃣", "unicode": "0030-20E3", - "unicode_alternates": ["0030-FE0F-20E3"], + "unicode_alternates": [ + "0030-FE0F-20E3" + ], "name": "digit zero", "shortname": ":zero:", "category": "other", "aliases": [], "aliases_ascii": [], - "keywords": ["blue-square", "null", "numbers"] + "keywords": [ + "blue-square", + "null", + "numbers" + ] + }, + "zipper_mouth": { + "unicode": "1F910", + "unicode_alternates": "", + "name": "zipper-mouth face", + "shortname": ":zipper_mouth:", + "category": "people", + "aliases": [ + ":zipper_mouth_face:" + ], + "aliases_ascii": [], + "keywords": [] }, "zzz": { "unicode": "1F4A4", @@ -13370,7 +29820,10 @@ "category": "emoticons", "aliases": [], "aliases_ascii": [], - "keywords": ["sleepy", "tired"], + "keywords": [ + "sleepy", + "tired" + ], "moji": "💤" } } diff --git a/lib/api/api.rb b/lib/api/api.rb index 7efe0a0262..7d65145176 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -56,5 +56,6 @@ module API mount Triggers mount Builds mount Variables + mount Runners end end diff --git a/lib/api/builds.rb b/lib/api/builds.rb index d293f98816..2b104f90aa 100644 --- a/lib/api/builds.rb +++ b/lib/api/builds.rb @@ -13,11 +13,12 @@ module API # Example Request: # GET /projects/:id/builds get ':id/builds' do + builds = user_project.builds.order('id DESC') builds = filter_builds(builds, params[:scope]) present paginate(builds), with: Entities::Build, - user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) + user_can_download_artifacts: can?(current_user, :read_build, user_project) end # Get builds for a specific commit of a project @@ -30,6 +31,8 @@ module API # Example Request: # GET /projects/:id/repository/commits/:sha/builds get ':id/repository/commits/:sha/builds' do + authorize_read_builds! + commit = user_project.ci_commits.find_by_sha(params[:sha]) return not_found! unless commit @@ -37,7 +40,7 @@ module API builds = filter_builds(builds, params[:scope]) present paginate(builds), with: Entities::Build, - user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) + user_can_download_artifacts: can?(current_user, :read_build, user_project) end # Get a specific build of a project @@ -48,11 +51,37 @@ module API # Example Request: # GET /projects/:id/builds/:build_id get ':id/builds/:build_id' do + authorize_read_builds! + build = get_build(params[:build_id]) return not_found!(build) unless build present build, with: Entities::Build, - user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) + user_can_download_artifacts: can?(current_user, :read_build, user_project) + end + + # Download the artifacts file from build + # + # Parameters: + # id (required) - The ID of a build + # token (required) - The build authorization token + # Example Request: + # GET /projects/:id/builds/:build_id/artifacts + get ':id/builds/:build_id/artifacts' do + authorize_read_builds! + + build = get_build(params[:build_id]) + return not_found!(build) unless build + + artifacts_file = build.artifacts_file + + unless artifacts_file.file_storage? + return redirect_to build.artifacts_file.url + end + + return not_found! unless artifacts_file.exists? + + present_file!(artifacts_file.path, artifacts_file.filename) end # Get a trace of a specific build of a project @@ -67,6 +96,8 @@ module API # is saved in the DB instead of file). But before that, we need to consider how to replace the value of # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse. get ':id/builds/:build_id/trace' do + authorize_read_builds! + build = get_build(params[:build_id]) return not_found!(build) unless build @@ -86,7 +117,7 @@ module API # example request: # post /projects/:id/build/:build_id/cancel post ':id/builds/:build_id/cancel' do - authorize_manage_builds! + authorize_update_builds! build = get_build(params[:build_id]) return not_found!(build) unless build @@ -94,7 +125,7 @@ module API build.cancel present build, with: Entities::Build, - user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) + user_can_download_artifacts: can?(current_user, :read_build, user_project) end # Retry a specific build of a project @@ -105,13 +136,33 @@ module API # example request: # post /projects/:id/build/:build_id/retry post ':id/builds/:build_id/retry' do - authorize_manage_builds! + authorize_update_builds! build = get_build(params[:build_id]) - return forbidden!('Build is not retryable') unless build && build.retryable? + return not_found!(build) unless build + return forbidden!('Build is not retryable') unless build.retryable? build = Ci::Build.retry(build) + present build, with: Entities::Build, + user_can_download_artifacts: can?(current_user, :read_build, user_project) + end + + # Erase build (remove artifacts and build trace) + # + # Parameters: + # id (required) - the id of a project + # build_id (required) - the id of a build + # example Request: + # post /projects/:id/build/:build_id/erase + post ':id/builds/:build_id/erase' do + authorize_update_builds! + + build = get_build(params[:build_id]) + return not_found!(build) unless build + return forbidden!('Build is not erasable!') unless build.erasable? + + build.erase(erased_by: current_user) present build, with: Entities::Build, user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) end @@ -141,8 +192,12 @@ module API builds.where(status: available_statuses && scope) end - def authorize_manage_builds! - authorize! :manage_builds, user_project + def authorize_read_builds! + authorize! :read_build, user_project + end + + def authorize_update_builds! + authorize! :update_build, user_project end end end diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 1162271f5f..9422d438d2 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -18,7 +18,7 @@ module API # Examples: # GET /projects/:id/repository/commits/:sha/statuses get ':id/repository/commits/:sha/statuses' do - authorize! :read_commit_statuses, user_project + authorize! :read_commit_status, user_project sha = params[:sha] ci_commit = user_project.ci_commit(sha) not_found! 'Commit' unless ci_commit diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 82a75734de..a3b5f1eb8d 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -49,7 +49,7 @@ module API expose :enable_ssl_verification end - class ForkedFromProject < Grape::Entity + class BasicProjectDetails < Grape::Entity expose :id expose :name, :name_with_namespace expose :path, :path_with_namespace @@ -67,11 +67,12 @@ module API expose :shared_runners_enabled expose :creator_id expose :namespace - expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ |project, options| project.forked? } + expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? } expose :avatar_url expose :star_count, :forks_count expose :open_issues_count, if: lambda { |project, options| project.issues_enabled? && project.default_issues_tracker? } expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } + expose :public_builds end class ProjectMember < UserBasic @@ -175,6 +176,7 @@ module API expose :work_in_progress?, as: :work_in_progress expose :milestone, using: Entities::Milestone expose :merge_when_build_succeeds + expose :merge_status end class MergeRequestChanges < MergeRequest @@ -375,6 +377,24 @@ module API expose :name end + class RunnerDetails < Runner + expose :tag_list + expose :version, :revision, :platform, :architecture + expose :contacted_at + expose :token, if: lambda { |runner, options| options[:current_user].is_admin? || !runner.is_shared? } + expose :projects, with: Entities::BasicProjectDetails do |runner, options| + if options[:current_user].is_admin? + runner.projects + else + options[:current_user].authorized_projects.where(id: runner.projects) + end + end + end + + class BuildArtifactFile < Grape::Entity + expose :filename, :size + end + class Build < Grape::Entity expose :id, :status, :stage, :name, :ref, :tag, :coverage expose :created_at, :started_at, :finished_at @@ -383,9 +403,10 @@ module API # for downloading of artifacts (see: https://gitlab.com/gitlab-org/gitlab-ce/issues/4255) expose :download_url do |repo_obj, options| if options[:user_can_download_artifacts] - repo_obj.download_url + repo_obj.artifacts_download_url end end + expose :artifacts_file, using: BuildArtifactFile, if: -> (build, opts) { build.artifacts? } expose :commit, with: RepoCommit do |repo_obj, _options| if repo_obj.respond_to?(:commit) repo_obj.commit.commit_data diff --git a/lib/api/files.rb b/lib/api/files.rb index 8ad2c1883c..c1d86f313b 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -58,9 +58,11 @@ module API commit = user_project.commit(ref) not_found! 'Commit' unless commit - blob = user_project.repository.blob_at(commit.sha, file_path) + repo = user_project.repository + blob = repo.blob_at(commit.sha, file_path) if blob + blob.load_all_data!(repo) status(200) { @@ -72,7 +74,7 @@ module API ref: ref, blob_id: blob.id, commit_id: commit.id, - last_commit_id: user_project.repository.last_commit_for_path(commit.sha, file_path).id + last_commit_id: repo.last_commit_for_path(commit.sha, file_path).id } else not_found! 'File' diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index c4f58c7911..a72044e805 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -30,7 +30,7 @@ module API end def sudo_identifier() - identifier ||= params[SUDO_PARAM] ||= env[SUDO_HEADER] + identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER] # Regex for integers if !!(identifier =~ /^[0-9]+$/) @@ -265,6 +265,10 @@ module API projects = projects.search(params[:search]) end + if params[:visibility].present? + projects = projects.search_by_visibility(params[:visibility]) + end + projects.reorder(project_order_by => project_sort) end @@ -340,12 +344,22 @@ module API def pagination_links(paginated_data) request_url = request.url.split('?').first + request_params = params.clone + request_params[:per_page] = paginated_data.limit_value links = [] - links << %(<#{request_url}?page=#{paginated_data.current_page - 1}&per_page=#{paginated_data.limit_value}>; rel="prev") unless paginated_data.first_page? - links << %(<#{request_url}?page=#{paginated_data.current_page + 1}&per_page=#{paginated_data.limit_value}>; rel="next") unless paginated_data.last_page? - links << %(<#{request_url}?page=1&per_page=#{paginated_data.limit_value}>; rel="first") - links << %(<#{request_url}?page=#{paginated_data.total_pages}&per_page=#{paginated_data.limit_value}>; rel="last") + + request_params[:page] = paginated_data.current_page - 1 + links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") unless paginated_data.first_page? + + request_params[:page] = paginated_data.current_page + 1 + links << %(<#{request_url}?#{request_params.to_query}>; rel="next") unless paginated_data.last_page? + + request_params[:page] = 1 + links << %(<#{request_url}?#{request_params.to_query}>; rel="first") + + request_params[:page] = paginated_data.total_pages + links << %(<#{request_url}?#{request_params.to_query}>; rel="last") links.join(', ') end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 6e7a767207..252744515d 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -3,6 +3,8 @@ module API class Issues < Grape::API before { authenticate! } + helpers ::Gitlab::AkismetHelper + helpers do def filter_issues_state(issues, state) case state @@ -19,6 +21,17 @@ module API def filter_issues_milestone(issues, milestone) issues.includes(:milestone).where('milestones.title' => milestone) end + + def create_spam_log(project, current_user, attrs) + params = attrs.merge({ + source_ip: env['REMOTE_ADDR'], + user_agent: env['HTTP_USER_AGENT'], + noteable_type: 'Issue', + via_api: true + }) + + ::CreateSpamLogService.new(project, current_user, params).execute + end end resource :issues do @@ -114,7 +127,15 @@ module API render_api_error!({ labels: errors }, 400) end - issue = ::Issues::CreateService.new(user_project, current_user, attrs).execute + project = user_project + text = [attrs[:title], attrs[:description]].reject(&:blank?).join("\n") + + if check_for_spam?(project, current_user) && is_spam?(env, current_user, text) + create_spam_log(project, current_user, attrs) + render_api_error!({ error: 'Spam detected' }, 400) + end + + issue = ::Issues::CreateService.new(project, current_user, attrs).execute if issue.valid? # Find or create labels and attach to issue. Labels are valid because diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 5c97fe1c88..c5e5d57ed4 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -59,55 +59,6 @@ module API present paginate(merge_requests), with: Entities::MergeRequest end - # Show MR - # - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - The ID of MR - # - # Example: - # GET /projects/:id/merge_request/:merge_request_id - # - get ":id/merge_request/:merge_request_id" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) - - authorize! :read_merge_request, merge_request - - present merge_request, with: Entities::MergeRequest - end - - # Show MR commits - # - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - The ID of MR - # - # Example: - # GET /projects/:id/merge_request/:merge_request_id/commits - # - get ':id/merge_request/:merge_request_id/commits' do - merge_request = user_project.merge_requests. - find(params[:merge_request_id]) - authorize! :read_merge_request, merge_request - present merge_request.commits, with: Entities::RepoCommit - end - - # Show MR changes - # - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - The ID of MR - # - # Example: - # GET /projects/:id/merge_request/:merge_request_id/changes - # - get ':id/merge_request/:merge_request_id/changes' do - merge_request = user_project.merge_requests. - find(params[:merge_request_id]) - authorize! :read_merge_request, merge_request - present merge_request, with: Entities::MergeRequestChanges - end - # Create MR # # Parameters: @@ -120,6 +71,7 @@ module API # title (required) - Title of MR # description - Description of MR # labels (optional) - Labels for MR as a comma-separated list + # milestone_id (optional) - Milestone ID # # Example: # POST /projects/:id/merge_requests @@ -127,7 +79,7 @@ module API post ":id/merge_requests" do authorize! :create_merge_request, user_project required_attributes! [:source_branch, :target_branch, :title] - attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :target_project_id, :description] + attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :target_project_id, :description, :milestone_id] # Validate label names in advance if (errors = validate_label_params(params)).any? @@ -148,146 +100,220 @@ module API end end - # Update MR + # Routing "merge_request/:merge_request_id/..." is DEPRECATED and WILL BE REMOVED in version 9.0 + # Use "merge_requests/:merge_request_id/..." instead. # - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - ID of MR - # target_branch - The target branch - # assignee_id - Assignee user ID - # title - Title of MR - # state_event - Status of MR. (close|reopen|merge) - # description - Description of MR - # labels (optional) - Labels for a MR as a comma-separated list - # Example: - # PUT /projects/:id/merge_request/:merge_request_id - # - put ":id/merge_request/:merge_request_id" do - attrs = attributes_for_keys [:target_branch, :assignee_id, :title, :state_event, :description] - merge_request = user_project.merge_requests.find(params[:merge_request_id]) - authorize! :update_merge_request, merge_request + [":id/merge_request/:merge_request_id", ":id/merge_requests/:merge_request_id"].each do |path| + # Show MR + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - The ID of MR + # + # Example: + # GET /projects/:id/merge_requests/:merge_request_id + # + get path do + merge_request = user_project.merge_requests.find(params[:merge_request_id]) - # Ensure source_branch is not specified - if params[:source_branch].present? - render_api_error!('Source branch cannot be changed', 400) + authorize! :read_merge_request, merge_request + + present merge_request, with: Entities::MergeRequest end - # Validate label names in advance - if (errors = validate_label_params(params)).any? - render_api_error!({ labels: errors }, 400) + # Show MR commits + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - The ID of MR + # + # Example: + # GET /projects/:id/merge_requests/:merge_request_id/commits + # + get "#{path}/commits" do + merge_request = user_project.merge_requests. + find(params[:merge_request_id]) + authorize! :read_merge_request, merge_request + present merge_request.commits, with: Entities::RepoCommit end - merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, attrs).execute(merge_request) + # Show MR changes + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - The ID of MR + # + # Example: + # GET /projects/:id/merge_requests/:merge_request_id/changes + # + get "#{path}/changes" do + merge_request = user_project.merge_requests. + find(params[:merge_request_id]) + authorize! :read_merge_request, merge_request + present merge_request, with: Entities::MergeRequestChanges + end - if merge_request.valid? - # Find or create labels and attach to issue - unless params[:labels].nil? - merge_request.remove_labels - merge_request.add_labels_by_names(params[:labels].split(",")) + # Update MR + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - ID of MR + # target_branch - The target branch + # assignee_id - Assignee user ID + # title - Title of MR + # state_event - Status of MR. (close|reopen|merge) + # description - Description of MR + # labels (optional) - Labels for a MR as a comma-separated list + # milestone_id (optional) - Milestone ID + # Example: + # PUT /projects/:id/merge_requests/:merge_request_id + # + put path do + attrs = attributes_for_keys [:target_branch, :assignee_id, :title, :state_event, :description, :milestone_id] + merge_request = user_project.merge_requests.find(params[:merge_request_id]) + authorize! :update_merge_request, merge_request + + # Ensure source_branch is not specified + if params[:source_branch].present? + render_api_error!('Source branch cannot be changed', 400) + end + + # Validate label names in advance + if (errors = validate_label_params(params)).any? + render_api_error!({ labels: errors }, 400) + end + + merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, attrs).execute(merge_request) + + if merge_request.valid? + # Find or create labels and attach to issue + unless params[:labels].nil? + merge_request.remove_labels + merge_request.add_labels_by_names(params[:labels].split(",")) + end + + present merge_request, with: Entities::MergeRequest + else + handle_merge_request_errors! merge_request.errors + end + end + + # Merge MR + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - ID of MR + # merge_commit_message (optional) - Custom merge commit message + # should_remove_source_branch (optional) - When true, the source branch will be deleted if possible + # merge_when_build_succeeds (optional) - When true, this MR will be merged when the build succeeds + # Example: + # PUT /projects/:id/merge_requests/:merge_request_id/merge + # + put "#{path}/merge" do + merge_request = user_project.merge_requests.find(params[:merge_request_id]) + + # Merge request can not be merged + # because user dont have permissions to push into target branch + unauthorized! unless merge_request.can_be_merged_by?(current_user) + not_allowed! if !merge_request.open? || merge_request.work_in_progress? + + merge_request.check_if_can_be_merged + + render_api_error!('Branch cannot be merged', 406) unless merge_request.can_be_merged? + + merge_params = { + commit_message: params[:merge_commit_message], + should_remove_source_branch: params[:should_remove_source_branch] + } + + if parse_boolean(params[:merge_when_build_succeeds]) && merge_request.ci_commit && merge_request.ci_commit.active? + ::MergeRequests::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user, merge_params). + execute(merge_request) + else + ::MergeRequests::MergeService.new(merge_request.target_project, current_user, merge_params). + execute(merge_request) end present merge_request, with: Entities::MergeRequest - else - handle_merge_request_errors! merge_request.errors - end - end - - # Merge MR - # - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - ID of MR - # merge_commit_message (optional) - Custom merge commit message - # should_remove_source_branch (optional) - When true, the source branch will be deleted if possible - # merge_when_build_succeeds (optional) - When true, this MR will be merged when the build succeeds - # Example: - # PUT /projects/:id/merge_request/:merge_request_id/merge - # - put ":id/merge_request/:merge_request_id/merge" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) - - # Merge request can not be merged - # because user dont have permissions to push into target branch - unauthorized! unless merge_request.can_be_merged_by?(current_user) - not_allowed! if !merge_request.open? || merge_request.work_in_progress? - - merge_request.check_if_can_be_merged - - render_api_error!('Branch cannot be merged', 406) unless merge_request.can_be_merged? - - merge_params = { - commit_message: params[:merge_commit_message], - should_remove_source_branch: params[:should_remove_source_branch] - } - - if parse_boolean(params[:merge_when_build_succeeds]) && merge_request.ci_commit && merge_request.ci_commit.active? - ::MergeRequests::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user, merge_params). - execute(merge_request) - else - ::MergeRequests::MergeService.new(merge_request.target_project, current_user, merge_params). - execute(merge_request) end - present merge_request, with: Entities::MergeRequest - end + # Cancel Merge if Merge When build succeeds is enabled + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - ID of MR + # + post "#{path}/cancel_merge_when_build_succeeds" do + merge_request = user_project.merge_requests.find(params[:merge_request_id]) - # Cancel Merge if Merge When build succeeds is enabled - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - ID of MR - # - post ":id/merge_request/:merge_request_id/cancel_merge_when_build_succeeds" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) + unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user) - unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user) + ::MergeRequest::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user).cancel(merge_request) + end - ::MergeRequest::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user).cancel(merge_request) - end + # Duplicate. DEPRECATED and WILL BE REMOVED in 9.0. + # Use GET "/projects/:id/merge_requests/:merge_request_id/notes" instead + # + # Get a merge request's comments + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - ID of MR + # Examples: + # GET /projects/:id/merge_requests/:merge_request_id/comments + # + get "#{path}/comments" do + merge_request = user_project.merge_requests.find(params[:merge_request_id]) - # Get a merge request's comments - # - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - ID of MR - # Examples: - # GET /projects/:id/merge_request/:merge_request_id/comments - # - get ":id/merge_request/:merge_request_id/comments" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) + authorize! :read_merge_request, merge_request - authorize! :read_merge_request, merge_request + present paginate(merge_request.notes.fresh), with: Entities::MRNote + end - present paginate(merge_request.notes.fresh), with: Entities::MRNote - end + # Duplicate. DEPRECATED and WILL BE REMOVED in 9.0. + # Use POST "/projects/:id/merge_requests/:merge_request_id/notes" instead + # + # Post comment to merge request + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - ID of MR + # note (required) - Text of comment + # Examples: + # POST /projects/:id/merge_requests/:merge_request_id/comments + # + post "#{path}/comments" do + required_attributes! [:note] - # Post comment to merge request - # - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - ID of MR - # note (required) - Text of comment - # Examples: - # POST /projects/:id/merge_request/:merge_request_id/comments - # - post ":id/merge_request/:merge_request_id/comments" do - required_attributes! [:note] + merge_request = user_project.merge_requests.find(params[:merge_request_id]) - merge_request = user_project.merge_requests.find(params[:merge_request_id]) + authorize! :create_note, merge_request - authorize! :create_note, merge_request + opts = { + note: params[:note], + noteable_type: 'MergeRequest', + noteable_id: merge_request.id + } - opts = { - note: params[:note], - noteable_type: 'MergeRequest', - noteable_id: merge_request.id - } + note = ::Notes::CreateService.new(user_project, current_user, opts).execute - note = ::Notes::CreateService.new(user_project, current_user, opts).execute + if note.save + present note, with: Entities::MRNote + else + render_api_error!("Failed to save note #{note.errors.messages}", 400) + end + end - if note.save - present note, with: Entities::MRNote - else - render_api_error!("Failed to save note #{note.errors.messages}", 400) + # List issues that will close on merge + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - ID of MR + # Examples: + # GET /projects/:id/merge_requests/:merge_request_id/closes_issues + get "#{path}/closes_issues" do + merge_request = user_project.merge_requests.find(params[:merge_request_id]) + issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) + present paginate(issues), with: Entities::Issue end end end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 71bb342f84..6067c8b4a5 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -99,6 +99,7 @@ module API # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) - 0 by default # import_url (optional) + # public_builds (optional) # Example Request # POST /projects post do @@ -115,7 +116,8 @@ module API :namespace_id, :public, :visibility_level, - :import_url] + :import_url, + :public_builds] attrs = map_public_to_visibility_level(attrs) @project = ::Projects::CreateService.new(current_user, attrs).execute if @project.saved? @@ -145,6 +147,7 @@ module API # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) # import_url (optional) + # public_builds (optional) # Example Request # POST /projects/user/:user_id post "user/:user_id" do @@ -161,7 +164,8 @@ module API :shared_runners_enabled, :public, :visibility_level, - :import_url] + :import_url, + :public_builds] attrs = map_public_to_visibility_level(attrs) @project = ::Projects::CreateService.new(user, attrs).execute if @project.saved? @@ -187,7 +191,7 @@ module API else present @forked_project, with: Entities::Project, user_can_admin_project: can?(current_user, :admin_project, @forked_project) - end + end end # Update an existing project @@ -205,6 +209,7 @@ module API # shared_runners_enabled (optional) # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) - visibility level of a project + # public_builds (optional) # Example Request # PUT /projects/:id put ':id' do @@ -219,7 +224,8 @@ module API :snippets_enabled, :shared_runners_enabled, :public, - :visibility_level] + :visibility_level, + :public_builds] attrs = map_public_to_visibility_level(attrs) authorize_admin_project authorize! :rename_project, user_project if attrs[:name].present? @@ -246,7 +252,7 @@ module API # DELETE /projects/:id delete ":id" do authorize! :remove_project, user_project - ::Projects::DestroyService.new(user_project, current_user, {}).execute + ::Projects::DestroyService.new(user_project, current_user, {}).pending_delete! end # Mark this project as forked from another diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index d7c48639eb..c95d2d2001 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -57,7 +57,7 @@ module API not_found! "File" unless blob content_type 'text/plain' - present blob.data + header *Gitlab::Workhorse.send_git_blob(repo, blob) end # Get a raw blob contents by blob sha @@ -83,7 +83,7 @@ module API env['api.format'] = :txt content_type blob.mime_type - present blob.data + header *Gitlab::Workhorse.send_git_blob(repo, blob) end # Get a an archive of the repository diff --git a/lib/api/runners.rb b/lib/api/runners.rb new file mode 100644 index 0000000000..8ec91485b2 --- /dev/null +++ b/lib/api/runners.rb @@ -0,0 +1,175 @@ +module API + # Runners API + class Runners < Grape::API + before { authenticate! } + + resource :runners do + # Get runners available for user + # + # Example Request: + # GET /runners + get do + runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: ['specific', 'shared']) + present paginate(runners), with: Entities::Runner + end + + # Get all runners - shared and specific + # + # Example Request: + # GET /runners/all + get 'all' do + authenticated_as_admin! + runners = filter_runners(Ci::Runner.all, params[:scope]) + present paginate(runners), with: Entities::Runner + end + + # Get runner's details + # + # Parameters: + # id (required) - The ID of ther runner + # Example Request: + # GET /runners/:id + get ':id' do + runner = get_runner(params[:id]) + authenticate_show_runner!(runner) + + present runner, with: Entities::RunnerDetails, current_user: current_user + end + + # Update runner's details + # + # Parameters: + # id (required) - The ID of ther runner + # description (optional) - Runner's description + # active (optional) - Runner's status + # tag_list (optional) - Array of tags for runner + # Example Request: + # PUT /runners/:id + put ':id' do + runner = get_runner(params[:id]) + authenticate_update_runner!(runner) + + attrs = attributes_for_keys [:description, :active, :tag_list] + if runner.update(attrs) + present runner, with: Entities::RunnerDetails, current_user: current_user + else + render_validation_error!(runner) + end + end + + # Remove runner + # + # Parameters: + # id (required) - The ID of ther runner + # Example Request: + # DELETE /runners/:id + delete ':id' do + runner = get_runner(params[:id]) + authenticate_delete_runner!(runner) + runner.destroy! + + present runner, with: Entities::Runner + end + end + + resource :projects do + before { authorize_admin_project } + + # Get runners available for project + # + # Example Request: + # GET /projects/:id/runners + get ':id/runners' do + runners = filter_runners(Ci::Runner.owned_or_shared(user_project.id), params[:scope]) + present paginate(runners), with: Entities::Runner + end + + # Enable runner for project + # + # Parameters: + # id (required) - The ID of the project + # runner_id (required) - The ID of the runner + # Example Request: + # POST /projects/:id/runners/:runner_id + post ':id/runners' do + required_attributes! [:runner_id] + + runner = get_runner(params[:runner_id]) + authenticate_enable_runner!(runner) + Ci::RunnerProject.create(runner: runner, project: user_project) + + present runner, with: Entities::Runner + end + + # Disable project's runner + # + # Parameters: + # id (required) - The ID of the project + # runner_id (required) - The ID of the runner + # Example Request: + # DELETE /projects/:id/runners/:runner_id + delete ':id/runners/:runner_id' do + runner_project = user_project.runner_projects.find_by(runner_id: params[:runner_id]) + not_found!('Runner') unless runner_project + + runner = runner_project.runner + forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1 + + runner_project.destroy + + present runner, with: Entities::Runner + end + end + + helpers do + def filter_runners(runners, scope, options = {}) + return runners unless scope.present? + + available_scopes = ::Ci::Runner::AVAILABLE_SCOPES + if options[:without] + available_scopes = available_scopes - options[:without] + end + + if (available_scopes & [scope]).empty? + render_api_error!('Scope contains invalid value', 400) + end + + runners.send(scope) + end + + def get_runner(id) + runner = Ci::Runner.find(id) + not_found!('Runner') unless runner + runner + end + + def authenticate_show_runner!(runner) + return if runner.is_shared || current_user.is_admin? + forbidden!("No access granted") unless user_can_access_runner?(runner) + end + + def authenticate_update_runner!(runner) + return if current_user.is_admin? + forbidden!("Runner is shared") if runner.is_shared? + forbidden!("No access granted") unless user_can_access_runner?(runner) + end + + def authenticate_delete_runner!(runner) + return if current_user.is_admin? + forbidden!("Runner is shared") if runner.is_shared? + forbidden!("Runner associated with more than one project") if runner.projects.count > 1 + forbidden!("No access granted") unless user_can_access_runner?(runner) + end + + def authenticate_enable_runner!(runner) + forbidden!("Runner is shared") if runner.is_shared? + return if current_user.is_admin? + forbidden!("No access granted") unless user_can_access_runner?(runner) + end + + def user_can_access_runner?(runner) + current_user.ci_authorized_runners.exists?(runner.id) + end + end + end +end diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index 5e4964f446..d1d07394e9 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -54,7 +54,7 @@ module API # GET /projects/:id/triggers get ':id/triggers' do authenticate! - authorize_admin_project + authorize! :admin_build, user_project triggers = user_project.triggers.includes(:trigger_requests) triggers = paginate(triggers) @@ -71,7 +71,7 @@ module API # GET /projects/:id/triggers/:token get ':id/triggers/:token' do authenticate! - authorize_admin_project + authorize! :admin_build, user_project trigger = user_project.triggers.find_by(token: params[:token].to_s) return not_found!('Trigger') unless trigger @@ -87,7 +87,7 @@ module API # POST /projects/:id/triggers post ':id/triggers' do authenticate! - authorize_admin_project + authorize! :admin_build, user_project trigger = user_project.triggers.create @@ -103,7 +103,7 @@ module API # DELETE /projects/:id/triggers/:token delete ':id/triggers/:token' do authenticate! - authorize_admin_project + authorize! :admin_build, user_project trigger = user_project.triggers.find_by(token: params[:token].to_s) return not_found!('Trigger') unless trigger diff --git a/lib/api/variables.rb b/lib/api/variables.rb index d9a055f6c9..f6495071a1 100644 --- a/lib/api/variables.rb +++ b/lib/api/variables.rb @@ -2,7 +2,7 @@ module API # Projects variables API class Variables < Grape::API before { authenticate! } - before { authorize_admin_project } + before { authorize! :admin_build, user_project } resource :projects do # Get project variables diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 099062eeb8..4962f5e53c 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -1,6 +1,9 @@ module Backup class Manager def pack + # Make sure there is a connection + ActiveRecord::Base.connection.reconnect! + # saving additional informations s = {} s[:db_version] = "#{ActiveRecord::Migrator.current_version}" diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb index 5952a03162..207437ba7c 100644 --- a/lib/banzai/filter/emoji_filter.rb +++ b/lib/banzai/filter/emoji_filter.rb @@ -45,7 +45,8 @@ module Banzai private def emoji_url(name) - emoji_path = "emoji/#{emoji_filename(name)}" + emoji_path = emoji_filename(name) + if context[:asset_host] # Asset host is specified. url_to_image(emoji_path) diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb index 3f49d492f2..04ddfe53ed 100644 --- a/lib/banzai/filter/sanitization_filter.rb +++ b/lib/banzai/filter/sanitization_filter.rb @@ -8,14 +8,7 @@ module Banzai # Extends HTML::Pipeline::SanitizationFilter with a custom whitelist. class SanitizationFilter < HTML::Pipeline::SanitizationFilter def whitelist - # Descriptions are more heavily sanitized, allowing only a few elements. - # See http://git.io/vkuAN - if context[:inline_sanitization] - whitelist = LIMITED - whitelist[:elements] -= %w(pre code img ol ul li) - else - whitelist = super - end + whitelist = super customize_whitelist(whitelist) @@ -43,6 +36,10 @@ module Banzai # Allow span elements whitelist[:elements].push('span') + # Allow abbr elements with title attribute + whitelist[:elements].push('abbr') + whitelist[:attributes]['abbr'] = %w(title) + # Allow any protocol in `a` elements... whitelist[:protocols].delete('a') diff --git a/lib/banzai/pipeline/asciidoc_pipeline.rb b/lib/banzai/pipeline/asciidoc_pipeline.rb deleted file mode 100644 index f1331c0ebf..0000000000 --- a/lib/banzai/pipeline/asciidoc_pipeline.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Banzai - module Pipeline - class AsciidocPipeline < BasePipeline - def self.filters - [ - Filter::RelativeLinkFilter - ] - end - end - end -end diff --git a/lib/banzai/pipeline/broadcast_message_pipeline.rb b/lib/banzai/pipeline/broadcast_message_pipeline.rb new file mode 100644 index 0000000000..4bb85e24c3 --- /dev/null +++ b/lib/banzai/pipeline/broadcast_message_pipeline.rb @@ -0,0 +1,16 @@ +module Banzai + module Pipeline + class BroadcastMessagePipeline < DescriptionPipeline + def self.filters + @filters ||= [ + Filter::MarkdownFilter, + Filter::SanitizationFilter, + + Filter::EmojiFilter, + Filter::AutolinkFilter, + Filter::ExternalLinkFilter + ] + end + end + end +end diff --git a/lib/banzai/pipeline/description_pipeline.rb b/lib/banzai/pipeline/description_pipeline.rb index 20e24ace35..f239586765 100644 --- a/lib/banzai/pipeline/description_pipeline.rb +++ b/lib/banzai/pipeline/description_pipeline.rb @@ -4,9 +4,20 @@ module Banzai def self.transform_context(context) super(context).merge( # SanitizationFilter - inline_sanitization: true + whitelist: whitelist ) end + + private + + def self.whitelist + # Descriptions are more heavily sanitized, allowing only a few elements. + # See http://git.io/vkuAN + whitelist = Banzai::Filter::SanitizationFilter::LIMITED + whitelist[:elements] -= %w(pre code img ol ul li) + + whitelist + end end end end diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb index 5c347e432b..4e85d2c3c7 100644 --- a/lib/ci/api/api.rb +++ b/lib/ci/api/api.rb @@ -25,7 +25,7 @@ module Ci format :json - helpers Helpers + helpers ::Ci::API::Helpers helpers ::API::Helpers helpers Gitlab::CurrentSettings diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index 416b0b5f0b..2e9a5d311f 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -38,6 +38,8 @@ module Ci authenticate_runner! update_runner_last_contact build = Ci::Build.where(runner_id: current_runner.id).running.find(params[:id]) + forbidden!('Build has been erased!') if build.erased? + build.update_attributes(trace: params[:trace]) if params[:trace] case params[:state].to_s @@ -99,6 +101,7 @@ module Ci not_found! unless build authenticate_build_token!(build) forbidden!('Build is not running!') unless build.running? + forbidden!('Build has been erased!') if build.erased? artifacts_upload_path = ArtifactUploader.artifacts_upload_path artifacts = uploaded_file(:file, artifacts_upload_path) @@ -143,7 +146,7 @@ module Ci present_file!(artifacts_file.path, artifacts_file.filename) end - # Remove the artifacts file from build + # Remove the artifacts file from build - Runners only # # Parameters: # id (required) - The ID of a build @@ -156,6 +159,7 @@ module Ci build = Ci::Build.find_by_id(params[:id]) not_found! unless build authenticate_build_token!(build) + build.remove_artifacts_file! build.remove_artifacts_metadata! end diff --git a/lib/ci/status.rb b/lib/ci/status.rb index c02b3b8f3e..3fb1fe2949 100644 --- a/lib/ci/status.rb +++ b/lib/ci/status.rb @@ -1,11 +1,9 @@ module Ci class Status def self.get_status(statuses) - statuses.reject! { |status| status.try(&:allow_failure?) } - if statuses.none? 'skipped' - elsif statuses.all?(&:success?) + elsif statuses.all? { |status| status.success? || status.ignored? } 'success' elsif statuses.all?(&:pending?) 'pending' diff --git a/lib/dnsxl_check.rb b/lib/dnsxl_check.rb deleted file mode 100644 index 1e506b2d9c..0000000000 --- a/lib/dnsxl_check.rb +++ /dev/null @@ -1,105 +0,0 @@ -require 'resolv' - -class DNSXLCheck - - class Resolver - def self.search(query) - begin - Resolv.getaddress(query) - true - rescue Resolv::ResolvError - false - end - end - end - - IP_REGEXP = /\A(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\z/ - DEFAULT_THRESHOLD = 0.33 - - def self.create_from_list(list) - dnsxl_check = DNSXLCheck.new - - list.each do |entry| - dnsxl_check.add_list(entry.domain, entry.weight) - end - - dnsxl_check - end - - def test(ip) - if use_threshold? - test_with_threshold(ip) - else - test_strict(ip) - end - end - - def test_with_threshold(ip) - return false if lists.empty? - - search(ip) - final_score >= threshold - end - - def test_strict(ip) - return false if lists.empty? - - search(ip) - @score > 0 - end - - def use_threshold=(value) - @use_threshold = value == true - end - - def use_threshold? - @use_threshold &&= true - end - - def threshold=(threshold) - raise ArgumentError, "'threshold' value must be grather than 0 and less than or equal to 1" unless threshold > 0 && threshold <= 1 - @threshold = threshold - end - - def threshold - @threshold ||= DEFAULT_THRESHOLD - end - - def add_list(domain, weight) - @lists ||= [] - @lists << { domain: domain, weight: weight } - end - - def lists - @lists ||= [] - end - - private - - def search(ip) - raise ArgumentError, "'ip' value must be in #{IP_REGEXP} format" unless ip.match(IP_REGEXP) - - @score = 0 - - reversed = reverse_ip(ip) - search_in_rbls(reversed) - end - - def reverse_ip(ip) - ip.split('.').reverse.join('.') - end - - def search_in_rbls(reversed_ip) - lists.each do |rbl| - query = "#{reversed_ip}.#{rbl[:domain]}" - @score += rbl[:weight] if Resolver.search(query) - end - end - - def final_score - weights = lists.map{ |rbl| rbl[:weight] }.reduce(:+).to_i - return 0 if weights == 0 - - (@score.to_f / weights.to_f).round(2) - end -end diff --git a/lib/gitlab/akismet_helper.rb b/lib/gitlab/akismet_helper.rb new file mode 100644 index 0000000000..b366c89889 --- /dev/null +++ b/lib/gitlab/akismet_helper.rb @@ -0,0 +1,39 @@ +module Gitlab + module AkismetHelper + def akismet_enabled? + current_application_settings.akismet_enabled + end + + def akismet_client + @akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key, + Gitlab.config.gitlab.url) + end + + def check_for_spam?(project, user) + akismet_enabled? && !project.team.member?(user) + end + + def is_spam?(environment, user, text) + client = akismet_client + ip_address = environment['REMOTE_ADDR'] + user_agent = environment['HTTP_USER_AGENT'] + + params = { + type: 'comment', + text: text, + created_at: DateTime.now, + author: user.name, + author_email: user.email, + referrer: environment['HTTP_REFERER'], + } + + begin + is_spam, is_blatant = client.check(ip_address, user_agent, params) + is_spam || is_blatant + rescue => e + Rails.logger.error("Unable to connect to Akismet: #{e}, skipping check") + false + end + end + end +end diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb index b203b9d70e..0b9c2e730f 100644 --- a/lib/gitlab/asciidoc.rb +++ b/lib/gitlab/asciidoc.rb @@ -31,9 +31,7 @@ module Gitlab html = ::Asciidoctor.convert(input, asciidoc_opts) - if context[:project] - html = Banzai.render(html, context.merge(pipeline: :asciidoc)) - end + html = Banzai.post_process(html, context) html.html_safe end diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index 4c15d58d68..b9bb6e7608 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -36,7 +36,7 @@ module Gitlab # import_repository("gitlab/gitlab-ci", "https://github.com/randx/six.git") # def import_repository(name, url) - output, status = Popen::popen([gitlab_shell_projects_path, 'import-project', "#{name}.git", url, '240']) + output, status = Popen::popen([gitlab_shell_projects_path, 'import-project', "#{name}.git", url, '900']) raise Error, output unless status.zero? true end @@ -47,7 +47,7 @@ module Gitlab # new_path - new project path with namespace # # Ex. - # mv_repository("gitlab/gitlab-ci", "randx/gitlab-ci-new.git") + # mv_repository("gitlab/gitlab-ci", "randx/gitlab-ci-new") # def mv_repository(path, new_path) Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'mv-project', diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 2355b3c6dd..3f483847ef 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -13,12 +13,36 @@ module Gitlab end def execute - project_identifier = project.import_source + import_issues if has_issues? - return true unless client.project(project_identifier)["has_issues"] + true + rescue ActiveRecord::RecordInvalid => e + raise Projects::ImportService::Error.new, e.message + ensure + Gitlab::BitbucketImport::KeyDeleter.new(project).execute + end - #Issues && Comments - issues = client.issues(project_identifier) + private + + def gl_user_id(project, bitbucket_id) + if bitbucket_id + user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s) + (user && user.id) || project.creator_id + else + project.creator_id + end + end + + def identifier + project.import_source + end + + def has_issues? + client.project(identifier)["has_issues"] + end + + def import_issues + issues = client.issues(identifier) issues.each do |issue| body = '' @@ -33,7 +57,7 @@ module Gitlab body = @formatter.author_line(author) body += issue["content"] - comments = client.issue_comments(project_identifier, issue["local_id"]) + comments = client.issue_comments(identifier, issue["local_id"]) if comments.any? body += @formatter.comments_header @@ -56,20 +80,9 @@ module Gitlab author_id: gl_user_id(project, reporter) ) end - - true + rescue ActiveRecord::RecordInvalid => e + raise Projects::ImportService::Error, e.message end - - private - - def gl_user_id(project, bitbucket_id) - if bitbucket_id - user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s) - (user && user.id) || project.creator_id - else - project.creator_id - end - end end end end diff --git a/lib/gitlab/blame.rb b/lib/gitlab/blame.rb index 313e6b9fc0..997a22779a 100644 --- a/lib/gitlab/blame.rb +++ b/lib/gitlab/blame.rb @@ -40,6 +40,7 @@ module Gitlab end def highlighted_lines + @blob.load_all_data!(repository) @highlighted_lines ||= Gitlab::Highlight.highlight(@blob.name, @blob.data).lines end diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 7f938780ab..761b63e98f 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -4,11 +4,14 @@ module Gitlab key = :current_application_settings RequestStore.store[key] ||= begin + settings = nil + if connect_to_db? - ApplicationSetting.current || ApplicationSetting.create_from_defaults - else - fake_application_settings + settings = ::ApplicationSetting.current + settings ||= ::ApplicationSetting.create_from_defaults unless ActiveRecord::Migrator.needs_migration? end + + settings || fake_application_settings end end @@ -18,29 +21,33 @@ module Gitlab default_branch_protection: Settings.gitlab['default_branch_protection'], signup_enabled: Settings.gitlab['signup_enabled'], signin_enabled: Settings.gitlab['signin_enabled'], + twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'], gravatar_enabled: Settings.gravatar['enabled'], sign_in_text: Settings.extra['sign_in_text'], restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], max_attachment_size: Settings.gitlab['max_attachment_size'], session_expire_delay: Settings.gitlab['session_expire_delay'], - import_sources: Settings.gitlab['import_sources'], + default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], + default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], + restricted_signup_domains: Settings.gitlab['restricted_signup_domains'], + import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], max_artifacts_size: Settings.artifacts['max_size'], + require_two_factor_authentication: false, + two_factor_grace_period: 48, + akismet_enabled: false ) end private def connect_to_db? - use_db = if ENV['USE_DB'] == "false" - false - else - true - end + # When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised + active_db_connection = ActiveRecord::Base.connection.active? rescue false - use_db && ActiveRecord::Base.connection.active? && - !ActiveRecord::Migrator.needs_migration? && - ActiveRecord::Base.connection.table_exists?('application_settings') + ENV['USE_DB'] != 'false' && + active_db_connection && + ActiveRecord::Base.connection.table_exists?('application_settings') rescue ActiveRecord::NoDatabaseError false diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index de77a6fbff..6f9da69983 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -1,16 +1,23 @@ module Gitlab module Database + def self.adapter_name + connection.adapter_name + end + def self.mysql? - ActiveRecord::Base.connection.adapter_name.downcase == 'mysql2' + adapter_name.downcase == 'mysql2' end def self.postgresql? - ActiveRecord::Base.connection.adapter_name.downcase == 'postgresql' + adapter_name.downcase == 'postgresql' + end + + def self.version + database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1] end def true_value - case ActiveRecord::Base.connection.adapter_name.downcase - when 'postgresql' + if Gitlab::Database.postgresql? "'t'" else 1 @@ -18,12 +25,27 @@ module Gitlab end def false_value - case ActiveRecord::Base.connection.adapter_name.downcase - when 'postgresql' + if Gitlab::Database.postgresql? "'f'" else 0 end end + + private + + def self.connection + ActiveRecord::Base.connection + end + + def self.database_version + row = connection.execute("SELECT VERSION()").first + + if postgresql? + row['version'] + else + row.first + end + end end end diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index a7f925ce13..9429b3ff88 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -21,13 +21,13 @@ module Gitlab # ignore highlighting for "match" lines next diff_line if diff_line.type == 'match' || diff_line.type == 'nonewline' - rich_line = highlight_line(diff_line, i) + rich_line = highlight_line(diff_line) || diff_line.text if line_inline_diffs = inline_diffs[i] rich_line = InlineDiffMarker.new(diff_line.text, rich_line).mark(line_inline_diffs) end - diff_line.text = rich_line.html_safe + diff_line.text = rich_line diff_line end @@ -35,8 +35,8 @@ module Gitlab private - def highlight_line(diff_line, index) - return html_escape(diff_line.text) unless diff_file && diff_file.diff_refs + def highlight_line(diff_line) + return unless diff_file && diff_file.diff_refs line_prefix = diff_line.text.match(/\A(.)/) ? $1 : ' ' @@ -49,11 +49,11 @@ module Gitlab # Only update text if line is found. This will prevent # issues with submodules given the line only exists in diff content. - rich_line ? line_prefix + rich_line : html_escape(diff_line.text) + "#{line_prefix}#{rich_line}".html_safe if rich_line end def inline_diffs - @inline_diffs ||= InlineDiff.new(@raw_lines).inline_diffs + @inline_diffs ||= InlineDiff.for_lines(@raw_lines) end def old_lines @@ -72,11 +72,6 @@ module Gitlab [ref.project.repository, ref.id, path] end - - def html_escape(str) - replacements = { '&' => '&', '>' => '>', '<' => '<', '"' => '"', "'" => ''' } - str.gsub(/[&"'><]/, replacements) - end end end end diff --git a/lib/gitlab/diff/inline_diff.rb b/lib/gitlab/diff/inline_diff.rb index b8a61ad611..789c14518b 100644 --- a/lib/gitlab/diff/inline_diff.rb +++ b/lib/gitlab/diff/inline_diff.rb @@ -1,43 +1,58 @@ module Gitlab module Diff class InlineDiff - attr_accessor :lines + attr_accessor :old_line, :new_line, :offset - def initialize(lines) - @lines = lines - end + def self.for_lines(lines) + local_edit_indexes = self.find_local_edits(lines) - def inline_diffs inline_diffs = [] local_edit_indexes.each do |index| old_index = index new_index = index + 1 - old_line = @lines[old_index] - new_line = @lines[new_index] + old_line = lines[old_index] + new_line = lines[new_index] - # Skip inline diff if empty line was replaced with content - next if old_line[1..-1] == "" + old_diffs, new_diffs = new(old_line, new_line, offset: 1).inline_diffs - # Add one, because this is based on the prefixless version - lcp = longest_common_prefix(old_line[1..-1], new_line[1..-1]) + 1 - lcs = longest_common_suffix(old_line[lcp..-1], new_line[lcp..-1]) - - old_diff_range = lcp..(old_line.length - lcs - 1) - new_diff_range = lcp..(new_line.length - lcs - 1) - - inline_diffs[old_index] = [old_diff_range] if old_diff_range.begin <= old_diff_range.end - inline_diffs[new_index] = [new_diff_range] if new_diff_range.begin <= new_diff_range.end + inline_diffs[old_index] = old_diffs + inline_diffs[new_index] = new_diffs end inline_diffs end + def initialize(old_line, new_line, offset: 0) + @old_line = old_line[offset..-1] + @new_line = new_line[offset..-1] + @offset = offset + end + + def inline_diffs + # Skip inline diff if empty line was replaced with content + return if old_line == "" + + lcp = longest_common_prefix(old_line, new_line) + lcs = longest_common_suffix(old_line[lcp..-1], new_line[lcp..-1]) + + lcp += offset + old_length = old_line.length + offset + new_length = new_line.length + offset + + old_diff_range = lcp..(old_length - lcs - 1) + new_diff_range = lcp..(new_length - lcs - 1) + + old_diffs = [old_diff_range] if old_diff_range.begin <= old_diff_range.end + new_diffs = [new_diff_range] if new_diff_range.begin <= new_diff_range.end + + [old_diffs, new_diffs] + end + private - # Find runs of single line edits - def local_edit_indexes - line_prefixes = @lines.map { |line| line.match(/\A([+-])/) ? $1 : ' ' } + def self.find_local_edits(lines) + line_prefixes = lines.map { |line| line.match(/\A([+-])/) ? $1 : ' ' } joined_line_prefixes = " #{line_prefixes.join} " offset = 0 diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb index 1d7fa1bce0..dccb717e95 100644 --- a/lib/gitlab/diff/inline_diff_marker.rb +++ b/lib/gitlab/diff/inline_diff_marker.rb @@ -5,10 +5,12 @@ module Gitlab def initialize(raw_line, rich_line = raw_line) @raw_line = raw_line - @rich_line = rich_line + @rich_line = ERB::Util.html_escape(rich_line) end def mark(line_inline_diffs) + return rich_line unless line_inline_diffs + marker_ranges = [] line_inline_diffs.each do |inline_diff_range| # Map the inline-diff range based on the raw line to character positions in the rich line @@ -19,11 +21,15 @@ module Gitlab offset = 0 # Mark each range - marker_ranges.each do |range| - offset = insert_around_range(rich_line, range, "", "", offset) + marker_ranges.each_with_index do |range, i| + class_names = ["idiff"] + class_names << "left" if i == 0 + class_names << "right" if i == marker_ranges.length - 1 + + offset = insert_around_range(rich_line, range, "", "", offset) end - rich_line + rich_line.html_safe end private diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb new file mode 100644 index 0000000000..abf5fbe7ff --- /dev/null +++ b/lib/gitlab/exclusive_lease.rb @@ -0,0 +1,41 @@ +module Gitlab + # This class implements an 'exclusive lease'. We call it a 'lease' + # because it has a set expiry time. We call it 'exclusive' because only + # one caller may obtain a lease for a given key at a time. The + # implementation is intended to work across GitLab processes and across + # servers. It is a 'cheap' alternative to using SQL queries and updates: + # you do not need to change the SQL schema to start using + # ExclusiveLease. + # + # It is important to choose the timeout wisely. If the timeout is very + # high (1 hour) then the throughput of your operation gets very low (at + # most once an hour). If the timeout is lower than how long your + # operation may take then you cannot count on exclusivity. For example, + # if the timeout is 10 seconds and you do an operation which may take 20 + # seconds then two overlapping operations may hold a lease for the same + # key at the same time. + # + class ExclusiveLease + def initialize(key, timeout:) + @key, @timeout = key, timeout + end + + # Try to obtain the lease. Return true on success, + # false if the lease is already taken. + def try_obtain + # Performing a single SET is atomic + !!redis.set(redis_key, '1', nx: true, ex: @timeout) + end + + private + + def redis + # Maybe someday we want to use a connection pool... + @redis ||= Redis.new(url: Gitlab::REDIS_URL) + end + + def redis_key + "gitlab:exclusive_lease:#{@key}" + end + end +end diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index f065cc5e9e..191bea86ac 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -1,8 +1,8 @@ module Gitlab module Git - BLANK_SHA = '0' * 40 - TAG_REF_PREFIX = "refs/tags/" - BRANCH_REF_PREFIX = "refs/heads/" + BLANK_SHA = ('0' * 40).freeze + TAG_REF_PREFIX = "refs/tags/".freeze + BRANCH_REF_PREFIX = "refs/heads/".freeze class << self def ref_name(ref) diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 663402e819..e2a85f2982 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -35,8 +35,8 @@ module Gitlab end true - rescue ActiveRecord::RecordInvalid - false + rescue ActiveRecord::RecordInvalid => e + raise Projects::ImportService::Error, e.message end def import_pull_requests @@ -53,8 +53,8 @@ module Gitlab end true - rescue ActiveRecord::RecordInvalid - false + rescue ActiveRecord::RecordInvalid => e + raise Projects::ImportService::Error, e.message end def import_comments(issue_number, noteable) @@ -83,10 +83,13 @@ module Gitlab true rescue Gitlab::Shell::Error => e - if e.message =~ /repository not exported/ - true + # GitHub error message when the wiki repo has not been created, + # this means that repo has wiki enabled, but have no pages. So, + # we can skip the import. + if e.message !~ /repository not exported/ + raise Projects::ImportService::Error, e.message else - false + true end end end diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb index 59926084d0..850b73244c 100644 --- a/lib/gitlab/gitlab_import/importer.rb +++ b/lib/gitlab/gitlab_import/importer.rb @@ -12,7 +12,7 @@ module Gitlab end def execute - project_identifier = CGI.escape(project.import_source, '/') + project_identifier = CGI.escape(project.import_source) #Issues && Comments issues = client.issues(project_identifier) diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb index 4ddb4fea97..cac7644232 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -8,6 +8,7 @@ module Gitlab blob = repository.blob_at(ref, file_name) return [] unless blob + blob.load_all_data!(repository) highlight(file_name, blob.data).lines.map!(&:html_safe) end diff --git a/lib/gitlab/ip_check.rb b/lib/gitlab/ip_check.rb deleted file mode 100644 index f2e9b50d22..0000000000 --- a/lib/gitlab/ip_check.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Gitlab - class IpCheck - - def initialize(ip) - @ip = ip - - application_settings = ApplicationSetting.current - @ip_blocking_enabled = application_settings.ip_blocking_enabled - @dnsbl_servers_list = application_settings.dnsbl_servers_list - end - - def spam? - @ip_blocking_enabled && blacklisted? - end - - private - - def blacklisted? - on_dns_blacklist? - end - - def on_dns_blacklist? - dnsbl_check = DNSXLCheck.new - prepare_dnsbl_list(dnsbl_check) - dnsbl_check.test(@ip) - end - - def prepare_dnsbl_list(dnsbl_check) - @dnsbl_servers_list.split(',').map(&:strip).reject(&:empty?).each do |domain| - dnsbl_check.add_list(domain, 1) - end - end - end -end diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb index e044f0ecc6..b84c81f1a6 100644 --- a/lib/gitlab/ldap/user.rb +++ b/lib/gitlab/ldap/user.rb @@ -24,6 +24,10 @@ module Gitlab update_user_attributes end + def save + super('LDAP') + end + # instance methods def gl_user @gl_user ||= find_by_uid_and_provider || find_by_email || build_new_user diff --git a/lib/gitlab/note_data_builder.rb b/lib/gitlab/note_data_builder.rb index ea6b0ee796..71cf6a0d88 100644 --- a/lib/gitlab/note_data_builder.rb +++ b/lib/gitlab/note_data_builder.rb @@ -53,13 +53,10 @@ module Gitlab object_kind: "note", user: user.hook_attrs, project_id: project.id, - repository: { - name: project.name, - url: project.url_to_repo, - description: project.description, - homepage: project.web_url, - }, - object_attributes: note.hook_attrs + project: project.hook_attrs, + object_attributes: note.hook_attrs, + # DEPRECATED + repository: project.hook_attrs.slice(:name, :url, :description, :homepage) } base_data[:object_attributes][:url] = diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index d87a72f7ba..832fb08a52 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -26,7 +26,7 @@ module Gitlab gl_user.try(:valid?) end - def save + def save(provider = 'OAuth') unauthorized_to_create unless gl_user if needs_blocking? @@ -36,10 +36,10 @@ module Gitlab gl_user.save! end - log.info "(OAuth) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}" + log.info "(#{provider}) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}" gl_user rescue ActiveRecord::RecordInvalid => e - log.info "(OAuth) Error saving user: #{gl_user.errors.full_messages}" + log.info "(#{provider}) Error saving user: #{gl_user.errors.full_messages}" return self, e.record.errors end @@ -105,7 +105,12 @@ module Gitlab end def signup_enabled? - Gitlab.config.omniauth.allow_single_sign_on + providers = Gitlab.config.omniauth.allow_single_sign_on + if providers.is_a?(Array) + providers.include?(auth_hash.provider) + else + providers + end end def block_after_signup? diff --git a/lib/gitlab/other_markup.rb b/lib/gitlab/other_markup.rb new file mode 100644 index 0000000000..746ec28333 --- /dev/null +++ b/lib/gitlab/other_markup.rb @@ -0,0 +1,24 @@ +module Gitlab + # Parser/renderer for markups without other special support code. + module OtherMarkup + + # Public: Converts the provided markup into HTML. + # + # input - the source text in a markup format + # context - a Hash with the template context: + # :commit + # :project + # :project_wiki + # :requested_path + # :ref + # + def self.render(file_name, input, context) + html = GitHub::Markup.render(file_name, input). + force_encoding(input.encoding) + + html = Banzai.post_process(html, context) + + html.html_safe + end + end +end diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb index 4f9cdef386..da1c15fef6 100644 --- a/lib/gitlab/push_data_builder.rb +++ b/lib/gitlab/push_data_builder.rb @@ -22,6 +22,8 @@ module Gitlab # } # def build(project, user, oldrev, newrev, ref, commits = [], message = nil) + commits = Array(commits) + # Total commits count commits_count = commits.size @@ -47,18 +49,14 @@ module Gitlab user_id: user.id, user_name: user.name, user_email: user.email, + user_avatar: user.avatar_url, project_id: project.id, - repository: { - name: project.name, - url: project.url_to_repo, - description: project.description, - homepage: project.web_url, - git_http_url: project.http_url_to_repo, - git_ssh_url: project.ssh_url_to_repo, - visibility_level: project.visibility_level - }, + project: project.hook_attrs, commits: commit_attrs, - total_commits_count: commits_count + total_commits_count: commits_count, + # DEPRECATED + repository: project.hook_attrs.slice(:name, :url, :description, :homepage, + :git_http_url, :git_ssh_url, :visibility_level) } data diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 53ab2686b4..ace906a6f5 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -34,29 +34,29 @@ module Gitlab def project_path_regex - @project_path_regex ||= /\A[a-zA-Z0-9_.][a-zA-Z0-9_\-\.]*(? min ? line - surrounding_lines : min - upper = line + surrounding_lines < max ? line + surrounding_lines : max - (lower..upper).to_a - end - - # Returns a sorted set of lines to be included in a snippet preview. - # This ensures matching adjacent lines do not display duplicated - # surrounding code. - # - # @returns Array, unique and sorted. - def matching_lines(lined_content) - used_lines = [] - lined_content.each_with_index do |line, line_number| - used_lines.concat bounded_line_numbers( - line_number, - 0, - lined_content.size - ) if line.include?(query) - end - - used_lines.uniq.sort - end - - # 'Chunkify' entire snippet. Splits the snippet data into matching lines + - # surrounding_lines() worth of unmatching lines. - # - # @returns a hash with {snippet_object, snippet_chunks:{data,start_line}} - def chunk_snippet(snippet) - lined_content = snippet.content.split("\n") - used_lines = matching_lines(lined_content) - - snippet_chunk = [] - snippet_chunks = [] - snippet_start_line = 0 - last_line = -1 - - # Go through each used line, and add consecutive lines as a single chunk - # to the snippet chunk array. - used_lines.each do |line_number| - if last_line < 0 - # Start a new chunk. - snippet_start_line = line_number - snippet_chunk << lined_content[line_number] - elsif last_line == line_number - 1 - # Consecutive line, continue chunk. - snippet_chunk << lined_content[line_number] - else - # Non-consecutive line, add chunk to chunk array. - snippet_chunks << { - data: snippet_chunk.join("\n"), - start_line: snippet_start_line + 1 - } - - # Start a new chunk. - snippet_chunk = [lined_content[line_number]] - snippet_start_line = line_number - end - last_line = line_number - end - # Add final chunk to chunk array - snippet_chunks << { - data: snippet_chunk.join("\n"), - start_line: snippet_start_line + 1 - } - - # Return snippet with chunk array - { snippet_object: snippet, snippet_chunks: snippet_chunks } - end - - # Defines how many unmatching lines should be - # included around the matching lines in a snippet - def surrounding_lines - 3 - end end end diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index 4885baf952..d1b42c1f9b 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -3,7 +3,7 @@ module Gitlab def self.allowed?(user) return false if user.blocked? - if user.requires_ldap_check? + if user.requires_ldap_check? && user.try_obtain_ldap_lease return false unless Gitlab::LDAP::Access.allowed?(user) end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb new file mode 100644 index 0000000000..a23120a417 --- /dev/null +++ b/lib/gitlab/workhorse.rb @@ -0,0 +1,21 @@ +require 'base64' +require 'json' + +module Gitlab + class Workhorse + class << self + def send_git_blob(repository, blob) + params_hash = { + 'RepoPath' => repository.path_to_repo, + 'BlobId' => blob.id, + } + params = Base64.urlsafe_encode64(JSON.dump(params_hash)) + + [ + 'Gitlab-Workhorse-Send-Data', + "git-blob:#{params}", + ] + end + end + end +end diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab index 1633891c8a..d95e7023d2 100755 --- a/lib/support/init.d/gitlab +++ b/lib/support/init.d/gitlab @@ -38,7 +38,7 @@ web_server_pid_path="$pid_path/unicorn.pid" sidekiq_pid_path="$pid_path/sidekiq.pid" mail_room_enabled=false mail_room_pid_path="$pid_path/mail_room.pid" -gitlab_workhorse_dir=$(cd $app_root/../gitlab-workhorse && pwd) +gitlab_workhorse_dir=$(cd $app_root/../gitlab-workhorse 2> /dev/null && pwd) gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid" gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $rails_socket -documentRoot $app_root/public" gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log" @@ -49,7 +49,7 @@ test -f /etc/default/gitlab && . /etc/default/gitlab # Switch to the app_user if it is not he/she who is running the script. if [ `whoami` != "$app_user" ]; then - eval su - "$app_user" -s $shell_path -c $(echo \")$0 "$@"$(echo \"); exit; + eval su - "$app_user" -c $(echo \")$shell_path -l -c \'$0 "$@"\'$(echo \"); exit; fi # Switch to the gitlab path, exit on failure. @@ -219,7 +219,7 @@ start_gitlab() { echo "The Unicorn web server already running with pid $wpid, not restarting." else # Remove old socket if it exists - rm -f "$socket_path"/gitlab.socket 2>/dev/null + rm -f "$rails_socket" 2>/dev/null # Start the web server RAILS_ENV=$RAILS_ENV bin/web start fi diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example index 4e6e56ac2d..cc8617b72c 100755 --- a/lib/support/init.d/gitlab.default.example +++ b/lib/support/init.d/gitlab.default.example @@ -34,11 +34,16 @@ sidekiq_pid_path="$pid_path/sidekiq.pid" # /home/git/gitlab-workhorse . gitlab_workhorse_dir=$(cd $app_root/../gitlab-workhorse && pwd) gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid" + # The -listenXxx settings determine where gitlab-workhorse -# listens for connections from NGINX. To listen on localhost:8181, write -# '-listenNetwork tcp -listenAddr localhost:8181'. -# The -authBackend setting tells gitlab-workhorse where it can reach -# Unicorn. +# listens for connections from the web server. By default it listens to a +# socket. To listen on TCP connections (needed by Apache) change to: +# '-listenNetwork tcp -listenAddr 127.0.0.1:8181' +# +# The -authBackend setting tells gitlab-workhorse where it can reach Unicorn. +# For relative URL support change to: +# '-authBackend http://127.0.0.1/8080/gitlab' +# Read more in http://doc.gitlab.com/ce/install/relative_url.html gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $socket_path/gitlab.socket -documentRoot $app_root/public" gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log" diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake index 1728dda72c..b262ea898d 100644 --- a/lib/tasks/cache.rake +++ b/lib/tasks/cache.rake @@ -1,11 +1,22 @@ namespace :cache do + CLEAR_BATCH_SIZE = 1000 + REDIS_SCAN_START_STOP = '0' # Magic value, see http://redis.io/commands/scan + desc "GitLab | Clear redis cache" task :clear => :environment do - # Hack into Rails.cache until https://github.com/redis-store/redis-store/pull/225 - # is accepted (I hope) and we can update the redis-store gem. redis_store = Rails.cache.instance_variable_get(:@data) - redis_store.keys.each_slice(1000) do |key_slice| - redis_store.del(*key_slice) + cursor = [REDIS_SCAN_START_STOP, []] + loop do + cursor = redis_store.scan( + cursor.first, + match: "#{Gitlab::REDIS_CACHE_NAMESPACE}*", + count: CLEAR_BATCH_SIZE + ) + + keys = cursor.last + redis_store.del(*keys) if keys.any? + + break if cursor.first == REDIS_SCAN_START_STOP end end end diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 2dc2953e32..0b28b5b131 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -16,7 +16,6 @@ namespace :gitlab do check_git_config check_database_config_exists - check_database_is_not_sqlite check_migrations_are_up check_orphaned_group_members check_gitlab_config_exists @@ -90,24 +89,6 @@ namespace :gitlab do end end - def check_database_is_not_sqlite - print "Database is SQLite ... " - - database_config_file = Rails.root.join("config", "database.yml") - - unless File.read(database_config_file) =~ /adapter:\s+sqlite/ - puts "no".green - else - puts "yes".red - puts "Please fix this by removing the SQLite entry from the database.yml".blue - for_more_information( - "https://github.com/gitlabhq/gitlabhq/wiki/Migrate-from-SQLite-to-MySQL", - see_database_guide - ) - fix_and_rerun - end - end - def check_gitlab_config_exists print "GitLab config exists? ... " @@ -285,7 +266,7 @@ namespace :gitlab do unless File.directory?(Rails.root.join('public/uploads')) puts "no".red try_fixing_it( - "sudo -u #{gitlab_user} mkdir -m 750 #{Rails.root}/public/uploads" + "sudo -u #{gitlab_user} mkdir #{Rails.root}/public/uploads" ) for_more_information( see_installation_guide_section "GitLab" @@ -297,21 +278,22 @@ namespace :gitlab do upload_path = File.realpath(Rails.root.join('public/uploads')) upload_path_tmp = File.join(upload_path, 'tmp') - if File.stat(upload_path).mode == 040750 + if File.stat(upload_path).mode == 040700 unless Dir.exists?(upload_path_tmp) puts 'skipped (no tmp uploads folder yet)'.magenta return end - # if tmp upload dir has incorrect permissions, assume others do as well - if File.stat(upload_path_tmp).mode == 040755 && File.owned?(upload_path_tmp) # verify drwxr-xr-x permissions + # If tmp upload dir has incorrect permissions, assume others do as well + # Verify drwx------ permissions + if File.stat(upload_path_tmp).mode == 040700 && File.owned?(upload_path_tmp) puts "yes".green else puts "no".red try_fixing_it( "sudo chown -R #{gitlab_user} #{upload_path}", "sudo find #{upload_path} -type f -exec chmod 0644 {} \\;", - "sudo find #{upload_path} -type d -not -path #{upload_path} -exec chmod 0755 {} \\;" + "sudo find #{upload_path} -type d -not -path #{upload_path} -exec chmod 0700 {} \\;" ) for_more_information( see_installation_guide_section "GitLab" @@ -321,7 +303,7 @@ namespace :gitlab do else puts "no".red try_fixing_it( - "sudo chmod 0750 #{upload_path}", + "sudo find #{upload_path} -type d -not -path #{upload_path} -exec chmod 0700 {} \\;" ) for_more_information( see_installation_guide_section "GitLab" @@ -929,7 +911,7 @@ namespace :gitlab do end def check_git_version - required_version = Gitlab::VersionInfo.new(1, 7, 10) + required_version = Gitlab::VersionInfo.new(2, 7, 3) current_version = Gitlab::VersionInfo.parse(run(%W(#{Gitlab.config.git.bin_path} --version))) puts "Your git bin path is \"#{Gitlab.config.git.bin_path}\"" diff --git a/scripts/ci/prepare_build.sh b/scripts/ci/prepare_build.sh deleted file mode 100755 index 864a683a1b..0000000000 --- a/scripts/ci/prepare_build.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -if [ -f /.dockerinit ]; then - export FLAGS=(--deployment --path /cache) - - apt-get update -qq - apt-get install -y -qq nodejs - - wget -q http://ftp.de.debian.org/debian/pool/main/p/phantomjs/phantomjs_1.9.0-1+b1_amd64.deb - dpkg -i phantomjs_1.9.0-1+b1_amd64.deb - - cp config/database.yml.mysql config/database.yml - sed -i "s/username:.*/username: root/g" config/database.yml - sed -i "s/password:.*/password:/g" config/database.yml - sed -i "s/# socket:.*/host: mysql/g" config/database.yml -else - export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin - - cp config/database.yml.mysql config/database.yml - sed -i "s/username\:.*$/username\: runner/" config/database.yml - sed -i "s/password\:.*$/password\: 'password'/" config/database.yml - sed -i "s/gitlab_ci_test/gitlab_ci_test_$((RANDOM/5000))/" config/database.yml -fi diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index 119cc90fc1..b6f076a90c 100755 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -1,10 +1,16 @@ #!/bin/bash + if [ -f /.dockerinit ]; then - wget -q https://gitlab.com/axil/phantomjs-debian/raw/master/phantomjs_1.9.8-0jessie_amd64.deb - dpkg -i phantomjs_1.9.8-0jessie_amd64.deb + mkdir -p vendor + if [ ! -e vendor/phantomjs_1.9.8-0jessie_amd64.deb ]; then + wget -q https://gitlab.com/axil/phantomjs-debian/raw/master/phantomjs_1.9.8-0jessie_amd64.deb + mv phantomjs_1.9.8-0jessie_amd64.deb vendor/ + fi + dpkg -i vendor/phantomjs_1.9.8-0jessie_amd64.deb apt-get update -qq - apt-get install -y -qq libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client + apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \ + libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip cp config/database.yml.mysql config/database.yml sed -i 's/username:.*/username: root/g' config/database.yml @@ -13,8 +19,8 @@ if [ -f /.dockerinit ]; then cp config/resque.yml.example config/resque.yml sed -i 's/localhost/redis/g' config/resque.yml - FLAGS=(--deployment --path /cache) - export FLAGS + + export FLAGS=(--path vendor) else export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin cp config/database.yml.mysql config/database.yml diff --git a/spec/controllers/admin/spam_logs_controller_spec.rb b/spec/controllers/admin/spam_logs_controller_spec.rb new file mode 100644 index 0000000000..b51b303a71 --- /dev/null +++ b/spec/controllers/admin/spam_logs_controller_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Admin::SpamLogsController do + let(:admin) { create(:admin) } + let(:user) { create(:user) } + let!(:first_spam) { create(:spam_log, user: user) } + let!(:second_spam) { create(:spam_log, user: user) } + + before do + sign_in(admin) + end + + describe '#index' do + it 'lists all spam logs' do + get :index + + expect(response.status).to eq(200) + end + end + + describe '#destroy' do + it 'removes only the spam log when removing log' do + expect { delete :destroy, id: first_spam.id }.to change { SpamLog.count }.by(-1) + expect(User.find(user.id)).to be_truthy + expect(response.status).to eq(200) + end + + it 'removes user and his spam logs when removing the user' do + delete :destroy, id: first_spam.id, remove_user: true + + expect(flash[:notice]).to eq "User #{user.username} was successfully removed." + expect(response.status).to eq(302) + expect(SpamLog.count).to eq(0) + expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + end +end diff --git a/spec/controllers/ci/projects_controller_spec.rb b/spec/controllers/ci/projects_controller_spec.rb new file mode 100644 index 0000000000..db0748f323 --- /dev/null +++ b/spec/controllers/ci/projects_controller_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe Ci::ProjectsController do + let(:visibility) { :public } + let!(:project) { create(:project, visibility, ci_id: 1) } + let(:ci_id) { project.ci_id } + + ## + # Specs for *deprecated* CI badge + # + describe '#badge' do + shared_examples 'badge provider' do + it 'shows badge' do + expect(response.status).to eq 200 + expect(response.headers) + .to include('Content-Type' => 'image/svg+xml') + end + end + + context 'user not signed in' do + before { get(:badge, id: ci_id) } + + context 'project has no ci_id reference' do + let(:ci_id) { 123 } + + it 'returns 404' do + expect(response.status).to eq 404 + end + end + + context 'project is public' do + let(:visibility) { :public } + it_behaves_like 'badge provider' + end + + context 'project is private' do + let(:visibility) { :private } + it_behaves_like 'badge provider' + end + end + + context 'user signed in' do + let(:user) { create(:user) } + before { sign_in(user) } + before { get(:badge, id: ci_id) } + + context 'private is internal' do + let(:visibility) { :internal } + it_behaves_like 'badge provider' + end + end + end +end diff --git a/spec/controllers/commit_controller_spec.rb b/spec/controllers/commit_controller_spec.rb index 7793bf1e42..bbe400dad8 100644 --- a/spec/controllers/commit_controller_spec.rb +++ b/spec/controllers/commit_controller_spec.rb @@ -143,4 +143,53 @@ describe Projects::CommitController do expect(assigns(:tags)).to include("v1.1.0") end end + + describe '#revert' do + context 'when target branch is not provided' do + it 'should render the 404 page' do + post(:revert, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: commit.id) + + expect(response).not_to be_success + expect(response.status).to eq(404) + end + end + + context 'when the revert was successful' do + it 'should redirect to the commits page' do + post(:revert, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + target_branch: 'master', + id: commit.id) + + expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master') + expect(flash[:notice]).to eq('The commit has been successfully reverted.') + end + end + + context 'when the revert failed' do + before do + post(:revert, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + target_branch: 'master', + id: commit.id) + end + + it 'should redirect to the commit page' do + # Reverting a commit that has been already reverted. + post(:revert, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + target_branch: 'master', + id: commit.id) + + expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, commit.id) + expect(flash[:alert]).to match('Sorry, we cannot revert this commit automatically.') + end + end + end end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb new file mode 100644 index 0000000000..938e97298b --- /dev/null +++ b/spec/controllers/groups_controller_spec.rb @@ -0,0 +1,23 @@ +require 'rails_helper' + +describe GroupsController do + describe 'GET index' do + context 'as a user' do + it 'redirects to Groups Dashboard' do + sign_in(create(:user)) + + get :index + + expect(response).to redirect_to(dashboard_groups_path) + end + end + + context 'as a guest' do + it 'redirects to Explore Groups' do + get :index + + expect(response).to redirect_to(explore_groups_path) + end + end + end +end diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb new file mode 100644 index 0000000000..438e776ec4 --- /dev/null +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -0,0 +1,37 @@ +require 'rails_helper' + +describe Projects::CommitController do + describe 'GET show' do + let(:project) { create(:project) } + + before do + user = create(:user) + project.team << [user, :master] + + sign_in(user) + end + + context 'with valid id' do + it 'responds with 200' do + go id: project.commit.id + + expect(response).to be_ok + end + end + + context 'with invalid id' do + it 'responds with 404' do + go id: project.commit.id.reverse + + expect(response).to be_not_found + end + end + + def go(id:) + get :show, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: id + end + end +end diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb index 85d1d1e052..0147bd2b95 100644 --- a/spec/controllers/projects/imports_controller_spec.rb +++ b/spec/controllers/projects/imports_controller_spec.rb @@ -104,6 +104,18 @@ describe Projects::ImportsController do end end end + + context 'when import never happened' do + before do + project.update_attribute(:import_status, :none) + end + + it 'redirects to namespace_project_path' do + get :show, namespace_id: project.namespace.to_param, project_id: project.to_param + + expect(response).to redirect_to namespace_project_path(project.namespace, project) + end + end end end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 6aaec224f6..e82fe26c7a 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -123,6 +123,40 @@ describe Projects::MergeRequestsController do end end + describe 'GET #index' do + def get_merge_requests + get :index, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + state: 'opened' + end + + context 'when filtering by opened state' do + + context 'with opened merge requests' do + it 'should list those merge requests' do + get_merge_requests + + expect(assigns(:merge_requests)).to include(merge_request) + end + end + + context 'with reopened merge requests' do + before do + merge_request.close! + merge_request.reopen! + end + + it 'should list those merge requests' do + get_merge_requests + + expect(assigns(:merge_requests)).to include(merge_request) + end + end + + end + end + describe 'GET diffs' do def go(format: 'html') get :diffs, @@ -188,7 +222,7 @@ describe Projects::MergeRequestsController do expect(response).to render_template('diffs') end end - + context 'as json' do it 'renders the diffs template to a string' do go format: 'json' @@ -199,6 +233,32 @@ describe Projects::MergeRequestsController do end end + describe 'GET diffs with view' do + def go(extra_params = {}) + params = { + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: merge_request.iid + } + + get :diffs, params.merge(extra_params) + end + + it 'saves the preferred diff view in a cookie' do + go view: 'parallel' + + expect(response.cookies['diff_view']).to eq('parallel') + end + + it 'assigns :view param based on cookie' do + request.cookies['diff_view'] = 'parallel' + + go + + expect(controller.params[:view]).to eq 'parallel' + end + end + describe 'GET commits' do def go(format: 'html') get :commits, diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 665526fde9..6eee4dfe22 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -86,6 +86,14 @@ describe ProjectsController do end end end + + context "when the url contains .atom" do + let(:public_project_with_dot_atom) { build(:project, :public, name: 'my.atom', path: 'my.atom') } + + it 'expect an error creating the project' do + expect(public_project_with_dot_atom).not_to be_valid + end + end end describe "#destroy" do diff --git a/spec/factories.rb b/spec/factories.rb index 2a81684dfc..d6483ed6ce 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -32,6 +32,7 @@ FactoryGirl.define do before(:create) do |user| user.two_factor_enabled = true user.otp_secret = User.generate_otp_secret(32) + user.otp_grace_period_started_at = Time.now user.generate_otp_backup_codes! end end diff --git a/spec/factories/appearances.rb b/spec/factories/appearances.rb new file mode 100644 index 0000000000..cf2a2b76bc --- /dev/null +++ b/spec/factories/appearances.rb @@ -0,0 +1,8 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :appearance do + title "MepMep" + description "This is my Community Edition instance" + end +end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index d2db77f628..a46466798d 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -1,30 +1,3 @@ -# == Schema Information -# -# Table name: builds -# -# id :integer not null, primary key -# project_id :integer -# status :string(255) -# finished_at :datetime -# trace :text -# created_at :datetime -# updated_at :datetime -# started_at :datetime -# runner_id :integer -# commit_id :integer -# coverage :float -# commands :text -# job_id :integer -# name :string(255) -# deploy :boolean default(FALSE) -# options :text -# allow_failure :boolean default(FALSE), not null -# stage :string(255) -# trigger_request_id :integer -# - -# Read about factories at https://github.com/thoughtbot/factory_girl - FactoryGirl.define do factory :ci_build, class: Ci::Build do name 'test' @@ -43,10 +16,30 @@ FactoryGirl.define do commit factory: :ci_commit + trait :success do + status 'success' + end + + trait :failed do + status 'failed' + end + trait :canceled do status 'canceled' end + trait :running do + status 'running' + end + + trait :pending do + status 'pending' + end + + trait :allowed_to_fail do + allow_failure true + end + after(:build) do |build, evaluator| build.project = build.commit.project end @@ -60,10 +53,24 @@ FactoryGirl.define do tag true end - factory :ci_build_with_trace do - after(:create) do |build, evaluator| + trait :trace do + after(:create) do |build, evaluator| build.trace = 'BUILD TRACE' end end + + trait :artifacts do + after(:create) do |build, _| + build.artifacts_file = + fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), + 'application/zip') + + build.artifacts_metadata = + fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'), + 'application/x-gzip') + + build.save! + end + end end end diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb index db759eca9a..265663e845 100644 --- a/spec/factories/ci/runners.rb +++ b/spec/factories/ci/runners.rb @@ -25,14 +25,12 @@ FactoryGirl.define do "My runner#{n}" end - platform "darwin" + platform "darwin" + is_shared false + active true - factory :ci_shared_runner do + trait :shared do is_shared true end - - factory :ci_specific_runner do - is_shared false - end end end diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 0c6a881f86..00de7bb529 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -24,6 +24,7 @@ # merge_params :text # merge_when_build_succeeds :boolean default(FALSE), not null # merge_user_id :integer +# merge_commit_sha :string # FactoryGirl.define do diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 35a20adeef..32c202891d 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -34,6 +34,8 @@ FactoryGirl.define do factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff] factory :note_on_project_snippet, traits: [:on_project_snippet] factory :system_note, traits: [:system] + factory :downvote_note, traits: [:award, :downvote] + factory :upvote_note, traits: [:award, :upvote] trait :on_commit do project @@ -65,6 +67,18 @@ FactoryGirl.define do system true end + trait :award do + is_award true + end + + trait :downvote do + note "thumbsdown" + end + + trait :upvote do + note "thumbsup" + end + trait :with_attachment do attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") } end diff --git a/spec/factories/spam_logs.rb b/spec/factories/spam_logs.rb new file mode 100644 index 0000000000..d90e5d6bf2 --- /dev/null +++ b/spec/factories/spam_logs.rb @@ -0,0 +1,11 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :spam_log do + user + source_ip { FFaker::Internet.ip_v4_address } + noteable_type 'Issue' + title { FFaker::Lorem.sentence } + description { FFaker::Lorem.paragraph(5) } + end +end diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb new file mode 100644 index 0000000000..bd85b1d798 --- /dev/null +++ b/spec/factories/todos.rb @@ -0,0 +1,34 @@ +# == Schema Information +# +# Table name: todos +# +# id :integer not null, primary key +# user_id :integer not null +# project_id :integer not null +# target_id :integer not null +# target_type :string not null +# author_id :integer +# note_id :integer +# action :integer not null +# state :string not null +# created_at :datetime +# updated_at :datetime +# + +FactoryGirl.define do + factory :todo do + project + author + user + target factory: :issue + action { Todo::ASSIGNED } + + trait :assigned do + action { Todo::ASSIGNED } + end + + trait :mentioned do + action { Todo::MENTIONED } + end + end +end diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb index b955d0b0c4..2e9851fb44 100644 --- a/spec/features/admin/admin_builds_spec.rb +++ b/spec/features/admin/admin_builds_spec.rb @@ -18,7 +18,7 @@ describe 'Admin Builds' do visit admin_builds_path - expect(page).to have_selector('.project-issuable-filter li.active', text: 'All') + expect(page).to have_selector('.nav-links li.active', text: 'All') expect(page.all('.build-link').size).to eq(4) expect(page).to have_link 'Cancel all' end @@ -28,7 +28,7 @@ describe 'Admin Builds' do it 'shows a message' do visit admin_builds_path - expect(page).to have_selector('.project-issuable-filter li.active', text: 'All') + expect(page).to have_selector('.nav-links li.active', text: 'All') expect(page).to have_content 'No builds to show' expect(page).not_to have_link 'Cancel all' end @@ -44,7 +44,7 @@ describe 'Admin Builds' do visit admin_builds_path(scope: :running) - expect(page).to have_selector('.project-issuable-filter li.active', text: 'Running') + expect(page).to have_selector('.nav-links li.active', text: 'Running') expect(page.find('.build-link')).to have_content(build1.id) expect(page.find('.build-link')).not_to have_content(build2.id) expect(page.find('.build-link')).not_to have_content(build3.id) @@ -58,7 +58,7 @@ describe 'Admin Builds' do visit admin_builds_path(scope: :running) - expect(page).to have_selector('.project-issuable-filter li.active', text: 'Running') + expect(page).to have_selector('.nav-links li.active', text: 'Running') expect(page).to have_content 'No builds to show' expect(page).not_to have_link 'Cancel all' end @@ -74,7 +74,7 @@ describe 'Admin Builds' do visit admin_builds_path(scope: :finished) - expect(page).to have_selector('.project-issuable-filter li.active', text: 'Finished') + expect(page).to have_selector('.nav-links li.active', text: 'Finished') expect(page.find('.build-link')).not_to have_content(build1.id) expect(page.find('.build-link')).not_to have_content(build2.id) expect(page.find('.build-link')).to have_content(build3.id) @@ -88,7 +88,7 @@ describe 'Admin Builds' do visit admin_builds_path(scope: :finished) - expect(page).to have_selector('.project-issuable-filter li.active', text: 'Finished') + expect(page).to have_selector('.nav-links li.active', text: 'Finished') expect(page).to have_content 'No builds to show' expect(page).to have_link 'Cancel all' end diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index d37bd10371..6da3a857b3 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -8,7 +8,7 @@ describe "Builds" do @commit = FactoryGirl.create :ci_commit @build = FactoryGirl.create :ci_build, commit: @commit @project = @commit.project - @project.team << [@user, :master] + @project.team << [@user, :developer] end describe "GET /:project/builds" do @@ -18,7 +18,7 @@ describe "Builds" do visit namespace_project_builds_path(@project.namespace, @project, scope: :running) end - it { expect(page).to have_selector('.project-issuable-filter li.active', text: 'Running') } + it { expect(page).to have_selector('.nav-links li.active', text: 'Running') } it { expect(page).to have_link 'Cancel running' } it { expect(page).to have_content @build.short_sha } it { expect(page).to have_content @build.ref } @@ -31,7 +31,7 @@ describe "Builds" do visit namespace_project_builds_path(@project.namespace, @project, scope: :finished) end - it { expect(page).to have_selector('.project-issuable-filter li.active', text: 'Finished') } + it { expect(page).to have_selector('.nav-links li.active', text: 'Finished') } it { expect(page).to have_content 'No builds to show' } it { expect(page).to have_link 'Cancel running' } end @@ -42,7 +42,7 @@ describe "Builds" do visit namespace_project_builds_path(@project.namespace, @project) end - it { expect(page).to have_selector('.project-issuable-filter li.active', text: 'All') } + it { expect(page).to have_selector('.nav-links li.active', text: 'All') } it { expect(page).to have_content @build.short_sha } it { expect(page).to have_content @build.ref } it { expect(page).to have_content @build.name } @@ -57,7 +57,7 @@ describe "Builds" do click_link "Cancel running" end - it { expect(page).to have_selector('.project-issuable-filter li.active', text: 'All') } + it { expect(page).to have_selector('.nav-links li.active', text: 'All') } it { expect(page).to have_content 'canceled' } it { expect(page).to have_content @build.short_sha } it { expect(page).to have_content @build.ref } diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 5a62da1061..dacaa96d76 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -8,7 +8,6 @@ describe 'Commits' do describe 'CI' do before do login_as :user - project.team << [@user, :master] stub_ci_commit_to_return_yaml_file end @@ -19,6 +18,10 @@ describe 'Commits' do context 'commit status is Generic Commit Status' do let!(:status) { FactoryGirl.create :generic_commit_status, commit: commit } + before do + project.team << [@user, :reporter] + end + describe 'Commit builds' do before do visit ci_status_path(commit) @@ -37,83 +40,124 @@ describe 'Commits' do context 'commit status is Ci Build' do let!(:build) { FactoryGirl.create :ci_build, commit: commit } + let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } - describe 'Project commits' do + context 'when logged as developer' do before do - visit namespace_project_commits_path(project.namespace, project, :master) + project.team << [@user, :developer] end - it 'should show build status' do - page.within("//li[@id='commit-#{commit.short_sha}']") do - expect(page).to have_css(".ci-status-link") + describe 'Project commits' do + before do + visit namespace_project_commits_path(project.namespace, project, :master) + end + + it 'should show build status' do + page.within("//li[@id='commit-#{commit.short_sha}']") do + expect(page).to have_css(".ci-status-link") + end + end + end + + describe 'Commit builds' do + before do + visit ci_status_path(commit) + end + + it { expect(page).to have_content commit.sha[0..7] } + it { expect(page).to have_content commit.git_commit_message } + it { expect(page).to have_content commit.git_author_name } + end + + context 'Download artifacts' do + before do + build.update_attributes(artifacts_file: artifacts_file) + end + + it do + visit ci_status_path(commit) + click_on 'Download artifacts' + expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) + end + end + + describe 'Cancel all builds' do + it 'cancels commit' do + visit ci_status_path(commit) + click_on 'Cancel running' + expect(page).to have_content 'canceled' + end + end + + describe 'Cancel build' do + it 'cancels build' do + visit ci_status_path(commit) + click_on 'Cancel' + expect(page).to have_content 'canceled' + end + end + + describe '.gitlab-ci.yml not found warning' do + context 'ci builds enabled' do + it "does not show warning" do + visit ci_status_path(commit) + expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' + end + + it 'shows warning' do + stub_ci_commit_yaml_file(nil) + visit ci_status_path(commit) + expect(page).to have_content '.gitlab-ci.yml not found in this commit' + end + end + + context 'ci builds disabled' do + before do + stub_ci_builds_disabled + stub_ci_commit_yaml_file(nil) + visit ci_status_path(commit) + end + + it 'does not show warning' do + expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' + end end end end - describe 'Commit builds' do - before do - visit ci_status_path(commit) - end - - it { expect(page).to have_content commit.sha[0..7] } - it { expect(page).to have_content commit.git_commit_message } - it { expect(page).to have_content commit.git_author_name } - end - - context 'Download artifacts' do - let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } - + context "when logged as reporter" do before do + project.team << [@user, :reporter] build.update_attributes(artifacts_file: artifacts_file) + visit ci_status_path(commit) end it do - visit ci_status_path(commit) - click_on 'Download artifacts' - expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) + expect(page).to have_content commit.sha[0..7] + expect(page).to have_content commit.git_commit_message + expect(page).to have_content commit.git_author_name + expect(page).to have_link('Download artifacts') + expect(page).to_not have_link('Cancel running') + expect(page).to_not have_link('Retry failed') end end - describe 'Cancel all builds' do - it 'cancels commit' do + context 'when accessing internal project with disallowed access' do + before do + project.update( + visibility_level: Gitlab::VisibilityLevel::INTERNAL, + public_builds: false) + build.update_attributes(artifacts_file: artifacts_file) visit ci_status_path(commit) - click_on 'Cancel running' - expect(page).to have_content 'canceled' - end - end - - describe 'Cancel build' do - it 'cancels build' do - visit ci_status_path(commit) - click_on 'Cancel' - expect(page).to have_content 'canceled' - end - end - - describe '.gitlab-ci.yml not found warning' do - context 'ci builds enabled' do - it "does not show warning" do - visit ci_status_path(commit) - expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' - end - - it 'shows warning' do - stub_ci_commit_yaml_file(nil) - visit ci_status_path(commit) - expect(page).to have_content '.gitlab-ci.yml not found in this commit' - end end - context 'ci builds disabled' do - before do - stub_ci_builds_disabled - stub_ci_commit_yaml_file(nil) - visit ci_status_path(commit) - end - - it 'does not show warning' do - expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' - end + it do + expect(page).to have_content commit.sha[0..7] + expect(page).to have_content commit.git_commit_message + expect(page).to have_content commit.git_author_name + expect(page).to_not have_link('Download artifacts') + expect(page).to_not have_link('Cancel running') + expect(page).to_not have_link('Retry failed') end end end diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index 2451e56fe7..dac9205449 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -112,10 +112,10 @@ feature 'Login', feature: true do context 'within the grace period' do it 'redirects to two-factor configuration page' do expect(current_path).to eq new_profile_two_factor_auth_path - expect(page).to have_content('You must configure Two-Factor Authentication in your account until') + expect(page).to have_content('You must enable Two-factor Authentication for your account before') end - it 'two-factor configuration is skippable' do + it 'disallows skipping two-factor configuration' do expect(current_path).to eq new_profile_two_factor_auth_path click_link 'Configure it later' @@ -128,10 +128,10 @@ feature 'Login', feature: true do it 'redirects to two-factor configuration page' do expect(current_path).to eq new_profile_two_factor_auth_path - expect(page).to have_content('You must configure Two-Factor Authentication in your account.') + expect(page).to have_content('You must enable Two-factor Authentication for your account.') end - it 'two-factor configuration is not skippable' do + it 'disallows skipping two-factor configuration' do expect(current_path).to eq new_profile_two_factor_auth_path expect(page).not_to have_link('Configure it later') end @@ -146,7 +146,7 @@ feature 'Login', feature: true do it 'redirects to two-factor configuration page' do expect(current_path).to eq new_profile_two_factor_auth_path - expect(page).to have_content('You must configure Two-Factor Authentication in your account.') + expect(page).to have_content('You must enable Two-factor Authentication for your account.') end end end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index f0fc6916c4..1a360cd1eb 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -167,7 +167,7 @@ describe 'Comments', feature: true do end it 'should be removed when canceled' do - page.within(".diff-file form[rel$='#{line_code}']") do + page.within(".diff-file form[id$='#{line_code}']") do find('.js-close-discussion-note-form').trigger('click') end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 9a01c89ae2..ed97b6cb57 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -82,7 +82,26 @@ feature 'Project', feature: true do it 'click project-settings and find leave project' do find('#project-settings-button').click - expect(page).to have_link('Leave Project') + expect(page).to have_link('Leave Project') + end + end + + describe 'project title' do + include WaitForAjax + + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + + before do + login_with(user) + project.team.add_user(user, Gitlab::Access::MASTER) + visit namespace_project_path(project.namespace, project) + end + + it 'click toggle and show dropdown', js: true do + find('.js-projects-dropdown-toggle').click + wait_for_ajax + expect(page).to have_css('.select2-results li', count: 1) end end diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb index d97831aae1..e8886e7edf 100644 --- a/spec/features/runners_spec.rb +++ b/spec/features/runners_spec.rb @@ -17,10 +17,10 @@ describe "Runners" do @project3 = FactoryGirl.create :empty_project @project3.team << [user, :developer] - @shared_runner = FactoryGirl.create :ci_shared_runner - @specific_runner = FactoryGirl.create :ci_specific_runner - @specific_runner2 = FactoryGirl.create :ci_specific_runner - @specific_runner3 = FactoryGirl.create :ci_specific_runner + @shared_runner = FactoryGirl.create :ci_runner, :shared + @specific_runner = FactoryGirl.create :ci_runner + @specific_runner2 = FactoryGirl.create :ci_runner + @specific_runner3 = FactoryGirl.create :ci_runner @project.runners << @specific_runner @project2.runners << @specific_runner2 @project3.runners << @specific_runner3 @@ -84,7 +84,7 @@ describe "Runners" do before do @project = FactoryGirl.create :empty_project @project.team << [user, :master] - @specific_runner = FactoryGirl.create :ci_specific_runner + @specific_runner = FactoryGirl.create :ci_runner @project.runners << @specific_runner end diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index 655d2c8b7d..b98476f854 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -96,6 +96,60 @@ describe "Public Project Access", feature: true do it { is_expected.to be_denied_for :visitor } end + describe "GET /:project_path/builds" do + subject { namespace_project_builds_path(project.namespace, project) } + + context "when allowed for public" do + before { project.update(public_builds: true) } + + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } + end + + context "when disallowed for public" do + before { project.update(public_builds: false) } + + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } + end + end + + describe "GET /:project_path/builds/:id" do + let(:commit) { create(:ci_commit, project: project) } + let(:build) { create(:ci_build, commit: commit) } + subject { namespace_project_build_path(project.namespace, project, build.id) } + + context "when allowed for public" do + before { project.update(public_builds: true) } + + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } + end + + context "when disallowed for public" do + before { project.update(public_builds: false) } + + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } + end + end + describe "GET /:project_path/blob" do before do commit = project.repository.commit diff --git a/spec/fixtures/logo_sample.svg b/spec/fixtures/logo_sample.svg new file mode 100644 index 0000000000..883e7e6cf9 --- /dev/null +++ b/spec/fixtures/logo_sample.svg @@ -0,0 +1,27 @@ + + + + Slice 1 + Created with Sketch. + + + + + + diff --git a/spec/fixtures/parallel_diff_result.yml b/spec/fixtures/parallel_diff_result.yml index a326b651aa..a8b7907d4b 100644 --- a/spec/fixtures/parallel_diff_result.yml +++ b/spec/fixtures/parallel_diff_result.yml @@ -55,7 +55,7 @@ :type: new :number: 9 :text: | - + raise RuntimeError, "System commands must be given as an array of strings" + + raise RuntimeError, "System commands must be given as an array of strings" :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9 - :left: :type: diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 30e353148a..f6c1005d26 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -293,6 +293,10 @@ describe ApplicationHelper do describe 'render_markup' do let(:content) { 'Noël' } + let(:user) { create(:user) } + before do + allow(helper).to receive(:current_user).and_return(user) + end it 'should preserve encoding' do expect(content.encoding.name).to eq('UTF-8') diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 955d2852cf..14986a74c2 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -104,8 +104,7 @@ describe DiffHelper do end end - describe 'diff_line_content' do - + describe '#diff_line_content' do it 'should return non breaking space when line is empty' do expect(diff_line_content(nil)).to eq('  ') end @@ -116,9 +115,19 @@ describe DiffHelper do expect(diff_line_content(diff_file.diff_lines.first.type)).to eq('match') expect(diff_file.diff_lines.first.new_pos).to eq(6) end + end - it 'should return safe HTML' do - expect(diff_line_content(diff_file.diff_lines.first.text)).to be_html_safe + describe "#mark_inline_diffs" do + let(:old_line) { %{abc 'def'} } + let(:new_line) { %{abc "def"} } + + it "returns strings with marked inline diffs" do + marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line) + + expect(marked_old_line).to eq("abc 'def'") + expect(marked_old_line).to be_html_safe + expect(marked_new_line).to eq("abc "def"") + expect(marked_new_line).to be_html_safe end end end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 9a05b21335..9adcd916ce 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -113,7 +113,7 @@ describe GitlabMarkdownHelper do it 'should replace commit message with emoji to link' do actual = link_to_gfm(':book:Book', '/foo') expect(actual). - to eq %Q(:book:Book) + to eq %Q(:book:Book) end end diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index 0c8d06b705..4f129eca18 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe LabelsHelper do describe 'link_to_label' do let(:project) { create(:empty_project) } - let(:label) { create(:label, project: project) } + let(:label) { create(:label, project: project) } context 'with @project set' do before do @@ -11,34 +11,31 @@ describe LabelsHelper do end it 'uses the instance variable' do - expect(label).not_to receive(:project) - link_to_label(label) + expect(link_to_label(label)).to match %r{.*} end end context 'without @project set' do it "uses the label's project" do - expect(label).to receive(:project).and_return(project) - link_to_label(label) + expect(link_to_label(label)).to match %r{.*} end end - context 'with a named project argument' do - it 'uses the provided project' do - arg = double('project') - expect(arg).to receive(:namespace).and_return('foo') - expect(arg).to receive(:to_param).and_return('foo') + context 'with a project argument' do + let(:another_project) { double('project', namespace: 'foo3', to_param: 'bar3') } - link_to_label(label, project: arg) + it 'links to merge requests page' do + expect(link_to_label(label, project: another_project)).to match %r{.*} end + end - it 'takes precedence over other types' do - @project = project - expect(@project).not_to receive(:namespace) - expect(label).not_to receive(:project) - - arg = double('project', namespace: 'foo', to_param: 'foo') - link_to_label(label, project: arg) + context 'with a type argument' do + ['issue', :issue, 'merge_request', :merge_request].each do |type| + context "set to #{type}" do + it 'links to correct page' do + expect(link_to_label(label, type: type)).to match %r{.*} + end + end end end @@ -66,5 +63,10 @@ describe LabelsHelper do it 'uses dark text on light backgrounds' do expect(text_color_for_bg('#EEEEEE')).to eq('#333333') end + + it 'supports RGB triplets' do + expect(text_color_for_bg('#FFF')).to eq '#333333' + expect(text_color_for_bg('#000')).to eq '#FFFFFF' + end end end diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index f0d553f5f1..601b6915e2 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -42,9 +42,9 @@ describe SearchHelper do expect(search_autocomplete_opts(project.name).size).to eq(1) end - it "includes the public group" do + it "should not include the public group" do group = create(:group) - expect(search_autocomplete_opts(group.name).size).to eq(1) + expect(search_autocomplete_opts(group.name).size).to eq(0) end context "with a current project" do diff --git a/spec/javascripts/fixtures/project_title.html.haml b/spec/javascripts/fixtures/project_title.html.haml new file mode 100644 index 0000000000..e5850b6265 --- /dev/null +++ b/spec/javascripts/fixtures/project_title.html.haml @@ -0,0 +1,7 @@ +%h1.title + %a + GitLab Org + %a.project-item-select-holder{href: "/gitlab-org/gitlab-test"} + GitLab Test + %input#project_path.project-item-select.js-projects-dropdown.ajax-project-select{type: "hidden", name: "project_path", "data-include-groups" => "false"} + %i.fa.chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/projects.json new file mode 100644 index 0000000000..84e8d0ba1e --- /dev/null +++ b/spec/javascripts/fixtures/projects.json @@ -0,0 +1 @@ +[{"id":9,"description":"","default_branch":null,"tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:root/test.git","http_url_to_repo":"http://localhost:3000/root/test.git","web_url":"http://localhost:3000/root/test","owner":{"name":"Administrator","username":"root","id":1,"state":"active","avatar_url":"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon","web_url":"http://localhost:3000/u/root"},"name":"test","name_with_namespace":"Administrator / test","path":"test","path_with_namespace":"root/test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-14T19:08:05.364Z","last_activity_at":"2016-01-14T19:08:07.418Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":1,"name":"root","path":"root","owner_id":1,"created_at":"2016-01-13T20:19:44.439Z","updated_at":"2016-01-13T20:19:44.439Z","description":"","avatar":null},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":0,"permissions":{"project_access":null,"group_access":null}},{"id":8,"description":"Voluptatem quae nulla eius numquam ullam voluptatibus quia modi.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:h5bp/html5-boilerplate.git","http_url_to_repo":"http://localhost:3000/h5bp/html5-boilerplate.git","web_url":"http://localhost:3000/h5bp/html5-boilerplate","name":"Html5 Boilerplate","name_with_namespace":"H5bp / Html5 Boilerplate","path":"html5-boilerplate","path_with_namespace":"h5bp/html5-boilerplate","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:57.525Z","last_activity_at":"2016-01-13T20:27:57.280Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":5,"name":"H5bp","path":"h5bp","owner_id":null,"created_at":"2016-01-13T20:19:57.239Z","updated_at":"2016-01-13T20:19:57.239Z","description":"Tempore accusantium possimus aut libero.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":10,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":7,"description":"Modi odio mollitia dolorem qui.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:twitter/typeahead-js.git","http_url_to_repo":"http://localhost:3000/twitter/typeahead-js.git","web_url":"http://localhost:3000/twitter/typeahead-js","name":"Typeahead.Js","name_with_namespace":"Twitter / Typeahead.Js","path":"typeahead-js","path_with_namespace":"twitter/typeahead-js","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:56.212Z","last_activity_at":"2016-01-13T20:27:51.496Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":6,"description":"Omnis asperiores ipsa et beatae quidem necessitatibus quia.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:twitter/flight.git","http_url_to_repo":"http://localhost:3000/twitter/flight.git","web_url":"http://localhost:3000/twitter/flight","name":"Flight","name_with_namespace":"Twitter / Flight","path":"flight","path_with_namespace":"twitter/flight","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:54.754Z","last_activity_at":"2016-01-13T20:27:50.502Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":5,"description":"Voluptatem commodi voluptate placeat architecto beatae illum dolores fugiat.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-test.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-test.git","web_url":"http://localhost:3000/gitlab-org/gitlab-test","name":"Gitlab Test","name_with_namespace":"Gitlab Org / Gitlab Test","path":"gitlab-test","path_with_namespace":"gitlab-org/gitlab-test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:53.202Z","last_activity_at":"2016-01-13T20:27:41.626Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":4,"description":"Aut molestias quas est ut aperiam officia quod libero.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-shell.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-shell.git","web_url":"http://localhost:3000/gitlab-org/gitlab-shell","name":"Gitlab Shell","name_with_namespace":"Gitlab Org / Gitlab Shell","path":"gitlab-shell","path_with_namespace":"gitlab-org/gitlab-shell","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:51.882Z","last_activity_at":"2016-01-13T20:27:35.678Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":20,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":3,"description":"Excepturi molestiae quia repellendus omnis est illo illum eligendi.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ci.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ci.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ci","name":"Gitlab Ci","name_with_namespace":"Gitlab Org / Gitlab Ci","path":"gitlab-ci","path_with_namespace":"gitlab-org/gitlab-ci","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:50.346Z","last_activity_at":"2016-01-13T20:27:30.115Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":3,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":2,"description":"Adipisci quaerat dignissimos enim sed ipsam dolorem quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":10,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ce.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ce.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ce","name":"Gitlab Ce","name_with_namespace":"Gitlab Org / Gitlab Ce","path":"gitlab-ce","path_with_namespace":"gitlab-org/gitlab-ce","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:49.065Z","last_activity_at":"2016-01-13T20:26:58.454Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":30,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":1,"description":"Vel voluptatem maxime saepe ex quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:documentcloud/underscore.git","http_url_to_repo":"http://localhost:3000/documentcloud/underscore.git","web_url":"http://localhost:3000/documentcloud/underscore","name":"Underscore","name_with_namespace":"Documentcloud / Underscore","path":"underscore","path_with_namespace":"documentcloud/underscore","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:45.862Z","last_activity_at":"2016-01-13T20:25:03.106Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":2,"name":"Documentcloud","path":"documentcloud","owner_id":null,"created_at":"2016-01-13T20:19:44.464Z","updated_at":"2016-01-13T20:19:44.464Z","description":"Aut impedit perferendis fuga et ipsa repellat cupiditate et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}}] diff --git a/spec/javascripts/project_title_spec.js.coffee b/spec/javascripts/project_title_spec.js.coffee new file mode 100644 index 0000000000..47c7b7febe --- /dev/null +++ b/spec/javascripts/project_title_spec.js.coffee @@ -0,0 +1,46 @@ +#= require select2 +#= require api +#= require project_select +#= require project + +window.gon = {} +window.gon.api_version = 'v3' + +describe 'Project Title', -> + fixture.preload('project_title.html') + fixture.preload('projects.json') + + beforeEach -> + fixture.load('project_title.html') + @project = new Project() + + spyOn(@project, 'changeProject').and.callFake (url) -> + window.current_project_url = url + + describe 'project list', -> + beforeEach => + @projects_data = fixture.load('projects.json')[0] + + spyOn(jQuery, 'ajax').and.callFake (req) => + expect(req.url).toBe('/api/v3/projects.json') + d = $.Deferred() + d.resolve @projects_data + d.promise() + + it 'to show on toggle click', => + $('.js-projects-dropdown-toggle').click() + + expect($('.title .select2-container').hasClass('select2-dropdown-open')).toBe(true) + expect($('.ajax-project-dropdown li').length).toBe(@projects_data.length) + + it 'hide dropdown', -> + $("#select2-drop-mask").click() + + expect($('.title .select2-container').hasClass('select2-dropdown-open')).toBe(false) + + it 'change project when clicking item', -> + $('.js-projects-dropdown-toggle').click() + $('.ajax-project-dropdown li:nth-child(2)').trigger('mouseup') + + expect($('.title .select2-container').hasClass('select2-dropdown-open')).toBe(false) + expect(window.current_project_url).toBe('http://localhost:3000/h5bp/html5-boilerplate') diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb index 473534ba68..63a32d9d45 100644 --- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb @@ -21,7 +21,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do let(:reference) { commit.id } # Let's test a variety of commit SHA sizes just to be paranoid - [6, 8, 12, 18, 20, 32, 40].each do |size| + [7, 8, 12, 18, 20, 32, 40].each do |size| it "links to a valid reference of #{size} characters" do doc = reference_filter("See #{reference[0...size]}") @@ -35,7 +35,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do doc = reference_filter("See #{commit.id}") expect(doc.text).to eq "See #{commit.short_id}" - doc = reference_filter("See #{commit.id[0...6]}") + doc = reference_filter("See #{commit.id[0...7]}") expect(doc.text).to eq "See #{commit.short_id}" end diff --git a/spec/lib/banzai/filter/emoji_filter_spec.rb b/spec/lib/banzai/filter/emoji_filter_spec.rb index cf31405815..b5b38cf0c8 100644 --- a/spec/lib/banzai/filter/emoji_filter_spec.rb +++ b/spec/lib/banzai/filter/emoji_filter_spec.rb @@ -14,7 +14,7 @@ describe Banzai::Filter::EmojiFilter, lib: true do it 'replaces supported emoji' do doc = filter('

:heart:

') - expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/2764.png' + expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/2764.png' end it 'ignores unsupported emoji' do @@ -25,7 +25,7 @@ describe Banzai::Filter::EmojiFilter, lib: true do it 'correctly encodes the URL' do doc = filter('

:+1:

') - expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/1F44D.png' + expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/1F44D.png' end it 'matches at the start of a string' do diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb index 760d60a419..e14a6dbf92 100644 --- a/spec/lib/banzai/filter/sanitization_filter_spec.rb +++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb @@ -75,6 +75,11 @@ describe Banzai::Filter::SanitizationFilter, lib: true do expect(filter(act).to_html).to eq exp end + it 'allows `abbr` elements' do + exp = act = %q{HTML} + expect(filter(act).to_html).to eq exp + end + it 'removes `rel` attribute from `a` elements' do act = %q{Link} exp = %q{Link} @@ -172,26 +177,4 @@ describe Banzai::Filter::SanitizationFilter, lib: true do expect(act.to_html).to eq exp end end - - context 'when inline_sanitization is true' do - it 'uses a stricter whitelist' do - doc = filter('

Description

', inline_sanitization: true) - expect(doc.to_html.strip).to eq 'Description' - end - - %w(pre code img ol ul li).each do |elem| - it "removes '#{elem}' elements" do - act = "<#{elem}>Description" - expect(filter(act, inline_sanitization: true).to_html.strip). - to eq 'Description' - end - end - - %w(b i strong em a ins del sup sub p).each do |elem| - it "still allows '#{elem}' elements" do - exp = act = "<#{elem}>Description" - expect(filter(act, inline_sanitization: true).to_html).to eq exp - end - end - end end diff --git a/spec/lib/banzai/pipeline/description_pipeline_spec.rb b/spec/lib/banzai/pipeline/description_pipeline_spec.rb new file mode 100644 index 0000000000..76f4207181 --- /dev/null +++ b/spec/lib/banzai/pipeline/description_pipeline_spec.rb @@ -0,0 +1,37 @@ +require 'rails_helper' + +describe Banzai::Pipeline::DescriptionPipeline do + def parse(html) + # When we pass HTML to Redcarpet, it gets wrapped in `p` tags... + # ...except when we pass it pre-wrapped text. Rabble rabble. + unwrap = !html.start_with?('

') + + output = described_class.to_html(html, project: spy) + + output.gsub!(%r{\A

(.*)

(.*)\z}, '\1\2') if unwrap + + output + end + + it 'uses a limited whitelist' do + doc = parse('# Description') + + expect(doc.strip).to eq 'Description' + end + + %w(pre code img ol ul li).each do |elem| + it "removes '#{elem}' elements" do + act = "<#{elem}>Description" + + expect(parse(act).strip).to eq 'Description' + end + end + + %w(b i strong em a ins del sup sub p).each do |elem| + it "still allows '#{elem}' elements" do + exp = act = "<#{elem}>Description" + + expect(parse(act).strip).to eq exp + end + end +end diff --git a/spec/lib/ci/status_spec.rb b/spec/lib/ci/status_spec.rb new file mode 100644 index 0000000000..a2eb14f3a9 --- /dev/null +++ b/spec/lib/ci/status_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe Ci::Status do + describe '.get_status' do + subject { described_class.get_status(statuses) } + + [:ci_build, :generic_commit_status].each do |type| + context "for #{type}" do + context 'all successful' do + let(:statuses) { Array.new(2) { create(type, status: :success) } } + it { is_expected.to eq 'success' } + end + + context 'at least one failed' do + let(:statuses) { [create(type, status: :success), create(type, status: :failed)] } + it { is_expected.to eq 'failed' } + end + + context 'at least one running' do + let(:statuses) { [create(type, status: :success), create(type, status: :running)] } + it { is_expected.to eq 'running' } + end + + context 'at least one pending' do + let(:statuses) { [create(type, status: :success), create(type, status: :pending)] } + it { is_expected.to eq 'running' } + end + + context 'success and failed but allowed to fail' do + let(:statuses) { [create(type, status: :success), create(type, status: :failed, allow_failure: true)] } + it { is_expected.to eq 'success' } + end + + context 'one failed but allowed to fail' do + let(:statuses) { [create(type, status: :failed, allow_failure: true)] } + it { is_expected.to eq 'success' } + end + end + end + end +end diff --git a/spec/lib/dnsxl_check_spec.rb b/spec/lib/dnsxl_check_spec.rb deleted file mode 100644 index a35a1be0c9..0000000000 --- a/spec/lib/dnsxl_check_spec.rb +++ /dev/null @@ -1,68 +0,0 @@ -require 'spec_helper' -require 'ostruct' - -describe 'DNSXLCheck', lib: true, no_db: true do - let(:spam_ip) { '127.0.0.2' } - let(:no_spam_ip) { '127.0.0.3' } - let(:invalid_ip) { 'a.b.c.d' } - let!(:dnsxl_check) { DNSXLCheck.create_from_list([OpenStruct.new({ domain: 'test', weight: 1 })]) } - - before(:context) do - class DNSXLCheck::Resolver - class << self - alias_method :old_search, :search - def search(query) - return false if query.match(/always\.failing\.domain\z/) - return true if query.match(/\A2\.0\.0\.127\./) - return false if query.match(/\A3\.0\.0\.127\./) - end - end - end - end - - describe '#test' do - before do - dnsxl_check.threshold = 0.75 - dnsxl_check.add_list('always.failing.domain', 1) - end - - context 'when threshold is used' do - before { dnsxl_check.use_threshold= true } - - it { expect(dnsxl_check.test(spam_ip)).to be_falsey } - end - - context 'when threshold is not used' do - before { dnsxl_check.use_threshold= false } - - it { expect(dnsxl_check.test(spam_ip)).to be_truthy } - end - end - - describe '#test_with_threshold' do - it { expect{ dnsxl_check.test_with_threshold(invalid_ip) }.to raise_error(ArgumentError) } - - it { expect(dnsxl_check.test_with_threshold(spam_ip)).to be_truthy } - it { expect(dnsxl_check.test_with_threshold(no_spam_ip)).to be_falsey } - end - - describe '#test_strict' do - before do - dnsxl_check.threshold = 1 - dnsxl_check.add_list('always.failing.domain', 1) - end - - it { expect{ dnsxl_check.test_strict(invalid_ip) }.to raise_error(ArgumentError) } - - it { expect(dnsxl_check.test_with_threshold(spam_ip)).to be_falsey } - it { expect(dnsxl_check.test_with_threshold(no_spam_ip)).to be_falsey } - it { expect(dnsxl_check.test_strict(spam_ip)).to be_truthy } - it { expect(dnsxl_check.test_strict(no_spam_ip)).to be_falsey } - end - - describe '#threshold=' do - it { expect{ dnsxl_check.threshold = 0 }.to raise_error(ArgumentError) } - it { expect{ dnsxl_check.threshold = 1.1 }.to raise_error(ArgumentError) } - it { expect{ dnsxl_check.threshold = 0.5 }.not_to raise_error } - end -end diff --git a/spec/lib/gitlab/akismet_helper_spec.rb b/spec/lib/gitlab/akismet_helper_spec.rb new file mode 100644 index 0000000000..9858935180 --- /dev/null +++ b/spec/lib/gitlab/akismet_helper_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Gitlab::AkismetHelper, type: :helper do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url)) + current_application_settings.akismet_enabled = true + current_application_settings.akismet_api_key = '12345' + end + + describe '#check_for_spam?' do + it 'returns true for non-member' do + expect(helper.check_for_spam?(project, user)).to eq(true) + end + + it 'returns false for member' do + project.team << [user, :guest] + expect(helper.check_for_spam?(project, user)).to eq(false) + end + end + + describe '#is_spam?' do + it 'returns true for spam' do + environment = { + 'REMOTE_ADDR' => '127.0.0.1', + 'HTTP_USER_AGENT' => 'Test User Agent' + } + + allow_any_instance_of(::Akismet::Client).to receive(:check).and_return([true, true]) + expect(helper.is_spam?(environment, user, 'Is this spam?')).to eq(true) + end + end +end diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index 6beb21c6d2..736bf78720 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -42,22 +42,6 @@ module Gitlab end end - context "with project in context" do - - let(:context) { { project: create(:project) } } - - it "should filter converted input via HTML pipeline and return result" do - filtered_html = 'ASCII' - - allow(Asciidoctor).to receive(:convert).and_return(html) - expect(Banzai).to receive(:render) - .with(html, context.merge(pipeline: :asciidoc)) - .and_return(filtered_html) - - expect( render('foo', context) ).to eql filtered_html - end - end - def render(*args) described_class.render(*args) end diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index 99288da1e4..04cf11fc6f 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -135,6 +135,17 @@ describe Gitlab::ClosingIssueExtractor, lib: true do message = "resolve #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + + context 'with an external issue tracker reference' do + it 'extracts the referenced issue' do + jira_project = create(:jira_project, name: 'JIRA_EXT1') + jira_issue = ExternalIssue.new("#{jira_project.name}-1", project: jira_project) + closing_issue_extractor = described_class.new jira_project + message = "Resolve #{jira_issue.to_reference}" + + expect(closing_issue_extractor.closed_by_message(message)).to eq([jira_issue]) + end + end end context "with a cross-project reference" do diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 8461e8ce50..d0a447753b 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -1,5 +1,9 @@ require 'spec_helper' +class MigrationTest + include Gitlab::Database +end + describe Gitlab::Database, lib: true do # These are just simple smoke tests to check if the methods work (regardless # of what they may return). @@ -14,4 +18,52 @@ describe Gitlab::Database, lib: true do it { is_expected.to satisfy { |val| val == true || val == false } } end + + describe '.version' do + context "on mysql" do + it "extracts the version number" do + allow(described_class).to receive(:database_version). + and_return("5.7.12-standard") + + expect(described_class.version).to eq '5.7.12-standard' + end + end + + context "on postgresql" do + it "extracts the version number" do + allow(described_class).to receive(:database_version). + and_return("PostgreSQL 9.4.4 on x86_64-apple-darwin14.3.0") + + expect(described_class.version).to eq '9.4.4' + end + end + end + + describe '#true_value' do + it 'returns correct value for PostgreSQL' do + expect(described_class).to receive(:postgresql?).and_return(true) + + expect(MigrationTest.new.true_value).to eq "'t'" + end + + it 'returns correct value for MySQL' do + expect(described_class).to receive(:postgresql?).and_return(false) + + expect(MigrationTest.new.true_value).to eq 1 + end + end + + describe '#false_value' do + it 'returns correct value for PostgreSQL' do + expect(described_class).to receive(:postgresql?).and_return(true) + + expect(MigrationTest.new.false_value).to eq "'f'" + end + + it 'returns correct value for MySQL' do + expect(described_class).to receive(:postgresql?).and_return(false) + + expect(MigrationTest.new.false_value).to eq 0 + end + end end diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index b84a57f357..d19bf4ac84 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -9,33 +9,69 @@ describe Gitlab::Diff::Highlight, lib: true do let(:diff_file) { Gitlab::Diff::File.new(diff, [commit.parent, commit]) } describe '#highlight' do - let(:diff_lines) { Gitlab::Diff::Highlight.new(diff_file).highlight } + context "with a diff file" do + let(:subject) { Gitlab::Diff::Highlight.new(diff_file).highlight } - it 'should return Gitlab::Diff::Line elements' do - expect(diff_lines.first).to be_an_instance_of(Gitlab::Diff::Line) + it 'should return Gitlab::Diff::Line elements' do + expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line) + end + + it 'should not modify "match" lines' do + expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') + expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') + end + + it 'highlights and marks unchanged lines' do + code = %Q{ def popen(cmd, path=nil)\n} + + expect(subject[2].text).to eq(code) + end + + it 'highlights and marks removed lines' do + code = %Q{- raise "System commands must be given as an array of strings"\n} + + expect(subject[4].text).to eq(code) + end + + it 'highlights and marks added lines' do + code = %Q{+ raise RuntimeError, "System commands must be given as an array of strings"\n} + + expect(subject[5].text).to eq(code) + end end - it 'should not modify "match" lines' do - expect(diff_lines[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') - expect(diff_lines[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') - end + context "with diff lines" do + let(:subject) { Gitlab::Diff::Highlight.new(diff_file.diff_lines).highlight } - it 'should highlight unchanged lines' do - code = %Q{ def popen(cmd, path=nil)\n} + it 'should return Gitlab::Diff::Line elements' do + expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line) + end - expect(diff_lines[2].text).to eq(code) - end + it 'should not modify "match" lines' do + expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') + expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') + end - it 'should highlight removed lines' do - code = %Q{- raise "System commands must be given as an array of strings"\n} + it 'marks unchanged lines' do + code = %Q{ def popen(cmd, path=nil)} - expect(diff_lines[4].text).to eq(code) - end + expect(subject[2].text).to eq(code) + expect(subject[2].text).not_to be_html_safe + end - it 'should highlight added lines' do - code = %Q{+ raise RuntimeError, "System commands must be given as an array of strings"\n} + it 'marks removed lines' do + code = %Q{- raise "System commands must be given as an array of strings"} - expect(diff_lines[5].text).to eq(code) + expect(subject[4].text).to eq(code) + expect(subject[4].text).not_to be_html_safe + end + + it 'marks added lines' do + code = %Q{+ raise RuntimeError, "System commands must be given as an array of strings"} + + expect(subject[5].text).to eq(code) + expect(subject[5].text).to be_html_safe + end end end end diff --git a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb index 6f3276a8b5..ea5c31011f 100644 --- a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb +++ b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb @@ -2,14 +2,28 @@ require 'spec_helper' describe Gitlab::Diff::InlineDiffMarker, lib: true do describe '#inline_diffs' do - let(:raw) { "abc 'def'" } - let(:rich) { %{abc 'def'} } - let(:inline_diffs) { [2..5] } - let(:subject) { Gitlab::Diff::InlineDiffMarker.new(raw, rich).mark(inline_diffs) } + context "when the rich text is html safe" do + let(:raw) { "abc 'def'" } + let(:rich) { %{abc 'def'}.html_safe } + let(:inline_diffs) { [2..5] } + let(:subject) { Gitlab::Diff::InlineDiffMarker.new(raw, rich).mark(inline_diffs) } - it 'marks the inline diffs' do - expect(subject).to eq(%{abc 'def'}) + it 'marks the inline diffs' do + expect(subject).to eq(%{abc 'def'}) + expect(subject).to be_html_safe + end + end + + context "when the text text is not html safe" do + let(:raw) { "abc 'def'" } + let(:inline_diffs) { [2..5] } + let(:subject) { Gitlab::Diff::InlineDiffMarker.new(raw).mark(inline_diffs) } + + it 'marks the inline diffs' do + expect(subject).to eq(%{abc 'def'}) + expect(subject).to be_html_safe + end end end end diff --git a/spec/lib/gitlab/diff/inline_diff_spec.rb b/spec/lib/gitlab/diff/inline_diff_spec.rb index 056917df89..95a993d26c 100644 --- a/spec/lib/gitlab/diff/inline_diff_spec.rb +++ b/spec/lib/gitlab/diff/inline_diff_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Diff::InlineDiff, lib: true do - describe '#inline_diffs' do + describe '.for_lines' do let(:diff) do < issue.hook_attrs['updated_at'] end + + include_examples 'project hook data' + include_examples 'deprecated repository hook data' end describe 'When asking for a note on merge request' do let(:merge_request) { create(:merge_request, created_at: fixed_time, updated_at: fixed_time) } - let(:note) { create(:note_on_merge_request, noteable_id: merge_request.id) } + let(:note) { create(:note_on_merge_request, noteable_id: merge_request.id, project: project) } it 'returns the note and merge request data' do expect(data).to have_key(:merge_request) - expect(data[:merge_request]).to eq(merge_request.hook_attrs) + expect(data[:merge_request].except('updated_at')).to eq(merge_request.hook_attrs.except('updated_at')) + expect(data[:merge_request]['updated_at']).to be > merge_request.hook_attrs['updated_at'] end + + include_examples 'project hook data' + include_examples 'deprecated repository hook data' end describe 'When asking for a note on merge request diff' do let(:merge_request) { create(:merge_request, created_at: fixed_time, updated_at: fixed_time) } - let(:note) { create(:note_on_merge_request_diff, noteable_id: merge_request.id) } + let(:note) { create(:note_on_merge_request_diff, noteable_id: merge_request.id, project: project) } it 'returns the note and merge request diff data' do expect(data).to have_key(:merge_request) - expect(data[:merge_request]).to eq(merge_request.hook_attrs) + expect(data[:merge_request].except('updated_at')).to eq(merge_request.hook_attrs.except('updated_at')) + expect(data[:merge_request]['updated_at']).to be > merge_request.hook_attrs['updated_at'] end + + include_examples 'project hook data' + include_examples 'deprecated repository hook data' end describe 'When asking for a note on project snippet' do let!(:snippet) { create(:project_snippet, created_at: fixed_time, updated_at: fixed_time) } - let!(:note) { create(:note_on_project_snippet, noteable_id: snippet.id) } + let!(:note) { create(:note_on_project_snippet, noteable_id: snippet.id, project: project) } it 'returns the note and project snippet data' do expect(data).to have_key(:snippet) - expect(data[:snippet]).to eq(snippet.hook_attrs) + expect(data[:snippet].except('updated_at')).to eq(snippet.hook_attrs.except('updated_at')) + expect(data[:snippet]['updated_at']).to be > snippet.hook_attrs['updated_at'] end + + include_examples 'project hook data' + include_examples 'deprecated repository hook data' end end diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 925bc442a9..3a769acfdc 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -41,7 +41,20 @@ describe Gitlab::OAuth::User, lib: true do describe 'signup' do shared_examples "to verify compliance with allow_single_sign_on" do - context "with allow_single_sign_on enabled" do + context "with new allow_single_sign_on enabled syntax" do + before { stub_omniauth_config(allow_single_sign_on: ['twitter']) } + + it "creates a user from Omniauth" do + oauth_user.save + + expect(gl_user).to be_valid + identity = gl_user.identities.first + expect(identity.extern_uid).to eql uid + expect(identity.provider).to eql 'twitter' + end + end + + context "with old allow_single_sign_on enabled syntax" do before { stub_omniauth_config(allow_single_sign_on: true) } it "creates a user from Omniauth" do @@ -54,7 +67,14 @@ describe Gitlab::OAuth::User, lib: true do end end - context "with allow_single_sign_on disabled (Default)" do + context "with new allow_single_sign_on disabled syntax" do + before { stub_omniauth_config(allow_single_sign_on: []) } + it "throws an error" do + expect{ oauth_user.save }.to raise_error StandardError + end + end + + context "with old allow_single_sign_on disabled (Default)" do before { stub_omniauth_config(allow_single_sign_on: false) } it "throws an error" do expect{ oauth_user.save }.to raise_error StandardError @@ -135,7 +155,7 @@ describe Gitlab::OAuth::User, lib: true do describe 'blocking' do let(:provider) { 'twitter' } - before { stub_omniauth_config(allow_single_sign_on: true) } + before { stub_omniauth_config(allow_single_sign_on: ['twitter']) } context 'signup with omniauth only' do context 'dont block on create' do diff --git a/spec/lib/gitlab/push_data_builder_spec.rb b/spec/lib/gitlab/push_data_builder_spec.rb index 3ef6168539..961022b9d1 100644 --- a/spec/lib/gitlab/push_data_builder_spec.rb +++ b/spec/lib/gitlab/push_data_builder_spec.rb @@ -1,34 +1,32 @@ require 'spec_helper' -describe 'Gitlab::PushDataBuilder', lib: true do +describe Gitlab::PushDataBuilder, lib: true do let(:project) { create(:project) } let(:user) { create(:user) } - describe :build_sample do - let(:data) { Gitlab::PushDataBuilder.build_sample(project, user) } + describe '.build_sample' do + let(:data) { described_class.build_sample(project, user) } it { expect(data).to be_a(Hash) } it { expect(data[:before]).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } it { expect(data[:after]).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') } it { expect(data[:ref]).to eq('refs/heads/master') } it { expect(data[:commits].size).to eq(3) } - it { expect(data[:repository][:git_http_url]).to eq(project.http_url_to_repo) } - it { expect(data[:repository][:git_ssh_url]).to eq(project.ssh_url_to_repo) } - it { expect(data[:repository][:visibility_level]).to eq(project.visibility_level) } it { expect(data[:total_commits_count]).to eq(3) } it { expect(data[:commits].first[:added]).to eq(["gitlab-grack"]) } it { expect(data[:commits].first[:modified]).to eq([".gitmodules"]) } it { expect(data[:commits].first[:removed]).to eq([]) } + + include_examples 'project hook data' + include_examples 'deprecated repository hook data' end - describe :build do + describe '.build' do let(:data) do - Gitlab::PushDataBuilder.build(project, - user, - Gitlab::Git::BLANK_SHA, - '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b', - 'refs/tags/v1.1.0') + described_class.build(project, user, Gitlab::Git::BLANK_SHA, + '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b', + 'refs/tags/v1.1.0') end it { expect(data).to be_a(Hash) } @@ -38,5 +36,10 @@ describe 'Gitlab::PushDataBuilder', lib: true do it { expect(data[:ref]).to eq('refs/tags/v1.1.0') } it { expect(data[:commits]).to be_empty } it { expect(data[:total_commits_count]).to be_zero } + + it 'does not raise an error when given nil commits' do + expect { described_class.build(spy, spy, spy, spy, spy, nil) }. + not_to raise_error + end end end diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index d67ee423b9..c51b10bdc6 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -21,4 +21,12 @@ describe Gitlab::Regex, lib: true do it { expect('Dash – is this').to match(Gitlab::Regex.project_name_regex) } it { expect('?gitlab').not_to match(Gitlab::Regex.project_name_regex) } end + + describe 'file name regex' do + it { expect('foo@bar').to match(Gitlab::Regex.file_name_regex) } + end + + describe 'file path regex' do + it { expect('foo@/bar').to match(Gitlab::Regex.file_path_regex) } + end end diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb new file mode 100644 index 0000000000..de7cd99d49 --- /dev/null +++ b/spec/lib/gitlab/saml/user_spec.rb @@ -0,0 +1,271 @@ +require 'spec_helper' + +describe Gitlab::Saml::User, lib: true do + let(:saml_user) { described_class.new(auth_hash) } + let(:gl_user) { saml_user.gl_user } + let(:uid) { 'my-uid' } + let(:provider) { 'saml' } + let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash) } + let(:info_hash) do + { + name: 'John', + email: 'john@mail.com' + } + end + let(:ldap_user) { Gitlab::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') } + + describe '#save' do + def stub_omniauth_config(messages) + allow(Gitlab.config.omniauth).to receive_messages(messages) + end + + def stub_ldap_config(messages) + allow(Gitlab::LDAP::Config).to receive_messages(messages) + end + + describe 'account exists on server' do + before { stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) } + context 'and should bind with SAML' do + let!(:existing_user) { create(:user, email: 'john@mail.com', username: 'john') } + it 'adds the SAML identity to the existing user' do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user).to eq existing_user + identity = gl_user.identities.first + expect(identity.extern_uid).to eql uid + expect(identity.provider).to eql 'saml' + end + end + end + + describe 'no account exists on server' do + shared_examples 'to verify compliance with allow_single_sign_on' do + context 'with allow_single_sign_on enabled' do + before { stub_omniauth_config(allow_single_sign_on: ['saml']) } + + it 'creates a user from SAML' do + saml_user.save + + expect(gl_user).to be_valid + identity = gl_user.identities.first + expect(identity.extern_uid).to eql uid + expect(identity.provider).to eql 'saml' + end + end + + context 'with allow_single_sign_on default (["saml"])' do + before { stub_omniauth_config(allow_single_sign_on: ['saml']) } + it 'should not throw an error' do + expect{ saml_user.save }.not_to raise_error + end + end + + context 'with allow_single_sign_on disabled' do + before { stub_omniauth_config(allow_single_sign_on: false) } + it 'should throw an error' do + expect{ saml_user.save }.to raise_error StandardError + end + end + end + + context 'with auto_link_ldap_user disabled (default)' do + before { stub_omniauth_config({ auto_link_ldap_user: false, auto_link_saml_user: false, allow_single_sign_on: ['saml'] }) } + include_examples 'to verify compliance with allow_single_sign_on' + end + + context 'with auto_link_ldap_user enabled' do + before { stub_omniauth_config({ auto_link_ldap_user: true, auto_link_saml_user: false }) } + + context 'and no LDAP provider defined' do + before { stub_ldap_config(providers: []) } + + include_examples 'to verify compliance with allow_single_sign_on' + end + + context 'and at least one LDAP provider is defined' do + before { stub_ldap_config(providers: %w(ldapmain)) } + + context 'and a corresponding LDAP person' do + before do + allow(ldap_user).to receive(:uid) { uid } + allow(ldap_user).to receive(:username) { uid } + allow(ldap_user).to receive(:email) { ['johndoe@example.com','john2@example.com'] } + allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' } + allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) + end + + context 'and no account for the LDAP user' do + + it 'creates a user with dual LDAP and SAML identities' do + saml_user.save + + expect(gl_user).to be_valid + expect(gl_user.username).to eql uid + expect(gl_user.email).to eql 'johndoe@example.com' + expect(gl_user.identities.length).to eql 2 + identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } + expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + { provider: 'saml', extern_uid: uid } + ]) + end + end + + context 'and LDAP user has an account already' do + let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') } + it "adds the omniauth identity to the LDAP account" do + saml_user.save + + expect(gl_user).to be_valid + expect(gl_user.username).to eql 'john' + expect(gl_user.email).to eql 'john@example.com' + expect(gl_user.identities.length).to eql 2 + identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } + expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + { provider: 'saml', extern_uid: uid } + ]) + end + end + end + + context 'and no corresponding LDAP person' do + before { allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil) } + + include_examples 'to verify compliance with allow_single_sign_on' + end + end + end + + end + + describe 'blocking' do + before { stub_omniauth_config({ allow_saml_sign_up: true, auto_link_saml_user: true }) } + + context 'signup with SAML only' do + context 'dont block on create' do + before { stub_omniauth_config(block_auto_created_users: false) } + + it 'should not block the user' do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'block on create' do + before { stub_omniauth_config(block_auto_created_users: true) } + + it 'should block user' do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user).to be_blocked + end + end + end + + context 'signup with linked omniauth and LDAP account' do + before do + stub_omniauth_config(auto_link_ldap_user: true) + allow(ldap_user).to receive(:uid) { uid } + allow(ldap_user).to receive(:username) { uid } + allow(ldap_user).to receive(:email) { ['johndoe@example.com','john2@example.com'] } + allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' } + allow(saml_user).to receive(:ldap_person).and_return(ldap_user) + end + + context "and no account for the LDAP user" do + context 'dont block on create (LDAP)' do + before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) } + + it do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'block on create (LDAP)' do + before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) } + + it do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user).to be_blocked + end + end + end + + context 'and LDAP user has an account already' do + let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') } + + context 'dont block on create (LDAP)' do + before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) } + + it do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'block on create (LDAP)' do + before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) } + + it do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + end + end + + + context 'sign-in' do + before do + saml_user.save + saml_user.gl_user.activate + end + + context 'dont block on create' do + before { stub_omniauth_config(block_auto_created_users: false) } + + it do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'block on create' do + before { stub_omniauth_config(block_auto_created_users: true) } + + it do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'dont block on create (LDAP)' do + before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) } + + it do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'block on create (LDAP)' do + before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) } + + it do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + end + end + end +end diff --git a/spec/mailers/emails/builds_spec.rb b/spec/mailers/emails/builds_spec.rb new file mode 100644 index 0000000000..0df89938e9 --- /dev/null +++ b/spec/mailers/emails/builds_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' +require 'email_spec' +require 'mailers/shared/notify' + +describe Notify do + include EmailSpec::Matchers + + include_context 'gitlab email notification' + + describe 'build notification email' do + let(:build) { create(:ci_build) } + let(:project) { build.project } + + shared_examples 'build email' do + it 'contains name of project' do + is_expected.to have_body_text build.project_name + end + + it 'contains link to project' do + is_expected.to have_body_text namespace_project_path(project.namespace, project) + end + end + + shared_examples 'an email with X-GitLab headers containing build details' do + it 'has X-GitLab-Build* headers' do + is_expected.to have_header 'X-GitLab-Build-Id', /#{build.id}/ + is_expected.to have_header 'X-GitLab-Build-Ref', /#{build.ref}/ + end + end + + describe 'build success' do + subject { Notify.build_success_email(build.id, 'wow@example.com') } + before { build.success } + + it_behaves_like 'build email' + it_behaves_like 'an email with X-GitLab headers containing build details' + it_behaves_like 'an email with X-GitLab headers containing project details' + + it 'has header indicating build status' do + is_expected.to have_header 'X-GitLab-Build-Status', 'success' + end + + it 'has the correct subject' do + is_expected.to have_subject /Build success for/ + end + end + + describe 'build fail' do + subject { Notify.build_fail_email(build.id, 'wow@example.com') } + before { build.drop } + + it_behaves_like 'build email' + it_behaves_like 'an email with X-GitLab headers containing build details' + it_behaves_like 'an email with X-GitLab headers containing project details' + + it 'has header indicating build status' do + is_expected.to have_header 'X-GitLab-Build-Status', 'failed' + end + + it 'has the correct subject' do + is_expected.to have_subject /Build failed for/ + end + end + end +end diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb new file mode 100644 index 0000000000..5b575da34f --- /dev/null +++ b/spec/mailers/emails/profile_spec.rb @@ -0,0 +1,107 @@ +require 'spec_helper' +require 'email_spec' +require 'mailers/shared/notify' + +describe Notify do + include EmailSpec::Matchers + include_context 'gitlab email notification' + + describe 'profile notifications' do + describe 'for new users, the email' do + let(:example_site_path) { root_path } + let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) } + let(:token) { 'kETLwRaayvigPq_x3SNM' } + + subject { Notify.new_user_email(new_user.id, token) } + + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'a new user email' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like 'a user cannot unsubscribe through footer link' + + it 'contains the password text' do + is_expected.to have_body_text /Click here to set your password/ + end + + it 'includes a link for user to set password' do + params = "reset_password_token=#{token}" + is_expected.to have_body_text( + %r{http://localhost(:\d+)?/users/password/edit\?#{params}} + ) + end + + it 'explains the reset link expiration' do + is_expected.to have_body_text(/This link is valid for \d+ (hours?|days?)/) + is_expected.to have_body_text(new_user_password_url) + is_expected.to have_body_text(/\?user_email=.*%40.*/) + end + end + + describe 'for users that signed up, the email' do + let(:example_site_path) { root_path } + let(:new_user) { create(:user, email: new_user_address, password: "securePassword") } + + subject { Notify.new_user_email(new_user.id) } + + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'a new user email' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like 'a user cannot unsubscribe through footer link' + + it 'should not contain the new user\'s password' do + is_expected.not_to have_body_text /password/ + end + end + + describe 'user added ssh key' do + let(:key) { create(:personal_key) } + + subject { Notify.new_ssh_key_email(key.id) } + + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like 'a user cannot unsubscribe through footer link' + + it 'is sent to the new user' do + is_expected.to deliver_to key.user.email + end + + it 'has the correct subject' do + is_expected.to have_subject /^SSH key was added to your account$/i + end + + it 'contains the new ssh key title' do + is_expected.to have_body_text /#{key.title}/ + end + + it 'includes a link to ssh keys page' do + is_expected.to have_body_text /#{profile_keys_path}/ + end + end + + describe 'user added email' do + let(:email) { create(:email) } + + subject { Notify.new_email_email(email.id) } + + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like 'a user cannot unsubscribe through footer link' + + it 'is sent to the new user' do + is_expected.to deliver_to email.user.email + end + + it 'has the correct subject' do + is_expected.to have_subject /^Email was added to your account$/i + end + + it 'contains the new email address' do + is_expected.to have_body_text /#{email.email}/ + end + + it 'includes a link to emails page' do + is_expected.to have_body_text /#{profile_emails_path}/ + end + end + end +end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 7289e596ef..232a11245a 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -1,237 +1,13 @@ require 'spec_helper' require 'email_spec' +require 'mailers/shared/notify' describe Notify do include EmailSpec::Helpers include EmailSpec::Matchers include RepoHelpers - new_user_address = 'newguy@example.com' - - let(:gitlab_sender_display_name) { Gitlab.config.gitlab.email_display_name } - let(:gitlab_sender) { Gitlab.config.gitlab.email_from } - let(:gitlab_sender_reply_to) { Gitlab.config.gitlab.email_reply_to } - let(:recipient) { create(:user, email: 'recipient@example.com') } - let(:project) { create(:project) } - let(:build) { create(:ci_build) } - - before(:each) do - ActionMailer::Base.deliveries.clear - email = recipient.emails.create(email: "notifications@example.com") - recipient.update_attribute(:notification_email, email.email) - end - - shared_examples 'a multiple recipients email' do - it 'is sent to the given recipient' do - is_expected.to deliver_to recipient.notification_email - end - end - - shared_examples 'an email sent from GitLab' do - it 'is sent from GitLab' do - sender = subject.header[:from].addrs[0] - expect(sender.display_name).to eq(gitlab_sender_display_name) - expect(sender.address).to eq(gitlab_sender) - end - - it 'has a Reply-To address' do - reply_to = subject.header[:reply_to].addresses - expect(reply_to).to eq([gitlab_sender_reply_to]) - end - end - - shared_examples 'an email with X-GitLab headers containing project details' do - it 'has X-GitLab-Project* headers' do - is_expected.to have_header 'X-GitLab-Project', /#{project.name}/ - is_expected.to have_header 'X-GitLab-Project-Id', /#{project.id}/ - is_expected.to have_header 'X-GitLab-Project-Path', /#{project.path_with_namespace}/ - end - end - - shared_examples 'an email with X-GitLab headers containing build details' do - it 'has X-GitLab-Build* headers' do - is_expected.to have_header 'X-GitLab-Build-Id', /#{build.id}/ - is_expected.to have_header 'X-GitLab-Build-Ref', /#{build.ref}/ - end - end - - shared_examples 'an email that contains a header with author username' do - it 'has X-GitLab-Author header containing author\'s username' do - is_expected.to have_header 'X-GitLab-Author', user.username - end - end - - shared_examples 'an email starting a new thread' do |message_id_prefix| - include_examples 'an email with X-GitLab headers containing project details' - - it 'has a discussion identifier' do - is_expected.to have_header 'Message-ID', /<#{message_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ - end - end - - shared_examples 'an answer to an existing thread' do |thread_id_prefix| - include_examples 'an email with X-GitLab headers containing project details' - - it 'has a subject that begins with Re: ' do - is_expected.to have_subject /^Re: / - end - - it 'has headers that reference an existing thread' do - is_expected.to have_header 'Message-ID', /<(.*)@#{Gitlab.config.gitlab.host}>/ - is_expected.to have_header 'References', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ - is_expected.to have_header 'In-Reply-To', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ - end - end - - shared_examples 'a new user email' do |user_email, site_path| - it 'is sent to the new user' do - is_expected.to deliver_to user_email - end - - it 'has the correct subject' do - is_expected.to have_subject /^Account was created for you$/i - end - - it 'contains the new user\'s login name' do - is_expected.to have_body_text /#{user_email}/ - end - - it 'includes a link to the site' do - is_expected.to have_body_text /#{site_path}/ - end - end - - shared_examples 'it should have Gmail Actions links' do - it { is_expected.to have_body_text /ViewAction/ } - end - - shared_examples 'it should not have Gmail Actions links' do - it { is_expected.to_not have_body_text /ViewAction/ } - end - - shared_examples 'it should show Gmail Actions View Issue link' do - it_behaves_like 'it should have Gmail Actions links' - - it { is_expected.to have_body_text /View Issue/ } - end - - shared_examples 'it should show Gmail Actions View Merge request link' do - it_behaves_like 'it should have Gmail Actions links' - - it { is_expected.to have_body_text /View Merge request/ } - end - - shared_examples 'it should show Gmail Actions View Commit link' do - it_behaves_like 'it should have Gmail Actions links' - - it { is_expected.to have_body_text /View Commit/ } - end - - shared_examples 'an unsubscribeable thread' do - it { is_expected.to have_body_text /unsubscribe/ } - end - - shared_examples "a user cannot unsubscribe through footer link" do - it { is_expected.not_to have_body_text /unsubscribe/ } - end - - describe 'for new users, the email' do - let(:example_site_path) { root_path } - let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) } - - token = 'kETLwRaayvigPq_x3SNM' - - subject { Notify.new_user_email(new_user.id, token) } - - it_behaves_like 'an email sent from GitLab' - it_behaves_like 'a new user email', new_user_address - it_behaves_like 'it should not have Gmail Actions links' - it_behaves_like 'a user cannot unsubscribe through footer link' - - it 'contains the password text' do - is_expected.to have_body_text /Click here to set your password/ - end - - it 'includes a link for user to set password' do - params = "reset_password_token=#{token}" - is_expected.to have_body_text( - %r{http://localhost(:\d+)?/users/password/edit\?#{params}} - ) - end - - it 'explains the reset link expiration' do - is_expected.to have_body_text(/This link is valid for \d+ (hours?|days?)/) - is_expected.to have_body_text(new_user_password_url) - is_expected.to have_body_text(/\?user_email=.*%40.*/) - end - end - - describe 'for users that signed up, the email' do - let(:example_site_path) { root_path } - let(:new_user) { create(:user, email: new_user_address, password: "securePassword") } - - subject { Notify.new_user_email(new_user.id) } - - it_behaves_like 'an email sent from GitLab' - it_behaves_like 'a new user email', new_user_address - it_behaves_like 'it should not have Gmail Actions links' - it_behaves_like 'a user cannot unsubscribe through footer link' - - it 'should not contain the new user\'s password' do - is_expected.not_to have_body_text /password/ - end - end - - describe 'user added ssh key' do - let(:key) { create(:personal_key) } - - subject { Notify.new_ssh_key_email(key.id) } - - it_behaves_like 'an email sent from GitLab' - it_behaves_like 'it should not have Gmail Actions links' - it_behaves_like 'a user cannot unsubscribe through footer link' - - it 'is sent to the new user' do - is_expected.to deliver_to key.user.email - end - - it 'has the correct subject' do - is_expected.to have_subject /^SSH key was added to your account$/i - end - - it 'contains the new ssh key title' do - is_expected.to have_body_text /#{key.title}/ - end - - it 'includes a link to ssh keys page' do - is_expected.to have_body_text /#{profile_keys_path}/ - end - end - - describe 'user added email' do - let(:email) { create(:email) } - - subject { Notify.new_email_email(email.id) } - - it_behaves_like 'it should not have Gmail Actions links' - it_behaves_like 'a user cannot unsubscribe through footer link' - - it 'is sent to the new user' do - is_expected.to deliver_to email.user.email - end - - it 'has the correct subject' do - is_expected.to have_subject /^Email was added to your account$/i - end - - it 'contains the new email address' do - is_expected.to have_body_text /#{email.email}/ - end - - it 'includes a link to emails page' do - is_expected.to have_body_text /#{profile_emails_path}/ - end - end + include_context 'gitlab email notification' context 'for a project' do describe 'items that are assignable, the email' do @@ -270,6 +46,17 @@ describe Notify do it 'contains a link to the new issue' do is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ end + + context 'when enabled email_author_in_body' do + before do + allow(current_application_settings).to receive(:email_author_in_body).and_return(true) + end + + it 'contains a link to note author' do + is_expected.to have_body_text issue.author_name + is_expected.to have_body_text /wrote\:/ + end + end end describe 'that are new with a description' do @@ -377,6 +164,17 @@ describe Notify do it 'has the correct message-id set' do is_expected.to have_header 'Message-ID', "" end + + context 'when enabled email_author_in_body' do + before do + allow(current_application_settings).to receive(:email_author_in_body).and_return(true) + end + + it 'contains a link to note author' do + is_expected.to have_body_text merge_request.author_name + is_expected.to have_body_text /wrote\:/ + end + end end describe 'that are new with a description' do @@ -550,6 +348,21 @@ describe Notify do it 'contains the message from the note' do is_expected.to have_body_text /#{note.note}/ end + + it 'not contains note author' do + is_expected.not_to have_body_text /wrote\:/ + end + + context 'when enabled email_author_in_body' do + before do + allow(current_application_settings).to receive(:email_author_in_body).and_return(true) + end + + it 'contains a link to note author' do + is_expected.to have_body_text note.author_name + is_expected.to have_body_text /wrote\:/ + end + end end describe 'on a commit' do @@ -934,49 +747,4 @@ describe Notify do end end - describe 'build success' do - before { build.success } - - subject { Notify.build_success_email(build.id, 'wow@example.com') } - - it_behaves_like 'an email with X-GitLab headers containing build details' - it_behaves_like 'an email with X-GitLab headers containing project details' do - let(:project) { build.project } - end - - it 'has header indicating build status' do - is_expected.to have_header 'X-GitLab-Build-Status', 'success' - end - - it 'has the correct subject' do - should have_subject /Build success for/ - end - - it 'contains name of project' do - should have_body_text build.project_name - end - end - - describe 'build fail' do - before { build.drop } - - subject { Notify.build_fail_email(build.id, 'wow@example.com') } - - it_behaves_like 'an email with X-GitLab headers containing build details' - it_behaves_like 'an email with X-GitLab headers containing project details' do - let(:project) { build.project } - end - - it 'has header indicating build status' do - is_expected.to have_header 'X-GitLab-Build-Status', 'failed' - end - - it 'has the correct subject' do - should have_subject /Build failed for/ - end - - it 'contains name of project' do - should have_body_text build.project_name - end - end end diff --git a/spec/mailers/shared/notify.rb b/spec/mailers/shared/notify.rb new file mode 100644 index 0000000000..48c851ebbd --- /dev/null +++ b/spec/mailers/shared/notify.rb @@ -0,0 +1,117 @@ +shared_context 'gitlab email notification' do + let(:gitlab_sender_display_name) { Gitlab.config.gitlab.email_display_name } + let(:gitlab_sender) { Gitlab.config.gitlab.email_from } + let(:gitlab_sender_reply_to) { Gitlab.config.gitlab.email_reply_to } + let(:recipient) { create(:user, email: 'recipient@example.com') } + let(:project) { create(:project) } + let(:new_user_address) { 'newguy@example.com' } + + before do + ActionMailer::Base.deliveries.clear + email = recipient.emails.create(email: "notifications@example.com") + recipient.update_attribute(:notification_email, email.email) + end +end + +shared_examples 'a multiple recipients email' do + it 'is sent to the given recipient' do + is_expected.to deliver_to recipient.notification_email + end +end + +shared_examples 'an email sent from GitLab' do + it 'is sent from GitLab' do + sender = subject.header[:from].addrs[0] + expect(sender.display_name).to eq(gitlab_sender_display_name) + expect(sender.address).to eq(gitlab_sender) + end + + it 'has a Reply-To address' do + reply_to = subject.header[:reply_to].addresses + expect(reply_to).to eq([gitlab_sender_reply_to]) + end +end + +shared_examples 'an email that contains a header with author username' do + it 'has X-GitLab-Author header containing author\'s username' do + is_expected.to have_header 'X-GitLab-Author', user.username + end +end + +shared_examples 'an email with X-GitLab headers containing project details' do + it 'has X-GitLab-Project* headers' do + is_expected.to have_header 'X-GitLab-Project', /#{project.name}/ + is_expected.to have_header 'X-GitLab-Project-Id', /#{project.id}/ + is_expected.to have_header 'X-GitLab-Project-Path', /#{project.path_with_namespace}/ + end +end + +shared_examples 'an email starting a new thread' do |message_id_prefix| + include_examples 'an email with X-GitLab headers containing project details' + + it 'has a discussion identifier' do + is_expected.to have_header 'Message-ID', /<#{message_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ + end +end + +shared_examples 'an answer to an existing thread' do |thread_id_prefix| + include_examples 'an email with X-GitLab headers containing project details' + + it 'has a subject that begins with Re: ' do + is_expected.to have_subject /^Re: / + end + + it 'has headers that reference an existing thread' do + is_expected.to have_header 'Message-ID', /<(.*)@#{Gitlab.config.gitlab.host}>/ + is_expected.to have_header 'References', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ + is_expected.to have_header 'In-Reply-To', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ + end +end + +shared_examples 'a new user email' do + it 'is sent to the new user' do + is_expected.to deliver_to new_user_address + end + + it 'has the correct subject' do + is_expected.to have_subject /^Account was created for you$/i + end + + it 'contains the new user\'s login name' do + is_expected.to have_body_text /#{new_user_address}/ + end +end + +shared_examples 'it should have Gmail Actions links' do + it { is_expected.to have_body_text /ViewAction/ } +end + +shared_examples 'it should not have Gmail Actions links' do + it { is_expected.to_not have_body_text /ViewAction/ } +end + +shared_examples 'it should show Gmail Actions View Issue link' do + it_behaves_like 'it should have Gmail Actions links' + + it { is_expected.to have_body_text /View Issue/ } +end + +shared_examples 'it should show Gmail Actions View Merge request link' do + it_behaves_like 'it should have Gmail Actions links' + + it { is_expected.to have_body_text /View Merge request/ } +end + +shared_examples 'it should show Gmail Actions View Commit link' do + it_behaves_like 'it should have Gmail Actions links' + + it { is_expected.to have_body_text /View Commit/ } +end + +shared_examples 'an unsubscribeable thread' do + it { is_expected.to have_body_text /unsubscribe/ } +end + +shared_examples "a user cannot unsubscribe through footer link" do + it { is_expected.not_to have_body_text /unsubscribe/ } +end diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb index f9be8fcbcf..4799bbaa57 100644 --- a/spec/models/abuse_report_spec.rb +++ b/spec/models/abuse_report_spec.rb @@ -26,7 +26,7 @@ RSpec.describe AbuseReport, type: :model do it { is_expected.to validate_presence_of(:reporter) } it { is_expected.to validate_presence_of(:user) } it { is_expected.to validate_presence_of(:message) } - it { is_expected.to validate_uniqueness_of(:user_id) } + it { is_expected.to validate_uniqueness_of(:user_id).with_message('has already been reported') } end describe '#remove_user' do diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb new file mode 100644 index 0000000000..c5658bd26e --- /dev/null +++ b/spec/models/appearance_spec.rb @@ -0,0 +1,10 @@ +require 'rails_helper' + +RSpec.describe Appearance, type: :model do + subject { create(:appearance) } + + it { is_expected.to be_valid } + + it { is_expected.to validate_presence_of(:title) } + it { is_expected.to validate_presence_of(:description) } +end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index f4c5888275..b1764d7ac0 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -66,6 +66,18 @@ describe ApplicationSetting, models: true do it { is_expected.to allow_value(http).for(:after_sign_out_path) } it { is_expected.to allow_value(https).for(:after_sign_out_path) } it { is_expected.not_to allow_value(ftp).for(:after_sign_out_path) } + + it { is_expected.to validate_presence_of(:max_attachment_size) } + + it do + is_expected.to validate_numericality_of(:max_attachment_size) + .only_integer + .is_greater_than(0) + end + + it_behaves_like 'an object with email-formated attributes', :admin_notification_email do + subject { setting } + end end context 'restricted signup domains' do diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 606340d87e..e3d3d45365 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -243,7 +243,7 @@ describe Ci::Build, models: true do end describe :can_be_served? do - let(:runner) { FactoryGirl.create :ci_specific_runner } + let(:runner) { FactoryGirl.create :ci_runner } before { build.project.runners << runner } @@ -285,7 +285,7 @@ describe Ci::Build, models: true do end context 'if there are runner' do - let(:runner) { FactoryGirl.create :ci_specific_runner } + let(:runner) { FactoryGirl.create :ci_runner } before do build.project.runners << runner @@ -322,7 +322,7 @@ describe Ci::Build, models: true do it { is_expected.to be_truthy } context "and there are specific runner" do - let(:runner) { FactoryGirl.create :ci_specific_runner, contacted_at: 1.second.ago } + let(:runner) { FactoryGirl.create :ci_runner, contacted_at: 1.second.ago } before do build.project.runners << runner @@ -346,15 +346,14 @@ describe Ci::Build, models: true do describe :artifacts_download_url do subject { build.artifacts_download_url } - it "should be nil if artifact doesn't exist" do - build.update_attributes(artifacts_file: nil) - is_expected.to be_nil + context 'artifacts file does not exist' do + before { build.update_attributes(artifacts_file: nil) } + it { is_expected.to be_nil } end - it 'should not be nil if artifact exist' do - gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') - build.update_attributes(artifacts_file: gif) - is_expected.to_not be_nil + context 'artifacts file exists' do + let(:build) { create(:ci_build, :artifacts) } + it { is_expected.to_not be_nil } end end @@ -381,11 +380,7 @@ describe Ci::Build, models: true do end context 'artifacts archive exists' do - before do - gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') - build.update_attributes(artifacts_file: gif) - end - + let(:build) { create(:ci_build, :artifacts) } it { is_expected.to be_truthy } end end @@ -398,16 +393,7 @@ describe Ci::Build, models: true do end context 'artifacts archive is a zip file and metadata exists' do - before do - fixture_dir = Rails.root + 'spec/fixtures/' - archive = fixture_file_upload(fixture_dir + 'ci_build_artifacts.zip', - 'application/zip') - metadata = fixture_file_upload(fixture_dir + 'ci_build_artifacts_metadata.gz', - 'application/x-gzip') - build.update_attributes(artifacts_file: archive) - build.update_attributes(artifacts_metadata: metadata) - end - + let(:build) { create(:ci_build, :artifacts) } it { is_expected.to be_truthy } end end @@ -511,6 +497,103 @@ describe Ci::Build, models: true do expect(@build2.merge_request.id).to eq(@merge_request.id) end end + end + describe 'build erasable' do + shared_examples 'erasable' do + it 'should remove artifact file' do + expect(build.artifacts_file.exists?).to be_falsy + end + + it 'should remove artifact metadata file' do + expect(build.artifacts_metadata.exists?).to be_falsy + end + + it 'should erase build trace in trace file' do + expect(build.trace).to be_empty + end + + it 'should set erased to true' do + expect(build.erased?).to be true + end + + it 'should set erase date' do + expect(build.erased_at).to_not be_falsy + end + end + + context 'build is not erasable' do + let!(:build) { create(:ci_build) } + + describe '#erase' do + subject { build.erase } + + it { is_expected.to be false } + end + + describe '#erasable?' do + subject { build.erasable? } + it { is_expected.to eq false } + end + end + + context 'build is erasable' do + let!(:build) { create(:ci_build, :trace, :success, :artifacts) } + + describe '#erase' do + before { build.erase(erased_by: user) } + + context 'erased by user' do + let!(:user) { create(:user, username: 'eraser') } + + include_examples 'erasable' + + it 'should record user who erased a build' do + expect(build.erased_by).to eq user + end + end + + context 'erased by system' do + let(:user) { nil } + + include_examples 'erasable' + + it 'should not set user who erased a build' do + expect(build.erased_by).to be_nil + end + end + end + + describe '#erasable?' do + subject { build.erasable? } + it { is_expected.to eq true } + end + + describe '#erased?' do + let!(:build) { create(:ci_build, :trace, :success, :artifacts) } + subject { build.erased? } + + context 'build has not been erased' do + it { is_expected.to be false } + end + + context 'build has been erased' do + before { build.erase } + + it { is_expected.to be true } + end + end + + context 'metadata and build trace are not available' do + let!(:build) { create(:ci_build, :success, :artifacts) } + before { build.remove_artifacts_metadata! } + + describe '#erase' do + it 'should not raise error' do + expect { build.erase }.to_not raise_error + end + end + end + end end end diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index dfc0cc3be1..4dc309a425 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -247,6 +247,35 @@ describe Ci::Commit, models: true do end end + + context 'custom stage with first job allowed to fail' do + let(:yaml) do + { + stages: ['clean', 'test'], + clean_job: { + stage: 'clean', + allow_failure: true, + script: 'BUILD', + }, + test_job: { + stage: 'test', + script: 'TEST', + }, + } + end + + before do + stub_ci_commit_yaml_file(YAML.dump(yaml)) + create_builds + end + + it 'properly schedules builds' do + expect(commit.builds.pluck(:status)).to contain_exactly('pending') + commit.builds.running_or_pending.each(&:drop) + expect(commit.builds.pluck(:status)).to contain_exactly('pending', 'failed') + end + end + context 'properly creates builds when "when" is defined' do let(:yaml) do { diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 232760dfeb..e891838672 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -39,7 +39,7 @@ describe Ci::Runner, models: true do describe :assign_to do let!(:project) { FactoryGirl.create :empty_project } - let!(:shared_runner) { FactoryGirl.create(:ci_shared_runner) } + let!(:shared_runner) { FactoryGirl.create(:ci_runner, :shared) } before { shared_runner.assign_to(project) } @@ -52,15 +52,15 @@ describe Ci::Runner, models: true do subject { Ci::Runner.online } before do - @runner1 = FactoryGirl.create(:ci_shared_runner, contacted_at: 1.year.ago) - @runner2 = FactoryGirl.create(:ci_shared_runner, contacted_at: 1.second.ago) + @runner1 = FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.year.ago) + @runner2 = FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.second.ago) end it { is_expected.to eq([@runner2])} end describe :online? do - let(:runner) { FactoryGirl.create(:ci_shared_runner) } + let(:runner) { FactoryGirl.create(:ci_runner, :shared) } subject { runner.online? } @@ -84,7 +84,7 @@ describe Ci::Runner, models: true do end describe :status do - let(:runner) { FactoryGirl.create(:ci_shared_runner, contacted_at: 1.second.ago) } + let(:runner) { FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.second.ago) } subject { runner.status } @@ -115,7 +115,7 @@ describe Ci::Runner, models: true do describe "belongs_to_one_project?" do it "returns false if there are two projects runner assigned to" do - runner = FactoryGirl.create(:ci_specific_runner) + runner = FactoryGirl.create(:ci_runner) project = FactoryGirl.create(:empty_project) project1 = FactoryGirl.create(:empty_project) project.runners << runner @@ -125,7 +125,7 @@ describe Ci::Runner, models: true do end it "returns true" do - runner = FactoryGirl.create(:ci_specific_runner) + runner = FactoryGirl.create(:ci_runner) project = FactoryGirl.create(:empty_project) project.runners << runner diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index ecf37b40c5..253902512c 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -118,4 +118,38 @@ eos it { expect(data[:modified]).to eq([".gitmodules"]) } it { expect(data[:removed]).to eq([]) } end + + describe '#reverts_commit?' do + let(:another_commit) { double(:commit, revert_description: "This reverts commit #{commit.sha}") } + + it { expect(commit.reverts_commit?(another_commit)).to be_falsy } + + context 'commit has no description' do + before { allow(commit).to receive(:description?).and_return(false) } + + it { expect(commit.reverts_commit?(another_commit)).to be_falsy } + end + + context "another_commit's description does not revert commit" do + before { allow(commit).to receive(:description).and_return("Foo Bar") } + + it { expect(commit.reverts_commit?(another_commit)).to be_falsy } + end + + context "another_commit's description reverts commit" do + before { allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar") } + + it { expect(commit.reverts_commit?(another_commit)).to be_truthy } + end + + context "another_commit's description reverts merged merge request" do + before do + revert_description = "This reverts merge request !foo123" + allow(another_commit).to receive(:revert_description).and_return(revert_description) + allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar") + end + + it { expect(commit.reverts_commit?(another_commit)).to be_truthy } + end + end end diff --git a/spec/models/concerns/case_sensitivity_spec.rb b/spec/models/concerns/case_sensitivity_spec.rb index 25b3f4e50d..92fdc5cd65 100644 --- a/spec/models/concerns/case_sensitivity_spec.rb +++ b/spec/models/concerns/case_sensitivity_spec.rb @@ -37,7 +37,7 @@ describe CaseSensitivity, models: true do with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar'). and_return(criteria) - expect(model.iwhere(:'foo.bar' => 'bar')).to eq(criteria) + expect(model.iwhere('foo.bar'.to_sym => 'bar')).to eq(criteria) end end @@ -87,8 +87,8 @@ describe CaseSensitivity, models: true do with(%q{LOWER("foo"."baz") = LOWER(:value)}, value: 'baz'). and_return(final) - got = model.iwhere(:'foo.bar' => 'bar', - :'foo.baz' => 'baz') + got = model.iwhere('foo.bar'.to_sym => 'bar', + 'foo.baz'.to_sym => 'baz') expect(got).to eq(final) end @@ -127,7 +127,7 @@ describe CaseSensitivity, models: true do with(%q{`foo`.`bar` = :value}, value: 'bar'). and_return(criteria) - expect(model.iwhere(:'foo.bar' => 'bar')). + expect(model.iwhere('foo.bar'.to_sym => 'bar')). to eq(criteria) end end @@ -178,8 +178,8 @@ describe CaseSensitivity, models: true do with(%q{`foo`.`baz` = :value}, value: 'baz'). and_return(final) - got = model.iwhere(:'foo.bar' => 'bar', - :'foo.baz' => 'baz') + got = model.iwhere('foo.bar'.to_sym => 'bar', + 'foo.baz'.to_sym => 'baz') expect(got).to eq(final) end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 021d62cdf0..600089802b 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -69,17 +69,28 @@ describe Issue, "Issuable" do end describe "#to_hook_data" do - let(:hook_data) { issue.to_hook_data(user) } + let(:data) { issue.to_hook_data(user) } + let(:project) { issue.project } + it "returns correct hook data" do - expect(hook_data[:object_kind]).to eq("issue") - expect(hook_data[:user]).to eq(user.hook_attrs) - expect(hook_data[:repository][:name]).to eq(issue.project.name) - expect(hook_data[:repository][:url]).to eq(issue.project.url_to_repo) - expect(hook_data[:repository][:description]).to eq(issue.project.description) - expect(hook_data[:repository][:homepage]).to eq(issue.project.web_url) - expect(hook_data[:object_attributes]).to eq(issue.hook_attrs) + expect(data[:object_kind]).to eq("issue") + expect(data[:user]).to eq(user.hook_attrs) + expect(data[:object_attributes]).to eq(issue.hook_attrs) + expect(data).to_not have_key(:assignee) end + + context "issue is assigned" do + before { issue.update_attribute(:assignee, user) } + + it "returns correct hook data" do + expect(data[:object_attributes]['assignee_id']).to eq(user.id) + expect(data[:assignee]).to eq(user.hook_attrs) + end + end + + include_examples 'project hook data' + include_examples 'deprecated repository hook data' end describe '#card_attributes' do diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb new file mode 100644 index 0000000000..a20a614964 --- /dev/null +++ b/spec/models/email_spec.rb @@ -0,0 +1,22 @@ +# == Schema Information +# +# Table name: emails +# +# id :integer not null, primary key +# user_id :integer not null +# email :string(255) not null +# created_at :datetime +# updated_at :datetime +# + +require 'spec_helper' + +describe Email, models: true do + + describe 'validations' do + it_behaves_like 'an object with email-formated attributes', :email do + subject { build(:email) } + end + end + +end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 071582b028..ec2a923f91 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -65,27 +65,6 @@ describe Event, models: true do it { expect(@event.author).to eq(@user) } end - describe '.latest_update_time' do - describe 'when events are present' do - let(:time) { Time.utc(2015, 1, 1) } - - before do - create(:closed_issue_event, updated_at: time) - create(:closed_issue_event, updated_at: time + 5) - end - - it 'returns the latest update time' do - expect(Event.latest_update_time).to eq(time + 5) - end - end - - describe 'when no events exist' do - it 'returns nil' do - expect(Event.latest_update_time).to be_nil - end - end - end - describe '.limit_recent' do let!(:event1) { create(:closed_issue_event) } let!(:event2) { create(:closed_issue_event) } diff --git a/spec/models/external_issue_spec.rb b/spec/models/external_issue_spec.rb index 6ec6b9037a..9b144dd1ec 100644 --- a/spec/models/external_issue_spec.rb +++ b/spec/models/external_issue_spec.rb @@ -10,6 +10,21 @@ describe ExternalIssue, models: true do it { is_expected.to include_module(Referable) } end + describe '.reference_pattern' do + it 'allows underscores in the project name' do + expect(ExternalIssue.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' + end + + it 'allows numbers in the project name' do + expect(ExternalIssue.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234' + end + + it 'requires the project name to begin with A-Z' do + expect(ExternalIssue.reference_pattern.match('3EXT_EXT-1234')).to eq nil + expect(ExternalIssue.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' + end + end + describe '#to_reference' do it 'returns a String reference to the object' do expect(issue.to_reference).to eq issue.id diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb index 138b87a9a0..fd1513cab1 100644 --- a/spec/models/hooks/system_hook_spec.rb +++ b/spec/models/hooks/system_hook_spec.rb @@ -36,7 +36,7 @@ describe SystemHook, models: true do it "project_destroy hook" do user = create(:user) project = create(:empty_project, namespace: user.namespace) - Projects::DestroyService.new(project, user, {}).execute + Projects::DestroyService.new(project, user, {}).pending_delete! expect(WebMock).to have_requested(:post, @system_hook.url).with( body: /project_destroy/, headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } @@ -65,7 +65,7 @@ describe SystemHook, models: true do project = create(:project) project.team << [user, :master] expect(WebMock).to have_requested(:post, @system_hook.url).with( - body: /user_add_to_team/, + body: /user_add_to_team/, headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } ).once end @@ -76,7 +76,7 @@ describe SystemHook, models: true do project.team << [user, :master] project.project_members.destroy_all expect(WebMock).to have_requested(:post, @system_hook.url).with( - body: /user_remove_from_team/, + body: /user_remove_from_team/, headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } ).once end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 2aedca20df..2d8f1cc1ad 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -31,6 +31,10 @@ describe Member, models: true do it { is_expected.to validate_presence_of(:source) } it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) } + it_behaves_like 'an object with email-formated attributes', :invite_email do + subject { build(:project_member) } + end + context "when an invite email is provided" do let(:member) { build(:project_member, invite_email: "user@example.com", user: nil) } @@ -159,7 +163,7 @@ describe Member, models: true do describe "#generate_invite_token" do let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) } - + it "sets the invite token" do expect { member.generate_invite_token }.to change { member.invite_token} end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 291e6200a5..c51f34034d 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -24,6 +24,7 @@ # merge_params :text # merge_when_build_succeeds :boolean default(FALSE), not null # merge_user_id :integer +# merge_commit_sha :string # require 'spec_helper' @@ -137,9 +138,10 @@ describe MergeRequest, models: true do describe 'detection of issues to be closed' do let(:issue0) { create :issue, project: subject.project } let(:issue1) { create :issue, project: subject.project } - let(:commit0) { double('commit0', closes_issues: [issue0]) } - let(:commit1) { double('commit1', closes_issues: [issue0]) } - let(:commit2) { double('commit2', closes_issues: [issue1]) } + + let(:commit0) { double('commit0', safe_message: "Fixes #{issue0.to_reference}") } + let(:commit1) { double('commit1', safe_message: "Fixes #{issue0.to_reference}") } + let(:commit2) { double('commit2', safe_message: "Fixes #{issue1.to_reference}") } before do allow(subject).to receive(:commits).and_return([commit0, commit1, commit2]) @@ -149,7 +151,9 @@ describe MergeRequest, models: true do allow(subject.project).to receive(:default_branch). and_return(subject.target_branch) - expect(subject.closes_issues).to eq([issue0, issue1].sort_by(&:id)) + closed = subject.closes_issues + + expect(closed).to include(issue0, issue1) end it 'only lists issues as to be closed if it targets the default branch' do @@ -167,17 +171,6 @@ describe MergeRequest, models: true do expect(subject.closes_issues).to include(issue2) end - - context 'for a project with JIRA integration' do - let(:issue0) { JiraIssue.new('JIRA-123', subject.project) } - let(:issue1) { JiraIssue.new('FOOBAR-4567', subject.project) } - - it 'returns sorted JiraIssues' do - allow(subject.project).to receive_messages(default_branch: subject.target_branch) - - expect(subject.closes_issues).to eq([issue0, issue1]) - end - end end describe "#work_in_progress?" do @@ -196,6 +189,11 @@ describe MergeRequest, models: true do expect(subject).to be_work_in_progress end + it "detects the '[WIP]' prefix" do + subject.title = "[WIP]#{subject.title}" + expect(subject).to be_work_in_progress + end + it "doesn't detect WIP for words starting with WIP" do subject.title = "Wipwap #{subject.title}" expect(subject).not_to be_work_in_progress @@ -234,9 +232,15 @@ describe MergeRequest, models: true do expect(subject.can_remove_source_branch?(user2)).to be_falsey end - it "is can be removed in all other cases" do + it "can be removed if the last commit is the head of the source branch" do + allow(subject.source_project).to receive(:commit).and_return(subject.last_commit) + expect(subject.can_remove_source_branch?(user)).to be_truthy end + + it "cannot be removed if the last commit is not also the head of the source branch" do + expect(subject.can_remove_source_branch?(user)).to be_falsey + end end describe "#reset_merge_when_build_succeeds" do @@ -251,13 +255,22 @@ describe MergeRequest, models: true do end describe "#hook_attrs" do + let(:attrs_hash) { subject.hook_attrs.to_h } + + [:source, :target].each do |key| + describe "#{key} key" do + include_examples 'project hook data', project_key: key do + let(:data) { attrs_hash } + let(:project) { subject.send("#{key}_project") } + end + end + end + it "has all the required keys" do - attrs = subject.hook_attrs - attrs = attrs.to_h - expect(attrs).to include(:source) - expect(attrs).to include(:target) - expect(attrs).to include(:last_commit) - expect(attrs).to include(:work_in_progress) + expect(attrs_hash).to include(:source) + expect(attrs_hash).to include(:target) + expect(attrs_hash).to include(:last_commit) + expect(attrs_hash).to include(:work_in_progress) end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 30a71987d8..1b1380ce4e 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -33,6 +33,20 @@ describe Milestone, models: true do let(:milestone) { create(:milestone) } let(:issue) { create(:issue) } + describe "unique milestone title per project" do + it "shouldn't accept the same title in a project twice" do + new_milestone = Milestone.new(project: milestone.project, title: milestone.title) + expect(new_milestone).not_to be_valid + end + + it "should accept the same title in another project" do + project = build(:project) + new_milestone = Milestone.new(project: project, title: milestone.title) + + expect(new_milestone).to be_valid + end + end + describe "#percent_complete" do it "should not count open issues" do milestone.issues << issue diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 9182b42661..583937ca74 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -26,6 +26,8 @@ describe Note, models: true do it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:noteable) } it { is_expected.to belong_to(:author).class_name('User') } + + it { is_expected.to have_many(:todos).dependent(:destroy) } end describe 'validation' do @@ -203,11 +205,19 @@ describe Note, models: true do end describe "set_award!" do - let(:issue) { create :issue } + let(:merge_request) { create :merge_request } it "converts aliases to actual name" do - note = create :note, note: ":+1:", noteable: issue + note = create(:note, note: ":+1:", noteable: merge_request) expect(note.reload.note).to eq("thumbsup") end + + it "is not an award emoji when comment is on a diff" do + note = create(:note, note: ":blowfish:", noteable: merge_request, line_code: "11d5d2e667e9da4f7f610f81d86c974b146b13bd_0_2") + note = note.reload + + expect(note.note).to eq(":blowfish:") + expect(note.is_award?).to be_falsy + end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index a3de23369e..012be3e2df 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -68,6 +68,7 @@ describe Project, models: true do it { is_expected.to have_many(:runners) } it { is_expected.to have_many(:variables) } it { is_expected.to have_many(:triggers) } + it { is_expected.to have_many(:todos).dependent(:destroy) } end describe 'modules' do @@ -102,7 +103,7 @@ describe Project, models: true do expect(project2.errors[:limit_reached].first).to match(/Your project limit is 0/) end end - + describe 'project token' do it 'should set an random token if none provided' do project = FactoryGirl.create :empty_project, runners_token: '' @@ -519,35 +520,35 @@ describe Project, models: true do describe :any_runners do let(:project) { create(:empty_project, shared_runners_enabled: shared_runners_enabled) } - let(:specific_runner) { create(:ci_specific_runner) } - let(:shared_runner) { create(:ci_shared_runner) } + let(:specific_runner) { create(:ci_runner) } + let(:shared_runner) { create(:ci_runner, :shared) } context 'for shared runners disabled' do let(:shared_runners_enabled) { false } - + it 'there are no runners available' do expect(project.any_runners?).to be_falsey end - + it 'there is a specific runner' do project.runners << specific_runner expect(project.any_runners?).to be_truthy end - + it 'there is a shared runner, but they are prohibited to use' do shared_runner expect(project.any_runners?).to be_falsey end - + it 'checks the presence of specific runner' do project.runners << specific_runner expect(project.any_runners? { |runner| runner == specific_runner }).to be_truthy end end - + context 'for shared runners enabled' do let(:shared_runners_enabled) { true } - + it 'there is a shared runner' do shared_runner expect(project.any_runners?).to be_truthy @@ -583,4 +584,67 @@ describe Project, models: true do end end + + describe '#rename_repo' do + let(:project) { create(:project) } + let(:gitlab_shell) { Gitlab::Shell.new } + + before do + # Project#gitlab_shell returns a new instance of Gitlab::Shell on every + # call. This makes testing a bit easier. + allow(project).to receive(:gitlab_shell).and_return(gitlab_shell) + end + + it 'renames a repository' do + allow(project).to receive(:previous_changes).and_return('path' => ['foo']) + + ns = project.namespace_dir + + expect(gitlab_shell).to receive(:mv_repository). + ordered. + with("#{ns}/foo", "#{ns}/#{project.path}"). + and_return(true) + + expect(gitlab_shell).to receive(:mv_repository). + ordered. + with("#{ns}/foo.wiki", "#{ns}/#{project.path}.wiki"). + and_return(true) + + expect_any_instance_of(SystemHooksService). + to receive(:execute_hooks_for). + with(project, :rename) + + expect_any_instance_of(Gitlab::UploadsTransfer). + to receive(:rename_project). + with('foo', project.path, ns) + + expect(project).to receive(:expire_caches_before_rename) + + project.rename_repo + end + end + + describe '#expire_caches_before_rename' do + let(:project) { create(:project) } + let(:repo) { double(:repo, exists?: true) } + let(:wiki) { double(:wiki, exists?: true) } + + it 'expires the caches of the repository and wiki' do + allow(Repository).to receive(:new). + with('foo', project). + and_return(repo) + + allow(Repository).to receive(:new). + with('foo.wiki', project). + and_return(wiki) + + expect(repo).to receive(:expire_cache) + expect(repo).to receive(:expire_emptiness_caches) + + expect(wiki).to receive(:expire_cache) + expect(wiki).to receive(:expire_emptiness_caches) + + project.expire_caches_before_rename('foo') + end + end end diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 5cd5ae327b..7b63da005f 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -68,14 +68,24 @@ describe ProjectTeam, models: true do end describe "#human_max_access" do - it "return master role" do - user = create :user - group = create :group - group.add_users([user.id], GroupMember::MASTER) - project = create(:project, namespace: group) - project.team << [user, :guest] + it 'returns Master role' do + user = create(:user) + group = create(:group) + group.add_master(user) - expect(project.team.human_max_access(user.id)).to eq("Master") + project = build_stubbed(:empty_project, namespace: group) + + expect(project.team.human_max_access(user.id)).to eq 'Master' + end + + it 'returns Owner role' do + user = create(:user) + group = create(:group) + group.add_owner(user) + + project = build_stubbed(:empty_project, namespace: group) + + expect(project.team.human_max_access(user.id)).to eq 'Owner' end end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index c484ae8fc8..b596782f4e 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -5,6 +5,15 @@ describe Repository, models: true do let(:repository) { create(:project).repository } let(:user) { create(:user) } + let(:commit_options) do + author = repository.user_to_committer(user) + { message: 'Test message', committer: author, author: author } + end + let(:merge_commit) do + source_sha = repository.find_branch('feature').target + merge_commit_id = repository.merge(user, source_sha, 'master', commit_options) + repository.commit(merge_commit_id) + end describe :branch_names_contains do subject { repository.branch_names_contains(sample_commit.id) } @@ -200,13 +209,22 @@ describe Repository, models: true do describe :commit_with_hooks do context 'when pre hooks were successful' do - it 'should run without errors' do - expect_any_instance_of(GitHooksService).to receive(:execute).and_return(true) + before do + expect_any_instance_of(GitHooksService).to receive(:execute). + and_return(true) + end + it 'should run without errors' do expect do repository.commit_with_hooks(user, 'feature') { sample_commit.id } end.not_to raise_error end + + it 'should ensure the autocrlf Git option is set to :input' do + expect(repository).to receive(:update_autocrlf_option) + + repository.commit_with_hooks(user, 'feature') { sample_commit.id } + end end context 'when pre hooks failed' do @@ -220,6 +238,25 @@ describe Repository, models: true do end end + describe '#exists?' do + it 'returns true when a repository exists' do + expect(repository.exists?).to eq(true) + end + + it 'returns false when a repository does not exist' do + expect(repository.raw_repository).to receive(:rugged). + and_raise(Gitlab::Git::Repository::NoRepository) + + expect(repository.exists?).to eq(false) + end + + it 'returns false when there is no namespace' do + allow(repository).to receive(:path_with_namespace).and_return(nil) + + expect(repository.exists?).to eq(false) + end + end + describe '#has_visible_content?' do subject { repository.has_visible_content? } @@ -232,11 +269,199 @@ describe Repository, models: true do end describe 'when there are branches' do - before do - allow(repository.raw_repository).to receive(:branch_count).and_return(3) + it 'returns true' do + expect(repository.raw_repository).to receive(:branch_count).and_return(3) + + expect(subject).to eq(true) end - it { is_expected.to eq(true) } + it 'caches the output' do + expect(repository.raw_repository).to receive(:branch_count). + once. + and_return(3) + + repository.has_visible_content? + repository.has_visible_content? + end + end + end + + describe '#update_autocrlf_option' do + describe 'when autocrlf is not already set to :input' do + before do + repository.raw_repository.autocrlf = true + end + + it 'sets autocrlf to :input' do + repository.update_autocrlf_option + + expect(repository.raw_repository.autocrlf).to eq(:input) + end + end + + describe 'when autocrlf is already set to :input' do + before do + repository.raw_repository.autocrlf = :input + end + + it 'does nothing' do + expect(repository.raw_repository).to_not receive(:autocrlf=). + with(:input) + + repository.update_autocrlf_option + end + end + end + + describe '#empty?' do + let(:empty_repository) { create(:project_empty_repo).repository } + + it 'returns true for an empty repository' do + expect(empty_repository.empty?).to eq(true) + end + + it 'returns false for a non-empty repository' do + expect(repository.empty?).to eq(false) + end + + it 'caches the output' do + expect(repository.raw_repository).to receive(:empty?). + once. + and_return(false) + + repository.empty? + repository.empty? + end + end + + describe '#root_ref' do + it 'returns a branch name' do + expect(repository.root_ref).to be_an_instance_of(String) + end + + it 'caches the output' do + expect(repository.raw_repository).to receive(:root_ref). + once. + and_return('master') + + repository.root_ref + repository.root_ref + end + end + + describe '#expire_cache' do + it 'expires all caches' do + expect(repository).to receive(:expire_branch_cache) + + repository.expire_cache + end + + it 'expires the caches for a specific branch' do + expect(repository).to receive(:expire_branch_cache).with('master') + + repository.expire_cache('master') + end + + it 'expires the emptiness cache for an empty repository' do + expect(repository).to receive(:empty?).and_return(true) + expect(repository).to receive(:expire_emptiness_caches) + + repository.expire_cache + end + + it 'does not expire the emptiness cache for a non-empty repository' do + expect(repository).to receive(:empty?).and_return(false) + expect(repository).to_not receive(:expire_emptiness_caches) + + repository.expire_cache + end + end + + describe '#expire_root_ref_cache' do + it 'expires the root reference cache' do + repository.root_ref + + expect(repository.raw_repository).to receive(:root_ref). + once. + and_return('foo') + + repository.expire_root_ref_cache + + expect(repository.root_ref).to eq('foo') + end + end + + describe '#expire_has_visible_content_cache' do + it 'expires the visible content cache' do + repository.has_visible_content? + + expect(repository.raw_repository).to receive(:branch_count). + once. + and_return(0) + + repository.expire_has_visible_content_cache + + expect(repository.has_visible_content?).to eq(false) + end + end + + describe '#expire_branch_ache' do + # This method is private but we need it for testing purposes. Sadly there's + # no other proper way of testing caching operations. + let(:cache) { repository.send(:cache) } + + it 'expires the cache for all branches' do + expect(cache).to receive(:expire). + at_least(repository.branches.length). + times + + repository.expire_branch_cache + end + + it 'expires the cache for all branches when the root branch is given' do + expect(cache).to receive(:expire). + at_least(repository.branches.length). + times + + repository.expire_branch_cache(repository.root_ref) + end + + it 'expires the cache for a specific branch' do + expect(cache).to receive(:expire).once + + repository.expire_branch_cache('foo') + end + end + + describe '#expire_emptiness_caches' do + let(:cache) { repository.send(:cache) } + + it 'expires the caches' do + expect(cache).to receive(:expire).with(:empty?) + expect(repository).to receive(:expire_has_visible_content_cache) + + repository.expire_emptiness_caches + end + end + + describe :skip_merged_commit do + subject { repository.commits(Gitlab::Git::BRANCH_REF_PREFIX + "'test'", nil, 100, 0, true).map{ |k| k.id } } + + it { is_expected.not_to include('e56497bb5f03a90a51293fc6d516788730953899') } + end + + describe '#merge' do + it 'should merge the code and return the commit id' do + expect(merge_commit).to be_present + expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present + end + end + + describe '#revert_merge' do + it 'should revert the changes' do + repository.revert(user, merge_commit, 'master') + + expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).not_to be_present end end end diff --git a/spec/models/spam_log_spec.rb b/spec/models/spam_log_spec.rb new file mode 100644 index 0000000000..c4ec7625cb --- /dev/null +++ b/spec/models/spam_log_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe SpamLog, models: true do + describe 'associations' do + it { is_expected.to belong_to(:user) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:user) } + end + + describe '#remove_user' do + it 'blocks the user' do + spam_log = build(:spam_log) + + expect { spam_log.remove_user }.to change { spam_log.user.blocked? }.to(true) + end + + it 'removes the user' do + spam_log = build(:spam_log) + + expect { spam_log.remove_user }.to change { User.count }.by(-1) + end + end +end diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb new file mode 100644 index 0000000000..fe9ea7e7d1 --- /dev/null +++ b/spec/models/todo_spec.rb @@ -0,0 +1,69 @@ +# == Schema Information +# +# Table name: todos +# +# id :integer not null, primary key +# user_id :integer not null +# project_id :integer not null +# target_id :integer not null +# target_type :string not null +# author_id :integer +# note_id :integer +# action :integer not null +# state :string not null +# created_at :datetime +# updated_at :datetime +# + +require 'spec_helper' + +describe Todo, models: true do + describe 'relationships' do + it { is_expected.to belong_to(:author).class_name("User") } + it { is_expected.to belong_to(:note) } + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:target).touch(true) } + it { is_expected.to belong_to(:user) } + end + + describe 'respond to' do + it { is_expected.to respond_to(:author_name) } + it { is_expected.to respond_to(:author_email) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:action) } + it { is_expected.to validate_presence_of(:target) } + it { is_expected.to validate_presence_of(:user) } + end + + describe '#body' do + before do + subject.target = build(:issue, title: 'Bugfix') + end + + it 'returns target title when note is blank' do + subject.note = nil + + expect(subject.body).to eq 'Bugfix' + end + + it 'returns note when note is present' do + subject.note = build(:note, note: 'quick fix') + + expect(subject.body).to eq 'quick fix' + end + end + + describe '#done!' do + it 'changes state to done' do + todo = create(:todo, state: :pending) + expect { todo.done! }.to change(todo, :state).from('pending').to('done') + end + + it 'does not raise error when is already done' do + todo = create(:todo, state: :done) + expect { todo.done! }.not_to raise_error + end + end +end diff --git a/spec/models/tree_spec.rb b/spec/models/tree_spec.rb new file mode 100644 index 0000000000..0737999e12 --- /dev/null +++ b/spec/models/tree_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe Tree, models: true do + let(:repository) { create(:project).repository } + let(:sha) { repository.root_ref } + + subject { described_class.new(repository, '54fcc214') } + + describe '#readme' do + class FakeBlob + attr_reader :name + + def initialize(name) + @name = name + end + + def readme? + name =~ /^readme/i + end + end + + it 'returns nil when repository does not contains a README file' do + files = [FakeBlob.new('file'), FakeBlob.new('license'), FakeBlob.new('copying')] + expect(subject).to receive(:blobs).and_return(files) + + expect(subject.readme).to eq nil + end + + it 'returns nil when repository does not contains a previewable README file' do + files = [FakeBlob.new('file'), FakeBlob.new('README.pages'), FakeBlob.new('README.png')] + expect(subject).to receive(:blobs).and_return(files) + + expect(subject.readme).to eq nil + end + + it 'returns README when repository contains a previewable README file' do + files = [FakeBlob.new('README.png'), FakeBlob.new('README'), FakeBlob.new('file')] + expect(subject).to receive(:blobs).and_return(files) + + expect(subject.readme.name).to eq 'README' + end + + it 'returns first previewable README when repository contains more than one' do + files = [FakeBlob.new('file'), FakeBlob.new('README.md'), FakeBlob.new('README.asciidoc')] + expect(subject).to receive(:blobs).and_return(files) + + expect(subject.readme.name).to eq 'README.md' + end + + it 'returns first plain text README when repository contains more than one' do + files = [FakeBlob.new('file'), FakeBlob.new('README'), FakeBlob.new('README.txt')] + expect(subject).to receive(:blobs).and_return(files) + + expect(subject.readme.name).to eq 'README' + end + + it 'prioritizes previewable README file over one in plain text' do + files = [FakeBlob.new('file'), FakeBlob.new('README'), FakeBlob.new('README.md')] + expect(subject).to receive(:blobs).and_return(files) + + expect(subject.readme.name).to eq 'README.md' + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 0bef68e288..95188f518c 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -91,6 +91,8 @@ describe User, models: true do it { is_expected.to have_many(:assigned_merge_requests).dependent(:destroy) } it { is_expected.to have_many(:identities).dependent(:destroy) } it { is_expected.to have_one(:abuse_report) } + it { is_expected.to have_many(:spam_logs).dependent(:destroy) } + it { is_expected.to have_many(:todos).dependent(:destroy) } end describe 'validations' do @@ -118,37 +120,15 @@ describe User, models: true do it { is_expected.to validate_length_of(:bio).is_within(0..255) } + it_behaves_like 'an object with email-formated attributes', :email do + subject { build(:user) } + end + + it_behaves_like 'an object with email-formated attributes', :public_email, :notification_email do + subject { build(:user).tap { |user| user.emails << build(:email, email: email_value) } } + end + describe 'email' do - it 'accepts info@example.com' do - user = build(:user, email: 'info@example.com') - expect(user).to be_valid - end - - it 'accepts info+test@example.com' do - user = build(:user, email: 'info+test@example.com') - expect(user).to be_valid - end - - it "accepts o'reilly@example.com" do - user = build(:user, email: "o'reilly@example.com") - expect(user).to be_valid - end - - it 'rejects test@test@example.com' do - user = build(:user, email: 'test@test@example.com') - expect(user).to be_invalid - end - - it 'rejects mailto:test@example.com' do - user = build(:user, email: 'mailto:test@example.com') - expect(user).to be_invalid - end - - it "rejects lol!'+=?><#$%^&*()@gmail.com" do - user = build(:user, email: "lol!'+=?><#$%^&*()@gmail.com") - expect(user).to be_invalid - end - context 'when no signup domains listed' do before { allow(current_application_settings).to receive(:restricted_signup_domains).and_return([]) } it 'accepts any email' do @@ -276,6 +256,7 @@ describe User, models: true do expect(user).to be_two_factor_enabled expect(user.encrypted_otp_secret).not_to be_nil expect(user.otp_backup_codes).not_to be_nil + expect(user.otp_grace_period_started_at).not_to be_nil user.disable_two_factor! @@ -284,6 +265,7 @@ describe User, models: true do expect(user.encrypted_otp_secret_iv).to be_nil expect(user.encrypted_otp_secret_salt).to be_nil expect(user.otp_backup_codes).to be_nil + expect(user.otp_grace_period_started_at).to be_nil end end diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index c1b03838aa..ddc49495ed 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -189,6 +189,38 @@ describe WikiPage, models: true do end end + describe '#historical?' do + before do + create_page('Update', 'content') + @page = wiki.find_page('Update') + 3.times { |i| @page.update("content #{i}") } + end + + after do + destroy_page('Update') + end + + it 'returns true when requesting an old version' do + old_version = @page.versions.last.to_s + old_page = wiki.find_page('Update', old_version) + + expect(old_page.historical?).to eq true + end + + it 'returns false when requesting latest version' do + latest_version = @page.versions.first.to_s + latest_page = wiki.find_page('Update', latest_version) + + expect(latest_page.historical?).to eq false + end + + it 'returns false when version is nil' do + latest_page = wiki.find_page('Update', nil) + + expect(latest_page.historical?).to eq false + end + end + private def remove_temp_repo(path) diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 8c9f5a382b..175ee861a7 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -4,169 +4,241 @@ describe API::API, api: true do include ApiHelpers let(:user) { create(:user) } + let(:api_user) { user } let(:user2) { create(:user) } let!(:project) { create(:project, creator_id: user.id) } let!(:developer) { create(:project_member, user: user, project: project, access_level: ProjectMember::DEVELOPER) } let!(:reporter) { create(:project_member, user: user2, project: project, access_level: ProjectMember::REPORTER) } let(:commit) { create(:ci_commit, project: project)} let(:build) { create(:ci_build, commit: commit) } - let(:build_with_trace) { create(:ci_build_with_trace, commit: commit) } - let(:build_canceled) { create(:ci_build, :canceled, commit: commit) } describe 'GET /projects/:id/builds ' do + let(:query) { '' } + + before { get api("/projects/#{project.id}/builds?#{query}", api_user) } + context 'authorized user' do it 'should return project builds' do - get api("/projects/#{project.id}/builds", user) - expect(response.status).to eq(200) expect(json_response).to be_an Array end - it 'should filter project with one scope element' do - get api("/projects/#{project.id}/builds?scope=pending", user) + context 'filter project with one scope element' do + let(:query) { 'scope=pending' } - expect(response.status).to eq(200) - expect(json_response).to be_an Array + it do + expect(response.status).to eq(200) + expect(json_response).to be_an Array + end end - it 'should filter project with array of scope elements' do - get api("/projects/#{project.id}/builds?scope[0]=pending&scope[1]=running", user) + context 'filter project with array of scope elements' do + let(:query) { 'scope[0]=pending&scope[1]=running' } - expect(response.status).to eq(200) - expect(json_response).to be_an Array + it do + expect(response.status).to eq(200) + expect(json_response).to be_an Array + end end - it 'should respond 400 when scope contains invalid state' do - get api("/projects/#{project.id}/builds?scope[0]=pending&scope[1]=unknown_status", user) + context 'respond 400 when scope contains invalid state' do + let(:query) { 'scope[0]=pending&scope[1]=unknown_status' } - expect(response.status).to eq(400) + it { expect(response.status).to eq(400) } end end context 'unauthorized user' do - it 'should not return project builds' do - get api("/projects/#{project.id}/builds") + let(:api_user) { nil } + it 'should not return project builds' do expect(response.status).to eq(401) end end end describe 'GET /projects/:id/repository/commits/:sha/builds' do + before do + project.ensure_ci_commit(commit.sha) + get api("/projects/#{project.id}/repository/commits/#{commit.sha}/builds", api_user) + end + context 'authorized user' do it 'should return project builds for specific commit' do - project.ensure_ci_commit(commit.sha) - get api("/projects/#{project.id}/repository/commits/#{commit.sha}/builds", user) - expect(response.status).to eq(200) expect(json_response).to be_an Array end end context 'unauthorized user' do - it 'should not return project builds' do - project.ensure_ci_commit(commit.sha) - get api("/projects/#{project.id}/repository/commits/#{commit.sha}/builds") + let(:api_user) { nil } + it 'should not return project builds' do expect(response.status).to eq(401) end end end describe 'GET /projects/:id/builds/:build_id' do + before { get api("/projects/#{project.id}/builds/#{build.id}", api_user) } + context 'authorized user' do it 'should return specific build data' do - get api("/projects/#{project.id}/builds/#{build.id}", user) - expect(response.status).to eq(200) expect(json_response['name']).to eq('test') end end context 'unauthorized user' do - it 'should not return specific build data' do - get api("/projects/#{project.id}/builds/#{build.id}") + let(:api_user) { nil } + it 'should not return specific build data' do expect(response.status).to eq(401) end end end + describe 'GET /projects/:id/builds/:build_id/artifacts' do + before { get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) } + + context 'build with artifacts' do + let(:build) { create(:ci_build, :artifacts, commit: commit) } + + context 'authorized user' do + let(:download_headers) do + { 'Content-Transfer-Encoding'=>'binary', + 'Content-Disposition'=>'attachment; filename=ci_build_artifacts.zip' } + end + + it 'should return specific build artifacts' do + expect(response.status).to eq(200) + expect(response.headers).to include(download_headers) + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'should not return specific build artifacts' do + expect(response.status).to eq(401) + end + end + end + + it 'should not return build artifacts if not uploaded' do + expect(response.status).to eq(404) + end + end + describe 'GET /projects/:id/builds/:build_id/trace' do + let(:build) { create(:ci_build, :trace, commit: commit) } + + before { get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) } + context 'authorized user' do it 'should return specific build trace' do - get api("/projects/#{project.id}/builds/#{build_with_trace.id}/trace", user) - expect(response.status).to eq(200) - expect(response.body).to eq(build_with_trace.trace) + expect(response.body).to eq(build.trace) end end context 'unauthorized user' do - it 'should not return specific build trace' do - get api("/projects/#{project.id}/builds/#{build_with_trace.id}/trace") + let(:api_user) { nil } + it 'should not return specific build trace' do expect(response.status).to eq(401) end end end describe 'POST /projects/:id/builds/:build_id/cancel' do - context 'authorized user' do - context 'user with :manage_builds persmission' do - it 'should cancel running or pending build' do - post api("/projects/#{project.id}/builds/#{build.id}/cancel", user) + before { post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) } + context 'authorized user' do + context 'user with :update_build persmission' do + it 'should cancel running or pending build' do expect(response.status).to eq(201) expect(project.builds.first.status).to eq('canceled') end end - context 'user without :manage_builds permission' do - it 'should not cancel build' do - post api("/projects/#{project.id}/builds/#{build.id}/cancel", user2) + context 'user without :update_build permission' do + let(:api_user) { user2 } + it 'should not cancel build' do expect(response.status).to eq(403) end end end context 'unauthorized user' do - it 'should not cancel build' do - post api("/projects/#{project.id}/builds/#{build.id}/cancel") + let(:api_user) { nil } + it 'should not cancel build' do expect(response.status).to eq(401) end end end describe 'POST /projects/:id/builds/:build_id/retry' do - context 'authorized user' do - context 'user with :manage_builds persmission' do - it 'should retry non-running build' do - post api("/projects/#{project.id}/builds/#{build_canceled.id}/retry", user) + let(:build) { create(:ci_build, :canceled, commit: commit) } + before { post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) } + + context 'authorized user' do + context 'user with :update_build permission' do + it 'should retry non-running build' do expect(response.status).to eq(201) expect(project.builds.first.status).to eq('canceled') expect(json_response['status']).to eq('pending') end end - context 'user without :manage_builds permission' do - it 'should not retry build' do - post api("/projects/#{project.id}/builds/#{build_canceled.id}/retry", user2) + context 'user without :update_build permission' do + let(:api_user) { user2 } + it 'should not retry build' do expect(response.status).to eq(403) end end end context 'unauthorized user' do - it 'should not retry build' do - post api("/projects/#{project.id}/builds/#{build_canceled.id}/retry") + let(:api_user) { nil } + it 'should not retry build' do expect(response.status).to eq(401) end end end + + describe 'POST /projects/:id/builds/:build_id/erase' do + before do + post api("/projects/#{project.id}/builds/#{build.id}/erase", user) + end + + context 'build is erasable' do + let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, commit: commit) } + + it 'should erase build content' do + expect(response.status).to eq 201 + expect(build.trace).to be_empty + expect(build.artifacts_file.exists?).to be_falsy + expect(build.artifacts_metadata.exists?).to be_falsy + end + + it 'should update build' do + expect(build.reload.erased_at).to be_truthy + expect(build.reload.erased_by).to eq user + end + end + + context 'build is not erasable' do + let(:build) { create(:ci_build, :trace, project: project, commit: commit) } + + it 'should respond with forbidden' do + expect(response.status).to eq 403 + end + end + end end diff --git a/spec/requests/api/commit_status_spec.rb b/spec/requests/api/commit_status_spec.rb index 21482fc107..89b554622e 100644 --- a/spec/requests/api/commit_status_spec.rb +++ b/spec/requests/api/commit_status_spec.rb @@ -2,18 +2,17 @@ require 'spec_helper' describe API::CommitStatus, api: true do include ApiHelpers - let(:user) { create(:user) } - let(:user2) { create(:user) } - let!(:project) { create(:project, creator_id: user.id) } - let!(:reporter) { create(:project_member, user: user, project: project, access_level: ProjectMember::REPORTER) } - let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) } + let!(:project) { create(:project) } let(:commit) { project.repository.commit } let!(:ci_commit) { project.ensure_ci_commit(commit.id) } let(:commit_status) { create(:commit_status, commit: ci_commit) } + let(:guest) { create_user(ProjectMember::GUEST) } + let(:reporter) { create_user(ProjectMember::REPORTER) } + let(:developer) { create_user(ProjectMember::DEVELOPER) } describe "GET /projects/:id/repository/commits/:sha/statuses" do it_behaves_like 'a paginated resources' do - let(:request) { get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", user) } + let(:request) { get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", reporter) } end context "reporter user" do @@ -29,7 +28,7 @@ describe API::CommitStatus, api: true do end it "should return latest commit statuses" do - get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", user) + get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", reporter) expect(response.status).to eq(200) expect(json_response).to be_an Array @@ -39,7 +38,7 @@ describe API::CommitStatus, api: true do end it "should return all commit statuses" do - get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?all=1", user) + get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?all=1", reporter) expect(response.status).to eq(200) expect(json_response).to be_an Array @@ -47,7 +46,7 @@ describe API::CommitStatus, api: true do end it "should return latest commit statuses for specific ref" do - get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?ref=develop", user) + get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?ref=develop", reporter) expect(response.status).to eq(200) expect(json_response).to be_an Array @@ -55,7 +54,7 @@ describe API::CommitStatus, api: true do end it "should return latest commit statuses for specific name" do - get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?name=coverage", user) + get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?name=coverage", reporter) expect(response.status).to eq(200) expect(json_response).to be_an Array @@ -65,7 +64,7 @@ describe API::CommitStatus, api: true do context "guest user" do it "should not return project commits" do - get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", user2) + get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", guest) expect(response.status).to eq(403) end end @@ -81,10 +80,10 @@ describe API::CommitStatus, api: true do describe 'POST /projects/:id/statuses/:sha' do let(:post_url) { "/projects/#{project.id}/statuses/#{commit.id}" } - context 'reporter user' do + context 'developer user' do context 'should create commit status' do it 'with only required parameters' do - post api(post_url, user), state: 'success' + post api(post_url, developer), state: 'success' expect(response.status).to eq(201) expect(json_response['sha']).to eq(commit.id) expect(json_response['status']).to eq('success') @@ -95,7 +94,7 @@ describe API::CommitStatus, api: true do end it 'with all optional parameters' do - post api(post_url, user), state: 'success', context: 'coverage', ref: 'develop', target_url: 'url', description: 'test' + post api(post_url, developer), state: 'success', context: 'coverage', ref: 'develop', target_url: 'url', description: 'test' expect(response.status).to eq(201) expect(json_response['sha']).to eq(commit.id) expect(json_response['status']).to eq('success') @@ -108,25 +107,32 @@ describe API::CommitStatus, api: true do context 'should not create commit status' do it 'with invalid state' do - post api(post_url, user), state: 'invalid' + post api(post_url, developer), state: 'invalid' expect(response.status).to eq(400) end it 'without state' do - post api(post_url, user) + post api(post_url, developer) expect(response.status).to eq(400) end it 'invalid commit' do - post api("/projects/#{project.id}/statuses/invalid_sha", user), state: 'running' + post api("/projects/#{project.id}/statuses/invalid_sha", developer), state: 'running' expect(response.status).to eq(404) end end end + context 'reporter user' do + it 'should not create commit status' do + post api(post_url, reporter) + expect(response.status).to eq(403) + end + end + context 'guest user' do it 'should not create commit status' do - post api(post_url, user2) + post api(post_url, guest) expect(response.status).to eq(403) end end @@ -138,4 +144,10 @@ describe API::CommitStatus, api: true do end end end + + def create_user(access_level) + user = create(:user) + create(:project_member, user: user, project: project, access_level: access_level) + user + end end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 5e65ad18c0..571ea2dae4 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -46,10 +46,10 @@ describe API::API, api: true do expect(json_response.first['title']).to eq(issue.title) end - it "should add pagination headers" do - get api("/issues?per_page=3", user) + it "should add pagination headers and keep query params" do + get api("/issues?state=closed&per_page=3", user) expect(response.headers['Link']).to eq( - '; rel="first", ; rel="last"' + '; rel="first", ; rel="last"' % [user.private_token, user.private_token] ) end @@ -241,6 +241,37 @@ describe API::API, api: true do end end + describe 'POST /projects/:id/issues with spam filtering' do + before do + Grape::Endpoint.before_each do |endpoint| + allow(endpoint).to receive(:check_for_spam?).and_return(true) + allow(endpoint).to receive(:is_spam?).and_return(true) + end + end + + let(:params) do + { + title: 'new issue', + description: 'content here', + labels: 'label, label2' + } + end + + it "should not create a new project issue" do + expect { post api("/projects/#{project.id}/issues", user), params }.not_to change(Issue, :count) + expect(response.status).to eq(400) + expect(json_response['message']).to eq({ "error" => "Spam detected" }) + + spam_logs = SpamLog.all + expect(spam_logs.count).to eq(1) + expect(spam_logs[0].title).to eq('new issue') + expect(spam_logs[0].description).to eq('content here') + expect(spam_logs[0].user).to eq(user) + expect(spam_logs[0].noteable_type).to eq('Issue') + expect(spam_logs[0].project_id).to eq(project.id) + end + end + describe "PUT /projects/:id/issues/:issue_id to update only title" do it "should update a project issue" do put api("/projects/#{project.id}/issues/#{issue.id}", user), diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index e194eb93cf..4fd1df2556 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -10,6 +10,7 @@ describe API::API, api: true do let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds) } let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") } + let(:milestone) { create(:milestone, title: '1.0.0', project: project) } before do project.team << [user, :reporters] @@ -109,12 +110,13 @@ describe API::API, api: true do end end - describe "GET /projects/:id/merge_request/:merge_request_id" do + describe "GET /projects/:id/merge_requests/:merge_request_id" do it "should return merge_request" do - get api("/projects/#{project.id}/merge_request/#{merge_request.id}", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) expect(response.status).to eq(200) expect(json_response['title']).to eq(merge_request.title) expect(json_response['iid']).to eq(merge_request.iid) + expect(json_response['merge_status']).to eq('can_be_merged') end it 'should return merge_request by iid' do @@ -126,14 +128,14 @@ describe API::API, api: true do end it "should return a 404 error if merge_request_id not found" do - get api("/projects/#{project.id}/merge_request/999", user) + get api("/projects/#{project.id}/merge_requests/999", user) expect(response.status).to eq(404) end end - describe 'GET /projects/:id/merge_request/:merge_request_id/commits' do + describe 'GET /projects/:id/merge_requests/:merge_request_id/commits' do context 'valid merge request' do - before { get api("/projects/#{project.id}/merge_request/#{merge_request.id}/commits", user) } + before { get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user) } let(:commit) { merge_request.commits.first } it { expect(response.status).to eq 200 } @@ -143,20 +145,20 @@ describe API::API, api: true do end it 'returns a 404 when merge_request_id not found' do - get api("/projects/#{project.id}/merge_request/999/commits", user) + get api("/projects/#{project.id}/merge_requests/999/commits", user) expect(response.status).to eq(404) end end - describe 'GET /projects/:id/merge_request/:merge_request_id/changes' do + describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do it 'should return the change information of the merge_request' do - get api("/projects/#{project.id}/merge_request/#{merge_request.id}/changes", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user) expect(response.status).to eq 200 expect(json_response['changes'].size).to eq(merge_request.diffs.size) end it 'returns a 404 when merge_request_id not found' do - get api("/projects/#{project.id}/merge_request/999/changes", user) + get api("/projects/#{project.id}/merge_requests/999/changes", user) expect(response.status).to eq(404) end end @@ -169,10 +171,12 @@ describe API::API, api: true do source_branch: 'feature_conflict', target_branch: 'master', author: user, - labels: 'label, label2' + labels: 'label, label2', + milestone_id: milestone.id expect(response.status).to eq(201) expect(json_response['title']).to eq('Test merge_request') expect(json_response['labels']).to eq(['label', 'label2']) + expect(json_response['milestone']['id']).to eq(milestone.id) end it "should return 422 when source_branch equals target_branch" do @@ -311,19 +315,19 @@ describe API::API, api: true do end end - describe "PUT /projects/:id/merge_request/:merge_request_id to close MR" do + describe "PUT /projects/:id/merge_requests/:merge_request_id to close MR" do it "should return merge_request" do - put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), state_event: "close" + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close" expect(response.status).to eq(200) expect(json_response['state']).to eq('closed') end end - describe "PUT /projects/:id/merge_request/:merge_request_id/merge" do + describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do let(:ci_commit) { create(:ci_commit_without_jobs) } it "should return merge_request in case of success" do - put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user) + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) expect(response.status).to eq(200) end @@ -332,7 +336,7 @@ describe API::API, api: true do allow_any_instance_of(MergeRequest). to receive(:can_be_merged?).and_return(false) - put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user) + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) expect(response.status).to eq(406) expect(json_response['message']).to eq('Branch cannot be merged') @@ -340,14 +344,14 @@ describe API::API, api: true do it "should return 405 if merge_request is not open" do merge_request.close - put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user) + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) expect(response.status).to eq(405) expect(json_response['message']).to eq('405 Method Not Allowed') end it "should return 405 if merge_request is a work in progress" do merge_request.update_attribute(:title, "WIP: #{merge_request.title}") - put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user) + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) expect(response.status).to eq(405) expect(json_response['message']).to eq('405 Method Not Allowed') end @@ -355,7 +359,7 @@ describe API::API, api: true do it "should return 401 if user has no permissions to merge" do user2 = create(:user) project.team << [user2, :reporter] - put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user2) + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2) expect(response.status).to eq(401) expect(json_response['message']).to eq('401 Unauthorized') end @@ -364,7 +368,7 @@ describe API::API, api: true do allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) allow(ci_commit).to receive(:active?).and_return(true) - put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user), merge_when_build_succeeds: true + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true expect(response.status).to eq(200) expect(json_response['title']).to eq('Test') @@ -372,33 +376,39 @@ describe API::API, api: true do end end - describe "PUT /projects/:id/merge_request/:merge_request_id" do - it "should return merge_request" do - put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), title: "New title" + describe "PUT /projects/:id/merge_requests/:merge_request_id" do + it "updates title and returns merge_request" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: "New title" expect(response.status).to eq(200) expect(json_response['title']).to eq('New title') end - it "should return merge_request" do - put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), description: "New description" + it "updates description and returns merge_request" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), description: "New description" expect(response.status).to eq(200) expect(json_response['description']).to eq('New description') end + it "updates milestone_id and returns merge_request" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), milestone_id: milestone.id + expect(response.status).to eq(200) + expect(json_response['milestone']['id']).to eq(milestone.id) + end + it "should return 400 when source_branch is specified" do - put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), source_branch: "master", target_branch: "master" expect(response.status).to eq(400) end it "should return merge_request with renamed target_branch" do - put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), target_branch: "wiki" + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki" expect(response.status).to eq(200) expect(json_response['target_branch']).to eq('wiki') end it 'should return 400 on invalid label names' do - put api("/projects/#{project.id}/merge_request/#{merge_request.id}", + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: 'new issue', labels: 'label, ?' @@ -407,11 +417,11 @@ describe API::API, api: true do end end - describe "POST /projects/:id/merge_request/:merge_request_id/comments" do + describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do it "should return comment" do original_count = merge_request.notes.size - post api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user), note: "My comment" + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment" expect(response.status).to eq(201) expect(json_response['note']).to eq('My comment') expect(json_response['author']['name']).to eq(user.name) @@ -420,20 +430,20 @@ describe API::API, api: true do end it "should return 400 if note is missing" do - post api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user) + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) expect(response.status).to eq(400) end it "should return 404 if note is attached to non existent merge request" do - post api("/projects/#{project.id}/merge_request/404/comments", user), + post api("/projects/#{project.id}/merge_requests/404/comments", user), note: 'My comment' expect(response.status).to eq(404) end end - describe "GET :id/merge_request/:merge_request_id/comments" do + describe "GET :id/merge_requests/:merge_request_id/comments" do it "should return merge_request comments ordered by created_at" do - get api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response.length).to eq(2) @@ -443,11 +453,33 @@ describe API::API, api: true do end it "should return a 404 error if merge_request_id not found" do - get api("/projects/#{project.id}/merge_request/999/comments", user) + get api("/projects/#{project.id}/merge_requests/999/comments", user) expect(response.status).to eq(404) end end + describe 'GET :id/merge_requests/:merge_request_id/closes_issues' do + it 'returns the issue that will be closed on merge' do + issue = create(:issue, project: project) + mr = merge_request.tap do |mr| + mr.update_attribute(:description, "Closes #{issue.to_reference(mr.project)}") + end + + get api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", user) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(issue.id) + end + + it 'returns an empty array when there are no issues to be closed' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + end + def mr_with_later_created_and_updated_at_time merge_request merge_request.created_at += 1.hour diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 6f4c336b66..2a310f3834 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -90,6 +90,29 @@ describe API::API, api: true do end end + context 'and using the visibility filter' do + it 'should filter based on private visibility param' do + get api('/projects', user), { visibility: 'private' } + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).count) + end + + it 'should filter based on internal visibility param' do + get api('/projects', user), { visibility: 'internal' } + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).count) + end + + it 'should filter based on public visibility param' do + get api('/projects', user), { visibility: 'public' } + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PUBLIC).count) + end + end + context 'and using sorting' do before do project2 diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb new file mode 100644 index 0000000000..78484747d6 --- /dev/null +++ b/spec/requests/api/runners_spec.rb @@ -0,0 +1,464 @@ +require 'spec_helper' + +describe API::Runners, api: true do + include ApiHelpers + + let(:admin) { create(:user, :admin) } + let(:user) { create(:user) } + let(:user2) { create(:user) } + + let(:project) { create(:project, creator_id: user.id) } + let(:project2) { create(:project, creator_id: user.id) } + + let!(:shared_runner) { create(:ci_runner, :shared) } + let!(:unused_specific_runner) { create(:ci_runner) } + + let!(:specific_runner) do + create(:ci_runner).tap do |runner| + create(:ci_runner_project, runner: runner, project: project) + end + end + + let!(:two_projects_runner) do + create(:ci_runner).tap do |runner| + create(:ci_runner_project, runner: runner, project: project) + create(:ci_runner_project, runner: runner, project: project2) + end + end + + before do + # Set project access for users + create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) + create(:project_member, user: user, project: project2, access_level: ProjectMember::MASTER) + create(:project_member, user: user2, project: project, access_level: ProjectMember::REPORTER) + end + + describe 'GET /runners' do + context 'authorized user' do + it 'should return user available runners' do + get api('/runners', user) + shared = json_response.any?{ |r| r['is_shared'] } + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(shared).to be_falsey + end + + it 'should filter runners by scope' do + get api('/runners?scope=active', user) + shared = json_response.any?{ |r| r['is_shared'] } + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(shared).to be_falsey + end + + it 'should avoid filtering if scope is invalid' do + get api('/runners?scope=unknown', user) + expect(response.status).to eq(400) + end + end + + context 'unauthorized user' do + it 'should not return runners' do + get api('/runners') + + expect(response.status).to eq(401) + end + end + end + + describe 'GET /runners/all' do + context 'authorized user' do + context 'with admin privileges' do + it 'should return all runners' do + get api('/runners/all', admin) + shared = json_response.any?{ |r| r['is_shared'] } + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(shared).to be_truthy + end + end + + context 'without admin privileges' do + it 'should not return runners list' do + get api('/runners/all', user) + + expect(response.status).to eq(403) + end + end + + it 'should filter runners by scope' do + get api('/runners/all?scope=specific', admin) + shared = json_response.any?{ |r| r['is_shared'] } + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(shared).to be_falsey + end + + it 'should avoid filtering if scope is invalid' do + get api('/runners?scope=unknown', admin) + expect(response.status).to eq(400) + end + end + + context 'unauthorized user' do + it 'should not return runners' do + get api('/runners') + + expect(response.status).to eq(401) + end + end + end + + describe 'GET /runners/:id' do + context 'admin user' do + context 'when runner is shared' do + it "should return runner's details" do + get api("/runners/#{shared_runner.id}", admin) + + expect(response.status).to eq(200) + expect(json_response['description']).to eq(shared_runner.description) + end + end + + context 'when runner is not shared' do + it "should return runner's details" do + get api("/runners/#{specific_runner.id}", admin) + + expect(response.status).to eq(200) + expect(json_response['description']).to eq(specific_runner.description) + end + end + + it 'should return 404 if runner does not exists' do + get api('/runners/9999', admin) + + expect(response.status).to eq(404) + end + end + + context "runner project's administrative user" do + context 'when runner is not shared' do + it "should return runner's details" do + get api("/runners/#{specific_runner.id}", user) + + expect(response.status).to eq(200) + expect(json_response['description']).to eq(specific_runner.description) + end + end + + context 'when runner is shared' do + it "should return runner's details" do + get api("/runners/#{shared_runner.id}", user) + + expect(response.status).to eq(200) + expect(json_response['description']).to eq(shared_runner.description) + end + end + end + + context 'other authorized user' do + it "should not return runner's details" do + get api("/runners/#{specific_runner.id}", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it "should not return runner's details" do + get api("/runners/#{specific_runner.id}") + + expect(response.status).to eq(401) + end + end + end + + describe 'PUT /runners/:id' do + context 'admin user' do + context 'when runner is shared' do + it 'should update runner' do + description = shared_runner.description + active = shared_runner.active + + put api("/runners/#{shared_runner.id}", admin), description: "#{description}_updated", active: !active, + tag_list: ['ruby2.1', 'pgsql', 'mysql'] + shared_runner.reload + + expect(response.status).to eq(200) + expect(shared_runner.description).to eq("#{description}_updated") + expect(shared_runner.active).to eq(!active) + expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql') + end + end + + context 'when runner is not shared' do + it 'should update runner' do + description = specific_runner.description + put api("/runners/#{specific_runner.id}", admin), description: 'test' + specific_runner.reload + + expect(response.status).to eq(200) + expect(specific_runner.description).to eq('test') + expect(specific_runner.description).not_to eq(description) + end + end + + it 'should return 404 if runner does not exists' do + put api('/runners/9999', admin), description: 'test' + + expect(response.status).to eq(404) + end + end + + context 'authorized user' do + context 'when runner is shared' do + it 'should not update runner' do + put api("/runners/#{shared_runner.id}", user) + + expect(response.status).to eq(403) + end + end + + context 'when runner is not shared' do + it 'should not update runner without access to it' do + put api("/runners/#{specific_runner.id}", user2) + + expect(response.status).to eq(403) + end + + it 'should update runner with access to it' do + description = specific_runner.description + put api("/runners/#{specific_runner.id}", admin), description: 'test' + specific_runner.reload + + expect(response.status).to eq(200) + expect(specific_runner.description).to eq('test') + expect(specific_runner.description).not_to eq(description) + end + end + end + + context 'unauthorized user' do + it 'should not delete runner' do + put api("/runners/#{specific_runner.id}") + + expect(response.status).to eq(401) + end + end + end + + describe 'DELETE /runners/:id' do + context 'admin user' do + context 'when runner is shared' do + it 'should delete runner' do + expect do + delete api("/runners/#{shared_runner.id}", admin) + end.to change{ Ci::Runner.shared.count }.by(-1) + expect(response.status).to eq(200) + end + end + + context 'when runner is not shared' do + it 'should delete unused runner' do + expect do + delete api("/runners/#{unused_specific_runner.id}", admin) + end.to change{ Ci::Runner.specific.count }.by(-1) + expect(response.status).to eq(200) + end + + it 'should delete used runner' do + expect do + delete api("/runners/#{specific_runner.id}", admin) + end.to change{ Ci::Runner.specific.count }.by(-1) + expect(response.status).to eq(200) + end + end + + it 'should return 404 if runner does not exists' do + delete api('/runners/9999', admin) + + expect(response.status).to eq(404) + end + end + + context 'authorized user' do + context 'when runner is shared' do + it 'should not delete runner' do + delete api("/runners/#{shared_runner.id}", user) + expect(response.status).to eq(403) + end + end + + context 'when runner is not shared' do + it 'should not delete runner without access to it' do + delete api("/runners/#{specific_runner.id}", user2) + expect(response.status).to eq(403) + end + + it 'should not delete runner with more than one associated project' do + delete api("/runners/#{two_projects_runner.id}", user) + expect(response.status).to eq(403) + end + + it 'should delete runner for one owned project' do + expect do + delete api("/runners/#{specific_runner.id}", user) + end.to change{ Ci::Runner.specific.count }.by(-1) + expect(response.status).to eq(200) + end + end + end + + context 'unauthorized user' do + it 'should not delete runner' do + delete api("/runners/#{specific_runner.id}") + + expect(response.status).to eq(401) + end + end + end + + describe 'GET /projects/:id/runners' do + context 'authorized user with master privileges' do + it "should return project's runners" do + get api("/projects/#{project.id}/runners", user) + shared = json_response.any?{ |r| r['is_shared'] } + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(shared).to be_truthy + end + end + + context 'authorized user without master privileges' do + it "should not return project's runners" do + get api("/projects/#{project.id}/runners", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it "should not return project's runners" do + get api("/projects/#{project.id}/runners") + + expect(response.status).to eq(401) + end + end + end + + describe 'POST /projects/:id/runners' do + context 'authorized user' do + it 'should enable specific runner' do + specific_runner2 = create(:ci_runner).tap do |runner| + create(:ci_runner_project, runner: runner, project: project2) + end + + expect do + post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id + end.to change{ project.runners.count }.by(+1) + expect(response.status).to eq(201) + end + + it 'should avoid changes when enabling already enabled runner' do + expect do + post api("/projects/#{project.id}/runners", user), runner_id: specific_runner.id + end.to change{ project.runners.count }.by(0) + expect(response.status).to eq(201) + end + + it 'should not enable shared runner' do + post api("/projects/#{project.id}/runners", user), runner_id: shared_runner.id + + expect(response.status).to eq(403) + end + + context 'user is admin' do + it 'should enable any specific runner' do + expect do + post api("/projects/#{project.id}/runners", admin), runner_id: unused_specific_runner.id + end.to change{ project.runners.count }.by(+1) + expect(response.status).to eq(201) + end + end + + context 'user is not admin' do + it 'should not enable runner without access to' do + post api("/projects/#{project.id}/runners", user), runner_id: unused_specific_runner.id + + expect(response.status).to eq(403) + end + end + + it 'should raise an error when no runner_id param is provided' do + post api("/projects/#{project.id}/runners", admin) + + expect(response.status).to eq(400) + end + end + + context 'authorized user without permissions' do + it 'should not enable runner' do + post api("/projects/#{project.id}/runners", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it 'should not enable runner' do + post api("/projects/#{project.id}/runners") + + expect(response.status).to eq(401) + end + end + end + + describe 'DELETE /projects/:id/runners/:runner_id' do + context 'authorized user' do + context 'when runner have more than one associated projects' do + it "should disable project's runner" do + expect do + delete api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user) + end.to change{ project.runners.count }.by(-1) + expect(response.status).to eq(200) + end + end + + context 'when runner have one associated projects' do + it "should not disable project's runner" do + expect do + delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user) + end.to change{ project.runners.count }.by(0) + expect(response.status).to eq(403) + end + end + + it 'should return 404 is runner is not found' do + delete api("/projects/#{project.id}/runners/9999", user) + + expect(response.status).to eq(404) + end + end + + context 'authorized user without permissions' do + it "should not disable project's runner" do + delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it "should not disable project's runner" do + delete api("/projects/#{project.id}/runners/#{specific_runner.id}") + + expect(response.status).to eq(401) + end + end + end +end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 244947762d..57d7eb927f 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -131,28 +131,36 @@ describe Ci::API::API do end describe "PUT /builds/:id" do - let(:commit) { FactoryGirl.create(:ci_commit, project: project)} - let(:build) { FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id) } + let(:commit) {create(:ci_commit, project: project)} + let(:build) { create(:ci_build, :trace, commit: commit, runner_id: runner.id) } - it "should update a running build" do + before do build.run! put ci_api("/builds/#{build.id}"), token: runner.token + end + + it "should update a running build" do expect(response.status).to eq(200) end - it 'Should not override trace information when no trace is given' do - build.run! - build.update!(trace: 'hello_world') - put ci_api("/builds/#{build.id}"), token: runner.token - expect(build.reload.trace).to eq 'hello_world' + it 'should not override trace information when no trace is given' do + expect(build.reload.trace).to eq 'BUILD TRACE' + end + + context 'build has been erased' do + let(:build) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) } + + it 'should respond with forbidden' do + expect(response.status).to eq 403 + end end end context "Artifacts" do let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') } - let(:commit) { FactoryGirl.create(:ci_commit, project: project) } - let(:build) { FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id) } + let(:commit) { create(:ci_commit, project: project) } + let(:build) { create(:ci_build, commit: commit, runner_id: runner.id) } let(:authorize_url) { ci_api("/builds/#{build.id}/artifacts/authorize") } let(:post_url) { ci_api("/builds/#{build.id}/artifacts") } let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") } @@ -160,12 +168,10 @@ describe Ci::API::API do let(:headers) { { "GitLab-Workhorse" => "1.0" } } let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token) } + before { build.run! } + describe "POST /builds/:id/artifacts/authorize" do context "should authorize posting artifact to running build" do - before do - build.run! - end - it "using token as parameter" do post authorize_url, { token: build.token }, headers expect(response.status).to eq(200) @@ -180,10 +186,6 @@ describe Ci::API::API do end context "should fail to post too large artifact" do - before do - build.run! - end - it "using token as parameter" do stub_application_setting(max_artifacts_size: 0) post authorize_url, { token: build.token, filesize: 100 }, headers @@ -197,26 +199,32 @@ describe Ci::API::API do end end - context "should get denied" do - it do - post authorize_url, { token: 'invalid', filesize: 100 } + context 'authorization token is invalid' do + before { post authorize_url, { token: 'invalid', filesize: 100 } } + + it 'should respond with forbidden' do expect(response.status).to eq(403) end end end describe "POST /builds/:id/artifacts" do - context "Disable sanitizer" do + context "disable sanitizer" do before do # by configuring this path we allow to pass temp file from any path allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return('/') end - context "should post artifact to running build" do - before do - build.run! - end + context 'build has been erased' do + let(:build) { create(:ci_build, erased_at: Time.now) } + before { upload_artifacts(file_upload, headers_with_token) } + it 'should respond with forbidden' do + expect(response.status).to eq 403 + end + end + + context "should post artifact to running build" do it "uses regual file post" do upload_artifacts(file_upload, headers_with_token, false) expect(response.status).to eq(201) @@ -245,7 +253,6 @@ describe Ci::API::API do let(:stored_metadata_file) { build.reload.artifacts_metadata.file } before do - build.run! post(post_url, post_data, headers_with_token) end @@ -257,11 +264,8 @@ describe Ci::API::API do 'metadata.name' => metadata.original_filename } end - it 'responds with valid status' do - expect(response.status).to eq(201) - end - it 'stores artifacts and artifacts metadata' do + expect(response.status).to eq(201) expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename) expect(stored_metadata_file.original_filename).to eq(metadata.original_filename) end @@ -282,56 +286,42 @@ describe Ci::API::API do end end - - context "should fail to post too large artifact" do - before do - build.run! - end - - it do + context "artifacts file is too large" do + it "should fail to post too large artifact" do stub_application_setting(max_artifacts_size: 0) upload_artifacts(file_upload, headers_with_token) expect(response.status).to eq(413) end end - context "should fail to post artifacts without file" do - before do - build.run! - end - - it do + context "artifacts post request does not contain file" do + it "should fail to post artifacts without file" do post post_url, {}, headers_with_token expect(response.status).to eq(400) end end - context "should fail to post artifacts without GitLab-Workhorse" do - before do - build.run! - end - - it do + context 'GitLab Workhorse is not configured' do + it "should fail to post artifacts without GitLab-Workhorse" do post post_url, { token: build.token }, {} expect(response.status).to eq(403) end end end - context "should fail to post artifacts for outside of tmp path" do + context "artifacts are being stored outside of tmp path" do before do # by configuring this path we allow to pass file from @tmpdir only # but all temporary files are stored in system tmp directory @tmpdir = Dir.mktmpdir allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return(@tmpdir) - build.run! end after do FileUtils.remove_entry @tmpdir end - it do + it "should fail to post artifacts for outside of tmp path" do upload_artifacts(file_upload, headers_with_token) expect(response.status).to eq(400) end @@ -349,33 +339,37 @@ describe Ci::API::API do end end - describe "DELETE /builds/:id/artifacts" do - before do - build.run! - post delete_url, token: build.token, file: file_upload - end + describe 'DELETE /builds/:id/artifacts' do + let(:build) { create(:ci_build, :artifacts) } + before { delete delete_url, token: build.token } - it "should delete artifact build" do - build.success - delete delete_url, token: build.token + it 'should remove build artifacts' do expect(response.status).to eq(200) + expect(build.artifacts_file.exists?).to be_falsy + expect(build.artifacts_metadata.exists?).to be_falsy end end - describe "GET /builds/:id/artifacts" do - before do - build.run! + describe 'GET /builds/:id/artifacts' do + before { get get_url, token: build.token } + + context 'build has artifacts' do + let(:build) { create(:ci_build, :artifacts) } + let(:download_headers) do + { 'Content-Transfer-Encoding'=>'binary', + 'Content-Disposition'=>'attachment; filename=ci_build_artifacts.zip' } + end + + it 'should download artifact' do + expect(response.status).to eq(200) + expect(response.headers).to include download_headers + end end - it "should download artifact" do - build.update_attributes(artifacts_file: file_upload) - get get_url, token: build.token - expect(response.status).to eq(200) - end - - it "should fail to download if no artifact uploaded" do - get get_url, token: build.token - expect(response.status).to eq(404) + context 'build does not has artifacts' do + it 'should respond with not found' do + expect(response.status).to eq(404) + end end end end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 22ba25217f..538f44e4f3 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -321,12 +321,12 @@ describe Projects::HooksController, 'routing' do end end -# project_commit GET /:project_id/commit/:id(.:format) commit#show {id: /[[:alnum:]]{6,40}/, project_id: /[^\/]+/} +# project_commit GET /:project_id/commit/:id(.:format) commit#show {id: /\h{7,40}/, project_id: /[^\/]+/} describe Projects::CommitController, 'routing' do it 'to #show' do - expect(get('/gitlab/gitlabhq/commit/4246fb')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fb') - expect(get('/gitlab/gitlabhq/commit/4246fb.diff')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fb', format: 'diff') - expect(get('/gitlab/gitlabhq/commit/4246fb.patch')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fb', format: 'patch') + expect(get('/gitlab/gitlabhq/commit/4246fbd')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd') + expect(get('/gitlab/gitlabhq/commit/4246fbd.diff')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd', format: 'diff') + expect(get('/gitlab/gitlabhq/commit/4246fbd.patch')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd', format: 'patch') expect(get('/gitlab/gitlabhq/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5') end end @@ -496,11 +496,11 @@ end describe Projects::ForksController, 'routing' do it 'to #new' do - expect(get('/gitlab/gitlabhq/fork/new')).to route_to('projects/forks#new', namespace_id: 'gitlab', project_id: 'gitlabhq') + expect(get('/gitlab/gitlabhq/forks/new')).to route_to('projects/forks#new', namespace_id: 'gitlab', project_id: 'gitlabhq') end it 'to #create' do - expect(post('/gitlab/gitlabhq/fork')).to route_to('projects/forks#create', namespace_id: 'gitlab', project_id: 'gitlabhq') + expect(post('/gitlab/gitlabhq/forks')).to route_to('projects/forks#create', namespace_id: 'gitlab', project_id: 'gitlabhq') end end diff --git a/spec/services/ci/create_builds_service_spec.rb b/spec/services/ci/create_builds_service_spec.rb new file mode 100644 index 0000000000..1fca362868 --- /dev/null +++ b/spec/services/ci/create_builds_service_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Ci::CreateBuildsService, services: true do + let(:commit) { create(:ci_commit) } + let(:user) { create(:user) } + + describe '#execute' do + # Using stubbed .gitlab-ci.yml created in commit factory + # + + subject do + described_class.new.execute(commit, 'test', 'master', nil, user, nil, status) + end + + context 'next builds available' do + let(:status) { 'success' } + + it { is_expected.to be_an_instance_of Array } + it { is_expected.to all(be_an_instance_of Ci::Build) } + end + + context 'builds skipped' do + let(:status) { 'skipped' } + + it { is_expected.to be_empty } + end + end +end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index c1080ef190..994585fb32 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -5,7 +5,6 @@ describe GitPushService, services: true do let(:user) { create :user } let(:project) { create :project } - let(:service) { GitPushService.new } before do @blankrev = Gitlab::Git::BLANK_SHA @@ -15,34 +14,67 @@ describe GitPushService, services: true do end describe 'Push branches' do + + let(:oldrev) { @oldrev } + let(:newrev) { @newrev } + + subject do + execute_service(project, user, oldrev, newrev, @ref ) + end + context 'new branch' do - subject do - service.execute(project, user, @blankrev, @newrev, @ref) - end + + let(:oldrev) { @blankrev } it { is_expected.to be_truthy } + + it 'flushes general cached data' do + expect(project.repository).to receive(:expire_cache).with('master') + + subject + end + + it 'flushes the visible content cache' do + expect(project.repository).to receive(:expire_has_visible_content_cache) + + subject + end end context 'existing branch' do - subject do - service.execute(project, user, @oldrev, @newrev, @ref) - end it { is_expected.to be_truthy } + + it 'flushes general cached data' do + expect(project.repository).to receive(:expire_cache).with('master') + + subject + end end context 'rm branch' do - subject do - service.execute(project, user, @oldrev, @blankrev, @ref) - end + + let(:newrev) { @blankrev } it { is_expected.to be_truthy } + + it 'flushes the visible content cache' do + expect(project.repository).to receive(:expire_has_visible_content_cache) + + subject + end + + it 'flushes general cached data' do + expect(project.repository).to receive(:expire_cache).with('master') + + subject + end end end describe "Git Push Data" do before do - service.execute(project, user, @oldrev, @newrev, @ref) + service = execute_service(project, user, @oldrev, @newrev, @ref ) @push_data = service.push_data @commit = project.commit(@newrev) end @@ -104,20 +136,21 @@ describe GitPushService, services: true do describe "Push Event" do before do - service.execute(project, user, @oldrev, @newrev, @ref) + service = execute_service(project, user, @oldrev, @newrev, @ref ) @event = Event.last + @push_data = service.push_data end it { expect(@event).not_to be_nil } it { expect(@event.project).to eq(project) } it { expect(@event.action).to eq(Event::PUSHED) } - it { expect(@event.data).to eq(service.push_data) } + it { expect(@event.data).to eq(@push_data) } context "Updates merge requests" do it "when pushing a new branch for the first time" do expect(project).to receive(:update_merge_requests). with(@blankrev, 'newrev', 'refs/heads/master', user) - service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master') + execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) end end end @@ -128,7 +161,7 @@ describe GitPushService, services: true do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: false }) - service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master') + execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) end it "when pushing a branch for the first time with default branch protection disabled" do @@ -137,7 +170,7 @@ describe GitPushService, services: true do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") expect(project.protected_branches).not_to receive(:create) - service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master') + execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) end it "when pushing a branch for the first time with default branch protection set to 'developers can push'" do @@ -146,12 +179,12 @@ describe GitPushService, services: true do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: true }) - service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master') + execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) end it "when pushing new commits to existing branch" do expect(project).to receive(:execute_hooks) - service.execute(project, user, 'oldrev', 'newrev', 'refs/heads/master') + execute_service(project, user, 'oldrev', 'newrev', 'refs/heads/master' ) end end end @@ -174,7 +207,7 @@ describe GitPushService, services: true do it "creates a note if a pushed commit mentions an issue" do expect(SystemNoteService).to receive(:cross_reference).with(issue, commit, commit_author) - service.execute(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, @oldrev, @newrev, @ref ) end it "only creates a cross-reference note if one doesn't already exist" do @@ -182,7 +215,7 @@ describe GitPushService, services: true do expect(SystemNoteService).not_to receive(:cross_reference).with(issue, commit, commit_author) - service.execute(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, @oldrev, @newrev, @ref ) end it "defaults to the pushing user if the commit's author is not known" do @@ -192,7 +225,7 @@ describe GitPushService, services: true do ) expect(SystemNoteService).to receive(:cross_reference).with(issue, commit, user) - service.execute(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, @oldrev, @newrev, @ref ) end it "finds references in the first push to a non-default branch" do @@ -201,7 +234,7 @@ describe GitPushService, services: true do expect(SystemNoteService).to receive(:cross_reference).with(issue, commit, commit_author) - service.execute(project, user, @blankrev, @newrev, 'refs/heads/other') + execute_service(project, user, @blankrev, @newrev, 'refs/heads/other' ) end end @@ -225,18 +258,18 @@ describe GitPushService, services: true do context "to default branches" do it "closes issues" do - service.execute(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, @oldrev, @newrev, @ref ) expect(Issue.find(issue.id)).to be_closed end it "adds a note indicating that the issue is now closed" do expect(SystemNoteService).to receive(:change_status).with(issue, project, commit_author, "closed", closing_commit) - service.execute(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, @oldrev, @newrev, @ref ) end it "doesn't create additional cross-reference notes" do expect(SystemNoteService).not_to receive(:cross_reference) - service.execute(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, @oldrev, @newrev, @ref ) end it "doesn't close issues when external issue tracker is in use" do @@ -244,7 +277,7 @@ describe GitPushService, services: true do # The push still shouldn't create cross-reference notes. expect do - service.execute(project, user, @oldrev, @newrev, 'refs/heads/hurf') + execute_service(project, user, @oldrev, @newrev, 'refs/heads/hurf' ) end.not_to change { Note.where(project_id: project.id, system: true).count } end end @@ -257,11 +290,11 @@ describe GitPushService, services: true do it "creates cross-reference notes" do expect(SystemNoteService).to receive(:cross_reference).with(issue, closing_commit, commit_author) - service.execute(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, @oldrev, @newrev, @ref ) end it "doesn't close issues" do - service.execute(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, @oldrev, @newrev, @ref ) expect(Issue.find(issue.id)).to be_opened end end @@ -298,7 +331,7 @@ describe GitPushService, services: true do let(:message) { "this is some work.\n\nrelated to JIRA-1" } it "should initiate one api call to jira server to mention the issue" do - service.execute(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, @oldrev, @newrev, @ref ) expect(WebMock).to have_requested(:post, jira_api_comment_url).with( body: /mentioned this issue in/ @@ -316,7 +349,7 @@ describe GitPushService, services: true do } }.to_json - service.execute(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, @oldrev, @newrev, @ref ) expect(WebMock).to have_requested(:post, jira_api_transition_url).with( body: transition_body ).once @@ -327,7 +360,7 @@ describe GitPushService, services: true do body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]." }.to_json - service.execute(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, @oldrev, @newrev, @ref ) expect(WebMock).to have_requested(:post, jira_api_comment_url).with( body: comment_body ).once @@ -346,7 +379,13 @@ describe GitPushService, services: true do end it 'push to first branch updates HEAD' do - service.execute(project, user, @blankrev, @newrev, new_ref) + execute_service(project, user, @blankrev, @newrev, new_ref ) end end + + def execute_service(project, user, oldrev, newrev, ref) + service = described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref ) + service.execute + service + end end diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index 3a8daf28f5..62b25709a5 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -5,6 +5,7 @@ describe Issues::CloseService, services: true do let(:user2) { create(:user) } let(:issue) { create(:issue, assignee: user2) } let(:project) { issue.project } + let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) } before do project.team << [user, :master] @@ -32,6 +33,10 @@ describe Issues::CloseService, services: true do note = @issue.notes.last expect(note.note).to include "Status changed to closed" end + + it 'marks todos as done' do + expect(todo.reload).to be_done + end end context "external issue tracker" do @@ -42,6 +47,7 @@ describe Issues::CloseService, services: true do it { expect(@issue).to be_valid } it { expect(@issue).to be_opened } + it { expect(todo.reload).to be_pending } end end end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 2148d091a5..5e7915db7e 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -3,14 +3,18 @@ require 'spec_helper' describe Issues::CreateService, services: true do let(:project) { create(:empty_project) } let(:user) { create(:user) } + let(:assignee) { create(:user) } describe :execute do - context "valid params" do + context 'valid params' do before do project.team << [user, :master] + project.team << [assignee, :master] + opts = { title: 'Awesome issue', - description: 'please fix' + description: 'please fix', + assignee: assignee } @issue = Issues::CreateService.new(project, user, opts).execute @@ -18,6 +22,21 @@ describe Issues::CreateService, services: true do it { expect(@issue).to be_valid } it { expect(@issue.title).to eq('Awesome issue') } + it { expect(@issue.assignee).to eq assignee } + + it 'creates a pending todo for new assignee' do + attributes = { + project: project, + author: user, + user: assignee, + target_id: @issue.id, + target_type: @issue.class.name, + action: Todo::ASSIGNED, + state: :pending + } + + expect(Todo.where(attributes).count).to eq 1 + end end end end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 87da0e9618..e579e49dfa 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -80,6 +80,74 @@ describe Issues::UpdateService, services: true do end end + context 'todos' do + let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) } + + context 'when the title change' do + before do + update_issue({ title: 'New title' }) + end + + it 'marks pending todos as done' do + expect(todo.reload.done?).to eq true + end + end + + context 'when the description change' do + before do + update_issue({ description: 'Also please fix' }) + end + + it 'marks todos as done' do + expect(todo.reload.done?).to eq true + end + end + + context 'when is reassigned' do + before do + update_issue({ assignee: user2 }) + end + + it 'marks previous assignee todos as done' do + expect(todo.reload.done?).to eq true + end + + it 'creates a todo for new assignee' do + attributes = { + project: project, + author: user, + user: user2, + target_id: issue.id, + target_type: issue.class.name, + action: Todo::ASSIGNED, + state: :pending + } + + expect(Todo.where(attributes).count).to eq 1 + end + end + + context 'when the milestone change' do + before do + update_issue({ milestone: create(:milestone) }) + end + + it 'marks todos as done' do + expect(todo.reload.done?).to eq true + end + end + + context 'when the labels change' do + before do + update_issue({ label_ids: [label.id] }) + end + + it 'marks todos as done' do + expect(todo.reload.done?).to eq true + end + end + end + context 'when Issue has tasks' do before { update_issue({ description: "- [ ] Task 1\n- [ ] Task 2" }) } @@ -144,6 +212,5 @@ describe Issues::UpdateService, services: true do end end end - end end diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index 50d0c28879..8443a00e70 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -5,6 +5,7 @@ describe MergeRequests::CloseService, services: true do let(:user2) { create(:user) } let(:merge_request) { create(:merge_request, assignee: user2) } let(:project) { merge_request.project } + let!(:todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) } before do project.team << [user, :master] @@ -41,6 +42,10 @@ describe MergeRequests::CloseService, services: true do note = @merge_request.notes.last expect(note.note).to include 'Status changed to closed' end + + it 'marks todos as done' do + expect(todo.reload).to be_done + end end end end diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index be8f1676ee..120f4d6a66 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe MergeRequests::CreateService, services: true do let(:project) { create(:project) } let(:user) { create(:user) } + let(:assignee) { create(:user) } describe :execute do context 'valid params' do @@ -14,10 +15,12 @@ describe MergeRequests::CreateService, services: true do target_branch: 'master' } end + let(:service) { MergeRequests::CreateService.new(project, user, opts) } before do project.team << [user, :master] + project.team << [assignee, :developer] allow(service).to receive(:execute_hooks) @merge_request = service.execute @@ -25,10 +28,49 @@ describe MergeRequests::CreateService, services: true do it { expect(@merge_request).to be_valid } it { expect(@merge_request.title).to eq('Awesome merge_request') } + it { expect(@merge_request.assignee).to be_nil } it 'should execute hooks with default action' do expect(service).to have_received(:execute_hooks).with(@merge_request) end + + it 'does not creates todos' do + attributes = { + project: project, + target_id: @merge_request.id, + target_type: @merge_request.class.name + } + + expect(Todo.where(attributes).count).to be_zero + end + + context 'when merge request is assigned to someone' do + let(:opts) do + { + title: 'Awesome merge_request', + description: 'please fix', + source_branch: 'feature', + target_branch: 'master', + assignee: assignee + } + end + + it { expect(@merge_request.assignee).to eq assignee } + + it 'creates a todo for new assignee' do + attributes = { + project: project, + author: user, + user: assignee, + target_id: @merge_request.id, + target_type: @merge_request.class.name, + action: Todo::ASSIGNED, + state: :pending + } + + expect(Todo.where(attributes).count).to eq 1 + end + end end end end diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb index de9fed2b7d..f285517cda 100644 --- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb @@ -54,14 +54,68 @@ describe MergeRequests::MergeWhenBuildSucceedsService do end describe "#trigger" do - let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") } + context 'build with ref' do + let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") } - it "merges all merge requests with merge when build succeeds enabled" do - allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) - allow(ci_commit).to receive(:success?).and_return(true) + it "merges all merge requests with merge when build succeeds enabled" do + allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) + allow(ci_commit).to receive(:success?).and_return(true) - expect(MergeWorker).to receive(:perform_async) - service.trigger(build) + expect(MergeWorker).to receive(:perform_async) + service.trigger(build) + end + end + + context 'commit status without ref' do + let(:commit_status) { create(:generic_commit_status, status: 'success') } + + it "doesn't merge a requests for status on other branch" do + allow(project.repository).to receive(:branch_names_contains).with(commit_status.sha).and_return([]) + + expect(MergeWorker).to_not receive(:perform_async) + service.trigger(commit_status) + end + + it 'discovers branches and merges all merge requests when status is success' do + allow(project.repository).to receive(:branch_names_contains). + with(commit_status.sha).and_return([mr_merge_if_green_enabled.source_branch]) + allow(ci_commit).to receive(:success?).and_return(true) + allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) + allow(ci_commit).to receive(:success?).and_return(true) + + expect(MergeWorker).to receive(:perform_async) + service.trigger(commit_status) + end + end + + context 'properly handles multiple stages' do + let(:ref) { mr_merge_if_green_enabled.source_branch } + let(:build) { create(:ci_build, commit: ci_commit, ref: ref, name: 'build', stage: 'build') } + let(:test) { create(:ci_build, commit: ci_commit, ref: ref, name: 'test', stage: 'test') } + + before do + # This behavior of MergeRequest: we instantiate a new object + allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_wrap_original do + Ci::Commit.find(ci_commit.id) + end + + # We create test after the build + allow(ci_commit).to receive(:create_next_builds).and_wrap_original do + test + end + end + + it "doesn't merge if some stages failed" do + expect(MergeWorker).to_not receive(:perform_async) + build.success + test.drop + end + + it 'merge when all stages succeeded' do + expect(MergeWorker).to receive(:perform_async) + build.success + test.success + end end end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 2e9e6e0870..99703c7a8e 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -98,6 +98,84 @@ describe MergeRequests::UpdateService, services: true do end end + context 'todos' do + let!(:pending_todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) } + + context 'when the title change' do + before do + update_merge_request({ title: 'New title' }) + end + + it 'marks pending todos as done' do + expect(pending_todo.reload).to be_done + end + end + + context 'when the description change' do + before do + update_merge_request({ description: 'Also please fix' }) + end + + it 'marks pending todos as done' do + expect(pending_todo.reload).to be_done + end + end + + context 'when is reassigned' do + before do + update_merge_request({ assignee: user2 }) + end + + it 'marks previous assignee pending todos as done' do + expect(pending_todo.reload).to be_done + end + + it 'creates a pending todo for new assignee' do + attributes = { + project: project, + author: user, + user: user2, + target_id: merge_request.id, + target_type: merge_request.class.name, + action: Todo::ASSIGNED, + state: :pending + } + + expect(Todo.where(attributes).count).to eq 1 + end + end + + context 'when the milestone change' do + before do + update_merge_request({ milestone: create(:milestone) }) + end + + it 'marks pending todos as done' do + expect(pending_todo.reload).to be_done + end + end + + context 'when the labels change' do + before do + update_merge_request({ label_ids: [label.id] }) + end + + it 'marks pending todos as done' do + expect(pending_todo.reload).to be_done + end + end + + context 'when the target branch change' do + before do + update_merge_request({ target_branch: 'target' }) + end + + it 'marks pending todos as done' do + expect(pending_todo.reload).to be_done + end + end + end + context 'when MergeRequest has tasks' do before { update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" }) } @@ -130,6 +208,5 @@ describe MergeRequests::UpdateService, services: true do end end end - end end diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index a797a2fe4a..ff23f13e1c 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -14,9 +14,7 @@ describe Notes::CreateService, services: true do noteable_type: 'Issue', noteable_id: issue.id } - - expect(project).to receive(:execute_hooks) - expect(project).to receive(:execute_services) + @note = Notes::CreateService.new(project, user, opts).execute end diff --git a/spec/services/notes/post_process_service_spec.rb b/spec/services/notes/post_process_service_spec.rb new file mode 100644 index 0000000000..d4c50f824c --- /dev/null +++ b/spec/services/notes/post_process_service_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Notes::PostProcessService, services: true do + let(:project) { create(:empty_project) } + let(:issue) { create(:issue, project: project) } + let(:user) { create(:user) } + + describe :execute do + before do + project.team << [user, :master] + note_opts = { + note: 'Awesome comment', + noteable_type: 'Issue', + noteable_id: issue.id + } + + @note = Notes::CreateService.new(project, user, note_opts).execute + end + + it do + expect(project).to receive(:execute_hooks) + expect(project).to receive(:execute_services) + + Notes::PostProcessService.new(@note).execute + end + end +end diff --git a/spec/services/notes/update_service_spec.rb b/spec/services/notes/update_service_spec.rb new file mode 100644 index 0000000000..dde4bde7dc --- /dev/null +++ b/spec/services/notes/update_service_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe Notes::UpdateService, services: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:user2) { create(:user) } + let(:issue) { create(:issue, project: project) } + let(:note) { create(:note, project: project, noteable: issue, author: user, note: 'Old note') } + + before do + project.team << [user, :master] + project.team << [user2, :developer] + end + + describe '#execute' do + def update_note(opts) + @note = Notes::UpdateService.new(project, user, opts).execute(note) + @note.reload + end + + context 'todos' do + let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) } + + context 'when the note change' do + before do + update_note({ note: 'New note' }) + end + + it 'marks todos as done' do + expect(todo.reload).to be_done + end + end + + context 'when the note does not change' do + before do + update_note({ note: 'Old note' }) + end + + it 'keep todos' do + expect(todo.reload).to be_pending + end + end + end + end +end diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb new file mode 100644 index 0000000000..04f474c736 --- /dev/null +++ b/spec/services/projects/import_service_spec.rb @@ -0,0 +1,106 @@ +require 'spec_helper' + +describe Projects::ImportService, services: true do + let!(:project) { create(:empty_project) } + let(:user) { project.creator } + + subject { described_class.new(project, user) } + + describe '#execute' do + context 'with unknown url' do + before do + project.import_url = Project::UNKNOWN_IMPORT_URL + end + + it 'succeeds if repository is created successfully' do + expect(project).to receive(:create_repository).and_return(true) + + result = subject.execute + + expect(result[:status]).to eq :success + end + + it 'fails if repository creation fails' do + expect(project).to receive(:create_repository).and_return(false) + + result = subject.execute + + expect(result[:status]).to eq :error + expect(result[:message]).to eq 'The repository could not be created.' + end + end + + context 'with known url' do + before do + project.import_url = 'https://github.com/vim/vim.git' + end + + it 'succeeds if repository import is successfully' do + expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_return(true) + + result = subject.execute + + expect(result[:status]).to eq :success + end + + it 'fails if repository import fails' do + expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_raise(Gitlab::Shell::Error.new('Failed to import the repository')) + + result = subject.execute + + expect(result[:status]).to eq :error + expect(result[:message]).to eq 'Failed to import the repository' + end + end + + context 'with valid importer' do + before do + stub_github_omniauth_provider + + project.import_url = 'https://github.com/vim/vim.git' + project.import_type = 'github' + + allow(project).to receive(:import_data).and_return(double.as_null_object) + end + + it 'succeeds if importer succeeds' do + expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_return(true) + expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(true) + + result = subject.execute + + expect(result[:status]).to eq :success + end + + it 'fails if importer fails' do + expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_return(true) + expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(false) + + result = subject.execute + + expect(result[:status]).to eq :error + expect(result[:message]).to eq 'The remote data could not be imported.' + end + + it 'fails if importer raise an error' do + expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_return(true) + expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_raise(Projects::ImportService::Error.new('Github: failed to connect API')) + + result = subject.execute + + expect(result[:status]).to eq :error + expect(result[:message]).to eq 'Github: failed to connect API' + end + end + + def stub_github_omniauth_provider + provider = OpenStruct.new( + name: 'github', + app_id: 'asd123', + app_secret: 'asd123' + ) + + Gitlab.config.omniauth.providers << provider + end + end +end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index d3364a7102..1bdc03af12 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -424,6 +424,21 @@ describe SystemNoteService, services: true do to be_falsey end end + + context 'commit with cross-reference from fork' do + let(:author2) { create(:user) } + let(:forked_project) { Projects::ForkService.new(project, author2).execute } + let(:commit2) { forked_project.commit } + + before do + described_class.cross_reference(noteable, commit0, author2) + end + + it 'is true when a fork mentions an external issue' do + expect(described_class.cross_reference_exists?(noteable, commit2)). + to be true + end + end end include JiraServiceHelper diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb new file mode 100644 index 0000000000..96420acb31 --- /dev/null +++ b/spec/services/todo_service_spec.rb @@ -0,0 +1,274 @@ +require 'spec_helper' + +describe TodoService, services: true do + let(:author) { create(:user) } + let(:john_doe) { create(:user, username: 'john_doe') } + let(:michael) { create(:user, username: 'michael') } + let(:stranger) { create(:user, username: 'stranger') } + let(:project) { create(:project) } + let(:mentions) { [author.to_reference, john_doe.to_reference, michael.to_reference, stranger.to_reference].join(' ') } + let(:service) { described_class.new } + + before do + project.team << [author, :developer] + project.team << [john_doe, :developer] + project.team << [michael, :developer] + end + + describe 'Issues' do + let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: mentions) } + let(:unassigned_issue) { create(:issue, project: project, assignee: nil) } + + describe '#new_issue' do + it 'creates a todo if assigned' do + service.new_issue(issue, author) + + should_create_todo(user: john_doe, target: issue, action: Todo::ASSIGNED) + end + + it 'does not create a todo if unassigned' do + should_not_create_any_todo { service.new_issue(unassigned_issue, author) } + end + + it 'does not create a todo if assignee is the current user' do + should_not_create_any_todo { service.new_issue(unassigned_issue, john_doe) } + end + + it 'creates a todo for each valid mentioned user' do + service.new_issue(issue, author) + + should_create_todo(user: michael, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: stranger, target: issue, action: Todo::MENTIONED) + end + end + + describe '#update_issue' do + it 'creates a todo for each valid mentioned user' do + service.update_issue(issue, author) + + should_create_todo(user: michael, target: issue, action: Todo::MENTIONED) + should_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: stranger, target: issue, action: Todo::MENTIONED) + end + + it 'does not create a todo if user was already mentioned' do + create(:todo, :mentioned, user: michael, project: project, target: issue, author: author) + + expect { service.update_issue(issue, author) }.not_to change(michael.todos, :count) + end + end + + describe '#close_issue' do + it 'marks related pending todos to the target for the user as done' do + first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) + second_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) + + service.close_issue(issue, john_doe) + + expect(first_todo.reload).to be_done + expect(second_todo.reload).to be_done + end + end + + describe '#reassigned_issue' do + it 'creates a pending todo for new assignee' do + unassigned_issue.update_attribute(:assignee, john_doe) + service.reassigned_issue(unassigned_issue, author) + + should_create_todo(user: john_doe, target: unassigned_issue, action: Todo::ASSIGNED) + end + + it 'does not create a todo if unassigned' do + issue.update_attribute(:assignee, nil) + + should_not_create_any_todo { service.reassigned_issue(issue, author) } + end + + it 'does not create a todo if new assignee is the current user' do + unassigned_issue.update_attribute(:assignee, john_doe) + + should_not_create_any_todo { service.reassigned_issue(unassigned_issue, john_doe) } + end + end + + describe '#mark_pending_todos_as_done' do + it 'marks related pending todos to the target for the user as done' do + first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) + second_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) + + service.mark_pending_todos_as_done(issue, john_doe) + + expect(first_todo.reload).to be_done + expect(second_todo.reload).to be_done + end + end + + describe '#new_note' do + let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } + let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } + let(:note) { create(:note, project: project, noteable: issue, author: john_doe, note: mentions) } + let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) } + let(:note_on_project_snippet) { create(:note_on_project_snippet, project: project, author: john_doe, note: mentions) } + let(:award_note) { create(:note, :award, project: project, noteable: issue, author: john_doe, note: 'thumbsup') } + let(:system_note) { create(:system_note, project: project, noteable: issue) } + + it 'mark related pending todos to the noteable for the note author as done' do + first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) + second_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) + + service.new_note(note, john_doe) + + expect(first_todo.reload).to be_done + expect(second_todo.reload).to be_done + end + + it 'mark related pending todos to the noteable for the award note author as done' do + service.new_note(award_note, john_doe) + + expect(first_todo.reload).to be_done + expect(second_todo.reload).to be_done + end + + it 'does not mark related pending todos it is a system note' do + service.new_note(system_note, john_doe) + + expect(first_todo.reload).to be_pending + expect(second_todo.reload).to be_pending + end + + it 'creates a todo for each valid mentioned user' do + service.new_note(note, john_doe) + + should_create_todo(user: michael, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) + should_create_todo(user: author, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) + should_not_create_todo(user: john_doe, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) + should_not_create_todo(user: stranger, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) + end + + it 'does not create todo when leaving a note on commit' do + should_not_create_any_todo { service.new_note(note_on_commit, john_doe) } + end + + it 'does not create todo when leaving a note on snippet' do + should_not_create_any_todo { service.new_note(note_on_project_snippet, john_doe) } + end + end + end + + describe 'Merge Requests' do + let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: mentions) } + let(:mr_unassigned) { create(:merge_request, source_project: project, author: author, assignee: nil) } + + describe '#new_merge_request' do + it 'creates a pending todo if assigned' do + service.new_merge_request(mr_assigned, author) + + should_create_todo(user: john_doe, target: mr_assigned, action: Todo::ASSIGNED) + end + + it 'does not create a todo if unassigned' do + should_not_create_any_todo { service.new_merge_request(mr_unassigned, author) } + end + + it 'does not create a todo if assignee is the current user' do + should_not_create_any_todo { service.new_merge_request(mr_unassigned, john_doe) } + end + + it 'creates a todo for each valid mentioned user' do + service.new_merge_request(mr_assigned, author) + + should_create_todo(user: michael, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: stranger, target: mr_assigned, action: Todo::MENTIONED) + end + end + + describe '#update_merge_request' do + it 'creates a todo for each valid mentioned user' do + service.update_merge_request(mr_assigned, author) + + should_create_todo(user: michael, target: mr_assigned, action: Todo::MENTIONED) + should_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: stranger, target: mr_assigned, action: Todo::MENTIONED) + end + + it 'does not create a todo if user was already mentioned' do + create(:todo, :mentioned, user: michael, project: project, target: mr_assigned, author: author) + + expect { service.update_merge_request(mr_assigned, author) }.not_to change(michael.todos, :count) + end + end + + describe '#close_merge_request' do + it 'marks related pending todos to the target for the user as done' do + first_todo = create(:todo, :assigned, user: john_doe, project: project, target: mr_assigned, author: author) + second_todo = create(:todo, :assigned, user: john_doe, project: project, target: mr_assigned, author: author) + service.close_merge_request(mr_assigned, john_doe) + + expect(first_todo.reload).to be_done + expect(second_todo.reload).to be_done + end + end + + describe '#reassigned_merge_request' do + it 'creates a pending todo for new assignee' do + mr_unassigned.update_attribute(:assignee, john_doe) + service.reassigned_merge_request(mr_unassigned, author) + + should_create_todo(user: john_doe, target: mr_unassigned, action: Todo::ASSIGNED) + end + + it 'does not create a todo if unassigned' do + mr_assigned.update_attribute(:assignee, nil) + + should_not_create_any_todo { service.reassigned_merge_request(mr_assigned, author) } + end + + it 'does not create a todo if new assignee is the current user' do + mr_assigned.update_attribute(:assignee, john_doe) + + should_not_create_any_todo { service.reassigned_merge_request(mr_assigned, john_doe) } + end + end + + describe '#merge_merge_request' do + it 'marks related pending todos to the target for the user as done' do + first_todo = create(:todo, :assigned, user: john_doe, project: project, target: mr_assigned, author: author) + second_todo = create(:todo, :assigned, user: john_doe, project: project, target: mr_assigned, author: author) + service.merge_merge_request(mr_assigned, john_doe) + + expect(first_todo.reload).to be_done + expect(second_todo.reload).to be_done + end + end + end + + def should_create_todo(attributes = {}) + attributes.reverse_merge!( + project: project, + author: author, + state: :pending + ) + + expect(Todo.where(attributes).count).to eq 1 + end + + def should_not_create_todo(attributes = {}) + attributes.reverse_merge!( + project: project, + author: author, + state: :pending + ) + + expect(Todo.where(attributes).count).to eq 0 + end + + def should_not_create_any_todo + expect { yield }.not_to change(Todo, :count) + end +end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index fed1ab6ee3..65d59e6813 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -7,7 +7,7 @@ timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10 Capybara.javascript_driver = :poltergeist Capybara.register_driver :poltergeist do |app| - Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout) + Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout, window_size: [1366, 768]) end Capybara.default_wait_time = timeout @@ -19,3 +19,9 @@ unless ENV['CI'] || ENV['CI_SERVER'] # Keep only the screenshots generated from the last failing test suite Capybara::Screenshot.prune_strategy = :keep_last_run end + +RSpec.configure do |config| + config.before(:suite) do + TestEnv.warm_asset_cache + end +end diff --git a/spec/support/email_format_shared_examples.rb b/spec/support/email_format_shared_examples.rb new file mode 100644 index 0000000000..b924a208e7 --- /dev/null +++ b/spec/support/email_format_shared_examples.rb @@ -0,0 +1,44 @@ +# Specifications for behavior common to all objects with an email attribute. +# Takes a list of email-format attributes and requires: +# - subject { "the object with a attribute= setter" } +# Note: You have access to `email_value` which is the email address value +# being currently tested). + +shared_examples 'an object with email-formated attributes' do |*attributes| + attributes.each do |attribute| + describe "specifically its :#{attribute} attribute" do + %w[ + info@example.com + info+test@example.com + o'reilly@example.com + mailto:test@example.com + lol!'+=?><#$%^&*()@gmail.com + ].each do |valid_email| + context "with a value of '#{valid_email}'" do + let(:email_value) { valid_email } + + it 'is valid' do + subject.send("#{attribute}=", valid_email) + + expect(subject).to be_valid + end + end + end + + %w[ + foobar + test@test@example.com + ].each do |invalid_email| + context "with a value of '#{invalid_email}'" do + let(:email_value) { invalid_email } + + it 'is invalid' do + subject.send("#{attribute}=", invalid_email) + + expect(subject).to be_invalid + end + end + end + end + end +end diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb index d6e03cbef3..ef5ea7d626 100644 --- a/spec/support/filter_spec_helper.rb +++ b/spec/support/filter_spec_helper.rb @@ -67,9 +67,9 @@ module FilterSpecHelper if reference =~ /\A(.+)?.\d+\z/ # Integer-based reference with optional project prefix reference.gsub(/\d+\z/) { |i| i.to_i + 1 } - elsif reference =~ /\A(.+@)?(\h{6,40}\z)/ + elsif reference =~ /\A(.+@)?(\h{7,40}\z)/ # SHA-based reference with optional prefix - reference.gsub(/\h{6,40}\z/) { |v| v.reverse } + reference.gsub(/\h{7,40}\z/) { |v| v.reverse } else reference.gsub(/\w+\z/) { |v| v.reverse } end diff --git a/spec/support/project_hook_data_shared_example.rb b/spec/support/project_hook_data_shared_example.rb new file mode 100644 index 0000000000..422083875d --- /dev/null +++ b/spec/support/project_hook_data_shared_example.rb @@ -0,0 +1,27 @@ +RSpec.shared_examples 'project hook data' do |project_key: :project| + it 'contains project data' do + expect(data[project_key][:name]).to eq(project.name) + expect(data[project_key][:description]).to eq(project.description) + expect(data[project_key][:web_url]).to eq(project.web_url) + expect(data[project_key][:avatar_url]).to eq(project.avatar_url) + expect(data[project_key][:git_http_url]).to eq(project.http_url_to_repo) + expect(data[project_key][:git_ssh_url]).to eq(project.ssh_url_to_repo) + expect(data[project_key][:namespace]).to eq(project.namespace.name) + expect(data[project_key][:visibility_level]).to eq(project.visibility_level) + expect(data[project_key][:path_with_namespace]).to eq(project.path_with_namespace) + expect(data[project_key][:default_branch]).to eq(project.default_branch) + expect(data[project_key][:homepage]).to eq(project.web_url) + expect(data[project_key][:url]).to eq(project.url_to_repo) + expect(data[project_key][:ssh_url]).to eq(project.ssh_url_to_repo) + expect(data[project_key][:http_url]).to eq(project.http_url_to_repo) + end +end + +RSpec.shared_examples 'deprecated repository hook data' do |project_key: :project| + it 'contains deprecated repository data' do + expect(data[:repository][:name]).to eq(project.name) + expect(data[:repository][:description]).to eq(project.description) + expect(data[:repository][:url]).to eq(project.url_to_repo) + expect(data[:repository][:homepage]).to eq(project.web_url) + end +end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 4f4743bff6..0d1bd030f3 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -146,6 +146,22 @@ module TestEnv FileUtils.chmod_R 0755, target_repo_path end + # When no cached assets exist, manually hit the root path to create them + # + # Otherwise they'd be created by the first test, often timing out and + # causing a transient test failure + def warm_asset_cache + return if warm_asset_cache? + return unless defined?(Capybara) + + Capybara.current_session.driver.visit '/' + end + + def warm_asset_cache? + cache = Rails.root.join(*%w(tmp cache assets test)) + Dir.exist?(cache) && Dir.entries(cache).length > 2 + end + private def factory_repo_path @@ -172,7 +188,6 @@ module TestEnv 'gitlab-test-fork' end - # Prevent developer git configurations from being persisted to test # repositories def git_env diff --git a/spec/views/devise/shared/_signin_box.html.haml_spec.rb b/spec/views/devise/shared/_signin_box.html.haml_spec.rb new file mode 100644 index 0000000000..05a76ee4bd --- /dev/null +++ b/spec/views/devise/shared/_signin_box.html.haml_spec.rb @@ -0,0 +1,37 @@ +require 'rails_helper' + +describe 'devise/shared/_signin_box' do + describe 'Crowd form' do + before do + stub_devise + assign(:ldap_servers, []) + end + + it 'is shown when Crowd is enabled' do + enable_crowd + + render + + expect(rendered).to have_selector('#tab-crowd form') + end + + it 'is not shown when Crowd is disabled' do + render + + expect(rendered).not_to have_selector('#tab-crowd') + end + end + + def stub_devise + allow(view).to receive(:devise_mapping).and_return(Devise.mappings[:user]) + allow(view).to receive(:resource).and_return(spy) + allow(view).to receive(:resource_name).and_return(:user) + end + + def enable_crowd + allow(view).to receive(:form_based_providers).and_return([:crowd]) + allow(view).to receive(:crowd_enabled?).and_return(true) + allow(view).to receive(:user_omniauth_authorize_path).with('crowd'). + and_return('/crowd') + end +end diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb index dae3199262..172537474e 100644 --- a/spec/workers/repository_fork_worker_spec.rb +++ b/spec/workers/repository_fork_worker_spec.rb @@ -19,6 +19,18 @@ describe RepositoryForkWorker do fork_project.namespace.path) end + it 'flushes the empty caches' do + expect_any_instance_of(Gitlab::Shell).to receive(:fork_repository). + with(project.path_with_namespace, fork_project.namespace.path). + and_return(true) + + expect_any_instance_of(Repository).to receive(:expire_emptiness_caches). + and_call_original + + subject.perform(project.id, project.path_with_namespace, + fork_project.namespace.path) + end + it "handles bad fork" do expect_any_instance_of(Gitlab::Shell).to receive(:fork_repository).and_return(false) subject.perform( diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb new file mode 100644 index 0000000000..6739063543 --- /dev/null +++ b/spec/workers/repository_import_worker_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe RepositoryImportWorker do + let(:project) { create(:project) } + + subject { described_class.new } + + describe '#perform' do + it 'imports a project' do + expect_any_instance_of(Projects::ImportService).to receive(:execute). + and_return({ status: :ok }) + + expect_any_instance_of(Repository).to receive(:expire_emptiness_caches) + expect_any_instance_of(Project).to receive(:import_finish) + + subject.perform(project.id) + end + end +end diff --git a/vendor/assets/javascripts/Chart.js b/vendor/assets/javascripts/Chart.js new file mode 100755 index 0000000000..c264262ba7 --- /dev/null +++ b/vendor/assets/javascripts/Chart.js @@ -0,0 +1,3477 @@ +/*! + * Chart.js + * http://chartjs.org/ + * Version: 1.0.2 + * + * Copyright 2015 Nick Downie + * Released under the MIT license + * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md + */ + + +(function(){ + + "use strict"; + + //Declare root variable - window in the browser, global on the server + var root = this, + previous = root.Chart; + + //Occupy the global variable of Chart, and create a simple base class + var Chart = function(context){ + var chart = this; + this.canvas = context.canvas; + + this.ctx = context; + + //Variables global to the chart + var computeDimension = function(element,dimension) + { + if (element['offset'+dimension]) + { + return element['offset'+dimension]; + } + else + { + return document.defaultView.getComputedStyle(element).getPropertyValue(dimension); + } + } + + var width = this.width = computeDimension(context.canvas,'Width'); + var height = this.height = computeDimension(context.canvas,'Height'); + + // Firefox requires this to work correctly + context.canvas.width = width; + context.canvas.height = height; + + var width = this.width = context.canvas.width; + var height = this.height = context.canvas.height; + this.aspectRatio = this.width / this.height; + //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. + helpers.retinaScale(this); + + return this; + }; + //Globally expose the defaults to allow for user updating/changing + Chart.defaults = { + global: { + // Boolean - Whether to animate the chart + animation: true, + + // Number - Number of animation steps + animationSteps: 60, + + // String - Animation easing effect + animationEasing: "easeOutQuart", + + // Boolean - If we should show the scale at all + showScale: true, + + // Boolean - If we want to override with a hard coded scale + scaleOverride: false, + + // ** Required if scaleOverride is true ** + // Number - The number of steps in a hard coded scale + scaleSteps: null, + // Number - The value jump in the hard coded scale + scaleStepWidth: null, + // Number - The scale starting value + scaleStartValue: null, + + // String - Colour of the scale line + scaleLineColor: "rgba(0,0,0,.1)", + + // Number - Pixel width of the scale line + scaleLineWidth: 1, + + // Boolean - Whether to show labels on the scale + scaleShowLabels: true, + + // Interpolated JS string - can access value + scaleLabel: "<%=value%>", + + // Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there + scaleIntegersOnly: true, + + // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value + scaleBeginAtZero: false, + + // String - Scale label font declaration for the scale label + scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Scale label font size in pixels + scaleFontSize: 12, + + // String - Scale label font weight style + scaleFontStyle: "normal", + + // String - Scale label font colour + scaleFontColor: "#666", + + // Boolean - whether or not the chart should be responsive and resize when the browser does. + responsive: false, + + // Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container + maintainAspectRatio: true, + + // Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove + showTooltips: true, + + // Boolean - Determines whether to draw built-in tooltip or call custom tooltip function + customTooltips: false, + + // Array - Array of string names to attach tooltip events + tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"], + + // String - Tooltip background colour + tooltipFillColor: "rgba(0,0,0,0.8)", + + // String - Tooltip label font declaration for the scale label + tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Tooltip label font size in pixels + tooltipFontSize: 14, + + // String - Tooltip font weight style + tooltipFontStyle: "normal", + + // String - Tooltip label font colour + tooltipFontColor: "#fff", + + // String - Tooltip title font declaration for the scale label + tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Tooltip title font size in pixels + tooltipTitleFontSize: 14, + + // String - Tooltip title font weight style + tooltipTitleFontStyle: "bold", + + // String - Tooltip title font colour + tooltipTitleFontColor: "#fff", + + // Number - pixel width of padding around tooltip text + tooltipYPadding: 6, + + // Number - pixel width of padding around tooltip text + tooltipXPadding: 6, + + // Number - Size of the caret on the tooltip + tooltipCaretSize: 8, + + // Number - Pixel radius of the tooltip border + tooltipCornerRadius: 6, + + // Number - Pixel offset from point x to tooltip edge + tooltipXOffset: 10, + + // String - Template string for single tooltips + tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>", + + // String - Template string for single tooltips + multiTooltipTemplate: "<%= value %>", + + // String - Colour behind the legend colour block + multiTooltipKeyBackground: '#fff', + + // Function - Will fire on animation progression. + onAnimationProgress: function(){}, + + // Function - Will fire on animation completion. + onAnimationComplete: function(){} + + } + }; + + //Create a dictionary of chart types, to allow for extension of existing types + Chart.types = {}; + + //Global Chart helpers object for utility methods and classes + var helpers = Chart.helpers = {}; + + //-- Basic js utility methods + var each = helpers.each = function(loopable,callback,self){ + var additionalArgs = Array.prototype.slice.call(arguments, 3); + // Check to see if null or undefined firstly. + if (loopable){ + if (loopable.length === +loopable.length){ + var i; + for (i=0; i= 0; i--) { + var currentItem = arrayToSearch[i]; + if (filterCallback(currentItem)){ + return currentItem; + } + } + }, + inherits = helpers.inherits = function(extensions){ + //Basic javascript inheritance based on the model created in Backbone.js + var parent = this; + var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); }; + + var Surrogate = function(){ this.constructor = ChartElement;}; + Surrogate.prototype = parent.prototype; + ChartElement.prototype = new Surrogate(); + + ChartElement.extend = inherits; + + if (extensions) extend(ChartElement.prototype, extensions); + + ChartElement.__super__ = parent.prototype; + + return ChartElement; + }, + noop = helpers.noop = function(){}, + uid = helpers.uid = (function(){ + var id=0; + return function(){ + return "chart-" + id++; + }; + })(), + warn = helpers.warn = function(str){ + //Method for warning of errors + if (window.console && typeof window.console.warn == "function") console.warn(str); + }, + amd = helpers.amd = (typeof define == 'function' && define.amd), + //-- Math methods + isNumber = helpers.isNumber = function(n){ + return !isNaN(parseFloat(n)) && isFinite(n); + }, + max = helpers.max = function(array){ + return Math.max.apply( Math, array ); + }, + min = helpers.min = function(array){ + return Math.min.apply( Math, array ); + }, + cap = helpers.cap = function(valueToCap,maxValue,minValue){ + if(isNumber(maxValue)) { + if( valueToCap > maxValue ) { + return maxValue; + } + } + else if(isNumber(minValue)){ + if ( valueToCap < minValue ){ + return minValue; + } + } + return valueToCap; + }, + getDecimalPlaces = helpers.getDecimalPlaces = function(num){ + if (num%1!==0 && isNumber(num)){ + return num.toString().split(".")[1].length; + } + else { + return 0; + } + }, + toRadians = helpers.radians = function(degrees){ + return degrees * (Math.PI/180); + }, + // Gets the angle from vertical upright to the point about a centre. + getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){ + var distanceFromXCenter = anglePoint.x - centrePoint.x, + distanceFromYCenter = anglePoint.y - centrePoint.y, + radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); + + + var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter); + + //If the segment is in the top left quadrant, we need to add another rotation to the angle + if (distanceFromXCenter < 0 && distanceFromYCenter < 0){ + angle += Math.PI*2; + } + + return { + angle: angle, + distance: radialDistanceFromCenter + }; + }, + aliasPixel = helpers.aliasPixel = function(pixelWidth){ + return (pixelWidth % 2 === 0) ? 0 : 0.5; + }, + splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){ + //Props to Rob Spencer at scaled innovation for his post on splining between points + //http://scaledinnovation.com/analytics/splines/aboutSplines.html + var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)), + d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)), + fa=t*d01/(d01+d12),// scaling factor for triangle Ta + fb=t*d12/(d01+d12); + return { + inner : { + x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x), + y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y) + }, + outer : { + x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x), + y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y) + } + }; + }, + calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){ + return Math.floor(Math.log(val) / Math.LN10); + }, + calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){ + + //Set a minimum step of two - a point at the top of the graph, and a point at the base + var minSteps = 2, + maxSteps = Math.floor(drawingSize/(textSize * 1.5)), + skipFitting = (minSteps >= maxSteps); + + var maxValue = max(valuesArray), + minValue = min(valuesArray); + + // We need some degree of seperation here to calculate the scales if all the values are the same + // Adding/minusing 0.5 will give us a range of 1. + if (maxValue === minValue){ + maxValue += 0.5; + // So we don't end up with a graph with a negative start value if we've said always start from zero + if (minValue >= 0.5 && !startFromZero){ + minValue -= 0.5; + } + else{ + // Make up a whole number above the values + maxValue += 0.5; + } + } + + var valueRange = Math.abs(maxValue - minValue), + rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange), + graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), + graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), + graphRange = graphMax - graphMin, + stepValue = Math.pow(10, rangeOrderOfMagnitude), + numberOfSteps = Math.round(graphRange / stepValue); + + //If we have more space on the graph we'll use it to give more definition to the data + while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) { + if(numberOfSteps > maxSteps){ + stepValue *=2; + numberOfSteps = Math.round(graphRange/stepValue); + // Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps. + if (numberOfSteps % 1 !== 0){ + skipFitting = true; + } + } + //We can fit in double the amount of scale points on the scale + else{ + //If user has declared ints only, and the step value isn't a decimal + if (integersOnly && rangeOrderOfMagnitude >= 0){ + //If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float + if(stepValue/2 % 1 === 0){ + stepValue /=2; + numberOfSteps = Math.round(graphRange/stepValue); + } + //If it would make it a float break out of the loop + else{ + break; + } + } + //If the scale doesn't have to be an int, make the scale more granular anyway. + else{ + stepValue /=2; + numberOfSteps = Math.round(graphRange/stepValue); + } + + } + } + + if (skipFitting){ + numberOfSteps = minSteps; + stepValue = graphRange / numberOfSteps; + } + + return { + steps : numberOfSteps, + stepValue : stepValue, + min : graphMin, + max : graphMin + (numberOfSteps * stepValue) + }; + + }, + /* jshint ignore:start */ + // Blows up jshint errors based on the new Function constructor + //Templating methods + //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/ + template = helpers.template = function(templateString, valuesObject){ + + // If templateString is function rather than string-template - call the function for valuesObject + + if(templateString instanceof Function){ + return templateString(valuesObject); + } + + var cache = {}; + function tmpl(str, data){ + // Figure out if we're getting a template, or if we need to + // load the template - and be sure to cache the result. + var fn = !/\W/.test(str) ? + cache[str] = cache[str] : + + // Generate a reusable function that will serve as a template + // generator (and which will be cached). + new Function("obj", + "var p=[],print=function(){p.push.apply(p,arguments);};" + + + // Introduce the data as local variables using with(){} + "with(obj){p.push('" + + + // Convert the template into pure JavaScript + str + .replace(/[\r\t\n]/g, " ") + .split("<%").join("\t") + .replace(/((^|%>)[^\t]*)'/g, "$1\r") + .replace(/\t=(.*?)%>/g, "',$1,'") + .split("\t").join("');") + .split("%>").join("p.push('") + .split("\r").join("\\'") + + "');}return p.join('');" + ); + + // Provide some basic currying to the user + return data ? fn( data ) : fn; + } + return tmpl(templateString,valuesObject); + }, + /* jshint ignore:end */ + generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){ + var labelsArray = new Array(numberOfSteps); + if (labelTemplateString){ + each(labelsArray,function(val,index){ + labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))}); + }); + } + return labelsArray; + }, + //--Animation methods + //Easing functions adapted from Robert Penner's easing equations + //http://www.robertpenner.com/easing/ + easingEffects = helpers.easingEffects = { + linear: function (t) { + return t; + }, + easeInQuad: function (t) { + return t * t; + }, + easeOutQuad: function (t) { + return -1 * t * (t - 2); + }, + easeInOutQuad: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t; + return -1 / 2 * ((--t) * (t - 2) - 1); + }, + easeInCubic: function (t) { + return t * t * t; + }, + easeOutCubic: function (t) { + return 1 * ((t = t / 1 - 1) * t * t + 1); + }, + easeInOutCubic: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t; + return 1 / 2 * ((t -= 2) * t * t + 2); + }, + easeInQuart: function (t) { + return t * t * t * t; + }, + easeOutQuart: function (t) { + return -1 * ((t = t / 1 - 1) * t * t * t - 1); + }, + easeInOutQuart: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t; + return -1 / 2 * ((t -= 2) * t * t * t - 2); + }, + easeInQuint: function (t) { + return 1 * (t /= 1) * t * t * t * t; + }, + easeOutQuint: function (t) { + return 1 * ((t = t / 1 - 1) * t * t * t * t + 1); + }, + easeInOutQuint: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t * t; + return 1 / 2 * ((t -= 2) * t * t * t * t + 2); + }, + easeInSine: function (t) { + return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1; + }, + easeOutSine: function (t) { + return 1 * Math.sin(t / 1 * (Math.PI / 2)); + }, + easeInOutSine: function (t) { + return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1); + }, + easeInExpo: function (t) { + return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1)); + }, + easeOutExpo: function (t) { + return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1); + }, + easeInOutExpo: function (t) { + if (t === 0) return 0; + if (t === 1) return 1; + if ((t /= 1 / 2) < 1) return 1 / 2 * Math.pow(2, 10 * (t - 1)); + return 1 / 2 * (-Math.pow(2, -10 * --t) + 2); + }, + easeInCirc: function (t) { + if (t >= 1) return t; + return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1); + }, + easeOutCirc: function (t) { + return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t); + }, + easeInOutCirc: function (t) { + if ((t /= 1 / 2) < 1) return -1 / 2 * (Math.sqrt(1 - t * t) - 1); + return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1); + }, + easeInElastic: function (t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) return 0; + if ((t /= 1) == 1) return 1; + if (!p) p = 1 * 0.3; + if (a < Math.abs(1)) { + a = 1; + s = p / 4; + } else s = p / (2 * Math.PI) * Math.asin(1 / a); + return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); + }, + easeOutElastic: function (t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) return 0; + if ((t /= 1) == 1) return 1; + if (!p) p = 1 * 0.3; + if (a < Math.abs(1)) { + a = 1; + s = p / 4; + } else s = p / (2 * Math.PI) * Math.asin(1 / a); + return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1; + }, + easeInOutElastic: function (t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) return 0; + if ((t /= 1 / 2) == 2) return 1; + if (!p) p = 1 * (0.3 * 1.5); + if (a < Math.abs(1)) { + a = 1; + s = p / 4; + } else s = p / (2 * Math.PI) * Math.asin(1 / a); + if (t < 1) return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); + return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1; + }, + easeInBack: function (t) { + var s = 1.70158; + return 1 * (t /= 1) * t * ((s + 1) * t - s); + }, + easeOutBack: function (t) { + var s = 1.70158; + return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1); + }, + easeInOutBack: function (t) { + var s = 1.70158; + if ((t /= 1 / 2) < 1) return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)); + return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2); + }, + easeInBounce: function (t) { + return 1 - easingEffects.easeOutBounce(1 - t); + }, + easeOutBounce: function (t) { + if ((t /= 1) < (1 / 2.75)) { + return 1 * (7.5625 * t * t); + } else if (t < (2 / 2.75)) { + return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75); + } else if (t < (2.5 / 2.75)) { + return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375); + } else { + return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375); + } + }, + easeInOutBounce: function (t) { + if (t < 1 / 2) return easingEffects.easeInBounce(t * 2) * 0.5; + return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5; + } + }, + //Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ + requestAnimFrame = helpers.requestAnimFrame = (function(){ + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback) { + return window.setTimeout(callback, 1000 / 60); + }; + })(), + cancelAnimFrame = helpers.cancelAnimFrame = (function(){ + return window.cancelAnimationFrame || + window.webkitCancelAnimationFrame || + window.mozCancelAnimationFrame || + window.oCancelAnimationFrame || + window.msCancelAnimationFrame || + function(callback) { + return window.clearTimeout(callback, 1000 / 60); + }; + })(), + animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){ + + var currentStep = 0, + easingFunction = easingEffects[easingString] || easingEffects.linear; + + var animationFrame = function(){ + currentStep++; + var stepDecimal = currentStep/totalSteps; + var easeDecimal = easingFunction(stepDecimal); + + callback.call(chartInstance,easeDecimal,stepDecimal, currentStep); + onProgress.call(chartInstance,easeDecimal,stepDecimal); + if (currentStep < totalSteps){ + chartInstance.animationFrame = requestAnimFrame(animationFrame); + } else{ + onComplete.apply(chartInstance); + } + }; + requestAnimFrame(animationFrame); + }, + //-- DOM methods + getRelativePosition = helpers.getRelativePosition = function(evt){ + var mouseX, mouseY; + var e = evt.originalEvent || evt, + canvas = evt.currentTarget || evt.srcElement, + boundingRect = canvas.getBoundingClientRect(); + + if (e.touches){ + mouseX = e.touches[0].clientX - boundingRect.left; + mouseY = e.touches[0].clientY - boundingRect.top; + + } + else{ + mouseX = e.clientX - boundingRect.left; + mouseY = e.clientY - boundingRect.top; + } + + return { + x : mouseX, + y : mouseY + }; + + }, + addEvent = helpers.addEvent = function(node,eventType,method){ + if (node.addEventListener){ + node.addEventListener(eventType,method); + } else if (node.attachEvent){ + node.attachEvent("on"+eventType, method); + } else { + node["on"+eventType] = method; + } + }, + removeEvent = helpers.removeEvent = function(node, eventType, handler){ + if (node.removeEventListener){ + node.removeEventListener(eventType, handler, false); + } else if (node.detachEvent){ + node.detachEvent("on"+eventType,handler); + } else{ + node["on" + eventType] = noop; + } + }, + bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){ + // Create the events object if it's not already present + if (!chartInstance.events) chartInstance.events = {}; + + each(arrayOfEvents,function(eventName){ + chartInstance.events[eventName] = function(){ + handler.apply(chartInstance, arguments); + }; + addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]); + }); + }, + unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) { + each(arrayOfEvents, function(handler,eventName){ + removeEvent(chartInstance.chart.canvas, eventName, handler); + }); + }, + getMaximumWidth = helpers.getMaximumWidth = function(domNode){ + var container = domNode.parentNode; + // TODO = check cross browser stuff with this. + return container.clientWidth; + }, + getMaximumHeight = helpers.getMaximumHeight = function(domNode){ + var container = domNode.parentNode; + // TODO = check cross browser stuff with this. + return container.clientHeight; + }, + getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support + retinaScale = helpers.retinaScale = function(chart){ + var ctx = chart.ctx, + width = chart.canvas.width, + height = chart.canvas.height; + + if (window.devicePixelRatio) { + ctx.canvas.style.width = width + "px"; + ctx.canvas.style.height = height + "px"; + ctx.canvas.height = height * window.devicePixelRatio; + ctx.canvas.width = width * window.devicePixelRatio; + ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + } + }, + //-- Canvas methods + clear = helpers.clear = function(chart){ + chart.ctx.clearRect(0,0,chart.width,chart.height); + }, + fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){ + return fontStyle + " " + pixelSize+"px " + fontFamily; + }, + longestText = helpers.longestText = function(ctx,font,arrayOfStrings){ + ctx.font = font; + var longest = 0; + each(arrayOfStrings,function(string){ + var textWidth = ctx.measureText(string).width; + longest = (textWidth > longest) ? textWidth : longest; + }); + return longest; + }, + drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){ + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); + }; + + + //Store a reference to each instance - allowing us to globally resize chart instances on window resize. + //Destroy method on the chart will remove the instance of the chart from this reference. + Chart.instances = {}; + + Chart.Type = function(data,options,chart){ + this.options = options; + this.chart = chart; + this.id = uid(); + //Add the chart instance to the global namespace + Chart.instances[this.id] = this; + + // Initialize is always called when a chart type is created + // By default it is a no op, but it should be extended + if (options.responsive){ + this.resize(); + } + this.initialize.call(this,data); + }; + + //Core methods that'll be a part of every chart type + extend(Chart.Type.prototype,{ + initialize : function(){return this;}, + clear : function(){ + clear(this.chart); + return this; + }, + stop : function(){ + // Stops any current animation loop occuring + cancelAnimFrame(this.animationFrame); + return this; + }, + resize : function(callback){ + this.stop(); + var canvas = this.chart.canvas, + newWidth = getMaximumWidth(this.chart.canvas), + newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas); + + canvas.width = this.chart.width = newWidth; + canvas.height = this.chart.height = newHeight; + + retinaScale(this.chart); + + if (typeof callback === "function"){ + callback.apply(this, Array.prototype.slice.call(arguments, 1)); + } + return this; + }, + reflow : noop, + render : function(reflow){ + if (reflow){ + this.reflow(); + } + if (this.options.animation && !reflow){ + helpers.animationLoop( + this.draw, + this.options.animationSteps, + this.options.animationEasing, + this.options.onAnimationProgress, + this.options.onAnimationComplete, + this + ); + } + else{ + this.draw(); + this.options.onAnimationComplete.call(this); + } + return this; + }, + generateLegend : function(){ + return template(this.options.legendTemplate,this); + }, + destroy : function(){ + this.clear(); + unbindEvents(this, this.events); + var canvas = this.chart.canvas; + + // Reset canvas height/width attributes starts a fresh with the canvas context + canvas.width = this.chart.width; + canvas.height = this.chart.height; + + // < IE9 doesn't support removeProperty + if (canvas.style.removeProperty) { + canvas.style.removeProperty('width'); + canvas.style.removeProperty('height'); + } else { + canvas.style.removeAttribute('width'); + canvas.style.removeAttribute('height'); + } + + delete Chart.instances[this.id]; + }, + showTooltip : function(ChartElements, forceRedraw){ + // Only redraw the chart if we've actually changed what we're hovering on. + if (typeof this.activeElements === 'undefined') this.activeElements = []; + + var isChanged = (function(Elements){ + var changed = false; + + if (Elements.length !== this.activeElements.length){ + changed = true; + return changed; + } + + each(Elements, function(element, index){ + if (element !== this.activeElements[index]){ + changed = true; + } + }, this); + return changed; + }).call(this, ChartElements); + + if (!isChanged && !forceRedraw){ + return; + } + else{ + this.activeElements = ChartElements; + } + this.draw(); + if(this.options.customTooltips){ + this.options.customTooltips(false); + } + if (ChartElements.length > 0){ + // If we have multiple datasets, show a MultiTooltip for all of the data points at that index + if (this.datasets && this.datasets.length > 1) { + var dataArray, + dataIndex; + + for (var i = this.datasets.length - 1; i >= 0; i--) { + dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments; + dataIndex = indexOf(dataArray, ChartElements[0]); + if (dataIndex !== -1){ + break; + } + } + var tooltipLabels = [], + tooltipColors = [], + medianPosition = (function(index) { + + // Get all the points at that particular index + var Elements = [], + dataCollection, + xPositions = [], + yPositions = [], + xMax, + yMax, + xMin, + yMin; + helpers.each(this.datasets, function(dataset){ + dataCollection = dataset.points || dataset.bars || dataset.segments; + if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){ + Elements.push(dataCollection[dataIndex]); + } + }); + + helpers.each(Elements, function(element) { + xPositions.push(element.x); + yPositions.push(element.y); + + + //Include any colour information about the element + tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element)); + tooltipColors.push({ + fill: element._saved.fillColor || element.fillColor, + stroke: element._saved.strokeColor || element.strokeColor + }); + + }, this); + + yMin = min(yPositions); + yMax = max(yPositions); + + xMin = min(xPositions); + xMax = max(xPositions); + + return { + x: (xMin > this.chart.width/2) ? xMin : xMax, + y: (yMin + yMax)/2 + }; + }).call(this, dataIndex); + + new Chart.MultiTooltip({ + x: medianPosition.x, + y: medianPosition.y, + xPadding: this.options.tooltipXPadding, + yPadding: this.options.tooltipYPadding, + xOffset: this.options.tooltipXOffset, + fillColor: this.options.tooltipFillColor, + textColor: this.options.tooltipFontColor, + fontFamily: this.options.tooltipFontFamily, + fontStyle: this.options.tooltipFontStyle, + fontSize: this.options.tooltipFontSize, + titleTextColor: this.options.tooltipTitleFontColor, + titleFontFamily: this.options.tooltipTitleFontFamily, + titleFontStyle: this.options.tooltipTitleFontStyle, + titleFontSize: this.options.tooltipTitleFontSize, + cornerRadius: this.options.tooltipCornerRadius, + labels: tooltipLabels, + legendColors: tooltipColors, + legendColorBackground : this.options.multiTooltipKeyBackground, + title: ChartElements[0].label, + chart: this.chart, + ctx: this.chart.ctx, + custom: this.options.customTooltips + }).draw(); + + } else { + each(ChartElements, function(Element) { + var tooltipPosition = Element.tooltipPosition(); + new Chart.Tooltip({ + x: Math.round(tooltipPosition.x), + y: Math.round(tooltipPosition.y), + xPadding: this.options.tooltipXPadding, + yPadding: this.options.tooltipYPadding, + fillColor: this.options.tooltipFillColor, + textColor: this.options.tooltipFontColor, + fontFamily: this.options.tooltipFontFamily, + fontStyle: this.options.tooltipFontStyle, + fontSize: this.options.tooltipFontSize, + caretHeight: this.options.tooltipCaretSize, + cornerRadius: this.options.tooltipCornerRadius, + text: template(this.options.tooltipTemplate, Element), + chart: this.chart, + custom: this.options.customTooltips + }).draw(); + }, this); + } + } + return this; + }, + toBase64Image : function(){ + return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments); + } + }); + + Chart.Type.extend = function(extensions){ + + var parent = this; + + var ChartType = function(){ + return parent.apply(this,arguments); + }; + + //Copy the prototype object of the this class + ChartType.prototype = clone(parent.prototype); + //Now overwrite some of the properties in the base class with the new extensions + extend(ChartType.prototype, extensions); + + ChartType.extend = Chart.Type.extend; + + if (extensions.name || parent.prototype.name){ + + var chartName = extensions.name || parent.prototype.name; + //Assign any potential default values of the new chart type + + //If none are defined, we'll use a clone of the chart type this is being extended from. + //I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart + //doesn't define some defaults of their own. + + var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {}; + + Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults); + + Chart.types[chartName] = ChartType; + + //Register this new chart type in the Chart prototype + Chart.prototype[chartName] = function(data,options){ + var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {}); + return new ChartType(data,config,this); + }; + } else{ + warn("Name not provided for this chart, so it hasn't been registered"); + } + return parent; + }; + + Chart.Element = function(configuration){ + extend(this,configuration); + this.initialize.apply(this,arguments); + this.save(); + }; + extend(Chart.Element.prototype,{ + initialize : function(){}, + restore : function(props){ + if (!props){ + extend(this,this._saved); + } else { + each(props,function(key){ + this[key] = this._saved[key]; + },this); + } + return this; + }, + save : function(){ + this._saved = clone(this); + delete this._saved._saved; + return this; + }, + update : function(newProps){ + each(newProps,function(value,key){ + this._saved[key] = this[key]; + this[key] = value; + },this); + return this; + }, + transition : function(props,ease){ + each(props,function(value,key){ + this[key] = ((value - this._saved[key]) * ease) + this._saved[key]; + },this); + return this; + }, + tooltipPosition : function(){ + return { + x : this.x, + y : this.y + }; + }, + hasValue: function(){ + return isNumber(this.value); + } + }); + + Chart.Element.extend = inherits; + + + Chart.Point = Chart.Element.extend({ + display: true, + inRange: function(chartX,chartY){ + var hitDetectionRange = this.hitDetectionRadius + this.radius; + return ((Math.pow(chartX-this.x, 2)+Math.pow(chartY-this.y, 2)) < Math.pow(hitDetectionRange,2)); + }, + draw : function(){ + if (this.display){ + var ctx = this.ctx; + ctx.beginPath(); + + ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2); + ctx.closePath(); + + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + ctx.fillStyle = this.fillColor; + + ctx.fill(); + ctx.stroke(); + } + + + //Quick debug for bezier curve splining + //Highlights control points and the line between them. + //Handy for dev - stripped in the min version. + + // ctx.save(); + // ctx.fillStyle = "black"; + // ctx.strokeStyle = "black" + // ctx.beginPath(); + // ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2); + // ctx.fill(); + + // ctx.beginPath(); + // ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2); + // ctx.fill(); + + // ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y); + // ctx.lineTo(this.x, this.y); + // ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y); + // ctx.stroke(); + + // ctx.restore(); + + + + } + }); + + Chart.Arc = Chart.Element.extend({ + inRange : function(chartX,chartY){ + + var pointRelativePosition = helpers.getAngleFromPoint(this, { + x: chartX, + y: chartY + }); + + //Check if within the range of the open/close angle + var betweenAngles = (pointRelativePosition.angle >= this.startAngle && pointRelativePosition.angle <= this.endAngle), + withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius); + + return (betweenAngles && withinRadius); + //Ensure within the outside of the arc centre, but inside arc outer + }, + tooltipPosition : function(){ + var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2), + rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius; + return { + x : this.x + (Math.cos(centreAngle) * rangeFromCentre), + y : this.y + (Math.sin(centreAngle) * rangeFromCentre) + }; + }, + draw : function(animationPercent){ + + var easingDecimal = animationPercent || 1; + + var ctx = this.ctx; + + ctx.beginPath(); + + ctx.arc(this.x, this.y, this.outerRadius, this.startAngle, this.endAngle); + + ctx.arc(this.x, this.y, this.innerRadius, this.endAngle, this.startAngle, true); + + ctx.closePath(); + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + ctx.fillStyle = this.fillColor; + + ctx.fill(); + ctx.lineJoin = 'bevel'; + + if (this.showStroke){ + ctx.stroke(); + } + } + }); + + Chart.Rectangle = Chart.Element.extend({ + draw : function(){ + var ctx = this.ctx, + halfWidth = this.width/2, + leftX = this.x - halfWidth, + rightX = this.x + halfWidth, + top = this.base - (this.base - this.y), + halfStroke = this.strokeWidth / 2; + + // Canvas doesn't allow us to stroke inside the width so we can + // adjust the sizes to fit if we're setting a stroke on the line + if (this.showStroke){ + leftX += halfStroke; + rightX -= halfStroke; + top += halfStroke; + } + + ctx.beginPath(); + + ctx.fillStyle = this.fillColor; + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + // It'd be nice to keep this class totally generic to any rectangle + // and simply specify which border to miss out. + ctx.moveTo(leftX, this.base); + ctx.lineTo(leftX, top); + ctx.lineTo(rightX, top); + ctx.lineTo(rightX, this.base); + ctx.fill(); + if (this.showStroke){ + ctx.stroke(); + } + }, + height : function(){ + return this.base - this.y; + }, + inRange : function(chartX,chartY){ + return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base); + } + }); + + Chart.Tooltip = Chart.Element.extend({ + draw : function(){ + + var ctx = this.chart.ctx; + + ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); + + this.xAlign = "center"; + this.yAlign = "above"; + + //Distance between the actual element.y position and the start of the tooltip caret + var caretPadding = this.caretPadding = 2; + + var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding, + tooltipRectHeight = this.fontSize + 2*this.yPadding, + tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding; + + if (this.x + tooltipWidth/2 >this.chart.width){ + this.xAlign = "left"; + } else if (this.x - tooltipWidth/2 < 0){ + this.xAlign = "right"; + } + + if (this.y - tooltipHeight < 0){ + this.yAlign = "below"; + } + + + var tooltipX = this.x - tooltipWidth/2, + tooltipY = this.y - tooltipHeight; + + ctx.fillStyle = this.fillColor; + + // Custom Tooltips + if(this.custom){ + this.custom(this); + } + else{ + switch(this.yAlign) + { + case "above": + //Draw a caret above the x/y + ctx.beginPath(); + ctx.moveTo(this.x,this.y - caretPadding); + ctx.lineTo(this.x + this.caretHeight, this.y - (caretPadding + this.caretHeight)); + ctx.lineTo(this.x - this.caretHeight, this.y - (caretPadding + this.caretHeight)); + ctx.closePath(); + ctx.fill(); + break; + case "below": + tooltipY = this.y + caretPadding + this.caretHeight; + //Draw a caret below the x/y + ctx.beginPath(); + ctx.moveTo(this.x, this.y + caretPadding); + ctx.lineTo(this.x + this.caretHeight, this.y + caretPadding + this.caretHeight); + ctx.lineTo(this.x - this.caretHeight, this.y + caretPadding + this.caretHeight); + ctx.closePath(); + ctx.fill(); + break; + } + + switch(this.xAlign) + { + case "left": + tooltipX = this.x - tooltipWidth + (this.cornerRadius + this.caretHeight); + break; + case "right": + tooltipX = this.x - (this.cornerRadius + this.caretHeight); + break; + } + + drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius); + + ctx.fill(); + + ctx.fillStyle = this.textColor; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2); + } + } + }); + + Chart.MultiTooltip = Chart.Element.extend({ + initialize : function(){ + this.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); + + this.titleFont = fontString(this.titleFontSize,this.titleFontStyle,this.titleFontFamily); + + this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleFontSize *1.5; + + this.ctx.font = this.titleFont; + + var titleWidth = this.ctx.measureText(this.title).width, + //Label has a legend square as well so account for this. + labelWidth = longestText(this.ctx,this.font,this.labels) + this.fontSize + 3, + longestTextWidth = max([labelWidth,titleWidth]); + + this.width = longestTextWidth + (this.xPadding*2); + + + var halfHeight = this.height/2; + + //Check to ensure the height will fit on the canvas + if (this.y - halfHeight < 0 ){ + this.y = halfHeight; + } else if (this.y + halfHeight > this.chart.height){ + this.y = this.chart.height - halfHeight; + } + + //Decide whether to align left or right based on position on canvas + if (this.x > this.chart.width/2){ + this.x -= this.xOffset + this.width; + } else { + this.x += this.xOffset; + } + + + }, + getLineHeight : function(index){ + var baseLineHeight = this.y - (this.height/2) + this.yPadding, + afterTitleIndex = index-1; + + //If the index is zero, we're getting the title + if (index === 0){ + return baseLineHeight + this.titleFontSize/2; + } else{ + return baseLineHeight + ((this.fontSize*1.5*afterTitleIndex) + this.fontSize/2) + this.titleFontSize * 1.5; + } + + }, + draw : function(){ + // Custom Tooltips + if(this.custom){ + this.custom(this); + } + else{ + drawRoundedRectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerRadius); + var ctx = this.ctx; + ctx.fillStyle = this.fillColor; + ctx.fill(); + ctx.closePath(); + + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.fillStyle = this.titleTextColor; + ctx.font = this.titleFont; + + ctx.fillText(this.title,this.x + this.xPadding, this.getLineHeight(0)); + + ctx.font = this.font; + helpers.each(this.labels,function(label,index){ + ctx.fillStyle = this.textColor; + ctx.fillText(label,this.x + this.xPadding + this.fontSize + 3, this.getLineHeight(index + 1)); + + //A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas) + //ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + //Instead we'll make a white filled block to put the legendColour palette over. + + ctx.fillStyle = this.legendColorBackground; + ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + + ctx.fillStyle = this.legendColors[index].fill; + ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + + + },this); + } + } + }); + + Chart.Scale = Chart.Element.extend({ + initialize : function(){ + this.fit(); + }, + buildYLabels : function(){ + this.yLabels = []; + + var stepDecimalPlaces = getDecimalPlaces(this.stepValue); + + for (var i=0; i<=this.steps; i++){ + this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); + } + this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx,this.font,this.yLabels) : 0; + }, + addXLabel : function(label){ + this.xLabels.push(label); + this.valuesCount++; + this.fit(); + }, + removeXLabel : function(){ + this.xLabels.shift(); + this.valuesCount--; + this.fit(); + }, + // Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use + fit: function(){ + // First we need the width of the yLabels, assuming the xLabels aren't rotated + + // To do that we need the base line at the top and base of the chart, assuming there is no x label rotation + this.startPoint = (this.display) ? this.fontSize : 0; + this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels + + // Apply padding settings to the start and end point. + this.startPoint += this.padding; + this.endPoint -= this.padding; + + // Cache the starting height, so can determine if we need to recalculate the scale yAxis + var cachedHeight = this.endPoint - this.startPoint, + cachedYLabelWidth; + + // Build the current yLabels so we have an idea of what size they'll be to start + /* + * This sets what is returned from calculateScaleRange as static properties of this class: + * + this.steps; + this.stepValue; + this.min; + this.max; + * + */ + this.calculateYRange(cachedHeight); + + // With these properties set we can now build the array of yLabels + // and also the width of the largest yLabel + this.buildYLabels(); + + this.calculateXLabelRotation(); + + while((cachedHeight > this.endPoint - this.startPoint)){ + cachedHeight = this.endPoint - this.startPoint; + cachedYLabelWidth = this.yLabelWidth; + + this.calculateYRange(cachedHeight); + this.buildYLabels(); + + // Only go through the xLabel loop again if the yLabel width has changed + if (cachedYLabelWidth < this.yLabelWidth){ + this.calculateXLabelRotation(); + } + } + + }, + calculateXLabelRotation : function(){ + //Get the width of each grid by calculating the difference + //between x offsets between 0 and 1. + + this.ctx.font = this.font; + + var firstWidth = this.ctx.measureText(this.xLabels[0]).width, + lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width, + firstRotated, + lastRotated; + + + this.xScalePaddingRight = lastWidth/2 + 3; + this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth + 10) ? firstWidth/2 : this.yLabelWidth + 10; + + this.xLabelRotation = 0; + if (this.display){ + var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels), + cosRotation, + firstRotatedWidth; + this.xLabelWidth = originalLabelWidth; + //Allow 3 pixels x2 padding either side for label readability + var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6; + + //Max label rotate should be 90 - also act as a loop counter + while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){ + cosRotation = Math.cos(toRadians(this.xLabelRotation)); + + firstRotated = cosRotation * firstWidth; + lastRotated = cosRotation * lastWidth; + + // We're right aligning the text now. + if (firstRotated + this.fontSize / 2 > this.yLabelWidth + 8){ + this.xScalePaddingLeft = firstRotated + this.fontSize / 2; + } + this.xScalePaddingRight = this.fontSize/2; + + + this.xLabelRotation++; + this.xLabelWidth = cosRotation * originalLabelWidth; + + } + if (this.xLabelRotation > 0){ + this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3; + } + } + else{ + this.xLabelWidth = 0; + this.xScalePaddingRight = this.padding; + this.xScalePaddingLeft = this.padding; + } + + }, + // Needs to be overidden in each Chart type + // Otherwise we need to pass all the data into the scale class + calculateYRange: noop, + drawingArea: function(){ + return this.startPoint - this.endPoint; + }, + calculateY : function(value){ + var scalingFactor = this.drawingArea() / (this.min - this.max); + return this.endPoint - (scalingFactor * (value - this.min)); + }, + calculateX : function(index){ + var isRotated = (this.xLabelRotation > 0), + // innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding, + innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight), + valueWidth = innerWidth/Math.max((this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), 1), + valueOffset = (valueWidth * index) + this.xScalePaddingLeft; + + if (this.offsetGridLines){ + valueOffset += (valueWidth/2); + } + + return Math.round(valueOffset); + }, + update : function(newProps){ + helpers.extend(this, newProps); + this.fit(); + }, + draw : function(){ + var ctx = this.ctx, + yLabelGap = (this.endPoint - this.startPoint) / this.steps, + xStart = Math.round(this.xScalePaddingLeft); + if (this.display){ + ctx.fillStyle = this.textColor; + ctx.font = this.font; + each(this.yLabels,function(labelString,index){ + var yLabelCenter = this.endPoint - (yLabelGap * index), + linePositionY = Math.round(yLabelCenter), + drawHorizontalLine = this.showHorizontalLines; + + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + if (this.showLabels){ + ctx.fillText(labelString,xStart - 10,yLabelCenter); + } + + // This is X axis, so draw it + if (index === 0 && !drawHorizontalLine){ + drawHorizontalLine = true; + } + + if (drawHorizontalLine){ + ctx.beginPath(); + } + + if (index > 0){ + // This is a grid line in the centre, so drop that + ctx.lineWidth = this.gridLineWidth; + ctx.strokeStyle = this.gridLineColor; + } else { + // This is the first line on the scale + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + } + + linePositionY += helpers.aliasPixel(ctx.lineWidth); + + if(drawHorizontalLine){ + ctx.moveTo(xStart, linePositionY); + ctx.lineTo(this.width, linePositionY); + ctx.stroke(); + ctx.closePath(); + } + + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + ctx.beginPath(); + ctx.moveTo(xStart - 5, linePositionY); + ctx.lineTo(xStart, linePositionY); + ctx.stroke(); + ctx.closePath(); + + },this); + + each(this.xLabels,function(label,index){ + var xPos = this.calculateX(index) + aliasPixel(this.lineWidth), + // Check to see if line/bar here and decide where to place the line + linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth), + isRotated = (this.xLabelRotation > 0), + drawVerticalLine = this.showVerticalLines; + + // This is Y axis, so draw it + if (index === 0 && !drawVerticalLine){ + drawVerticalLine = true; + } + + if (drawVerticalLine){ + ctx.beginPath(); + } + + if (index > 0){ + // This is a grid line in the centre, so drop that + ctx.lineWidth = this.gridLineWidth; + ctx.strokeStyle = this.gridLineColor; + } else { + // This is the first line on the scale + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + } + + if (drawVerticalLine){ + ctx.moveTo(linePos,this.endPoint); + ctx.lineTo(linePos,this.startPoint - 3); + ctx.stroke(); + ctx.closePath(); + } + + + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + + + // Small lines at the bottom of the base grid line + ctx.beginPath(); + ctx.moveTo(linePos,this.endPoint); + ctx.lineTo(linePos,this.endPoint + 5); + ctx.stroke(); + ctx.closePath(); + + ctx.save(); + ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8); + ctx.rotate(toRadians(this.xLabelRotation)*-1); + ctx.font = this.font; + ctx.textAlign = (isRotated) ? "right" : "center"; + ctx.textBaseline = (isRotated) ? "middle" : "top"; + ctx.fillText(label, 0, 0); + ctx.restore(); + },this); + + } + } + + }); + + Chart.RadialScale = Chart.Element.extend({ + initialize: function(){ + this.size = min([this.height, this.width]); + this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); + }, + calculateCenterOffset: function(value){ + // Take into account half font size + the yPadding of the top value + var scalingFactor = this.drawingArea / (this.max - this.min); + + return (value - this.min) * scalingFactor; + }, + update : function(){ + if (!this.lineArc){ + this.setScaleSize(); + } else { + this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); + } + this.buildYLabels(); + }, + buildYLabels: function(){ + this.yLabels = []; + + var stepDecimalPlaces = getDecimalPlaces(this.stepValue); + + for (var i=0; i<=this.steps; i++){ + this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); + } + }, + getCircumference : function(){ + return ((Math.PI*2) / this.valuesCount); + }, + setScaleSize: function(){ + /* + * Right, this is really confusing and there is a lot of maths going on here + * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 + * + * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif + * + * Solution: + * + * We assume the radius of the polygon is half the size of the canvas at first + * at each index we check if the text overlaps. + * + * Where it does, we store that angle and that index. + * + * After finding the largest index and angle we calculate how much we need to remove + * from the shape radius to move the point inwards by that x. + * + * We average the left and right distances to get the maximum shape radius that can fit in the box + * along with labels. + * + * Once we have that, we can find the centre point for the chart, by taking the x text protrusion + * on each side, removing that from the size, halving it and adding the left x protrusion width. + * + * This will mean we have a shape fitted to the canvas, as large as it can be with the labels + * and position it in the most space efficient manner + * + * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif + */ + + + // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. + // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points + var largestPossibleRadius = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]), + pointPosition, + i, + textWidth, + halfTextWidth, + furthestRight = this.width, + furthestRightIndex, + furthestRightAngle, + furthestLeft = 0, + furthestLeftIndex, + furthestLeftAngle, + xProtrusionLeft, + xProtrusionRight, + radiusReductionRight, + radiusReductionLeft, + maxWidthRadius; + this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); + for (i=0;i furthestRight) { + furthestRight = pointPosition.x + halfTextWidth; + furthestRightIndex = i; + } + if (pointPosition.x - halfTextWidth < furthestLeft) { + furthestLeft = pointPosition.x - halfTextWidth; + furthestLeftIndex = i; + } + } + else if (i < this.valuesCount/2) { + // Less than half the values means we'll left align the text + if (pointPosition.x + textWidth > furthestRight) { + furthestRight = pointPosition.x + textWidth; + furthestRightIndex = i; + } + } + else if (i > this.valuesCount/2){ + // More than half the values means we'll right align the text + if (pointPosition.x - textWidth < furthestLeft) { + furthestLeft = pointPosition.x - textWidth; + furthestLeftIndex = i; + } + } + } + + xProtrusionLeft = furthestLeft; + + xProtrusionRight = Math.ceil(furthestRight - this.width); + + furthestRightAngle = this.getIndexAngle(furthestRightIndex); + + furthestLeftAngle = this.getIndexAngle(furthestLeftIndex); + + radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI/2); + + radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI/2); + + // Ensure we actually need to reduce the size of the chart + radiusReductionRight = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0; + radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; + + this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2; + + //this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2]) + this.setCenterPoint(radiusReductionLeft, radiusReductionRight); + + }, + setCenterPoint: function(leftMovement, rightMovement){ + + var maxRight = this.width - rightMovement - this.drawingArea, + maxLeft = leftMovement + this.drawingArea; + + this.xCenter = (maxLeft + maxRight)/2; + // Always vertically in the centre as the text height doesn't change + this.yCenter = (this.height/2); + }, + + getIndexAngle : function(index){ + var angleMultiplier = (Math.PI * 2) / this.valuesCount; + // Start from the top instead of right, so remove a quarter of the circle + + return index * angleMultiplier - (Math.PI/2); + }, + getPointPosition : function(index, distanceFromCenter){ + var thisAngle = this.getIndexAngle(index); + return { + x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter, + y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter + }; + }, + draw: function(){ + if (this.display){ + var ctx = this.ctx; + each(this.yLabels, function(label, index){ + // Don't draw a centre value + if (index > 0){ + var yCenterOffset = index * (this.drawingArea/this.steps), + yHeight = this.yCenter - yCenterOffset, + pointPosition; + + // Draw circular lines around the scale + if (this.lineWidth > 0){ + ctx.strokeStyle = this.lineColor; + ctx.lineWidth = this.lineWidth; + + if(this.lineArc){ + ctx.beginPath(); + ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2); + ctx.closePath(); + ctx.stroke(); + } else{ + ctx.beginPath(); + for (var i=0;i= 0; i--) { + if (this.angleLineWidth > 0){ + var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max)); + ctx.beginPath(); + ctx.moveTo(this.xCenter, this.yCenter); + ctx.lineTo(outerPosition.x, outerPosition.y); + ctx.stroke(); + ctx.closePath(); + } + // Extra 3px out for some label spacing + var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5); + ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); + ctx.fillStyle = this.pointLabelFontColor; + + var labelsCount = this.labels.length, + halfLabelsCount = this.labels.length/2, + quarterLabelsCount = halfLabelsCount/2, + upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount), + exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount); + if (i === 0){ + ctx.textAlign = 'center'; + } else if(i === halfLabelsCount){ + ctx.textAlign = 'center'; + } else if (i < halfLabelsCount){ + ctx.textAlign = 'left'; + } else { + ctx.textAlign = 'right'; + } + + // Set the correct text baseline based on outer positioning + if (exactQuarter){ + ctx.textBaseline = 'middle'; + } else if (upperHalf){ + ctx.textBaseline = 'bottom'; + } else { + ctx.textBaseline = 'top'; + } + + ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y); + } + } + } + } + }); + + // Attach global event to resize each chart instance when the browser resizes + helpers.addEvent(window, "resize", (function(){ + // Basic debounce of resize function so it doesn't hurt performance when resizing browser. + var timeout; + return function(){ + clearTimeout(timeout); + timeout = setTimeout(function(){ + each(Chart.instances,function(instance){ + // If the responsive flag is set in the chart instance config + // Cascade the resize event down to the chart. + if (instance.options.responsive){ + instance.resize(instance.render, true); + } + }); + }, 50); + }; + })()); + + + if (amd) { + define(function(){ + return Chart; + }); + } else if (typeof module === 'object' && module.exports) { + module.exports = Chart; + } + + root.Chart = Chart; + + Chart.noConflict = function(){ + root.Chart = previous; + return Chart; + }; + +}).call(this); + +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + + var defaultConfig = { + //Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value + scaleBeginAtZero : true, + + //Boolean - Whether grid lines are shown across the chart + scaleShowGridLines : true, + + //String - Colour of the grid lines + scaleGridLineColor : "rgba(0,0,0,.05)", + + //Number - Width of the grid lines + scaleGridLineWidth : 1, + + //Boolean - Whether to show horizontal lines (except X axis) + scaleShowHorizontalLines: true, + + //Boolean - Whether to show vertical lines (except Y axis) + scaleShowVerticalLines: true, + + //Boolean - If there is a stroke on each bar + barShowStroke : true, + + //Number - Pixel width of the bar stroke + barStrokeWidth : 2, + + //Number - Spacing between each of the X value sets + barValueSpacing : 5, + + //Number - Spacing between data sets within X values + barDatasetSpacing : 1, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" + + }; + + + Chart.Type.extend({ + name: "Bar", + defaults : defaultConfig, + initialize: function(data){ + + //Expose options as a scope variable here so we can access it in the ScaleClass + var options = this.options; + + this.ScaleClass = Chart.Scale.extend({ + offsetGridLines : true, + calculateBarX : function(datasetCount, datasetIndex, barIndex){ + //Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar + var xWidth = this.calculateBaseWidth(), + xAbsolute = this.calculateX(barIndex) - (xWidth/2), + barWidth = this.calculateBarWidth(datasetCount); + + return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2; + }, + calculateBaseWidth : function(){ + return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing); + }, + calculateBarWidth : function(datasetCount){ + //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset + var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing); + + return (baseWidth / datasetCount); + } + }); + + this.datasets = []; + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : []; + + this.eachBars(function(bar){ + bar.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activeBars, function(activeBar){ + activeBar.fillColor = activeBar.highlightFill; + activeBar.strokeColor = activeBar.highlightStroke; + }); + this.showTooltip(activeBars); + }); + } + + //Declare the extension of the default point, to cater for the options passed in to the constructor + this.BarClass = Chart.Rectangle.extend({ + strokeWidth : this.options.barStrokeWidth, + showStroke : this.options.barShowStroke, + ctx : this.chart.ctx + }); + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset,datasetIndex){ + + var datasetObject = { + label : dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + bars : [] + }; + + this.datasets.push(datasetObject); + + helpers.each(dataset.data,function(dataPoint,index){ + //Add a new point for each piece of data, passing any required data to draw. + datasetObject.bars.push(new this.BarClass({ + value : dataPoint, + label : data.labels[index], + datasetLabel: dataset.label, + strokeColor : dataset.strokeColor, + fillColor : dataset.fillColor, + highlightFill : dataset.highlightFill || dataset.fillColor, + highlightStroke : dataset.highlightStroke || dataset.strokeColor + })); + },this); + + },this); + + this.buildScale(data.labels); + + this.BarClass.prototype.base = this.scale.endPoint; + + this.eachBars(function(bar, index, datasetIndex){ + helpers.extend(bar, { + width : this.scale.calculateBarWidth(this.datasets.length), + x: this.scale.calculateBarX(this.datasets.length, datasetIndex, index), + y: this.scale.endPoint + }); + bar.save(); + }, this); + + this.render(); + }, + update : function(){ + this.scale.update(); + // Reset any highlight colours before updating. + helpers.each(this.activeElements, function(activeElement){ + activeElement.restore(['fillColor', 'strokeColor']); + }); + + this.eachBars(function(bar){ + bar.save(); + }); + this.render(); + }, + eachBars : function(callback){ + helpers.each(this.datasets,function(dataset, datasetIndex){ + helpers.each(dataset.bars, callback, this, datasetIndex); + },this); + }, + getBarsAtEvent : function(e){ + var barsArray = [], + eventPosition = helpers.getRelativePosition(e), + datasetIterator = function(dataset){ + barsArray.push(dataset.bars[barIndex]); + }, + barIndex; + + for (var datasetIndex = 0; datasetIndex < this.datasets.length; datasetIndex++) { + for (barIndex = 0; barIndex < this.datasets[datasetIndex].bars.length; barIndex++) { + if (this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){ + helpers.each(this.datasets, datasetIterator); + return barsArray; + } + } + } + + return barsArray; + }, + buildScale : function(labels){ + var self = this; + + var dataTotal = function(){ + var values = []; + self.eachBars(function(bar){ + values.push(bar.value); + }); + return values; + }; + + var scaleOptions = { + templateString : this.options.scaleLabel, + height : this.chart.height, + width : this.chart.width, + ctx : this.chart.ctx, + textColor : this.options.scaleFontColor, + fontSize : this.options.scaleFontSize, + fontStyle : this.options.scaleFontStyle, + fontFamily : this.options.scaleFontFamily, + valuesCount : labels.length, + beginAtZero : this.options.scaleBeginAtZero, + integersOnly : this.options.scaleIntegersOnly, + calculateYRange: function(currentHeight){ + var updatedRanges = helpers.calculateScaleRange( + dataTotal(), + currentHeight, + this.fontSize, + this.beginAtZero, + this.integersOnly + ); + helpers.extend(this, updatedRanges); + }, + xLabels : labels, + font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), + lineWidth : this.options.scaleLineWidth, + lineColor : this.options.scaleLineColor, + showHorizontalLines : this.options.scaleShowHorizontalLines, + showVerticalLines : this.options.scaleShowVerticalLines, + gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, + gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", + padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0, + showLabels : this.options.scaleShowLabels, + display : this.options.showScale + }; + + if (this.options.scaleOverride){ + helpers.extend(scaleOptions, { + calculateYRange: helpers.noop, + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + }); + } + + this.scale = new this.ScaleClass(scaleOptions); + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + helpers.each(valuesArray,function(value,datasetIndex){ + //Add a new point for each piece of data, passing any required data to draw. + this.datasets[datasetIndex].bars.push(new this.BarClass({ + value : value, + label : label, + x: this.scale.calculateBarX(this.datasets.length, datasetIndex, this.scale.valuesCount+1), + y: this.scale.endPoint, + width : this.scale.calculateBarWidth(this.datasets.length), + base : this.scale.endPoint, + strokeColor : this.datasets[datasetIndex].strokeColor, + fillColor : this.datasets[datasetIndex].fillColor + })); + },this); + + this.scale.addXLabel(label); + //Then re-render the chart. + this.update(); + }, + removeData : function(){ + this.scale.removeXLabel(); + //Then re-render the chart. + helpers.each(this.datasets,function(dataset){ + dataset.bars.shift(); + },this); + this.update(); + }, + reflow : function(){ + helpers.extend(this.BarClass.prototype,{ + y: this.scale.endPoint, + base : this.scale.endPoint + }); + var newScaleProps = helpers.extend({ + height : this.chart.height, + width : this.chart.width + }); + this.scale.update(newScaleProps); + }, + draw : function(ease){ + var easingDecimal = ease || 1; + this.clear(); + + var ctx = this.chart.ctx; + + this.scale.draw(easingDecimal); + + //Draw all the bars for each dataset + helpers.each(this.datasets,function(dataset,datasetIndex){ + helpers.each(dataset.bars,function(bar,index){ + if (bar.hasValue()){ + bar.base = this.scale.endPoint; + //Transition then draw + bar.transition({ + x : this.scale.calculateBarX(this.datasets.length, datasetIndex, index), + y : this.scale.calculateY(bar.value), + width : this.scale.calculateBarWidth(this.datasets.length) + }, easingDecimal).draw(); + } + },this); + + },this); + } + }); + + +}).call(this); + +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + //Cache a local reference to Chart.helpers + helpers = Chart.helpers; + + var defaultConfig = { + //Boolean - Whether we should show a stroke on each segment + segmentShowStroke : true, + + //String - The colour of each segment stroke + segmentStrokeColor : "#fff", + + //Number - The width of each segment stroke + segmentStrokeWidth : 2, + + //The percentage of the chart that we cut out of the middle. + percentageInnerCutout : 50, + + //Number - Amount of animation steps + animationSteps : 100, + + //String - Animation easing effect + animationEasing : "easeOutBounce", + + //Boolean - Whether we animate the rotation of the Doughnut + animateRotate : true, + + //Boolean - Whether we animate scaling the Doughnut from the centre + animateScale : false, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" + + }; + + + Chart.Type.extend({ + //Passing in a name registers this chart in the Chart namespace + name: "Doughnut", + //Providing a defaults will also register the deafults in the chart namespace + defaults : defaultConfig, + //Initialize is fired when the chart is initialized - Data is passed in as a parameter + //Config is automatically merged by the core of Chart.js, and is available at this.options + initialize: function(data){ + + //Declare segments as a static property to prevent inheriting across the Chart type prototype + this.segments = []; + this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; + + this.SegmentArc = Chart.Arc.extend({ + ctx : this.chart.ctx, + x : this.chart.width/2, + y : this.chart.height/2 + }); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; + + helpers.each(this.segments,function(segment){ + segment.restore(["fillColor"]); + }); + helpers.each(activeSegments,function(activeSegment){ + activeSegment.fillColor = activeSegment.highlightColor; + }); + this.showTooltip(activeSegments); + }); + } + this.calculateTotal(data); + + helpers.each(data,function(datapoint, index){ + this.addData(datapoint, index, true); + },this); + + this.render(); + }, + getSegmentsAtEvent : function(e){ + var segmentsArray = []; + + var location = helpers.getRelativePosition(e); + + helpers.each(this.segments,function(segment){ + if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); + },this); + return segmentsArray; + }, + addData : function(segment, atIndex, silent){ + var index = atIndex || this.segments.length; + this.segments.splice(index, 0, new this.SegmentArc({ + value : segment.value, + outerRadius : (this.options.animateScale) ? 0 : this.outerRadius, + innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout, + fillColor : segment.color, + highlightColor : segment.highlight || segment.color, + showStroke : this.options.segmentShowStroke, + strokeWidth : this.options.segmentStrokeWidth, + strokeColor : this.options.segmentStrokeColor, + startAngle : Math.PI * 1.5, + circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value), + label : segment.label + })); + if (!silent){ + this.reflow(); + this.update(); + } + }, + calculateCircumference : function(value){ + return (Math.PI*2)*(Math.abs(value) / this.total); + }, + calculateTotal : function(data){ + this.total = 0; + helpers.each(data,function(segment){ + this.total += Math.abs(segment.value); + },this); + }, + update : function(){ + this.calculateTotal(this.segments); + + // Reset any highlight colours before updating. + helpers.each(this.activeElements, function(activeElement){ + activeElement.restore(['fillColor']); + }); + + helpers.each(this.segments,function(segment){ + segment.save(); + }); + this.render(); + }, + + removeData: function(atIndex){ + var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; + this.segments.splice(indexToDelete, 1); + this.reflow(); + this.update(); + }, + + reflow : function(){ + helpers.extend(this.SegmentArc.prototype,{ + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; + helpers.each(this.segments, function(segment){ + segment.update({ + outerRadius : this.outerRadius, + innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout + }); + }, this); + }, + draw : function(easeDecimal){ + var animDecimal = (easeDecimal) ? easeDecimal : 1; + this.clear(); + helpers.each(this.segments,function(segment,index){ + segment.transition({ + circumference : this.calculateCircumference(segment.value), + outerRadius : this.outerRadius, + innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout + },animDecimal); + + segment.endAngle = segment.startAngle + segment.circumference; + + segment.draw(); + if (index === 0){ + segment.startAngle = Math.PI * 1.5; + } + //Check to see if it's the last segment, if not get the next and update the start angle + if (index < this.segments.length-1){ + this.segments[index+1].startAngle = segment.endAngle; + } + },this); + + } + }); + + Chart.types.Doughnut.extend({ + name : "Pie", + defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0}) + }); + +}).call(this); +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + var defaultConfig = { + + ///Boolean - Whether grid lines are shown across the chart + scaleShowGridLines : true, + + //String - Colour of the grid lines + scaleGridLineColor : "rgba(0,0,0,.05)", + + //Number - Width of the grid lines + scaleGridLineWidth : 1, + + //Boolean - Whether to show horizontal lines (except X axis) + scaleShowHorizontalLines: true, + + //Boolean - Whether to show vertical lines (except Y axis) + scaleShowVerticalLines: true, + + //Boolean - Whether the line is curved between points + bezierCurve : true, + + //Number - Tension of the bezier curve between points + bezierCurveTension : 0.4, + + //Boolean - Whether to show a dot for each point + pointDot : true, + + //Number - Radius of each point dot in pixels + pointDotRadius : 4, + + //Number - Pixel width of point dot stroke + pointDotStrokeWidth : 1, + + //Number - amount extra to add to the radius to cater for hit detection outside the drawn point + pointHitDetectionRadius : 20, + + //Boolean - Whether to show a stroke for datasets + datasetStroke : true, + + //Number - Pixel width of dataset stroke + datasetStrokeWidth : 2, + + //Boolean - Whether to fill the dataset with a colour + datasetFill : true, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" + + }; + + + Chart.Type.extend({ + name: "Line", + defaults : defaultConfig, + initialize: function(data){ + //Declare the extension of the default point, to cater for the options passed in to the constructor + this.PointClass = Chart.Point.extend({ + strokeWidth : this.options.pointDotStrokeWidth, + radius : this.options.pointDotRadius, + display: this.options.pointDot, + hitDetectionRadius : this.options.pointHitDetectionRadius, + ctx : this.chart.ctx, + inRange : function(mouseX){ + return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2)); + } + }); + + this.datasets = []; + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; + this.eachPoints(function(point){ + point.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activePoints, function(activePoint){ + activePoint.fillColor = activePoint.highlightFill; + activePoint.strokeColor = activePoint.highlightStroke; + }); + this.showTooltip(activePoints); + }); + } + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset){ + + var datasetObject = { + label : dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + pointColor : dataset.pointColor, + pointStrokeColor : dataset.pointStrokeColor, + points : [] + }; + + this.datasets.push(datasetObject); + + + helpers.each(dataset.data,function(dataPoint,index){ + //Add a new point for each piece of data, passing any required data to draw. + datasetObject.points.push(new this.PointClass({ + value : dataPoint, + label : data.labels[index], + datasetLabel: dataset.label, + strokeColor : dataset.pointStrokeColor, + fillColor : dataset.pointColor, + highlightFill : dataset.pointHighlightFill || dataset.pointColor, + highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor + })); + },this); + + this.buildScale(data.labels); + + + this.eachPoints(function(point, index){ + helpers.extend(point, { + x: this.scale.calculateX(index), + y: this.scale.endPoint + }); + point.save(); + }, this); + + },this); + + + this.render(); + }, + update : function(){ + this.scale.update(); + // Reset any highlight colours before updating. + helpers.each(this.activeElements, function(activeElement){ + activeElement.restore(['fillColor', 'strokeColor']); + }); + this.eachPoints(function(point){ + point.save(); + }); + this.render(); + }, + eachPoints : function(callback){ + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,callback,this); + },this); + }, + getPointsAtEvent : function(e){ + var pointsArray = [], + eventPosition = helpers.getRelativePosition(e); + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,function(point){ + if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point); + }); + },this); + return pointsArray; + }, + buildScale : function(labels){ + var self = this; + + var dataTotal = function(){ + var values = []; + self.eachPoints(function(point){ + values.push(point.value); + }); + + return values; + }; + + var scaleOptions = { + templateString : this.options.scaleLabel, + height : this.chart.height, + width : this.chart.width, + ctx : this.chart.ctx, + textColor : this.options.scaleFontColor, + fontSize : this.options.scaleFontSize, + fontStyle : this.options.scaleFontStyle, + fontFamily : this.options.scaleFontFamily, + valuesCount : labels.length, + beginAtZero : this.options.scaleBeginAtZero, + integersOnly : this.options.scaleIntegersOnly, + calculateYRange : function(currentHeight){ + var updatedRanges = helpers.calculateScaleRange( + dataTotal(), + currentHeight, + this.fontSize, + this.beginAtZero, + this.integersOnly + ); + helpers.extend(this, updatedRanges); + }, + xLabels : labels, + font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), + lineWidth : this.options.scaleLineWidth, + lineColor : this.options.scaleLineColor, + showHorizontalLines : this.options.scaleShowHorizontalLines, + showVerticalLines : this.options.scaleShowVerticalLines, + gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, + gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", + padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth, + showLabels : this.options.scaleShowLabels, + display : this.options.showScale + }; + + if (this.options.scaleOverride){ + helpers.extend(scaleOptions, { + calculateYRange: helpers.noop, + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + }); + } + + + this.scale = new Chart.Scale(scaleOptions); + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + + helpers.each(valuesArray,function(value,datasetIndex){ + //Add a new point for each piece of data, passing any required data to draw. + this.datasets[datasetIndex].points.push(new this.PointClass({ + value : value, + label : label, + x: this.scale.calculateX(this.scale.valuesCount+1), + y: this.scale.endPoint, + strokeColor : this.datasets[datasetIndex].pointStrokeColor, + fillColor : this.datasets[datasetIndex].pointColor + })); + },this); + + this.scale.addXLabel(label); + //Then re-render the chart. + this.update(); + }, + removeData : function(){ + this.scale.removeXLabel(); + //Then re-render the chart. + helpers.each(this.datasets,function(dataset){ + dataset.points.shift(); + },this); + this.update(); + }, + reflow : function(){ + var newScaleProps = helpers.extend({ + height : this.chart.height, + width : this.chart.width + }); + this.scale.update(newScaleProps); + }, + draw : function(ease){ + var easingDecimal = ease || 1; + this.clear(); + + var ctx = this.chart.ctx; + + // Some helper methods for getting the next/prev points + var hasValue = function(item){ + return item.value !== null; + }, + nextPoint = function(point, collection, index){ + return helpers.findNextWhere(collection, hasValue, index) || point; + }, + previousPoint = function(point, collection, index){ + return helpers.findPreviousWhere(collection, hasValue, index) || point; + }; + + this.scale.draw(easingDecimal); + + + helpers.each(this.datasets,function(dataset){ + var pointsWithValues = helpers.where(dataset.points, hasValue); + + //Transition each point first so that the line and point drawing isn't out of sync + //We can use this extra loop to calculate the control points of this dataset also in this loop + + helpers.each(dataset.points, function(point, index){ + if (point.hasValue()){ + point.transition({ + y : this.scale.calculateY(point.value), + x : this.scale.calculateX(index) + }, easingDecimal); + } + },this); + + + // Control points need to be calculated in a seperate loop, because we need to know the current x/y of the point + // This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed + if (this.options.bezierCurve){ + helpers.each(pointsWithValues, function(point, index){ + var tension = (index > 0 && index < pointsWithValues.length - 1) ? this.options.bezierCurveTension : 0; + point.controlPoints = helpers.splineCurve( + previousPoint(point, pointsWithValues, index), + point, + nextPoint(point, pointsWithValues, index), + tension + ); + + // Prevent the bezier going outside of the bounds of the graph + + // Cap puter bezier handles to the upper/lower scale bounds + if (point.controlPoints.outer.y > this.scale.endPoint){ + point.controlPoints.outer.y = this.scale.endPoint; + } + else if (point.controlPoints.outer.y < this.scale.startPoint){ + point.controlPoints.outer.y = this.scale.startPoint; + } + + // Cap inner bezier handles to the upper/lower scale bounds + if (point.controlPoints.inner.y > this.scale.endPoint){ + point.controlPoints.inner.y = this.scale.endPoint; + } + else if (point.controlPoints.inner.y < this.scale.startPoint){ + point.controlPoints.inner.y = this.scale.startPoint; + } + },this); + } + + + //Draw the line between all the points + ctx.lineWidth = this.options.datasetStrokeWidth; + ctx.strokeStyle = dataset.strokeColor; + ctx.beginPath(); + + helpers.each(pointsWithValues, function(point, index){ + if (index === 0){ + ctx.moveTo(point.x, point.y); + } + else{ + if(this.options.bezierCurve){ + var previous = previousPoint(point, pointsWithValues, index); + + ctx.bezierCurveTo( + previous.controlPoints.outer.x, + previous.controlPoints.outer.y, + point.controlPoints.inner.x, + point.controlPoints.inner.y, + point.x, + point.y + ); + } + else{ + ctx.lineTo(point.x,point.y); + } + } + }, this); + + ctx.stroke(); + + if (this.options.datasetFill && pointsWithValues.length > 0){ + //Round off the line by going to the base of the chart, back to the start, then fill. + ctx.lineTo(pointsWithValues[pointsWithValues.length - 1].x, this.scale.endPoint); + ctx.lineTo(pointsWithValues[0].x, this.scale.endPoint); + ctx.fillStyle = dataset.fillColor; + ctx.closePath(); + ctx.fill(); + } + + //Now draw the points over the line + //A little inefficient double looping, but better than the line + //lagging behind the point positions + helpers.each(pointsWithValues,function(point){ + point.draw(); + }); + },this); + } + }); + + +}).call(this); + +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + //Cache a local reference to Chart.helpers + helpers = Chart.helpers; + + var defaultConfig = { + //Boolean - Show a backdrop to the scale label + scaleShowLabelBackdrop : true, + + //String - The colour of the label backdrop + scaleBackdropColor : "rgba(255,255,255,0.75)", + + // Boolean - Whether the scale should begin at zero + scaleBeginAtZero : true, + + //Number - The backdrop padding above & below the label in pixels + scaleBackdropPaddingY : 2, + + //Number - The backdrop padding to the side of the label in pixels + scaleBackdropPaddingX : 2, + + //Boolean - Show line for each value in the scale + scaleShowLine : true, + + //Boolean - Stroke a line around each segment in the chart + segmentShowStroke : true, + + //String - The colour of the stroke on each segement. + segmentStrokeColor : "#fff", + + //Number - The width of the stroke value in pixels + segmentStrokeWidth : 2, + + //Number - Amount of animation steps + animationSteps : 100, + + //String - Animation easing effect. + animationEasing : "easeOutBounce", + + //Boolean - Whether to animate the rotation of the chart + animateRotate : true, + + //Boolean - Whether to animate scaling the chart from the centre + animateScale : false, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" + }; + + + Chart.Type.extend({ + //Passing in a name registers this chart in the Chart namespace + name: "PolarArea", + //Providing a defaults will also register the deafults in the chart namespace + defaults : defaultConfig, + //Initialize is fired when the chart is initialized - Data is passed in as a parameter + //Config is automatically merged by the core of Chart.js, and is available at this.options + initialize: function(data){ + this.segments = []; + //Declare segment class as a chart instance specific class, so it can share props for this instance + this.SegmentArc = Chart.Arc.extend({ + showStroke : this.options.segmentShowStroke, + strokeWidth : this.options.segmentStrokeWidth, + strokeColor : this.options.segmentStrokeColor, + ctx : this.chart.ctx, + innerRadius : 0, + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.scale = new Chart.RadialScale({ + display: this.options.showScale, + fontStyle: this.options.scaleFontStyle, + fontSize: this.options.scaleFontSize, + fontFamily: this.options.scaleFontFamily, + fontColor: this.options.scaleFontColor, + showLabels: this.options.scaleShowLabels, + showLabelBackdrop: this.options.scaleShowLabelBackdrop, + backdropColor: this.options.scaleBackdropColor, + backdropPaddingY : this.options.scaleBackdropPaddingY, + backdropPaddingX: this.options.scaleBackdropPaddingX, + lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, + lineColor: this.options.scaleLineColor, + lineArc: true, + width: this.chart.width, + height: this.chart.height, + xCenter: this.chart.width/2, + yCenter: this.chart.height/2, + ctx : this.chart.ctx, + templateString: this.options.scaleLabel, + valuesCount: data.length + }); + + this.updateScaleRange(data); + + this.scale.update(); + + helpers.each(data,function(segment,index){ + this.addData(segment,index,true); + },this); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; + helpers.each(this.segments,function(segment){ + segment.restore(["fillColor"]); + }); + helpers.each(activeSegments,function(activeSegment){ + activeSegment.fillColor = activeSegment.highlightColor; + }); + this.showTooltip(activeSegments); + }); + } + + this.render(); + }, + getSegmentsAtEvent : function(e){ + var segmentsArray = []; + + var location = helpers.getRelativePosition(e); + + helpers.each(this.segments,function(segment){ + if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); + },this); + return segmentsArray; + }, + addData : function(segment, atIndex, silent){ + var index = atIndex || this.segments.length; + + this.segments.splice(index, 0, new this.SegmentArc({ + fillColor: segment.color, + highlightColor: segment.highlight || segment.color, + label: segment.label, + value: segment.value, + outerRadius: (this.options.animateScale) ? 0 : this.scale.calculateCenterOffset(segment.value), + circumference: (this.options.animateRotate) ? 0 : this.scale.getCircumference(), + startAngle: Math.PI * 1.5 + })); + if (!silent){ + this.reflow(); + this.update(); + } + }, + removeData: function(atIndex){ + var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; + this.segments.splice(indexToDelete, 1); + this.reflow(); + this.update(); + }, + calculateTotal: function(data){ + this.total = 0; + helpers.each(data,function(segment){ + this.total += segment.value; + },this); + this.scale.valuesCount = this.segments.length; + }, + updateScaleRange: function(datapoints){ + var valuesArray = []; + helpers.each(datapoints,function(segment){ + valuesArray.push(segment.value); + }); + + var scaleSizes = (this.options.scaleOverride) ? + { + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + } : + helpers.calculateScaleRange( + valuesArray, + helpers.min([this.chart.width, this.chart.height])/2, + this.options.scaleFontSize, + this.options.scaleBeginAtZero, + this.options.scaleIntegersOnly + ); + + helpers.extend( + this.scale, + scaleSizes, + { + size: helpers.min([this.chart.width, this.chart.height]), + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + } + ); + + }, + update : function(){ + this.calculateTotal(this.segments); + + helpers.each(this.segments,function(segment){ + segment.save(); + }); + + this.reflow(); + this.render(); + }, + reflow : function(){ + helpers.extend(this.SegmentArc.prototype,{ + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.updateScaleRange(this.segments); + this.scale.update(); + + helpers.extend(this.scale,{ + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + }); + + helpers.each(this.segments, function(segment){ + segment.update({ + outerRadius : this.scale.calculateCenterOffset(segment.value) + }); + }, this); + + }, + draw : function(ease){ + var easingDecimal = ease || 1; + //Clear & draw the canvas + this.clear(); + helpers.each(this.segments,function(segment, index){ + segment.transition({ + circumference : this.scale.getCircumference(), + outerRadius : this.scale.calculateCenterOffset(segment.value) + },easingDecimal); + + segment.endAngle = segment.startAngle + segment.circumference; + + // If we've removed the first segment we need to set the first one to + // start at the top. + if (index === 0){ + segment.startAngle = Math.PI * 1.5; + } + + //Check to see if it's the last segment, if not get the next and update the start angle + if (index < this.segments.length - 1){ + this.segments[index+1].startAngle = segment.endAngle; + } + segment.draw(); + }, this); + this.scale.draw(); + } + }); + +}).call(this); +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + + + Chart.Type.extend({ + name: "Radar", + defaults:{ + //Boolean - Whether to show lines for each scale point + scaleShowLine : true, + + //Boolean - Whether we show the angle lines out of the radar + angleShowLineOut : true, + + //Boolean - Whether to show labels on the scale + scaleShowLabels : false, + + // Boolean - Whether the scale should begin at zero + scaleBeginAtZero : true, + + //String - Colour of the angle line + angleLineColor : "rgba(0,0,0,.1)", + + //Number - Pixel width of the angle line + angleLineWidth : 1, + + //String - Point label font declaration + pointLabelFontFamily : "'Arial'", + + //String - Point label font weight + pointLabelFontStyle : "normal", + + //Number - Point label font size in pixels + pointLabelFontSize : 10, + + //String - Point label font colour + pointLabelFontColor : "#666", + + //Boolean - Whether to show a dot for each point + pointDot : true, + + //Number - Radius of each point dot in pixels + pointDotRadius : 3, + + //Number - Pixel width of point dot stroke + pointDotStrokeWidth : 1, + + //Number - amount extra to add to the radius to cater for hit detection outside the drawn point + pointHitDetectionRadius : 20, + + //Boolean - Whether to show a stroke for datasets + datasetStroke : true, + + //Number - Pixel width of dataset stroke + datasetStrokeWidth : 2, + + //Boolean - Whether to fill the dataset with a colour + datasetFill : true, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" + + }, + + initialize: function(data){ + this.PointClass = Chart.Point.extend({ + strokeWidth : this.options.pointDotStrokeWidth, + radius : this.options.pointDotRadius, + display: this.options.pointDot, + hitDetectionRadius : this.options.pointHitDetectionRadius, + ctx : this.chart.ctx + }); + + this.datasets = []; + + this.buildScale(data); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; + + this.eachPoints(function(point){ + point.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activePointsCollection, function(activePoint){ + activePoint.fillColor = activePoint.highlightFill; + activePoint.strokeColor = activePoint.highlightStroke; + }); + + this.showTooltip(activePointsCollection); + }); + } + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset){ + + var datasetObject = { + label: dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + pointColor : dataset.pointColor, + pointStrokeColor : dataset.pointStrokeColor, + points : [] + }; + + this.datasets.push(datasetObject); + + helpers.each(dataset.data,function(dataPoint,index){ + //Add a new point for each piece of data, passing any required data to draw. + var pointPosition; + if (!this.scale.animation){ + pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(dataPoint)); + } + datasetObject.points.push(new this.PointClass({ + value : dataPoint, + label : data.labels[index], + datasetLabel: dataset.label, + x: (this.options.animation) ? this.scale.xCenter : pointPosition.x, + y: (this.options.animation) ? this.scale.yCenter : pointPosition.y, + strokeColor : dataset.pointStrokeColor, + fillColor : dataset.pointColor, + highlightFill : dataset.pointHighlightFill || dataset.pointColor, + highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor + })); + },this); + + },this); + + this.render(); + }, + eachPoints : function(callback){ + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,callback,this); + },this); + }, + + getPointsAtEvent : function(evt){ + var mousePosition = helpers.getRelativePosition(evt), + fromCenter = helpers.getAngleFromPoint({ + x: this.scale.xCenter, + y: this.scale.yCenter + }, mousePosition); + + var anglePerIndex = (Math.PI * 2) /this.scale.valuesCount, + pointIndex = Math.round((fromCenter.angle - Math.PI * 1.5) / anglePerIndex), + activePointsCollection = []; + + // If we're at the top, make the pointIndex 0 to get the first of the array. + if (pointIndex >= this.scale.valuesCount || pointIndex < 0){ + pointIndex = 0; + } + + if (fromCenter.distance <= this.scale.drawingArea){ + helpers.each(this.datasets, function(dataset){ + activePointsCollection.push(dataset.points[pointIndex]); + }); + } + + return activePointsCollection; + }, + + buildScale : function(data){ + this.scale = new Chart.RadialScale({ + display: this.options.showScale, + fontStyle: this.options.scaleFontStyle, + fontSize: this.options.scaleFontSize, + fontFamily: this.options.scaleFontFamily, + fontColor: this.options.scaleFontColor, + showLabels: this.options.scaleShowLabels, + showLabelBackdrop: this.options.scaleShowLabelBackdrop, + backdropColor: this.options.scaleBackdropColor, + backdropPaddingY : this.options.scaleBackdropPaddingY, + backdropPaddingX: this.options.scaleBackdropPaddingX, + lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, + lineColor: this.options.scaleLineColor, + angleLineColor : this.options.angleLineColor, + angleLineWidth : (this.options.angleShowLineOut) ? this.options.angleLineWidth : 0, + // Point labels at the edge of each line + pointLabelFontColor : this.options.pointLabelFontColor, + pointLabelFontSize : this.options.pointLabelFontSize, + pointLabelFontFamily : this.options.pointLabelFontFamily, + pointLabelFontStyle : this.options.pointLabelFontStyle, + height : this.chart.height, + width: this.chart.width, + xCenter: this.chart.width/2, + yCenter: this.chart.height/2, + ctx : this.chart.ctx, + templateString: this.options.scaleLabel, + labels: data.labels, + valuesCount: data.datasets[0].data.length + }); + + this.scale.setScaleSize(); + this.updateScaleRange(data.datasets); + this.scale.buildYLabels(); + }, + updateScaleRange: function(datasets){ + var valuesArray = (function(){ + var totalDataArray = []; + helpers.each(datasets,function(dataset){ + if (dataset.data){ + totalDataArray = totalDataArray.concat(dataset.data); + } + else { + helpers.each(dataset.points, function(point){ + totalDataArray.push(point.value); + }); + } + }); + return totalDataArray; + })(); + + + var scaleSizes = (this.options.scaleOverride) ? + { + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + } : + helpers.calculateScaleRange( + valuesArray, + helpers.min([this.chart.width, this.chart.height])/2, + this.options.scaleFontSize, + this.options.scaleBeginAtZero, + this.options.scaleIntegersOnly + ); + + helpers.extend( + this.scale, + scaleSizes + ); + + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + this.scale.valuesCount++; + helpers.each(valuesArray,function(value,datasetIndex){ + var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value)); + this.datasets[datasetIndex].points.push(new this.PointClass({ + value : value, + label : label, + x: pointPosition.x, + y: pointPosition.y, + strokeColor : this.datasets[datasetIndex].pointStrokeColor, + fillColor : this.datasets[datasetIndex].pointColor + })); + },this); + + this.scale.labels.push(label); + + this.reflow(); + + this.update(); + }, + removeData : function(){ + this.scale.valuesCount--; + this.scale.labels.shift(); + helpers.each(this.datasets,function(dataset){ + dataset.points.shift(); + },this); + this.reflow(); + this.update(); + }, + update : function(){ + this.eachPoints(function(point){ + point.save(); + }); + this.reflow(); + this.render(); + }, + reflow: function(){ + helpers.extend(this.scale, { + width : this.chart.width, + height: this.chart.height, + size : helpers.min([this.chart.width, this.chart.height]), + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + }); + this.updateScaleRange(this.datasets); + this.scale.setScaleSize(); + this.scale.buildYLabels(); + }, + draw : function(ease){ + var easeDecimal = ease || 1, + ctx = this.chart.ctx; + this.clear(); + this.scale.draw(); + + helpers.each(this.datasets,function(dataset){ + + //Transition each point first so that the line and point drawing isn't out of sync + helpers.each(dataset.points,function(point,index){ + if (point.hasValue()){ + point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal); + } + },this); + + + + //Draw the line between all the points + ctx.lineWidth = this.options.datasetStrokeWidth; + ctx.strokeStyle = dataset.strokeColor; + ctx.beginPath(); + helpers.each(dataset.points,function(point,index){ + if (index === 0){ + ctx.moveTo(point.x,point.y); + } + else{ + ctx.lineTo(point.x,point.y); + } + },this); + ctx.closePath(); + ctx.stroke(); + + ctx.fillStyle = dataset.fillColor; + ctx.fill(); + + //Now draw the points over the line + //A little inefficient double looping, but better than the line + //lagging behind the point positions + helpers.each(dataset.points,function(point){ + if (point.hasValue()){ + point.draw(); + } + }); + + },this); + + } + + }); + + + + + +}).call(this); \ No newline at end of file diff --git a/vendor/assets/javascripts/fuzzaldrin-plus.js b/vendor/assets/javascripts/fuzzaldrin-plus.js new file mode 100644 index 0000000000..1985e3f8f6 --- /dev/null +++ b/vendor/assets/javascripts/fuzzaldrin-plus.js @@ -0,0 +1,1161 @@ +/*! + * fuzzaldrin-plus.js - 0.3.1 + * https://github.com/jeancroy/fuzzaldrin-plus + * + * Copyright 2016 - Jean Christophe Roy + * Released under the MIT license + * https://github.com/jeancroy/fuzzaldrin-plus/raw/master/LICENSE.md + */ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 ? maxInners : candidates.length; + bAllowErrors = !!allowErrors; + bKey = key != null; + prepQuery = scorer.prepQuery(query); + if (!legacy) { + for (_i = 0, _len = candidates.length; _i < _len; _i++) { + candidate = candidates[_i]; + string = bKey ? candidate[key] : candidate; + if (!string) { + continue; + } + score = scorer.score(string, query, prepQuery, bAllowErrors); + if (score > 0) { + scoredCandidates.push({ + candidate: candidate, + score: score + }); + if (!--spotLeft) { + break; + } + } + } + } else { + queryHasSlashes = prepQuery.depth > 0; + coreQuery = prepQuery.core; + for (_j = 0, _len1 = candidates.length; _j < _len1; _j++) { + candidate = candidates[_j]; + string = key != null ? candidate[key] : candidate; + if (!string) { + continue; + } + score = legacy_scorer.score(string, coreQuery, queryHasSlashes); + if (!queryHasSlashes) { + score = legacy_scorer.basenameScore(string, coreQuery, score); + } + if (score > 0) { + scoredCandidates.push({ + candidate: candidate, + score: score + }); + } + } + } + scoredCandidates.sort(sortCandidates); + candidates = scoredCandidates.map(pluckCandidates); + if (maxResults != null) { + candidates = candidates.slice(0, maxResults); + } + return candidates; + }; + +}).call(this); + +},{"./legacy":4,"./scorer":6,"path":7}],3:[function(require,module,exports){ +(function() { + var PathSeparator, filter, legacy_scorer, matcher, prepQueryCache, scorer; + + scorer = require('./scorer'); + + legacy_scorer = require('./legacy'); + + filter = require('./filter'); + + matcher = require('./matcher'); + + PathSeparator = require('path').sep; + + prepQueryCache = null; + + module.exports = { + filter: function(candidates, query, options) { + if (!((query != null ? query.length : void 0) && (candidates != null ? candidates.length : void 0))) { + return []; + } + return filter(candidates, query, options); + }, + prepQuery: function(query) { + return scorer.prepQuery(query); + }, + score: function(string, query, prepQuery, _arg) { + var allowErrors, coreQuery, legacy, queryHasSlashes, score, _ref; + _ref = _arg != null ? _arg : {}, allowErrors = _ref.allowErrors, legacy = _ref.legacy; + if (!((string != null ? string.length : void 0) && (query != null ? query.length : void 0))) { + return 0; + } + if (prepQuery == null) { + prepQuery = prepQueryCache && prepQueryCache.query === query ? prepQueryCache : (prepQueryCache = scorer.prepQuery(query)); + } + if (!legacy) { + score = scorer.score(string, query, prepQuery, !!allowErrors); + } else { + queryHasSlashes = prepQuery.depth > 0; + coreQuery = prepQuery.core; + score = legacy_scorer.score(string, coreQuery, queryHasSlashes); + if (!queryHasSlashes) { + score = legacy_scorer.basenameScore(string, coreQuery, score); + } + } + return score; + }, + match: function(string, query, prepQuery, _arg) { + var allowErrors, baseMatches, matches, query_lw, string_lw, _i, _ref, _results; + allowErrors = (_arg != null ? _arg : {}).allowErrors; + if (!string) { + return []; + } + if (!query) { + return []; + } + if (string === query) { + return (function() { + _results = []; + for (var _i = 0, _ref = string.length; 0 <= _ref ? _i < _ref : _i > _ref; 0 <= _ref ? _i++ : _i--){ _results.push(_i); } + return _results; + }).apply(this); + } + if (prepQuery == null) { + prepQuery = prepQueryCache && prepQueryCache.query === query ? prepQueryCache : (prepQueryCache = scorer.prepQuery(query)); + } + if (!(allowErrors || scorer.isMatch(string, prepQuery.core_lw, prepQuery.core_up))) { + return []; + } + string_lw = string.toLowerCase(); + query_lw = prepQuery.query_lw; + matches = matcher.match(string, string_lw, prepQuery); + if (matches.length === 0) { + return matches; + } + if (string.indexOf(PathSeparator) > -1) { + baseMatches = matcher.basenameMatch(string, string_lw, prepQuery); + matches = matcher.mergeMatches(matches, baseMatches); + } + return matches; + } + }; + +}).call(this); + +},{"./filter":2,"./legacy":4,"./matcher":5,"./scorer":6,"path":7}],4:[function(require,module,exports){ +(function() { + var PathSeparator, queryIsLastPathSegment; + + PathSeparator = require('path').sep; + + exports.basenameScore = function(string, query, score) { + var base, depth, index, lastCharacter, segmentCount, slashCount; + index = string.length - 1; + while (string[index] === PathSeparator) { + index--; + } + slashCount = 0; + lastCharacter = index; + base = null; + while (index >= 0) { + if (string[index] === PathSeparator) { + slashCount++; + if (base == null) { + base = string.substring(index + 1, lastCharacter + 1); + } + } else if (index === 0) { + if (lastCharacter < string.length - 1) { + if (base == null) { + base = string.substring(0, lastCharacter + 1); + } + } else { + if (base == null) { + base = string; + } + } + } + index--; + } + if (base === string) { + score *= 2; + } else if (base) { + score += exports.score(base, query); + } + segmentCount = slashCount + 1; + depth = Math.max(1, 10 - segmentCount); + score *= depth * 0.01; + return score; + }; + + exports.score = function(string, query) { + var character, characterScore, indexInQuery, indexInString, lowerCaseIndex, minIndex, queryLength, queryScore, stringLength, totalCharacterScore, upperCaseIndex, _ref; + if (string === query) { + return 1; + } + if (queryIsLastPathSegment(string, query)) { + return 1; + } + totalCharacterScore = 0; + queryLength = query.length; + stringLength = string.length; + indexInQuery = 0; + indexInString = 0; + while (indexInQuery < queryLength) { + character = query[indexInQuery++]; + lowerCaseIndex = string.indexOf(character.toLowerCase()); + upperCaseIndex = string.indexOf(character.toUpperCase()); + minIndex = Math.min(lowerCaseIndex, upperCaseIndex); + if (minIndex === -1) { + minIndex = Math.max(lowerCaseIndex, upperCaseIndex); + } + indexInString = minIndex; + if (indexInString === -1) { + return 0; + } + characterScore = 0.1; + if (string[indexInString] === character) { + characterScore += 0.1; + } + if (indexInString === 0 || string[indexInString - 1] === PathSeparator) { + characterScore += 0.8; + } else if ((_ref = string[indexInString - 1]) === '-' || _ref === '_' || _ref === ' ') { + characterScore += 0.7; + } + string = string.substring(indexInString + 1, stringLength); + totalCharacterScore += characterScore; + } + queryScore = totalCharacterScore / queryLength; + return ((queryScore * (queryLength / stringLength)) + queryScore) / 2; + }; + + queryIsLastPathSegment = function(string, query) { + if (string[string.length - query.length - 1] === PathSeparator) { + return string.lastIndexOf(query) === string.length - query.length; + } + }; + + exports.match = function(string, query, stringOffset) { + var character, indexInQuery, indexInString, lowerCaseIndex, matches, minIndex, queryLength, stringLength, upperCaseIndex, _i, _ref, _results; + if (stringOffset == null) { + stringOffset = 0; + } + if (string === query) { + return (function() { + _results = []; + for (var _i = stringOffset, _ref = stringOffset + string.length; stringOffset <= _ref ? _i < _ref : _i > _ref; stringOffset <= _ref ? _i++ : _i--){ _results.push(_i); } + return _results; + }).apply(this); + } + queryLength = query.length; + stringLength = string.length; + indexInQuery = 0; + indexInString = 0; + matches = []; + while (indexInQuery < queryLength) { + character = query[indexInQuery++]; + lowerCaseIndex = string.indexOf(character.toLowerCase()); + upperCaseIndex = string.indexOf(character.toUpperCase()); + minIndex = Math.min(lowerCaseIndex, upperCaseIndex); + if (minIndex === -1) { + minIndex = Math.max(lowerCaseIndex, upperCaseIndex); + } + indexInString = minIndex; + if (indexInString === -1) { + return []; + } + matches.push(stringOffset + indexInString); + stringOffset += indexInString + 1; + string = string.substring(indexInString + 1, stringLength); + } + return matches; + }; + +}).call(this); + +},{"path":7}],5:[function(require,module,exports){ +(function() { + var PathSeparator, scorer; + + PathSeparator = require('path').sep; + + scorer = require('./scorer'); + + exports.basenameMatch = function(subject, subject_lw, prepQuery) { + var basePos, depth, end; + end = subject.length - 1; + while (subject[end] === PathSeparator) { + end--; + } + basePos = subject.lastIndexOf(PathSeparator, end); + if (basePos === -1) { + return []; + } + depth = prepQuery.depth; + while (depth-- > 0) { + basePos = subject.lastIndexOf(PathSeparator, basePos - 1); + if (basePos === -1) { + return []; + } + } + basePos++; + end++; + return exports.match(subject.slice(basePos, end), subject_lw.slice(basePos, end), prepQuery, basePos); + }; + + exports.mergeMatches = function(a, b) { + var ai, bj, i, j, m, n, out; + m = a.length; + n = b.length; + if (n === 0) { + return a.slice(); + } + if (m === 0) { + return b.slice(); + } + i = -1; + j = 0; + bj = b[j]; + out = []; + while (++i < m) { + ai = a[i]; + while (bj <= ai && ++j < n) { + if (bj < ai) { + out.push(bj); + } + bj = b[j]; + } + out.push(ai); + } + while (j < n) { + out.push(b[j++]); + } + return out; + }; + + exports.match = function(subject, subject_lw, prepQuery, offset) { + var DIAGONAL, LEFT, STOP, UP, acro_score, align, backtrack, csc_diag, csc_row, csc_score, i, j, m, matches, move, n, pos, query, query_lw, score, score_diag, score_row, score_up, si_lw, start, trace; + if (offset == null) { + offset = 0; + } + query = prepQuery.query; + query_lw = prepQuery.query_lw; + m = subject.length; + n = query.length; + acro_score = scorer.scoreAcronyms(subject, subject_lw, query, query_lw).score; + score_row = new Array(n); + csc_row = new Array(n); + STOP = 0; + UP = 1; + LEFT = 2; + DIAGONAL = 3; + trace = new Array(m * n); + pos = -1; + j = -1; + while (++j < n) { + score_row[j] = 0; + csc_row[j] = 0; + } + i = -1; + while (++i < m) { + score = 0; + score_up = 0; + csc_diag = 0; + si_lw = subject_lw[i]; + j = -1; + while (++j < n) { + csc_score = 0; + align = 0; + score_diag = score_up; + if (query_lw[j] === si_lw) { + start = scorer.isWordStart(i, subject, subject_lw); + csc_score = csc_diag > 0 ? csc_diag : scorer.scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start); + align = score_diag + scorer.scoreCharacter(i, j, start, acro_score, csc_score); + } + score_up = score_row[j]; + csc_diag = csc_row[j]; + if (score > score_up) { + move = LEFT; + } else { + score = score_up; + move = UP; + } + if (align > score) { + score = align; + move = DIAGONAL; + } else { + csc_score = 0; + } + score_row[j] = score; + csc_row[j] = csc_score; + trace[++pos] = score > 0 ? move : STOP; + } + } + i = m - 1; + j = n - 1; + pos = i * n + j; + backtrack = true; + matches = []; + while (backtrack && i >= 0 && j >= 0) { + switch (trace[pos]) { + case UP: + i--; + pos -= n; + break; + case LEFT: + j--; + pos--; + break; + case DIAGONAL: + matches.push(i + offset); + j--; + i--; + pos -= n + 1; + break; + default: + backtrack = false; + } + } + matches.reverse(); + return matches; + }; + +}).call(this); + +},{"./scorer":6,"path":7}],6:[function(require,module,exports){ +(function() { + var AcronymResult, PathSeparator, Query, basenameScore, coreChars, countDir, doScore, emptyAcronymResult, file_coeff, isMatch, isSeparator, isWordEnd, isWordStart, miss_coeff, opt_char_re, pos_bonus, scoreAcronyms, scoreCharacter, scoreConsecutives, scoreExact, scoreExactMatch, scorePattern, scorePosition, scoreSize, tau_depth, tau_size, truncatedUpperCase, wm; + + PathSeparator = require('path').sep; + + wm = 150; + + pos_bonus = 20; + + tau_depth = 13; + + tau_size = 85; + + file_coeff = 1.2; + + miss_coeff = 0.75; + + opt_char_re = /[ _\-:\/\\]/g; + + exports.coreChars = coreChars = function(query) { + return query.replace(opt_char_re, ''); + }; + + exports.score = function(string, query, prepQuery, allowErrors) { + var score, string_lw; + if (prepQuery == null) { + prepQuery = new Query(query); + } + if (allowErrors == null) { + allowErrors = false; + } + if (!(allowErrors || isMatch(string, prepQuery.core_lw, prepQuery.core_up))) { + return 0; + } + string_lw = string.toLowerCase(); + score = doScore(string, string_lw, prepQuery); + return Math.ceil(basenameScore(string, string_lw, prepQuery, score)); + }; + + Query = (function() { + function Query(query) { + if (!(query != null ? query.length : void 0)) { + return null; + } + this.query = query; + this.query_lw = query.toLowerCase(); + this.core = coreChars(query); + this.core_lw = this.core.toLowerCase(); + this.core_up = truncatedUpperCase(this.core); + this.depth = countDir(query, query.length); + } + + return Query; + + })(); + + exports.prepQuery = function(query) { + return new Query(query); + }; + + exports.isMatch = isMatch = function(subject, query_lw, query_up) { + var i, j, m, n, qj_lw, qj_up, si; + m = subject.length; + n = query_lw.length; + if (!m || n > m) { + return false; + } + i = -1; + j = -1; + while (++j < n) { + qj_lw = query_lw[j]; + qj_up = query_up[j]; + while (++i < m) { + si = subject[i]; + if (si === qj_lw || si === qj_up) { + break; + } + } + if (i === m) { + return false; + } + } + return true; + }; + + doScore = function(subject, subject_lw, prepQuery) { + var acro, acro_score, align, csc_diag, csc_row, csc_score, i, j, m, miss_budget, miss_left, mm, n, pos, query, query_lw, record_miss, score, score_diag, score_row, score_up, si_lw, start, sz; + query = prepQuery.query; + query_lw = prepQuery.query_lw; + m = subject.length; + n = query.length; + acro = scoreAcronyms(subject, subject_lw, query, query_lw); + acro_score = acro.score; + if (acro.count === n) { + return scoreExact(n, m, acro_score, acro.pos); + } + pos = subject_lw.indexOf(query_lw); + if (pos > -1) { + return scoreExactMatch(subject, subject_lw, query, query_lw, pos, n, m); + } + score_row = new Array(n); + csc_row = new Array(n); + sz = scoreSize(n, m); + miss_budget = Math.ceil(miss_coeff * n) + 5; + miss_left = miss_budget; + j = -1; + while (++j < n) { + score_row[j] = 0; + csc_row[j] = 0; + } + i = subject_lw.indexOf(query_lw[0]); + if (i > -1) { + i--; + } + mm = subject_lw.lastIndexOf(query_lw[n - 1], m); + if (mm > i) { + m = mm + 1; + } + while (++i < m) { + score = 0; + score_diag = 0; + csc_diag = 0; + si_lw = subject_lw[i]; + record_miss = true; + j = -1; + while (++j < n) { + score_up = score_row[j]; + if (score_up > score) { + score = score_up; + } + csc_score = 0; + if (query_lw[j] === si_lw) { + start = isWordStart(i, subject, subject_lw); + csc_score = csc_diag > 0 ? csc_diag : scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start); + align = score_diag + scoreCharacter(i, j, start, acro_score, csc_score); + if (align > score) { + score = align; + miss_left = miss_budget; + } else { + if (record_miss && --miss_left <= 0) { + return score_row[n - 1] * sz; + } + record_miss = false; + } + } + score_diag = score_up; + csc_diag = csc_row[j]; + csc_row[j] = csc_score; + score_row[j] = score; + } + } + return score * sz; + }; + + exports.isWordStart = isWordStart = function(pos, subject, subject_lw) { + var curr_s, prev_s; + if (pos === 0) { + return true; + } + curr_s = subject[pos]; + prev_s = subject[pos - 1]; + return isSeparator(curr_s) || isSeparator(prev_s) || (curr_s !== subject_lw[pos] && prev_s === subject_lw[pos - 1]); + }; + + exports.isWordEnd = isWordEnd = function(pos, subject, subject_lw, len) { + var curr_s, next_s; + if (pos === len - 1) { + return true; + } + curr_s = subject[pos]; + next_s = subject[pos + 1]; + return isSeparator(curr_s) || isSeparator(next_s) || (curr_s === subject_lw[pos] && next_s !== subject_lw[pos + 1]); + }; + + isSeparator = function(c) { + return c === ' ' || c === '.' || c === '-' || c === '_' || c === '/' || c === '\\'; + }; + + scorePosition = function(pos) { + var sc; + if (pos < pos_bonus) { + sc = pos_bonus - pos; + return 100 + sc * sc; + } else { + return Math.max(100 + pos_bonus - pos, 0); + } + }; + + scoreSize = function(n, m) { + return tau_size / (tau_size + Math.abs(m - n)); + }; + + scoreExact = function(n, m, quality, pos) { + return 2 * n * (wm * quality + scorePosition(pos)) * scoreSize(n, m); + }; + + exports.scorePattern = scorePattern = function(count, len, sameCase, start, end) { + var bonus, sz; + sz = count; + bonus = 6; + if (sameCase === count) { + bonus += 2; + } + if (start) { + bonus += 3; + } + if (end) { + bonus += 1; + } + if (count === len) { + if (start) { + if (sameCase === len) { + sz += 2; + } else { + sz += 1; + } + } + if (end) { + bonus += 1; + } + } + return sameCase + sz * (sz + bonus); + }; + + exports.scoreCharacter = scoreCharacter = function(i, j, start, acro_score, csc_score) { + var posBonus; + posBonus = scorePosition(i); + if (start) { + return posBonus + wm * ((acro_score > csc_score ? acro_score : csc_score) + 10); + } + return posBonus + wm * csc_score; + }; + + exports.scoreConsecutives = scoreConsecutives = function(subject, subject_lw, query, query_lw, i, j, start) { + var k, m, mi, n, nj, sameCase, startPos, sz; + m = subject.length; + n = query.length; + mi = m - i; + nj = n - j; + k = mi < nj ? mi : nj; + startPos = i; + sameCase = 0; + sz = 0; + if (query[j] === subject[i]) { + sameCase++; + } + while (++sz < k && query_lw[++j] === subject_lw[++i]) { + if (query[j] === subject[i]) { + sameCase++; + } + } + if (sz === 1) { + return 1 + 2 * sameCase; + } + return scorePattern(sz, n, sameCase, start, isWordEnd(i, subject, subject_lw, m)); + }; + + exports.scoreExactMatch = scoreExactMatch = function(subject, subject_lw, query, query_lw, pos, n, m) { + var end, i, pos2, sameCase, start; + start = isWordStart(pos, subject, subject_lw); + if (!start) { + pos2 = subject_lw.indexOf(query_lw, pos + 1); + if (pos2 > -1) { + start = isWordStart(pos2, subject, subject_lw); + if (start) { + pos = pos2; + } + } + } + i = -1; + sameCase = 0; + while (++i < n) { + if (query[pos + i] === subject[i]) { + sameCase++; + } + } + end = isWordEnd(pos + n - 1, subject, subject_lw, m); + return scoreExact(n, m, scorePattern(n, n, sameCase, start, end), pos); + }; + + AcronymResult = (function() { + function AcronymResult(score, pos, count) { + this.score = score; + this.pos = pos; + this.count = count; + } + + return AcronymResult; + + })(); + + emptyAcronymResult = new AcronymResult(0, 0.1, 0); + + exports.scoreAcronyms = scoreAcronyms = function(subject, subject_lw, query, query_lw) { + var count, i, j, m, n, pos, qj_lw, sameCase, score; + m = subject.length; + n = query.length; + if (!(m > 1 && n > 1)) { + return emptyAcronymResult; + } + count = 0; + pos = 0; + sameCase = 0; + i = -1; + j = -1; + while (++j < n) { + qj_lw = query_lw[j]; + while (++i < m) { + if (qj_lw === subject_lw[i] && isWordStart(i, subject, subject_lw)) { + if (query[j] === subject[i]) { + sameCase++; + } + pos += i; + count++; + break; + } + } + if (i === m) { + break; + } + } + if (count < 2) { + return emptyAcronymResult; + } + score = scorePattern(count, n, sameCase, true, false); + return new AcronymResult(score, pos / count, count); + }; + + basenameScore = function(subject, subject_lw, prepQuery, fullPathScore) { + var alpha, basePathScore, basePos, depth, end; + if (fullPathScore === 0) { + return 0; + } + end = subject.length - 1; + while (subject[end] === PathSeparator) { + end--; + } + basePos = subject.lastIndexOf(PathSeparator, end); + if (basePos === -1) { + return fullPathScore; + } + depth = prepQuery.depth; + while (depth-- > 0) { + basePos = subject.lastIndexOf(PathSeparator, basePos - 1); + if (basePos === -1) { + return fullPathScore; + } + } + basePos++; + end++; + basePathScore = doScore(subject.slice(basePos, end), subject_lw.slice(basePos, end), prepQuery); + alpha = 0.5 * tau_depth / (tau_depth + countDir(subject, end + 1)); + return alpha * basePathScore + (1 - alpha) * fullPathScore * scoreSize(0, file_coeff * (end - basePos)); + }; + + exports.countDir = countDir = function(path, end) { + var count, i; + if (end < 1) { + return 0; + } + count = 0; + i = -1; + while (++i < end && path[i] === PathSeparator) { + continue; + } + while (++i < end) { + if (path[i] === PathSeparator) { + count++; + while (++i < end && path[i] === PathSeparator) { + continue; + } + } + } + return count; + }; + + truncatedUpperCase = function(str) { + var char, upper, _i, _len; + upper = ""; + for (_i = 0, _len = str.length; _i < _len; _i++) { + char = str[_i]; + upper += char.toUpperCase()[0]; + } + return upper; + }; + +}).call(this); + +},{"path":7}],7:[function(require,module,exports){ +(function (process){ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +// resolves . and .. elements in a path array with directory names there +// must be no slashes, empty elements, or device names (c:\) in the array +// (so also no leading and trailing slashes - it does not distinguish +// relative and absolute paths) +function normalizeArray(parts, allowAboveRoot) { + // if the path tries to go above the root, `up` ends up > 0 + var up = 0; + for (var i = parts.length - 1; i >= 0; i--) { + var last = parts[i]; + if (last === '.') { + parts.splice(i, 1); + } else if (last === '..') { + parts.splice(i, 1); + up++; + } else if (up) { + parts.splice(i, 1); + up--; + } + } + + // if the path is allowed to go above the root, restore leading ..s + if (allowAboveRoot) { + for (; up--; up) { + parts.unshift('..'); + } + } + + return parts; +} + +// Split a filename into [root, dir, basename, ext], unix version +// 'root' is just a slash, or nothing. +var splitPathRe = + /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/; +var splitPath = function(filename) { + return splitPathRe.exec(filename).slice(1); +}; + +// path.resolve([from ...], to) +// posix version +exports.resolve = function() { + var resolvedPath = '', + resolvedAbsolute = false; + + for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { + var path = (i >= 0) ? arguments[i] : process.cwd(); + + // Skip empty and invalid entries + if (typeof path !== 'string') { + throw new TypeError('Arguments to path.resolve must be strings'); + } else if (!path) { + continue; + } + + resolvedPath = path + '/' + resolvedPath; + resolvedAbsolute = path.charAt(0) === '/'; + } + + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) + + // Normalize the path + resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) { + return !!p; + }), !resolvedAbsolute).join('/'); + + return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; +}; + +// path.normalize(path) +// posix version +exports.normalize = function(path) { + var isAbsolute = exports.isAbsolute(path), + trailingSlash = substr(path, -1) === '/'; + + // Normalize the path + path = normalizeArray(filter(path.split('/'), function(p) { + return !!p; + }), !isAbsolute).join('/'); + + if (!path && !isAbsolute) { + path = '.'; + } + if (path && trailingSlash) { + path += '/'; + } + + return (isAbsolute ? '/' : '') + path; +}; + +// posix version +exports.isAbsolute = function(path) { + return path.charAt(0) === '/'; +}; + +// posix version +exports.join = function() { + var paths = Array.prototype.slice.call(arguments, 0); + return exports.normalize(filter(paths, function(p, index) { + if (typeof p !== 'string') { + throw new TypeError('Arguments to path.join must be strings'); + } + return p; + }).join('/')); +}; + + +// path.relative(from, to) +// posix version +exports.relative = function(from, to) { + from = exports.resolve(from).substr(1); + to = exports.resolve(to).substr(1); + + function trim(arr) { + var start = 0; + for (; start < arr.length; start++) { + if (arr[start] !== '') break; + } + + var end = arr.length - 1; + for (; end >= 0; end--) { + if (arr[end] !== '') break; + } + + if (start > end) return []; + return arr.slice(start, end - start + 1); + } + + var fromParts = trim(from.split('/')); + var toParts = trim(to.split('/')); + + var length = Math.min(fromParts.length, toParts.length); + var samePartsLength = length; + for (var i = 0; i < length; i++) { + if (fromParts[i] !== toParts[i]) { + samePartsLength = i; + break; + } + } + + var outputParts = []; + for (var i = samePartsLength; i < fromParts.length; i++) { + outputParts.push('..'); + } + + outputParts = outputParts.concat(toParts.slice(samePartsLength)); + + return outputParts.join('/'); +}; + +exports.sep = '/'; +exports.delimiter = ':'; + +exports.dirname = function(path) { + var result = splitPath(path), + root = result[0], + dir = result[1]; + + if (!root && !dir) { + // No dirname whatsoever + return '.'; + } + + if (dir) { + // It has a dirname, strip trailing slash + dir = dir.substr(0, dir.length - 1); + } + + return root + dir; +}; + + +exports.basename = function(path, ext) { + var f = splitPath(path)[2]; + // TODO: make this comparison case-insensitive on windows? + if (ext && f.substr(-1 * ext.length) === ext) { + f = f.substr(0, f.length - ext.length); + } + return f; +}; + + +exports.extname = function(path) { + return splitPath(path)[3]; +}; + +function filter (xs, f) { + if (xs.filter) return xs.filter(f); + var res = []; + for (var i = 0; i < xs.length; i++) { + if (f(xs[i], i, xs)) res.push(xs[i]); + } + return res; +} + +// String.prototype.substr - negative index don't work in IE8 +var substr = 'ab'.substr(-1) === 'b' + ? function (str, start, len) { return str.substr(start, len) } + : function (str, start, len) { + if (start < 0) start = str.length + start; + return str.substr(start, len); + } +; + +}).call(this,require('_process')) +},{"_process":8}],8:[function(require,module,exports){ +// shim for using process in browser + +var process = module.exports = {}; +var queue = []; +var draining = false; +var currentQueue; +var queueIndex = -1; + +function cleanUpNextTick() { + draining = false; + if (currentQueue.length) { + queue = currentQueue.concat(queue); + } else { + queueIndex = -1; + } + if (queue.length) { + drainQueue(); + } +} + +function drainQueue() { + if (draining) { + return; + } + var timeout = setTimeout(cleanUpNextTick); + draining = true; + + var len = queue.length; + while(len) { + currentQueue = queue; + queue = []; + while (++queueIndex < len) { + if (currentQueue) { + currentQueue[queueIndex].run(); + } + } + queueIndex = -1; + len = queue.length; + } + currentQueue = null; + draining = false; + clearTimeout(timeout); +} + +process.nextTick = function (fun) { + var args = new Array(arguments.length - 1); + if (arguments.length > 1) { + for (var i = 1; i < arguments.length; i++) { + args[i - 1] = arguments[i]; + } + } + queue.push(new Item(fun, args)); + if (queue.length === 1 && !draining) { + setTimeout(drainQueue, 0); + } +}; + +// v8 likes predictible objects +function Item(fun, array) { + this.fun = fun; + this.array = array; +} +Item.prototype.run = function () { + this.fun.apply(null, this.array); +}; +process.title = 'browser'; +process.browser = true; +process.env = {}; +process.argv = []; +process.version = ''; // empty string to avoid regexp issues +process.versions = {}; + +function noop() {} + +process.on = noop; +process.addListener = noop; +process.once = noop; +process.off = noop; +process.removeListener = noop; +process.removeAllListeners = noop; +process.emit = noop; + +process.binding = function (name) { + throw new Error('process.binding is not supported'); +}; + +process.cwd = function () { return '/' }; +process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); +}; +process.umask = function() { return 0; }; + +},{}]},{},[1]); diff --git a/vendor/assets/javascripts/g.bar.js b/vendor/assets/javascripts/g.bar.js new file mode 100644 index 0000000000..166bd654d6 --- /dev/null +++ b/vendor/assets/javascripts/g.bar.js @@ -0,0 +1,674 @@ +/*! + * g.Raphael 0.51 - Charting library, based on Raphaël + * + * Copyright (c) 2009-2012 Dmitry Baranovskiy (http://g.raphaeljs.com) + * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license. + */ +(function () { + var mmin = Math.min, + mmax = Math.max; + + function finger(x, y, width, height, dir, ending, isPath, paper) { + var path, + ends = { round: 'round', sharp: 'sharp', soft: 'soft', square: 'square' }; + + // dir 0 for horizontal and 1 for vertical + if ((dir && !height) || (!dir && !width)) { + return isPath ? "" : paper.path(); + } + + ending = ends[ending] || "square"; + height = Math.round(height); + width = Math.round(width); + x = Math.round(x); + y = Math.round(y); + + switch (ending) { + case "round": + if (!dir) { + var r = ~~(height / 2); + + if (width < r) { + r = width; + path = [ + "M", x + .5, y + .5 - ~~(height / 2), + "l", 0, 0, + "a", r, ~~(height / 2), 0, 0, 1, 0, height, + "l", 0, 0, + "z" + ]; + } else { + path = [ + "M", x + .5, y + .5 - r, + "l", width - r, 0, + "a", r, r, 0, 1, 1, 0, height, + "l", r - width, 0, + "z" + ]; + } + } else { + r = ~~(width / 2); + + if (height < r) { + r = height; + path = [ + "M", x - ~~(width / 2), y, + "l", 0, 0, + "a", ~~(width / 2), r, 0, 0, 1, width, 0, + "l", 0, 0, + "z" + ]; + } else { + path = [ + "M", x - r, y, + "l", 0, r - height, + "a", r, r, 0, 1, 1, width, 0, + "l", 0, height - r, + "z" + ]; + } + } + break; + case "sharp": + if (!dir) { + var half = ~~(height / 2); + + path = [ + "M", x, y + half, + "l", 0, -height, mmax(width - half, 0), 0, mmin(half, width), half, -mmin(half, width), half + (half * 2 < height), + "z" + ]; + } else { + half = ~~(width / 2); + path = [ + "M", x + half, y, + "l", -width, 0, 0, -mmax(height - half, 0), half, -mmin(half, height), half, mmin(half, height), half, + "z" + ]; + } + break; + case "square": + if (!dir) { + path = [ + "M", x, y + ~~(height / 2), + "l", 0, -height, width, 0, 0, height, + "z" + ]; + } else { + path = [ + "M", x + ~~(width / 2), y, + "l", 1 - width, 0, 0, -height, width - 1, 0, + "z" + ]; + } + break; + case "soft": + if (!dir) { + r = mmin(width, Math.round(height / 5)); + path = [ + "M", x + .5, y + .5 - ~~(height / 2), + "l", width - r, 0, + "a", r, r, 0, 0, 1, r, r, + "l", 0, height - r * 2, + "a", r, r, 0, 0, 1, -r, r, + "l", r - width, 0, + "z" + ]; + } else { + r = mmin(Math.round(width / 5), height); + path = [ + "M", x - ~~(width / 2), y, + "l", 0, r - height, + "a", r, r, 0, 0, 1, r, -r, + "l", width - 2 * r, 0, + "a", r, r, 0, 0, 1, r, r, + "l", 0, height - r, + "z" + ]; + } + } + + if (isPath) { + return path.join(","); + } else { + return paper.path(path); + } + } + +/*\ + * Paper.vbarchart + [ method ] + ** + * Creates a vertical bar chart + ** + > Parameters + ** + - x (number) x coordinate of the chart + - y (number) y coordinate of the chart + - width (number) width of the chart (respected by all elements in the set) + - height (number) height of the chart (respected by all elements in the set) + - values (array) values + - opts (object) options for the chart + o { + o type (string) type of endings of the bar. Default: 'square'. Other options are: 'round', 'sharp', 'soft'. + o gutter (number)(string) default '20%' (WHAT DOES IT DO?) + o vgutter (number) + o colors (array) colors be used repeatedly to plot the bars. If multicolumn bar is used each sequence of bars with use a different color. + o stacked (boolean) whether or not to tread values as in a stacked bar chart + o to + o stretch (boolean) + o } + ** + = (object) path element of the popup + > Usage + | r.vbarchart(0, 0, 620, 260, [76, 70, 67, 71, 69], {}) + \*/ + + function VBarchart(paper, x, y, width, height, values, opts) { + opts = opts || {}; + + var chartinst = this, + type = opts.type || "square", + gutter = parseFloat(opts.gutter || "20%"), + chart = paper.set(), + bars = paper.set(), + covers = paper.set(), + covers2 = paper.set(), + total = Math.max.apply(Math, values), + stacktotal = [], + multi = 0, + colors = opts.colors || chartinst.colors, + len = values.length; + + if (Raphael.is(values[0], "array")) { + total = []; + multi = len; + len = 0; + + for (var i = values.length; i--;) { + bars.push(paper.set()); + total.push(Math.max.apply(Math, values[i])); + len = Math.max(len, values[i].length); + } + + if (opts.stacked) { + for (var i = len; i--;) { + var tot = 0; + + for (var j = values.length; j--;) { + tot +=+ values[j][i] || 0; + } + + stacktotal.push(tot); + } + } + + for (var i = values.length; i--;) { + if (values[i].length < len) { + for (var j = len; j--;) { + values[i].push(0); + } + } + } + + total = Math.max.apply(Math, opts.stacked ? stacktotal : total); + } + + total = (opts.to) || total; + + var barwidth = width / (len * (100 + gutter) + gutter) * 100, + barhgutter = barwidth * gutter / 100, + barvgutter = opts.vgutter == null ? 20 : opts.vgutter, + stack = [], + X = x + barhgutter, + Y = (height - 2 * barvgutter) / total; + + if (!opts.stretch) { + barhgutter = Math.round(barhgutter); + barwidth = Math.floor(barwidth); + } + + !opts.stacked && (barwidth /= multi || 1); + + for (var i = 0; i < len; i++) { + stack = []; + + for (var j = 0; j < (multi || 1); j++) { + var h = Math.round((multi ? values[j][i] : values[i]) * Y), + top = y + height - barvgutter - h, + bar = finger(Math.round(X + barwidth / 2), top + h, barwidth, h, true, type, null, paper).attr({ stroke: "none", fill: colors[multi ? j : i] }); + + if (multi) { + bars[j].push(bar); + } else { + bars.push(bar); + } + + bar.y = top; + bar.x = Math.round(X + barwidth / 2); + bar.w = barwidth; + bar.h = h; + bar.value = multi ? values[j][i] : values[i]; + + if (!opts.stacked) { + X += barwidth; + } else { + stack.push(bar); + } + } + + if (opts.stacked) { + var cvr; + + covers2.push(cvr = paper.rect(stack[0].x - stack[0].w / 2, y, barwidth, height).attr(chartinst.shim)); + cvr.bars = paper.set(); + + var size = 0; + + for (var s = stack.length; s--;) { + stack[s].toFront(); + } + + for (var s = 0, ss = stack.length; s < ss; s++) { + var bar = stack[s], + cover, + h = (size + bar.value) * Y, + path = finger(bar.x, y + height - barvgutter - !!size * .5, barwidth, h, true, type, 1, paper); + + cvr.bars.push(bar); + size && bar.attr({path: path}); + bar.h = h; + bar.y = y + height - barvgutter - !!size * .5 - h; + covers.push(cover = paper.rect(bar.x - bar.w / 2, bar.y, barwidth, bar.value * Y).attr(chartinst.shim)); + cover.bar = bar; + cover.value = bar.value; + size += bar.value; + } + + X += barwidth; + } + + X += barhgutter; + } + + covers2.toFront(); + X = x + barhgutter; + + if (!opts.stacked) { + for (var i = 0; i < len; i++) { + for (var j = 0; j < (multi || 1); j++) { + var cover; + + covers.push(cover = paper.rect(Math.round(X), y + barvgutter, barwidth, height - barvgutter).attr(chartinst.shim)); + cover.bar = multi ? bars[j][i] : bars[i]; + cover.value = cover.bar.value; + X += barwidth; + } + + X += barhgutter; + } + } + + chart.label = function (labels, isBottom) { + labels = labels || []; + this.labels = paper.set(); + + var L, l = -Infinity; + + if (opts.stacked) { + for (var i = 0; i < len; i++) { + var tot = 0; + + for (var j = 0; j < (multi || 1); j++) { + tot += multi ? values[j][i] : values[i]; + + if (j == multi - 1) { + var label = paper.labelise(labels[i], tot, total); + + L = paper.text(bars[i * (multi || 1) + j].x, y + height - barvgutter / 2, label).attr(txtattr).insertBefore(covers[i * (multi || 1) + j]); + + var bb = L.getBBox(); + + if (bb.x - 7 < l) { + L.remove(); + } else { + this.labels.push(L); + l = bb.x + bb.width; + } + } + } + } + } else { + for (var i = 0; i < len; i++) { + for (var j = 0; j < (multi || 1); j++) { + var label = paper.labelise(multi ? labels[j] && labels[j][i] : labels[i], multi ? values[j][i] : values[i], total); + + L = paper.text(bars[i * (multi || 1) + j].x, isBottom ? y + height - barvgutter / 2 : bars[i * (multi || 1) + j].y - 10, label).attr(txtattr).insertBefore(covers[i * (multi || 1) + j]); + + var bb = L.getBBox(); + + if (bb.x - 7 < l) { + L.remove(); + } else { + this.labels.push(L); + l = bb.x + bb.width; + } + } + } + } + return this; + }; + + chart.hover = function (fin, fout) { + covers2.hide(); + covers.show(); + covers.mouseover(fin).mouseout(fout); + return this; + }; + + chart.hoverColumn = function (fin, fout) { + covers.hide(); + covers2.show(); + fout = fout || function () {}; + covers2.mouseover(fin).mouseout(fout); + return this; + }; + + chart.click = function (f) { + covers2.hide(); + covers.show(); + covers.click(f); + return this; + }; + + chart.each = function (f) { + if (!Raphael.is(f, "function")) { + return this; + } + for (var i = covers.length; i--;) { + f.call(covers[i]); + } + return this; + }; + + chart.eachColumn = function (f) { + if (!Raphael.is(f, "function")) { + return this; + } + for (var i = covers2.length; i--;) { + f.call(covers2[i]); + } + return this; + }; + + chart.clickColumn = function (f) { + covers.hide(); + covers2.show(); + covers2.click(f); + return this; + }; + + chart.push(bars, covers, covers2); + chart.bars = bars; + chart.covers = covers; + return chart; + }; + + //inheritance + var F = function() {}; + F.prototype = Raphael.g; + HBarchart.prototype = VBarchart.prototype = new F; //prototype reused by hbarchart + + Raphael.fn.barchart = function(x, y, width, height, values, opts) { + return new VBarchart(this, x, y, width, height, values, opts); + }; + +/*\ + * Paper.barchart + [ method ] + ** + * Creates a horizontal bar chart + ** + > Parameters + ** + - x (number) x coordinate of the chart + - y (number) y coordinate of the chart + - width (number) width of the chart (respected by all elements in the set) + - height (number) height of the chart (respected by all elements in the set) + - values (array) values + - opts (object) options for the chart + o { + o type (string) type of endings of the bar. Default: 'square'. Other options are: 'round', 'sharp', 'soft'. + o gutter (number)(string) default '20%' (WHAT DOES IT DO?) + o vgutter (number) + o colors (array) colors be used repeatedly to plot the bars. If multicolumn bar is used each sequence of bars with use a different color. + o stacked (boolean) whether or not to tread values as in a stacked bar chart + o to + o stretch (boolean) + o } + ** + = (object) path element of the popup + > Usage + | r.barchart(0, 0, 620, 260, [76, 70, 67, 71, 69], {}) + \*/ + + function HBarchart(paper, x, y, width, height, values, opts) { + opts = opts || {}; + + var chartinst = this, + type = opts.type || "square", + gutter = parseFloat(opts.gutter || "20%"), + chart = paper.set(), + bars = paper.set(), + covers = paper.set(), + covers2 = paper.set(), + total = Math.max.apply(Math, values), + stacktotal = [], + multi = 0, + colors = opts.colors || chartinst.colors, + len = values.length; + + if (Raphael.is(values[0], "array")) { + total = []; + multi = len; + len = 0; + + for (var i = values.length; i--;) { + bars.push(paper.set()); + total.push(Math.max.apply(Math, values[i])); + len = Math.max(len, values[i].length); + } + + if (opts.stacked) { + for (var i = len; i--;) { + var tot = 0; + for (var j = values.length; j--;) { + tot +=+ values[j][i] || 0; + } + stacktotal.push(tot); + } + } + + for (var i = values.length; i--;) { + if (values[i].length < len) { + for (var j = len; j--;) { + values[i].push(0); + } + } + } + + total = Math.max.apply(Math, opts.stacked ? stacktotal : total); + } + + total = (opts.to) || total; + + var barheight = Math.floor(height / (len * (100 + gutter) + gutter) * 100), + bargutter = Math.floor(barheight * gutter / 100), + stack = [], + Y = y + bargutter, + X = (width - 1) / total; + + !opts.stacked && (barheight /= multi || 1); + + for (var i = 0; i < len; i++) { + stack = []; + + for (var j = 0; j < (multi || 1); j++) { + var val = multi ? values[j][i] : values[i], + bar = finger(x, Y + barheight / 2, Math.round(val * X), barheight - 1, false, type, null, paper).attr({stroke: "none", fill: colors[multi ? j : i]}); + + if (multi) { + bars[j].push(bar); + } else { + bars.push(bar); + } + + bar.x = x + Math.round(val * X); + bar.y = Y + barheight / 2; + bar.w = Math.round(val * X); + bar.h = barheight; + bar.value = +val; + + if (!opts.stacked) { + Y += barheight; + } else { + stack.push(bar); + } + } + + if (opts.stacked) { + var cvr = paper.rect(x, stack[0].y - stack[0].h / 2, width, barheight).attr(chartinst.shim); + + covers2.push(cvr); + cvr.bars = paper.set(); + + var size = 0; + + for (var s = stack.length; s--;) { + stack[s].toFront(); + } + + for (var s = 0, ss = stack.length; s < ss; s++) { + var bar = stack[s], + cover, + val = Math.round((size + bar.value) * X), + path = finger(x, bar.y, val, barheight - 1, false, type, 1, paper); + + cvr.bars.push(bar); + size && bar.attr({ path: path }); + bar.w = val; + bar.x = x + val; + covers.push(cover = paper.rect(x + size * X, bar.y - bar.h / 2, bar.value * X, barheight).attr(chartinst.shim)); + cover.bar = bar; + size += bar.value; + } + + Y += barheight; + } + + Y += bargutter; + } + + covers2.toFront(); + Y = y + bargutter; + + if (!opts.stacked) { + for (var i = 0; i < len; i++) { + for (var j = 0; j < (multi || 1); j++) { + var cover = paper.rect(x, Y, width, barheight).attr(chartinst.shim); + + covers.push(cover); + cover.bar = multi ? bars[j][i] : bars[i]; + cover.value = cover.bar.value; + Y += barheight; + } + + Y += bargutter; + } + } + + chart.label = function (labels, isRight) { + labels = labels || []; + this.labels = paper.set(); + + for (var i = 0; i < len; i++) { + for (var j = 0; j < multi; j++) { + var label = paper.labelise(multi ? labels[j] && labels[j][i] : labels[i], multi ? values[j][i] : values[i], total), + X = isRight ? bars[i * (multi || 1) + j].x - barheight / 2 + 3 : x + 5, + A = isRight ? "end" : "start", + L; + + this.labels.push(L = paper.text(X, bars[i * (multi || 1) + j].y, label).attr(txtattr).attr({ "text-anchor": A }).insertBefore(covers[0])); + + if (L.getBBox().x < x + 5) { + L.attr({x: x + 5, "text-anchor": "start"}); + } else { + bars[i * (multi || 1) + j].label = L; + } + } + } + + return this; + }; + + chart.hover = function (fin, fout) { + covers2.hide(); + covers.show(); + fout = fout || function () {}; + covers.mouseover(fin).mouseout(fout); + return this; + }; + + chart.hoverColumn = function (fin, fout) { + covers.hide(); + covers2.show(); + fout = fout || function () {}; + covers2.mouseover(fin).mouseout(fout); + return this; + }; + + chart.each = function (f) { + if (!Raphael.is(f, "function")) { + return this; + } + for (var i = covers.length; i--;) { + f.call(covers[i]); + } + return this; + }; + + chart.eachColumn = function (f) { + if (!Raphael.is(f, "function")) { + return this; + } + for (var i = covers2.length; i--;) { + f.call(covers2[i]); + } + return this; + }; + + chart.click = function (f) { + covers2.hide(); + covers.show(); + covers.click(f); + return this; + }; + + chart.clickColumn = function (f) { + covers.hide(); + covers2.show(); + covers2.click(f); + return this; + }; + + chart.push(bars, covers, covers2); + chart.bars = bars; + chart.covers = covers; + return chart; + }; + + Raphael.fn.hbarchart = function(x, y, width, height, values, opts) { + return new HBarchart(this, x, y, width, height, values, opts); + }; + +})(); diff --git a/vendor/assets/javascripts/g.raphael.js b/vendor/assets/javascripts/g.raphael.js new file mode 100644 index 0000000000..27f27caf9f --- /dev/null +++ b/vendor/assets/javascripts/g.raphael.js @@ -0,0 +1,861 @@ +/*! + * g.Raphael 0.51 - Charting library, based on Raphaël + * + * Copyright (c) 2009-2012 Dmitry Baranovskiy (http://g.raphaeljs.com) + * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license. + */ + +/* + * Tooltips on Element prototype + */ +/*\ + * Element.popup + [ method ] + ** + * Puts the context Element in a 'popup' tooltip. Can also be used on sets. + ** + > Parameters + ** + - dir (string) location of Element relative to the tail: `'down'`, `'left'`, `'up'` [default], or `'right'`. + - size (number) amount of bevel/padding around the Element, as well as half the width and height of the tail [default: `5`] + - x (number) x coordinate of the popup's tail [default: Element's `x` or `cx`] + - y (number) y coordinate of the popup's tail [default: Element's `y` or `cy`] + ** + = (object) path element of the popup + \*/ +Raphael.el.popup = function (dir, size, x, y) { + var paper = this.paper || this[0].paper, + bb, xy, center, cw, ch; + + if (!paper) return; + + switch (this.type) { + case 'text': + case 'circle': + case 'ellipse': center = true; break; + default: center = false; + } + + dir = dir == null ? 'up' : dir; + size = size || 5; + bb = this.getBBox(); + + x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x); + y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y); + cw = Math.max(bb.width / 2 - size, 0); + ch = Math.max(bb.height / 2 - size, 0); + + this.translate(x - bb.x - (center ? bb.width / 2 : 0), y - bb.y - (center ? bb.height / 2 : 0)); + bb = this.getBBox(); + + var paths = { + up: [ + 'M', x, y, + 'l', -size, -size, -cw, 0, + 'a', size, size, 0, 0, 1, -size, -size, + 'l', 0, -bb.height, + 'a', size, size, 0, 0, 1, size, -size, + 'l', size * 2 + cw * 2, 0, + 'a', size, size, 0, 0, 1, size, size, + 'l', 0, bb.height, + 'a', size, size, 0, 0, 1, -size, size, + 'l', -cw, 0, + 'z' + ].join(','), + down: [ + 'M', x, y, + 'l', size, size, cw, 0, + 'a', size, size, 0, 0, 1, size, size, + 'l', 0, bb.height, + 'a', size, size, 0, 0, 1, -size, size, + 'l', -(size * 2 + cw * 2), 0, + 'a', size, size, 0, 0, 1, -size, -size, + 'l', 0, -bb.height, + 'a', size, size, 0, 0, 1, size, -size, + 'l', cw, 0, + 'z' + ].join(','), + left: [ + 'M', x, y, + 'l', -size, size, 0, ch, + 'a', size, size, 0, 0, 1, -size, size, + 'l', -bb.width, 0, + 'a', size, size, 0, 0, 1, -size, -size, + 'l', 0, -(size * 2 + ch * 2), + 'a', size, size, 0, 0, 1, size, -size, + 'l', bb.width, 0, + 'a', size, size, 0, 0, 1, size, size, + 'l', 0, ch, + 'z' + ].join(','), + right: [ + 'M', x, y, + 'l', size, -size, 0, -ch, + 'a', size, size, 0, 0, 1, size, -size, + 'l', bb.width, 0, + 'a', size, size, 0, 0, 1, size, size, + 'l', 0, size * 2 + ch * 2, + 'a', size, size, 0, 0, 1, -size, size, + 'l', -bb.width, 0, + 'a', size, size, 0, 0, 1, -size, -size, + 'l', 0, -ch, + 'z' + ].join(',') + }; + + xy = { + up: { x: -!center * (bb.width / 2), y: -size * 2 - (center ? bb.height / 2 : bb.height) }, + down: { x: -!center * (bb.width / 2), y: size * 2 + (center ? bb.height / 2 : bb.height) }, + left: { x: -size * 2 - (center ? bb.width / 2 : bb.width), y: -!center * (bb.height / 2) }, + right: { x: size * 2 + (center ? bb.width / 2 : bb.width), y: -!center * (bb.height / 2) } + }[dir]; + + this.translate(xy.x, xy.y); + return paper.path(paths[dir]).attr({ fill: "#000", stroke: "none" }).insertBefore(this.node ? this : this[0]); +}; + +/*\ + * Element.tag + [ method ] + ** + * Puts the context Element in a 'tag' tooltip. Can also be used on sets. + ** + > Parameters + ** + - angle (number) angle of orientation in degrees [default: `0`] + - r (number) radius of the loop [default: `5`] + - x (number) x coordinate of the center of the tag loop [default: Element's `x` or `cx`] + - y (number) y coordinate of the center of the tag loop [default: Element's `x` or `cx`] + ** + = (object) path element of the tag + \*/ +Raphael.el.tag = function (angle, r, x, y) { + var d = 3, + paper = this.paper || this[0].paper; + + if (!paper) return; + + var p = paper.path().attr({ fill: '#000', stroke: '#000' }), + bb = this.getBBox(), + dx, R, center, tmp; + + switch (this.type) { + case 'text': + case 'circle': + case 'ellipse': center = true; break; + default: center = false; + } + + angle = angle || 0; + x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x); + y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y); + r = r == null ? 5 : r; + R = .5522 * r; + + if (bb.height >= r * 2) { + p.attr({ + path: [ + "M", x, y + r, + "a", r, r, 0, 1, 1, 0, -r * 2, r, r, 0, 1, 1, 0, r * 2, + "m", 0, -r * 2 -d, + "a", r + d, r + d, 0, 1, 0, 0, (r + d) * 2, + "L", x + r + d, y + bb.height / 2 + d, + "l", bb.width + 2 * d, 0, 0, -bb.height - 2 * d, -bb.width - 2 * d, 0, + "L", x, y - r - d + ].join(",") + }); + } else { + dx = Math.sqrt(Math.pow(r + d, 2) - Math.pow(bb.height / 2 + d, 2)); + p.attr({ + path: [ + "M", x, y + r, + "c", -R, 0, -r, R - r, -r, -r, 0, -R, r - R, -r, r, -r, R, 0, r, r - R, r, r, 0, R, R - r, r, -r, r, + "M", x + dx, y - bb.height / 2 - d, + "a", r + d, r + d, 0, 1, 0, 0, bb.height + 2 * d, + "l", r + d - dx + bb.width + 2 * d, 0, 0, -bb.height - 2 * d, + "L", x + dx, y - bb.height / 2 - d + ].join(",") + }); + } + + angle = 360 - angle; + p.rotate(angle, x, y); + + if (this.attrs) { + //elements + this.attr(this.attrs.x ? 'x' : 'cx', x + r + d + (!center ? this.type == 'text' ? bb.width : 0 : bb.width / 2)).attr('y', center ? y : y - bb.height / 2); + this.rotate(angle, x, y); + angle > 90 && angle < 270 && this.attr(this.attrs.x ? 'x' : 'cx', x - r - d - (!center ? bb.width : bb.width / 2)).rotate(180, x, y); + } else { + //sets + if (angle > 90 && angle < 270) { + this.translate(x - bb.x - bb.width - r - d, y - bb.y - bb.height / 2); + this.rotate(angle - 180, bb.x + bb.width + r + d, bb.y + bb.height / 2); + } else { + this.translate(x - bb.x + r + d, y - bb.y - bb.height / 2); + this.rotate(angle, bb.x - r - d, bb.y + bb.height / 2); + } + } + + return p.insertBefore(this.node ? this : this[0]); +}; + +/*\ + * Element.drop + [ method ] + ** + * Puts the context Element in a 'drop' tooltip. Can also be used on sets. + ** + > Parameters + ** + - angle (number) angle of orientation in degrees [default: `0`] + - x (number) x coordinate of the drop's point [default: Element's `x` or `cx`] + - y (number) y coordinate of the drop's point [default: Element's `x` or `cx`] + ** + = (object) path element of the drop + \*/ +Raphael.el.drop = function (angle, x, y) { + var bb = this.getBBox(), + paper = this.paper || this[0].paper, + center, size, p, dx, dy; + + if (!paper) return; + + switch (this.type) { + case 'text': + case 'circle': + case 'ellipse': center = true; break; + default: center = false; + } + + angle = angle || 0; + + x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x); + y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y); + size = Math.max(bb.width, bb.height) + Math.min(bb.width, bb.height); + p = paper.path([ + "M", x, y, + "l", size, 0, + "A", size * .4, size * .4, 0, 1, 0, x + size * .7, y - size * .7, + "z" + ]).attr({fill: "#000", stroke: "none"}).rotate(22.5 - angle, x, y); + + angle = (angle + 90) * Math.PI / 180; + dx = (x + size * Math.sin(angle)) - (center ? 0 : bb.width / 2); + dy = (y + size * Math.cos(angle)) - (center ? 0 : bb.height / 2); + + this.attrs ? + this.attr(this.attrs.x ? 'x' : 'cx', dx).attr(this.attrs.y ? 'y' : 'cy', dy) : + this.translate(dx - bb.x, dy - bb.y); + + return p.insertBefore(this.node ? this : this[0]); +}; + +/*\ + * Element.flag + [ method ] + ** + * Puts the context Element in a 'flag' tooltip. Can also be used on sets. + ** + > Parameters + ** + - angle (number) angle of orientation in degrees [default: `0`] + - x (number) x coordinate of the flag's point [default: Element's `x` or `cx`] + - y (number) y coordinate of the flag's point [default: Element's `x` or `cx`] + ** + = (object) path element of the flag + \*/ +Raphael.el.flag = function (angle, x, y) { + var d = 3, + paper = this.paper || this[0].paper; + + if (!paper) return; + + var p = paper.path().attr({ fill: '#000', stroke: '#000' }), + bb = this.getBBox(), + h = bb.height / 2, + center; + + switch (this.type) { + case 'text': + case 'circle': + case 'ellipse': center = true; break; + default: center = false; + } + + angle = angle || 0; + x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x); + y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2: bb.y); + + p.attr({ + path: [ + "M", x, y, + "l", h + d, -h - d, bb.width + 2 * d, 0, 0, bb.height + 2 * d, -bb.width - 2 * d, 0, + "z" + ].join(",") + }); + + angle = 360 - angle; + p.rotate(angle, x, y); + + if (this.attrs) { + //elements + this.attr(this.attrs.x ? 'x' : 'cx', x + h + d + (!center ? this.type == 'text' ? bb.width : 0 : bb.width / 2)).attr('y', center ? y : y - bb.height / 2); + this.rotate(angle, x, y); + angle > 90 && angle < 270 && this.attr(this.attrs.x ? 'x' : 'cx', x - h - d - (!center ? bb.width : bb.width / 2)).rotate(180, x, y); + } else { + //sets + if (angle > 90 && angle < 270) { + this.translate(x - bb.x - bb.width - h - d, y - bb.y - bb.height / 2); + this.rotate(angle - 180, bb.x + bb.width + h + d, bb.y + bb.height / 2); + } else { + this.translate(x - bb.x + h + d, y - bb.y - bb.height / 2); + this.rotate(angle, bb.x - h - d, bb.y + bb.height / 2); + } + } + + return p.insertBefore(this.node ? this : this[0]); +}; + +/*\ + * Element.label + [ method ] + ** + * Puts the context Element in a 'label' tooltip. Can also be used on sets. + ** + = (object) path element of the label. + \*/ +Raphael.el.label = function () { + var bb = this.getBBox(), + paper = this.paper || this[0].paper, + r = Math.min(20, bb.width + 10, bb.height + 10) / 2; + + if (!paper) return; + + return paper.rect(bb.x - r / 2, bb.y - r / 2, bb.width + r, bb.height + r, r).attr({ stroke: 'none', fill: '#000' }).insertBefore(this.node ? this : this[0]); +}; + +/*\ + * Element.blob + [ method ] + ** + * Puts the context Element in a 'blob' tooltip. Can also be used on sets. + ** + > Parameters + ** + - angle (number) angle of orientation in degrees [default: `0`] + - x (number) x coordinate of the blob's tail [default: Element's `x` or `cx`] + - y (number) y coordinate of the blob's tail [default: Element's `x` or `cx`] + ** + = (object) path element of the blob + \*/ +Raphael.el.blob = function (angle, x, y) { + var bb = this.getBBox(), + rad = Math.PI / 180, + paper = this.paper || this[0].paper, + p, center, size; + + if (!paper) return; + + switch (this.type) { + case 'text': + case 'circle': + case 'ellipse': center = true; break; + default: center = false; + } + + p = paper.path().attr({ fill: "#000", stroke: "none" }); + angle = (+angle + 1 ? angle : 45) + 90; + size = Math.min(bb.height, bb.width); + x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x); + y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y); + + var w = Math.max(bb.width + size, size * 25 / 12), + h = Math.max(bb.height + size, size * 25 / 12), + x2 = x + size * Math.sin((angle - 22.5) * rad), + y2 = y + size * Math.cos((angle - 22.5) * rad), + x1 = x + size * Math.sin((angle + 22.5) * rad), + y1 = y + size * Math.cos((angle + 22.5) * rad), + dx = (x1 - x2) / 2, + dy = (y1 - y2) / 2, + rx = w / 2, + ry = h / 2, + k = -Math.sqrt(Math.abs(rx * rx * ry * ry - rx * rx * dy * dy - ry * ry * dx * dx) / (rx * rx * dy * dy + ry * ry * dx * dx)), + cx = k * rx * dy / ry + (x1 + x2) / 2, + cy = k * -ry * dx / rx + (y1 + y2) / 2; + + p.attr({ + x: cx, + y: cy, + path: [ + "M", x, y, + "L", x1, y1, + "A", rx, ry, 0, 1, 1, x2, y2, + "z" + ].join(",") + }); + + this.translate(cx - bb.x - bb.width / 2, cy - bb.y - bb.height / 2); + + return p.insertBefore(this.node ? this : this[0]); +}; + +/* + * Tooltips on Paper prototype + */ +/*\ + * Paper.label + [ method ] + ** + * Puts the given `text` into a 'label' tooltip. The text is given a default style according to @g.txtattr. See @Element.label + ** + > Parameters + ** + - x (number) x coordinate of the center of the label + - y (number) y coordinate of the center of the label + - text (string) text to place inside the label + ** + = (object) set containing the label path and the text element + > Usage + | paper.label(50, 50, "$9.99"); + \*/ +Raphael.fn.label = function (x, y, text) { + var set = this.set(); + + text = this.text(x, y, text).attr(Raphael.g.txtattr); + return set.push(text.label(), text); +}; + +/*\ + * Paper.popup + [ method ] + ** + * Puts the given `text` into a 'popup' tooltip. The text is given a default style according to @g.txtattr. See @Element.popup + * + * Note: The `dir` parameter has changed from g.Raphael 0.4.1 to 0.5. The options `0`, `1`, `2`, and `3` has been changed to `'down'`, `'left'`, `'up'`, and `'right'` respectively. + ** + > Parameters + ** + - x (number) x coordinate of the popup's tail + - y (number) y coordinate of the popup's tail + - text (string) text to place inside the popup + - dir (string) location of the text relative to the tail: `'down'`, `'left'`, `'up'` [default], or `'right'`. + - size (number) amount of padding around the Element [default: `5`] + ** + = (object) set containing the popup path and the text element + > Usage + | paper.popup(50, 50, "$9.99", 'down'); + \*/ +Raphael.fn.popup = function (x, y, text, dir, size) { + var set = this.set(); + + text = this.text(x, y, text).attr(Raphael.g.txtattr); + return set.push(text.popup(dir, size), text); +}; + +/*\ + * Paper.tag + [ method ] + ** + * Puts the given text into a 'tag' tooltip. The text is given a default style according to @g.txtattr. See @Element.tag + ** + > Parameters + ** + - x (number) x coordinate of the center of the tag loop + - y (number) y coordinate of the center of the tag loop + - text (string) text to place inside the tag + - angle (number) angle of orientation in degrees [default: `0`] + - r (number) radius of the loop [default: `5`] + ** + = (object) set containing the tag path and the text element + > Usage + | paper.tag(50, 50, "$9.99", 60); + \*/ +Raphael.fn.tag = function (x, y, text, angle, r) { + var set = this.set(); + + text = this.text(x, y, text).attr(Raphael.g.txtattr); + return set.push(text.tag(angle, r), text); +}; + +/*\ + * Paper.flag + [ method ] + ** + * Puts the given `text` into a 'flag' tooltip. The text is given a default style according to @g.txtattr. See @Element.flag + ** + > Parameters + ** + - x (number) x coordinate of the flag's point + - y (number) y coordinate of the flag's point + - text (string) text to place inside the flag + - angle (number) angle of orientation in degrees [default: `0`] + ** + = (object) set containing the flag path and the text element + > Usage + | paper.flag(50, 50, "$9.99", 60); + \*/ +Raphael.fn.flag = function (x, y, text, angle) { + var set = this.set(); + + text = this.text(x, y, text).attr(Raphael.g.txtattr); + return set.push(text.flag(angle), text); +}; + +/*\ + * Paper.drop + [ method ] + ** + * Puts the given text into a 'drop' tooltip. The text is given a default style according to @g.txtattr. See @Element.drop + ** + > Parameters + ** + - x (number) x coordinate of the drop's point + - y (number) y coordinate of the drop's point + - text (string) text to place inside the drop + - angle (number) angle of orientation in degrees [default: `0`] + ** + = (object) set containing the drop path and the text element + > Usage + | paper.drop(50, 50, "$9.99", 60); + \*/ +Raphael.fn.drop = function (x, y, text, angle) { + var set = this.set(); + + text = this.text(x, y, text).attr(Raphael.g.txtattr); + return set.push(text.drop(angle), text); +}; + +/*\ + * Paper.blob + [ method ] + ** + * Puts the given text into a 'blob' tooltip. The text is given a default style according to @g.txtattr. See @Element.blob + ** + > Parameters + ** + - x (number) x coordinate of the blob's tail + - y (number) y coordinate of the blob's tail + - text (string) text to place inside the blob + - angle (number) angle of orientation in degrees [default: `0`] + ** + = (object) set containing the blob path and the text element + > Usage + | paper.blob(50, 50, "$9.99", 60); + \*/ +Raphael.fn.blob = function (x, y, text, angle) { + var set = this.set(); + + text = this.text(x, y, text).attr(Raphael.g.txtattr); + return set.push(text.blob(angle), text); +}; + +/** + * Brightness functions on the Element prototype + */ +/*\ + * Element.lighter + [ method ] + ** + * Makes the context element lighter by increasing the brightness and reducing the saturation by a given factor. Can be called on Sets. + ** + > Parameters + ** + - times (number) adjustment factor [default: `2`] + ** + = (object) Element + > Usage + | paper.circle(50, 50, 20).attr({ + | fill: "#ff0000", + | stroke: "#fff", + | "stroke-width": 2 + | }).lighter(6); + \*/ +Raphael.el.lighter = function (times) { + times = times || 2; + + var fs = [this.attrs.fill, this.attrs.stroke]; + + this.fs = this.fs || [fs[0], fs[1]]; + + fs[0] = Raphael.rgb2hsb(Raphael.getRGB(fs[0]).hex); + fs[1] = Raphael.rgb2hsb(Raphael.getRGB(fs[1]).hex); + fs[0].b = Math.min(fs[0].b * times, 1); + fs[0].s = fs[0].s / times; + fs[1].b = Math.min(fs[1].b * times, 1); + fs[1].s = fs[1].s / times; + + this.attr({fill: "hsb(" + [fs[0].h, fs[0].s, fs[0].b] + ")", stroke: "hsb(" + [fs[1].h, fs[1].s, fs[1].b] + ")"}); + return this; +}; + +/*\ + * Element.darker + [ method ] + ** + * Makes the context element darker by decreasing the brightness and increasing the saturation by a given factor. Can be called on Sets. + ** + > Parameters + ** + - times (number) adjustment factor [default: `2`] + ** + = (object) Element + > Usage + | paper.circle(50, 50, 20).attr({ + | fill: "#ff0000", + | stroke: "#fff", + | "stroke-width": 2 + | }).darker(6); + \*/ +Raphael.el.darker = function (times) { + times = times || 2; + + var fs = [this.attrs.fill, this.attrs.stroke]; + + this.fs = this.fs || [fs[0], fs[1]]; + + fs[0] = Raphael.rgb2hsb(Raphael.getRGB(fs[0]).hex); + fs[1] = Raphael.rgb2hsb(Raphael.getRGB(fs[1]).hex); + fs[0].s = Math.min(fs[0].s * times, 1); + fs[0].b = fs[0].b / times; + fs[1].s = Math.min(fs[1].s * times, 1); + fs[1].b = fs[1].b / times; + + this.attr({fill: "hsb(" + [fs[0].h, fs[0].s, fs[0].b] + ")", stroke: "hsb(" + [fs[1].h, fs[1].s, fs[1].b] + ")"}); + return this; +}; + +/*\ + * Element.resetBrightness + [ method ] + ** + * Resets brightness and saturation levels to their original values. See @Element.lighter and @Element.darker. Can be called on Sets. + ** + = (object) Element + > Usage + | paper.circle(50, 50, 20).attr({ + | fill: "#ff0000", + | stroke: "#fff", + | "stroke-width": 2 + | }).lighter(6).resetBrightness(); + \*/ +Raphael.el.resetBrightness = function () { + if (this.fs) { + this.attr({ fill: this.fs[0], stroke: this.fs[1] }); + delete this.fs; + } + return this; +}; + +//alias to set prototype +(function () { + var brightness = ['lighter', 'darker', 'resetBrightness'], + tooltips = ['popup', 'tag', 'flag', 'label', 'drop', 'blob']; + + for (var f in tooltips) (function (name) { + Raphael.st[name] = function () { + return Raphael.el[name].apply(this, arguments); + }; + })(tooltips[f]); + + for (var f in brightness) (function (name) { + Raphael.st[name] = function () { + for (var i = 0; i < this.length; i++) { + this[i][name].apply(this[i], arguments); + } + + return this; + }; + })(brightness[f]); +})(); + +//chart prototype for storing common functions +Raphael.g = { + /*\ + * g.shim + [ object ] + ** + * An attribute object that charts will set on all generated shims (shims being the invisible objects that mouse events are bound to) + ** + > Default value + | { stroke: 'none', fill: '#000', 'fill-opacity': 0 } + \*/ + shim: { stroke: 'none', fill: '#000', 'fill-opacity': 0 }, + + /*\ + * g.txtattr + [ object ] + ** + * An attribute object that charts and tooltips will set on any generated text + ** + > Default value + | { font: '12px Arial, sans-serif', fill: '#fff' } + \*/ + txtattr: { font: '12px Arial, sans-serif', fill: '#fff' }, + + /*\ + * g.colors + [ array ] + ** + * An array of color values that charts will iterate through when drawing chart data values. + ** + \*/ + colors: (function () { + var hues = [.6, .2, .05, .1333, .75, 0], + colors = []; + + for (var i = 0; i < 10; i++) { + if (i < hues.length) { + colors.push('hsb(' + hues[i] + ',.75, .75)'); + } else { + colors.push('hsb(' + hues[i - hues.length] + ', 1, .5)'); + } + } + + return colors; + })(), + + snapEnds: function(from, to, steps) { + var f = from, + t = to; + + if (f == t) { + return {from: f, to: t, power: 0}; + } + + function round(a) { + return Math.abs(a - .5) < .25 ? ~~(a) + .5 : Math.round(a); + } + + var d = (t - f) / steps, + r = ~~(d), + R = r, + i = 0; + + if (r) { + while (R) { + i--; + R = ~~(d * Math.pow(10, i)) / Math.pow(10, i); + } + + i ++; + } else { + if(d == 0 || !isFinite(d)) { + i = 1; + } else { + while (!r) { + i = i || 1; + r = ~~(d * Math.pow(10, i)) / Math.pow(10, i); + i++; + } + } + + i && i--; + } + + t = round(to * Math.pow(10, i)) / Math.pow(10, i); + + if (t < to) { + t = round((to + .5) * Math.pow(10, i)) / Math.pow(10, i); + } + + f = round((from - (i > 0 ? 0 : .5)) * Math.pow(10, i)) / Math.pow(10, i); + return { from: f, to: t, power: i }; + }, + + axis: function (x, y, length, from, to, steps, orientation, labels, type, dashsize, paper) { + dashsize = dashsize == null ? 2 : dashsize; + type = type || "t"; + steps = steps || 10; + paper = arguments[arguments.length-1] //paper is always last argument + + var path = type == "|" || type == " " ? ["M", x + .5, y, "l", 0, .001] : orientation == 1 || orientation == 3 ? ["M", x + .5, y, "l", 0, -length] : ["M", x, y + .5, "l", length, 0], + ends = this.snapEnds(from, to, steps), + f = ends.from, + t = ends.to, + i = ends.power, + j = 0, + txtattr = { font: "11px 'Fontin Sans', Fontin-Sans, sans-serif" }, + text = paper.set(), + d; + + d = (t - f) / steps; + + var label = f, + rnd = i > 0 ? i : 0; + dx = length / steps; + + if (+orientation == 1 || +orientation == 3) { + var Y = y, + addon = (orientation - 1 ? 1 : -1) * (dashsize + 3 + !!(orientation - 1)); + + while (Y >= y - length) { + type != "-" && type != " " && (path = path.concat(["M", x - (type == "+" || type == "|" ? dashsize : !(orientation - 1) * dashsize * 2), Y + .5, "l", dashsize * 2 + 1, 0])); + text.push(paper.text(x + addon, Y, (labels && labels[j++]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr).attr({ "text-anchor": orientation - 1 ? "start" : "end" })); + label += d; + Y -= dx; + } + + if (Math.round(Y + dx - (y - length))) { + type != "-" && type != " " && (path = path.concat(["M", x - (type == "+" || type == "|" ? dashsize : !(orientation - 1) * dashsize * 2), y - length + .5, "l", dashsize * 2 + 1, 0])); + text.push(paper.text(x + addon, y - length, (labels && labels[j]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr).attr({ "text-anchor": orientation - 1 ? "start" : "end" })); + } + } else { + label = f; + rnd = (i > 0) * i; + addon = (orientation ? -1 : 1) * (dashsize + 9 + !orientation); + + var X = x, + dx = length / steps, + txt = 0, + prev = 0; + + while (X <= x + length) { + type != "-" && type != " " && (path = path.concat(["M", X + .5, y - (type == "+" ? dashsize : !!orientation * dashsize * 2), "l", 0, dashsize * 2 + 1])); + text.push(txt = paper.text(X, y + addon, (labels && labels[j++]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr)); + + var bb = txt.getBBox(); + + if (prev >= bb.x - 5) { + text.pop(text.length - 1).remove(); + } else { + prev = bb.x + bb.width; + } + + label += d; + X += dx; + } + + if (Math.round(X - dx - x - length)) { + type != "-" && type != " " && (path = path.concat(["M", x + length + .5, y - (type == "+" ? dashsize : !!orientation * dashsize * 2), "l", 0, dashsize * 2 + 1])); + text.push(paper.text(x + length, y + addon, (labels && labels[j]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr)); + } + } + + var res = paper.path(path); + + res.text = text; + res.all = paper.set([res, text]); + res.remove = function () { + this.text.remove(); + this.constructor.prototype.remove.call(this); + }; + + return res; + }, + + labelise: function(label, val, total) { + if (label) { + return (label + "").replace(/(##+(?:\.#+)?)|(%%+(?:\.%+)?)/g, function (all, value, percent) { + if (value) { + return (+val).toFixed(value.replace(/^#+\.?/g, "").length); + } + if (percent) { + return (val * 100 / total).toFixed(percent.replace(/^%+\.?/g, "").length) + "%"; + } + }); + } else { + return (+val).toFixed(0); + } + } +} diff --git a/vendor/assets/javascripts/jquery.nicescroll.js b/vendor/assets/javascripts/jquery.nicescroll.js new file mode 100644 index 0000000000..7653f25df4 --- /dev/null +++ b/vendor/assets/javascripts/jquery.nicescroll.js @@ -0,0 +1,3634 @@ +/* jquery.nicescroll +-- version 3.6.0 +-- copyright 2014-11-21 InuYaksa*2014 +-- licensed under the MIT +-- +-- http://nicescroll.areaaperta.com/ +-- https://github.com/inuyaksa/jquery.nicescroll +-- +*/ + +(function(factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as anonymous module. + define(['jquery'], factory); + } else { + // Browser globals. + factory(jQuery); + } +}(function(jQuery) { + "use strict"; + + // globals + var domfocus = false; + var mousefocus = false; + var tabindexcounter = 0; + var ascrailcounter = 2000; + var globalmaxzindex = 0; + + var $ = jQuery; // sandbox + + // http://stackoverflow.com/questions/2161159/get-script-path + function getScriptPath() { + var scripts = document.getElementsByTagName('script'); + var path = scripts[scripts.length - 1].src.split('?')[0]; + return (path.split('/').length > 0) ? path.split('/').slice(0, -1).join('/') + '/' : ''; + } + + var vendors = ['webkit','ms','moz','o']; + + var setAnimationFrame = window.requestAnimationFrame || false; + var clearAnimationFrame = window.cancelAnimationFrame || false; + + if (!setAnimationFrame) { // legacy detection + for (var vx in vendors) { + var v = vendors[vx]; + if (!setAnimationFrame) setAnimationFrame = window[v + 'RequestAnimationFrame']; + if (!clearAnimationFrame) clearAnimationFrame = window[v + 'CancelAnimationFrame'] || window[v + 'CancelRequestAnimationFrame']; + } + } + + var ClsMutationObserver = window.MutationObserver || window.WebKitMutationObserver || false; + + var _globaloptions = { + zindex: "auto", + cursoropacitymin: 0, + cursoropacitymax: 1, + cursorcolor: "#424242", + cursorwidth: "5px", + cursorborder: "1px solid #fff", + cursorborderradius: "5px", + scrollspeed: 60, + mousescrollstep: 8 * 3, + touchbehavior: false, + hwacceleration: true, + usetransition: true, + boxzoom: false, + dblclickzoom: true, + gesturezoom: true, + grabcursorenabled: true, + autohidemode: true, + background: "", + iframeautoresize: true, + cursorminheight: 32, + preservenativescrolling: true, + railoffset: false, + railhoffset: false, + bouncescroll: true, + spacebarenabled: true, + railpadding: { + top: 0, + right: 0, + left: 0, + bottom: 0 + }, + disableoutline: true, + horizrailenabled: true, + railalign: "right", + railvalign: "bottom", + enabletranslate3d: true, + enablemousewheel: true, + enablekeyboard: true, + smoothscroll: true, + sensitiverail: true, + enablemouselockapi: true, + // cursormaxheight:false, + cursorfixedheight: false, + directionlockdeadzone: 6, + hidecursordelay: 400, + nativeparentscrolling: true, + enablescrollonselection: true, + overflowx: true, + overflowy: true, + cursordragspeed: 0.3, + rtlmode: "auto", + cursordragontouch: false, + oneaxismousemode: "auto", + scriptpath: getScriptPath(), + preventmultitouchscrolling: true + }; + + var browserdetected = false; + + var getBrowserDetection = function() { + + if (browserdetected) return browserdetected; + + var _el = document.createElement('DIV'), + _style = _el.style, + _agent = navigator.userAgent, + _platform = navigator.platform, + d = {}; + + d.haspointerlock = "pointerLockElement" in document || "webkitPointerLockElement" in document || "mozPointerLockElement" in document; + + d.isopera = ("opera" in window); // 12- + d.isopera12 = (d.isopera && ("getUserMedia" in navigator)); + d.isoperamini = (Object.prototype.toString.call(window.operamini) === "[object OperaMini]"); + + d.isie = (("all" in document) && ("attachEvent" in _el) && !d.isopera); //IE10- + d.isieold = (d.isie && !("msInterpolationMode" in _style)); // IE6 and older + d.isie7 = d.isie && !d.isieold && (!("documentMode" in document) || (document.documentMode == 7)); + d.isie8 = d.isie && ("documentMode" in document) && (document.documentMode == 8); + d.isie9 = d.isie && ("performance" in window) && (document.documentMode >= 9); + d.isie10 = d.isie && ("performance" in window) && (document.documentMode == 10); + d.isie11 = ("msRequestFullscreen" in _el) && (document.documentMode >= 11); // IE11+ + + d.isie9mobile = /iemobile.9/i.test(_agent); //wp 7.1 mango + if (d.isie9mobile) d.isie9 = false; + d.isie7mobile = (!d.isie9mobile && d.isie7) && /iemobile/i.test(_agent); //wp 7.0 + + d.ismozilla = ("MozAppearance" in _style); + + d.iswebkit = ("WebkitAppearance" in _style); + + d.ischrome = ("chrome" in window); + d.ischrome22 = (d.ischrome && d.haspointerlock); + d.ischrome26 = (d.ischrome && ("transition" in _style)); // issue with transform detection (maintain prefix) + + d.cantouch = ("ontouchstart" in document.documentElement) || ("ontouchstart" in window); // detection for Chrome Touch Emulation + d.hasmstouch = (window.MSPointerEvent || false); // IE10 pointer events + d.hasw3ctouch = (window.PointerEvent || false); //IE11 pointer events, following W3C Pointer Events spec + + d.ismac = /^mac$/i.test(_platform); + + d.isios = (d.cantouch && /iphone|ipad|ipod/i.test(_platform)); + d.isios4 = ((d.isios) && !("seal" in Object)); + d.isios7 = ((d.isios)&&("webkitHidden" in document)); //iOS 7+ + + d.isandroid = (/android/i.test(_agent)); + + d.haseventlistener = ("addEventListener" in _el); + + d.trstyle = false; + d.hastransform = false; + d.hastranslate3d = false; + d.transitionstyle = false; + d.hastransition = false; + d.transitionend = false; + + var a; + var check = ['transform', 'msTransform', 'webkitTransform', 'MozTransform', 'OTransform']; + for (a = 0; a < check.length; a++) { + if (typeof _style[check[a]] != "undefined") { + d.trstyle = check[a]; + break; + } + } + d.hastransform = (!!d.trstyle); + if (d.hastransform) { + _style[d.trstyle] = "translate3d(1px,2px,3px)"; + d.hastranslate3d = /translate3d/.test(_style[d.trstyle]); + } + + d.transitionstyle = false; + d.prefixstyle = ''; + d.transitionend = false; + check = ['transition', 'webkitTransition', 'msTransition', 'MozTransition', 'OTransition', 'OTransition', 'KhtmlTransition']; + var prefix = ['', '-webkit-', '-ms-', '-moz-', '-o-', '-o', '-khtml-']; + var evs = ['transitionend', 'webkitTransitionEnd', 'msTransitionEnd', 'transitionend', 'otransitionend', 'oTransitionEnd', 'KhtmlTransitionEnd']; + for (a = 0; a < check.length; a++) { + if (check[a] in _style) { + d.transitionstyle = check[a]; + d.prefixstyle = prefix[a]; + d.transitionend = evs[a]; + break; + } + } + if (d.ischrome26) { // always use prefix + d.prefixstyle = prefix[1]; + } + + d.hastransition = (d.transitionstyle); + + function detectCursorGrab() { + var lst = ['-webkit-grab', '-moz-grab', 'grab']; + if ((d.ischrome && !d.ischrome22) || d.isie) lst = []; // force setting for IE returns false positive and chrome cursor bug + for (var a = 0; a < lst.length; a++) { + var p = lst[a]; + _style.cursor = p; + if (_style.cursor == p) return p; + } + return 'url(//mail.google.com/mail/images/2/openhand.cur),n-resize'; // thank you google for custom cursor! + } + d.cursorgrabvalue = detectCursorGrab(); + + d.hasmousecapture = ("setCapture" in _el); + + d.hasMutationObserver = (ClsMutationObserver !== false); + + _el = null; //memory released + + browserdetected = d; + + return d; + }; + + var NiceScrollClass = function(myopt, me) { + + var self = this; + + this.version = '3.6.0'; + this.name = 'nicescroll'; + + this.me = me; + + this.opt = { + doc: $("body"), + win: false + }; + + $.extend(this.opt, _globaloptions); // clone opts + + // Options for internal use + this.opt.snapbackspeed = 80; + + if (myopt || false) { + for (var a in self.opt) { + if (typeof myopt[a] != "undefined") self.opt[a] = myopt[a]; + } + } + + this.doc = self.opt.doc; + this.iddoc = (this.doc && this.doc[0]) ? this.doc[0].id || '' : ''; + this.ispage = /^BODY|HTML/.test((self.opt.win) ? self.opt.win[0].nodeName : this.doc[0].nodeName); + this.haswrapper = (self.opt.win !== false); + this.win = self.opt.win || (this.ispage ? $(window) : this.doc); + this.docscroll = (this.ispage && !this.haswrapper) ? $(window) : this.win; + this.body = $("body"); + this.viewport = false; + + this.isfixed = false; + + this.iframe = false; + this.isiframe = ((this.doc[0].nodeName == 'IFRAME') && (this.win[0].nodeName == 'IFRAME')); + + this.istextarea = (this.win[0].nodeName == 'TEXTAREA'); + + this.forcescreen = false; //force to use screen position on events + + this.canshowonmouseevent = (self.opt.autohidemode != "scroll"); + + // Events jump table + this.onmousedown = false; + this.onmouseup = false; + this.onmousemove = false; + this.onmousewheel = false; + this.onkeypress = false; + this.ongesturezoom = false; + this.onclick = false; + + // Nicescroll custom events + this.onscrollstart = false; + this.onscrollend = false; + this.onscrollcancel = false; + + this.onzoomin = false; + this.onzoomout = false; + + // Let's start! + this.view = false; + this.page = false; + + this.scroll = { + x: 0, + y: 0 + }; + this.scrollratio = { + x: 0, + y: 0 + }; + this.cursorheight = 20; + this.scrollvaluemax = 0; + + this.isrtlmode = (this.opt.rtlmode == "auto") ? ((this.win[0] == window ? this.body : this.win).css("direction") == "rtl") : (this.opt.rtlmode === true); + // this.checkrtlmode = false; + + this.scrollrunning = false; + + this.scrollmom = false; + + this.observer = false; // observer div changes + this.observerremover = false; // observer on parent for remove detection + this.observerbody = false; // observer on body for position change + + do { + this.id = "ascrail" + (ascrailcounter++); + } while (document.getElementById(this.id)); + + this.rail = false; + this.cursor = false; + this.cursorfreezed = false; + this.selectiondrag = false; + + this.zoom = false; + this.zoomactive = false; + + this.hasfocus = false; + this.hasmousefocus = false; + + this.visibility = true; + this.railslocked = false; // locked by resize + this.locked = false; // prevent lost of locked status sets by user + this.hidden = false; // rails always hidden + this.cursoractive = true; // user can interact with cursors + + this.wheelprevented = false; //prevent mousewheel event + + this.overflowx = self.opt.overflowx; + this.overflowy = self.opt.overflowy; + + this.nativescrollingarea = false; + this.checkarea = 0; + + this.events = []; // event list for unbind + + this.saved = {}; // style saved + + this.delaylist = {}; + this.synclist = {}; + + this.lastdeltax = 0; + this.lastdeltay = 0; + + this.detected = getBrowserDetection(); + + var cap = $.extend({}, this.detected); + + this.canhwscroll = (cap.hastransform && self.opt.hwacceleration); + this.ishwscroll = (this.canhwscroll && self.haswrapper); + + this.hasreversehr = (this.isrtlmode&&!cap.iswebkit); //RTL mode with reverse horizontal axis + + this.istouchcapable = false; // desktop devices with touch screen support + + //## Check WebKit-based desktop with touch support + //## + Firefox 18 nightly build (desktop) false positive (or desktop with touch support) + if (cap.cantouch && !cap.isios && !cap.isandroid && (cap.iswebkit || cap.ismozilla)) { + this.istouchcapable = true; + cap.cantouch = false; // parse normal desktop events + } + + //## disable MouseLock API on user request + if (!self.opt.enablemouselockapi) { + cap.hasmousecapture = false; + cap.haspointerlock = false; + } + +/* deprecated + this.delayed = function(name, fn, tm, lazy) { + }; +*/ + + this.debounced = function(name, fn, tm) { + var dd = self.delaylist[name]; + self.delaylist[name] = fn; + if (!dd) { + setTimeout(function() { + var fn = self.delaylist[name]; + self.delaylist[name] = false; + fn.call(self); + }, tm); + } + }; + + var _onsync = false; + + this.synched = function(name, fn) { + + function requestSync() { + if (_onsync) return; + setAnimationFrame(function() { + _onsync = false; + for (var nn in self.synclist) { + var fn = self.synclist[nn]; + if (fn) fn.call(self); + self.synclist[nn] = false; + } + }); + _onsync = true; + } + + self.synclist[name] = fn; + requestSync(); + return name; + }; + + this.unsynched = function(name) { + if (self.synclist[name]) self.synclist[name] = false; + }; + + this.css = function(el, pars) { // save & set + for (var n in pars) { + self.saved.css.push([el, n, el.css(n)]); + el.css(n, pars[n]); + } + }; + + this.scrollTop = function(val) { + return (typeof val == "undefined") ? self.getScrollTop() : self.setScrollTop(val); + }; + + this.scrollLeft = function(val) { + return (typeof val == "undefined") ? self.getScrollLeft() : self.setScrollLeft(val); + }; + + // derived by by Dan Pupius www.pupius.net + var BezierClass = function(st, ed, spd, p1, p2, p3, p4) { + + this.st = st; + this.ed = ed; + this.spd = spd; + + this.p1 = p1 || 0; + this.p2 = p2 || 1; + this.p3 = p3 || 0; + this.p4 = p4 || 1; + + this.ts = (new Date()).getTime(); + this.df = this.ed - this.st; + }; + BezierClass.prototype = { + B2: function(t) { + return 3 * t * t * (1 - t); + }, + B3: function(t) { + return 3 * t * (1 - t) * (1 - t); + }, + B4: function(t) { + return (1 - t) * (1 - t) * (1 - t); + }, + getNow: function() { + var nw = (new Date()).getTime(); + var pc = 1 - ((nw - this.ts) / this.spd); + var bz = this.B2(pc) + this.B3(pc) + this.B4(pc); + return (pc < 0) ? this.ed : this.st + Math.round(this.df * bz); + }, + update: function(ed, spd) { + this.st = this.getNow(); + this.ed = ed; + this.spd = spd; + this.ts = (new Date()).getTime(); + this.df = this.ed - this.st; + return this; + } + }; + + //derived from http://stackoverflow.com/questions/11236090/ + function getMatrixValues() { + var tr = self.doc.css(cap.trstyle); + if (tr && (tr.substr(0, 6) == "matrix")) { + return tr.replace(/^.*\((.*)\)$/g, "$1").replace(/px/g, '').split(/, +/); + } + return false; + } + + if (this.ishwscroll) { + // hw accelerated scroll + this.doc.translate = { + x: 0, + y: 0, + tx: "0px", + ty: "0px" + }; + + //this one can help to enable hw accel on ios6 http://indiegamr.com/ios6-html-hardware-acceleration-changes-and-how-to-fix-them/ + if (cap.hastranslate3d && cap.isios) this.doc.css("-webkit-backface-visibility", "hidden"); // prevent flickering http://stackoverflow.com/questions/3461441/ + + this.getScrollTop = function(last) { + if (!last) { + var mtx = getMatrixValues(); + if (mtx) return (mtx.length == 16) ? -mtx[13] : -mtx[5]; //matrix3d 16 on IE10 + if (self.timerscroll && self.timerscroll.bz) return self.timerscroll.bz.getNow(); + } + return self.doc.translate.y; + }; + + this.getScrollLeft = function(last) { + if (!last) { + var mtx = getMatrixValues(); + if (mtx) return (mtx.length == 16) ? -mtx[12] : -mtx[4]; //matrix3d 16 on IE10 + if (self.timerscroll && self.timerscroll.bh) return self.timerscroll.bh.getNow(); + } + return self.doc.translate.x; + }; + + this.notifyScrollEvent = function(el) { + var e = document.createEvent("UIEvents"); + e.initUIEvent("scroll", false, true, window, 1); + e.niceevent = true; + el.dispatchEvent(e); + }; + + var cxscrollleft = (this.isrtlmode) ? 1 : -1; + + if (cap.hastranslate3d && self.opt.enabletranslate3d) { + this.setScrollTop = function(val, silent) { + self.doc.translate.y = val; + self.doc.translate.ty = (val * -1) + "px"; + self.doc.css(cap.trstyle, "translate3d(" + self.doc.translate.tx + "," + self.doc.translate.ty + ",0px)"); + if (!silent) self.notifyScrollEvent(self.win[0]); + }; + this.setScrollLeft = function(val, silent) { + self.doc.translate.x = val; + self.doc.translate.tx = (val * cxscrollleft) + "px"; + self.doc.css(cap.trstyle, "translate3d(" + self.doc.translate.tx + "," + self.doc.translate.ty + ",0px)"); + if (!silent) self.notifyScrollEvent(self.win[0]); + }; + } else { + this.setScrollTop = function(val, silent) { + self.doc.translate.y = val; + self.doc.translate.ty = (val * -1) + "px"; + self.doc.css(cap.trstyle, "translate(" + self.doc.translate.tx + "," + self.doc.translate.ty + ")"); + if (!silent) self.notifyScrollEvent(self.win[0]); + }; + this.setScrollLeft = function(val, silent) { + self.doc.translate.x = val; + self.doc.translate.tx = (val * cxscrollleft) + "px"; + self.doc.css(cap.trstyle, "translate(" + self.doc.translate.tx + "," + self.doc.translate.ty + ")"); + if (!silent) self.notifyScrollEvent(self.win[0]); + }; + } + } else { + // native scroll + this.getScrollTop = function() { + return self.docscroll.scrollTop(); + }; + this.setScrollTop = function(val) { + return self.docscroll.scrollTop(val); + }; + this.getScrollLeft = function() { + if (self.detected.ismozilla && self.isrtlmode) + return Math.abs(self.docscroll.scrollLeft()); + return self.docscroll.scrollLeft(); + }; + this.setScrollLeft = function(val) { + return self.docscroll.scrollLeft((self.detected.ismozilla && self.isrtlmode) ? -val : val); + }; + } + + this.getTarget = function(e) { + if (!e) return false; + if (e.target) return e.target; + if (e.srcElement) return e.srcElement; + return false; + }; + + this.hasParent = function(e, id) { + if (!e) return false; + var el = e.target || e.srcElement || e || false; + while (el && el.id != id) { + el = el.parentNode || false; + } + return (el !== false); + }; + + function getZIndex() { + var dom = self.win; + if ("zIndex" in dom) return dom.zIndex(); // use jQuery UI method when available + while (dom.length > 0) { + if (dom[0].nodeType == 9) return false; + var zi = dom.css('zIndex'); + if (!isNaN(zi) && zi != 0) return parseInt(zi); + dom = dom.parent(); + } + return false; + } + + //inspired by http://forum.jquery.com/topic/width-includes-border-width-when-set-to-thin-medium-thick-in-ie + var _convertBorderWidth = { + "thin": 1, + "medium": 3, + "thick": 5 + }; + + function getWidthToPixel(dom, prop, chkheight) { + var wd = dom.css(prop); + var px = parseFloat(wd); + if (isNaN(px)) { + px = _convertBorderWidth[wd] || 0; + var brd = (px == 3) ? ((chkheight) ? (self.win.outerHeight() - self.win.innerHeight()) : (self.win.outerWidth() - self.win.innerWidth())) : 1; //DON'T TRUST CSS + if (self.isie8 && px) px += 1; + return (brd) ? px : 0; + } + return px; + } + + this.getDocumentScrollOffset = function() { + return {top:window.pageYOffset||document.documentElement.scrollTop, + left:window.pageXOffset||document.documentElement.scrollLeft}; + } + + this.getOffset = function() { + if (self.isfixed) { + var ofs = self.win.offset(); // fix Chrome auto issue (when right/bottom props only) + var scrl = self.getDocumentScrollOffset(); + ofs.top-=scrl.top; + ofs.left-=scrl.left; + return ofs; + } + var ww = self.win.offset(); + if (!self.viewport) return ww; + var vp = self.viewport.offset(); + return { + top: ww.top - vp.top,// + self.viewport.scrollTop(), + left: ww.left - vp.left // + self.viewport.scrollLeft() + }; + }; + + this.updateScrollBar = function(len) { + if (self.ishwscroll) { + self.rail.css({ //** + height: self.win.innerHeight() - (self.opt.railpadding.top + self.opt.railpadding.bottom) + }); + if (self.railh) self.railh.css({ //** + width: self.win.innerWidth() - (self.opt.railpadding.left + self.opt.railpadding.right) + }); + + } else { + var wpos = self.getOffset(); + var pos = { + top: wpos.top, + left: wpos.left - (self.opt.railpadding.left + self.opt.railpadding.right) + }; + pos.top += getWidthToPixel(self.win, 'border-top-width', true); + pos.left += (self.rail.align) ? self.win.outerWidth() - getWidthToPixel(self.win, 'border-right-width') - self.rail.width : getWidthToPixel(self.win, 'border-left-width'); + + var off = self.opt.railoffset; + if (off) { + if (off.top) pos.top += off.top; + if (self.rail.align && off.left) pos.left += off.left; + } + + if (!self.railslocked) self.rail.css({ + top: pos.top, + left: pos.left, + height: ((len) ? len.h : self.win.innerHeight()) - (self.opt.railpadding.top + self.opt.railpadding.bottom) + }); + + if (self.zoom) { + self.zoom.css({ + top: pos.top + 1, + left: (self.rail.align == 1) ? pos.left - 20 : pos.left + self.rail.width + 4 + }); + } + + if (self.railh && !self.railslocked) { + var pos = { + top: wpos.top, + left: wpos.left + }; + var off = self.opt.railhoffset; + if (!!off) { + if (!!off.top) pos.top += off.top; + if (!!off.left) pos.left += off.left; + } + var y = (self.railh.align) ? pos.top + getWidthToPixel(self.win, 'border-top-width', true) + self.win.innerHeight() - self.railh.height : pos.top + getWidthToPixel(self.win, 'border-top-width', true); + var x = pos.left + getWidthToPixel(self.win, 'border-left-width'); + self.railh.css({ + top: y - (self.opt.railpadding.top + self.opt.railpadding.bottom), + left: x, + width: self.railh.width + }); + } + + + } + }; + + this.doRailClick = function(e, dbl, hr) { + var fn, pg, cur, pos; + + if (self.railslocked) return; + self.cancelEvent(e); + + if (dbl) { + fn = (hr) ? self.doScrollLeft : self.doScrollTop; + cur = (hr) ? ((e.pageX - self.railh.offset().left - (self.cursorwidth / 2)) * self.scrollratio.x) : ((e.pageY - self.rail.offset().top - (self.cursorheight / 2)) * self.scrollratio.y); + fn(cur); + } else { + fn = (hr) ? self.doScrollLeftBy : self.doScrollBy; + cur = (hr) ? self.scroll.x : self.scroll.y; + pos = (hr) ? e.pageX - self.railh.offset().left : e.pageY - self.rail.offset().top; + pg = (hr) ? self.view.w : self.view.h; + fn((cur >= pos) ? pg: -pg);// (cur >= pos) ? fn(pg): fn(-pg); + } + + }; + + self.hasanimationframe = (setAnimationFrame); + self.hascancelanimationframe = (clearAnimationFrame); + + if (!self.hasanimationframe) { + setAnimationFrame = function(fn) { + return setTimeout(fn, 15 - Math.floor((+new Date()) / 1000) % 16); + }; // 1000/60)}; + clearAnimationFrame = clearInterval; + } else if (!self.hascancelanimationframe) clearAnimationFrame = function() { + self.cancelAnimationFrame = true; + }; + + this.init = function() { + + self.saved.css = []; + + if (cap.isie7mobile) return true; // SORRY, DO NOT WORK! + if (cap.isoperamini) return true; // SORRY, DO NOT WORK! + + if (cap.hasmstouch) self.css((self.ispage) ? $("html") : self.win, { + '-ms-touch-action': 'none' + }); + + self.zindex = "auto"; + if (!self.ispage && self.opt.zindex == "auto") { + self.zindex = getZIndex() || "auto"; + } else { + self.zindex = self.opt.zindex; + } + + if (!self.ispage && self.zindex != "auto") { + if (self.zindex > globalmaxzindex) globalmaxzindex = self.zindex; + } + + if (self.isie && self.zindex == 0 && self.opt.zindex == "auto") { // fix IE auto == 0 + self.zindex = "auto"; + } + + if (!self.ispage || (!cap.cantouch && !cap.isieold && !cap.isie9mobile)) { + + var cont = self.docscroll; + if (self.ispage) cont = (self.haswrapper) ? self.win : self.doc; + + if (!cap.isie9mobile) self.css(cont, { + 'overflow-y': 'hidden' + }); + + if (self.ispage && cap.isie7) { + if (self.doc[0].nodeName == 'BODY') self.css($("html"), { + 'overflow-y': 'hidden' + }); //IE7 double scrollbar issue + else if (self.doc[0].nodeName == 'HTML') self.css($("body"), { + 'overflow-y': 'hidden' + }); //IE7 double scrollbar issue + } + + if (cap.isios && !self.ispage && !self.haswrapper) self.css($("body"), { + "-webkit-overflow-scrolling": "touch" + }); //force hw acceleration + + var cursor = $(document.createElement('div')); + cursor.css({ + position: "relative", + top: 0, + "float": "right", + width: self.opt.cursorwidth, + height: "0px", + 'background-color': self.opt.cursorcolor, + border: self.opt.cursorborder, + 'background-clip': 'padding-box', + '-webkit-border-radius': self.opt.cursorborderradius, + '-moz-border-radius': self.opt.cursorborderradius, + 'border-radius': self.opt.cursorborderradius + }); + + cursor.hborder = parseFloat(cursor.outerHeight() - cursor.innerHeight()); + + cursor.addClass('nicescroll-cursors'); + + self.cursor = cursor; + + var rail = $(document.createElement('div')); + rail.attr('id', self.id); + rail.addClass('nicescroll-rails nicescroll-rails-vr'); + + var v, a, kp = ["left","right","top","bottom"]; //** + for (var n in kp) { + a = kp[n]; + v = self.opt.railpadding[a]; + (v) ? rail.css("padding-"+a,v+"px") : self.opt.railpadding[a] = 0; + } + + rail.append(cursor); + + rail.width = Math.max(parseFloat(self.opt.cursorwidth), cursor.outerWidth()); + rail.css({ + width: rail.width + "px", + 'zIndex': self.zindex, + "background": self.opt.background, + cursor: "default" + }); + + rail.visibility = true; + rail.scrollable = true; + + rail.align = (self.opt.railalign == "left") ? 0 : 1; + + self.rail = rail; + + self.rail.drag = false; + + var zoom = false; + if (self.opt.boxzoom && !self.ispage && !cap.isieold) { + zoom = document.createElement('div'); + + self.bind(zoom, "click", self.doZoom); + self.bind(zoom, "mouseenter", function() { + self.zoom.css('opacity', self.opt.cursoropacitymax); + }); + self.bind(zoom, "mouseleave", function() { + self.zoom.css('opacity', self.opt.cursoropacitymin); + }); + + self.zoom = $(zoom); + self.zoom.css({ + "cursor": "pointer", + 'z-index': self.zindex, + 'backgroundImage': 'url(' + self.opt.scriptpath + 'zoomico.png)', + 'height': 18, + 'width': 18, + 'backgroundPosition': '0px 0px' + }); + if (self.opt.dblclickzoom) self.bind(self.win, "dblclick", self.doZoom); + if (cap.cantouch && self.opt.gesturezoom) { + self.ongesturezoom = function(e) { + if (e.scale > 1.5) self.doZoomIn(e); + if (e.scale < 0.8) self.doZoomOut(e); + return self.cancelEvent(e); + }; + self.bind(self.win, "gestureend", self.ongesturezoom); + } + } + + // init HORIZ + + self.railh = false; + var railh; + + if (self.opt.horizrailenabled) { + + self.css(cont, { + 'overflow-x': 'hidden' + }); + + var cursor = $(document.createElement('div')); + cursor.css({ + position: "absolute", + top: 0, + height: self.opt.cursorwidth, + width: "0px", + 'background-color': self.opt.cursorcolor, + border: self.opt.cursorborder, + 'background-clip': 'padding-box', + '-webkit-border-radius': self.opt.cursorborderradius, + '-moz-border-radius': self.opt.cursorborderradius, + 'border-radius': self.opt.cursorborderradius + }); + + if (cap.isieold) cursor.css({'overflow':'hidden'}); //IE6 horiz scrollbar issue + + cursor.wborder = parseFloat(cursor.outerWidth() - cursor.innerWidth()); + + cursor.addClass('nicescroll-cursors'); + + self.cursorh = cursor; + + railh = $(document.createElement('div')); + railh.attr('id', self.id + '-hr'); + railh.addClass('nicescroll-rails nicescroll-rails-hr'); + railh.height = Math.max(parseFloat(self.opt.cursorwidth), cursor.outerHeight()); + railh.css({ + height: railh.height + "px", + 'zIndex': self.zindex, + "background": self.opt.background + }); + + railh.append(cursor); + + railh.visibility = true; + railh.scrollable = true; + + railh.align = (self.opt.railvalign == "top") ? 0 : 1; + + self.railh = railh; + + self.railh.drag = false; + + } + + // + + if (self.ispage) { + rail.css({ + position: "fixed", + top: "0px", + height: "100%" + }); + (rail.align) ? rail.css({ + right: "0px" + }): rail.css({ + left: "0px" + }); + self.body.append(rail); + if (self.railh) { + railh.css({ + position: "fixed", + left: "0px", + width: "100%" + }); + (railh.align) ? railh.css({ + bottom: "0px" + }): railh.css({ + top: "0px" + }); + self.body.append(railh); + } + } else { + if (self.ishwscroll) { + if (self.win.css('position') == 'static') self.css(self.win, { + 'position': 'relative' + }); + var bd = (self.win[0].nodeName == 'HTML') ? self.body : self.win; + $(bd).scrollTop(0).scrollLeft(0); // fix rail position if content already scrolled + if (self.zoom) { + self.zoom.css({ + position: "absolute", + top: 1, + right: 0, + "margin-right": rail.width + 4 + }); + bd.append(self.zoom); + } + rail.css({ + position: "absolute", + top: 0 + }); + (rail.align) ? rail.css({ + right: 0 + }): rail.css({ + left: 0 + }); + bd.append(rail); + if (railh) { + railh.css({ + position: "absolute", + left: 0, + bottom: 0 + }); + (railh.align) ? railh.css({ + bottom: 0 + }): railh.css({ + top: 0 + }); + bd.append(railh); + } + } else { + self.isfixed = (self.win.css("position") == "fixed"); + var rlpos = (self.isfixed) ? "fixed" : "absolute"; + + if (!self.isfixed) self.viewport = self.getViewport(self.win[0]); + if (self.viewport) { + self.body = self.viewport; + if ((/fixed|absolute/.test(self.viewport.css("position"))) == false) self.css(self.viewport, { + "position": "relative" + }); + } + + rail.css({ + position: rlpos + }); + if (self.zoom) self.zoom.css({ + position: rlpos + }); + self.updateScrollBar(); + self.body.append(rail); + if (self.zoom) self.body.append(self.zoom); + if (self.railh) { + railh.css({ + position: rlpos + }); + self.body.append(railh); + } + } + + if (cap.isios) self.css(self.win, { + '-webkit-tap-highlight-color': 'rgba(0,0,0,0)', + '-webkit-touch-callout': 'none' + }); // prevent grey layer on click + + if (cap.isie && self.opt.disableoutline) self.win.attr("hideFocus", "true"); // IE, prevent dotted rectangle on focused div + if (cap.iswebkit && self.opt.disableoutline) self.win.css({"outline": "none"}); // Webkit outline + //if (cap.isopera&&self.opt.disableoutline) self.win.css({"outline":"0"}); // Opera 12- to test [TODO] + + } + + if (self.opt.autohidemode === false) { + self.autohidedom = false; + self.rail.css({ + opacity: self.opt.cursoropacitymax + }); + if (self.railh) self.railh.css({ + opacity: self.opt.cursoropacitymax + }); + } else if ((self.opt.autohidemode === true) || (self.opt.autohidemode === "leave")) { + self.autohidedom = $().add(self.rail); + if (cap.isie8) self.autohidedom = self.autohidedom.add(self.cursor); + if (self.railh) self.autohidedom = self.autohidedom.add(self.railh); + if (self.railh && cap.isie8) self.autohidedom = self.autohidedom.add(self.cursorh); + } else if (self.opt.autohidemode == "scroll") { + self.autohidedom = $().add(self.rail); + if (self.railh) self.autohidedom = self.autohidedom.add(self.railh); + } else if (self.opt.autohidemode == "cursor") { + self.autohidedom = $().add(self.cursor); + if (self.railh) self.autohidedom = self.autohidedom.add(self.cursorh); + } else if (self.opt.autohidemode == "hidden") { + self.autohidedom = false; + self.hide(); + self.railslocked = false; + } + + if (cap.isie9mobile) { + + self.scrollmom = new ScrollMomentumClass2D(self); + + self.onmangotouch = function() { + var py = self.getScrollTop(); + var px = self.getScrollLeft(); + + if ((py == self.scrollmom.lastscrolly) && (px == self.scrollmom.lastscrollx)) return true; + + var dfy = py - self.mangotouch.sy; + var dfx = px - self.mangotouch.sx; + var df = Math.round(Math.sqrt(Math.pow(dfx, 2) + Math.pow(dfy, 2))); + if (df == 0) return; + + var dry = (dfy < 0) ? -1 : 1; + var drx = (dfx < 0) ? -1 : 1; + + var tm = +new Date(); + if (self.mangotouch.lazy) clearTimeout(self.mangotouch.lazy); + + if (((tm - self.mangotouch.tm) > 80) || (self.mangotouch.dry != dry) || (self.mangotouch.drx != drx)) { + self.scrollmom.stop(); + self.scrollmom.reset(px, py); + self.mangotouch.sy = py; + self.mangotouch.ly = py; + self.mangotouch.sx = px; + self.mangotouch.lx = px; + self.mangotouch.dry = dry; + self.mangotouch.drx = drx; + self.mangotouch.tm = tm; + } else { + + self.scrollmom.stop(); + self.scrollmom.update(self.mangotouch.sx - dfx, self.mangotouch.sy - dfy); + self.mangotouch.tm = tm; + + var ds = Math.max(Math.abs(self.mangotouch.ly - py), Math.abs(self.mangotouch.lx - px)); + self.mangotouch.ly = py; + self.mangotouch.lx = px; + + if (ds > 2) { + self.mangotouch.lazy = setTimeout(function() { + self.mangotouch.lazy = false; + self.mangotouch.dry = 0; + self.mangotouch.drx = 0; + self.mangotouch.tm = 0; + self.scrollmom.doMomentum(30); + }, 100); + } + } + }; + + var top = self.getScrollTop(); + var lef = self.getScrollLeft(); + self.mangotouch = { + sy: top, + ly: top, + dry: 0, + sx: lef, + lx: lef, + drx: 0, + lazy: false, + tm: 0 + }; + + self.bind(self.docscroll, "scroll", self.onmangotouch); + + } else { + + if (cap.cantouch || self.istouchcapable || self.opt.touchbehavior || cap.hasmstouch) { + + self.scrollmom = new ScrollMomentumClass2D(self); + + self.ontouchstart = function(e) { + if (e.pointerType && e.pointerType != 2 && e.pointerType != "touch") return false; + + self.hasmoving = false; + + if (!self.railslocked) { + + var tg; + if (cap.hasmstouch) { + tg = (e.target) ? e.target : false; + while (tg) { + var nc = $(tg).getNiceScroll(); + if ((nc.length > 0) && (nc[0].me == self.me)) break; + if (nc.length > 0) return false; + if ((tg.nodeName == 'DIV') && (tg.id == self.id)) break; + tg = (tg.parentNode) ? tg.parentNode : false; + } + } + + self.cancelScroll(); + + tg = self.getTarget(e); + + if (tg) { + var skp = (/INPUT/i.test(tg.nodeName)) && (/range/i.test(tg.type)); + if (skp) return self.stopPropagation(e); + } + + if (!("clientX" in e) && ("changedTouches" in e)) { + e.clientX = e.changedTouches[0].clientX; + e.clientY = e.changedTouches[0].clientY; + } + + if (self.forcescreen) { + var le = e; + e = { + "original": (e.original) ? e.original : e + }; + e.clientX = le.screenX; + e.clientY = le.screenY; + } + + self.rail.drag = { + x: e.clientX, + y: e.clientY, + sx: self.scroll.x, + sy: self.scroll.y, + st: self.getScrollTop(), + sl: self.getScrollLeft(), + pt: 2, + dl: false + }; + + if (self.ispage || !self.opt.directionlockdeadzone) { + self.rail.drag.dl = "f"; + } else { + + var view = { + w: $(window).width(), + h: $(window).height() + }; + + var page = { + w: Math.max(document.body.scrollWidth, document.documentElement.scrollWidth), + h: Math.max(document.body.scrollHeight, document.documentElement.scrollHeight) + }; + + var maxh = Math.max(0, page.h - view.h); + var maxw = Math.max(0, page.w - view.w); + + if (!self.rail.scrollable && self.railh.scrollable) self.rail.drag.ck = (maxh > 0) ? "v" : false; + else if (self.rail.scrollable && !self.railh.scrollable) self.rail.drag.ck = (maxw > 0) ? "h" : false; + else self.rail.drag.ck = false; + if (!self.rail.drag.ck) self.rail.drag.dl = "f"; + } + + if (self.opt.touchbehavior && self.isiframe && cap.isie) { + var wp = self.win.position(); + self.rail.drag.x += wp.left; + self.rail.drag.y += wp.top; + } + + self.hasmoving = false; + self.lastmouseup = false; + self.scrollmom.reset(e.clientX, e.clientY); + + if (!cap.cantouch && !this.istouchcapable && !e.pointerType) { + + var ip = (tg) ? /INPUT|SELECT|TEXTAREA/i.test(tg.nodeName) : false; + if (!ip) { + if (!self.ispage && cap.hasmousecapture) tg.setCapture(); + if (self.opt.touchbehavior) { + if (tg.onclick && !(tg._onclick || false)) { // intercept DOM0 onclick event + tg._onclick = tg.onclick; + tg.onclick = function(e) { + if (self.hasmoving) return false; + tg._onclick.call(this, e); + }; + } + return self.cancelEvent(e); + } + return self.stopPropagation(e); + } + + if (/SUBMIT|CANCEL|BUTTON/i.test($(tg).attr('type'))) { + pc = { + "tg": tg, + "click": false + }; + self.preventclick = pc; + } + + } + } + + }; + + self.ontouchend = function(e) { + if (!self.rail.drag) return true; + if (self.rail.drag.pt == 2) { + if (e.pointerType && e.pointerType != 2 && e.pointerType != "touch") return false; + self.scrollmom.doMomentum(); + self.rail.drag = false; + if (self.hasmoving) { + self.lastmouseup = true; + self.hideCursor(); + if (cap.hasmousecapture) document.releaseCapture(); + if (!cap.cantouch) return self.cancelEvent(e); + } + } + else if (self.rail.drag.pt == 1) { + return self.onmouseup(e); + } + + }; + + var moveneedoffset = (self.opt.touchbehavior && self.isiframe && !cap.hasmousecapture); + + self.ontouchmove = function(e, byiframe) { + + if (!self.rail.drag) return false; + + if (e.targetTouches && self.opt.preventmultitouchscrolling) { + if (e.targetTouches.length > 1) return false; // multitouch + } + + if (e.pointerType && e.pointerType != 2 && e.pointerType != "touch") return false; + + if (self.rail.drag.pt == 2) { + if (cap.cantouch && (cap.isios) && (typeof e.original == "undefined")) return true; // prevent ios "ghost" events by clickable elements + + self.hasmoving = true; + + if (self.preventclick && !self.preventclick.click) { + self.preventclick.click = self.preventclick.tg.onclick || false; + self.preventclick.tg.onclick = self.onpreventclick; + } + + var ev = $.extend({ + "original": e + }, e); + e = ev; + + if (("changedTouches" in e)) { + e.clientX = e.changedTouches[0].clientX; + e.clientY = e.changedTouches[0].clientY; + } + + if (self.forcescreen) { + var le = e; + e = { + "original": (e.original) ? e.original : e + }; + e.clientX = le.screenX; + e.clientY = le.screenY; + } + + var ofy,ofx; + ofx = ofy = 0; + + if (moveneedoffset && !byiframe) { + var wp = self.win.position(); + ofx = -wp.left; + ofy = -wp.top; + } + + var fy = e.clientY + ofy; + var my = (fy - self.rail.drag.y); + var fx = e.clientX + ofx; + var mx = (fx - self.rail.drag.x); + + var ny = self.rail.drag.st - my; + + if (self.ishwscroll && self.opt.bouncescroll) { + if (ny < 0) { + ny = Math.round(ny / 2); + // fy = 0; + } else if (ny > self.page.maxh) { + ny = self.page.maxh + Math.round((ny - self.page.maxh) / 2); + // fy = 0; + } + } else { + if (ny < 0) { + ny = 0; + fy = 0; + } + if (ny > self.page.maxh) { + ny = self.page.maxh; + fy = 0; + } + } + + var nx; + if (self.railh && self.railh.scrollable) { + nx = (self.isrtlmode) ? mx - self.rail.drag.sl : self.rail.drag.sl - mx; + + if (self.ishwscroll && self.opt.bouncescroll) { + if (nx < 0) { + nx = Math.round(nx / 2); + // fx = 0; + } else if (nx > self.page.maxw) { + nx = self.page.maxw + Math.round((nx - self.page.maxw) / 2); + // fx = 0; + } + } else { + if (nx < 0) { + nx = 0; + fx = 0; + } + if (nx > self.page.maxw) { + nx = self.page.maxw; + fx = 0; + } + } + + } + + var grabbed = false; + if (self.rail.drag.dl) { + grabbed = true; + if (self.rail.drag.dl == "v") nx = self.rail.drag.sl; + else if (self.rail.drag.dl == "h") ny = self.rail.drag.st; + } else { + var ay = Math.abs(my); + var ax = Math.abs(mx); + var dz = self.opt.directionlockdeadzone; + if (self.rail.drag.ck == "v") { + if (ay > dz && (ax <= (ay * 0.3))) { + self.rail.drag = false; + return true; + } else if (ax > dz) { + self.rail.drag.dl = "f"; + $("body").scrollTop($("body").scrollTop()); // stop iOS native scrolling (when active javascript has blocked) + } + } else if (self.rail.drag.ck == "h") { + if (ax > dz && (ay <= (ax * 0.3))) { + self.rail.drag = false; + return true; + } else if (ay > dz) { + self.rail.drag.dl = "f"; + $("body").scrollLeft($("body").scrollLeft()); // stop iOS native scrolling (when active javascript has blocked) + } + } + } + + self.synched("touchmove", function() { + if (self.rail.drag && (self.rail.drag.pt == 2)) { + if (self.prepareTransition) self.prepareTransition(0); + if (self.rail.scrollable) self.setScrollTop(ny); + self.scrollmom.update(fx, fy); + if (self.railh && self.railh.scrollable) { + self.setScrollLeft(nx); + self.showCursor(ny, nx); + } else { + self.showCursor(ny); + } + if (cap.isie10) document.selection.clear(); + } + }); + + if (cap.ischrome && self.istouchcapable) grabbed = false; //chrome touch emulation doesn't like! + if (grabbed) return self.cancelEvent(e); + } + else if (self.rail.drag.pt == 1) { // drag on cursor + return self.onmousemove(e); + } + + }; + + } + + self.onmousedown = function(e, hronly) { + if (self.rail.drag && self.rail.drag.pt != 1) return; + if (self.railslocked) return self.cancelEvent(e); + self.cancelScroll(); + self.rail.drag = { + x: e.clientX, + y: e.clientY, + sx: self.scroll.x, + sy: self.scroll.y, + pt: 1, + hr: (!!hronly) + }; + var tg = self.getTarget(e); + if (!self.ispage && cap.hasmousecapture) tg.setCapture(); + if (self.isiframe && !cap.hasmousecapture) { + self.saved.csspointerevents = self.doc.css("pointer-events"); + self.css(self.doc, { + "pointer-events": "none" + }); + } + self.hasmoving = false; + return self.cancelEvent(e); + }; + + self.onmouseup = function(e) { + if (self.rail.drag) { + if (self.rail.drag.pt != 1) return true; + if (cap.hasmousecapture) document.releaseCapture(); + if (self.isiframe && !cap.hasmousecapture) self.doc.css("pointer-events", self.saved.csspointerevents); + self.rail.drag = false; + //if (!self.rail.active) self.hideCursor(); + if (self.hasmoving) self.triggerScrollEnd(); // TODO - check &&!self.scrollrunning + return self.cancelEvent(e); + } + }; + + self.onmousemove = function(e) { + if (self.rail.drag) { + if (self.rail.drag.pt != 1) return; + + if (cap.ischrome && e.which == 0) return self.onmouseup(e); + + self.cursorfreezed = true; + self.hasmoving = true; + + if (self.rail.drag.hr) { + self.scroll.x = self.rail.drag.sx + (e.clientX - self.rail.drag.x); + if (self.scroll.x < 0) self.scroll.x = 0; + var mw = self.scrollvaluemaxw; + if (self.scroll.x > mw) self.scroll.x = mw; + } else { + self.scroll.y = self.rail.drag.sy + (e.clientY - self.rail.drag.y); + if (self.scroll.y < 0) self.scroll.y = 0; + var my = self.scrollvaluemax; + if (self.scroll.y > my) self.scroll.y = my; + } + + self.synched('mousemove', function() { + if (self.rail.drag && (self.rail.drag.pt == 1)) { + self.showCursor(); + if (self.rail.drag.hr) { + if (self.hasreversehr) { + self.doScrollLeft(self.scrollvaluemaxw-Math.round(self.scroll.x * self.scrollratio.x), self.opt.cursordragspeed); + } else { + self.doScrollLeft(Math.round(self.scroll.x * self.scrollratio.x), self.opt.cursordragspeed); + } + } + else self.doScrollTop(Math.round(self.scroll.y * self.scrollratio.y), self.opt.cursordragspeed); + } + }); + + return self.cancelEvent(e); + } + /* + else { + self.checkarea = true; + } +*/ + }; + + if (cap.cantouch || self.opt.touchbehavior) { + + self.onpreventclick = function(e) { + if (self.preventclick) { + self.preventclick.tg.onclick = self.preventclick.click; + self.preventclick = false; + return self.cancelEvent(e); + } + } + + self.bind(self.win, "mousedown", self.ontouchstart); // control content dragging + + self.onclick = (cap.isios) ? false : function(e) { + if (self.lastmouseup) { + self.lastmouseup = false; + return self.cancelEvent(e); + } else { + return true; + } + }; + + if (self.opt.grabcursorenabled && cap.cursorgrabvalue) { + self.css((self.ispage) ? self.doc : self.win, { + 'cursor': cap.cursorgrabvalue + }); + self.css(self.rail, { + 'cursor': cap.cursorgrabvalue + }); + } + + } else { + + var checkSelectionScroll = function(e) { + if (!self.selectiondrag) return; + + if (e) { + var ww = self.win.outerHeight(); + var df = (e.pageY - self.selectiondrag.top); + if (df > 0 && df < ww) df = 0; + if (df >= ww) df -= ww; + self.selectiondrag.df = df; + } + if (self.selectiondrag.df == 0) return; + + var rt = -Math.floor(self.selectiondrag.df / 6) * 2; + self.doScrollBy(rt); + + self.debounced("doselectionscroll", function() { + checkSelectionScroll() + }, 50); + }; + + if ("getSelection" in document) { // A grade - Major browsers + self.hasTextSelected = function() { + return (document.getSelection().rangeCount > 0); + }; + } else if ("selection" in document) { //IE9- + self.hasTextSelected = function() { + return (document.selection.type != "None"); + }; + } else { + self.hasTextSelected = function() { // no support + return false; + }; + } + + self.onselectionstart = function(e) { +/* More testing - severe chrome issues + if (!self.haswrapper&&(e.which&&e.which==2)) { // fool browser to manage middle button scrolling + self.win.css({'overflow':'auto'}); + setTimeout(function(){ + self.win.css({'overflow':''}); + },10); + return true; + } +*/ + if (self.ispage) return; + self.selectiondrag = self.win.offset(); + }; + + self.onselectionend = function(e) { + self.selectiondrag = false; + }; + self.onselectiondrag = function(e) { + if (!self.selectiondrag) return; + if (self.hasTextSelected()) self.debounced("selectionscroll", function() { + checkSelectionScroll(e) + }, 250); + }; + + + } + + if (cap.hasw3ctouch) { //IE11+ + self.css(self.rail, { + 'touch-action': 'none' + }); + self.css(self.cursor, { + 'touch-action': 'none' + }); + self.bind(self.win, "pointerdown", self.ontouchstart); + self.bind(document, "pointerup", self.ontouchend); + self.bind(document, "pointermove", self.ontouchmove); + } else if (cap.hasmstouch) { //IE10 + self.css(self.rail, { + '-ms-touch-action': 'none' + }); + self.css(self.cursor, { + '-ms-touch-action': 'none' + }); + self.bind(self.win, "MSPointerDown", self.ontouchstart); + self.bind(document, "MSPointerUp", self.ontouchend); + self.bind(document, "MSPointerMove", self.ontouchmove); + self.bind(self.cursor, "MSGestureHold", function(e) { + e.preventDefault() + }); + self.bind(self.cursor, "contextmenu", function(e) { + e.preventDefault() + }); + } else if (this.istouchcapable) { //desktop with screen touch enabled + self.bind(self.win, "touchstart", self.ontouchstart); + self.bind(document, "touchend", self.ontouchend); + self.bind(document, "touchcancel", self.ontouchend); + self.bind(document, "touchmove", self.ontouchmove); + } + + + if (self.opt.cursordragontouch || (!cap.cantouch && !self.opt.touchbehavior)) { + + self.rail.css({ + "cursor": "default" + }); + self.railh && self.railh.css({ + "cursor": "default" + }); + + self.jqbind(self.rail, "mouseenter", function() { + if (!self.ispage && !self.win.is(":visible")) return false; + if (self.canshowonmouseevent) self.showCursor(); + self.rail.active = true; + }); + self.jqbind(self.rail, "mouseleave", function() { + self.rail.active = false; + if (!self.rail.drag) self.hideCursor(); + }); + + if (self.opt.sensitiverail) { + self.bind(self.rail, "click", function(e) { + self.doRailClick(e, false, false) + }); + self.bind(self.rail, "dblclick", function(e) { + self.doRailClick(e, true, false) + }); + self.bind(self.cursor, "click", function(e) { + self.cancelEvent(e) + }); + self.bind(self.cursor, "dblclick", function(e) { + self.cancelEvent(e) + }); + } + + if (self.railh) { + self.jqbind(self.railh, "mouseenter", function() { + if (!self.ispage && !self.win.is(":visible")) return false; + if (self.canshowonmouseevent) self.showCursor(); + self.rail.active = true; + }); + self.jqbind(self.railh, "mouseleave", function() { + self.rail.active = false; + if (!self.rail.drag) self.hideCursor(); + }); + + if (self.opt.sensitiverail) { + self.bind(self.railh, "click", function(e) { + self.doRailClick(e, false, true) + }); + self.bind(self.railh, "dblclick", function(e) { + self.doRailClick(e, true, true) + }); + self.bind(self.cursorh, "click", function(e) { + self.cancelEvent(e) + }); + self.bind(self.cursorh, "dblclick", function(e) { + self.cancelEvent(e) + }); + } + + } + + } + + if (!cap.cantouch && !self.opt.touchbehavior) { + + self.bind((cap.hasmousecapture) ? self.win : document, "mouseup", self.onmouseup); + self.bind(document, "mousemove", self.onmousemove); + if (self.onclick) self.bind(document, "click", self.onclick); + + self.bind(self.cursor, "mousedown", self.onmousedown); + self.bind(self.cursor, "mouseup", self.onmouseup); + + if (self.railh) { + self.bind(self.cursorh, "mousedown", function(e) { + self.onmousedown(e, true) + }); + self.bind(self.cursorh, "mouseup", self.onmouseup); + } + + if (!self.ispage && self.opt.enablescrollonselection) { + self.bind(self.win[0], "mousedown", self.onselectionstart); + self.bind(document, "mouseup", self.onselectionend); + self.bind(self.cursor, "mouseup", self.onselectionend); + if (self.cursorh) self.bind(self.cursorh, "mouseup", self.onselectionend); + self.bind(document, "mousemove", self.onselectiondrag); + } + + if (self.zoom) { + self.jqbind(self.zoom, "mouseenter", function() { + if (self.canshowonmouseevent) self.showCursor(); + self.rail.active = true; + }); + self.jqbind(self.zoom, "mouseleave", function() { + self.rail.active = false; + if (!self.rail.drag) self.hideCursor(); + }); + } + + } else { + + self.bind((cap.hasmousecapture) ? self.win : document, "mouseup", self.ontouchend); + self.bind(document, "mousemove", self.ontouchmove); + if (self.onclick) self.bind(document, "click", self.onclick); + + if (self.opt.cursordragontouch) { + self.bind(self.cursor, "mousedown", self.onmousedown); + self.bind(self.cursor, "mouseup", self.onmouseup); + //self.bind(self.cursor, "mousemove", self.onmousemove); + self.cursorh && self.bind(self.cursorh, "mousedown", function(e) { + self.onmousedown(e, true) + }); + //self.cursorh && self.bind(self.cursorh, "mousemove", self.onmousemove); + self.cursorh && self.bind(self.cursorh, "mouseup", self.onmouseup); + } + + } + + if (self.opt.enablemousewheel) { + if (!self.isiframe) self.bind((cap.isie && self.ispage) ? document : self.win /*self.docscroll*/ , "mousewheel", self.onmousewheel); + self.bind(self.rail, "mousewheel", self.onmousewheel); + if (self.railh) self.bind(self.railh, "mousewheel", self.onmousewheelhr); + } + + if (!self.ispage && !cap.cantouch && !(/HTML|^BODY/.test(self.win[0].nodeName))) { + if (!self.win.attr("tabindex")) self.win.attr({ + "tabindex": tabindexcounter++ + }); + + self.jqbind(self.win, "focus", function(e) { + domfocus = (self.getTarget(e)).id || true; + self.hasfocus = true; + if (self.canshowonmouseevent) self.noticeCursor(); + }); + self.jqbind(self.win, "blur", function(e) { + domfocus = false; + self.hasfocus = false; + }); + + self.jqbind(self.win, "mouseenter", function(e) { + mousefocus = (self.getTarget(e)).id || true; + self.hasmousefocus = true; + if (self.canshowonmouseevent) self.noticeCursor(); + }); + self.jqbind(self.win, "mouseleave", function() { + mousefocus = false; + self.hasmousefocus = false; + if (!self.rail.drag) self.hideCursor(); + }); + + } + + } // !ie9mobile + + //Thanks to http://www.quirksmode.org !! + self.onkeypress = function(e) { + if (self.railslocked && self.page.maxh == 0) return true; + + e = (e) ? e : window.e; + var tg = self.getTarget(e); + if (tg && /INPUT|TEXTAREA|SELECT|OPTION/.test(tg.nodeName)) { + var tp = tg.getAttribute('type') || tg.type || false; + if ((!tp) || !(/submit|button|cancel/i.tp)) return true; + } + + if ($(tg).attr('contenteditable')) return true; + + if (self.hasfocus || (self.hasmousefocus && !domfocus) || (self.ispage && !domfocus && !mousefocus)) { + var key = e.keyCode; + + if (self.railslocked && key != 27) return self.cancelEvent(e); + + var ctrl = e.ctrlKey || false; + var shift = e.shiftKey || false; + + var ret = false; + switch (key) { + case 38: + case 63233: //safari + self.doScrollBy(24 * 3); + ret = true; + break; + case 40: + case 63235: //safari + self.doScrollBy(-24 * 3); + ret = true; + break; + case 37: + case 63232: //safari + if (self.railh) { + (ctrl) ? self.doScrollLeft(0): self.doScrollLeftBy(24 * 3); + ret = true; + } + break; + case 39: + case 63234: //safari + if (self.railh) { + (ctrl) ? self.doScrollLeft(self.page.maxw): self.doScrollLeftBy(-24 * 3); + ret = true; + } + break; + case 33: + case 63276: // safari + self.doScrollBy(self.view.h); + ret = true; + break; + case 34: + case 63277: // safari + self.doScrollBy(-self.view.h); + ret = true; + break; + case 36: + case 63273: // safari + (self.railh && ctrl) ? self.doScrollPos(0, 0): self.doScrollTo(0); + ret = true; + break; + case 35: + case 63275: // safari + (self.railh && ctrl) ? self.doScrollPos(self.page.maxw, self.page.maxh): self.doScrollTo(self.page.maxh); + ret = true; + break; + case 32: + if (self.opt.spacebarenabled) { + (shift) ? self.doScrollBy(self.view.h): self.doScrollBy(-self.view.h); + ret = true; + } + break; + case 27: // ESC + if (self.zoomactive) { + self.doZoom(); + ret = true; + } + break; + } + if (ret) return self.cancelEvent(e); + } + }; + + if (self.opt.enablekeyboard) self.bind(document, (cap.isopera && !cap.isopera12) ? "keypress" : "keydown", self.onkeypress); + + self.bind(document, "keydown", function(e) { + var ctrl = e.ctrlKey || false; + if (ctrl) self.wheelprevented = true; + }); + self.bind(document, "keyup", function(e) { + var ctrl = e.ctrlKey || false; + if (!ctrl) self.wheelprevented = false; + }); + self.bind(window,"blur",function(e){ + self.wheelprevented = false; + }); + + self.bind(window, 'resize', self.lazyResize); + self.bind(window, 'orientationchange', self.lazyResize); + + self.bind(window, "load", self.lazyResize); + + if (cap.ischrome && !self.ispage && !self.haswrapper) { //chrome void scrollbar bug - it persists in version 26 + var tmp = self.win.attr("style"); + var ww = parseFloat(self.win.css("width")) + 1; + self.win.css('width', ww); + self.synched("chromefix", function() { + self.win.attr("style", tmp) + }); + } + + + // Trying a cross-browser implementation - good luck! + + self.onAttributeChange = function(e) { + self.lazyResize(self.isieold ? 250 : 30); + }; + + if (ClsMutationObserver !== false) { + self.observerbody = new ClsMutationObserver(function(mutations) { + mutations.forEach(function(mut){ + if (mut.type=="attributes") { + return ($("body").hasClass("modal-open")) ? self.hide() : self.show(); // Support for Bootstrap modal + } + }); + if (document.body.scrollHeight!=self.page.maxh) return self.lazyResize(30); + }); + self.observerbody.observe(document.body, { + childList: true, + subtree: true, + characterData: false, + attributes: true, + attributeFilter: ['class'] + }); + } + + if (!self.ispage && !self.haswrapper) { + // redesigned MutationObserver for Chrome18+/Firefox14+/iOS6+ with support for: remove div, add/remove content + if (ClsMutationObserver !== false) { + self.observer = new ClsMutationObserver(function(mutations) { + mutations.forEach(self.onAttributeChange); + }); + self.observer.observe(self.win[0], { + childList: true, + characterData: false, + attributes: true, + subtree: false + }); + self.observerremover = new ClsMutationObserver(function(mutations) { + mutations.forEach(function(mo) { + if (mo.removedNodes.length > 0) { + for (var dd in mo.removedNodes) { + if (!!self && (mo.removedNodes[dd] == self.win[0])) return self.remove(); + } + } + }); + }); + self.observerremover.observe(self.win[0].parentNode, { + childList: true, + characterData: false, + attributes: false, + subtree: false + }); + } else { + self.bind(self.win, (cap.isie && !cap.isie9) ? "propertychange" : "DOMAttrModified", self.onAttributeChange); + if (cap.isie9) self.win[0].attachEvent("onpropertychange", self.onAttributeChange); //IE9 DOMAttrModified bug + self.bind(self.win, "DOMNodeRemoved", function(e) { + if (e.target == self.win[0]) self.remove(); + }); + } + } + + // + + if (!self.ispage && self.opt.boxzoom) self.bind(window, "resize", self.resizeZoom); + if (self.istextarea) self.bind(self.win, "mouseup", self.lazyResize); + + // self.checkrtlmode = true; + self.lazyResize(30); + + } + + if (this.doc[0].nodeName == 'IFRAME') { + var oniframeload = function() { + self.iframexd = false; + var doc; + try { + doc = 'contentDocument' in this ? this.contentDocument : this.contentWindow.document; + var a = doc.domain; + } catch (e) { + self.iframexd = true; + doc = false + } + + if (self.iframexd) { + if ("console" in window) console.log('NiceScroll error: policy restriced iframe'); + return true; //cross-domain - I can't manage this + } + + self.forcescreen = true; + + if (self.isiframe) { + self.iframe = { + "doc": $(doc), + "html": self.doc.contents().find('html')[0], + "body": self.doc.contents().find('body')[0] + }; + self.getContentSize = function() { + return { + w: Math.max(self.iframe.html.scrollWidth, self.iframe.body.scrollWidth), + h: Math.max(self.iframe.html.scrollHeight, self.iframe.body.scrollHeight) + }; + }; + self.docscroll = $(self.iframe.body); //$(this.contentWindow); + } + + if (!cap.isios && self.opt.iframeautoresize && !self.isiframe) { + self.win.scrollTop(0); // reset position + self.doc.height(""); //reset height to fix browser bug + var hh = Math.max(doc.getElementsByTagName('html')[0].scrollHeight, doc.body.scrollHeight); + self.doc.height(hh); + } + self.lazyResize(30); + + if (cap.isie7) self.css($(self.iframe.html), { + 'overflow-y': 'hidden' + }); + self.css($(self.iframe.body), { + 'overflow-y': 'hidden' + }); + + if (cap.isios && self.haswrapper) { + self.css($(doc.body), { + '-webkit-transform': 'translate3d(0,0,0)' + }); // avoid iFrame content clipping - thanks to http://blog.derraab.com/2012/04/02/avoid-iframe-content-clipping-with-css-transform-on-ios/ + } + + if ('contentWindow' in this) { + self.bind(this.contentWindow, "scroll", self.onscroll); //IE8 & minor + } else { + self.bind(doc, "scroll", self.onscroll); + } + + if (self.opt.enablemousewheel) { + self.bind(doc, "mousewheel", self.onmousewheel); + } + + if (self.opt.enablekeyboard) self.bind(doc, (cap.isopera) ? "keypress" : "keydown", self.onkeypress); + + if (cap.cantouch || self.opt.touchbehavior) { + self.bind(doc, "mousedown", self.ontouchstart); + self.bind(doc, "mousemove", function(e) { + return self.ontouchmove(e, true) + }); + if (self.opt.grabcursorenabled && cap.cursorgrabvalue) self.css($(doc.body), { + 'cursor': cap.cursorgrabvalue + }); + } + + self.bind(doc, "mouseup", self.ontouchend); + + if (self.zoom) { + if (self.opt.dblclickzoom) self.bind(doc, 'dblclick', self.doZoom); + if (self.ongesturezoom) self.bind(doc, "gestureend", self.ongesturezoom); + } + }; + + if (this.doc[0].readyState && this.doc[0].readyState == "complete") { + setTimeout(function() { + oniframeload.call(self.doc[0], false) + }, 500); + } + self.bind(this.doc, "load", oniframeload); + + } + + }; + + this.showCursor = function(py, px) { + if (self.cursortimeout) { + clearTimeout(self.cursortimeout); + self.cursortimeout = 0; + } + if (!self.rail) return; + if (self.autohidedom) { + self.autohidedom.stop().css({ + opacity: self.opt.cursoropacitymax + }); + self.cursoractive = true; + } + + if (!self.rail.drag || self.rail.drag.pt != 1) { + if ((typeof py != "undefined") && (py !== false)) { + self.scroll.y = Math.round(py * 1 / self.scrollratio.y); + } + if (typeof px != "undefined") { + self.scroll.x = Math.round(px * 1 / self.scrollratio.x); + } + } + + self.cursor.css({ + height: self.cursorheight, + top: self.scroll.y + }); + if (self.cursorh) { + var lx = (self.hasreversehr) ? self.scrollvaluemaxw-self.scroll.x : self.scroll.x; + (!self.rail.align && self.rail.visibility) ? self.cursorh.css({ + width: self.cursorwidth, + left: lx + self.rail.width + }): self.cursorh.css({ + width: self.cursorwidth, + left: lx + }); + self.cursoractive = true; + } + + if (self.zoom) self.zoom.stop().css({ + opacity: self.opt.cursoropacitymax + }); + }; + + this.hideCursor = function(tm) { + if (self.cursortimeout) return; + if (!self.rail) return; + if (!self.autohidedom) return; + if (self.hasmousefocus && self.opt.autohidemode == "leave") return; + self.cursortimeout = setTimeout(function() { + if (!self.rail.active || !self.showonmouseevent) { + self.autohidedom.stop().animate({ + opacity: self.opt.cursoropacitymin + }); + if (self.zoom) self.zoom.stop().animate({ + opacity: self.opt.cursoropacitymin + }); + self.cursoractive = false; + } + self.cursortimeout = 0; + }, tm || self.opt.hidecursordelay); + }; + + this.noticeCursor = function(tm, py, px) { + self.showCursor(py, px); + if (!self.rail.active) self.hideCursor(tm); + }; + + this.getContentSize = + (self.ispage) ? + function() { + return { + w: Math.max(document.body.scrollWidth, document.documentElement.scrollWidth), + h: Math.max(document.body.scrollHeight, document.documentElement.scrollHeight) + } + } : (self.haswrapper) ? + function() { + return { + w: self.doc.outerWidth() + parseInt(self.win.css('paddingLeft')) + parseInt(self.win.css('paddingRight')), + h: self.doc.outerHeight() + parseInt(self.win.css('paddingTop')) + parseInt(self.win.css('paddingBottom')) + } + } : function() { + return { + w: self.docscroll[0].scrollWidth, + h: self.docscroll[0].scrollHeight + } + }; + + this.onResize = function(e, page) { + + if (!self || !self.win) return false; + + if (!self.haswrapper && !self.ispage) { + if (self.win.css('display') == 'none') { + if (self.visibility) self.hideRail().hideRailHr(); + return false; + } else { + if (!self.hidden && !self.visibility) self.showRail().showRailHr(); + } + } + + var premaxh = self.page.maxh; + var premaxw = self.page.maxw; + + var preview = { + h: self.view.h, + w: self.view.w + }; + + self.view = { + w: (self.ispage) ? self.win.width() : parseInt(self.win[0].clientWidth), + h: (self.ispage) ? self.win.height() : parseInt(self.win[0].clientHeight) + }; + + self.page = (page) ? page : self.getContentSize(); + + self.page.maxh = Math.max(0, self.page.h - self.view.h); + self.page.maxw = Math.max(0, self.page.w - self.view.w); + + if ((self.page.maxh == premaxh) && (self.page.maxw == premaxw) && (self.view.w == preview.w) && (self.view.h == preview.h)) { + // test position + if (!self.ispage) { + var pos = self.win.offset(); + if (self.lastposition) { + var lst = self.lastposition; + if ((lst.top == pos.top) && (lst.left == pos.left)) return self; //nothing to do + } + self.lastposition = pos; + } else { + return self; //nothing to do + } + } + + if (self.page.maxh == 0) { + self.hideRail(); + self.scrollvaluemax = 0; + self.scroll.y = 0; + self.scrollratio.y = 0; + self.cursorheight = 0; + self.setScrollTop(0); + self.rail.scrollable = false; + } else { + self.page.maxh -= (self.opt.railpadding.top + self.opt.railpadding.bottom); //** + self.rail.scrollable = true; + } + + if (self.page.maxw == 0) { + self.hideRailHr(); + self.scrollvaluemaxw = 0; + self.scroll.x = 0; + self.scrollratio.x = 0; + self.cursorwidth = 0; + self.setScrollLeft(0); + self.railh.scrollable = false; + } else { + self.page.maxw -= (self.opt.railpadding.left + self.opt.railpadding.right); //** + self.railh.scrollable = true; + } + + self.railslocked = (self.locked) || ((self.page.maxh == 0) && (self.page.maxw == 0)); + if (self.railslocked) { + if (!self.ispage) self.updateScrollBar(self.view); + return false; + } + + if (!self.hidden && !self.visibility) { + self.showRail().showRailHr(); + } + else if (!self.hidden && !self.railh.visibility) self.showRailHr(); + + if (self.istextarea && self.win.css('resize') && self.win.css('resize') != 'none') self.view.h -= 20; + + self.cursorheight = Math.min(self.view.h, Math.round(self.view.h * (self.view.h / self.page.h))); + self.cursorheight = (self.opt.cursorfixedheight) ? self.opt.cursorfixedheight : Math.max(self.opt.cursorminheight, self.cursorheight); + + self.cursorwidth = Math.min(self.view.w, Math.round(self.view.w * (self.view.w / self.page.w))); + self.cursorwidth = (self.opt.cursorfixedheight) ? self.opt.cursorfixedheight : Math.max(self.opt.cursorminheight, self.cursorwidth); + + self.scrollvaluemax = self.view.h - self.cursorheight - self.cursor.hborder - (self.opt.railpadding.top + self.opt.railpadding.bottom); //** + + if (self.railh) { + self.railh.width = (self.page.maxh > 0) ? (self.view.w - self.rail.width) : self.view.w; + self.scrollvaluemaxw = self.railh.width - self.cursorwidth - self.cursorh.wborder - (self.opt.railpadding.left + self.opt.railpadding.right); //** + } + + /* + if (self.checkrtlmode&&self.railh) { + self.checkrtlmode = false; + if (self.opt.rtlmode&&self.scroll.x==0) self.setScrollLeft(self.page.maxw); + } +*/ + + if (!self.ispage) self.updateScrollBar(self.view); + + self.scrollratio = { + x: (self.page.maxw / self.scrollvaluemaxw), + y: (self.page.maxh / self.scrollvaluemax) + }; + + var sy = self.getScrollTop(); + if (sy > self.page.maxh) { + self.doScrollTop(self.page.maxh); + } else { + self.scroll.y = Math.round(self.getScrollTop() * (1 / self.scrollratio.y)); + self.scroll.x = Math.round(self.getScrollLeft() * (1 / self.scrollratio.x)); + if (self.cursoractive) self.noticeCursor(); + } + + if (self.scroll.y && (self.getScrollTop() == 0)) self.doScrollTo(Math.floor(self.scroll.y * self.scrollratio.y)); + + return self; + }; + + this.resize = self.onResize; + + this.lazyResize = function(tm) { // event debounce + tm = (isNaN(tm)) ? 30 : tm; + self.debounced('resize', self.resize, tm); + return self; + }; + + // modified by MDN https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/wheel + function _modernWheelEvent(dom, name, fn, bubble) { + self._bind(dom, name, function(e) { + var e = (e) ? e : window.event; + var event = { + original: e, + target: e.target || e.srcElement, + type: "wheel", + deltaMode: e.type == "MozMousePixelScroll" ? 0 : 1, + deltaX: 0, + deltaZ: 0, + preventDefault: function() { + e.preventDefault ? e.preventDefault() : e.returnValue = false; + return false; + }, + stopImmediatePropagation: function() { + (e.stopImmediatePropagation) ? e.stopImmediatePropagation(): e.cancelBubble = true; + } + }; + + if (name == "mousewheel") { + event.deltaY = -1 / 40 * e.wheelDelta; + e.wheelDeltaX && (event.deltaX = -1 / 40 * e.wheelDeltaX); + } else { + event.deltaY = e.detail; + } + + return fn.call(dom, event); + }, bubble); + }; + + + + this.jqbind = function(dom, name, fn) { // use jquery bind for non-native events (mouseenter/mouseleave) + self.events.push({ + e: dom, + n: name, + f: fn, + q: true + }); + $(dom).bind(name, fn); + }; + + this.bind = function(dom, name, fn, bubble) { // touch-oriented & fixing jquery bind + var el = ("jquery" in dom) ? dom[0] : dom; + + if (name == 'mousewheel') { + if (window.addEventListener||'onwheel' in document) { // modern brosers & IE9 detection fix + self._bind(el, "wheel", fn, bubble || false); + } else { + var wname = (typeof document.onmousewheel != "undefined") ? "mousewheel" : "DOMMouseScroll"; // older IE/Firefox + _modernWheelEvent(el, wname, fn, bubble || false); + if (wname == "DOMMouseScroll") _modernWheelEvent(el, "MozMousePixelScroll", fn, bubble || false); // Firefox legacy + } + } else if (el.addEventListener) { + if (cap.cantouch && /mouseup|mousedown|mousemove/.test(name)) { // touch device support + var tt = (name == 'mousedown') ? 'touchstart' : (name == 'mouseup') ? 'touchend' : 'touchmove'; + self._bind(el, tt, function(e) { + if (e.touches) { + if (e.touches.length < 2) { + var ev = (e.touches.length) ? e.touches[0] : e; + ev.original = e; + fn.call(this, ev); + } + } else if (e.changedTouches) { + var ev = e.changedTouches[0]; + ev.original = e; + fn.call(this, ev); + } //blackberry + }, bubble || false); + } + self._bind(el, name, fn, bubble || false); + if (cap.cantouch && name == "mouseup") self._bind(el, "touchcancel", fn, bubble || false); + } else { + self._bind(el, name, function(e) { + e = e || window.event || false; + if (e) { + if (e.srcElement) e.target = e.srcElement; + } + if (!("pageY" in e)) { + e.pageX = e.clientX + document.documentElement.scrollLeft; + e.pageY = e.clientY + document.documentElement.scrollTop; + } + return ((fn.call(el, e) === false) || bubble === false) ? self.cancelEvent(e) : true; + }); + } + }; + + if (cap.haseventlistener) { // W3C standard model + this._bind = function(el, name, fn, bubble) { // primitive bind + self.events.push({ + e: el, + n: name, + f: fn, + b: bubble, + q: false + }); + el.addEventListener(name, fn, bubble || false); + }; + this.cancelEvent = function(e) { + if (!e) return false; + var e = (e.original) ? e.original : e; + e.preventDefault(); + e.stopPropagation(); + if (e.preventManipulation) e.preventManipulation(); //IE10 + return false; + }; + this.stopPropagation = function(e) { + if (!e) return false; + var e = (e.original) ? e.original : e; + e.stopPropagation(); + return false; + }; + this._unbind = function(el, name, fn, bub) { // primitive unbind + el.removeEventListener(name, fn, bub); + }; + } else { // old IE model + this._bind = function(el, name, fn, bubble) { // primitive bind + self.events.push({ + e: el, + n: name, + f: fn, + b: bubble, + q: false + }); + if (el.attachEvent) { + el.attachEvent("on" + name, fn); + } else { + el["on" + name] = fn; + } + }; + // Thanks to http://www.switchonthecode.com !! + this.cancelEvent = function(e) { + var e = window.event || false; + if (!e) return false; + e.cancelBubble = true; + e.cancel = true; + e.returnValue = false; + return false; + }; + this.stopPropagation = function(e) { + var e = window.event || false; + if (!e) return false; + e.cancelBubble = true; + return false; + }; + this._unbind = function(el, name, fn, bub) { // primitive unbind IE old + if (el.detachEvent) { + el.detachEvent('on' + name, fn); + } else { + el['on' + name] = false; + } + }; + } + + this.unbindAll = function() { + for (var a = 0; a < self.events.length; a++) { + var r = self.events[a]; + (r.q) ? r.e.unbind(r.n, r.f): self._unbind(r.e, r.n, r.f, r.b); + } + }; + + this.showRail = function() { + if ((self.page.maxh != 0) && (self.ispage || self.win.css('display') != 'none')) { + self.visibility = true; + self.rail.visibility = true; + self.rail.css('display', 'block'); + } + return self; + }; + + this.showRailHr = function() { + if (!self.railh) return self; + if ((self.page.maxw != 0) && (self.ispage || self.win.css('display') != 'none')) { + self.railh.visibility = true; + self.railh.css('display', 'block'); + } + return self; + }; + + this.hideRail = function() { + self.visibility = false; + self.rail.visibility = false; + self.rail.css('display', 'none'); + return self; + }; + + this.hideRailHr = function() { + if (!self.railh) return self; + self.railh.visibility = false; + self.railh.css('display', 'none'); + return self; + }; + + this.show = function() { + self.hidden = false; + self.railslocked = false; + return self.showRail().showRailHr(); + }; + + this.hide = function() { + self.hidden = true; + self.railslocked = true; + return self.hideRail().hideRailHr(); + }; + + this.toggle = function() { + return (self.hidden) ? self.show() : self.hide(); + }; + + this.remove = function() { + self.stop(); + if (self.cursortimeout) clearTimeout(self.cursortimeout); + self.doZoomOut(); + self.unbindAll(); + + if (cap.isie9) self.win[0].detachEvent("onpropertychange", self.onAttributeChange); //IE9 DOMAttrModified bug + + if (self.observer !== false) self.observer.disconnect(); + if (self.observerremover !== false) self.observerremover.disconnect(); + if (self.observerbody !== false) self.observerbody.disconnect(); + + self.events = null; + + if (self.cursor) { + self.cursor.remove(); + } + if (self.cursorh) { + self.cursorh.remove(); + } + if (self.rail) { + self.rail.remove(); + } + if (self.railh) { + self.railh.remove(); + } + if (self.zoom) { + self.zoom.remove(); + } + for (var a = 0; a < self.saved.css.length; a++) { + var d = self.saved.css[a]; + d[0].css(d[1], (typeof d[2] == "undefined") ? '' : d[2]); + } + self.saved = false; + self.me.data('__nicescroll', ''); //erase all traces + + // memory leak fixed by GianlucaGuarini - thanks a lot! + // remove the current nicescroll from the $.nicescroll array & normalize array + var lst = $.nicescroll; + lst.each(function(i) { + if (!this) return; + if (this.id === self.id) { + delete lst[i]; + for (var b = ++i; b < lst.length; b++, i++) lst[i] = lst[b]; + lst.length--; + if (lst.length) delete lst[lst.length]; + } + }); + + for (var i in self) { + self[i] = null; + delete self[i]; + } + + self = null; + + }; + + this.scrollstart = function(fn) { + this.onscrollstart = fn; + return self; + }; + this.scrollend = function(fn) { + this.onscrollend = fn; + return self; + }; + this.scrollcancel = function(fn) { + this.onscrollcancel = fn; + return self; + }; + + this.zoomin = function(fn) { + this.onzoomin = fn; + return self; + }; + this.zoomout = function(fn) { + this.onzoomout = fn; + return self; + }; + + this.isScrollable = function(e) { + var dom = (e.target) ? e.target : e; + if (dom.nodeName == 'OPTION') return true; + while (dom && (dom.nodeType == 1) && !(/^BODY|HTML/.test(dom.nodeName))) { + var dd = $(dom); + var ov = dd.css('overflowY') || dd.css('overflowX') || dd.css('overflow') || ''; + if (/scroll|auto/.test(ov)) return (dom.clientHeight != dom.scrollHeight); + dom = (dom.parentNode) ? dom.parentNode : false; + } + return false; + }; + + this.getViewport = function(me) { + var dom = (me && me.parentNode) ? me.parentNode : false; + while (dom && (dom.nodeType == 1) && !(/^BODY|HTML/.test(dom.nodeName))) { + var dd = $(dom); + if (/fixed|absolute/.test(dd.css("position"))) return dd; + var ov = dd.css('overflowY') || dd.css('overflowX') || dd.css('overflow') || ''; + if ((/scroll|auto/.test(ov)) && (dom.clientHeight != dom.scrollHeight)) return dd; + if (dd.getNiceScroll().length > 0) return dd; + dom = (dom.parentNode) ? dom.parentNode : false; + } + return false; //(dom) ? $(dom) : false; + }; + + this.triggerScrollEnd = function() { + if (!self.onscrollend) return; + + var px = self.getScrollLeft(); + var py = self.getScrollTop(); + + var info = { + "type": "scrollend", + "current": { + "x": px, + "y": py + }, + "end": { + "x": px, + "y": py + } + }; + self.onscrollend.call(self, info); + } + + function execScrollWheel(e, hr, chkscroll) { + var px, py; + + if (e.deltaMode == 0) { // PIXEL + px = -Math.floor(e.deltaX * (self.opt.mousescrollstep / (18 * 3))); + py = -Math.floor(e.deltaY * (self.opt.mousescrollstep / (18 * 3))); + } else if (e.deltaMode == 1) { // LINE + px = -Math.floor(e.deltaX * self.opt.mousescrollstep); + py = -Math.floor(e.deltaY * self.opt.mousescrollstep); + } + + if (hr && self.opt.oneaxismousemode && (px == 0) && py) { // classic vertical-only mousewheel + browser with x/y support + px = py; + py = 0; + + if (chkscroll) { + var hrend = (px < 0) ? (self.getScrollLeft() >= self.page.maxw) : (self.getScrollLeft() <= 0); + if (hrend) { // preserve vertical scrolling + py = px; + px = 0; + } + } + + } + + if (px) { + if (self.scrollmom) { + self.scrollmom.stop() + } + self.lastdeltax += px; + self.debounced("mousewheelx", function() { + var dt = self.lastdeltax; + self.lastdeltax = 0; + if (!self.rail.drag) { + self.doScrollLeftBy(dt) + } + }, 15); + } + if (py) { + if (self.opt.nativeparentscrolling && chkscroll && !self.ispage && !self.zoomactive) { + if (py < 0) { + if (self.getScrollTop() >= self.page.maxh) return true; + } else { + if (self.getScrollTop() <= 0) return true; + } + } + if (self.scrollmom) { + self.scrollmom.stop() + } + self.lastdeltay += py; + self.debounced("mousewheely", function() { + var dt = self.lastdeltay; + self.lastdeltay = 0; + if (!self.rail.drag) { + self.doScrollBy(dt) + } + }, 15); + } + + e.stopImmediatePropagation(); + return e.preventDefault(); + }; + + this.onmousewheel = function(e) { + if (self.wheelprevented) return; + if (self.railslocked) { + self.debounced("checkunlock", self.resize, 250); + return true; + } + if (self.rail.drag) return self.cancelEvent(e); + + if (self.opt.oneaxismousemode == "auto" && e.deltaX != 0) self.opt.oneaxismousemode = false; // check two-axis mouse support (not very elegant) + + if (self.opt.oneaxismousemode && e.deltaX == 0) { + if (!self.rail.scrollable) { + if (self.railh && self.railh.scrollable) { + return self.onmousewheelhr(e); + } else { + return true; + } + } + } + + var nw = +(new Date()); + var chk = false; + if (self.opt.preservenativescrolling && ((self.checkarea + 600) < nw)) { + self.nativescrollingarea = self.isScrollable(e); + chk = true; + } + self.checkarea = nw; + if (self.nativescrollingarea) return true; // this isn't my business + var ret = execScrollWheel(e, false, chk); + if (ret) self.checkarea = 0; + return ret; + }; + + this.onmousewheelhr = function(e) { + if (self.wheelprevented) return; + if (self.railslocked || !self.railh.scrollable) return true; + if (self.rail.drag) return self.cancelEvent(e); + + var nw = +(new Date()); + var chk = false; + if (self.opt.preservenativescrolling && ((self.checkarea + 600) < nw)) { + self.nativescrollingarea = self.isScrollable(e); + chk = true; + } + self.checkarea = nw; + if (self.nativescrollingarea) return true; // this isn't my business + if (self.railslocked) return self.cancelEvent(e); + + return execScrollWheel(e, true, chk); + }; + + this.stop = function() { + self.cancelScroll(); + if (self.scrollmon) self.scrollmon.stop(); + self.cursorfreezed = false; + self.scroll.y = Math.round(self.getScrollTop() * (1 / self.scrollratio.y)); + self.noticeCursor(); + return self; + }; + + this.getTransitionSpeed = function(dif) { + var sp = Math.round(self.opt.scrollspeed * 10); + var ex = Math.min(sp, Math.round((dif / 20) * self.opt.scrollspeed)); + return (ex > 20) ? ex : 0; + }; + + if (!self.opt.smoothscroll) { + this.doScrollLeft = function(x, spd) { //direct + var y = self.getScrollTop(); + self.doScrollPos(x, y, spd); + }; + this.doScrollTop = function(y, spd) { //direct + var x = self.getScrollLeft(); + self.doScrollPos(x, y, spd); + }; + this.doScrollPos = function(x, y, spd) { //direct + var nx = (x > self.page.maxw) ? self.page.maxw : x; + if (nx < 0) nx = 0; + var ny = (y > self.page.maxh) ? self.page.maxh : y; + if (ny < 0) ny = 0; + self.synched('scroll', function() { + self.setScrollTop(ny); + self.setScrollLeft(nx); + }); + }; + this.cancelScroll = function() {}; // direct + } else if (self.ishwscroll && cap.hastransition && self.opt.usetransition && !!self.opt.smoothscroll) { + this.prepareTransition = function(dif, istime) { + var ex = (istime) ? ((dif > 20) ? dif : 0) : self.getTransitionSpeed(dif); + var trans = (ex) ? cap.prefixstyle + 'transform ' + ex + 'ms ease-out' : ''; + if (!self.lasttransitionstyle || self.lasttransitionstyle != trans) { + self.lasttransitionstyle = trans; + self.doc.css(cap.transitionstyle, trans); + } + return ex; + }; + + this.doScrollLeft = function(x, spd) { //trans + var y = (self.scrollrunning) ? self.newscrolly : self.getScrollTop(); + self.doScrollPos(x, y, spd); + }; + + this.doScrollTop = function(y, spd) { //trans + var x = (self.scrollrunning) ? self.newscrollx : self.getScrollLeft(); + self.doScrollPos(x, y, spd); + }; + + this.doScrollPos = function(x, y, spd) { //trans + + var py = self.getScrollTop(); + var px = self.getScrollLeft(); + + if (((self.newscrolly - py) * (y - py) < 0) || ((self.newscrollx - px) * (x - px) < 0)) self.cancelScroll(); //inverted movement detection + + if (self.opt.bouncescroll == false) { + if (y < 0) y = 0; + else if (y > self.page.maxh) y = self.page.maxh; + if (x < 0) x = 0; + else if (x > self.page.maxw) x = self.page.maxw; + } + + if (self.scrollrunning && x == self.newscrollx && y == self.newscrolly) return false; + + self.newscrolly = y; + self.newscrollx = x; + + self.newscrollspeed = spd || false; + + if (self.timer) return false; + + self.timer = setTimeout(function() { + + var top = self.getScrollTop(); + var lft = self.getScrollLeft(); + + var dst = {}; + dst.x = x - lft; + dst.y = y - top; + dst.px = lft; + dst.py = top; + + var dd = Math.round(Math.sqrt(Math.pow(dst.x, 2) + Math.pow(dst.y, 2))); + var ms = (self.newscrollspeed && self.newscrollspeed > 1) ? self.newscrollspeed : self.getTransitionSpeed(dd); + if (self.newscrollspeed && self.newscrollspeed <= 1) ms *= self.newscrollspeed; + + self.prepareTransition(ms, true); + + if (self.timerscroll && self.timerscroll.tm) clearInterval(self.timerscroll.tm); + + if (ms > 0) { + + if (!self.scrollrunning && self.onscrollstart) { + var info = { + "type": "scrollstart", + "current": { + "x": lft, + "y": top + }, + "request": { + "x": x, + "y": y + }, + "end": { + "x": self.newscrollx, + "y": self.newscrolly + }, + "speed": ms + }; + self.onscrollstart.call(self, info); + } + + if (cap.transitionend) { + if (!self.scrollendtrapped) { + self.scrollendtrapped = true; + self.bind(self.doc, cap.transitionend, self.onScrollTransitionEnd, false); //I have got to do something usefull!! + } + } else { + if (self.scrollendtrapped) clearTimeout(self.scrollendtrapped); + self.scrollendtrapped = setTimeout(self.onScrollTransitionEnd, ms); // simulate transitionend event + } + + var py = top; + var px = lft; + self.timerscroll = { + bz: new BezierClass(py, self.newscrolly, ms, 0, 0, 0.58, 1), + bh: new BezierClass(px, self.newscrollx, ms, 0, 0, 0.58, 1) + }; + if (!self.cursorfreezed) self.timerscroll.tm = setInterval(function() { + self.showCursor(self.getScrollTop(), self.getScrollLeft()) + }, 60); + + } + + self.synched("doScroll-set", function() { + self.timer = 0; + if (self.scrollendtrapped) self.scrollrunning = true; + self.setScrollTop(self.newscrolly); + self.setScrollLeft(self.newscrollx); + if (!self.scrollendtrapped) self.onScrollTransitionEnd(); + }); + + + }, 50); + + }; + + this.cancelScroll = function() { + if (!self.scrollendtrapped) return true; + var py = self.getScrollTop(); + var px = self.getScrollLeft(); + self.scrollrunning = false; + if (!cap.transitionend) clearTimeout(cap.transitionend); + self.scrollendtrapped = false; + self._unbind(self.doc[0], cap.transitionend, self.onScrollTransitionEnd); + self.prepareTransition(0); + self.setScrollTop(py); // fire event onscroll + if (self.railh) self.setScrollLeft(px); + if (self.timerscroll && self.timerscroll.tm) clearInterval(self.timerscroll.tm); + self.timerscroll = false; + + self.cursorfreezed = false; + + self.showCursor(py, px); + return self; + }; + this.onScrollTransitionEnd = function() { + if (self.scrollendtrapped) self._unbind(self.doc[0], cap.transitionend, self.onScrollTransitionEnd); + self.scrollendtrapped = false; + self.prepareTransition(0); + if (self.timerscroll && self.timerscroll.tm) clearInterval(self.timerscroll.tm); + self.timerscroll = false; + var py = self.getScrollTop(); + var px = self.getScrollLeft(); + self.setScrollTop(py); // fire event onscroll + if (self.railh) self.setScrollLeft(px); // fire event onscroll left + + self.noticeCursor(false, py, px); + + self.cursorfreezed = false; + + if (py < 0) py = 0 + else if (py > self.page.maxh) py = self.page.maxh; + if (px < 0) px = 0 + else if (px > self.page.maxw) px = self.page.maxw; + if ((py != self.newscrolly) || (px != self.newscrollx)) return self.doScrollPos(px, py, self.opt.snapbackspeed); + + if (self.onscrollend && self.scrollrunning) { + self.triggerScrollEnd(); + } + self.scrollrunning = false; + + }; + + } else { + + this.doScrollLeft = function(x, spd) { //no-trans + var y = (self.scrollrunning) ? self.newscrolly : self.getScrollTop(); + self.doScrollPos(x, y, spd); + }; + + this.doScrollTop = function(y, spd) { //no-trans + var x = (self.scrollrunning) ? self.newscrollx : self.getScrollLeft(); + self.doScrollPos(x, y, spd); + }; + + this.doScrollPos = function(x, y, spd) { //no-trans + var y = ((typeof y == "undefined") || (y === false)) ? self.getScrollTop(true) : y; + + if ((self.timer) && (self.newscrolly == y) && (self.newscrollx == x)) return true; + + if (self.timer) clearAnimationFrame(self.timer); + self.timer = 0; + + var py = self.getScrollTop(); + var px = self.getScrollLeft(); + + if (((self.newscrolly - py) * (y - py) < 0) || ((self.newscrollx - px) * (x - px) < 0)) self.cancelScroll(); //inverted movement detection + + self.newscrolly = y; + self.newscrollx = x; + + if (!self.bouncescroll || !self.rail.visibility) { + if (self.newscrolly < 0) { + self.newscrolly = 0; + } else if (self.newscrolly > self.page.maxh) { + self.newscrolly = self.page.maxh; + } + } + if (!self.bouncescroll || !self.railh.visibility) { + if (self.newscrollx < 0) { + self.newscrollx = 0; + } else if (self.newscrollx > self.page.maxw) { + self.newscrollx = self.page.maxw; + } + } + + self.dst = {}; + self.dst.x = x - px; + self.dst.y = y - py; + self.dst.px = px; + self.dst.py = py; + + var dst = Math.round(Math.sqrt(Math.pow(self.dst.x, 2) + Math.pow(self.dst.y, 2))); + + self.dst.ax = self.dst.x / dst; + self.dst.ay = self.dst.y / dst; + + var pa = 0; + var pe = dst; + + if (self.dst.x == 0) { + pa = py; + pe = y; + self.dst.ay = 1; + self.dst.py = 0; + } else if (self.dst.y == 0) { + pa = px; + pe = x; + self.dst.ax = 1; + self.dst.px = 0; + } + + var ms = self.getTransitionSpeed(dst); + if (spd && spd <= 1) ms *= spd; + if (ms > 0) { + self.bzscroll = (self.bzscroll) ? self.bzscroll.update(pe, ms) : new BezierClass(pa, pe, ms, 0, 1, 0, 1); + } else { + self.bzscroll = false; + } + + if (self.timer) return; + + if ((py == self.page.maxh && y >= self.page.maxh) || (px == self.page.maxw && x >= self.page.maxw)) self.checkContentSize(); + + var sync = 1; + + function scrolling() { + if (self.cancelAnimationFrame) return true; + + self.scrollrunning = true; + + sync = 1 - sync; + if (sync) return (self.timer = setAnimationFrame(scrolling) || 1); + + var done = 0; + var sx, sy; + + var sc = sy = self.getScrollTop(); + if (self.dst.ay) { + sc = (self.bzscroll) ? self.dst.py + (self.bzscroll.getNow() * self.dst.ay) : self.newscrolly; + var dr = sc - sy; + if ((dr < 0 && sc < self.newscrolly) || (dr > 0 && sc > self.newscrolly)) sc = self.newscrolly; + self.setScrollTop(sc); + if (sc == self.newscrolly) done = 1; + } else { + done = 1; + } + + var scx = sx = self.getScrollLeft(); + if (self.dst.ax) { + scx = (self.bzscroll) ? self.dst.px + (self.bzscroll.getNow() * self.dst.ax) : self.newscrollx; + var dr = scx - sx; + if ((dr < 0 && scx < self.newscrollx) || (dr > 0 && scx > self.newscrollx)) scx = self.newscrollx; + self.setScrollLeft(scx); + if (scx == self.newscrollx) done += 1; + } else { + done += 1; + } + + if (done == 2) { + self.timer = 0; + self.cursorfreezed = false; + self.bzscroll = false; + self.scrollrunning = false; + if (sc < 0) sc = 0; + else if (sc > self.page.maxh) sc = self.page.maxh; + if (scx < 0) scx = 0; + else if (scx > self.page.maxw) scx = self.page.maxw; + if ((scx != self.newscrollx) || (sc != self.newscrolly)) self.doScrollPos(scx, sc); + else { + if (self.onscrollend) { + self.triggerScrollEnd(); + } + } + } else { + self.timer = setAnimationFrame(scrolling) || 1; + } + }; + self.cancelAnimationFrame = false; + self.timer = 1; + + if (self.onscrollstart && !self.scrollrunning) { + var info = { + "type": "scrollstart", + "current": { + "x": px, + "y": py + }, + "request": { + "x": x, + "y": y + }, + "end": { + "x": self.newscrollx, + "y": self.newscrolly + }, + "speed": ms + }; + self.onscrollstart.call(self, info); + } + + scrolling(); + + if ((py == self.page.maxh && y >= py) || (px == self.page.maxw && x >= px)) self.checkContentSize(); + + self.noticeCursor(); + }; + + this.cancelScroll = function() { + if (self.timer) clearAnimationFrame(self.timer); + self.timer = 0; + self.bzscroll = false; + self.scrollrunning = false; + return self; + }; + + } + + this.doScrollBy = function(stp, relative) { + var ny = 0; + if (relative) { + ny = Math.floor((self.scroll.y - stp) * self.scrollratio.y) + } else { + var sy = (self.timer) ? self.newscrolly : self.getScrollTop(true); + ny = sy - stp; + } + if (self.bouncescroll) { + var haf = Math.round(self.view.h / 2); + if (ny < -haf) ny = -haf + else if (ny > (self.page.maxh + haf)) ny = (self.page.maxh + haf); + } + self.cursorfreezed = false; + + var py = self.getScrollTop(true); + if (ny < 0 && py <= 0) return self.noticeCursor(); + else if (ny > self.page.maxh && py >= self.page.maxh) { + self.checkContentSize(); + return self.noticeCursor(); + } + + self.doScrollTop(ny); + }; + + this.doScrollLeftBy = function(stp, relative) { + var nx = 0; + if (relative) { + nx = Math.floor((self.scroll.x - stp) * self.scrollratio.x) + } else { + var sx = (self.timer) ? self.newscrollx : self.getScrollLeft(true); + nx = sx - stp; + } + if (self.bouncescroll) { + var haf = Math.round(self.view.w / 2); + if (nx < -haf) nx = -haf; + else if (nx > (self.page.maxw + haf)) nx = (self.page.maxw + haf); + } + self.cursorfreezed = false; + + var px = self.getScrollLeft(true); + if (nx < 0 && px <= 0) return self.noticeCursor(); + else if (nx > self.page.maxw && px >= self.page.maxw) return self.noticeCursor(); + + self.doScrollLeft(nx); + }; + + this.doScrollTo = function(pos, relative) { + var ny = (relative) ? Math.round(pos * self.scrollratio.y) : pos; + if (ny < 0) ny = 0; + else if (ny > self.page.maxh) ny = self.page.maxh; + self.cursorfreezed = false; + self.doScrollTop(pos); + }; + + this.checkContentSize = function() { + var pg = self.getContentSize(); + if ((pg.h != self.page.h) || (pg.w != self.page.w)) self.resize(false, pg); + }; + + self.onscroll = function(e) { + if (self.rail.drag) return; + if (!self.cursorfreezed) { + self.synched('scroll', function() { + self.scroll.y = Math.round(self.getScrollTop() * (1 / self.scrollratio.y)); + if (self.railh) self.scroll.x = Math.round(self.getScrollLeft() * (1 / self.scrollratio.x)); + self.noticeCursor(); + }); + } + }; + self.bind(self.docscroll, "scroll", self.onscroll); + + this.doZoomIn = function(e) { + if (self.zoomactive) return; + self.zoomactive = true; + + self.zoomrestore = { + style: {} + }; + var lst = ['position', 'top', 'left', 'zIndex', 'backgroundColor', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight']; + var win = self.win[0].style; + for (var a in lst) { + var pp = lst[a]; + self.zoomrestore.style[pp] = (typeof win[pp] != "undefined") ? win[pp] : ''; + } + + self.zoomrestore.style.width = self.win.css('width'); + self.zoomrestore.style.height = self.win.css('height'); + + self.zoomrestore.padding = { + w: self.win.outerWidth() - self.win.width(), + h: self.win.outerHeight() - self.win.height() + }; + + if (cap.isios4) { + self.zoomrestore.scrollTop = $(window).scrollTop(); + $(window).scrollTop(0); + } + + self.win.css({ + "position": (cap.isios4) ? "absolute" : "fixed", + "top": 0, + "left": 0, + "z-index": globalmaxzindex + 100, + "margin": "0px" + }); + var bkg = self.win.css("backgroundColor"); + if (bkg == "" || /transparent|rgba\(0, 0, 0, 0\)|rgba\(0,0,0,0\)/.test(bkg)) self.win.css("backgroundColor", "#fff"); + self.rail.css({ + "z-index": globalmaxzindex + 101 + }); + self.zoom.css({ + "z-index": globalmaxzindex + 102 + }); + self.zoom.css('backgroundPosition', '0px -18px'); + self.resizeZoom(); + + if (self.onzoomin) self.onzoomin.call(self); + + return self.cancelEvent(e); + }; + + this.doZoomOut = function(e) { + if (!self.zoomactive) return; + self.zoomactive = false; + + self.win.css("margin", ""); + self.win.css(self.zoomrestore.style); + + if (cap.isios4) { + $(window).scrollTop(self.zoomrestore.scrollTop); + } + + self.rail.css({ + "z-index": self.zindex + }); + self.zoom.css({ + "z-index": self.zindex + }); + self.zoomrestore = false; + self.zoom.css('backgroundPosition', '0px 0px'); + self.onResize(); + + if (self.onzoomout) self.onzoomout.call(self); + + return self.cancelEvent(e); + }; + + this.doZoom = function(e) { + return (self.zoomactive) ? self.doZoomOut(e) : self.doZoomIn(e); + }; + + this.resizeZoom = function() { + if (!self.zoomactive) return; + + var py = self.getScrollTop(); //preserve scrolling position + self.win.css({ + width: $(window).width() - self.zoomrestore.padding.w + "px", + height: $(window).height() - self.zoomrestore.padding.h + "px" + }); + self.onResize(); + + self.setScrollTop(Math.min(self.page.maxh, py)); + }; + + this.init(); + + $.nicescroll.push(this); + + }; + + // Inspired by the work of Kin Blas + // http://webpro.host.adobe.com/people/jblas/momentum/includes/jquery.momentum.0.7.js + + + var ScrollMomentumClass2D = function(nc) { + var self = this; + this.nc = nc; + + this.lastx = 0; + this.lasty = 0; + this.speedx = 0; + this.speedy = 0; + this.lasttime = 0; + this.steptime = 0; + this.snapx = false; + this.snapy = false; + this.demulx = 0; + this.demuly = 0; + + this.lastscrollx = -1; + this.lastscrolly = -1; + + this.chkx = 0; + this.chky = 0; + + this.timer = 0; + + this.time = function() { + return +new Date(); //beautifull hack + }; + + this.reset = function(px, py) { + self.stop(); + var now = self.time(); + self.steptime = 0; + self.lasttime = now; + self.speedx = 0; + self.speedy = 0; + self.lastx = px; + self.lasty = py; + self.lastscrollx = -1; + self.lastscrolly = -1; + }; + + this.update = function(px, py) { + var now = self.time(); + self.steptime = now - self.lasttime; + self.lasttime = now; + var dy = py - self.lasty; + var dx = px - self.lastx; + var sy = self.nc.getScrollTop(); + var sx = self.nc.getScrollLeft(); + var newy = sy + dy; + var newx = sx + dx; + self.snapx = (newx < 0) || (newx > self.nc.page.maxw); + self.snapy = (newy < 0) || (newy > self.nc.page.maxh); + self.speedx = dx; + self.speedy = dy; + self.lastx = px; + self.lasty = py; + }; + + this.stop = function() { + self.nc.unsynched("domomentum2d"); + if (self.timer) clearTimeout(self.timer); + self.timer = 0; + self.lastscrollx = -1; + self.lastscrolly = -1; + }; + + this.doSnapy = function(nx, ny) { + var snap = false; + + if (ny < 0) { + ny = 0; + snap = true; + } else if (ny > self.nc.page.maxh) { + ny = self.nc.page.maxh; + snap = true; + } + + if (nx < 0) { + nx = 0; + snap = true; + } else if (nx > self.nc.page.maxw) { + nx = self.nc.page.maxw; + snap = true; + } + + (snap) ? self.nc.doScrollPos(nx, ny, self.nc.opt.snapbackspeed): self.nc.triggerScrollEnd(); + }; + + this.doMomentum = function(gp) { + var t = self.time(); + var l = (gp) ? t + gp : self.lasttime; + + var sl = self.nc.getScrollLeft(); + var st = self.nc.getScrollTop(); + + var pageh = self.nc.page.maxh; + var pagew = self.nc.page.maxw; + + self.speedx = (pagew > 0) ? Math.min(60, self.speedx) : 0; + self.speedy = (pageh > 0) ? Math.min(60, self.speedy) : 0; + + var chk = l && (t - l) <= 60; + + if ((st < 0) || (st > pageh) || (sl < 0) || (sl > pagew)) chk = false; + + var sy = (self.speedy && chk) ? self.speedy : false; + var sx = (self.speedx && chk) ? self.speedx : false; + + if (sy || sx) { + var tm = Math.max(16, self.steptime); //timeout granularity + + if (tm > 50) { // do smooth + var xm = tm / 50; + self.speedx *= xm; + self.speedy *= xm; + tm = 50; + } + + self.demulxy = 0; + + self.lastscrollx = self.nc.getScrollLeft(); + self.chkx = self.lastscrollx; + self.lastscrolly = self.nc.getScrollTop(); + self.chky = self.lastscrolly; + + var nx = self.lastscrollx; + var ny = self.lastscrolly; + + var onscroll = function() { + var df = ((self.time() - t) > 600) ? 0.04 : 0.02; + + if (self.speedx) { + nx = Math.floor(self.lastscrollx - (self.speedx * (1 - self.demulxy))); + self.lastscrollx = nx; + if ((nx < 0) || (nx > pagew)) df = 0.10; + } + + if (self.speedy) { + ny = Math.floor(self.lastscrolly - (self.speedy * (1 - self.demulxy))); + self.lastscrolly = ny; + if ((ny < 0) || (ny > pageh)) df = 0.10; + } + + self.demulxy = Math.min(1, self.demulxy + df); + + self.nc.synched("domomentum2d", function() { + + if (self.speedx) { + var scx = self.nc.getScrollLeft(); + if (scx != self.chkx) self.stop(); + self.chkx = nx; + self.nc.setScrollLeft(nx); + } + + if (self.speedy) { + var scy = self.nc.getScrollTop(); + if (scy != self.chky) self.stop(); + self.chky = ny; + self.nc.setScrollTop(ny); + } + + if (!self.timer) { + self.nc.hideCursor(); + self.doSnapy(nx, ny); + } + + }); + + if (self.demulxy < 1) { + self.timer = setTimeout(onscroll, tm); + } else { + self.stop(); + self.nc.hideCursor(); + self.doSnapy(nx, ny); + } + }; + + onscroll(); + + } else { + self.doSnapy(self.nc.getScrollLeft(), self.nc.getScrollTop()); + } + + } + + }; + + + // override jQuery scrollTop + + var _scrollTop = jQuery.fn.scrollTop; // preserve original function + + jQuery.cssHooks["pageYOffset"] = { + get: function(elem, computed, extra) { + var nice = $.data(elem, '__nicescroll') || false; + return (nice && nice.ishwscroll) ? nice.getScrollTop() : _scrollTop.call(elem); + }, + set: function(elem, value) { + var nice = $.data(elem, '__nicescroll') || false; + (nice && nice.ishwscroll) ? nice.setScrollTop(parseInt(value)): _scrollTop.call(elem, value); + return this; + } + }; + + /* + $.fx.step["scrollTop"] = function(fx){ + $.cssHooks["scrollTop"].set( fx.elem, fx.now + fx.unit ); + }; +*/ + + jQuery.fn.scrollTop = function(value) { + if (typeof value == "undefined") { + var nice = (this[0]) ? $.data(this[0], '__nicescroll') || false : false; + return (nice && nice.ishwscroll) ? nice.getScrollTop() : _scrollTop.call(this); + } else { + return this.each(function() { + var nice = $.data(this, '__nicescroll') || false; + (nice && nice.ishwscroll) ? nice.setScrollTop(parseInt(value)): _scrollTop.call($(this), value); + }); + } + }; + + // override jQuery scrollLeft + + var _scrollLeft = jQuery.fn.scrollLeft; // preserve original function + + $.cssHooks.pageXOffset = { + get: function(elem, computed, extra) { + var nice = $.data(elem, '__nicescroll') || false; + return (nice && nice.ishwscroll) ? nice.getScrollLeft() : _scrollLeft.call(elem); + }, + set: function(elem, value) { + var nice = $.data(elem, '__nicescroll') || false; + (nice && nice.ishwscroll) ? nice.setScrollLeft(parseInt(value)): _scrollLeft.call(elem, value); + return this; + } + }; + + /* + $.fx.step["scrollLeft"] = function(fx){ + $.cssHooks["scrollLeft"].set( fx.elem, fx.now + fx.unit ); + }; +*/ + + jQuery.fn.scrollLeft = function(value) { + if (typeof value == "undefined") { + var nice = (this[0]) ? $.data(this[0], '__nicescroll') || false : false; + return (nice && nice.ishwscroll) ? nice.getScrollLeft() : _scrollLeft.call(this); + } else { + return this.each(function() { + var nice = $.data(this, '__nicescroll') || false; + (nice && nice.ishwscroll) ? nice.setScrollLeft(parseInt(value)): _scrollLeft.call($(this), value); + }); + } + }; + + var NiceScrollArray = function(doms) { + var self = this; + this.length = 0; + this.name = "nicescrollarray"; + + this.each = function(fn) { + for (var a = 0, i = 0; a < self.length; a++) fn.call(self[a], i++); + return self; + }; + + this.push = function(nice) { + self[self.length] = nice; + self.length++; + }; + + this.eq = function(idx) { + return self[idx]; + }; + + if (doms) { + for (var a = 0; a < doms.length; a++) { + var nice = $.data(doms[a], '__nicescroll') || false; + if (nice) { + this[this.length] = nice; + this.length++; + } + }; + } + + return this; + }; + + function mplex(el, lst, fn) { + for (var a = 0; a < lst.length; a++) fn(el, lst[a]); + }; + mplex( + NiceScrollArray.prototype, ['show', 'hide', 'toggle', 'onResize', 'resize', 'remove', 'stop', 'doScrollPos'], + function(e, n) { + e[n] = function() { + var args = arguments; + return this.each(function() { + this[n].apply(this, args); + }); + }; + } + ); + + jQuery.fn.getNiceScroll = function(index) { + if (typeof index == "undefined") { + return new NiceScrollArray(this); + } else { + var nice = this[index] && $.data(this[index], '__nicescroll') || false; + return nice; + } + }; + + jQuery.extend(jQuery.expr[':'], { + nicescroll: function(a) { + return ($.data(a, '__nicescroll')) ? true : false; + } + }); + + $.fn.niceScroll = function(wrapper, opt) { + if (typeof opt == "undefined") { + if ((typeof wrapper == "object") && !("jquery" in wrapper)) { + opt = wrapper; + wrapper = false; + } + } + opt = $.extend({},opt); // cloning + var ret = new NiceScrollArray(); + if (typeof opt == "undefined") opt = {}; + + if (wrapper || false) { + opt.doc = $(wrapper); + opt.win = $(this); + } + var docundef = !("doc" in opt); + if (!docundef && !("win" in opt)) opt.win = $(this); + + this.each(function() { + var nice = $(this).data('__nicescroll') || false; + if (!nice) { + opt.doc = (docundef) ? $(this) : opt.doc; + nice = new NiceScrollClass(opt, $(this)); + $(this).data('__nicescroll', nice); + } + ret.push(nice); + }); + return (ret.length == 1) ? ret[0] : ret; + }; + + window.NiceScroll = { + getjQuery: function() { + return jQuery + } + }; + + if (!$.nicescroll) { + $.nicescroll = new NiceScrollArray(); + $.nicescroll.options = _globaloptions; + } + +})); \ No newline at end of file