diff options
677 files changed, 17235 insertions, 2051 deletions
diff --git a/.eslintignore b/.eslintignore index 93de4b10dfe..b4bfa5a1f7a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,6 @@ /coverage/ /coverage-javascript/ +/node_modules/ /public/ /tmp/ /vendor/ diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 524cb55242b..26aaba0e866 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -1.1.1 +1.2.0 @@ -132,7 +132,7 @@ gem 'after_commit_queue', '~> 1.3.0' gem 'acts-as-taggable-on', '~> 4.0' # Background jobs -gem 'sidekiq', '~> 4.2' +gem 'sidekiq', '~> 4.2.7' gem 'sidekiq-cron', '~> 0.4.4' gem 'redis-namespace', '~> 1.5.2' gem 'sidekiq-limit_fetch', '~> 3.4' @@ -178,6 +178,9 @@ gem 'asana', '~> 0.4.0' # FogBugz integration gem 'ruby-fogbugz', '~> 0.2.1' +# Kubernetes integration +gem 'kubeclient', '~> 2.2.0' + # d3 gem 'd3_rails', '~> 3.5.0' diff --git a/Gemfile.lock b/Gemfile.lock index c464ff70587..7269b528e30 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -126,7 +126,7 @@ GEM coffee-script-source (1.10.0) colorize (0.7.7) concurrent-ruby (1.0.2) - connection_pool (2.2.0) + connection_pool (2.2.1) crack (0.4.3) safe_yaml (~> 1.0.0) creole (0.5.0) @@ -161,6 +161,8 @@ GEM diff-lcs (1.2.5) diffy (3.1.0) docile (1.1.5) + domain_name (0.5.20161021) + unf (>= 0.0.5, < 1.0.0) doorkeeper (4.2.0) railties (>= 4.2) dropzonejs-rails (0.7.2) @@ -318,6 +320,15 @@ GEM html2text (0.2.0) nokogiri (~> 1.6) htmlentities (4.3.4) + http (0.9.8) + addressable (~> 2.3) + http-cookie (~> 1.0) + http-form_data (~> 1.0.1) + http_parser.rb (~> 0.6.0) + http-cookie (1.0.3) + domain_name (~> 0.5) + http-form_data (1.0.1) + http_parser.rb (0.6.0) httparty (0.13.7) json (~> 1.8) multi_xml (>= 0.5.2) @@ -352,6 +363,10 @@ GEM knapsack (1.11.0) rake timecop (>= 0.1.0) + kubeclient (2.2.0) + http (= 0.9.8) + recursive-open-struct (= 1.0.0) + rest-client launchy (2.4.3) addressable (~> 2.3) letter_opener (1.4.1) @@ -388,6 +403,7 @@ GEM mysql2 (0.3.20) net-ldap (0.12.1) net-ssh (3.0.1) + netrc (0.11.0) newrelic_rpm (3.16.0.318) nokogiri (1.6.8) mini_portile2 (~> 2.1.0) @@ -543,6 +559,7 @@ GEM json (~> 1.4) recaptcha (3.0.0) json + recursive-open-struct (1.0.0) redcarpet (3.3.3) redis (3.2.2) redis-actionpack (5.0.1) @@ -568,6 +585,10 @@ GEM listen (~> 3.0) responders (2.3.0) railties (>= 4.2.0, < 5.1) + rest-client (2.0.0) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 4.0) + netrc (~> 0.8) rinku (2.0.0) rotp (2.1.2) rouge (2.0.7) @@ -648,10 +669,10 @@ GEM rack shoulda-matchers (2.8.0) activesupport (>= 3.0.0) - sidekiq (4.2.1) + sidekiq (4.2.7) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) - rack-protection (~> 1.5) + rack-protection (>= 1.5.0) redis (~> 3.2, >= 3.2.1) sidekiq-cron (0.4.4) redis-namespace (>= 1.5.2) @@ -859,6 +880,7 @@ DEPENDENCIES jwt kaminari (~> 0.17.0) knapsack (~> 1.11.0) + kubeclient (~> 2.2.0) letter_opener_web (~> 1.3.0) license_finder (~> 2.1.0) licensee (~> 8.0.0) @@ -928,7 +950,7 @@ DEPENDENCIES settingslogic (~> 2.0.9) sham_rack (~> 1.3.6) shoulda-matchers (~> 2.8.0) - sidekiq (~> 4.2) + sidekiq (~> 4.2.7) sidekiq-cron (~> 0.4.4) sidekiq-limit_fetch (~> 3.4) simplecov (= 0.12.0) diff --git a/app/assets/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js.es6 index 82e526ae0ef..8a260aae1b1 100644 --- a/app/assets/javascripts/abuse_reports.js.es6 +++ b/app/assets/javascripts/abuse_reports.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-param-reassign */ + ((global) => { const MAX_MESSAGE_LENGTH = 500; const MESSAGE_CELL_SELECTOR = '.abuse-reports .message'; diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index 31852e4750c..5a7d823e84c 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, padded-blocks, max-len */ +/* global Turbolinks */ + (function() { this.Admin = (function() { function Admin() { diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 1c625e2f2b1..81c1e01901e 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -1,6 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, no-undef, comma-dangle, prefer-arrow-callback, indent, object-curly-spacing, quote-props, no-param-reassign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, comma-dangle, prefer-arrow-callback, indent, object-curly-spacing, quote-props, no-param-reassign, padded-blocks, max-len */ + (function() { - this.Api = { + var Api = { groupsPath: "/api/:version/groups.json", groupPath: "/api/:version/groups/:id.json", namespacesPath: "/api/:version/namespaces.json", @@ -140,4 +141,5 @@ } }; + window.Api = Api; }).call(this); diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index b7c4673c8e3..043c6a11c4f 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -1,4 +1,11 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, no-undef, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len */ +/* global bp */ +/* global Cookies */ +/* global Flash */ +/* global ConfirmDangerModal */ +/* global AwardsHandler */ +/* global Aside */ + // This is a manifest file that'll be compiled into including all the files listed below. // Add new JavaScript code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index f4302e2e9f6..107a7978a87 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, spaced-comment, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, radix, keyword-spacing, space-before-blocks, brace-style, no-underscore-dangle, no-undef, no-plusplus, no-return-assign, camelcase, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, spaced-comment, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, radix, keyword-spacing, space-before-blocks, brace-style, no-underscore-dangle, no-plusplus, no-return-assign, camelcase, padded-blocks */ +/* global Cookies */ + (function() { this.AwardsHandler = (function() { var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js index a5d62f881fe..c62a4c5a456 100644 --- a/app/assets/javascripts/behaviors/autosize.js +++ b/app/assets/javascripts/behaviors/autosize.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, padded-blocks, max-len */ +/* global autosize */ /*= require jquery.ba-resize */ /*= require autosize */ diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index 4edcaa15fe5..586f941a6e3 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-undef, prefer-arrow-callback, camelcase, max-len, consistent-return, quotes, object-shorthand, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, camelcase, consistent-return, quotes, object-shorthand, comma-dangle, padded-blocks, max-len */ + // Quick Submit behavior // // When a child field of a form with a `js-quick-submit` class receives a diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js.es6 index 37531aaec9b..57bd13eecf8 100644 --- a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 +++ b/app/assets/javascripts/blob/blob_ci_yaml.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */ +/* global Api */ + /*= require blob/template_selector */ ((global) => { diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index e0a2e8ac12e..eab686c45c3 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, camelcase, no-undef, object-shorthand, quotes, comma-dangle, prefer-arrow-callback, no-unused-vars, prefer-template, no-useless-escape, no-alert, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, camelcase, object-shorthand, quotes, comma-dangle, prefer-arrow-callback, no-unused-vars, prefer-template, no-useless-escape, no-alert, padded-blocks, max-len */ +/* global Dropzone */ + (function() { this.BlobFileDropzone = (function() { function BlobFileDropzone(form, method) { diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js index 7e8f1062ab3..15563e429a0 100644 --- a/app/assets/javascripts/blob/blob_gitignore_selector.js +++ b/app/assets/javascripts/blob/blob_gitignore_selector.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, padded-blocks */ +/* global Api */ /*= require blob/template_selector */ diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js index 9a694daa010..d7f95093688 100644 --- a/app/assets/javascripts/blob/blob_gitignore_selectors.js +++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, no-undef, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, comma-dangle, padded-blocks, max-len */ +/* global BlobGitignoreSelector */ + (function() { this.BlobGitignoreSelectors = (function() { function BlobGitignoreSelectors(opts) { diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js index 9a77fe35d55..d9c6f65a083 100644 --- a/app/assets/javascripts/blob/blob_license_selector.js +++ b/app/assets/javascripts/blob/blob_license_selector.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle, padded-blocks */ +/* global Api */ /*= require blob/template_selector */ diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.es6 b/app/assets/javascripts/blob/blob_license_selectors.js.es6 index adeb8ba1318..268640681d4 100644 --- a/app/assets/javascripts/blob/blob_license_selectors.js.es6 +++ b/app/assets/javascripts/blob/blob_license_selectors.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars, no-param-reassign, padded-blocks */ +/* global BlobLicenseSelector */ + ((global) => { class BlobLicenseSelectors { constructor({ $dropdowns, editor }) { diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6 index 0ff5c0fab05..7a1ee9998c8 100644 --- a/app/assets/javascripts/blob/template_selector.js.es6 +++ b/app/assets/javascripts/blob/template_selector.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable indent, comma-dangle, object-shorthand, func-names, space-before-function-paren, arrow-parens, no-unused-vars, class-methods-use-this, no-var, consistent-return, prefer-const, no-param-reassign, space-in-parens, max-len */ + ((global) => { class TemplateSelector { constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) { diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js index b8eb0f60a8e..8c40e36a80a 100644 --- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js @@ -1,4 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-undef, no-new, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, padded-blocks, max-len */ +/* global EditBlob */ +/* global NewCommitForm */ + /*= require_tree . */ (function() { diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index 0c74aaaa852..b528e32340d 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -1,4 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, no-param-reassign, no-undef, quotes, prefer-template, no-new, comma-dangle, one-var, one-var-declaration-per-line, prefer-arrow-callback, no-else-return, no-unused-vars, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, no-param-reassign, quotes, prefer-template, no-new, comma-dangle, one-var, one-var-declaration-per-line, prefer-arrow-callback, no-else-return, no-unused-vars, padded-blocks, max-len */ +/* global ace */ +/* global BlobGitignoreSelectors */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; @@ -57,7 +60,7 @@ content: this.editor.getValue() }, function(response) { currentPane.empty().append(response); - return currentPane.syntaxHighlight(); + return currentPane.renderGFM(); }); } else { this.$toggleButton.show(); diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index 7ba918a05f8..ab2343c72fc 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable one-var, indent, quote-props, comma-dangle, space-before-function-paren */ +/* global Vue */ +/* global BoardService */ + //= require vue //= require vue-resource //= require Sortable diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6 index 31de3b25284..d1fb0ec48e0 100644 --- a/app/assets/javascripts/boards/components/board.js.es6 +++ b/app/assets/javascripts/boards/components/board.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, one-var, indent, radix */ +/* global Vue */ +/* global Sortable */ + //= require ./board_blank_state //= require ./board_delete //= require ./board_list diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js.es6 index 691487b272a..0a47a22fad2 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.js.es6 +++ b/app/assets/javascripts/boards/components/board_blank_state.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable space-before-function-paren, comma-dangle, semi */ +/* global Vue */ +/* global ListLabel */ + (() => { const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6 index 2299dafd217..5fc50280811 100644 --- a/app/assets/javascripts/boards/components/board_card.js.es6 +++ b/app/assets/javascripts/boards/components/board_card.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, dot-notation */ +/* global Vue */ + (() => { const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/boards/components/board_delete.js.es6 b/app/assets/javascripts/boards/components/board_delete.js.es6 index c45e1926c5c..861600424a5 100644 --- a/app/assets/javascripts/boards/components/board_delete.js.es6 +++ b/app/assets/javascripts/boards/components/board_delete.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, no-alert */ +/* global Vue */ + (() => { window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index 43ebeef39c4..6711930622b 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, max-len, no-plusplus */ +/* global Vue */ +/* global Sortable */ + //= require ./board_card //= require ./board_new_issue diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6 index a7989a2ff4c..2386d3a613c 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.js.es6 +++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, no-unused-vars */ +/* global Vue */ +/* global ListIssue */ + (() => { const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6 index 1644a772737..02459722bbf 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js.es6 +++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6 @@ -1,4 +1,10 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, no-new */ +/* global Vue */ +/* global IssuableContext */ +/* global MilestoneSelect */ +/* global LabelsSelect */ +/* global Sidebar */ + (() => { const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 index 10ce746deb5..3f5cf8420a8 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var, indent */ + (() => { window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; @@ -45,10 +46,10 @@ return $li.append($a.prepend($labelColor)); }, - search: { - fields: ['title'] - }, - filterable: true, + search: { + fields: ['title'] + }, + filterable: true, selectable: true, multiSelect: true, clicked (label, $el, e) { diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js.es6 index 9eceac4eddd..7e192e90fe6 100644 --- a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 +++ b/app/assets/javascripts/boards/filters/due_date_filters.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* global Vue */ + Vue.filter('due-date', (value) => { const date = new Date(value); return $.datepicker.formatDate('M d, yy', date); diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 index 5f99de39122..a5e62ed775d 100644 --- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars, no-mixed-operators, prefer-const, comma-dangle, semi */ +/* global DocumentTouch */ + ((w) => { window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6 index 21d735e8231..1199e022ff1 100644 --- a/app/assets/javascripts/boards/models/issue.js.es6 +++ b/app/assets/javascripts/boards/models/issue.js.es6 @@ -1,4 +1,9 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, space-in-parens, arrow-parens, comma-dangle, max-len */ +/* global Vue */ +/* global ListLabel */ +/* global ListMilestone */ +/* global ListUser */ + class ListIssue { constructor (obj) { this.id = obj.iid; diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js.es6 index 0910fe9a854..8f20a1bbec7 100644 --- a/app/assets/javascripts/boards/models/label.js.es6 +++ b/app/assets/javascripts/boards/models/label.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars, space-before-function-paren */ + class ListLabel { constructor (obj) { this.id = obj.id; diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index 429bd27c3fb..a8d38c16485 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-plusplus, prefer-const, space-in-parens, no-shadow, no-param-reassign, max-len, no-unused-vars */ +/* global ListIssue */ +/* global ListLabel */ + class List { constructor (obj) { this.id = obj.id; diff --git a/app/assets/javascripts/boards/models/milestone.js.es6 b/app/assets/javascripts/boards/models/milestone.js.es6 index a48969e19c9..9c173c1b70b 100644 --- a/app/assets/javascripts/boards/models/milestone.js.es6 +++ b/app/assets/javascripts/boards/models/milestone.js.es6 @@ -1,6 +1,7 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars */ + class ListMilestone { - constructor (obj) { + constructor(obj) { this.id = obj.id; this.title = obj.title; } diff --git a/app/assets/javascripts/boards/models/user.js.es6 b/app/assets/javascripts/boards/models/user.js.es6 index 583a973fc46..a8a3892e2ad 100644 --- a/app/assets/javascripts/boards/models/user.js.es6 +++ b/app/assets/javascripts/boards/models/user.js.es6 @@ -1,6 +1,7 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars */ + class ListUser { - constructor (user) { + constructor(user) { this.id = user.id; this.name = user.name; this.username = user.username; diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index f59a2ed7937..189a8703198 100644 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, prefer-const, no-extra-semi, max-len, no-unused-vars */ +/* global Vue */ + class BoardService { constructor (root, boardId) { this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, { diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index bb2a4de8b18..e7a14ea5bca 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, one-var, indent, space-in-parens, no-shadow, radix, dot-notation, semi, max-len */ +/* global Cookies */ +/* global List */ + (() => { window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 index 80f137ca12e..3723a2039f9 100644 --- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 +++ b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars, no-plusplus */ +/* global Vue */ + Vue.http.interceptors.push((request, next) => { Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js index e7ceb602601..a7e72430141 100644 --- a/app/assets/javascripts/breakpoints.js +++ b/app/assets/javascripts/breakpoints.js @@ -1,6 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, padded-blocks, no-return-assign, new-parens, no-param-reassign, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, padded-blocks, no-return-assign, new-parens, no-param-reassign, max-len */ + (function() { - this.Breakpoints = (function() { + var Breakpoints = (function() { var BreakpointInstance, instance; function Breakpoints() {} @@ -68,4 +69,5 @@ }; })(this)); + window.Breakpoints = Breakpoints; }).call(this); diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 116a47b0907..824febe3fd3 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -1,4 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, no-undef, quotes, yoda, no-else-return, consistent-return, comma-dangle, semi, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, semi, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top, padded-blocks */ +/* global Breakpoints */ +/* global Turbolinks */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; @@ -193,6 +196,7 @@ }; Build.prototype.stepTrace = function(e) { + var $currentTarget; e.preventDefault(); $currentTarget = $(e.currentTarget); $.scrollTo($currentTarget.attr('href'), { diff --git a/app/assets/javascripts/build_variables.js.es6 b/app/assets/javascripts/build_variables.js.es6 index 0ecd20bc11e..993424d422f 100644 --- a/app/assets/javascripts/build_variables.js.es6 +++ b/app/assets/javascripts/build_variables.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable func-names, prefer-arrow-callback, space-before-blocks, space-before-function-paren, comma-spacing, max-len */ + $(function(){ $('.reveal-variables').off('click').on('click',function(){ $('.js-build').toggle().niceScroll(); diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js index 67509ea7d91..67b33a4d7ee 100644 --- a/app/assets/javascripts/commit.js +++ b/app/assets/javascripts/commit.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-undef, padded-blocks */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, padded-blocks */ +/* global CommitFile */ + (function() { this.Commit = (function() { function Commit() { diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js index 3f29826fa9b..27512312c7c 100644 --- a/app/assets/javascripts/commit/file.js +++ b/app/assets/javascripts/commit/file.js @@ -1,9 +1,11 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, padded-blocks */ +/* global ImageFile */ + (function() { this.CommitFile = (function() { function CommitFile(file) { if ($('.image', file).length) { - new ImageFile(file); + new gl.ImageFile(file); } } diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index 4c2ae595319..fd8910e916f 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -1,6 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, padded-blocks, max-len */ (function() { - this.ImageFile = (function() { + gl.ImageFile = (function() { var prepareFrames; // Width where images must fits in, for 2-up this gets divided by 2 diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 3627aaf5080..24a6e4ff0e9 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-undef, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, padded-blocks, max-len, prefer-arrow-callback */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, padded-blocks, max-len, prefer-arrow-callback */ +/* global Pager */ + (function() { this.CommitsList = (function() { function CommitsList() {} @@ -6,8 +8,8 @@ CommitsList.timer = null; CommitsList.init = function(limit) { - $("body").on("click", ".day-commits-table li.commit", function(event) { - if (event.target.nodeName !== "A") { + $("body").on("click", ".day-commits-table li.commit", function(e) { + if (e.target.nodeName !== "A") { location.href = $(this).attr("url"); e.stopPropagation(); return false; diff --git a/app/assets/javascripts/compare_autocomplete.js.es6 b/app/assets/javascripts/compare_autocomplete.js.es6 index bd980f87e72..45c974b2b68 100644 --- a/app/assets/javascripts/compare_autocomplete.js.es6 +++ b/app/assets/javascripts/compare_autocomplete.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, no-dupe-keys, wrap-iife, padded-blocks, max-len */ + (function() { this.CompareAutocomplete = (function() { function CompareAutocomplete() { diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js index 1cc34e490c2..6a13f38588d 100644 --- a/app/assets/javascripts/copy_to_clipboard.js +++ b/app/assets/javascripts/copy_to_clipboard.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-undef, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, padded-blocks, max-len */ +/* global Clipboard */ /*= require clipboard */ @@ -6,7 +7,7 @@ var genericError, genericSuccess, showTooltip; genericSuccess = function(e) { - showTooltip(e.trigger, 'Copied!'); + showTooltip(e.trigger, 'Copied'); // Clear the selection and blur the trigger so it loses its border e.clearSelection(); return $(e.trigger).blur(); @@ -31,7 +32,7 @@ var originalTitle = $target.data('original-title'); $target - .attr('title', 'Copied!') + .attr('title', 'Copied') .tooltip('fixTitle') .tooltip('show') .attr('title', originalTitle) diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6 index 744aa0afa03..947c129d5b5 100644 --- a/app/assets/javascripts/create_label.js.es6 +++ b/app/assets/javascripts/create_label.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-param-reassign, wrap-iife, max-len */ +/* global Api */ + (function (w) { class CreateLabelDropdown { constructor ($el, namespacePath, projectPath) { diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6 index ecf9d1de81c..9cf33e62958 100644 --- a/app/assets/javascripts/diff.js.es6 +++ b/app/assets/javascripts/diff.js.es6 @@ -5,8 +5,11 @@ class Diff { constructor() { - $('.files .diff-file').singleFileDiff(); - $('.files .diff-file').filesCommentButton(); + const $diffFile = $('.files .diff-file'); + $diffFile.singleFileDiff(); + $diffFile.filesCommentButton(); + + $diffFile.each((index, file) => new gl.ImageFile(file)); if (this.diffViewType() === 'parallel') { $('.content-wrapper .container-fluid').removeClass('container-limited'); diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 index 52e2846d279..c59d3996fab 100644 --- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, semi, max-len */ +/* global Vue */ +/* global CommentsStore */ + (() => { const CommentAndResolveBtn = Vue.extend({ props: { diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 index 983e554b9c1..f47867fc3b0 100644 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 @@ -1,6 +1,10 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, indent, space-before-function-paren, no-plusplus, no-lonely-if, no-continue, brace-style, max-len, quotes, semi */ +/* global Vue */ +/* global DiscussionMixins */ +/* global CommentsStore */ + (() => { - JumpToDiscussion = Vue.extend({ + const JumpToDiscussion = Vue.extend({ mixins: [DiscussionMixins], props: { discussionId: String diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 index 27af9fc96ad..88a19fc6e1d 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 @@ -1,4 +1,9 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, no-new, max-len */ +/* global Vue */ +/* global CommentsStore */ +/* global ResolveService */ +/* global Flash */ + (() => { const ResolveBtn = Vue.extend({ props: { diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 index 9522ccb49da..72cdae812bc 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 @@ -1,4 +1,8 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, object-shorthand, func-names, no-param-reassign */ +/* global Vue */ +/* global DiscussionMixins */ +/* global CommentsStore */ + ((w) => { w.ResolveCount = Vue.extend({ mixins: [DiscussionMixins], diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 index b945a09fcbe..ee5f62b2d9e 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 @@ -1,4 +1,8 @@ -/* eslint-disable */ +/* eslint-disable object-shorthand, func-names, space-before-function-paren, comma-dangle, no-else-return, quotes, max-len */ +/* global Vue */ +/* global CommentsStore */ +/* global ResolveService */ + (() => { const ResolveDiscussionBtn = Vue.extend({ props: { diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 index bd4c20aed8b..840b5aa922e 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable func-names, comma-dangle, new-cap, no-new */ +/* global Vue */ +/* global ResolveCount */ + //= require vue //= require vue-resource //= require_directory ./models diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 index 7a929017f36..a9ea18bf82b 100644 --- a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 +++ b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-plusplus, no-param-reassign, max-len */ + ((w) => { w.DiscussionMixins = { computed: { diff --git a/app/assets/javascripts/diff_notes/models/discussion.js.es6 b/app/assets/javascripts/diff_notes/models/discussion.js.es6 index badcdccc840..efcd46680a7 100644 --- a/app/assets/javascripts/diff_notes/models/discussion.js.es6 +++ b/app/assets/javascripts/diff_notes/models/discussion.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable space-before-function-paren, camelcase, guard-for-in, no-restricted-syntax, no-unused-vars, max-len */ +/* global Vue */ +/* global NoteModel */ + class DiscussionModel { constructor (discussionId) { this.id = discussionId; @@ -69,7 +72,7 @@ class DiscussionModel { gl.utils.localTimeAgo($('.js-timeago', `${discussionSelector}`)); } else { - $discussionHeadline.remove(); + $discussionHeadline.remove(); } } diff --git a/app/assets/javascripts/diff_notes/models/note.js.es6 b/app/assets/javascripts/diff_notes/models/note.js.es6 index d0541b02632..e3bce1d2038 100644 --- a/app/assets/javascripts/diff_notes/models/note.js.es6 +++ b/app/assets/javascripts/diff_notes/models/note.js.es6 @@ -1,6 +1,7 @@ -/* eslint-disable */ +/* eslint-disable camelcase, no-unused-vars */ + class NoteModel { - constructor (discussionId, noteId, canResolve, resolved, resolved_by) { + constructor(discussionId, noteId, canResolve, resolved, resolved_by) { this.discussionId = discussionId; this.id = noteId; this.canResolve = canResolve; diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6 index 86953ce7ffb..78c74985f78 100644 --- a/app/assets/javascripts/diff_notes/services/resolve.js.es6 +++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6 @@ -1,4 +1,8 @@ -/* eslint-disable */ +/* eslint-disable class-methods-use-this, one-var, indent, camelcase, no-new, comma-dangle, semi, no-param-reassign, max-len */ +/* global Vue */ +/* global Flash */ +/* global CommentsStore */ + ((w) => { class ResolveServiceClass { constructor() { diff --git a/app/assets/javascripts/diff_notes/stores/comments.js.es6 b/app/assets/javascripts/diff_notes/stores/comments.js.es6 index f42ca406bb1..1a7abbc6f75 100644 --- a/app/assets/javascripts/diff_notes/stores/comments.js.es6 +++ b/app/assets/javascripts/diff_notes/stores/comments.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable object-shorthand, func-names, camelcase, prefer-const, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */ +/* global Vue */ +/* global DiscussionModel */ + ((w) => { w.CommentsStore = { state: {}, diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 413117c2226..1e259a16f06 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -1,4 +1,42 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len, padded-blocks */ +/* global UsernameValidator */ +/* global ActiveTabMemoizer */ +/* global ShortcutsNavigation */ +/* global Build */ +/* global Issuable */ +/* global Issue */ +/* global ShortcutsIssuable */ +/* global ZenMode */ +/* global Milestone */ +/* global GLForm */ +/* global IssuableForm */ +/* global LabelsSelect */ +/* global MilestoneSelect */ +/* global MergedButtons */ +/* global Commit */ +/* global NotificationsForm */ +/* global TreeView */ +/* global NotificationsDropdown */ +/* global UsersSelect */ +/* global GroupAvatar */ +/* global LineHighlighter */ +/* global ShortcutsBlob */ +/* global ProjectFork */ +/* global BuildArtifacts */ +/* global GroupsSelect */ +/* global Search */ +/* global Admin */ +/* global NamespaceSelects */ +/* global ShortcutsDashboardNavigation */ +/* global Project */ +/* global ProjectAvatar */ +/* global CompareAutocomplete */ +/* global ProjectNew */ +/* global Star */ +/* global ProjectShow */ +/* global Labels */ +/* global Shortcuts */ + (function() { var Dispatcher; @@ -36,7 +74,9 @@ case 'projects:merge_requests:index': case 'projects:issues:index': Issuable.init(); - new gl.IssuableBulkActions(); + new gl.IssuableBulkActions({ + prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_', + }); shortcut_handler = new ShortcutsNavigation(); break; case 'projects:issues:show': @@ -106,10 +146,6 @@ new ZenMode(); new MergedButtons(); break; - case 'projects:merge_requests:index': - shortcut_handler = new ShortcutsNavigation(); - Issuable.init(); - break; case 'dashboard:activity': new gl.Activities(); break; diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index e1e76bca6ad..56cb39be642 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, no-undef, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, no-plusplus, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, no-plusplus, prefer-arrow-callback, padded-blocks */ +/* global Dropzone */ /*= require preview_markdown */ diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6 index e84f5ac9183..2b7d57d86c6 100644 --- a/app/assets/javascripts/due_date_select.js.es6 +++ b/app/assets/javascripts/due_date_select.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, prefer-const, padded-blocks, no-unused-vars, no-underscore-dangle, no-new, max-len, semi, no-sequences, no-unused-expressions, no-param-reassign */ + (function(global) { class DueDateSelect { constructor({ $dropdown, $loading } = {}) { diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index 1db29dd47fb..88c3d257cea 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -164,8 +164,7 @@ {{state.availableCounter}} </span> </a> - </li> - <li v-bind:class="{ 'active' : scope === 'stopped' }"> + </li><li v-bind:class="{ 'active' : scope === 'stopped' }"> <a :href="projectStoppedEnvironmentsPath"> Stopped <span class="badge js-stopped-environments-count"> diff --git a/app/assets/javascripts/extensions/object.js.es6 b/app/assets/javascripts/extensions/object.js.es6 new file mode 100644 index 00000000000..70a2d765abd --- /dev/null +++ b/app/assets/javascripts/extensions/object.js.es6 @@ -0,0 +1,26 @@ +/* eslint-disable no-restricted-syntax */ + +// Adapted from https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill +if (typeof Object.assign !== 'function') { + Object.assign = function assign(target, ...args) { + if (target == null) { // TypeError if undefined or null + throw new TypeError('Cannot convert undefined or null to object'); + } + + const to = Object(target); + + for (let index = 0; index < args.length; index += 1) { + const nextSource = args[index]; + + if (nextSource != null) { // Skip over if undefined or null + for (const nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + }; +} diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index 0122e847161..785f2869970 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, padded-blocks, consistent-return, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, padded-blocks, consistent-return */ +/* global FilesCommentButton */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 6f9d6283071..90010b9fd54 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, padded-blocks, vars-on-top, indent, no-extra-semi, no-multi-spaces, semi, max-len */ + // Creates the variables for setting up GFM auto-completion (function() { if (window.GitLab == null) { @@ -9,7 +10,7 @@ return str.replace(/<(?:.|\n)*?>/gm, ''); } - GitLab.GfmAutoComplete = { + window.GitLab.GfmAutoComplete = { dataLoading: false, dataLoaded: false, cachedData: {}, @@ -52,7 +53,11 @@ return $.fn.atwho["default"].callbacks.filter(query, data, searchKey); }, beforeInsert: function(value) { - if (!GitLab.GfmAutoComplete.dataLoaded) { + if (value && !this.setting.skipSpecialCharacterTest) { + var withoutAt = value.substring(1); + if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"'; + } + if (!window.GitLab.GfmAutoComplete.dataLoaded) { return this.at; } else { return value; @@ -62,14 +67,15 @@ // The below is taken from At.js source // Tweaked to commands to start without a space only if char before is a non-word character // https://github.com/ichord/At.js - var _a, _y, regexp, match; + var _a, _y, regexp, match, atSymbols; + atSymbols = Object.keys(this.app.controllers).join('|'); subtext = subtext.split(' ').pop(); flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); _a = decodeURI("%C3%80"); _y = decodeURI("%C3%BF"); - regexp = new RegExp("(?:\\B|\\W|\\s)" + flag + "(?!\\W)([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)|([^\\x00-\\xff]*)$", 'gi'); + regexp = new RegExp("(?:\\B|\\W|\\s)" + flag + "(?![" + atSymbols + "])([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)$", 'gi'); match = regexp.exec(subtext); @@ -117,6 +123,7 @@ insertTpl: ':${name}:', data: ['loading'], startWithSpace: false, + skipSpecialCharacterTest: true, callbacks: { sorter: this.DefaultOptions.sorter, filter: this.DefaultOptions.filter, @@ -141,6 +148,7 @@ data: ['loading'], startWithSpace: false, alwaysHighlightFirst: true, + skipSpecialCharacterTest: true, callbacks: { sorter: this.DefaultOptions.sorter, filter: this.DefaultOptions.filter, @@ -219,12 +227,13 @@ } }; })(this), - insertTpl: '${atwho-at}"${title}"', + insertTpl: '${atwho-at}${title}', data: ['loading'], startWithSpace: false, callbacks: { matcher: this.DefaultOptions.matcher, sorter: this.DefaultOptions.sorter, + beforeInsert: this.DefaultOptions.beforeInsert, beforeSave: function(milestones) { return $.map(milestones, function(m) { if (m.title == null) { @@ -284,18 +293,11 @@ callbacks: { matcher: this.DefaultOptions.matcher, sorter: this.DefaultOptions.sorter, + beforeInsert: this.DefaultOptions.beforeInsert, beforeSave: function(merges) { - var sanitizeLabelTitle; - sanitizeLabelTitle = function(title) { - if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) { - return "\"" + (sanitize(title)) + "\""; - } else { - return sanitize(title); - } - }; return $.map(merges, function(m) { return { - title: sanitizeLabelTitle(m.title), + title: sanitize(m.title), color: m.color, search: "" + m.title }; @@ -308,6 +310,7 @@ at: '/', alias: 'commands', searchKey: 'search', + skipSpecialCharacterTest: true, displayTpl: function(value) { var tpl = '<li>/${name}'; if (value.aliases.length > 0) { diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 9a91018a8e4..57dabfe05e4 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -1,4 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, space-before-blocks, prefer-rest-params, max-len, vars-on-top, no-plusplus, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, semi, no-return-assign, no-else-return, camelcase, no-undef, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, padded-blocks, prefer-template, no-param-reassign, no-loop-func, no-extra-semi, keyword-spacing, no-mixed-operators, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, space-before-blocks, prefer-rest-params, max-len, vars-on-top, no-plusplus, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, semi, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, padded-blocks, prefer-template, no-param-reassign, no-loop-func, no-extra-semi, keyword-spacing, no-mixed-operators */ +/* global fuzzaldrinPlus */ +/* global Turbolinks */ + (function() { var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, @@ -20,7 +23,6 @@ this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true; $inputContainer = this.input.parent(); $clearButton = $inputContainer.find('.js-dropdown-input-clear'); - this.indeterminateIds = []; $clearButton.on('click', (function(_this) { // Clear click return function(e) { @@ -188,7 +190,7 @@ })(); GitLabDropdown = (function() { - var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, currentIndex; + var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex; LOADING_CLASS = "is-loading"; @@ -345,12 +347,12 @@ $el = $(this); selected = self.rowClicked($el); if (self.options.clicked) { - self.options.clicked(selected, $el, e); + self.options.clicked(selected[0], $el, e, selected[1]); } // Update label right after all modifications in dropdown has been done if (self.options.toggleLabel) { - self.updateLabel(selected, $el, self); + self.updateLabel(selected[0], $el, self); } $el.trigger('blur'); @@ -441,12 +443,6 @@ this.resetRows(); this.addArrowKeyEvent(); - if (this.options.setIndeterminateIds) { - this.options.setIndeterminateIds.call(this); - } - if (this.options.setActiveIds) { - this.options.setActiveIds.call(this); - } // Makes indeterminate items effective if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { this.parseData(this.fullData); @@ -480,11 +476,6 @@ if (this.options.filterable) { $input.blur().val(""); } - // Triggering 'keyup' will re-render the dropdown which is not always required - // specially if we want to keep the state of the dropdown needed for bulk-assignment - if (!this.options.persistWhenHide) { - $input.trigger("input"); - } if (this.dropdown.find(".dropdown-toggle-page").length) { $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS); } @@ -617,7 +608,8 @@ }; GitLabDropdown.prototype.rowClicked = function(el) { - var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value; + var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking; + fieldName = this.options.fieldName; isInput = $(this.el).is('input'); if (this.renderedData) { @@ -638,7 +630,7 @@ el.addClass(ACTIVE_CLASS); } - return selectedObject; + return [selectedObject]; } field = []; @@ -656,6 +648,7 @@ } if (el.hasClass(ACTIVE_CLASS)) { + isMarking = false; el.removeClass(ACTIVE_CLASS); if (field && field.length) { if (isInput) { @@ -665,6 +658,7 @@ } } } else if (el.hasClass(INDETERMINATE_CLASS)) { + isMarking = true; el.addClass(ACTIVE_CLASS); el.removeClass(INDETERMINATE_CLASS); if (field && field.length && value == null) { @@ -674,6 +668,7 @@ this.addInput(fieldName, value, selectedObject); } } else { + isMarking = true; if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) { this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS); if (!isInput) { @@ -694,7 +689,7 @@ } } - return selectedObject; + return [selectedObject, isMarking]; }; GitLabDropdown.prototype.focusTextInput = function() { diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6 index 6ce392d2a5b..63f9cafa8d0 100644 --- a/app/assets/javascripts/gl_field_errors.js.es6 +++ b/app/assets/javascripts/gl_field_errors.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign, padded-blocks */ //= require gl_field_error diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index db5d9e75b3a..56a33eeaad5 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -1,4 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-undef, no-new, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, padded-blocks, max-len */ +/* global GitLab */ +/* global DropzoneInput */ +/* global autosize */ + (function() { this.GLForm = (function() { function GLForm(form) { diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js index c3a132b3c75..2d08a7c6ac3 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -1,4 +1,9 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, no-undef, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, padded-blocks */ +/* global ContributorsGraph */ +/* global ContributorsAuthorGraph */ +/* global ContributorsMasterGraph */ +/* global ContributorsStatGraphUtil */ +/* global d3 */ /*= require d3 */ diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js index cb2448e8cc7..9c5e9381e52 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, padded-blocks, no-undef, newline-per-chained-call, no-else-return, max-len */ +/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, padded-blocks, newline-per-chained-call, no-else-return */ +/* global d3 */ +/* global ContributorsGraph */ /*= require d3 */ diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js index 051ff98c774..1982f4af939 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js @@ -2,7 +2,7 @@ (function() { window.ContributorsStatGraphUtil = { parse_log: function(log) { - var by_author, by_email, data, entry, i, len, total; + var by_author, by_email, data, entry, i, len, total, normalized_email; total = {}; by_author = {}; by_email = {}; @@ -11,7 +11,8 @@ if (total[entry.date] == null) { this.add_date(entry.date, total); } - data = by_author[entry.author_name] || by_email[entry.author_email]; + normalized_email = entry.author_email.toLowerCase(); + data = by_author[entry.author_name] || by_email[normalized_email]; if (data == null) { data = this.add_author(entry, by_author, by_email); } @@ -32,12 +33,14 @@ return collection[date].date = date; }, add_author: function(author, by_author, by_email) { - var data; + var data, normalized_email; data = {}; data.author_name = author.author_name; data.author_email = author.author_email; + normalized_email = author.author_email.toLowerCase(); by_author[author.author_name] = data; - return by_email[author.author_email] = data; + by_email[normalized_email] = data; + return data; }, store_data: function(entry, total, by_author) { this.store_commits(total, by_author); diff --git a/app/assets/javascripts/group_label_subscription.js.es6 b/app/assets/javascripts/group_label_subscription.js.es6 index eea6cd40859..8e10e424412 100644 --- a/app/assets/javascripts/group_label_subscription.js.es6 +++ b/app/assets/javascripts/group_label_subscription.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, padded-blocks, max-len */ + (function(global) { class GroupLabelSubscription { constructor(container) { diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index 3dc6f05ca20..99700e7562a 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, no-undef, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, padded-blocks, max-len */ +/* global Api */ + (function() { var slice = [].slice; @@ -14,7 +16,7 @@ multiple: $(select).hasClass('multiselect'), minimumInputLength: 0, query: function(query) { - options = { all_available: all_available, skip_groups: skip_groups }; + var options = { all_available: all_available, skip_groups: skip_groups }; return Api.groups(query.term, options, function(groups) { var data; data = { diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js index 9425b6ed9d4..fa795be07ed 100644 --- a/app/assets/javascripts/importer_status.js +++ b/app/assets/javascripts/importer_status.js @@ -1,6 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, padded-blocks, vars-on-top, no-new, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, padded-blocks, vars-on-top, no-new, max-len */ + (function() { - this.ImporterStatus = (function() { + window.ImporterStatus = (function() { function ImporterStatus(jobs_url, import_url) { this.jobs_url = jobs_url; this.import_url = import_url; @@ -75,7 +76,7 @@ var jobsImportPath = $('.js-importer-status').data('jobs-import-path'); var importPath = $('.js-importer-status').data('import-path'); - new ImporterStatus(jobsImportPath, importPath); + new window.ImporterStatus(jobsImportPath, importPath); } }); }).call(this); diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index 46503c290ae..1c10a7445bb 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, prefer-const, padded-blocks, wrap-iife, max-len */ +/* global Issuable */ +/* global Turbolinks */ + (function() { var issuable_created; @@ -141,6 +144,9 @@ const $issuesOtherFilters = $('.issues-other-filters'); const $issuesBulkUpdate = $('.issues_bulk_update'); + this.issuableBulkActions.willUpdateLabels = false; + this.issuableBulkActions.setOriginalDropdownData(); + if ($checkedIssues.length > 0) { let ids = $.map($checkedIssues, function(value) { return $(value).data('id'); @@ -152,7 +158,6 @@ $updateIssuesIds.val([]); $issuesBulkUpdate.hide(); $issuesOtherFilters.show(); - this.issuableBulkActions.willUpdateLabels = false; } return true; }, diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js index 317818951fd..4aaad111082 100644 --- a/app/assets/javascripts/issuable_context.js +++ b/app/assets/javascripts/issuable_context.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, no-undef, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, padded-blocks, max-len */ +/* global UsersSelect */ + (function() { this.IssuableContext = (function() { function IssuableContext(currentUser) { diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 50fdbc89c7c..2f3cad13cc0 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -1,4 +1,9 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-undef, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, radix, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, radix, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, padded-blocks, max-len */ +/* global GitLab */ +/* global UsersSelect */ +/* global ZenMode */ +/* global Autosave */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 8540b199aba..63b70d4be17 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-undef, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, padded-blocks, max-len */ +/* global Flash */ /*= require flash */ /*= require jquery.waitforimages */ diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6 index 9697fb33566..52fd5d71b18 100644 --- a/app/assets/javascripts/issues_bulk_assignment.js.es6 +++ b/app/assets/javascripts/issues_bulk_assignment.js.es6 @@ -1,10 +1,14 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, radix, max-len, padded-blocks, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */ +/* global Issuable */ +/* global Flash */ + ((global) => { class IssuableBulkActions { - constructor({ container, form, issues } = {}) { - this.container = container || $('.content'), + constructor({ container, form, issues, prefixId } = {}) { + this.prefixId = prefixId || 'issue_'; this.form = form || this.getElement('.bulk-update'); + this.$labelDropdown = this.form.find('.js-label-select'); this.issues = issues || this.getElement('.issues-list .issue'); this.form.data('bulkActions', this); this.willUpdateLabels = false; @@ -13,10 +17,6 @@ Issuable.initChecks(); } - getElement(selector) { - return this.container.find(selector); - } - bindEvents() { return this.form.off('submit').on('submit', this.onFormSubmit.bind(this)); } @@ -70,10 +70,7 @@ getUnmarkedIndeterminedLabels() { const result = []; - const labelsToKeep = []; - - this.getElement('.labels-filter .is-indeterminate') - .each((i, el) => labelsToKeep.push($(el).data('labelId'))); + const labelsToKeep = this.$labelDropdown.data('indeterminate'); this.getLabelsFromSelection().forEach((id) => { if (labelsToKeep.indexOf(id) === -1) { @@ -103,45 +100,65 @@ } }; if (this.willUpdateLabels) { - this.getLabelsToApply().map(function(id) { - return formData.update.add_label_ids.push(id); - }); - this.getLabelsToRemove().map(function(id) { - return formData.update.remove_label_ids.push(id); - }); + formData.update.add_label_ids = this.$labelDropdown.data('marked'); + formData.update.remove_label_ids = this.$labelDropdown.data('unmarked'); } return formData; } - getLabelsToApply() { + setOriginalDropdownData() { + const $labelSelect = $('.bulk-update .js-label-select'); + $labelSelect.data('common', this.getOriginalCommonIds()); + $labelSelect.data('marked', this.getOriginalMarkedIds()); + $labelSelect.data('indeterminate', this.getOriginalIndeterminateIds()); + } + + // From issuable's initial bulk selection + getOriginalCommonIds() { const labelIds = []; - const $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]'); - $labels.each(function(k, label) { - if (label) { - return labelIds.push(parseInt($(label).val())); - } + + this.getElement('.selected_issue:checked').each((i, el) => { + labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels')); }); - return labelIds; + return _.intersection.apply(this, labelIds); } + // From issuable's initial bulk selection + getOriginalMarkedIds() { + const labelIds = []; + this.getElement('.selected_issue:checked').each((i, el) => { + labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels')); + }); + return _.intersection.apply(this, labelIds); + } - /** - * Returns Label IDs that will be removed from issue selection - * @return {Array} Array of labels IDs - */ - - getLabelsToRemove() { - const result = []; - const indeterminatedLabels = this.getUnmarkedIndeterminedLabels(); - const labelsToApply = this.getLabelsToApply(); - indeterminatedLabels.map(function(id) { - // We need to exclude label IDs that will be applied - // By not doing this will cause issues from selection to not add labels at all - if (labelsToApply.indexOf(id) === -1) { - return result.push(id); - } + // From issuable's initial bulk selection + getOriginalIndeterminateIds() { + const uniqueIds = []; + const labelIds = []; + let issuableLabels = []; + + // Collect unique label IDs for all checked issues + this.getElement('.selected_issue:checked').each((i, el) => { + issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'); + issuableLabels.forEach((labelId) => { + // Store unique IDs + if (uniqueIds.indexOf(labelId) === -1) { + uniqueIds.push(labelId); + } + }); + // Store array of IDs per issuable + labelIds.push(issuableLabels); }); - return result; + // Add uniqueIds to add it as argument for _.intersection + labelIds.unshift(uniqueIds); + // Return IDs that are present but not in all selected issueables + return _.difference(uniqueIds, _.intersection.apply(this, labelIds)); + } + + getElement(selector) { + this.scopeEl = this.scopeEl || $('.content'); + return this.scopeEl.find(selector); } } diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js.es6 index 175623e7448..33c5e35324d 100644 --- a/app/assets/javascripts/label_manager.js.es6 +++ b/app/assets/javascripts/label_manager.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, padded-blocks, max-len */ +/* global Flash */ + ((global) => { class LabelManager { @@ -104,4 +106,3 @@ gl.LabelManager = LabelManager; })(window.gl || (window.gl = {})); - diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index f334f35594d..ec2fc87bece 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -1,12 +1,16 @@ -/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, no-undef, semi, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread, padded-blocks, max-len */ +/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, semi, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread, padded-blocks */ +/* global Issuable */ +/* global ListLabel */ + (function() { this.LabelsSelect = (function() { function LabelsSelect() { var _this; _this = this; $('.js-label-select').each(function(i, dropdown) { - var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove; + var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer; $dropdown = $(dropdown); + $dropdownContainer = $dropdown.closest('.labels-filter'); $toggleText = $dropdown.find('.dropdown-toggle-text'); namespacePath = $dropdown.data('namespace-path'); projectPath = $dropdown.data('project-path'); @@ -122,7 +126,7 @@ }); }); }; - return $dropdown.glDropdown({ + $dropdown.glDropdown({ showMenuAbove: showMenuAbove, data: function(term, callback) { return $.ajax({ @@ -169,33 +173,40 @@ }); }, renderRow: function(label, instance) { - var $a, $li, active, color, colorEl, indeterminate, removesAll, selectedClass, spacing; + var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue; $li = $('<li>'); $a = $('<a href="#">'); selectedClass = []; removesAll = label.id <= 0 || (label.id == null); if ($dropdown.hasClass('js-filter-bulk-update')) { - indeterminate = instance.indeterminateIds; - active = instance.activeIds; + indeterminate = $dropdown.data('indeterminate') || []; + marked = $dropdown.data('marked') || []; + if (indeterminate.indexOf(label.id) !== -1) { selectedClass.push('is-indeterminate'); } - if (active.indexOf(label.id) !== -1) { + + if (marked.indexOf(label.id) !== -1) { // Remove is-indeterminate class if the item will be marked as active i = selectedClass.indexOf('is-indeterminate'); if (i !== -1) { selectedClass.splice(i, 1); } selectedClass.push('is-active'); - // Add input manually - instance.addInput(this.fieldName, label.id); } - } - if (this.id(label) && $form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + this.id(label).toString().replace(/'/g, '\\\'') + "']").length) { - selectedClass.push('is-active'); - } - if ($dropdown.hasClass('js-multiselect') && removesAll) { - selectedClass.push('dropdown-clear-active'); + } else { + if (this.id(label)) { + dropdownName = $dropdown.data('fieldName'); + dropdownValue = this.id(label).toString().replace(/'/g, '\\\''); + + if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) { + selectedClass.push('is-active'); + } + } + + if ($dropdown.hasClass('js-multiselect') && removesAll) { + selectedClass.push('dropdown-clear-active'); + } } if (label.duplicate) { spacing = 100 / label.color.length; @@ -231,7 +242,6 @@ // Return generated html return $li.html($a).prop('outerHTML'); }, - persistWhenHide: $dropdown.data('persistWhenHide'), search: { fields: ['title'] }, @@ -310,18 +320,15 @@ } } } - if ($dropdown.hasClass('js-filter-bulk-update')) { - // If we are persisting state we need the classes - if (!this.options.persistWhenHide) { - return $dropdown.parent().find('.is-active, .is-indeterminate').removeClass(); - } - } }, multiSelect: $dropdown.hasClass('js-multiselect'), vue: $dropdown.hasClass('js-issue-board-sidebar'), - clicked: function(label, $el, e) { + clicked: function(label, $el, e, isMarking) { var isIssueIndex, isMRIndex, page; - _this.enableBulkLabelDropdown(); + + page = $('body').data('page'); + isIssueIndex = page === 'projects:issues:index'; + isMRIndex = page === 'projects:merge_requests:index'; if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) { $dropdown.parent() @@ -330,12 +337,11 @@ } if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { + _this.enableBulkLabelDropdown(); + _this.setDropdownData($dropdown, isMarking, this.id(label)); return; } - page = $('body').data('page'); - isIssueIndex = page === 'projects:issues:index'; - isMRIndex = page === 'projects:merge_requests:index'; if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { if (label.isAny) { gl.issueBoards.BoardsStore.state.filters['label_name'] = []; @@ -397,17 +403,10 @@ } } }, - setIndeterminateIds: function() { - if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { - return this.indeterminateIds = _this.getIndeterminateIds(); - } - }, - setActiveIds: function() { - if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { - return this.activeIds = _this.getActiveIds(); - } - } }); + + // Set dropdown data + _this.setOriginalDropdownData($dropdownContainer, $dropdown); }); this.bindEvents(); } @@ -420,34 +419,9 @@ if ($('.selected_issue:checked').length) { return; } - // Remove inputs - $('.issues_bulk_update .labels-filter input[type="hidden"]').remove(); - // Also restore button text return $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label'); }; - LabelsSelect.prototype.getIndeterminateIds = function() { - var label_ids; - label_ids = []; - $('.selected_issue:checked').each(function(i, el) { - var issue_id; - issue_id = $(el).data('id'); - return label_ids.push($("#issue_" + issue_id).data('labels')); - }); - return _.flatten(label_ids); - }; - - LabelsSelect.prototype.getActiveIds = function() { - var label_ids; - label_ids = []; - $('.selected_issue:checked').each(function(i, el) { - var issue_id; - issue_id = $(el).data('id'); - return label_ids.push($("#issue_" + issue_id).data('labels')); - }); - return _.intersection.apply(_, label_ids); - }; - LabelsSelect.prototype.enableBulkLabelDropdown = function() { var issuableBulkActions; if ($('.selected_issue:checked').length) { @@ -456,8 +430,59 @@ } }; - return LabelsSelect; + LabelsSelect.prototype.setDropdownData = function($dropdown, isMarking, value) { + var i, markedIds, unmarkedIds, indeterminateIds; + var issuableBulkActions = $('.bulk-update').data('bulkActions'); + markedIds = $dropdown.data('marked') || []; + unmarkedIds = $dropdown.data('unmarked') || []; + indeterminateIds = $dropdown.data('indeterminate') || []; + + if (isMarking) { + markedIds.push(value); + + i = indeterminateIds.indexOf(value); + if (i > -1) { + indeterminateIds.splice(i, 1); + } + + i = unmarkedIds.indexOf(value); + if (i > -1) { + unmarkedIds.splice(i, 1); + } + } else { + // If marked item (not common) is unmarked + i = markedIds.indexOf(value); + if (i > -1) { + markedIds.splice(i, 1); + } + + // If an indeterminate item is being unmarked + if (issuableBulkActions.getOriginalIndeterminateIds().indexOf(value) > -1) { + unmarkedIds.push(value); + } + + // If a marked item is being unmarked + // (a marked item could also be a label that is present in all selection) + if (issuableBulkActions.getOriginalCommonIds().indexOf(value) > -1) { + unmarkedIds.push(value); + } + } + + $dropdown.data('marked', markedIds); + $dropdown.data('unmarked', unmarkedIds); + $dropdown.data('indeterminate', indeterminateIds); + }; + + LabelsSelect.prototype.setOriginalDropdownData = function($container, $dropdown) { + var labels = []; + $container.find('[name="label_name[]"]').map(function() { + return labels.push(this.value); + }); + $dropdown.data('marked', labels); + }; + + return LabelsSelect; })(); }).call(this); diff --git a/app/assets/javascripts/lib/ace.js b/app/assets/javascripts/lib/ace.js index b1718e89d3d..4cdf99cae72 100644 --- a/app/assets/javascripts/lib/ace.js +++ b/app/assets/javascripts/lib/ace.js @@ -1,3 +1,2 @@ -/* eslint-disable */ /*= require ace-rails-ap */ /*= require ace/ext-searchbox */ diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js index d0fe69260a5..3c9ad0e67c8 100644 --- a/app/assets/javascripts/lib/utils/notify.js +++ b/app/assets/javascripts/lib/utils/notify.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, no-undef, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, max-len */ + (function() { (function(w) { var notificationGranted, notifyMe, notifyPermissions; diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index b0f834705c3..9af89b79f84 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-param-reassign, no-undef, prefer-template, quotes, comma-dangle, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, spaced-comment, radix, no-else-return, max-len, no-plusplus, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-param-reassign, prefer-template, quotes, comma-dangle, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, spaced-comment, radix, no-else-return, max-len, no-plusplus, padded-blocks */ + // LineHighlighter // // Handles single- and multi-line selection and highlight for blob views. diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 index 9e4ffd07dbd..f95b079c972 100644 --- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 @@ -1,4 +1,8 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, prefer-const, no-new, padded-blocks, no-param-reassign, semi, max-len */ +/* global Vue */ +/* global ace */ +/* global Flash */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 index 23c4618af70..74544b7d0c7 100644 --- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */ +/* global Vue */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 index 4ccbdcd6daa..78c00c31c16 100644 --- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */ +/* global Vue */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; @@ -19,7 +21,7 @@ </td> <td class="diff-line-num old_line" :class="lineCssClass(line)" v-if="!line.isHeader">{{line.lineNumber}}</td> <td class="line_content parallel" :class="lineCssClass(line)" v-if="!line.isHeader" v-html="line.richText"></td> - </template> + </template> </tr> </table> `, diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 index 8a7519b0786..8df3170edac 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-param-reassign, comma-dangle, no-extra-semi, padded-blocks */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 index f94e51e783c..53b44007510 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, object-shorthand, no-dupe-keys, no-param-reassign, no-plusplus, camelcase, prefer-const, no-nested-ternary, no-continue, semi, func-call-spacing, no-spaced-func, padded-blocks, max-len */ +/* global Cookies */ +/* global Vue */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 index 815443fb54e..83520702f9b 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable new-cap, comma-dangle, no-new, semi */ +/* global Vue */ +/* global Flash */ + //= require vue //= require ./merge_conflict_store //= require ./merge_conflict_service diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 index c8de586aa21..e89b35d5407 100644 --- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 +++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-param-reassign, comma-dangle, padded-blocks */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 index 88c3a20ce13..a4aca85d460 100644 --- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 +++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-param-reassign, quote-props, comma-dangle, padded-blocks */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 88c3636be6c..70f9a8d1955 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-undef, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, padded-blocks, max-len */ +/* global MergeRequestTabs */ /*= require jquery.waitforimages */ /*= require task_list */ diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index d9495e50388..e47047c4cca 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -1,5 +1,10 @@ -/* eslint-disable */ - ((global) => { +/* eslint-disable max-len, no-var, func-names, space-before-function-paren, vars-on-top, no-plusplus, comma-dangle, no-return-assign, consistent-return, no-param-reassign, one-var, one-var-declaration-per-line, quotes, prefer-template, no-else-return, prefer-arrow-callback, no-unused-vars, no-underscore-dangle, no-shadow, no-mixed-operators, template-curly-spacing, camelcase, default-case, wrap-iife, semi, padded-blocks */ +/* global notify */ +/* global notifyPermissions */ +/* global merge_request_widget */ +/* global Turbolinks */ + +((global) => { var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; const DEPLOYMENT_TEMPLATE = `<div class="mr-widget-heading" id="<%- id %>"> @@ -27,7 +32,7 @@ </div> </div>`; - global.MergeRequestWidget = (function() { + global.MergeRequestWidget = (function() { function MergeRequestWidget(opts) { // Initialize MergeRequestWidget behavior // @@ -40,19 +45,26 @@ $('#modal_merge_info').modal({ show: false }); - this.firstCICheck = true; - this.readyForCICheck = false; - this.readyForCIEnvironmentCheck = false; - this.cancel = false; - clearInterval(this.fetchBuildStatusInterval); - clearInterval(this.fetchBuildEnvironmentStatusInterval); this.clearEventListeners(); this.addEventListeners(); this.getCIStatus(false); - this.getCIEnvironmentsStatus(); this.retrieveSuccessIcon(); - this.pollCIStatus(); - this.pollCIEnvironmentsStatus(); + + this.ciStatusInterval = new global.SmartInterval({ + callback: this.getCIStatus.bind(this, true), + startingInterval: 10000, + maxInterval: 30000, + hiddenInterval: 120000, + incrementByFactorOf: 5000, + }); + this.ciEnvironmentStatusInterval = new global.SmartInterval({ + callback: this.getCIEnvironmentsStatus.bind(this), + startingInterval: 30000, + maxInterval: 120000, + hiddenInterval: 240000, + incrementByFactorOf: 15000, + immediateExecution: true, + }); notifyPermissions(); } @@ -60,10 +72,6 @@ return $(document).off('page:change.merge_request'); }; - MergeRequestWidget.prototype.cancelPolling = function() { - return this.cancel = true; - }; - MergeRequestWidget.prototype.addEventListeners = function() { var allowedPages; allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes']; @@ -72,9 +80,6 @@ var page; page = $('body').data('page').split(':').last(); if (allowedPages.indexOf(page) < 0) { - clearInterval(_this.fetchBuildStatusInterval); - clearInterval(_this.fetchBuildEnvironmentStatusInterval); - _this.cancelPolling(); return _this.clearEventListeners(); } }; @@ -82,10 +87,10 @@ }; MergeRequestWidget.prototype.retrieveSuccessIcon = function() { - const $ciSuccessIcon = $('.js-success-icon'); - this.$ciSuccessIcon = $ciSuccessIcon.html(); - $ciSuccessIcon.remove(); - } + const $ciSuccessIcon = $('.js-success-icon'); + this.$ciSuccessIcon = $ciSuccessIcon.html(); + $ciSuccessIcon.remove(); + } MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) { if (deleteSourceBranch == null) { @@ -114,6 +119,11 @@ }); }; + MergeRequestWidget.prototype.cancelPolling = function () { + this.ciStatusInterval.cancel(); + this.ciEnvironmentStatusInterval.cancel(); + }; + MergeRequestWidget.prototype.getMergeStatus = function() { return $.get(this.opts.merge_check_url, function(data) { return $('.mr-state-widget').replaceWith(data); @@ -131,18 +141,6 @@ } }; - MergeRequestWidget.prototype.pollCIStatus = function() { - return this.fetchBuildStatusInterval = setInterval(((function(_this) { - return function() { - if (!_this.readyForCICheck) { - return; - } - _this.getCIStatus(true); - return _this.readyForCICheck = false; - }; - })(this)), 10000); - }; - MergeRequestWidget.prototype.getCIStatus = function(showNotification) { var _this; _this = this; @@ -150,23 +148,17 @@ return $.getJSON(this.opts.ci_status_url, (function(_this) { return function(data) { var message, status, title; - if (_this.cancel) { - return; - } - _this.readyForCICheck = true; if (data.status === '') { return; } if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); - if (_this.firstCICheck || data.status !== _this.opts.ci_status && (data.status != null)) { + if (data.status !== _this.opts.ci_status && (data.status != null)) { _this.opts.ci_status = data.status; _this.showCIStatus(data.status); if (data.coverage) { _this.showCICoverage(data.coverage); } - // The first check should only update the UI, a notification - // should only be displayed on status changes - if (showNotification && !_this.firstCICheck) { + if (showNotification) { status = _this.ciLabelForStatus(data.status); if (status === "preparing") { title = _this.opts.ci_title.preparing; @@ -184,24 +176,13 @@ return Turbolinks.visit(_this.opts.builds_path); }); } - return _this.firstCICheck = false; } }; })(this)); }; - MergeRequestWidget.prototype.pollCIEnvironmentsStatus = function() { - this.fetchBuildEnvironmentStatusInterval = setInterval(() => { - if (!this.readyForCIEnvironmentCheck) return; - this.getCIEnvironmentsStatus(); - this.readyForCIEnvironmentCheck = false; - }, 300000); - }; - MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() { $.getJSON(this.opts.ci_environments_status_url, (environments) => { - if (this.cancel) return; - this.readyForCIEnvironmentCheck = true; if (environments && environments.length) this.renderEnvironments(environments); }); }; @@ -212,11 +193,11 @@ if ($(`.mr-state-widget #${ environment.id }`).length) return; const $template = $(DEPLOYMENT_TEMPLATE); if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove(); - + if (!environment.stop_url) { $('.js-stop-env-link', $template).remove(); } - + if (environment.deployed_at && environment.deployed_at_formatted) { environment.deployed_at = gl.utils.getTimeago().format(environment.deployed_at, 'gl_en') + '.'; } else { @@ -228,7 +209,7 @@ const template = _.template(templateString)(environment) this.$widgetBody.before(template); } - }; + }; MergeRequestWidget.prototype.showCIStatus = function(state) { var allowed_states; @@ -270,4 +251,4 @@ })(); - })(window.gl || (window.gl = {})); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js index 15a12c3d985..9f8af46c715 100644 --- a/app/assets/javascripts/merged_buttons.js +++ b/app/assets/javascripts/merged_buttons.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, padded-blocks, max-len */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index db7561a3a75..42152362e60 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, no-undef, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, padded-blocks, max-len */ +/* global Flash */ + (function() { this.Milestone = (function() { Milestone.updateIssue = function(li, issue_url, data) { diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 67796083790..28054b78249 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -1,4 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-undef, no-param-reassign, no-shadow, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow, padded-blocks */ +/* global Vue */ +/* global Issuable */ +/* global ListMilestone */ + (function() { this.MilestoneSelect = (function() { function MilestoneSelect(currentProject) { diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js index 87c903ec576..6633f2c2709 100644 --- a/app/assets/javascripts/namespace_select.js +++ b/app/assets/javascripts/namespace_select.js @@ -1,8 +1,10 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, no-undef, prefer-arrow-callback, padded-blocks, no-param-reassign, no-cond-assign, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, padded-blocks, no-param-reassign, no-cond-assign, max-len */ +/* global Api */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - this.NamespaceSelect = (function() { + window.NamespaceSelect = (function() { function NamespaceSelect(opts) { this.onSelectItem = bind(this.onSelectItem, this); var fieldName, showAny; @@ -64,7 +66,7 @@ })(); - this.NamespaceSelects = (function() { + window.NamespaceSelects = (function() { function NamespaceSelects(opts) { var ref; if (opts == null) { @@ -74,7 +76,7 @@ this.$dropdowns.each(function(i, dropdown) { var $dropdown; $dropdown = $(dropdown); - return new NamespaceSelect({ + return new window.NamespaceSelect({ dropdown: $dropdown }); }); diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js index e3dc599b90a..64b19a54893 100644 --- a/app/assets/javascripts/network/branch_graph.js +++ b/app/assets/javascripts/network/branch_graph.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, new-cap, no-undef, no-plusplus, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, new-cap, no-plusplus, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len, padded-blocks */ +/* global Raphael */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js index 5a8f723a27b..2367d2497b2 100644 --- a/app/assets/javascripts/network/network.js +++ b/app/assets/javascripts/network/network.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, no-undef, quote-props, prefer-template, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, padded-blocks, max-len */ +/* global BranchGraph */ + (function() { this.Network = (function() { function Network(opts) { diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js index 732d92845cb..17833d3419a 100644 --- a/app/assets/javascripts/network/network_bundle.js +++ b/app/assets/javascripts/network/network_bundle.js @@ -1,4 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, no-undef, comma-dangle, consistent-return, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, padded-blocks, max-len */ +/* global Network */ +/* global ShortcutsNetwork */ + // This is a manifest file that'll be compiled into including all the files listed below. // Add new JavaScript code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 0ca0e255595..a8b9a352870 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1,4 +1,8 @@ -/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, no-undef, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, semi, indent, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, radix, padded-blocks, max-len */ +/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, semi, indent, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, radix, padded-blocks */ +/* global Flash */ +/* global GLForm */ +/* global Autosave */ +/* global ResolveService */ /*= require autosave */ /*= require autosize */ @@ -305,7 +309,7 @@ } row = form.closest("tr"); note_html = $(note.html); - note_html.syntaxHighlight(); + note_html.renderGFM(); // is this the first note of discussion? discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']"); if ((note.original_discussion_id != null) && discussionContainer.length === 0) { @@ -322,7 +326,7 @@ 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) { - $('ul.main-notes-list').append(note.discussion_html).syntaxHighlight(); + $('ul.main-notes-list').append(note.discussion_html).renderGFM(); } } else { // append new note to all matching discussions @@ -463,7 +467,7 @@ // Convert returned HTML to a jQuery object so we can modify it further $html = $(note.html); gl.utils.localTimeAgo($('.js-timeago', $html)); - $html.syntaxHighlight(); + $html.renderGFM(); $html.find('.js-task-list-container').taskList('enable'); // Find the note's `li` element by ID and replace it with the updated HTML $note_li = $('.note-row-' + note.id); @@ -671,7 +675,7 @@ */ Notes.prototype.addDiffNote = function(e) { - var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, replyButton, row, rowCssToAdd, targetContent; + var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent; e.preventDefault(); $link = $(e.currentTarget); row = $link.closest("tr"); diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js index b152d26733f..324b68a7efc 100644 --- a/app/assets/javascripts/notifications_dropdown.js +++ b/app/assets/javascripts/notifications_dropdown.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, padded-blocks, max-len */ +/* global Flash */ + (function() { this.NotificationsDropdown = (function() { function NotificationsDropdown() { diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index 72c6c4a1fcd..0b09ad113a3 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -1,10 +1,11 @@ +/* eslint-disable no-new, guard-for-in, no-restricted-syntax, no-continue, padded-blocks, no-param-reassign, max-len */ + //= require lib/utils/bootstrap_linked_tabs -/* eslint-disable */ ((global) => { class Pipelines { - constructor(options) { + constructor(options = {}) { if (options.initTabs && options.tabsOptions) { new global.LinkedTabs(options.tabsOptions); @@ -14,9 +15,11 @@ } addMarginToBuildColumns() { - this.pipelineGraph = document.querySelector('.pipeline-graph'); - const secondChildBuildNodes = document.querySelector('.pipeline-graph').querySelectorAll('.build:nth-child(2)'); - for (buildNodeIndex in secondChildBuildNodes) { + this.pipelineGraph = document.querySelector('.js-pipeline-graph'); + + const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)'); + + for (const buildNodeIndex in secondChildBuildNodes) { const buildNode = secondChildBuildNodes[buildNodeIndex]; const firstChildBuildNode = buildNode.previousElementSibling; if (!firstChildBuildNode || !firstChildBuildNode.matches('.build')) continue; @@ -28,6 +31,7 @@ const columnBuilds = previousColumn.querySelectorAll('.build'); if (columnBuilds.length === 1) previousColumn.classList.add('no-margin'); } + this.pipelineGraph.classList.remove('hidden'); } } diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js index 3723aa24942..1e261cd49c2 100644 --- a/app/assets/javascripts/preview_markdown.js +++ b/app/assets/javascripts/preview_markdown.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, wrap-iife, no-else-return, consistent-return, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, no-undef, camelcase, prefer-arrow-callback, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, wrap-iife, no-else-return, consistent-return, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, camelcase, prefer-arrow-callback, max-len */ + // MarkdownPreview // // Handles toggling the "Write" and "Preview" tab clicks, rendering the preview, @@ -7,7 +8,7 @@ (function() { var lastTextareaPreviewed, markdownPreview, previewButtonSelector, writeButtonSelector; - this.MarkdownPreview = (function() { + window.MarkdownPreview = (function() { function MarkdownPreview() {} // Minimum number of users referenced before triggering a warning @@ -27,7 +28,7 @@ return this.renderMarkdown(mdText, (function(_this) { return function(response) { preview.html(response.body); - preview.syntaxHighlight(); + preview.renderGFM(); return _this.renderReferencedUsers(response.references.users, form); }; })(this)); @@ -83,7 +84,7 @@ })(); - markdownPreview = new MarkdownPreview(); + markdownPreview = new window.MarkdownPreview(); previewButtonSelector = '.js-md-preview-button'; diff --git a/app/assets/javascripts/profile/gl_crop.js.es6 b/app/assets/javascripts/profile/gl_crop.js.es6 index 6da6c1d0295..b4b6da41f63 100644 --- a/app/assets/javascripts/profile/gl_crop.js.es6 +++ b/app/assets/javascripts/profile/gl_crop.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-useless-escape, max-len, padded-blocks, quotes, no-var, no-underscore-dangle, func-names, space-before-function-paren, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, no-plusplus, new-parens, semi */ + ((global) => { // Matches everything but the file name diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6 index 3eb81808bd6..eb2fe3477a2 100644 --- a/app/assets/javascripts/profile/profile.js.es6 +++ b/app/assets/javascripts/profile/profile.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len, padded-blocks */ +/* global Flash */ + ((global) => { class Profile { diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 016d999d77e..fcf3a4af956 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -1,4 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-undef, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, semi, vars-on-top, indent, prefer-template, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, semi, vars-on-top, indent, prefer-template, padded-blocks, max-len */ +/* global Cookies */ +/* global Turbolinks */ +/* global ProjectSelect */ + (function() { this.Project = (function() { function Project() { diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 804306a3293..1bd232314d0 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, no-undef, object-shorthand, no-param-reassign, comma-dangle, no-plusplus, prefer-template, no-unused-vars, no-return-assign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, no-plusplus, prefer-template, no-unused-vars, no-return-assign, padded-blocks */ +/* global fuzzaldrinPlus */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js index c99e55234cf..02dafcfb865 100644 --- a/app/assets/javascripts/project_import.js +++ b/app/assets/javascripts/project_import.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, padded-blocks, max-len */ +/* global Turbolinks */ + (function() { this.ProjectImport = (function() { function ProjectImport() { diff --git a/app/assets/javascripts/project_label_subscription.js.es6 b/app/assets/javascripts/project_label_subscription.js.es6 index 03a115cb35b..b8d6a198996 100644 --- a/app/assets/javascripts/project_label_subscription.js.es6 +++ b/app/assets/javascripts/project_label_subscription.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, prefer-const, max-len, no-param-reassign, padded-blocks */ + (function(global) { class ProjectLabelSubscription { constructor(container) { diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index fe1f96872f3..650996700ba 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-undef, no-else-return, quotes, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, padded-blocks, max-len */ +/* global Api */ + (function() { this.ProjectSelect = (function() { function ProjectSelect() { diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js index dbf530bed41..4548dc68fe1 100644 --- a/app/assets/javascripts/projects_list.js +++ b/app/assets/javascripts/projects_list.js @@ -1,6 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-var, one-var, one-var-declaration-per-line, no-undef, prefer-arrow-callback, consistent-return, no-unused-vars, camelcase, prefer-template, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-var, one-var, one-var-declaration-per-line, prefer-arrow-callback, consistent-return, no-unused-vars, camelcase, prefer-template, comma-dangle, padded-blocks, max-len */ + (function() { - this.ProjectsList = { + window.ProjectsList = { init: function() { $(".projects-list-filter").off('keyup'); this.initSearch(); @@ -9,7 +10,7 @@ initSearch: function() { var debounceFilter, projectsListFilter; projectsListFilter = $('.projects-list-filter'); - debounceFilter = _.debounce(ProjectsList.filterResults, 500); + debounceFilter = _.debounce(window.ProjectsList.filterResults, 500); return projectsListFilter.on('keyup', function(e) { if (projectsListFilter.val() !== '') { return debounceFilter(); diff --git a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 index 2d60947a666..4aef1c84b56 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable arrow-parens, no-param-reassign, no-irregular-whitespace, object-shorthand, no-else-return, comma-dangle, semi, padded-blocks, max-len */ + (global => { global.gl = global.gl || {}; diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 index c45c9d8ff22..f26fba979a4 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable no-new, arrow-parens, no-param-reassign, no-irregular-whitespace, comma-dangle, padded-blocks, semi, max-len */ +/* global ProtectedBranchDropdown */ + (global => { global.gl = global.gl || {}; diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 index e3f226e9a2a..08264ad3d2f 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, no-unused-vars */ + class ProtectedBranchDropdown { constructor(options) { this.onSelect = options.onSelect; diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 index ac3142ffb07..4ff2fa5a80f 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable no-new, arrow-parens, no-param-reassign, no-irregular-whitespace, padded-blocks, comma-dangle, no-trailing-spaces, semi, max-len */ +/* global Flash */ + (global => { global.gl = global.gl || {}; @@ -33,7 +35,7 @@ const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`); // Do not update if one dropdown has not selected any option - if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; + if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; $.ajax({ type: 'POST', diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 index 705378a364d..b6972ef2e16 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable arrow-parens, no-param-reassign, no-irregular-whitespace, no-new, comma-dangle, semi, padded-blocks, max-len */ + (global => { global.gl = global.gl || {}; diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js index 17e34163831..15b3affd469 100644 --- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js +++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js @@ -1,2 +1 @@ -/* eslint-disable */ /*= require_tree . */ diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js new file mode 100644 index 00000000000..bbb2f186655 --- /dev/null +++ b/app/assets/javascripts/render_gfm.js @@ -0,0 +1,16 @@ +/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */ +// Render Gitlab flavoured Markdown +// +// Delegates to syntax highlight and render math +// +(function() { + $.fn.renderGFM = function() { + this.find('.js-syntax-highlight').syntaxHighlight(); + this.find('.js-render-math').renderMath(); + }; + + $(document).on('ready page:load', function() { + return $('body').renderGFM(); + }); + +}).call(this); diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js new file mode 100644 index 00000000000..209e7a8661d --- /dev/null +++ b/app/assets/javascripts/render_math.js @@ -0,0 +1,55 @@ +/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len, no-console */ +// Renders math using KaTeX in any element with the +// `js-render-math` class +// +// ### Example Markup +// +// <code class="js-render-math"></div> +// +(function() { + // Only load once + var katexLoaded = false; + + // Loop over all math elements and render math + var renderWithKaTeX = function (elements) { + elements.each(function () { + var mathNode = $('<span></span>'); + var $this = $(this); + + var display = $this.attr('data-math-style') === 'display'; + try { + katex.render($this.text(), mathNode.get(0), { displayMode: display }); + mathNode.insertAfter($this); + $this.remove(); + } catch (err) { + // What can we do?? + console.log(err.message); + } + }); + }; + + $.fn.renderMath = function() { + var $this = this; + if ($this.length === 0) return; + + if (katexLoaded) renderWithKaTeX($this); + else { + // Request CSS file so it is in the cache + $.get(gon.katex_css_url, function() { + var css = $('<link>', + { rel: 'stylesheet', + type: 'text/css', + href: gon.katex_css_url, + }); + css.appendTo('head'); + + // Load KaTeX js + $.getScript(gon.katex_js_url, function() { + katexLoaded = true; + renderWithKaTeX($this); // Run KaTeX + }); + }); + } + }; + +}).call(this); diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 440b5da756d..b1e844b7302 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-unused-vars, semi, consistent-return, one-var, one-var-declaration-per-line, no-undef, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-unused-vars, semi, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, padded-blocks, max-len */ +/* global Cookies */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index 1d208f1494c..4b6ebadeac7 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, no-undef, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, padded-blocks, max-len */ +/* global Api */ + (function() { this.Search = (function() { function Search() { diff --git a/app/assets/javascripts/search_autocomplete.js.es6 b/app/assets/javascripts/search_autocomplete.js.es6 index 5fa94556501..437f5dbbf7d 100644 --- a/app/assets/javascripts/search_autocomplete.js.es6 +++ b/app/assets/javascripts/search_autocomplete.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, no-plusplus, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, padded-blocks, no-extra-semi, indent, max-len */ + ((global) => { const KEYCODE = { diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index fa2168723be..5ea00f408f4 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -1,4 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-undef, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-plusplus, no-else-return, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-plusplus, no-else-return, comma-dangle, padded-blocks, max-len */ +/* global Mousetrap */ +/* global Turbolinks */ +/* global findFileURL */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js index 65305b8c22f..c26903038b4 100644 --- a/app/assets/javascripts/shortcuts_blob.js +++ b/app/assets/javascripts/shortcuts_blob.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, consistent-return, padded-blocks, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, consistent-return, padded-blocks */ +/* global Shortcuts */ +/* global Mousetrap */ /*= require shortcuts */ diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js index 1b9a265ba39..4549742bbcb 100644 --- a/app/assets/javascripts/shortcuts_dashboard_navigation.js +++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks */ +/* global Mousetrap */ +/* global Shortcuts */ /*= require shortcuts */ diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js index 68cd6fad04e..3a81380eef0 100644 --- a/app/assets/javascripts/shortcuts_find_file.js +++ b/app/assets/javascripts/shortcuts_find_file.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks */ +/* global Mousetrap */ +/* global ShortcutsNavigation */ /*= require shortcuts_navigation */ diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index c4899f3566a..b892fbc3393 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -1,4 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators, padded-blocks */ +/* global Mousetrap */ +/* global Turbolinks */ +/* global ShortcutsNavigation */ +/* global sidebar */ /*= require mousetrap */ /*= require shortcuts_navigation */ diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index 7d4d6364c70..0776d0a9b67 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks */ +/* global Mousetrap */ +/* global Shortcuts */ /*= require shortcuts */ diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js index a4095d2c06b..ecc3fab84c3 100644 --- a/app/assets/javascripts/shortcuts_network.js +++ b/app/assets/javascripts/shortcuts_network.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks, max-len */ +/* global Mousetrap */ +/* global ShortcutsNavigation */ /*= require shortcuts_navigation */ diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6 index a23ca449c4b..9790a44972d 100644 --- a/app/assets/javascripts/sidebar.js.es6 +++ b/app/assets/javascripts/sidebar.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign, padded-blocks */ +/* global Cookies */ + ((global) => { let singleton; diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index 0d48e69cce9..ac8603ccd10 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -1,8 +1,9 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, padded-blocks, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, padded-blocks, max-len */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - this.SingleFileDiff = (function() { + window.SingleFileDiff = (function() { var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER; WRAPPER = '<div class="diff-content diff-wrap-lines"></div>'; @@ -93,7 +94,7 @@ $.fn.singleFileDiff = function(forceLoad, cb) { return this.each(function() { if (!$.data(this, 'singleFileDiff') || forceLoad) { - return $.data(this, 'singleFileDiff', new SingleFileDiff(this, forceLoad, cb)); + return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this, forceLoad, cb)); } }); }; diff --git a/app/assets/javascripts/smart_interval.js.es6 b/app/assets/javascripts/smart_interval.js.es6 index 5eb15dba79b..40f67637c7c 100644 --- a/app/assets/javascripts/smart_interval.js.es6 +++ b/app/assets/javascripts/smart_interval.js.es6 @@ -7,24 +7,31 @@ (() => { class SmartInterval { /** - * @param { function } callback Function to be called on each iteration (required) - * @param { milliseconds } startingInterval `currentInterval` is set to this initially - * @param { milliseconds } maxInterval `currentInterval` will be incremented to this - * @param { integer } incrementByFactorOf `currentInterval` is incremented by this factor - * @param { boolean } lazyStart Configure if timer is initialized on instantiation or lazily + * @param { function } opts.callback Function to be called on each iteration (required) + * @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially + * @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this + * @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this + * when the page is hidden + * @param { integer } opts.incrementByFactorOf `currentInterval` is incremented by this factor + * @param { boolean } opts.lazyStart Configure if timer is initialized on + * instantiation or lazily + * @param { boolean } opts.immediateExecution Configure if callback should + * be executed before the first interval. */ - constructor({ callback, startingInterval, maxInterval, incrementByFactorOf, lazyStart }) { + constructor(opts = {}) { this.cfg = { - callback, - startingInterval, - maxInterval, - incrementByFactorOf, - lazyStart, + callback: opts.callback, + startingInterval: opts.startingInterval, + maxInterval: opts.maxInterval, + hiddenInterval: opts.hiddenInterval, + incrementByFactorOf: opts.incrementByFactorOf, + lazyStart: opts.lazyStart, + immediateExecution: opts.immediateExecution, }; this.state = { intervalId: null, - currentInterval: startingInterval, + currentInterval: this.cfg.startingInterval, pageVisibility: 'visible', }; @@ -36,6 +43,11 @@ const cfg = this.cfg; const state = this.state; + if (cfg.immediateExecution) { + cfg.immediateExecution = false; + cfg.callback(); + } + state.intervalId = window.setInterval(() => { cfg.callback(); @@ -54,14 +66,29 @@ this.stopTimer(); } + onVisibilityHidden() { + if (this.cfg.hiddenInterval) { + this.setCurrentInterval(this.cfg.hiddenInterval); + this.resume(); + } else { + this.cancel(); + } + } + // start a timer, using the existing interval resume() { this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped this.start(); } + onVisibilityVisible() { + this.cancel(); + this.start(); + } + destroy() { this.cancel(); + document.removeEventListener('visibilitychange', this.handleVisibilityChange); $(document).off('visibilitychange').off('page:before-unload'); } @@ -80,11 +107,7 @@ initVisibilityChangeHandling() { // cancel interval when tab no longer shown (prevents cached pages from polling) - $(document) - .off('visibilitychange').on('visibilitychange', (e) => { - this.state.pageVisibility = e.target.visibilityState; - this.handleVisibilityChange(); - }); + document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); } initPageUnloadHandling() { @@ -92,10 +115,11 @@ $(document).on('page:before-unload', () => this.cancel()); } - handleVisibilityChange() { - const state = this.state; - - const intervalAction = state.pageVisibility === 'hidden' ? this.cancel : this.resume; + handleVisibilityChange(e) { + this.state.pageVisibility = e.target.visibilityState; + const intervalAction = this.isPageVisible() ? + this.onVisibilityVisible : + this.onVisibilityHidden; intervalAction.apply(this); } @@ -111,6 +135,7 @@ incrementInterval() { const cfg = this.cfg; const currentInterval = this.getCurrentInterval(); + if (cfg.hiddenInterval && !this.isPageVisible()) return; let nextInterval = currentInterval * cfg.incrementByFactorOf; if (nextInterval > cfg.maxInterval) { @@ -120,6 +145,8 @@ this.setCurrentInterval(nextInterval); } + isPageVisible() { return this.state.pageVisibility === 'visible'; } + stopTimer() { const state = this.state; diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js index 2c8ecba7de4..18512d179b3 100644 --- a/app/assets/javascripts/snippet/snippet_bundle.js +++ b/app/assets/javascripts/snippet/snippet_bundle.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, no-undef, quotes, semi, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, semi, padded-blocks, max-len */ +/* global ace */ + /*= require_tree . */ (function() { diff --git a/app/assets/javascripts/snippets_list.js.es6 b/app/assets/javascripts/snippets_list.js.es6 index c3afc3f2246..6f913326a3a 100644 --- a/app/assets/javascripts/snippets_list.js.es6 +++ b/app/assets/javascripts/snippets_list.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable arrow-parens, no-param-reassign, space-before-function-paren, func-names, no-var, semi, max-len */ + (global => { global.gl = global.gl || {}; diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js index 32803fa790b..f1fc526bf2e 100644 --- a/app/assets/javascripts/star.js +++ b/app/assets/javascripts/star.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, padded-blocks, max-len */ +/* global Flash */ + (function() { this.Star = (function() { function Star() { diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js index bd37d69165f..5d0fa62c50a 100644 --- a/app/assets/javascripts/syntax_highlight.js +++ b/app/assets/javascripts/syntax_highlight.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, padded-blocks, max-len */ + // Syntax Highlighter // // Applies a syntax highlighting color scheme CSS class to any element with the @@ -9,8 +10,10 @@ // <div class="js-syntax-highlight"></div> // (function() { + $.fn.syntaxHighlight = function() { var $children; + if ($(this).hasClass('js-syntax-highlight')) { // Given the element itself, apply highlighting return $(this).addClass(gon.user_color_scheme); @@ -23,8 +26,4 @@ } }; - $(document).on('ready page:load', function() { - return $('.js-syntax-highlight').syntaxHighlight(); - }); - }).call(this); diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6 index 93a3d67ee9f..d2b152045b4 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js.es6 +++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable prefer-const, comma-dangle, max-len, no-useless-return, object-curly-spacing, no-param-reassign, max-len */ +/* global Api */ + /*= require ../blob/template_selector */ ((global) => { diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js.es6 index 0a3890e85fe..7310b9de074 100644 --- a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 +++ b/app/assets/javascripts/templates/issuable_template_selectors.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-new, comma-dangle, class-methods-use-this, prefer-const, no-param-reassign */ + ((global) => { class IssuableTemplateSelectors { constructor({ $dropdowns, editor } = {}) { diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6 index 213e80825b7..d8713600030 100644 --- a/app/assets/javascripts/todos.js.es6 +++ b/app/assets/javascripts/todos.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable padded-blocks, class-methods-use-this, no-new, func-names, prefer-template, no-unneeded-ternary, object-shorthand, space-before-function-paren, comma-dangle, quote-props, consistent-return, no-else-return, semi, no-param-reassign, max-len */ +/* global UsersSelect */ +/* global Turbolinks */ + ((global) => { class Todos { @@ -72,7 +75,7 @@ allDoneClicked(e) { e.preventDefault(); e.stopImmediatePropagation(); - $target = $(e.currentTarget); + const $target = $(e.currentTarget); $target.disable(); return $.ajax({ type: 'POST', diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js index 5d991542b51..d2aa3c7a841 100644 --- a/app/assets/javascripts/u2f/authenticate.js +++ b/app/assets/javascripts/u2f/authenticate.js @@ -1,4 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-undef, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */ +/* global u2f */ +/* global U2FError */ +/* global U2FUtil */ + // Authenticate U2F (universal 2nd factor) devices for users to authenticate with. // // State Flow #1: setup -> in_progress -> authenticated -> POST to server diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js index 4c70a6e9bb6..69f98c9c0ad 100644 --- a/app/assets/javascripts/u2f/error.js +++ b/app/assets/javascripts/u2f/error.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, padded-blocks, max-len */ +/* global u2f */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js index 97d8993cac2..4f5d68f546b 100644 --- a/app/assets/javascripts/u2f/register.js +++ b/app/assets/javascripts/u2f/register.js @@ -1,4 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-undef, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */ +/* global u2f */ +/* global U2FError */ +/* global U2FUtil */ + // Register U2F (universal 2nd factor) devices for users to authenticate with. // // State Flow #1: setup -> in_progress -> registered -> POST to server diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js.es6 index 5e869e99fdb..0a2db7c05fe 100644 --- a/app/assets/javascripts/user.js.es6 +++ b/app/assets/javascripts/user.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable class-methods-use-this, comma-dangle, arrow-parens, no-param-reassign, semi */ +/* global Cookies */ + ((global) => { global.User = class { constructor({ action }) { diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6 index 5a625611987..b9c23b51b4d 100644 --- a/app/assets/javascripts/user_tabs.js.es6 +++ b/app/assets/javascripts/user_tabs.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, array-bracket-spacing, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, semi, no-param-reassign */ + /* UserTabs diff --git a/app/assets/javascripts/username_validator.js.es6 b/app/assets/javascripts/username_validator.js.es6 index c4dde575c6e..137cefa3b8e 100644 --- a/app/assets/javascripts/username_validator.js.es6 +++ b/app/assets/javascripts/username_validator.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, consistent-return, class-methods-use-this, arrow-parens, no-param-reassign, max-len */ + ((global) => { const debounceTimeoutDuration = 1000; const invalidInputClass = 'gl-field-error-outline'; @@ -77,7 +78,7 @@ this.renderState(); return $.ajax({ type: 'GET', - url: `/users/${username}/exists`, + url: `${gon.relative_url_root}/users/${username}/exists`, dataType: 'json', success: (res) => this.setAvailabilityState(res.exists) }); diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js index ba7f533c349..578be7c3590 100644 --- a/app/assets/javascripts/users/calendar.js +++ b/app/assets/javascripts/users/calendar.js @@ -1,4 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, vars-on-top, semi, keyword-spacing, no-plusplus, no-undef, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, vars-on-top, semi, keyword-spacing, no-plusplus, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, padded-blocks, max-len */ +/* global d3 */ +/* global dateFormat */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index c6e18fad832..d4b5e03aa35 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -1,4 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, no-undef, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-plusplus, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, keyword-spacing, no-param-reassign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-plusplus, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, keyword-spacing, no-param-reassign, padded-blocks */ +/* global Vue */ +/* global Issuable */ +/* global ListUser */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, slice = [].slice; diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index 82eb761442a..e09b59dd5aa 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -1,4 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, no-undef, camelcase, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, padded-blocks, max-len */ +/* global Dropzone */ +/* global Mousetrap */ + // Zen Mode (full screen) textarea // /*= provides zen_mode:enter */ diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index c82a9a2b9e3..40bc0579393 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -44,3 +44,6 @@ @import "framework/awards.scss"; @import "framework/images.scss"; @import "framework/broadcast-messages"; +@import "framework/emojis.scss"; +@import "framework/icons.scss"; +@import "framework/snippets.scss"; diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index dece5c3202b..9fc9bcebc44 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -12,8 +12,8 @@ z-index: 9; width: 300px; font-size: 14px; - background-color: $award-emoji-menu-bg; - border: 1px solid $award-emoji-menu-border; + background-color: $white-light; + border: 1px solid $border-white-light; border-radius: $border-radius-base; box-shadow: 0 6px 12px $award-emoji-menu-shadow; pointer-events: none; @@ -135,7 +135,7 @@ } .award-control-icon { - color: $award-emoji-new-btn-icon-color; + color: $border-gray-normal; margin-top: 1px; } } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 95c02499271..9f02749f5ab 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -24,7 +24,7 @@ .row-content-block { margin-top: 0; margin-bottom: -$gl-padding; - background-color: $background-color; + background-color: $gray-light; padding: $gl-padding; margin-bottom: 0; border-top: 1px solid $white-dark; @@ -118,7 +118,7 @@ .cover-block { text-align: center; - background: $background-color; + background: $gray-light; padding-top: 44px; position: relative; diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 1c7b2f4df7c..59ff17ad2c1 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -88,11 +88,11 @@ } @mixin btn-gray { - @include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-gray-dark); + @include btn-color($gray-light, $border-gray-normal, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-gray-dark); } @mixin btn-white { - @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $gl-text-color); + @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-gray-dark, $gl-text-color); } @mixin btn-with-margin { @@ -235,7 +235,7 @@ } .btn-transparent { - color: $btn-transparent-color; + color: $gl-gray-light; background-color: transparent; border: 0; @@ -289,7 +289,7 @@ .active { box-shadow: $gl-btn-active-background; - border: 1px solid $border-white-dark !important; + border: 1px solid $border-gray-dark !important; background-color: $btn-active-gray-light !important; } } @@ -309,8 +309,8 @@ text-align: left; padding: 6px 16px; border-color: $border-color; - color: $btn-placeholder-gray; - background-color: $background-color; + color: $gray-darkest; + background-color: $gray-light; &:hover, &:active, @@ -318,8 +318,8 @@ cursor: text; box-shadow: none; border-color: $border-color; - color: $btn-placeholder-gray; - background-color: $background-color; + color: $gray-darkest; + background-color: $gray-light; } } @@ -331,7 +331,7 @@ margin-left: 10px; i { - color: $gl-icon-color; + color: $gl-gray-light; } } @@ -344,8 +344,8 @@ } .btn-static { - background-color: $background-color !important; - border: 1px solid $border-gray-light; + background-color: $gray-light !important; + border: 1px solid $border-gray-normal; cursor: default; &:active { diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss index 2a100980aca..e0e46dd73af 100644 --- a/app/assets/stylesheets/framework/callout.scss +++ b/app/assets/stylesheets/framework/callout.scss @@ -11,7 +11,7 @@ padding: $gl-padding; border-left: 3px solid $border-color; color: $text-color; - background: $background-color; + background: $gray-light; } .bs-callout h4 { diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index d5914b900e2..11adf2568a1 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -10,7 +10,7 @@ @mixin chevron-active { .fa-chevron-down { - color: $dropdown-toggle-hover-icon-color; + color: $gray-darkest; } } @@ -28,14 +28,14 @@ .dropdown-toggle, .dropdown-menu-toggle { @include chevron-active; - border-color: $dropdown-toggle-hover-border-color; + border-color: $gray-darkest; } } .dropdown-toggle { padding: 6px 8px 6px 10px; - background-color: $dropdown-toggle-bg; - color: $dropdown-toggle-color; + background-color: $white-light; + color: $gl-text-color; font-size: 14px; text-align: left; border: 1px solid $border-color; @@ -73,7 +73,7 @@ } .fa { - color: $dropdown-toggle-icon-color; + color: $gray-darkest; } .fa-chevron-down { @@ -85,7 +85,7 @@ &:hover { @include chevron-active; - border-color: $dropdown-toggle-hover-border-color; + border-color: $gray-darkest; } &:focus:active { @@ -131,7 +131,7 @@ font-size: 14px; font-weight: normal; padding: 8px 0; - background-color: $dropdown-bg; + background-color: $white-light; border: 1px solid $dropdown-border-color; border-radius: $border-radius-base; box-shadow: 0 2px 4px $dropdown-shadow-color; @@ -188,7 +188,6 @@ &.is-focused { background-color: $dropdown-link-hover-bg; text-decoration: none; - outline: 0; } &.dropdown-menu-empty-link { @@ -202,7 +201,7 @@ } .icon-play { - fill: $table-text-gray; + fill: $gl-gray-light; margin-right: 6px; height: 12px; width: 11px; @@ -210,7 +209,7 @@ } .dropdown-header { - color: $dropdown-header-color; + color: $gl-gray-light; font-size: 13px; line-height: 22px; padding: 0 10px; @@ -223,7 +222,7 @@ .unclickable { cursor: not-allowed; padding: 5px 8px; - color: $dropdown-header-color; + color: $gl-gray-light; } } @@ -602,14 +601,14 @@ th { padding: 2px 0; - color: $calendar-header-color; + color: $note-disabled-comment-color; font-weight: normal; text-transform: lowercase; border-top: 1px solid $calendar-border-color; } .ui-datepicker-unselectable { - background-color: $calendar-unselectable-bg; + background-color: $gray-light; } } @@ -621,11 +620,11 @@ .dropdown-menu-inner-content { display: block; - color: $gl-placeholder-color; + color: $gl-gray-light; } .dropdown-toggle-text { &.is-default { - color: $gl-placeholder-color; + color: $gl-gray-light; } } diff --git a/app/assets/stylesheets/pages/emojis.scss b/app/assets/stylesheets/framework/emojis.scss index f17797b2381..7158de65143 100644 --- a/app/assets/stylesheets/pages/emojis.scss +++ b/app/assets/stylesheets/framework/emojis.scss @@ -1,4 +1,4 @@ -.emoji-0023-20E3 { background-position: 0 0px; } +.emoji-0023-20E3 { background-position: 0 0; } .emoji-002A-20E3 { background-position: -20px 0; } .emoji-0030-20E3 { background-position: 0 -20px; } .emoji-0031-20E3 { background-position: -20px -20px; } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index ab0b81f77f7..88ed0a4a17e 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -19,7 +19,7 @@ .file-title { position: relative; - background-color: $background-color; + background-color: $gray-light; border-bottom: 1px solid $border-color; margin: 0; text-align: left; diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 25a2b38baaa..940807fc399 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -22,7 +22,7 @@ input[type='text'].danger { margin-top: 0; margin-bottom: -$gl-padding; padding: $gl-padding; - background-color: $background-color; + background-color: $gray-light; border-top: 1px solid $border-color; } @@ -149,7 +149,7 @@ label { } .form-control::-webkit-input-placeholder { - color: $gl-placeholder-color; + color: $gl-gray-light; } .input-group { diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index cc2286038c0..971940773f7 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -9,7 +9,7 @@ header { &.navbar-empty { height: $header-height; background: $white-light; - border-bottom: 1px solid $btn-gray-hover; + border-bottom: 1px solid $white-normal; .center-logo { margin: 8px 0; @@ -27,7 +27,7 @@ header { z-index: 100; margin-bottom: 0; height: $header-height; - background-color: $background-color; + background-color: $gray-light; border: none; border-bottom: 1px solid $border-color; @@ -45,7 +45,7 @@ header { padding: 0; .nav > li > a { - color: $gl-icon-color; + color: $gl-gray-light; font-size: 18px; padding: 0; margin: ($header-height - 28) / 2 0; @@ -62,8 +62,8 @@ header { &:hover, &:focus, &:active { - background-color: $background-color; - color: darken($gl-icon-color, 30%); + background-color: $gray-light; + color: darken($gl-gray-light, 30%); .todos-pending-count { background: darken($todo-alert-blue, 10%); @@ -84,11 +84,11 @@ header { padding: 6px 10px; &:hover { - background-color: $btn-gray-hover; + background-color: $white-normal; } &.active { - color: $gl-icon-color; + color: $gl-gray-light; } } } @@ -100,10 +100,10 @@ header { font-size: 18px; padding: 6px 10px; border: none; - background-color: $background-color; + background-color: $gray-light; &:hover { - background-color: $btn-gray-hover; + background-color: $white-normal; } } } diff --git a/app/assets/stylesheets/pages/icons.scss b/app/assets/stylesheets/framework/icons.scss index 226bd2ead31..226bd2ead31 100644 --- a/app/assets/stylesheets/pages/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss index 878f44116ba..09a569ad415 100644 --- a/app/assets/stylesheets/framework/images.scss +++ b/app/assets/stylesheets/framework/images.scss @@ -4,7 +4,7 @@ } .appearance-light-logo-preview { - background-color: $background-color; + background-color: $gray-light; max-width: 72px; padding: 10px; margin-bottom: 10px; diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index 44834a84234..298913108ee 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -41,6 +41,6 @@ } &.status-box-upcoming { - background: $issue-box-upcoming-bg; + background: $gl-gray-light; } } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index ed4b60faf92..e96cd671e34 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -36,7 +36,7 @@ color: $list-warning-row-color; } - &.smoke { background-color: $background-color; } + &.smoke { background-color: $gray-light; } &:not(.ui-sort-disabled):hover { background: $row-hover; @@ -46,7 +46,7 @@ border-bottom: none; &.bottom { - background: $background-color; + background: $gray-light; } } @@ -59,7 +59,7 @@ p { padding-top: 1px; margin: 0; - color: $gray-dark; + color: $white-normal; img { position: relative; @@ -113,7 +113,7 @@ ul.content-list { padding: 0; li { - border-color: $table-border-color; + border-color: $white-normal; font-size: $list-font-size; color: $list-text-color; @@ -186,7 +186,7 @@ ul.content-list { &.list-placeholder { background-color: $gray-light; - border: dotted 1px $gray-dark; + border: dotted 1px $white-normal; margin: 1px 0; min-height: 52px; } @@ -224,7 +224,7 @@ ul.content-list { } .label-default { - color: $btn-transparent-color; + color: $gl-gray-light; } } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 59a30d31ac7..e30d81d09f0 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -114,7 +114,7 @@ // Border around images in issue and MR comments. img:not(.emoji) { - border: 1px solid $table-border-gray; + border: 1px solid $white-normal; padding: 5px; margin: 5px 0; // Ensure that image does not exceed viewport @@ -135,7 +135,7 @@ .toolbar-btn { float: left; padding: 0 5px; - color: $note-toolbar-color; + color: $gl-gray-light; background: transparent; border: 0; outline: 0; diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index ea77348633d..e4affbb1be1 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -51,7 +51,7 @@ margin-bottom: -1px; font-size: 14px; line-height: 28px; - color: $note-toolbar-color; + color: $gl-gray-light; border-bottom: 2px solid transparent; &:hover, @@ -80,17 +80,17 @@ .badge { font-weight: normal; background-color: $nav-badge-bg; - color: $btn-transparent-color; + color: $gl-gray-light; vertical-align: baseline; } } &.sub-nav { text-align: center; - background-color: $dark-background-color; + background-color: $gray-normal; .container-fluid { - background-color: $dark-background-color; + background-color: $gray-normal; margin-bottom: 0; } @@ -117,7 +117,7 @@ .top-area { @include clearfix; - border-bottom: 1px solid $btn-gray-hover; + border-bottom: 1px solid $white-normal; .nav-text { padding-top: 16px; @@ -289,7 +289,7 @@ top: $header-height; width: 100%; z-index: 11; - background: $background-color; + background: $gray-light; border-bottom: 1px solid $border-color; transition: padding $sidebar-transition-duration; text-align: center; @@ -317,7 +317,7 @@ .fa-caret-down { margin-left: 5px; - color: $gl-icon-color; + color: $gl-gray-light; } .dropdown { @@ -352,7 +352,7 @@ } .fade-right { - @include fade(left, $background-color); + @include fade(left, $gray-light); right: -5px; .fa { @@ -361,7 +361,7 @@ } .fade-left { - @include fade(right, $background-color); + @include fade(right, $gray-light); left: -5px; .fa { @@ -372,7 +372,7 @@ &.sub-nav-scroll { .fade-right { - @include fade(left, $dark-background-color); + @include fade(left, $gray-normal); right: 0; .fa { @@ -381,7 +381,7 @@ } .fade-left { - @include fade(right, $dark-background-color); + @include fade(right, $gray-normal); left: 0; .fa { diff --git a/app/assets/stylesheets/framework/page-header.scss b/app/assets/stylesheets/framework/page-header.scss index 85c1385d5d9..fff7d7f7524 100644 --- a/app/assets/stylesheets/framework/page-header.scss +++ b/app/assets/stylesheets/framework/page-header.scss @@ -14,7 +14,7 @@ .header-action-buttons { i { - color: $gl-icon-color; + color: $gl-gray-light; font-size: 13px; margin-right: 3px; } diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index fde1431b13e..9ab17e67d4c 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -39,7 +39,7 @@ } &:hover { - background-color: $gray-dark; + background-color: $white-normal; border-color: $border-white-normal; color: $gl-text-color; } @@ -108,7 +108,7 @@ border-color: $input-border; color: $gl-text-color; line-height: 15px; - background-color: $background-color; + background-color: $gray-light; background-image: none; .select2-search-choice-close { diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 0aa609b8dd5..46a06cd7eab 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -23,7 +23,7 @@ .sidebar-wrapper { z-index: 1000; - background: $background-color; + background: $gray-light; .nicescroll-rails-hr { // TODO: Figure out why nicescroll doesn't hide horizontal bar diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/framework/snippets.scss index 857eb76131a..5f7e1b17cc7 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/framework/snippets.scss @@ -1,3 +1,13 @@ +.snippet-row { + .title { + margin-bottom: 2px; + } + + .snippet-filename { + padding: 0 2px; + } +} + .snippet-form-holder .file-holder .file-title { padding: 2px; } @@ -12,23 +22,19 @@ .snippet-file-content { border-radius: 3px; - margin-bottom: $gl-padding; - - .btn-clipboard { - @extend .btn; - } } -.project-snippets .awards { - border-bottom: 1px solid $table-border-color; - padding-bottom: $gl-padding; +.snippet-header { + padding: $gl-padding 0; } .snippet-title { font-size: 24px; font-weight: 600; - padding: $gl-padding; - padding-left: 0; +} + +.snippet-edited-ago { + color: $gray-darkest; } .snippet-actions { diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index 5d0ca63ea08..6d9fa74a030 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -31,7 +31,7 @@ table { } th { - background-color: $background-color; + background-color: $gray-light; font-weight: normal; border-bottom: none; @@ -41,7 +41,7 @@ table { } td { - border-color: $table-border-color; + border-color: $white-normal; } } } diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index 875cded8b4e..6078505807e 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -6,7 +6,7 @@ .timeline-entry { padding: $gl-padding $gl-btn-padding 11px; - border-color: $table-border-color; + border-color: $white-normal; color: $gl-gray; border-bottom: 1px solid $border-white-light; diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index 55bc325b858..d998d654aa4 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -33,9 +33,9 @@ @import "bootstrap/labels"; @import "bootstrap/badges"; @import "bootstrap/alerts"; -@import "bootstrap/progress-bars"; +// @import "bootstrap/progress-bars"; @import "bootstrap/list-group"; -@import "bootstrap/wells"; +// @import "bootstrap/wells"; @import "bootstrap/close"; @import "bootstrap/panels"; diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index c731a8f222f..876adf7f712 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -78,7 +78,7 @@ $pagination-active-bg: $white-light; $pagination-active-border: $border-color; $pagination-disabled-color: #cdcdcd; -$pagination-disabled-bg: $background-color; +$pagination-disabled-bg: $gray-light; $pagination-disabled-border: $border-color; @@ -117,8 +117,8 @@ $alert-border-radius: 0; $panel-border-radius: 2px; $panel-default-text: $text-color; $panel-default-border: $border-color; -$panel-default-heading-bg: $background-color; -$panel-footer-bg: $background-color; +$panel-default-heading-bg: $gray-light; +$panel-footer-bg: $gray-light; $panel-inner-border: $border-color; //== Wells @@ -153,8 +153,8 @@ $nav-link-padding: 13px $gl-padding; //== Code // //## -$pre-bg: $background-color !default; +$pre-bg: $gray-light !default; $pre-color: $gl-gray !default; $pre-border-color: $border-color; -$table-bg-accent: $background-color; +$table-bg-accent: $gray-light; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 18716813c48..936aaf38254 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -17,16 +17,15 @@ $darken-dark-factor: 10%; $darken-border-factor: 5%; $white-light: #fff; -$white-normal: darken($white-light, $darken-normal-factor); -$white-dark: darken($white-light, $darken-dark-factor); +$white-normal: #f0f0f0; +$white-dark: #eaeaea; $gray-lightest: #fdfdfd; $gray-light: #fafafa; $gray-lighter: #f9f9f9; -$gray-normal: darken($gray-light, $darken-normal-factor); -$gray-dark: darken($gray-light, $darken-dark-factor); +$gray-normal: #f5f5f5; $gray-darker: #eee; -$gray-darkest: #c9c9c9; +$gray-darkest: #c4c4c4; $green-light: #3cbd70; $green-normal: darken($green-light, $darken-normal-factor); @@ -55,11 +54,9 @@ $black-transparent: rgba(0, 0, 0, 0.3); $border-white-light: darken($white-light, $darken-border-factor); $border-white-normal: darken($white-normal, $darken-border-factor); -$border-white-dark: darken($white-dark, $darken-border-factor); -$border-gray-light: darken($gray-light, $darken-border-factor); $border-gray-normal: darken($gray-normal, $darken-border-factor); -$border-gray-dark: darken($gray-dark, $darken-border-factor); +$border-gray-dark: darken($white-normal, $darken-border-factor); $border-green-extra-light: #9adb84; $border-green-light: darken($green-light, $darken-border-factor); @@ -78,9 +75,6 @@ $border-red-light: darken($red-light, $darken-border-factor); $border-red-normal: darken($red-normal, $darken-border-factor); $border-red-dark: darken($red-dark, $darken-border-factor); -$help-well-bg: $gray-light; -$help-well-border: #e5e5e5; - $warning-message-bg: #fbf2d9; $warning-message-color: #9e8e60; $warning-message-border: #f0e2bb; @@ -90,10 +84,6 @@ $warning-message-border: #f0e2bb; */ $border-color: #e5e5e5; $focus-border-color: #3aabf0; -$table-border-color: #f0f0f0; -$background-color: $gray-light; -$dark-background-color: #f5f5f5; -$table-text-gray: #8f8f8f; $well-expand-item: #e8f2f7; $well-inner-border: #eef0f2; $well-light-border: #f1f1f1; @@ -113,12 +103,10 @@ $gl-text-orange: #d90; $gl-link-color: #3777b0; $gl-diff-text-color: #555; $gl-dark-link-color: #333; -$gl-placeholder-color: #8f8f8f; -$gl-icon-color: $gl-placeholder-color; +$gl-gray-light: #8f8f8f; $gl-grayish-blue: #7f8fa4; $gl-gray: $gl-text-color; $gl-gray-dark: #313236; -$gl-gray-light: $gl-placeholder-color; $gl-header-color: #4c4e54; /* @@ -168,9 +156,7 @@ $header-height: 50px; $fixed-layout-width: 1280px; $error-exclamation-point: #e62958; $border-radius-default: 2px; -$btn-transparent-color: #8f8f8f; $settings-icon-size: 18px; -$provider-btn-group-border: #e5e5e5; $provider-btn-not-active-color: #4688f1; $link-underline-blue: #4a8bee; $active-item-blue: #4a8bee; @@ -193,7 +179,6 @@ $count-arrow-border: #dce0e5; $save-project-loader-color: #555; $divergence-graph-bar-bg: #ccc; $divergence-graph-separator-bg: #ccc; -$issue-box-upcoming-bg: #8f8f8f; /* * Common component specific colors @@ -246,8 +231,6 @@ $line-removed-dark: #fac5cd; $line-number-old: #f9d7dc; $line-number-new: #ddfbe6; $line-number-select: #fbf2da; -$match-line: $gray-light; -$table-border-gray: #f0f0f0; $line-target-blue: #f6faff; $line-select-yellow: #fcf8e7; $line-select-yellow-dark: #f0e2bd; @@ -257,7 +240,6 @@ $file-mode-changed: #777; $file-mode-changed: #777; $diff-image-bg: #ddd; $diff-image-info-color: grey; -$diff-image-img-bg: #e5e5e5; $diff-swipe-border: #999; $diff-view-modes-color: grey; $diff-view-modes-border: #c1c1c1; @@ -272,14 +254,12 @@ $regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-San * Dropdowns */ $dropdown-width: 300px; -$dropdown-bg: #fff; $dropdown-link-color: #555; $dropdown-link-hover-bg: $row-hover; $dropdown-empty-row-bg: rgba(#000, .04); $dropdown-border-color: $border-color; $dropdown-shadow-color: rgba(#000, .1); $dropdown-divider-color: rgba(#000, .1); -$dropdown-header-color: #959494; $dropdown-title-btn-color: #bfbfbf; $dropdown-input-color: #555; $dropdown-input-fa-color: #c7c7c7; @@ -287,31 +267,19 @@ $dropdown-input-focus-border: $focus-border-color; $dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4); $dropdown-loading-bg: rgba(#fff, .6); $dropdown-chevron-size: 10px; +$dropdown-toggle-active-border-color: darken($border-color, 14%); -$dropdown-toggle-bg: #fff; -$dropdown-toggle-color: #5c5c5c; -$dropdown-toggle-border-color: #e5e5e5; -$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 13%); -$dropdown-toggle-active-border-color: darken($dropdown-toggle-border-color, 14%); -$dropdown-toggle-icon-color: #c4c4c4; -$dropdown-toggle-hover-icon-color: darken($dropdown-toggle-icon-color, 7%); /* * Buttons */ $btn-active-gray: #ececec; $btn-active-gray-light: e4e7ed; -$btn-placeholder-gray: #c7c7c7; -$btn-white-active: #848484; -$btn-gray-hover: #eee; /* * Award emoji */ -$award-emoji-menu-bg: #fff; -$award-emoji-menu-border: #f1f2f4; $award-emoji-menu-shadow: rgba(0,0,0,.175); -$award-emoji-new-btn-icon-color: #dcdcdc; /* * Search Box @@ -319,22 +287,15 @@ $award-emoji-new-btn-icon-color: #dcdcdc; $search-input-border-color: rgba(#4688f1, .8); $search-input-focus-shadow-color: $dropdown-input-focus-shadow; $search-input-width: 220px; -$location-badge-color: #aaa; -$location-badge-bg: $dark-background-color; $location-badge-active-bg: #4f91f8; $location-icon-color: #e7e9ed; -$location-icon-active-color: #807e7e; /* * Notes */ $notes-light-color: #8e8e8e; -$notes-action-color: #c3c3c3; $notes-role-color: #8e8e8e; -$notes-role-border-color: #e4e4e4; $note-disabled-comment-color: #b2b2b2; -$note-form-border-color: #e5e5e5; -$note-toolbar-color: #959494; $note-targe3-outside: #fffff0; $note-targe3-inside: #ffffd3; $note-line2-border: #ddd; @@ -344,15 +305,12 @@ $note-line2-border: #ddd; * Zen */ $zen-control-color: #555; -$zen-control-hover-color: #111; /* * Calendar */ -$calendar-header-color: #b8b8b8; $calendar-hover-bg: #ecf3fe; $calendar-border-color: rgba(#000, .1); -$calendar-unselectable-bg: $gray-light; $calendar-user-contrib-text: #959494; /* @@ -366,15 +324,8 @@ $cycle-analytics-light-gray: #bfbfbf; $cycle-analytics-dismiss-icon-color: #b2b2b2; /* - * Personal Access Tokens - */ -$personal-access-tokens-disabled-label-color: #bbb; - -/* * CI */ -$ci-output-bg: #1d1f21; -$ci-text-color: #c5c8c6; $ci-skipped-color: #888; /* @@ -470,7 +421,6 @@ $help-shortcut-header-color: #333; /* * Issues */ -$issues-border: #e5e5e5; $issues-today-bg: #f3fff2; $issues-today-border: #e1e8d5; @@ -486,6 +436,7 @@ $jq-ui-default-color: #777; $label-gray-bg: #f8fafc; $label-inverse-bg: #333; $label-remove-border: rgba(0, 0, 0, .1); +$label-border-radius: 14px; /* * Lint diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss index f2860dfe84d..f9c850ebdc8 100644 --- a/app/assets/stylesheets/framework/wells.scss +++ b/app/assets/stylesheets/framework/wells.scss @@ -1,5 +1,5 @@ .info-well { - background: $background-color; + background: $gray-light; color: $gl-gray; border: 1px solid $border-color; border-radius: $border-radius-default; @@ -45,7 +45,7 @@ } .light-well { - background-color: $background-color; + background-color: $gray-light; padding: 15px; } diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index e5c7d70d45a..84b639fabf5 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -40,7 +40,7 @@ } .zen-control-full { - color: $note-toolbar-color; + color: $gl-gray-light; &:hover { color: $gl-link-color; @@ -57,6 +57,6 @@ font-size: 36px; &:hover { - color: $zen-control-hover-color; + color: $black; } } diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 1adab3ffd94..54a5664a874 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -69,14 +69,14 @@ $white-gc-bg: #eaf2f5; @mixin matchLine { color: $black-transparent; - background-color: $match-line; + background-color: $gray-light; } .code.white { // Line numbers .line-numbers, .diff-line-num { - background-color: $background-color; + background-color: $gray-light; } .diff-line-num, @@ -87,7 +87,7 @@ $white-gc-bg: #eaf2f5; // Code itself pre.code, .diff-line-num { - border-color: $table-border-gray; + border-color: $white-normal; } &, diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss index 024b4df6bd0..60ff72c703e 100644 --- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss +++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss @@ -91,9 +91,9 @@ $highlighted-gc-bg: #eaf2f5; padding: 0 5px; text-align: right; width: 35px; - background-color: $background-color; + background-color: $gray-light; color: $black-transparent; - border-right: 1px solid $table-border-gray; + border-right: 1px solid $white-normal; &.old { background-color: $line-number-old; @@ -130,7 +130,7 @@ $highlighted-gc-bg: #eaf2f5; &.match { color: $black-transparent; - background-color: $match-line; + background-color: $gray-light; } } diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 0d9cf679e7c..c735f104c20 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -98,7 +98,7 @@ .board-inner { height: 100%; font-size: $issue-boards-font-size; - background: $background-color; + background: $gray-light; border: 1px solid $border-color; border-radius: $border-radius-default; } @@ -253,7 +253,7 @@ .board-list-count { padding: 10px 0; - color: $gl-placeholder-color; + color: $gl-gray-light; font-size: 13px; > .fa { diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index dcc13f6d74a..66f7e7f97c8 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -42,7 +42,7 @@ } .environment-information { - background-color: $background-color; + background-color: $gray-light; border: 1px solid $border-color; padding: 12px $gl-padding; border-radius: $border-radius-default; @@ -96,8 +96,8 @@ } .build-trace { - background: $ci-output-bg; - color: $ci-text-color; + background: $black; + color: $gray-darkest; white-space: pre; overflow-x: auto; font-size: 12px; @@ -257,7 +257,7 @@ } .build-light-text { - color: $gl-placeholder-color; + color: $gl-gray-light; } .build-gutter-toggle { diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index c29b5fdea78..e76e1a73b25 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -7,7 +7,7 @@ .commit-header { padding: 5px 10px; - background-color: $background-color; + background-color: $gray-light; border-top: 1px solid $gray-darker; border-bottom: 1px solid $gray-darker; font-size: 14px; @@ -38,7 +38,7 @@ .text-expander { display: inline-block; background: $gray-light; - color: $gl-placeholder-color; + color: $gl-gray-light; padding: 0 5px; cursor: pointer; border: 1px solid $border-gray-dark; @@ -117,7 +117,7 @@ .commit-row-description { font-size: 14px; - border-left: 1px solid $btn-gray-hover; + border-left: 1px solid $white-normal; padding: 10px 15px; margin: 10px 0; background: $gray-light; @@ -174,7 +174,7 @@ height: 14px; width: 14px; vertical-align: middle; - fill: $table-text-gray; + fill: $gl-gray-light; } } diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 57146e1fccd..5aef31724e1 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -232,6 +232,7 @@ &:hover:not(.active) { background-color: $gray-lightest; box-shadow: inset 2px 0 0 0 $border-color; + cursor: pointer; } &:first-child { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 737f6e0f4be..f30795fd2c2 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -11,7 +11,7 @@ .diff-header { position: relative; - background: $background-color; + background: $gray-light; border-bottom: 1px solid $border-color; padding: 10px 16px; color: $gl-diff-text-color; @@ -38,7 +38,7 @@ cursor: pointer; &:hover { - background-color: $dark-background-color; + background-color: $gray-normal; } .diff-toggle-caret { @@ -187,8 +187,8 @@ img { border: 1px solid $white-light; - background-image: linear-gradient(45deg, $diff-image-img-bg 25%, transparent 25%, transparent 75%, $diff-image-img-bg 75%, $diff-image-img-bg 100%), - linear-gradient(45deg, $diff-image-img-bg 25%, transparent 25%, transparent 75%, $diff-image-img-bg 75%, $diff-image-img-bg 100%); + background-image: linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%), + linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%); background-size: 10px 10px; background-position: 0 0, 5px 5px; max-width: 100%; diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 6cde9c592de..3fdb4f510fa 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -10,7 +10,7 @@ } .ace_gutter-cell { - background-color: $background-color; + background-color: $gray-light; } .cancel-btn { @@ -34,7 +34,7 @@ } .editor-ref { - background: $background-color; + background: $gray-light; padding-right: $gl-padding; border-right: 1px solid $border-color; display: block; @@ -51,8 +51,16 @@ .new-file-name { display: inline-block; - width: 450px; + max-width: 450px; float: left; + + @media(max-width: $screen-md-max) { + width: 280px; + } + + @media(max-width: $screen-sm-max) { + width: 180px; + } } .file-buttons { @@ -114,3 +122,42 @@ } } } + +@media(max-width: $screen-xs-max){ + .file-editor { + .file-title { + .pull-right { + height: auto; + } + } + + .new-file-name { + max-width: none; + width: 100%; + margin-bottom: 3px; + } + + .file-buttons { + display: block; + width: 100%; + margin-bottom: 10px; + + .soft-wrap-toggle { + width: 100%; + margin: 3px 0; + } + + .encoding-selector, + .license-selector, + .gitignore-selector, + .gitlab-ci-yml-selector { + display: block; + margin: 3px 0; + + button { + width: 100%; + } + } + } + } +} diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index de3d2ba549f..92dd9885ab8 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -1,6 +1,8 @@ -.deployments-container { - width: 100%; - overflow: auto; +@media (max-width: $screen-md-max) { + .deployments-container { + width: 100%; + overflow: auto; + } } .environments-list-loading { @@ -64,14 +66,14 @@ .external-url, .dropdown-new { - color: $table-text-gray; + color: $gl-gray-light; } .dropdown-menu { .fa { margin-right: 6px; - color: $table-text-gray; + color: $gl-gray-light; } } @@ -82,7 +84,7 @@ .stop-env-link, .external-url { - color: $table-text-gray; + color: $gl-gray-light; .stop-env-icon { font-size: 14px; @@ -117,7 +119,7 @@ .badge { font-weight: normal; background-color: $gray-darker; - color: $gl-placeholder-color; + color: $gl-gray-light; vertical-align: baseline; } } diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index dc67d411c71..98925c2d0cb 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -5,7 +5,7 @@ .event-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; + border-bottom: 1px solid $white-normal; color: $list-text-color; &.event-inline { diff --git a/app/assets/stylesheets/pages/explore.scss b/app/assets/stylesheets/pages/explore.scss deleted file mode 100644 index 9b92128624c..00000000000 --- a/app/assets/stylesheets/pages/explore.scss +++ /dev/null @@ -1,8 +0,0 @@ -.explore-title { - text-align: center; - - h3 { - font-weight: normal; - font-size: 30px; - } -} diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index e2e644dc23b..dae8ccdef6c 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -60,7 +60,7 @@ // Border around images in the help pages. img:not(.emoji) { - border: 1px solid $table-border-gray; + border: 1px solid $white-normal; padding: 5px; margin: 5px; max-height: calc(100vh - 100px); diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 90587b9425b..0234f2d49e7 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -7,7 +7,7 @@ // Border around images in issue and MR descriptions. .description img:not(.emoji) { - border: 1px solid $table-border-gray; + border: 1px solid $white-normal; padding: 5px; margin: 5px; max-height: calc(100vh - 100px); @@ -30,6 +30,7 @@ .color-label { padding: 6px 10px; + border-radius: $label-border-radius; } } @@ -50,7 +51,7 @@ .block { @include clearfix; padding: $gl-padding 0; - border-bottom: 1px solid $border-gray-light; + border-bottom: 1px solid $border-gray-normal; // This prevents the mess when resizing the sidebar // of elements repositioning themselves.. width: $gutter_inner_width; @@ -168,7 +169,7 @@ } .no-value { - color: $gl-placeholder-color; + color: $gl-gray-light; } .sidebar-collapsed-icon { @@ -177,7 +178,7 @@ .gutter-toggle { margin-top: 7px; - border-left: 1px solid $border-gray-light; + border-left: 1px solid $border-gray-normal; } .assignee .avatar { @@ -215,7 +216,7 @@ } .participants { - border-bottom: 1px solid $border-gray-light; + border-bottom: 1px solid $border-gray-normal; } .hide-collapsed { @@ -332,7 +333,7 @@ margin-left: 5px; a { - color: $gl-placeholder-color; + color: $gl-gray-light; } } diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 3b47f99df2c..8734a3b1598 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -88,12 +88,12 @@ ul.related-merge-requests > li { &.closed { background: $gray-light; - border-color: $issues-border; + border-color: $border-color; } &.merged { background: $gray-light; - border-color: $issues-border; + border-color: $border-color; } } @@ -144,7 +144,7 @@ ul.related-merge-requests > li { } .btn { - background-color: $background-color; - border: 1px solid $border-gray-light; + background-color: $gray-light; + border: 1px solid $border-gray-normal; } } diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index b1ccd644450..25c91203ff4 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -104,7 +104,8 @@ } .color-label { - padding: 3px 4px; + padding: 3px 7px; + border-radius: $label-border-radius; } .dropdown-labels-error { diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index 7a90713dd3f..5a9f199fb34 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -274,7 +274,7 @@ $colors: ( } .discard-changes-alert { - background-color: $background-color; + background-color: $gray-light; text-align: right; padding: $gl-padding-top $gl-padding; color: $gl-text-color; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 6234779ac19..e779e65eca3 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -3,7 +3,7 @@ * */ .mr-state-widget { - background: $background-color; + background: $gray-light; color: $gl-gray; border: 1px solid $border-color; border-radius: 2px; @@ -359,7 +359,7 @@ th { background-color: $white-light; - color: $gl-placeholder-color; + color: $gl-gray-light; } } } @@ -375,7 +375,7 @@ } .mr-version-controls { - background: $background-color; + background: $gray-light; border-bottom: 1px solid $border-color; color: $gl-text-color; diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index dfc6079bd15..77c523d7310 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -108,7 +108,7 @@ margin-top: 7px; .issuable-number { - color: $gl-placeholder-color; + color: $gl-gray-light; margin-right: 5px; } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index c35d71f9e7b..074abec7692 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -62,7 +62,7 @@ .common-note-form { .md-area { padding: $gl-padding-top $gl-padding; - border: 1px solid $note-form-border-color; + border: 1px solid $border-color; border-radius: $border-radius-base; transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; @@ -204,7 +204,7 @@ .comment-toolbar { padding-top: $gl-padding-top; - color: $note-toolbar-color; + color: $gl-gray-light; border-top: 1px solid $border-color; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 10eb3d4203e..1b83b40486e 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -166,7 +166,7 @@ ul.notes { .note { display: block; position: relative; - border-bottom: 1px solid $table-border-gray; + border-bottom: 1px solid $white-normal; &.note-discussion { &.timeline-entry { @@ -291,14 +291,14 @@ ul.notes { font-family: $regular_font; td { - border: 1px solid $table-border-gray; + border: 1px solid $white-normal; border-left: none; &.notes_line { vertical-align: middle; text-align: center; padding: 10px 0; - background: $background-color; + background: $gray-light; color: $text-color; } @@ -309,7 +309,7 @@ ul.notes { } &.notes_content { - background-color: $background-color; + background-color: $gray-light; border-width: 1px 0; padding: 0; vertical-align: top; @@ -372,7 +372,7 @@ ul.notes { .note-actions { float: right; margin-left: 10px; - color: $notes-action-color; + color: $gray-darkest; } .note-actions { @@ -411,7 +411,7 @@ ul.notes { } .fa { - color: $notes-action-color; + color: $gray-darkest; position: relative; font-size: 17px; } @@ -448,7 +448,7 @@ ul.notes { color: $notes-role-color; font-size: 12px; line-height: 20px; - border: 1px solid $notes-role-border-color; + border: 1px solid $border-color; border-radius: $border-radius-base; } @@ -529,7 +529,7 @@ ul.notes { .line-resolve-all { display: inline-block; padding: 5px 10px; - background-color: $background-color; + background-color: $gray-light; border: 1px solid $border-color; border-radius: $border-radius-default; @@ -573,10 +573,10 @@ ul.notes { svg { position: relative; - color: $notes-action-color; + color: $gray-darkest; path { - fill: $notes-action-color; + fill: $gray-darkest; } } } diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss index 94fbbef3c77..bdf07a99daf 100644 --- a/app/assets/stylesheets/pages/notifications.scss +++ b/app/assets/stylesheets/pages/notifications.scss @@ -1,24 +1,16 @@ .notification-list-item { line-height: 34px; + + .dropdown-menu { + @extend .dropdown-menu-align-right; + } } .notification { position: relative; top: 1px; - > .fa { + .fa { font-size: 18px; } } - -.ns-part { - color: $gl-text-green; -} - -.ns-watch { - color: $gl-success; -} - -.ns-mute { - color: $gl-danger; -} diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 08062b85504..d5f9a7088be 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -37,12 +37,13 @@ } } -.content-list { - - &.pipelines, - &.builds-content-list { - width: 100%; - overflow: auto; +@media (max-width: $screen-md-max) { + .content-list { + &.pipelines, + &.builds-content-list { + width: 100%; + overflow: auto; + } } } @@ -117,7 +118,7 @@ height: 14px; width: 14px; vertical-align: middle; - fill: $table-text-gray; + fill: $gl-gray-light; } .fa { @@ -200,7 +201,7 @@ .duration, .finished-at { - color: $table-text-gray; + color: $gl-gray-light; margin: 4px 0; .fa { @@ -221,7 +222,7 @@ .btn { margin: 0; - color: $table-text-gray; + color: $gl-gray-light; } .cancel-retry-btns { @@ -234,10 +235,10 @@ .dropdown-toggle, .dropdown-menu { - color: $table-text-gray; + color: $gl-gray-light; .fa { - color: $table-text-gray; + color: $gl-gray-light; font-size: 14px; } @@ -289,7 +290,7 @@ // Pipeline visualization .toggle-pipeline-btn { - background-color: $gray-dark; + background-color: $white-normal; &.graph-collapsed { background-color: $white-light; @@ -298,7 +299,7 @@ .pipeline-graph { width: 100%; - background-color: $background-color; + background-color: $gray-light; padding: $gl-padding; overflow: auto; white-space: nowrap; @@ -654,7 +655,7 @@ .toggle-pipeline-btn { .fa { - color: $dropdown-header-color; + color: $gl-gray-light; } } @@ -666,10 +667,6 @@ min-width: 900px; } - .content-list.pipelines { - overflow: auto; - } - .stage { max-width: 100px; width: 100px; diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index f8677f93fe0..8a5b0e20a86 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -60,8 +60,8 @@ .account-well { padding: 10px; - background-color: $help-well-bg; - border: 1px solid $help-well-border; + background-color: $gray-light; + border: 1px solid $border-color; border-radius: $border-radius-base; ul { @@ -136,7 +136,7 @@ .provider-btn-group { display: inline-block; margin-right: 10px; - border: 1px solid $provider-btn-group-border; + border: 1px solid $border-color; border-radius: 3px; &:last-child { @@ -147,7 +147,7 @@ .provider-btn-image { display: inline-block; padding: 5px 10px; - border-right: 1px solid $provider-btn-group-border; + border-right: 1px solid $border-color; > img { width: 20px; @@ -198,7 +198,7 @@ } .personal-access-tokens-never-expires-label { - color: $personal-access-tokens-disabled-label-color; + color: $note-disabled-comment-color; } .datepicker.personal-access-tokens-expires-at .ui-state-disabled span { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 72b6685d940..9c3dbc58ae0 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -188,6 +188,10 @@ margin-left: 10px; } + .notification-dropdown .dropdown-menu { + @extend .dropdown-menu-align-right; + } + .download-button { @media (max-width: $screen-md-max) { margin-left: 0; @@ -416,13 +420,13 @@ a.deploy-project-label { width: 100px; height: 100px; background-color: $gray-light; - border: 1px solid $gray-dark; + border: 1px solid $white-normal; margin: 0 auto; border-radius: 50%; i { font-size: 100px; - color: $gray-dark; + color: $white-normal; } } @@ -532,7 +536,7 @@ a.deploy-project-label { } li.missing { - border: 1px dashed $border-gray-light; + border: 1px dashed $border-gray-normal; border-radius: $border-radius-default; a { @@ -587,7 +591,7 @@ pre.light-well { @include basic-list; .project-row { - border-color: $table-border-color; + border-color: $white-normal; .project-full-name { @include str-truncated; @@ -639,7 +643,7 @@ pre.light-well { &.container-fluid { padding-top: 12px; padding-bottom: 12px; - background-color: $background-color; + background-color: $gray-light; border: 1px solid $border-color; border-right-width: 0; border-left-width: 0; diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 63d0a34e610..cedd4cb2987 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -51,9 +51,9 @@ border-radius: $border-radius-default; font-size: 14px; font-style: normal; - color: $location-badge-color; + color: $note-disabled-comment-color; display: inline-block; - background-color: $location-badge-bg; + background-color: $gray-normal; vertical-align: top; cursor: default; } @@ -140,7 +140,7 @@ .search-input-wrap { i { - color: $location-icon-active-color; + color: $layout-link-gray; } } } diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 51c926608f9..ddee2c95247 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -1,5 +1,5 @@ .settings-list-icon { - color: $gl-placeholder-color; + color: $gl-gray-light; font-size: $settings-icon-size; line-height: 42px; } diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 5084b466722..f3b0608e545 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -102,15 +102,15 @@ &.ci-created, &.ci-skipped { - color: $table-text-gray; - border-color: $table-text-gray; + color: $gl-gray-light; + border-color: $gl-gray-light; &:not(span):hover { - background-color: rgba( $table-text-gray, .07); + background-color: rgba( $gl-gray-light, .07); } svg { - fill: $table-text-gray; + fill: $gl-gray-light; } } } diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 20ad63be045..c0341db7289 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -20,8 +20,8 @@ margin-bottom: 0; tr { - border-bottom: 1px solid $table-border-gray; - border-top: 1px solid $table-border-gray; + border-bottom: 1px solid $white-normal; + border-top: 1px solid $white-normal; td, th { @@ -39,7 +39,7 @@ .commit-history-link-spacer { margin: 0 10px; - color: $table-border-color; + color: $white-normal; } &:hover { @@ -53,7 +53,7 @@ &.selected { td { - background: $gray-dark; + background: $white-normal; border-top: 1px solid $border-gray-dark; border-bottom: 1px solid $border-gray-dark; } @@ -134,7 +134,7 @@ .blob-commit-info { list-style: none; padding: $gl-padding; - background: $background-color; + background: $gray-light; border: 1px solid $border-color; border-bottom: none; margin: 0; diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss index 9f9d630978a..b085c56390d 100644 --- a/app/assets/stylesheets/pages/xterm.scss +++ b/app/assets/stylesheets/pages/xterm.scss @@ -18,7 +18,7 @@ $l-blue: #81a2be; $l-magenta: #b294bb; $l-cyan: #8abeb7; - $l-white: $ci-text-color; + $l-white: $gray-darkest; /* * xterm colors diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index c33d7eecb9f..549a8526715 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -18,7 +18,7 @@ module ServiceParams :add_pusher, :send_from_committer_email, :disable_diffs, :external_wiki_url, :notify, :color, :server_host, :server_port, :default_irc_uri, :enable_ssl_verification, - :jira_issue_transition_id, :url, :project_key] + :jira_issue_transition_id, :url, :project_key, :ca_pem, :namespace] # Parameters to ignore if no value is specified FILTER_BLANK_PARAMS = [:password] diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 15ca080c696..b71509f2c9b 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -217,6 +217,6 @@ class Projects::NotesController < Projects::ApplicationController end def find_current_user_notes - @notes = NotesFinder.new.execute(project, current_user, params) + @notes = NotesFinder.new(project, current_user, params).execute.inc_author end end diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb index 0825a4311cb..2c097cb4d8d 100644 --- a/app/controllers/projects/releases_controller.rb +++ b/app/controllers/projects/releases_controller.rb @@ -10,7 +10,14 @@ class Projects::ReleasesController < Projects::ApplicationController end def update - release.update_attributes(release_params) + # Release belongs to Tag which is not active record object, + # it exists only to save a description to each Tag. + # If description is empty we should destroy the existing record. + if release_params[:description].present? + release.update_attributes(release_params) + else + release.destroy + end redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name) end diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index e290a0eadda..0720be2e55d 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -19,10 +19,12 @@ class Projects::SnippetsController < Projects::ApplicationController respond_to :html def index - @snippets = SnippetsFinder.new.execute(current_user, { + @snippets = SnippetsFinder.new.execute( + current_user, filter: :by_project, - project: @project - }) + project: @project, + scope: params[:scope] + ) @snippets = @snippets.page(params[:page]) end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 38e7c6f4a48..8c698695202 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -37,6 +37,12 @@ class SessionsController < Devise::SessionsController end end + def destroy + super + # hide the signed_out notice + flash[:notice] = nil + end + private # Handle an "initial setup" state, where there's only one user, it's an admin, diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index be00a219205..707eddd4d29 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -23,10 +23,26 @@ class IssuesFinder < IssuableFinder private def init_collection - Issue.visible_to_user(current_user) + IssuesFinder.not_restricted_by_confidentiality(current_user) end def iid_pattern @iid_pattern ||= %r{\A#{Regexp.escape(Issue.reference_prefix)}(?<iid>\d+)\z} end + + def self.not_restricted_by_confidentiality(user) + return Issue.where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank? + + return Issue.all if user.admin? + + Issue.where(' + issues.confidential IS NULL + OR issues.confidential IS FALSE + OR (issues.confidential = TRUE + AND (issues.author_id = :user_id + OR issues.assignee_id = :user_id + OR issues.project_id IN(:project_ids)))', + user_id: user.id, + project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id)) + end end diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index 2484339e3a4..4bd8c83081a 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -1,27 +1,102 @@ class NotesFinder FETCH_OVERLAP = 5.seconds - def execute(project, current_user, params) - target_type = params[:target_type] - target_id = params[:target_id] - # Default to 0 to remain compatible with old clients - last_fetched_at = Time.at(params.fetch(:last_fetched_at, 0).to_i) - - notes = - case target_type - when "commit" - project.notes.for_commit_id(target_id).non_diff_notes - when "issue" - IssuesFinder.new(current_user, project_id: project.id).find(target_id).notes.inc_author - when "merge_request" - MergeRequestsFinder.new(current_user, project_id: project.id).find(target_id).mr_and_commit_notes.inc_author - when "snippet", "project_snippet" - project.snippets.find(target_id).notes + # Used to filter Notes + # When used with target_type and target_id this returns notes specifically for the controller + # + # Arguments: + # current_user - which user check authorizations with + # project - which project to look for notes on + # params: + # target_type: string + # target_id: integer + # last_fetched_at: time + # search: string + # + def initialize(project, current_user, params = {}) + @project = project + @current_user = current_user + @params = params + init_collection + end + + def execute + @notes = since_fetch_at(@params[:last_fetched_at]) if @params[:last_fetched_at] + @notes + end + + private + + def init_collection + if @params[:target_id] + @notes = on_target(@params[:target_type], @params[:target_id]) + else + @notes = notes_of_any_type + end + end + + def notes_of_any_type + types = %w(commit issue merge_request snippet) + note_relations = types.map { |t| notes_for_type(t) } + note_relations.map!{ |notes| search(@params[:search], notes) } if @params[:search] + UnionFinder.new.find_union(note_relations, Note) + end + + def noteables_for_type(noteable_type) + case noteable_type + when "issue" + IssuesFinder.new(@current_user, project_id: @project.id).execute + when "merge_request" + MergeRequestsFinder.new(@current_user, project_id: @project.id).execute + when "snippet", "project_snippet" + SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project) + else + raise 'invalid target_type' + end + end + + def notes_for_type(noteable_type) + if noteable_type == "commit" + if Ability.allowed?(@current_user, :download_code, @project) + @project.notes.where(noteable_type: 'Commit') + else + Note.none + end + else + finder = noteables_for_type(noteable_type) + @project.notes.where(noteable_type: finder.base_class.name, noteable_id: finder.reorder(nil)) + end + end + + def on_target(target_type, target_id) + if target_type == "commit" + notes_for_type('commit').for_commit_id(target_id) + else + target = noteables_for_type(target_type).find(target_id) + + if target.respond_to?(:related_notes) + target.related_notes else - raise 'invalid target_type' + target.notes end + end + end + + # Searches for notes matching the given query. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + def search(query, notes_relation = @notes) + pattern = "%#{query}%" + notes_relation.where(Note.arel_table[:note].matches(pattern)) + end + + # Notes changed since last fetch + # Uses overlapping intervals to avoid worrying about race conditions + def since_fetch_at(fetch_time) + # Default to 0 to remain compatible with old clients + last_fetched_at = Time.at(@params.fetch(:last_fetched_at, 0).to_i) - # Use overlapping intervals to avoid worrying about race conditions - notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh + @notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh end end diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index 00ff1611039..da6e6e87a6f 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -1,14 +1,17 @@ class SnippetsFinder def execute(current_user, params = {}) filter = params[:filter] + user = params.fetch(:user, current_user) case filter when :all then snippets(current_user).fresh + when :public then + Snippet.are_public.fresh when :by_user then - by_user(current_user, params[:user], params[:scope]) + by_user(current_user, user, params[:scope]) when :by_project - by_project(current_user, params[:project]) + by_project(current_user, params[:project], params[:scope]) end end @@ -29,35 +32,35 @@ class SnippetsFinder def by_user(current_user, user, scope) snippets = user.snippets.fresh - return snippets.are_public unless current_user - - if user == current_user - case scope - when 'are_internal' then - snippets.are_internal - when 'are_private' then - snippets.are_private - when 'are_public' then - snippets.are_public - else - snippets - end + if current_user + include_private = user == current_user + by_scope(snippets, scope, include_private) else - snippets.public_and_internal + snippets.are_public end end - def by_project(current_user, project) + def by_project(current_user, project, scope) snippets = project.snippets.fresh if current_user - if project.team.member?(current_user) || current_user.admin? - snippets - else - snippets.public_and_internal - end + include_private = project.team.member?(current_user) || current_user.admin? + by_scope(snippets, scope, include_private) else snippets.are_public end end + + def by_scope(snippets, scope = nil, include_private = false) + case scope.to_s + when 'are_private' + include_private ? snippets.are_private : Snippet.none + when 'are_internal' + snippets.are_internal + when 'are_public' + snippets.are_public + else + include_private ? snippets : snippets.public_and_internal + end + end end diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index dee3c78df47..4c7c16d694c 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -16,7 +16,7 @@ module ButtonHelper # See http://clipboardjs.com/#usage def clipboard_button(data = {}) css_class = data[:class] || 'btn-clipboard btn-transparent' - title = data[:title] || 'Copy to Clipboard' + title = data[:title] || 'Copy to clipboard' data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data) content_tag :button, icon('clipboard'), diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 8e19752a8a1..d9f5e01f0dc 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -4,25 +4,7 @@ module CiStatusHelper builds_namespace_project_commit_path(project.namespace, project, pipeline.sha) end - def ci_status_with_icon(status, target = nil) - content = ci_icon_for_status(status) + ci_text_for_status(status) - klass = "ci-status ci-#{status}" - - if target - link_to content, target, class: klass - else - content_tag :span, content, class: klass - end - end - - def ci_text_for_status(status) - if detailed_status?(status) - status.text - else - status - end - end - + # Is used by Commit and Merge Request Widget def ci_label_for_status(status) if detailed_status?(status) return status.label diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb index 27975b7ddb7..ff8550439d0 100644 --- a/app/helpers/environment_helper.rb +++ b/app/helpers/environment_helper.rb @@ -14,10 +14,12 @@ module EnvironmentHelper end end - def deployment_link(deployment) + def deployment_link(deployment, text: nil) return unless deployment - link_to "##{deployment.iid}", [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable] + link_label = text ? text : "##{deployment.iid}" + + link_to link_label, [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable] end def last_deployment_link_for_environment_build(project, build) diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index af9087d8326..99db73c9ee0 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -159,6 +159,11 @@ module GitlabRoutingHelper resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) end + # Snippets + def personal_snippet_url(snippet, *args) + snippet_url(snippet) + end + # Groups ## Members diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index f6d4ea4659a..77dc9e7d538 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -12,11 +12,18 @@ module GroupsHelper end def group_title(group, name = nil, url = nil) - full_title = link_to(simple_sanitize(group.name), group_path(group)) + full_title = '' + + group.parents.each do |parent| + full_title += link_to(simple_sanitize(parent.name), group_path(parent)) + full_title += ' / '.html_safe + end + + full_title += link_to(simple_sanitize(group.name), group_path(group)) full_title += ' · '.html_safe + link_to(simple_sanitize(name), url) if name content_tag :span do - full_title + full_title.html_safe end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 9cda3b78761..d2177f683a1 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -52,7 +52,7 @@ module ProjectsHelper def project_title(project) namespace_link = if project.group - link_to(simple_sanitize(project.group.name), group_path(project.group)) + group_title(project.group) else owner = project.namespace.owner link_to(simple_sanitize(owner.name), user_path(owner)) @@ -390,7 +390,7 @@ module ProjectsHelper "success" end end - + def readme_cache_key sha = @project.commit.try(:sha) || 'nil' [@project.path_with_namespace, sha, "readme"].join('-') diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 7e33a562077..8c02b4061ca 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -8,6 +8,17 @@ module SnippetsHelper end end + # Return the path of a snippets index for a user or for a project + # + # @returns String, path to snippet index + def subject_snippets_path(subject = nil, opts = nil) + if subject.is_a?(Project) + namespace_project_snippets_path(subject.namespace, subject, opts) + else # assume subject === User + dashboard_snippets_path(opts) + end + end + # Get an array of line numbers surrounding a matching # line, bounded by min/max. # diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 88c46076df6..fdbf28a1d68 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -9,6 +9,14 @@ module Ci has_many :deployments, as: :deployable + # The "environment" field for builds is a String, and is the unexpanded name + def persisted_environment + @persisted_environment ||= Environment.find_by( + name: expanded_environment_name, + project_id: gl_project_id + ) + end + serialize :options serialize :yaml_variables @@ -100,6 +108,12 @@ module Ci end end + def detailed_status(current_user) + Gitlab::Ci::Status::Build::Factory + .new(self, current_user) + .fabricate! + end + def manual? self.when == 'manual' end @@ -123,8 +137,13 @@ module Ci end end + def cancelable? + active? + end + def retryable? - project.builds_enabled? && commands.present? && complete? + project.builds_enabled? && commands.present? && + (success? || failed? || canceled?) end def retried? @@ -132,7 +151,7 @@ module Ci end def expanded_environment_name - ExpandVariables.expand(environment, variables) if environment + ExpandVariables.expand(environment, simple_variables) if environment end def has_environment? @@ -148,7 +167,7 @@ module Ci end def environment_action - self.options.fetch(:environment, {}).fetch(:action, 'start') + self.options.fetch(:environment, {}).fetch(:action, 'start') if self.options end def outdated_deployment? @@ -184,7 +203,19 @@ module Ci project.build_timeout end - def variables + # A slugified version of the build ref, suitable for inclusion in URLs and + # domain names. Rules: + # + # * Lowercased + # * Anything not matching [a-z0-9-] is replaced with a - + # * Maximum length is 63 bytes + def ref_slug + slugified = ref.to_s.downcase + slugified.gsub(/[^a-z0-9]/, '-')[0..62] + end + + # Variables whose value does not depend on other variables + def simple_variables variables = predefined_variables variables += project.predefined_variables variables += pipeline.predefined_variables @@ -197,6 +228,13 @@ module Ci variables end + # All variables, including those dependent on other variables + def variables + variables = simple_variables + variables += persisted_environment.predefined_variables if persisted_environment.present? + variables + end + def merge_request merge_requests = MergeRequest.includes(:merge_request_diff) .where(source_branch: ref, source_project_id: pipeline.gl_project_id) @@ -518,6 +556,7 @@ module Ci { key: 'CI_BUILD_REF', value: sha, public: true }, { key: 'CI_BUILD_BEFORE_SHA', value: before_sha, public: true }, { key: 'CI_BUILD_REF_NAME', value: ref, public: true }, + { key: 'CI_BUILD_REF_SLUG', value: ref_slug, public: true }, { key: 'CI_BUILD_NAME', value: name, public: true }, { key: 'CI_BUILD_STAGE', value: stage, public: true }, { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index fda8228a1e9..48354cdbefb 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -88,8 +88,24 @@ module Ci end # ref can't be HEAD or SHA, can only be branch/tag name + scope :latest, ->(ref = nil) do + max_id = unscope(:select) + .select("max(#{quoted_table_name}.id)") + .group(:ref, :sha) + + if ref + where(id: max_id, ref: ref) + else + where(id: max_id) + end + end + + def self.latest_status(ref = nil) + latest(ref).status + end + def self.latest_successful_for(ref) - where(ref: ref).order(id: :desc).success.first + success.latest(ref).first end def self.truncate_sha(sha) @@ -336,8 +352,10 @@ module Ci .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id } end - def detailed_status - Gitlab::Ci::Status::Pipeline::Factory.new(self).fabricate! + def detailed_status(current_user) + Gitlab::Ci::Status::Pipeline::Factory + .new(self, current_user) + .fabricate! end private diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index d2a37c0a827..7ef59445d77 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -22,8 +22,10 @@ module Ci @status ||= statuses.latest.status end - def detailed_status - Gitlab::Ci::Status::Stage::Factory.new(self).fabricate! + def detailed_status(current_user) + Gitlab::Ci::Status::Stage::Factory + .new(self, current_user) + .fabricate! end def statuses diff --git a/app/models/commit.rb b/app/models/commit.rb index 1831cc7e175..69cfc47f5bf 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -228,13 +228,9 @@ class Commit def status(ref = nil) @statuses ||= {} - if @statuses.key?(ref) - @statuses[ref] - elsif ref - @statuses[ref] = pipelines.where(ref: ref).status - else - @statuses[ref] = pipelines.status - end + return @statuses[ref] if @statuses.key?(ref) + + @statuses[ref] = pipelines.latest_status(ref) end def revert_branch_name @@ -270,7 +266,7 @@ class Commit @merged_merge_request_hash ||= Hash.new do |hash, user| hash[user] = merged_merge_request_no_cache(user) end - + @merged_merge_request_hash[current_user] end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index cf90475f4d4..31cd381dcd2 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -131,4 +131,10 @@ class CommitStatus < ActiveRecord::Base def has_trace? false end + + def detailed_status(current_user) + Gitlab::Ci::Status::Factory + .new(self, current_user) + .fabricate! + end end diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index 875e9834487..4359f1d7b06 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -30,7 +30,7 @@ module Milestoneish end def issues_visible_to_user(user) - issues.visible_to_user(user) + IssuesFinder.new(user).execute.where(id: issues) end def upcoming? diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index d36bb9da296..1108a64c59e 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -7,6 +7,7 @@ module Routable has_one :route, as: :source, autosave: true, dependent: :destroy validates_associated :route + validates :route, presence: true before_validation :update_route_path, if: :full_path_changed? end @@ -28,17 +29,17 @@ module Routable order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)" - where_paths_in([path]).reorder(order_sql).take + where_full_path_in([path]).reorder(order_sql).take end # Builds a relation to find multiple objects by their full paths. # # Usage: # - # Klass.where_paths_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee}) + # Klass.where_full_path_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee}) # # Returns an ActiveRecord::Relation. - def where_paths_in(paths) + def where_full_path_in(paths) wheres = [] cast_lower = Gitlab::Database.postgresql? diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index 04d30f46210..1ca7f91dc03 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -39,6 +39,10 @@ module TokenAuthenticatable current_token.blank? ? write_new_token(token_field) : current_token end + define_method("set_#{token_field}") do |token| + write_attribute(token_field, token) if token + end + define_method("ensure_#{token_field}!") do send("reset_#{token_field}!") if read_attribute(token_field).blank? read_attribute(token_field) diff --git a/app/models/environment.rb b/app/models/environment.rb index 96700143ddd..8ef1c841ea3 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -1,9 +1,15 @@ class Environment < ActiveRecord::Base + # Used to generate random suffixes for the slug + NUMBERS = '0'..'9' + SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a + belongs_to :project, required: true, validate: true has_many :deployments before_validation :nullify_external_url + before_validation :generate_slug, if: ->(env) { env.slug.blank? } + before_save :set_environment_type validates :name, @@ -13,6 +19,13 @@ class Environment < ActiveRecord::Base format: { with: Gitlab::Regex.environment_name_regex, message: Gitlab::Regex.environment_name_regex_message } + validates :slug, + presence: true, + uniqueness: { scope: :project_id }, + length: { maximum: 24 }, + format: { with: Gitlab::Regex.environment_slug_regex, + message: Gitlab::Regex.environment_slug_regex_message } + validates :external_url, uniqueness: { scope: :project_id }, length: { maximum: 255 }, @@ -37,6 +50,13 @@ class Environment < ActiveRecord::Base state :stopped end + def predefined_variables + [ + { key: 'CI_ENVIRONMENT_NAME', value: name, public: true }, + { key: 'CI_ENVIRONMENT_SLUG', value: slug, public: true }, + ] + end + def recently_updated_on_branch?(ref) ref.to_s == last_deployment.try(:ref) end @@ -107,4 +127,41 @@ class Environment < ActiveRecord::Base action.expanded_environment_name == environment end end + + # An environment name is not necessarily suitable for use in URLs, DNS + # or other third-party contexts, so provide a slugified version. A slug has + # the following properties: + # * contains only lowercase letters (a-z), numbers (0-9), and '-' + # * begins with a letter + # * has a maximum length of 24 bytes (OpenShift limitation) + # * cannot end with `-` + def generate_slug + # Lowercase letters and numbers only + slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-') + + # Must start with a letter + slugified = "env-" + slugified if NUMBERS.cover?(slugified[0]) + + # Maximum length: 24 characters (OpenShift limitation) + slugified = slugified[0..23] + + # Cannot end with a "-" character (Kubernetes label limitation) + slugified = slugified[0..-2] if slugified[-1] == "-" + + # Add a random suffix, shortening the current string if necessary, if it + # has been slugified. This ensures uniqueness. + slugified = slugified[0..16] + "-" + random_suffix if slugified != name + + self.slug = slugified + end + + private + + # Slugifying a name may remove the uniqueness guarantee afforded by it being + # based on name (which must be unique). To compensate, we add a random + # 6-byte suffix in those circumstances. This is not *guaranteed* uniqueness, + # but the chance of collisions is vanishingly small + def random_suffix + (0..5).map { SUFFIX_CHARS.sample }.join + end end diff --git a/app/models/group.rb b/app/models/group.rb index 4248e1162d8..ac8a82c8c1e 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -83,7 +83,7 @@ class Group < Namespace end def human_name - name + full_name end def visibility_level_field diff --git a/app/models/issue.rb b/app/models/issue.rb index 7fe92051037..738c96e4db3 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -60,61 +60,6 @@ class Issue < ActiveRecord::Base attributes end - class << self - private - - # Returns the project that the current scope belongs to if any, nil otherwise. - # - # Examples: - # - my_project.issues.without_due_date.owner_project => my_project - # - Issue.all.owner_project => nil - def owner_project - # No owner if we're not being called from an association - return unless all.respond_to?(:proxy_association) - - owner = all.proxy_association.owner - - # Check if the association is or belongs to a project - if owner.is_a?(Project) - owner - else - begin - owner.association(:project).target - rescue ActiveRecord::AssociationNotFoundError - nil - end - end - end - end - - def self.visible_to_user(user) - return where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank? - return all if user.admin? - - # Check if we are scoped to a specific project's issues - if owner_project - if owner_project.team.member?(user, Gitlab::Access::REPORTER) - # If the project is authorized for the user, they can see all issues in the project - return all - else - # else only non confidential and authored/assigned to them - return where('issues.confidential IS NULL OR issues.confidential IS FALSE - OR issues.author_id = :user_id OR issues.assignee_id = :user_id', - user_id: user.id) - end - end - - where(' - issues.confidential IS NULL - OR issues.confidential IS FALSE - OR (issues.confidential = TRUE - AND (issues.author_id = :user_id - OR issues.assignee_id = :user_id - OR issues.project_id IN(:project_ids)))', - user_id: user.id, - project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id)) - end - def self.reference_prefix '#' end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index ea3cf1cdaac..b73d7acefea 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -452,7 +452,7 @@ class MergeRequest < ActiveRecord::Base should_remove_source_branch? || force_remove_source_branch? end - def mr_and_commit_notes + def related_notes # Fetch comments only from last 100 commits commits_for_notes_limit = 100 commit_ids = commits.last(commits_for_notes_limit).map(&:id) @@ -468,7 +468,7 @@ class MergeRequest < ActiveRecord::Base end def discussions - @discussions ||= self.mr_and_commit_notes. + @discussions ||= self.related_notes. inc_relations_for_view. fresh. discussions diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 37374044551..fd42f2328d8 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -17,14 +17,13 @@ class Namespace < ActiveRecord::Base validates :owner, presence: true, unless: ->(n) { n.type == "Group" } validates :name, presence: true, - uniqueness: true, + uniqueness: { scope: :parent_id }, length: { maximum: 255 }, namespace_name: true validates :description, length: { maximum: 255 } validates :path, presence: true, - uniqueness: { case_sensitive: false }, length: { maximum: 255 }, namespace: true @@ -162,6 +161,19 @@ class Namespace < ActiveRecord::Base end end + def full_name + @full_name ||= + if parent + parent.full_name + ' / ' + name + else + name + end + end + + def parents + @parents ||= parent ? parent.parents + [parent] : [] + end + private def repository_storage_paths diff --git a/app/models/note.rb b/app/models/note.rb index 08bd08743ef..0c1b05dabf2 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -107,23 +107,6 @@ class Note < ActiveRecord::Base Discussion.for_diff_notes(active_notes). map { |d| [d.line_code, d] }.to_h end - - # Searches for notes matching the given query. - # - # This method uses ILIKE on PostgreSQL and LIKE on MySQL. - # - # query - The search query as a String. - # as_user - Limit results to those viewable by a specific user - # - # Returns an ActiveRecord::Relation. - def search(query, as_user: nil) - table = arel_table - pattern = "%#{query}%" - - Note.joins('LEFT JOIN issues ON issues.id = noteable_id'). - where(table[:note].matches(pattern)). - merge(Issue.visible_to_user(as_user)) - end end def cross_reference? diff --git a/app/models/project.rb b/app/models/project.rb index 77d740081c6..2c726cfc5df 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -106,6 +106,7 @@ class Project < ActiveRecord::Base has_one :bugzilla_service, dependent: :destroy has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project has_one :external_wiki_service, dependent: :destroy + has_one :kubernetes_service, dependent: :destroy, inverse_of: :project has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_from_project, through: :forked_project_link @@ -742,6 +743,14 @@ class Project < ActiveRecord::Base @ci_service ||= ci_services.reorder(nil).find_by(active: true) end + def deployment_services + services.where(category: :deployment) + end + + def deployment_service + @deployment_service ||= deployment_services.reorder(nil).find_by(active: true) + end + def jira_tracker? issues_tracker.to_param == 'jira' end diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index 86a06321e21..fe6d7aabb22 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -3,7 +3,8 @@ require "addressable/uri" class BuildkiteService < CiService ENDPOINT = "https://buildkite.com" - prop_accessor :project_url, :token, :enable_ssl_verification + prop_accessor :project_url, :token + boolean_accessor :enable_ssl_verification validates :project_url, presence: true, url: true, if: :activated? validates :token, presence: true, if: :activated? diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb new file mode 100644 index 00000000000..55e98c31251 --- /dev/null +++ b/app/models/project_services/deployment_service.rb @@ -0,0 +1,11 @@ +# Base class for deployment services +# +# These services integrate with a deployment solution like Kubernetes/OpenShift, +# Mesosphere, etc, to provide additional features to environments. +class DeploymentService < Service + default_value_for :category, 'deployment' + + def supported_events + [] + end +end diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index 5e4dd101c53..adc78a427ee 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -1,5 +1,6 @@ class DroneCiService < CiService - prop_accessor :drone_url, :token, :enable_ssl_verification + prop_accessor :drone_url, :token + boolean_accessor :enable_ssl_verification validates :drone_url, presence: true, url: true, if: :activated? validates :token, presence: true, if: :activated? diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index e0083c43adb..79285cbd26d 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -1,6 +1,6 @@ class EmailsOnPushService < Service - prop_accessor :send_from_committer_email - prop_accessor :disable_diffs + boolean_accessor :send_from_committer_email + boolean_accessor :disable_diffs prop_accessor :recipients validates :recipients, presence: true, if: :activated? @@ -24,20 +24,20 @@ class EmailsOnPushService < Service return unless supported_events.include?(push_data[:object_kind]) EmailsOnPushWorker.perform_async( - project_id, - recipients, - push_data, - send_from_committer_email: send_from_committer_email?, - disable_diffs: disable_diffs? + project_id, + recipients, + push_data, + send_from_committer_email: send_from_committer_email?, + disable_diffs: disable_diffs? ) end def send_from_committer_email? - self.send_from_committer_email == "1" + Gitlab::Utils.to_boolean(self.send_from_committer_email) end def disable_diffs? - self.disable_diffs == "1" + Gitlab::Utils.to_boolean(self.disable_diffs) end def fields diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 660a8ae3421..915f6fed74c 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -8,8 +8,8 @@ class HipchatService < Service ul ol li dl dt dd ] - prop_accessor :token, :room, :server, :notify, :color, :api_version - boolean_accessor :notify_only_broken_builds + prop_accessor :token, :room, :server, :color, :api_version + boolean_accessor :notify_only_broken_builds, :notify validates :token, presence: true, if: :activated? def initialize_properties @@ -75,7 +75,7 @@ class HipchatService < Service end def message_options(data = nil) - { notify: notify.present? && notify == '1', color: message_color(data) } + { notify: notify.present? && Gitlab::Utils.to_boolean(notify), color: message_color(data) } end def create_message(data) diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index ce7d1c5d5b1..7355918feab 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -2,7 +2,8 @@ require 'uri' class IrkerService < Service prop_accessor :server_host, :server_port, :default_irc_uri - prop_accessor :colorize_messages, :recipients, :channels + prop_accessor :recipients, :channels + boolean_accessor :colorize_messages validates :recipients, presence: true, if: :activated? before_validation :get_channels diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 894315a8593..2d969d2fcb6 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -220,7 +220,7 @@ class JiraService < IssueTrackerService entity_title = data[:entity][:title] project_name = data[:project][:name] - message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'" + message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title.chomp}'" link_title = "GitLab: Mentioned on #{entity_name} - #{entity_title}" link_props = build_remote_link_props(url: entity_url, title: link_title) diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb new file mode 100644 index 00000000000..80ae1191108 --- /dev/null +++ b/app/models/project_services/kubernetes_service.rb @@ -0,0 +1,118 @@ +class KubernetesService < DeploymentService + # Namespace defaults to the project path, but can be overridden in case that + # is an invalid or inappropriate name + prop_accessor :namespace + + # Access to kubernetes is directly through the API + prop_accessor :api_url + + # Bearer authentication + # TODO: user/password auth, client certificates + prop_accessor :token + + # Provide a custom CA bundle for self-signed deployments + prop_accessor :ca_pem + + with_options presence: true, if: :activated? do + validates :api_url, url: true + validates :token + + validates :namespace, + format: { + with: Gitlab::Regex.kubernetes_namespace_regex, + message: Gitlab::Regex.kubernetes_namespace_regex_message, + }, + length: 1..63 + end + + def initialize_properties + if properties.nil? + self.properties = {} + self.namespace = project.path if project.present? + end + end + + def title + 'Kubernetes' + end + + def description + 'Kubernetes / Openshift integration' + end + + def help + '' + end + + def to_param + 'kubernetes' + end + + def fields + [ + { type: 'text', + name: 'namespace', + title: 'Kubernetes namespace', + placeholder: 'Kubernetes namespace', + }, + { type: 'text', + name: 'api_url', + title: 'API URL', + placeholder: 'Kubernetes API URL, like https://kube.example.com/', + }, + { type: 'text', + name: 'token', + title: 'Service token', + placeholder: 'Service token', + }, + { type: 'textarea', + name: 'ca_pem', + title: 'Custom CA bundle', + placeholder: 'Certificate Authority bundle (PEM format)', + }, + ] + end + + # Check we can connect to the Kubernetes API + def test(*args) + kubeclient = build_kubeclient + kubeclient.discover + + { success: kubeclient.discovered, result: "Checked API discovery endpoint" } + rescue => err + { success: false, result: err } + end + + private + + def build_kubeclient(api_path = '/api', api_version = 'v1') + return nil unless api_url && namespace && token + + url = URI.parse(api_url) + url.path = url.path[0..-2] if url.path[-1] == "/" + url.path += api_path + + ::Kubeclient::Client.new( + url, + api_version, + ssl_options: kubeclient_ssl_options, + auth_options: kubeclient_auth_options, + http_proxy_uri: ENV['http_proxy'] + ) + end + + def kubeclient_ssl_options + opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } + + if ca_pem.present? + opts[:cert_store] = OpenSSL::X509::Store.new + opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) + end + + opts + end + + def kubeclient_auth_options + { bearer_token: token } + end +end diff --git a/app/models/project_services/slack_service/pipeline_message.rb b/app/models/project_services/slack_service/pipeline_message.rb index f8d03c0e2fa..b6355fc4171 100644 --- a/app/models/project_services/slack_service/pipeline_message.rb +++ b/app/models/project_services/slack_service/pipeline_message.rb @@ -13,7 +13,7 @@ class SlackService @project_name = data[:project][:path_with_namespace] @project_url = data[:project][:web_url] - @user_name = data[:user] && data[:user][:name] + @user_name = (data[:user] && data[:user][:name]) || 'API' end def pretext diff --git a/app/models/service.rb b/app/models/service.rb index 0c36acfc1b7..e49a8fa2904 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -214,6 +214,7 @@ class Service < ActiveRecord::Base hipchat irker jira + kubernetes mattermost_slash_commands pipelines_email pivotaltracker diff --git a/app/models/user.rb b/app/models/user.rb index b9bb4a9e3f7..1bd28203523 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -304,10 +304,6 @@ class User < ActiveRecord::Base personal_access_token.user if personal_access_token end - def by_username_or_id(name_or_id) - find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i) - end - # Returns a user for the given SSH key. def find_by_ssh_key_id(key_id) find_by(id: Key.unscoped.select(:user_id).where(id: key_id)) diff --git a/app/policies/group_member_policy.rb b/app/policies/group_member_policy.rb index 62335527654..5a3fe814b77 100644 --- a/app/policies/group_member_policy.rb +++ b/app/policies/group_member_policy.rb @@ -15,5 +15,11 @@ class GroupMemberPolicy < BasePolicy elsif @user == target_user can! :destroy_group_member end + + additional_rules! + end + + def additional_rules! + # This is meant to be overriden in EE end end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index b65fb68cd88..6f943feb2a7 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -33,6 +33,8 @@ class GroupPolicy < BasePolicy if globally_viewable && @subject.request_access_enabled && !member can! :request_access end + + additional_rules!(master) end def can_read_group? @@ -43,4 +45,8 @@ class GroupPolicy < BasePolicy GroupProjectsFinder.new(@subject).execute(@user).any? end + + def additional_rules!(master) + # This is meant to be overriden in EE + end end diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb index 46c5aa1a5be..d3913986cd8 100644 --- a/app/policies/personal_snippet_policy.rb +++ b/app/policies/personal_snippet_policy.rb @@ -6,9 +6,14 @@ class PersonalSnippetPolicy < BasePolicy if @subject.author == @user can! :read_personal_snippet can! :update_personal_snippet + can! :destroy_personal_snippet can! :admin_personal_snippet end + unless @user.external? + can! :create_personal_snippet + end + if @subject.internal? && !@user.external? can! :read_personal_snippet end diff --git a/app/services/ci/create_pipeline_builds_service.rb b/app/services/ci/create_pipeline_builds_service.rb index 005014fa1de..b7da3f8e7eb 100644 --- a/app/services/ci/create_pipeline_builds_service.rb +++ b/app/services/ci/create_pipeline_builds_service.rb @@ -10,18 +10,29 @@ module Ci end end + def project + pipeline.project + end + private def create_build(build_attributes) build_attributes = build_attributes.merge( pipeline: pipeline, - project: pipeline.project, + project: project, ref: pipeline.ref, tag: pipeline.tag, user: current_user, trigger_request: trigger_request ) - pipeline.builds.create(build_attributes) + build = pipeline.builds.create(build_attributes) + + # Create the environment before the build starts. This sets its slug and + # makes it available as an environment variable + project.environments.find_or_create_by(name: build.expanded_environment_name) if + build.has_environment? + + build end def new_builds diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb index 75d847d5bee..240ddabec36 100644 --- a/app/services/ci/image_for_build_service.rb +++ b/app/services/ci/image_for_build_service.rb @@ -1,13 +1,13 @@ module Ci class ImageForBuildService def execute(project, opts) - sha = opts[:sha] || ref_sha(project, opts[:ref]) - + ref = opts[:ref] + sha = opts[:sha] || ref_sha(project, ref) pipelines = project.pipelines.where(sha: sha) - pipelines = pipelines.where(ref: opts[:ref]) if opts[:ref] - image_name = image_for_status(pipelines.status) + image_name = image_for_status(pipelines.latest_status(ref)) image_path = Rails.root.join('public/ci', image_name) + OpenStruct.new(path: image_path, name: image_name) end diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index db5f2bf9b2e..4d410f66c55 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -35,7 +35,7 @@ module Commits success else error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically. - It may have already been #{action.to_s.dasherize}, or a more recent commit may have updated some of its content." + A #{action.to_s.dasherize} may have already been performed with this #{@commit.change_type_title(current_user)}, or a more recent commit may have updated some of its content." raise ChangeError, error_msg end end diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb index b6c52ddac7a..86f317dcd18 100644 --- a/app/uploaders/artifact_uploader.rb +++ b/app/uploaders/artifact_uploader.rb @@ -1,4 +1,4 @@ -class ArtifactUploader < CarrierWave::Uploader::Base +class ArtifactUploader < GitlabUploader storage :file attr_accessor :build, :field @@ -38,12 +38,4 @@ class ArtifactUploader < CarrierWave::Uploader::Base def exists? file.try(:exists?) end - - def move_to_cache - true - end - - def move_to_store - true - end end diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb index fb3b5dfecd0..cfcb877cc3e 100644 --- a/app/uploaders/attachment_uploader.rb +++ b/app/uploaders/attachment_uploader.rb @@ -1,4 +1,4 @@ -class AttachmentUploader < CarrierWave::Uploader::Base +class AttachmentUploader < GitlabUploader include UploaderHelper storage :file diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb index 38683fdf6d7..a1ecb7bc00b 100644 --- a/app/uploaders/avatar_uploader.rb +++ b/app/uploaders/avatar_uploader.rb @@ -1,4 +1,4 @@ -class AvatarUploader < CarrierWave::Uploader::Base +class AvatarUploader < GitlabUploader include UploaderHelper storage :file diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 3ac6030c21c..47bef7cd1e4 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -1,4 +1,4 @@ -class FileUploader < CarrierWave::Uploader::Base +class FileUploader < GitlabUploader include UploaderHelper MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)} diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb new file mode 100644 index 00000000000..02d7c601d6c --- /dev/null +++ b/app/uploaders/gitlab_uploader.rb @@ -0,0 +1,11 @@ +class GitlabUploader < CarrierWave::Uploader::Base + # Reduce disk IO + def move_to_cache + true + end + + # Reduce disk IO + def move_to_store + true + end +end diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb index 4f356dd663e..faab539b8e0 100644 --- a/app/uploaders/lfs_object_uploader.rb +++ b/app/uploaders/lfs_object_uploader.rb @@ -1,4 +1,4 @@ -class LfsObjectUploader < CarrierWave::Uploader::Base +class LfsObjectUploader < GitlabUploader storage :file def store_dir @@ -9,14 +9,6 @@ class LfsObjectUploader < CarrierWave::Uploader::Base "#{Gitlab.config.lfs.storage_path}/tmp/cache" end - def move_to_cache - true - end - - def move_to_store - true - end - def exists? file.try(:exists?) end diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml index ec40391a3e3..b5f96363230 100644 --- a/app/views/admin/dashboard/_head.html.haml +++ b/app/views/admin/dashboard/_head.html.haml @@ -8,7 +8,7 @@ %span Overview = nav_link(controller: [:admin, :projects]) do - = link_to admin_namespaces_projects_path, title: 'Projects' do + = link_to admin_projects_path, title: 'Projects' do %span Projects = nav_link(controller: :users) do diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index e51f4ac1d93..5238623e936 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -116,7 +116,7 @@ .light-well.well-centered %h4 Projects .data - = link_to admin_namespaces_projects_path do + = link_to admin_projects_path do %h1= number_with_delimiter(Project.cached_count) %hr = link_to('New Project', new_project_path, class: "btn btn-new") diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml index 664bb417c6a..cf28f92853e 100644 --- a/app/views/admin/groups/_group.html.haml +++ b/app/views/admin/groups/_group.html.haml @@ -2,7 +2,7 @@ %li.group-row{ class: css_class } .controls - = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn' + = link_to 'Edit', admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn' = link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove' .stats %span @@ -20,7 +20,7 @@ = image_tag group_icon(group), class: "avatar s40 hidden-xs" .title = link_to [:admin, group], class: 'group-name' do - = group.name + = group.full_name - if group.description.present? .description diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 40871e32913..7b0175af214 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -1,8 +1,8 @@ - page_title @group.name, "Groups" %h3.page-title - Group: #{@group.name} + Group: #{@group.full_name} - = link_to edit_admin_group_path(@group), class: "btn pull-right" do + = link_to admin_group_edit_path(@group), class: "btn pull-right" do %i.fa.fa-pencil-square-o Edit %hr @@ -88,7 +88,7 @@ Read more about project permissions %strong= link_to "here", help_page_path("user/permissions"), class: "vlink" - = form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do + = form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do %div = users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all) %div.prepend-top-10 diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index 824edd171f3..0a954c20fcd 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -8,7 +8,7 @@ %div{ class: container_class } %ul.nav-links.log-tabs - loggers.each do |klass| - %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') } + %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }> = link_to klass::file_name, "##{klass::file_name_noext}", 'data-toggle' => 'tab' .row-content-block diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index b37b8d4fee7..8bc7dc7dd51 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -7,7 +7,7 @@ %div{ class: container_class } .top-area .prepend-top-default - = form_tag admin_namespaces_projects_path, method: :get do |f| + = form_tag admin_projects_path, method: :get do |f| .search-holder .search-field-holder = search_field_tag :name, params[:name], class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false, placeholder: 'Search by name' @@ -41,19 +41,19 @@ = button_tag "Search", class: "btn btn-primary btn-search" %ul.nav-links - - opts = params[:visibility_level].present? ? {} : { page: admin_namespaces_projects_path } + - opts = params[:visibility_level].present? ? {} : { page: admin_projects_path } = nav_link(opts) do - = link_to admin_namespaces_projects_path do + = link_to admin_projects_path do All = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s ? 'active' : '' }) do - = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do + = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do Private = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s ? 'active' : '' }) do - = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do + = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do Internal = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s ? 'active' : '' }) do - = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do + = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do Public .nav-controls diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 73038164056..ca503e35623 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -91,7 +91,7 @@ %strong ##{build.id} %td.status - = ci_status_with_icon(build.status) + = render 'ci/status/badge', status: build.detailed_status(current_user) %td.status - if project diff --git a/app/views/ci/status/_badge.html.haml b/app/views/ci/status/_badge.html.haml new file mode 100644 index 00000000000..f2135af2686 --- /dev/null +++ b/app/views/ci/status/_badge.html.haml @@ -0,0 +1,10 @@ +- status = local_assigns.fetch(:status) + +- if status.has_details? + = link_to status.details_path, class: "ci-status ci-#{status}" do + = custom_icon(status.icon) + = status.text +- else + %span{ class: "ci-status ci-#{status}" } + = custom_icon(status.icon) + = status.text diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml index b78e70ebc1e..02b94beee92 100644 --- a/app/views/dashboard/_activity_head.html.haml +++ b/app/views/dashboard/_activity_head.html.haml @@ -1,7 +1,7 @@ %ul.nav-links - %li{ class: ("active" unless params[:filter]) } + %li{ class: ("active" unless params[:filter]) }> = link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do Your Projects - %li{ class: ("active" if params[:filter] == 'starred') } + %li{ class: ("active" if params[:filter] == 'starred') }> = link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do Starred Projects diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml index b25e8ea1f0c..02e90bbfa55 100644 --- a/app/views/dashboard/_snippets_head.html.haml +++ b/app/views/dashboard/_snippets_head.html.haml @@ -1,7 +1,13 @@ -%ul.nav-links - = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do - = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do - Your Snippets - = nav_link(page: explore_snippets_path) do - = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do - Explore Snippets +.top-area + %ul.nav-links + = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do + = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do + Your Snippets + = nav_link(page: explore_snippets_path) do + = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do + Explore Snippets + + - if current_user + .nav-controls.hidden-xs + = link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do + New snippet diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index b2af438ea57..85cbe0bf0e6 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -2,41 +2,11 @@ - header_title "Snippets", dashboard_snippets_path = render 'dashboard/snippets_head' += render partial: 'snippets/snippets_scope_menu', locals: { include_private: true } -.nav-block - .controls.hidden-xs - = link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do - = icon('plus') - New snippet +.visible-xs + + = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do + New snippet - .nav-links.snippet-scope-menu - %li{ class: ("active" unless params[:scope]) } - = link_to dashboard_snippets_path do - All - %span.badge - = current_user.snippets.count - - %li{ class: ("active" if params[:scope] == "are_private") } - = link_to dashboard_snippets_path(scope: 'are_private') do - Private - %span.badge - = current_user.snippets.are_private.count - - %li{ class: ("active" if params[:scope] == "are_internal") } - = link_to dashboard_snippets_path(scope: 'are_internal') do - Internal - %span.badge - = current_user.snippets.are_internal.count - - %li{ class: ("active" if params[:scope] == "are_public") } - = link_to dashboard_snippets_path(scope: 'are_public') do - Public - %span.badge - = current_user.snippets.are_public.count - - .visible-xs - = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do - = icon('plus') - New snippet - -= render 'snippets/snippets' += render partial: 'snippets/snippets', locals: { link_project: true } diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 62f52086be4..e13f404fee2 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -5,14 +5,14 @@ .top-area %ul.nav-links - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending') - %li{class: "todos-pending #{todo_pending_active}"} + %li{class: "todos-pending #{todo_pending_active}"}> = link_to todos_filter_path(state: 'pending') do %span To do %span.badge = number_with_delimiter(todos_pending_count) - todo_done_active = ('active' if params[:state] == 'done') - %li{class: "todos-done #{todo_done_active}"} + %li{class: "todos-done #{todo_done_active}"}> = link_to todos_filter_path(state: 'done') do %span Done @@ -32,7 +32,7 @@ - if params[:project_id].present? = hidden_field_tag(:project_id, params[:project_id]) = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit', - placeholder: 'Search projects', data: { data: todo_projects_options } }) + placeholder: 'Search projects', data: { data: todo_projects_options, default_label: 'Project' } }) .filter-item.inline - if params[:author_id].present? = hidden_field_tag(:author_id, params[:author_id]) @@ -42,12 +42,12 @@ - if params[:type].present? = hidden_field_tag(:type, params[:type]) = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit', - data: { data: todo_types_options } }) + data: { data: todo_types_options, default_label: 'Type' } }) .filter-item.inline.actions-filter - if params[:action_id].present? = hidden_field_tag(:action_id, params[:action_id]) = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit', - data: { data: todo_actions_options }}) + data: { data: todo_actions_options, default_label: 'Action' } }) .pull-right .dropdown.inline.prepend-left-10 %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'} diff --git a/app/views/explore/_head.html.haml b/app/views/explore/_head.html.haml index d8a57560788..a3b0709e261 100644 --- a/app/views/explore/_head.html.haml +++ b/app/views/explore/_head.html.haml @@ -1,5 +1,5 @@ -.explore-title - %h3 +.explore-title.text-center + %h2 Explore GitLab %p.lead Discover projects, groups and snippets. Share your projects with others diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml index 7def9eacdc9..e5706d04736 100644 --- a/app/views/explore/snippets/index.html.haml +++ b/app/views/explore/snippets/index.html.haml @@ -6,12 +6,4 @@ - else = render 'explore/head' -.row-content-block - - if current_user - = link_to new_snippet_path, class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do - New snippet - - .oneline - Public snippets created by you and other users are listed here - -= render 'snippets/snippets' += render partial: 'snippets/snippets', locals: { link_project: true } diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 324a116a50e..b4aa4f24d9e 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -6,13 +6,13 @@ - if group_issues(@group).exists? .top-area = render 'shared/issuable/nav', type: :issues - .nav-controls - - if current_user + - if current_user + .nav-controls = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do = icon('rss') %span.icon-label Subscribe - = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" + = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" = render 'shared/issuable/filter', type: :issues diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index e6953d94531..dbbdb583a24 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -2,8 +2,9 @@ .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" + - if current_user + .nav-controls + = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request" = render 'shared/issuable/filter', type: :merge_requests diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 844fce59704..d79a1a9f368 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -30,7 +30,7 @@ %br .clearfix .form-group.pull-left.global-notification-setting - = render 'shared/notifications/button', notification_setting: @global_notification_setting, left_align: true + = render 'shared/notifications/button', notification_setting: @global_notification_setting .clearfix diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index f6aa20c4579..057a720a54a 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -1,6 +1,6 @@ .content-block.build-header .header-content - = ci_status_with_icon(@build.status) + = render 'ci/status/badge', status: @build.detailed_status(current_user) Build %strong ##{@build.id} in pipeline diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 108674dbba6..cdeb81372ee 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -46,8 +46,7 @@ - else This build is creating a deployment to #{environment_link_for_build(@build.project, @build)} - if environment.try(:last_deployment) - and will overwrite the - = link_to 'latest deployment', deployment_link(environment.last_deployment) + and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')} .prepend-top-default - if @build.erased? diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 18b3b04154f..f1cb0201032 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -9,10 +9,7 @@ %tr.build.commit{class: ('retried' if retried)} %td.status - - if can?(current_user, :read_build, build) - = ci_status_with_icon(build.status, namespace_project_build_url(build.project.namespace, build.project, build)) - - else - = ci_status_with_icon(build.status) + = render "ci/status/badge", status: build.detailed_status(current_user) %td.branch-commit - if can?(current_user, :read_build, build) diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml index 423a1282eb2..ad1a7360a8b 100644 --- a/app/views/projects/ci/builds/_build_pipeline.html.haml +++ b/app/views/projects/ci/builds/_build_pipeline.html.haml @@ -1,10 +1,10 @@ - is_playable = subject.playable? && can?(current_user, :update_build, @project) - if is_playable - = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.pipeline-graph', placement: 'bottom' } do + = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.js-pipeline-graph', placement: 'bottom' } do = ci_icon_for_status('play') .ci-status-text= subject.name - elsif can?(current_user, :read_build, @project) - = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } do + = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } do %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} = ci_icon_for_status(subject.status) .ci-status-text= subject.name diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index b58dceb58c9..3f05a21990f 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -1,13 +1,10 @@ - status = pipeline.status -- detailed_status = pipeline.detailed_status - show_commit = local_assigns.fetch(:show_commit, true) - show_branch = local_assigns.fetch(:show_branch, true) %tr.commit %td.commit-link - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "ci-status ci-#{detailed_status}" do - = ci_icon_for_status(detailed_status) - = ci_text_for_status(detailed_status) + = render 'ci/status/badge', status: pipeline.detailed_status(current_user) %td = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index f6e3d5e76f5..782f558e8b0 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -13,7 +13,7 @@ %a.close{href: "#", "data-dismiss" => "modal"} × %h3.page-title== #{label} this #{commit.change_type_title(current_user)} .modal-body - = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-#{type}-form js-requires-input' do + = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do .form-group.branch = label_tag 'target_branch', target_label, class: 'control-label' .col-sm-10 @@ -23,12 +23,11 @@ - 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}" + = label_tag do + = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: nil Start a <strong>new merge request</strong> with these changes - else - = hidden_field_tag 'create_merge_request', 1 + = hidden_field_tag 'create_merge_request', 1, id: nil .form-actions = submit_tag label, class: 'btn btn-create' = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 65151ac3a56..c08ed8f6c16 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -1,13 +1,8 @@ .page-content-header .header-main-content - %strong Commit - %strong.monospace.js-details-short= @commit.short_id - = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do - %span.text-expander - \... - %span.js-details-content.hide - %strong.monospace.commit-hash-full= @commit.id + %strong = clipboard_button(clipboard_text: @commit.id) + = @commit.short_id %span.hidden-xs authored #{time_ago_with_tooltip(@commit.authored_date)} %span by diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index c7b5c1124b3..08d3443b3d0 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -24,7 +24,7 @@ in = time_interval_in_words pipeline.duration - .row-content-block.build-content.middle-block.hidden + .row-content-block.build-content.middle-block.js-pipeline-graph.hidden = render "projects/pipelines/graph", pipeline: pipeline - if pipeline.yaml_errors.present? diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index 7f751d9ae2e..9f444f076c0 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -8,10 +8,7 @@ %tr.generic_commit_status{class: ('retried' if retried)} %td.status - - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url - = ci_status_with_icon(generic_commit_status.status, generic_commit_status.target_url) - - else - = ci_status_with_icon(generic_commit_status.status) + = render 'ci/status/badge', status: generic_commit_status.detailed_status(current_user) %td.generic_commit_status-link - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml index 7b82d913d29..1bba0443154 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml @@ -1,4 +1,4 @@ -%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } } +%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } } - if subject.target_url = link_to subject.target_url do %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index fa189ae62d8..b3c43286a50 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -1,4 +1,4 @@ -%li{ class: mr_css_classes(merge_request) } +%li{ id: dom_id(merge_request), class: mr_css_classes(merge_request), data: { labels: merge_request.label_ids, id: merge_request.id } } - if @bulk_edit .issue-check = check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue" @@ -39,7 +39,7 @@ = icon('thumbs-down') = downvotes - - note_count = merge_request.mr_and_commit_notes.user.count + - note_count = merge_request.related_notes.user.count %li = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do = icon('comments') diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 896f10104fa..5febe806bdd 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -53,7 +53,7 @@ %li.notes-tab = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do Discussion - %span.badge= @merge_request.mr_and_commit_notes.user.count + %span.badge= @merge_request.related_notes.user.count - if @merge_request.source_project %li.commits-tab = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml index a0e12fb3f38..baa1ade5eee 100644 --- a/app/views/projects/merge_requests/show/_commits.html.haml +++ b/app/views/projects/merge_requests/show/_commits.html.haml @@ -1,6 +1,2 @@ -.content-block.oneline-block - = icon("sort-amount-desc") - Most recent commits displayed first - %ol#commits-list.list-unstyled = render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index ba8895438c5..778a32e6345 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -65,7 +65,7 @@ .note-text.md = preserve do = note.redacted_note_html - = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) + = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) - if note_editable = render 'projects/notes/edit_form', note: note .note-awards diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 229bdfb0e8d..b00ba2d5307 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -1,6 +1,6 @@ .page-content-header .header-main-content - = ci_status_with_icon(@pipeline.detailed_status) + = render 'ci/status/badge', status: @pipeline.detailed_status(current_user) %strong Pipeline ##{@commit.pipelines.last.id} triggered #{time_ago_with_tooltip(@commit.authored_date)} by = author_avatar(@commit, size: 24) diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 739e5930822..88af41aa835 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -12,7 +12,7 @@ .tab-content #js-tab-pipeline.tab-pane - .build-content.middle-block + .build-content.middle-block.js-pipeline-graph = render "projects/pipelines/graph", pipeline: pipeline #js-tab-builds.tab-pane diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index e1e787dbde4..030cd8ef78f 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -5,23 +5,23 @@ %div{ class: container_class } .top-area %ul.nav-links - %li{class: ('active' if @scope.nil?)} + %li{class: ('active' if @scope.nil?)}> = link_to project_pipelines_path(@project) do All %span.badge.js-totalbuilds-count = number_with_delimiter(@pipelines_count) - %li{class: ('active' if @scope == 'running')} + %li{class: ('active' if @scope == 'running')}> = link_to project_pipelines_path(@project, scope: :running) do Running %span.badge.js-running-count = number_with_delimiter(@running_or_pending_count) - %li{class: ('active' if @scope == 'branches')} + %li{class: ('active' if @scope == 'branches')}> = link_to project_pipelines_path(@project, scope: :branches) do Branches - %li{class: ('active' if @scope == 'tags')} + %li{class: ('active' if @scope == 'tags')}> = link_to project_pipelines_path(@project, scope: :tags) do Tags diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index 32e1f8a21b0..068a6610350 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -1,13 +1,13 @@ .hidden-xs - - if can?(current_user, :create_project_snippet, @project) - = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New snippet" do - New snippet - - if can?(current_user, :update_project_snippet, @snippet) - = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do - Delete - if can?(current_user, :update_project_snippet, @snippet) - = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do + = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped" do Edit + - if can?(current_user, :update_project_snippet, @snippet) + = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do + Delete + - if can?(current_user, :create_project_snippet, @project) + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do + New snippet - if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet) .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index e77e1b026f6..84e05cd6d88 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -1,11 +1,19 @@ - page_title "Snippets" -.sub-header-block - - if can?(current_user, :create_project_snippet, @project) - = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do - New snippet +- if current_user + .top-area + - include_private = @project.team.member?(current_user) || current_user.admin? + = render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private } + + .nav-controls.hidden-xs + - if can?(current_user, :create_project_snippet, @project) + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New snippet" do + New snippet - .oneline - Share code pastes with others out of git repository +- if can?(current_user, :create_project_snippet, @project) + .visible-xs + + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-block", title: "New snippet" do + New snippet = render 'snippets/snippets' diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index 9503dbded13..79d87b7db12 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -8,10 +8,11 @@ = blob_icon 0, @snippet.file_name = @snippet.file_name .file-actions - = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") + = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm") = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" = render 'shared/snippets/blob' - = render 'award_emoji/awards_block', awardable: @snippet, inline: true + .row-content-block.top-block.content-component-block + = render 'award_emoji/awards_block', awardable: @snippet, inline: true %div#notes= render "projects/notes/notes_with_form" diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml index 1d8fa10db0c..bf8c75b6e5c 100644 --- a/app/views/projects/stage/_graph.html.haml +++ b/app/views/projects/stage/_graph.html.haml @@ -19,4 +19,4 @@ %li.build .curve .dropdown.inline.build-content - = render "projects/stage/in_stage_group", name: group_name, subject: grouped_statuses + = render 'projects/stage/in_stage_group', name: group_name, subject: grouped_statuses diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index 21e378b8735..d37c376c36b 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -5,14 +5,11 @@ %tr %th Name %th.hidden-xs - .pull-left Last Commit + .pull-left Last commit .last-commit.hidden-sm.pull-left - - %i.fa.fa-angle-right - - %small.light + %small.light + = clipboard_button(clipboard_text: @commit.id) = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" - – = time_ago_with_tooltip(@commit.committed_date) = @commit.full_title %small.commit-history-link-spacer | diff --git a/app/views/repository_check_mailer/notify.html.haml b/app/views/repository_check_mailer/notify.html.haml index a585147ddd1..94e5a5d9709 100644 --- a/app/views/repository_check_mailer/notify.html.haml +++ b/app/views/repository_check_mailer/notify.html.haml @@ -2,7 +2,7 @@ #{@message}. %p - = link_to "See the affected projects in the GitLab admin panel", admin_namespaces_projects_url(last_repository_check_failed: 1) + = link_to "See the affected projects in the GitLab admin panel", admin_projects_url(last_repository_check_failed: 1) %p You are receiving this message because you are a GitLab administrator for #{Gitlab.config.gitlab.url}. diff --git a/app/views/repository_check_mailer/notify.text.haml b/app/views/repository_check_mailer/notify.text.haml index 93db151329e..0902c50d052 100644 --- a/app/views/repository_check_mailer/notify.text.haml +++ b/app/views/repository_check_mailer/notify.text.haml @@ -1,6 +1,6 @@ #{@message}. \ -View details: #{admin_namespaces_projects_url(last_repository_check_failed: 1)} +View details: #{admin_projects_url(last_repository_check_failed: 1)} You are receiving this message because you are a GitLab administrator for #{Gitlab.config.gitlab.url}. diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml index c414acb6a11..027d42396b4 100644 --- a/app/views/search/results/_snippet_title.html.haml +++ b/app/views/search/results/_snippet_title.html.haml @@ -14,7 +14,7 @@ = link_to snippet_title.project.name_with_namespace, namespace_project_path(snippet_title.project.namespace, snippet_title.project) .snippet-info - = "##{snippet_title.id}" + = snippet_title.to_reference %span by = link_to user_snippets_path(snippet_title.author) do diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml index 73d288e2236..186ed4a7c8b 100644 --- a/app/views/shared/_milestones_filter.html.haml +++ b/app/views/shared/_milestones_filter.html.haml @@ -2,17 +2,17 @@ - counts = milestone_counts(@project.milestones) %ul.nav-links - %li{class: milestone_class_for_state(params[:state], 'opened', true)} + %li{class: milestone_class_for_state(params[:state], 'opened', true)}> = link_to milestones_filter_path(state: 'opened') do Open - if @project %span.badge #{counts[:opened]} - %li{class: milestone_class_for_state(params[:state], 'closed')} + %li{class: milestone_class_for_state(params[:state], 'closed')}> = link_to milestones_filter_path(state: 'closed') do Closed - if @project %span.badge #{counts[:closed]} - %li{class: milestone_class_for_state(params[:state], 'all')} + %li{class: milestone_class_for_state(params[:state], 'all')}> = link_to milestones_filter_path(state: 'all') do All - if @project diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml index 60353aee7f1..b6047ece592 100644 --- a/app/views/shared/builds/_tabs.html.haml +++ b/app/views/shared/builds/_tabs.html.haml @@ -1,23 +1,23 @@ %ul.nav-links - %li{ class: ('active' if scope.nil?) } + %li{ class: ('active' if scope.nil?) }> = link_to build_path_proc.call(nil) do All %span.badge.js-totalbuilds-count = number_with_delimiter(all_builds.count(:id)) - %li{ class: ('active' if scope == 'pending') } + %li{ class: ('active' if scope == 'pending') }> = link_to build_path_proc.call('pending') do Pending %span.badge = number_with_delimiter(all_builds.pending.count(:id)) - %li{ class: ('active' if scope == 'running') } + %li{ class: ('active' if scope == 'running') }> = link_to build_path_proc.call('running') do Running %span.badge = number_with_delimiter(all_builds.running.count(:id)) - %li{ class: ('active' if scope == 'finished') } + %li{ class: ('active' if scope == 'finished') }> = link_to build_path_proc.call('finished') do Finished %span.badge diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index 19221e3391f..8164f61797c 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -28,7 +28,7 @@ = image_tag group_icon(group), class: "avatar s40 hidden-xs" .title = link_to group, class: 'group-name' do - = group.name + = group.full_name - if group_member as diff --git a/app/views/shared/icons/_icon_status_manual.svg b/app/views/shared/icons/_icon_status_manual.svg new file mode 100755 index 00000000000..c98839f51a9 --- /dev/null +++ b/app/views/shared/icons/_icon_status_manual.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></svg> diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index 0af92b59584..d938edf4dbd 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -3,23 +3,23 @@ - issuables = @issues || @merge_requests %ul.nav-links.issues-state-filters - %li{class: ("active" if params[:state] == 'opened')} + %li{class: ("active" if params[:state] == 'opened')}> = link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened." do #{issuables_state_counter_text(type, :opened)} - if type == :merge_requests - %li{class: ("active" if params[:state] == 'merged')} + %li{class: ("active" if params[:state] == 'merged')}> = link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.' do #{issuables_state_counter_text(type, :merged)} - %li{class: ("active" if params[:state] == 'closed')} + %li{class: ("active" if params[:state] == 'closed')}> = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.' do #{issuables_state_counter_text(type, :closed)} - else - %li{class: ("active" if params[:state] == 'closed')} + %li{class: ("active" if params[:state] == 'closed')}> = link_to page_filter_path(state: 'closed', label: true), id: 'state-all', title: 'Filter by issues that are currently closed.' do #{issuables_state_counter_text(type, :closed)} - %li{class: ("active" if params[:state] == 'all')} + %li{class: ("active" if params[:state] == 'all')}> = link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}." do #{issuables_state_counter_text(type, :all)} diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 958f8413e1d..9fe1be5a597 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -167,4 +167,4 @@ new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}'); gl.Subscription.bindAll('.subscription'); new gl.DueDateSelectors(); - sidebar = new Sidebar(); + window.sidebar = new Sidebar(); diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml index 1f7df0bcd19..fbad0d05de3 100644 --- a/app/views/shared/notifications/_button.html.haml +++ b/app/views/shared/notifications/_button.html.haml @@ -1,4 +1,3 @@ -- left_align = local_assigns[:left_align] - if notification_setting .dropdown.notification-dropdown = form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f| @@ -19,7 +18,7 @@ = notification_title(notification_setting.level) = icon("caret-down") - = render "shared/notifications/notification_dropdown", notification_setting: notification_setting, left_align: left_align + = render "shared/notifications/notification_dropdown", notification_setting: notification_setting = content_for :scripts_body do = render "shared/notifications/custom_notifications", notification_setting: notification_setting diff --git a/app/views/shared/notifications/_notification_dropdown.html.haml b/app/views/shared/notifications/_notification_dropdown.html.haml index d3258ee64cb..85ad74f9a39 100644 --- a/app/views/shared/notifications/_notification_dropdown.html.haml +++ b/app/views/shared/notifications/_notification_dropdown.html.haml @@ -1,5 +1,4 @@ -- left_align = local_assigns[:left_align] -%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting), ("dropdown-menu-align-right" unless left_align)] } +%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting)] } - NotificationSetting.levels.each_key do |level| - next if level == "custom" - next if level == "global" && notification_setting.source.nil? diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index d7506e07ff6..d084f5e9684 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -8,10 +8,6 @@ %span.creator authored = time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago') - - if @snippet.updated_at != @snippet.created_at - %span - = icon('edit', title: 'edited') - = time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago') by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")} .snippet-actions @@ -20,5 +16,9 @@ - else = render "snippets/actions" -%h2.snippet-title.prepend-top-0.append-bottom-0 - = markdown_field(@snippet, :title) +.snippet-header + %h2.snippet-title.prepend-top-0.append-bottom-0 + = markdown_field(@snippet, :title) + + - if @snippet.updated_at != @snippet.created_at + = edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago') diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index ea17bec8677..5d2d2317f22 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -1,17 +1,16 @@ +- link_project = local_assigns.fetch(:link_project, false) + %li.snippet-row = image_tag avatar_icon(snippet.author_email), class: "avatar s40 hidden-xs", alt: '' .title = link_to reliable_snippet_path(snippet) do = snippet.title - - if snippet.private? - %span.label.label-gray.hidden-xs - = icon('lock') - private - %span.monospace.pull-right.hidden-xs - = snippet.file_name + - if snippet.file_name + %span.snippet-filename.monospace.hidden-xs + = snippet.file_name - %ul.controls.visible-xs + %ul.controls %li - note_count = snippet.notes.user.count = link_to reliable_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do @@ -22,11 +21,17 @@ = visibility_level_label(snippet.visibility_level) = visibility_level_icon(snippet.visibility_level, fw: false) - %small.pull-right.cgray.hidden-xs - - if snippet.project_id? - = link_to snippet.project.name_with_namespace, namespace_project_path(snippet.project.namespace, snippet.project) - - .snippet-info.hidden-xs + .snippet-info + #{snippet.to_reference} · + authored #{time_ago_with_tooltip(snippet.created_at, placement: 'bottom', html_class: 'snippet-created-ago')} + by = link_to user_snippets_path(snippet.author) do = snippet.author_name - authored #{time_ago_with_tooltip(snippet.created_at)} + - if link_project && snippet.project_id? + %span.hidden-xs + in + = link_to namespace_project_path(snippet.project.namespace, snippet.project) do + = snippet.project.name_with_namespace + + .pull-right.snippet-updated-at + %span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom')} diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index 1d0e549ed3d..95fc7198104 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -1,13 +1,13 @@ .hidden-xs - - if current_user - = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New snippet" do - New snippet - - if can?(current_user, :admin_personal_snippet, @snippet) - = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do - Delete - if can?(current_user, :update_personal_snippet, @snippet) - = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do + = link_to edit_snippet_path(@snippet), class: "btn btn-grouped" do Edit + - if can?(current_user, :admin_personal_snippet, @snippet) + = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do + Delete + - if current_user + = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do + New snippet - if current_user .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml index 77b66ca74b6..ac3701233ad 100644 --- a/app/views/snippets/_snippets.html.haml +++ b/app/views/snippets/_snippets.html.haml @@ -1,8 +1,9 @@ - remote = local_assigns.fetch(:remote, false) +- link_project = local_assigns.fetch(:link_project, false) .snippets-list-holder %ul.content-list - = render partial: 'shared/snippets/snippet', collection: @snippets + = render partial: 'shared/snippets/snippet', collection: @snippets, locals: { link_project: link_project } - if @snippets.empty? %li .nothing-here-block Nothing here. diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml new file mode 100644 index 00000000000..2dda5fed647 --- /dev/null +++ b/app/views/snippets/_snippets_scope_menu.html.haml @@ -0,0 +1,31 @@ +- subject = local_assigns.fetch(:subject, current_user) +- include_private = local_assigns.fetch(:include_private, false) + +.nav-links.snippet-scope-menu + %li{ class: ("active" unless params[:scope]) } + = link_to subject_snippets_path(subject) do + All + %span.badge + - if include_private + = subject.snippets.count + - else + = subject.snippets.public_and_internal.count + + - if include_private + %li{ class: ("active" if params[:scope] == "are_private") } + = link_to subject_snippets_path(subject, scope: 'are_private') do + Private + %span.badge + = subject.snippets.are_private.count + + %li{ class: ("active" if params[:scope] == "are_internal") } + = link_to subject_snippets_path(subject, scope: 'are_internal') do + Internal + %span.badge + = subject.snippets.are_internal.count + + %li{ class: ("active" if params[:scope] == "are_public") } + = link_to subject_snippets_path(subject, scope: 'are_public') do + Public + %span.badge + = subject.snippets.are_public.count diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 27d7a6c5bb6..837a1a0cc8c 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -7,9 +7,9 @@ = blob_icon 0, @snippet.file_name = @snippet.file_name .file-actions - = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") + = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm") = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" = link_to 'Download', download_snippet_path(@snippet), class: "btn btn-sm" = render 'shared/snippets/blob' -= render 'award_emoji/awards_block', awardable: @snippet, inline: true
\ No newline at end of file += render 'award_emoji/awards_block', awardable: @snippet, inline: true diff --git a/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml b/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml new file mode 100644 index 00000000000..742b10e72aa --- /dev/null +++ b/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml @@ -0,0 +1,4 @@ +--- +title: group authors in contribution graph with case insensitive email handle comparison +merge_request: 8021 +author: diff --git a/changelogs/unreleased/20052-actions-table-vscroll.yml b/changelogs/unreleased/20052-actions-table-vscroll.yml new file mode 100644 index 00000000000..779cd08de09 --- /dev/null +++ b/changelogs/unreleased/20052-actions-table-vscroll.yml @@ -0,0 +1,4 @@ +--- +title: Prevent overflow with vertical scroll when we have space to show content +merge_request: 8061 +author: diff --git a/changelogs/unreleased/22849-ci-build-ref-slug.yml b/changelogs/unreleased/22849-ci-build-ref-slug.yml new file mode 100644 index 00000000000..b159ecca6d8 --- /dev/null +++ b/changelogs/unreleased/22849-ci-build-ref-slug.yml @@ -0,0 +1,4 @@ +--- +title: Introduce $CI_BUILD_REF_SLUG +merge_request: 8072 +author: diff --git a/changelogs/unreleased/22864-add-environment-slug.yml b/changelogs/unreleased/22864-add-environment-slug.yml new file mode 100644 index 00000000000..f90f79337d5 --- /dev/null +++ b/changelogs/unreleased/22864-add-environment-slug.yml @@ -0,0 +1,4 @@ +--- +title: Add a slug to environments +merge_request: 7983 +author: diff --git a/changelogs/unreleased/22864-kubernetes-service.yml b/changelogs/unreleased/22864-kubernetes-service.yml new file mode 100644 index 00000000000..ea1323cbeb0 --- /dev/null +++ b/changelogs/unreleased/22864-kubernetes-service.yml @@ -0,0 +1,4 @@ +--- +title: Introduce deployment services, starting with a KubernetesService +merge_request: 7994 +author: diff --git a/changelogs/unreleased/24803-change-cursor-for-ca-stages.yml b/changelogs/unreleased/24803-change-cursor-for-ca-stages.yml new file mode 100644 index 00000000000..b9d84c0ce31 --- /dev/null +++ b/changelogs/unreleased/24803-change-cursor-for-ca-stages.yml @@ -0,0 +1,5 @@ +--- +title: Changed cursor icon to pointer when mousing over stages on the Cycle Analytics + pages +merge_request: +author: Ryan Harris diff --git a/changelogs/unreleased/24807-stop-ddosing-ourselves.yml b/changelogs/unreleased/24807-stop-ddosing-ourselves.yml new file mode 100644 index 00000000000..49e6c5e56e5 --- /dev/null +++ b/changelogs/unreleased/24807-stop-ddosing-ourselves.yml @@ -0,0 +1,4 @@ +--- +title: Use SmartInterval for MR widget and improve visibilitychange functionality +merge_request: 7762 +author: diff --git a/changelogs/unreleased/24824-dropdown-items-focus.yml b/changelogs/unreleased/24824-dropdown-items-focus.yml new file mode 100644 index 00000000000..66970c2a9a5 --- /dev/null +++ b/changelogs/unreleased/24824-dropdown-items-focus.yml @@ -0,0 +1,4 @@ +--- +title: Add focus state to dropdown items +merge_request: +author: diff --git a/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml b/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml new file mode 100644 index 00000000000..cc7c2604824 --- /dev/null +++ b/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml @@ -0,0 +1,4 @@ +--- +title: Improve bulk assignment for issuables +merge_request: +author: diff --git a/changelogs/unreleased/24927-custom-event-polyfill-test.yml b/changelogs/unreleased/24927-custom-event-polyfill-test.yml new file mode 100644 index 00000000000..879c28a951e --- /dev/null +++ b/changelogs/unreleased/24927-custom-event-polyfill-test.yml @@ -0,0 +1,4 @@ +--- +title: Adds tests for custom event polyfill +merge_request: 7996 +author: diff --git a/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml b/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml new file mode 100644 index 00000000000..b8ba9391530 --- /dev/null +++ b/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml @@ -0,0 +1,4 @@ +--- +title: Add image controls to MR diffs +merge_request: 7919 +author: diff --git a/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml b/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml new file mode 100644 index 00000000000..62030d3fc45 --- /dev/null +++ b/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml @@ -0,0 +1,4 @@ +--- +title: Prevent user creating issue or MR without signing in for a group +merge_request: 7902 +author: diff --git a/changelogs/unreleased/25136-last-deployment-link.yml b/changelogs/unreleased/25136-last-deployment-link.yml new file mode 100644 index 00000000000..eab1534aa66 --- /dev/null +++ b/changelogs/unreleased/25136-last-deployment-link.yml @@ -0,0 +1,4 @@ +--- +title: Fix Latest deployment link is broken +merge_request: 7839 +author: diff --git a/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml b/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml new file mode 100644 index 00000000000..531b0f83099 --- /dev/null +++ b/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml @@ -0,0 +1,4 @@ +--- +title: Improve help message for issue create slash command +merge_request: 7850 +author: diff --git a/changelogs/unreleased/25294-remove-signed-out-msg.yml b/changelogs/unreleased/25294-remove-signed-out-msg.yml new file mode 100644 index 00000000000..567294fe5f7 --- /dev/null +++ b/changelogs/unreleased/25294-remove-signed-out-msg.yml @@ -0,0 +1,4 @@ +--- +title: 'fix: removed signed_out notification' +merge_request: 7958 +author: jnoortheen diff --git a/changelogs/unreleased/25482-fix-api-sudo.yml b/changelogs/unreleased/25482-fix-api-sudo.yml new file mode 100644 index 00000000000..4c11fe1622e --- /dev/null +++ b/changelogs/unreleased/25482-fix-api-sudo.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Memoize the current_user so that sudo can work properly' +merge_request: 8017 +author: diff --git a/changelogs/unreleased/25483-broken-tabs.yml b/changelogs/unreleased/25483-broken-tabs.yml new file mode 100644 index 00000000000..d6c92014bea --- /dev/null +++ b/changelogs/unreleased/25483-broken-tabs.yml @@ -0,0 +1,4 @@ +--- +title: Fix TypeError: Cannot read property 'initTabs' on commit builds tab +merge_request: 8009 +author: diff --git a/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml b/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml new file mode 100644 index 00000000000..c6a92547c5c --- /dev/null +++ b/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml @@ -0,0 +1,4 @@ +--- +title: Added go back anchor on error pages. +merge_request: 8087 +author: diff --git a/changelogs/unreleased/25617-todos-filter-placeholder.yml b/changelogs/unreleased/25617-todos-filter-placeholder.yml new file mode 100644 index 00000000000..5d0adb04ef3 --- /dev/null +++ b/changelogs/unreleased/25617-todos-filter-placeholder.yml @@ -0,0 +1,4 @@ +--- +title: 25617 Fix placeholder color of todo filters +merge_request: +author: diff --git a/changelogs/unreleased/8003-katex-math.yml b/changelogs/unreleased/8003-katex-math.yml new file mode 100644 index 00000000000..a40dcde1393 --- /dev/null +++ b/changelogs/unreleased/8003-katex-math.yml @@ -0,0 +1,4 @@ +--- +title: Added support for math rendering, using KaTeX, in Markdown and asciidoc +merge_request: 8003 +author: Munken diff --git a/changelogs/unreleased/allow-more-filenames.yml b/changelogs/unreleased/allow-more-filenames.yml new file mode 100644 index 00000000000..7989f94e528 --- /dev/null +++ b/changelogs/unreleased/allow-more-filenames.yml @@ -0,0 +1,4 @@ +--- +title: Allow all alphanumeric characters in file names +merge_request: 8002 +author: winniehell diff --git a/changelogs/unreleased/api-cherry-pick.yml b/changelogs/unreleased/api-cherry-pick.yml new file mode 100644 index 00000000000..5f4cee450b9 --- /dev/null +++ b/changelogs/unreleased/api-cherry-pick.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Ability to cherry pick a commit' +merge_request: 8047 +author: Robert Schilling diff --git a/changelogs/unreleased/api-simple-group-project.yml b/changelogs/unreleased/api-simple-group-project.yml new file mode 100644 index 00000000000..54c8de610a6 --- /dev/null +++ b/changelogs/unreleased/api-simple-group-project.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Simple representation of group''s projects' +merge_request: 8060 +author: Robert Schilling diff --git a/changelogs/unreleased/awards_handler.yml b/changelogs/unreleased/awards_handler.yml new file mode 100644 index 00000000000..1f9904c0691 --- /dev/null +++ b/changelogs/unreleased/awards_handler.yml @@ -0,0 +1,4 @@ +--- +title: Replace static fixture for awards_handler_spec +merge_request: 7661 +author: winniehell diff --git a/changelogs/unreleased/chomp-git-status-message.yml b/changelogs/unreleased/chomp-git-status-message.yml new file mode 100644 index 00000000000..f70607df7a1 --- /dev/null +++ b/changelogs/unreleased/chomp-git-status-message.yml @@ -0,0 +1,5 @@ +--- +title: For single line git commit messages, the close quote should be on the same + line as the open quote +merge_request: +author: diff --git a/changelogs/unreleased/features-api-snippets.yml b/changelogs/unreleased/features-api-snippets.yml new file mode 100644 index 00000000000..80c7bb75359 --- /dev/null +++ b/changelogs/unreleased/features-api-snippets.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Endpoint to expose personal snippets as /snippets' +merge_request: 6373 +author: Bernard Guyzmo Pratz diff --git a/changelogs/unreleased/file-template-dropwdown-proper-position.yml b/changelogs/unreleased/file-template-dropwdown-proper-position.yml new file mode 100644 index 00000000000..cf2a622b7e6 --- /dev/null +++ b/changelogs/unreleased/file-template-dropwdown-proper-position.yml @@ -0,0 +1,4 @@ +--- +title: Fixed file template dropdown for the "New File" editor for smaller/zoomed screens +merge_request: +author: diff --git a/changelogs/unreleased/fix-slack-pipeline-message-by-api.yml b/changelogs/unreleased/fix-slack-pipeline-message-by-api.yml new file mode 100644 index 00000000000..aa5ad5cd8d6 --- /dev/null +++ b/changelogs/unreleased/fix-slack-pipeline-message-by-api.yml @@ -0,0 +1,4 @@ +--- +title: Fix Slack pipeline message from pipelines made by API +merge_request: 8059 +author: diff --git a/changelogs/unreleased/gitlab-workhorse-multipart.yml b/changelogs/unreleased/gitlab-workhorse-multipart.yml new file mode 100644 index 00000000000..23c2139cf93 --- /dev/null +++ b/changelogs/unreleased/gitlab-workhorse-multipart.yml @@ -0,0 +1,4 @@ +--- +title: Replace Rack::Multipart with GitLab-Workhorse based solution +merge_request: 5867 +author: diff --git a/changelogs/unreleased/issue_13270.yml b/changelogs/unreleased/issue_13270.yml new file mode 100644 index 00000000000..9c15c436876 --- /dev/null +++ b/changelogs/unreleased/issue_13270.yml @@ -0,0 +1,4 @@ +--- +title: Allow to delete tag release note +merge_request: +author: diff --git a/changelogs/unreleased/issue_25030.yml b/changelogs/unreleased/issue_25030.yml new file mode 100644 index 00000000000..e18b8d6a79b --- /dev/null +++ b/changelogs/unreleased/issue_25030.yml @@ -0,0 +1,4 @@ +--- +title: Allow branch names with dots on API endpoint +merge_request: +author: diff --git a/changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml b/changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml new file mode 100644 index 00000000000..db1389e2024 --- /dev/null +++ b/changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml @@ -0,0 +1,4 @@ +--- +title: Issue#visible_to_user moved to IssuesFinder to prevent accidental use +merge_request: +author: diff --git a/changelogs/unreleased/jej-note-search-uses-finder.yml b/changelogs/unreleased/jej-note-search-uses-finder.yml new file mode 100644 index 00000000000..1768bdfd487 --- /dev/null +++ b/changelogs/unreleased/jej-note-search-uses-finder.yml @@ -0,0 +1,4 @@ +--- +title: Fix missing Note access checks by moving Note#search to updated NoteFinder +merge_request: +author: diff --git a/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml b/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml new file mode 100644 index 00000000000..11250643a23 --- /dev/null +++ b/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml @@ -0,0 +1,4 @@ +--- +title: Move admin active tab spinach tests to rspec +merge_request: 8037 +author: Semyon Pupkov diff --git a/changelogs/unreleased/process-commit-worker-migration-encoding.yml b/changelogs/unreleased/process-commit-worker-migration-encoding.yml new file mode 100644 index 00000000000..26aabd9b647 --- /dev/null +++ b/changelogs/unreleased/process-commit-worker-migration-encoding.yml @@ -0,0 +1,4 @@ +--- +title: Encode input when migrating ProcessCommitWorker jobs to prevent migration errors +merge_request: +author: diff --git a/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml b/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml new file mode 100644 index 00000000000..754af641add --- /dev/null +++ b/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml @@ -0,0 +1,4 @@ +--- +title: Remove unnecessary commits order message +merge_request: 8004 +author: diff --git a/changelogs/unreleased/seed-runner-token.yml b/changelogs/unreleased/seed-runner-token.yml new file mode 100644 index 00000000000..e8153be043a --- /dev/null +++ b/changelogs/unreleased/seed-runner-token.yml @@ -0,0 +1,5 @@ +--- +title: Add support for setting the GitLab Runners Registration Token during initial + database seeding +merge_request: 6642 +author: diff --git a/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml b/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml new file mode 100644 index 00000000000..bbd7a217493 --- /dev/null +++ b/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml @@ -0,0 +1,4 @@ +--- +title: Show commit status from latest pipeline +merge_request: 7333 +author: diff --git a/changelogs/unreleased/unescape-relative-path.yml b/changelogs/unreleased/unescape-relative-path.yml new file mode 100644 index 00000000000..755b0379a16 --- /dev/null +++ b/changelogs/unreleased/unescape-relative-path.yml @@ -0,0 +1,4 @@ +--- +title: Avoid escaping relative links in Markdown twice +merge_request: 7940 +author: winniehell diff --git a/changelogs/unreleased/username-exists-root.yml b/changelogs/unreleased/username-exists-root.yml new file mode 100644 index 00000000000..1ffb3eb435c --- /dev/null +++ b/changelogs/unreleased/username-exists-root.yml @@ -0,0 +1,4 @@ +--- +title: Username exists check respects relative root path +merge_request: +author: diff --git a/config/application.rb b/config/application.rb index 0aa2873f94a..782a7a36895 100644 --- a/config/application.rb +++ b/config/application.rb @@ -45,7 +45,7 @@ module Gitlab # # Parameters filtered: # - Password (:password, :password_confirmation) - # - Private tokens (:private_token, :authentication_token) + # - Private tokens # - Two-factor tokens (:otp_attempt) # - Repo/Project Import URLs (:import_url) # - Build variables (:variables) @@ -60,11 +60,13 @@ module Gitlab encrypted_key hook import_url + incoming_email_token key otp_attempt password password_confirmation private_token + runners_token secret_token sentry_dsn variables @@ -85,6 +87,8 @@ module Gitlab config.assets.precompile << "print.css" config.assets.precompile << "notify.css" config.assets.precompile << "mailers/*.css" + config.assets.precompile << "katex.css" + config.assets.precompile << "katex.js" config.assets.precompile << "graphs/graphs_bundle.js" config.assets.precompile << "users/users_bundle.js" config.assets.precompile << "network/network_bundle.js" diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 9ddd1554811..0ee1b1ec634 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -302,7 +302,7 @@ Settings.cron_jobs['remove_expired_group_links_worker'] ||= Settingslogic.new({} Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *' Settings.cron_jobs['remove_expired_group_links_worker']['job_class'] = 'RemoveExpiredGroupLinksWorker' Settings.cron_jobs['prune_old_events_worker'] ||= Settingslogic.new({}) -Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '* */6 * * *' +Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '0 */6 * * *' Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker' Settings.cron_jobs['trending_projects_worker'] ||= Settingslogic.new({}) diff --git a/config/initializers/math_lexer.rb b/config/initializers/math_lexer.rb new file mode 100644 index 00000000000..8a3388a267e --- /dev/null +++ b/config/initializers/math_lexer.rb @@ -0,0 +1,2 @@ +# Touch the lexers so it is registered with Rouge +Rouge::Lexers::Math diff --git a/config/initializers/workhorse_multipart.rb b/config/initializers/workhorse_multipart.rb new file mode 100644 index 00000000000..3e2f25c354a --- /dev/null +++ b/config/initializers/workhorse_multipart.rb @@ -0,0 +1,3 @@ +Rails.application.configure do |config| + config.middleware.use(Gitlab::Middleware::Multipart) +end diff --git a/config/routes.rb b/config/routes.rb index 03b47261e7e..06d565df469 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,5 @@ require 'sidekiq/web' require 'sidekiq/cron/web' -require 'api/api' require 'constraints/group_url_constrainer' Rails.application.routes.draw do diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 5ae985da561..0dd2c8f7aef 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -28,9 +28,19 @@ namespace :admin do resources :applications - resources :groups, constraints: { id: /[^\/]+/ } do - member do + resources :groups, only: [:index, :new, :create] + + scope(path: 'groups/*id', + controller: :groups, + constraints: { id: Gitlab::Regex.namespace_route_regex }) do + + scope(as: :group) do put :members_update + get :edit, action: :edit + get '/', action: :show + patch '/', action: :update + put '/', action: :update + delete '/', action: :destroy end end @@ -50,14 +60,13 @@ namespace :admin do resource :system_info, controller: 'system_info', only: [:show] resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ } - resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do - root to: 'projects#index', as: :projects + resources :projects, only: [:index] + scope(path: 'projects/*namespace_id', as: :namespace) do resources(:projects, path: '/', - constraints: { id: /[a-zA-Z.0-9_\-]+/ }, - only: [:index, :show]) do - root to: 'projects#show' + constraints: { id: Gitlab::Regex.project_route_regex }, + only: [:show]) do member do put :transfer diff --git a/db/fixtures/production/010_settings.rb b/db/fixtures/production/010_settings.rb new file mode 100644 index 00000000000..5522f31629a --- /dev/null +++ b/db/fixtures/production/010_settings.rb @@ -0,0 +1,16 @@ +if ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN'].present? + settings = ApplicationSetting.current || ApplicationSetting.create_from_defaults + settings.set_runners_registration_token(ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN']) + + if settings.save + puts "Saved Runner Registration Token".color(:green) + else + puts "Could not save Runner Registration Token".color(:red) + puts + settings.errors.full_messages.map do |message| + puts "--> #{message}".color(:red) + end + puts + exit 1 + end +end diff --git a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb index 453a44e271a..77e0c40d850 100644 --- a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb +++ b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb @@ -47,14 +47,14 @@ class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration hash = { id: commit.oid, - message: commit.message, + message: encode(commit.message), parent_ids: commit.parent_ids, authored_date: commit.author[:time], - author_name: commit.author[:name], - author_email: commit.author[:email], + author_name: encode(commit.author[:name]), + author_email: encode(commit.author[:email]), committed_date: commit.committer[:time], - committer_email: commit.committer[:email], - committer_name: commit.committer[:name] + committer_email: encode(commit.committer[:email]), + committer_name: encode(commit.committer[:name]) } payload['args'][2] = hash @@ -89,4 +89,14 @@ class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration end end end + + def encode(data) + encoding = Encoding::UTF_8 + + if data.encoding == encoding + data + else + data.encode(encoding, invalid: :replace, undef: :replace) + end + end end diff --git a/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb b/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb new file mode 100644 index 00000000000..2977917f2d1 --- /dev/null +++ b/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb @@ -0,0 +1,36 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveUniqPathIndexFromNamespace < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = false + + def up + constraint_name = 'namespaces_path_key' + + transaction do + if index_exists?(:namespaces, :path) + remove_index(:namespaces, :path) + end + + # In some bizarre cases PostgreSQL might have a separate unique constraint + # that we'll need to drop. + if constraint_exists?(constraint_name) && Gitlab::Database.postgresql? + execute("ALTER TABLE namespaces DROP CONSTRAINT IF EXISTS #{constraint_name};") + end + end + end + + def down + unless index_exists?(:namespaces, :path) + add_concurrent_index(:namespaces, :path, unique: true) + end + end + + def constraint_exists?(name) + indexes(:namespaces).map(&:name).include?(name) + end +end diff --git a/db/migrate/20161206153751_add_path_index_to_namespace.rb b/db/migrate/20161206153751_add_path_index_to_namespace.rb new file mode 100644 index 00000000000..b0bac7d121e --- /dev/null +++ b/db/migrate/20161206153751_add_path_index_to_namespace.rb @@ -0,0 +1,20 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddPathIndexToNamespace < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = false + + def up + add_concurrent_index :namespaces, :path + end + + def down + if index_exists?(:namespaces, :path) + remove_index :namespaces, :path + end + end +end diff --git a/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb b/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb new file mode 100644 index 00000000000..cc9d4974baa --- /dev/null +++ b/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb @@ -0,0 +1,36 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveUniqNameIndexFromNamespace < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = false + + def up + constraint_name = 'namespaces_name_key' + + transaction do + if index_exists?(:namespaces, :name) + remove_index(:namespaces, :name) + end + + # In some bizarre cases PostgreSQL might have a separate unique constraint + # that we'll need to drop. + if constraint_exists?(constraint_name) && Gitlab::Database.postgresql? + execute("ALTER TABLE namespaces DROP CONSTRAINT IF EXISTS #{constraint_name};") + end + end + end + + def down + unless index_exists?(:namespaces, :name) + add_concurrent_index(:namespaces, :name, unique: true) + end + end + + def constraint_exists?(name) + indexes(:namespaces).map(&:name).include?(name) + end +end diff --git a/db/migrate/20161206153754_add_name_index_to_namespace.rb b/db/migrate/20161206153754_add_name_index_to_namespace.rb new file mode 100644 index 00000000000..aaa35ed6f0a --- /dev/null +++ b/db/migrate/20161206153754_add_name_index_to_namespace.rb @@ -0,0 +1,20 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddNameIndexToNamespace < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = false + + def up + add_concurrent_index(:namespaces, [:name, :parent_id], unique: true) + end + + def down + if index_exists?(:namespaces, :name) + remove_index :namespaces, [:name, :parent_id] + end + end +end diff --git a/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb new file mode 100644 index 00000000000..b74552e762d --- /dev/null +++ b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb @@ -0,0 +1,53 @@ +class FixupEnvironmentNameUniqueness < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = 'Renaming non-unique environments' + + def up + environments = Arel::Table.new(:environments) + + # Get all [project_id, name] pairs that occur more than once + finder_sql = environments. + group(environments[:project_id], environments[:name]). + having(Arel.sql("COUNT(1)").gt(1)). + project(environments[:project_id], environments[:name]). + to_sql + + conflicting = connection.exec_query(finder_sql) + + conflicting.rows.each do |project_id, name| + fix_duplicates(project_id, name) + end + end + + def down + # Nothing to do + end + + # Rename conflicting environments by appending "-#{id}" to all but the first + def fix_duplicates(project_id, name) + environments = Arel::Table.new(:environments) + finder_sql = environments. + where(environments[:project_id].eq(project_id)). + where(environments[:name].eq(name)). + order(environments[:id].asc). + project(environments[:id], environments[:name]). + to_sql + + # Now we have the data for all the conflicting rows + conflicts = connection.exec_query(finder_sql).rows + conflicts.shift # Leave the first row alone + + conflicts.each do |id, name| + update_sql = + Arel::UpdateManager.new(ActiveRecord::Base). + table(environments). + set(environments[:name] => name + "-" + id.to_s). + where(environments[:id].eq(id)). + to_sql + + connection.exec_update(update_sql, self.class.name, []) + end + end +end diff --git a/db/migrate/20161207231621_create_environment_name_unique_index.rb b/db/migrate/20161207231621_create_environment_name_unique_index.rb new file mode 100644 index 00000000000..ac680c8d10f --- /dev/null +++ b/db/migrate/20161207231621_create_environment_name_unique_index.rb @@ -0,0 +1,18 @@ +class CreateEnvironmentNameUniqueIndex < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = true + DOWNTIME_REASON = 'Making a non-unique index into a unique index' + + def up + remove_index :environments, [:project_id, :name] + add_concurrent_index :environments, [:project_id, :name], unique: true + end + + def down + remove_index :environments, [:project_id, :name], unique: true + add_concurrent_index :environments, [:project_id, :name] + end +end diff --git a/db/migrate/20161207231626_add_environment_slug.rb b/db/migrate/20161207231626_add_environment_slug.rb new file mode 100644 index 00000000000..7153e6a32b1 --- /dev/null +++ b/db/migrate/20161207231626_add_environment_slug.rb @@ -0,0 +1,60 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddEnvironmentSlug < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = 'Adding NOT NULL column environments.slug with dependent data' + + # Used to generate random suffixes for the slug + NUMBERS = '0'..'9' + SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a + + def up + environments = Arel::Table.new(:environments) + + add_column :environments, :slug, :string + finder = environments.project(:id, :name) + + connection.exec_query(finder.to_sql).rows.each do |id, name| + updater = Arel::UpdateManager.new(ActiveRecord::Base). + table(environments). + set(environments[:slug] => generate_slug(name)). + where(environments[:id].eq(id)) + + connection.exec_update(updater.to_sql, self.class.name, []) + end + + change_column_null :environments, :slug, false + end + + def down + remove_column :environments, :slug + end + + # Copy of the Environment#generate_slug implementation + def generate_slug(name) + # Lowercase letters and numbers only + slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-') + + # Must start with a letter + slugified = "env-" + slugified if NUMBERS.cover?(slugified[0]) + + # Maximum length: 24 characters (OpenShift limitation) + slugified = slugified[0..23] + + # Cannot end with a "-" character (Kubernetes label limitation) + slugified = slugified[0..-2] if slugified[-1] == "-" + + # Add a random suffix, shortening the current string if necessary, if it + # has been slugified. This ensures uniqueness. + slugified = slugified[0..16] + "-" + random_suffix if slugified != name + + slugified + end + + def random_suffix + (0..5).map { SUFFIX_CHARS.sample }.join + end +end diff --git a/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb new file mode 100644 index 00000000000..e9fcef1cd45 --- /dev/null +++ b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb @@ -0,0 +1,15 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddUniqueIndexForEnvironmentSlug < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = 'Adding a *unique* index to environments.slug' + + disable_ddl_transaction! + + def change + add_concurrent_index :environments, [:project_id, :slug], unique: true + end +end diff --git a/db/migrate/20161212142807_add_lower_path_index_to_routes.rb b/db/migrate/20161212142807_add_lower_path_index_to_routes.rb new file mode 100644 index 00000000000..6958500306f --- /dev/null +++ b/db/migrate/20161212142807_add_lower_path_index_to_routes.rb @@ -0,0 +1,22 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddLowerPathIndexToRoutes < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + return unless Gitlab::Database.postgresql? + + execute 'CREATE INDEX CONCURRENTLY index_on_routes_lower_path ON routes (LOWER(path));' + end + + def down + return unless Gitlab::Database.postgresql? + + remove_index :routes, name: :index_on_routes_lower_path + end +end diff --git a/db/schema.rb b/db/schema.rb index 9c46f573719..67ff83d96d9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161202152035) do +ActiveRecord::Schema.define(version: 20161212142807) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -98,14 +98,14 @@ ActiveRecord::Schema.define(version: 20161202152035) do t.text "help_page_text_html" t.text "shared_runners_text_html" t.text "after_sign_up_text_html" + t.boolean "sidekiq_throttling_enabled", default: false + t.string "sidekiq_throttling_queues" + t.decimal "sidekiq_throttling_factor" t.boolean "housekeeping_enabled", default: true, null: false t.boolean "housekeeping_bitmaps_enabled", default: true, null: false t.integer "housekeeping_incremental_repack_period", default: 10, null: false t.integer "housekeeping_full_repack_period", default: 50, null: false t.integer "housekeeping_gc_period", default: 200, null: false - t.boolean "sidekiq_throttling_enabled", default: false - t.string "sidekiq_throttling_queues" - t.decimal "sidekiq_throttling_factor" t.boolean "html_emails_enabled", default: true end @@ -428,9 +428,11 @@ ActiveRecord::Schema.define(version: 20161202152035) do t.string "external_url" t.string "environment_type" t.string "state", default: "available", null: false + t.string "slug", null: false end - add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree + add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", unique: true, using: :btree + add_index "environments", ["project_id", "slug"], name: "index_environments_on_project_id_and_slug", unique: true, using: :btree create_table "events", force: :cascade do |t| t.string "target_type" @@ -737,18 +739,18 @@ ActiveRecord::Schema.define(version: 20161202152035) do t.integer "visibility_level", default: 20, null: false t.boolean "request_access_enabled", default: false, null: false t.datetime "deleted_at" - t.text "description_html" t.boolean "lfs_enabled" + t.text "description_html" t.integer "parent_id" end add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree add_index "namespaces", ["deleted_at"], name: "index_namespaces_on_deleted_at", using: :btree - add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree + add_index "namespaces", ["name", "parent_id"], name: "index_namespaces_on_name_and_parent_id", unique: true, using: :btree add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree add_index "namespaces", ["parent_id", "id"], name: "index_namespaces_on_parent_id_and_id", unique: true, using: :btree - add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree + add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"} add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree @@ -1219,8 +1221,8 @@ ActiveRecord::Schema.define(version: 20161202152035) do t.datetime "otp_grace_period_started_at" t.boolean "ldap_email", default: false, null: false t.boolean "external", default: false - t.string "incoming_email_token" t.string "organization" + t.string "incoming_email_token" t.boolean "authorized_projects_populated" end diff --git a/doc/README.md b/doc/README.md index 66c8c26e4f0..eba1e9845b1 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,4 +1,4 @@ -# Documentation +# GitLab Community Edition documentation ## User documentation diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md index 07e548aaabe..2fc5d0355b5 100644 --- a/doc/administration/auth/README.md +++ b/doc/administration/auth/README.md @@ -7,5 +7,6 @@ providers. and 389 Server - [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, Crowd and Azure -- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider - [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS +- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider +- [Okta](okta.md) Configure GitLab to sign in using Okta diff --git a/doc/administration/auth/img/okta_admin_panel.png b/doc/administration/auth/img/okta_admin_panel.png Binary files differnew file mode 100644 index 00000000000..12e21956715 --- /dev/null +++ b/doc/administration/auth/img/okta_admin_panel.png diff --git a/doc/administration/auth/img/okta_saml_settings.png b/doc/administration/auth/img/okta_saml_settings.png Binary files differnew file mode 100644 index 00000000000..ee275ece369 --- /dev/null +++ b/doc/administration/auth/img/okta_saml_settings.png diff --git a/doc/administration/auth/okta.md b/doc/administration/auth/okta.md new file mode 100644 index 00000000000..cb42b7743c5 --- /dev/null +++ b/doc/administration/auth/okta.md @@ -0,0 +1,160 @@ +# Okta SSO provider + +Okta is a [Single Sign-on provider][okta-sso] that can be used to authenticate +with GitLab. + +The following documentation enables Okta as a SAML provider. + +## Configure the Okta application + +1. On Okta go to the admin section and choose to **Add an App**. +1. When the app screen comes up you see another button to **Create an App** and + choose SAML 2.0 on the next screen. +1. Now, very important, add a logo + (you can choose it from https://about.gitlab.com/press/). You'll have to + crop and resize it. +1. Next, you'll need the to fill in the SAML general config. Here's an example + image. + + ![Okta admin panel view](img/okta_admin_panel.png) + +1. The last part of the configuration is the feedback section where you can + just say you're a customer and creating an app for internal use. +1. When you have your app you'll have a few tabs on the top of the app's + profile. Click on the SAML 2.0 config instructions button which should + look like the following: + + ![Okta SAML settings](img/okta_saml_settings.png) + +1. On the screen that comes up take note of the + **Identity Provider Single Sign-On URL** which you'll use for the + `idp_sso_target_url` on your GitLab config file. + +1. **Before you leave Okta make sure you add your user and groups if any.** + +--- + +Now that the Okta app is configured, it's time to enable it in GitLab. + +## Configure GitLab + +1. On your GitLab server, open the configuration file: + + **For Omnibus GitLab installations** + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + **For installations from source** + + ```sh + cd /home/git/gitlab + sudo -u git -H editor config/gitlab.yml + ``` + +1. See [Initial OmniAuth Configuration](../../integration/omniauth.md#initial-omniauth-configuration) + for initial settings. + +1. To allow your users to use Okta to sign up without having to manually create + an account first, don't forget to add the following values to your + configuration: + + **For Omnibus GitLab installations** + + ```ruby + gitlab_rails['omniauth_allow_single_sign_on'] = ['saml'] + gitlab_rails['omniauth_block_auto_created_users'] = false + ``` + + **For installations from source** + + ```yaml + allow_single_sign_on: ["saml"] + block_auto_created_users: false + ``` + +1. You can also automatically link Okta users with existing GitLab users if + their email addresses match by adding the following setting: + + **For Omnibus GitLab installations** + + ```ruby + gitlab_rails['omniauth_auto_link_saml_user'] = true + ``` + + **For installations from source** + + ```yaml + auto_link_saml_user: true + ``` + +1. Add the provider configuration. + + >**Notes:** + >- Change the value for `assertion_consumer_service_url` to match the HTTPS endpoint + of GitLab (append `users/auth/saml/callback` to the HTTPS URL of your GitLab + installation to generate the correct value). + >- To get the `idp_cert_fingerprint` fingerprint, first download the + certificate from the Okta app you registered and then run: + `openssl x509 -in okta.cert -noout -fingerprint`. Substitute `okta.cert` + with the location of your certificate. + >- Change the value of `idp_sso_target_url`, with the value of the + **Identity Provider Single Sign-On URL** from the step when you + configured the Okta app. + >- Change the value of `issuer` to a unique name, which will identify the application + to the IdP. + >- Leave `name_identifier_format` as-is. + + **For Omnibus GitLab installations** + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + name: 'saml', + args: { + assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', + idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', + idp_sso_target_url: 'https://gitlab.oktapreview.com/app/gitlabdev773716_gitlabsaml_1/exk8odl81tBrjpD4B0h7/sso/saml', + issuer: 'https://gitlab.example.com', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + }, + label: 'Okta' # optional label for SAML login button, defaults to "Saml" + } + ] + ``` + + **For installations from source** + + ```yaml + - { + name: 'saml', + args: { + assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', + idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', + idp_sso_target_url: 'https://gitlab.oktapreview.com/app/gitlabdev773716_gitlabsaml_1/exk8odl81tBrjpD4B0h7/sso/saml', + issuer: 'https://gitlab.example.com', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + }, + label: 'Okta' # optional label for SAML login button, defaults to "Saml" + } + ``` + + +1. [Reconfigure][reconf] or [restart] GitLab for Omnibus and installations + from source respectively for the changes to take effect. + +You might want to try this out on a incognito browser window. + +## Configuring groups + +>**Note:** +Make sure the groups exist and are assigned to the Okta app. + +You can take a look of the [SAML documentation][saml] on external groups since +it works the same. + +[okta-sso]: https://www.okta.com/products/single-sign-on/ +[saml]: ../../integration/saml.md#external-groups +[reconf]: ../restart_gitlab.md#omnibus-gitlab-reconfigure +[restart]: ../restart_gitlab.md#installations-from-source diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md index b4a953d1ccc..76029b30dd8 100644 --- a/doc/administration/environment_variables.md +++ b/doc/administration/environment_variables.md @@ -13,17 +13,18 @@ override certain values. Variable | Type | Description -------- | ---- | ----------- -`GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation -`GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`) -`RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test` -`DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development` -`GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab -`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab -`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab -`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab -`GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The e-mail subject suffix used in e-mails sent by GitLab -`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer -`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer +`GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation +`GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`) +`RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test` +`DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development` +`GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab +`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab +`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab +`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab +`GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The e-mail subject suffix used in e-mails sent by GitLab +`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer +`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer +`GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN` | string | Sets the initial registration token used for GitLab Runners ## Complete database variables diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md index b36cf18d459..e4f94eb7cb6 100644 --- a/doc/administration/high_availability/database.md +++ b/doc/administration/high_availability/database.md @@ -44,6 +44,9 @@ If you use a cloud-managed service, or provide your own PostgreSQL: gitlab_rails['db_password'] = 'DB password' postgresql['md5_auth_cidr_addresses'] = ['0.0.0.0/0'] postgresql['listen_address'] = '0.0.0.0' + + # Disable automatic database migrations + gitlab_rails['auto_migrate'] = false ``` 1. Run `sudo gitlab-ctl reconfigure` to install and configure PostgreSQL. @@ -102,9 +105,6 @@ If you use a cloud-managed service, or provide your own PostgreSQL: 1. Exit the database prompt by typing `\q` and Enter. 1. Exit the `gitlab-psql` user by running `exit` twice. 1. Run `sudo gitlab-ctl reconfigure` a final time. -1. Run `sudo touch /etc/gitlab/skip-auto-migrations` to prevent database migrations - from running on upgrade. Only the primary GitLab application server should - handle migrations. --- diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md index f532a106bc6..b4e7bf21e35 100644 --- a/doc/administration/high_availability/redis.md +++ b/doc/administration/high_availability/redis.md @@ -287,14 +287,14 @@ The prerequisites for a HA Redis setup are the following: redis['password'] = 'redis-password-goes-here' ``` -1. To prevent database migrations from running on upgrade, run: +1. Only the primary GitLab application server should handle migrations. To + prevent database migrations from running on upgrade, add the following + configuration to your `/etc/gitlab/gitlab.rb` file: ``` - sudo touch /etc/gitlab/skip-auto-migrations + gitlab_rails['auto_migrate'] = false ``` - Only the primary GitLab application server should handle migrations. - 1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect. ### Step 2. Configuring the slave Redis instances diff --git a/doc/administration/logs.md b/doc/administration/logs.md index d757a3c2a66..4b8d5c5cc87 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -1,4 +1,4 @@ -## Log system +# Log system GitLab has an advanced log system where everything is logged so that you can analyze your instance using various system log files. In addition to @@ -9,10 +9,10 @@ documentation](http://docs.gitlab.com/ee/administration/audit_events.html) System log files are typically plain text in a standard log file format. This guide talks about how to read and use these system log files. -### production.log +## `production.log` This file lives in `/var/log/gitlab/gitlab-rails/production.log` for -omnibus package or in `/home/git/gitlab/log/production.log` for +Omnibus GitLab packages or in `/home/git/gitlab/log/production.log` for installations from source. (When Gitlab is running in an environment other than production, the corresponding logfile is shown here.) @@ -46,10 +46,10 @@ In this example we can see that server processed an HTTP request with URL 19:34:53 +0200. Also we can see that request was processed by `Projects::TreeController`. -### application.log +## `application.log` This file lives in `/var/log/gitlab/gitlab-rails/application.log` for -omnibus package or in `/home/git/gitlab/log/application.log` for +Omnibus GitLab packages or in `/home/git/gitlab/log/application.log` for installations from source. It helps you discover events happening in your instance such as user creation, @@ -63,10 +63,10 @@ October 07, 2014 11:25: User "Claudie Hodkiewicz" (nasir_stehr@olson.co.uk) was October 07, 2014 11:25: Project "project133" was removed ``` -### githost.log +## `githost.log` This file lives in `/var/log/gitlab/gitlab-rails/githost.log` for -omnibus package or in `/home/git/gitlab/log/githost.log` for +Omnibus GitLab packages or in `/home/git/gitlab/log/githost.log` for installations from source. GitLab has to interact with Git repositories but in some rare cases @@ -81,10 +81,10 @@ December 03, 2014 13:20 -> ERROR -> Command failed [1]: /usr/bin/git --git-dir=/ error: failed to push some refs to '/Users/vsizov/gitlab-development-kit/repositories/gitlabhq/gitlab_git.git' ``` -### sidekiq.log +## `sidekiq.log` This file lives in `/var/log/gitlab/gitlab-rails/sidekiq.log` for -omnibus package or in `/home/git/gitlab/log/sidekiq.log` for +Omnibus GitLab packages or in `/home/git/gitlab/log/sidekiq.log` for installations from source. GitLab uses background jobs for processing tasks which can take a long @@ -96,10 +96,10 @@ this file. For example: 2014-06-10T18:18:26Z 14299 TID-55uqo INFO: Booting Sidekiq 3.0.0 with redis options {:url=>"redis://localhost:6379/0", :namespace=>"sidekiq"} ``` -### gitlab-shell.log +## `gitlab-shell.log` This file lives in `/var/log/gitlab/gitlab-shell/gitlab-shell.log` for -omnibus package or in `/home/git/gitlab-shell/gitlab-shell.log` for +Omnibus GitLab packages or in `/home/git/gitlab-shell/gitlab-shell.log` for installations from source. GitLab shell is used by Gitlab for executing Git commands and provide @@ -110,10 +110,10 @@ I, [2015-02-13T06:17:00.671315 #9291] INFO -- : Adding project root/example.git I, [2015-02-13T06:17:00.679433 #9291] INFO -- : Moving existing hooks directory and symlinking global hooks directory for /var/opt/gitlab/git-data/repositories/root/example.git. ``` -### unicorn\_stderr.log +## `unicorn\_stderr.log` This file lives in `/var/log/gitlab/unicorn/unicorn_stderr.log` for -omnibus package or in `/home/git/gitlab/log/unicorn_stderr.log` for +Omnibus GitLab packages or in `/home/git/gitlab/log/unicorn_stderr.log` for installations from source. Unicorn is a high-performance forking Web server which is used for @@ -136,3 +136,13 @@ I, [2015-02-13T07:16:01.530733 #9047] INFO -- : reaped #<Process::Status: pid 9 I, [2015-02-13T07:16:01.534501 #13379] INFO -- : worker=1 spawned pid=13379 I, [2015-02-13T07:16:01.534848 #13379] INFO -- : worker=1 ready ``` + +## `repocheck.log` + +This file lives in `/var/log/gitlab/gitlab-rails/repocheck.log` for +Omnibus GitLab packages or in `/home/git/gitlab/log/repocheck.log` for +installations from source. + +It logs information whenever a [repository check is run][repocheck] on a project. + +[repocheck]: repository_checks.md diff --git a/doc/api/commits.md b/doc/api/commits.md index 0170af00e0e..5c11d0f83bb 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -183,6 +183,44 @@ Example response: } ``` +## Cherry pick a commit + +> [Introduced][ce-8047] in GitLab 8.15. + +Cherry picks a commit to a given branch. + +``` +POST /projects/:id/repository/commits/:sha/cherry_pick +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user +| `sha` | string | yes | The commit hash | +| `branch` | string | yes | The name of the branch | + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "branch=master" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/cherry_pick" +``` + +Example response: + +```json +{ + "id": "8b090c1b79a14f2bd9e8a738f717824ff53aebad", + "short_id": "8b090c1b", + "title": "Feature added", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "created_at": "2016-12-12T20:10:39.000+01:00", + "committer_name": "Administrator", + "committer_email": "admin@example.com", + "message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n" +} +``` + ## Get the diff of a commit Get the diff of a commit in a project. @@ -438,3 +476,4 @@ Example response: ``` [ce-6096]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6096 "Multi-file commit" +[ce-8047]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8047 diff --git a/doc/api/enviroments.md b/doc/api/enviroments.md index 87a5fa67124..1299aca8c45 100644 --- a/doc/api/enviroments.md +++ b/doc/api/enviroments.md @@ -22,8 +22,9 @@ Example response: [ { "id": 1, - "name": "Env1", - "external_url": "https://env1.example.gitlab.com" + "name": "review/fix-foo", + "slug": "review-fix-foo-dfjre3", + "external_url": "https://review-fix-foo-dfjre3.example.gitlab.com" } ] ``` @@ -54,6 +55,7 @@ Example response: { "id": 1, "name": "deploy", + "slug": "deploy", "external_url": "https://deploy.example.gitlab.com" } ``` @@ -85,6 +87,7 @@ Example response: { "id": 1, "name": "staging", + "slug": "staging", "external_url": "https://staging.example.gitlab.com" } ``` @@ -112,6 +115,7 @@ Example response: { "id": 1, "name": "deploy", + "slug": "deploy", "external_url": "https://deploy.example.gitlab.com" } ``` diff --git a/doc/api/groups.md b/doc/api/groups.md index 5e6f498c365..134d7bda22f 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -50,12 +50,17 @@ GET /groups/:id/projects Parameters: -- `archived` (optional) - if passed, limit by archived status -- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private` -- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at` -- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` -- `search` (optional) - Return list of authorized projects according to a search criteria -- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or path of a group | +| `archived` | boolean | no | Limit by archived status | +| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` | +| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` | +| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` | +| `search` | string | no | Return list of authorized projects matching the search criteria | +| `simple` | boolean | no | Return only the ID, URL, name, and path of each project | + +Example response: ```json [ diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 81df55ab4ab..662cc9da733 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -429,7 +429,7 @@ DELETE /projects/:id/merge_requests/:merge_request_id | `merge_request_id` | integer | yes | The ID of a project's merge request | ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_request/85 +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_requests/85 ``` ## Accept MR diff --git a/doc/api/projects.md b/doc/api/projects.md index 0bc2a5210aa..edffad555a5 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -664,7 +664,6 @@ Parameters: | `path` | string | no | Custom repository name for new project. By default generated based on name | | `default_branch` | string | no | `master` by default | | `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) | -| `default_branch` | string | no | `master` by default | | `description` | string | no | Short project description | | `issues_enabled` | boolean | no | Enable issues for this project | | `merge_requests_enabled` | boolean | no | Enable merge requests for this project | diff --git a/doc/api/services.md b/doc/api/services.md index a5d733fe6c7..3dad953cd1e 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -139,6 +139,43 @@ Get Buildkite service settings for a project. GET /projects/:id/services/buildkite ``` +## Build-Emails + +Get emails for GitLab CI builds. + +### Create/Edit Build-Emails service + +Set Build-Emails service for a project. + +``` +PUT /projects/:id/services/builds-email +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `recipients` | string | yes | Comma-separated list of recipient email addresses | +| `add_pusher` | boolean | no | Add pusher to recipients list | +| `notify_only_broken_builds` | boolean | no | Notify only broken builds | + + +### Delete Build-Emails service + +Delete Build-Emails service for a project. + +``` +DELETE /projects/:id/services/builds-email +``` + +### Get Build-Emails service settings + +Get Build-Emails service settings for a project. + +``` +GET /projects/:id/services/builds-email +``` + ## Campfire Simple web-based real-time group chat @@ -476,12 +513,11 @@ PUT /projects/:id/services/jira | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `active` | boolean| no | Enable/disable the JIRA service. | | `url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project, e.g., `https://jira.example.com`. | | `project_key` | string | yes | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. | | `username` | string | no | The username of the user created to be used with GitLab/JIRA. | | `password` | string | no | The password of the user created to be used with GitLab/JIRA. | -| `jira_issue_transition_id` | string | no | 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][trans]). By default, this ID is set to `2`. | +| `jira_issue_transition_id` | integer | no | 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][trans]). By default, this ID is set to `2`. | ### Delete JIRA service @@ -491,6 +527,78 @@ Remove all previously JIRA settings from a project. DELETE /projects/:id/services/jira ``` +## Mattermost Slash Commands + +Ability to receive slash commands from a Mattermost chat instance. + +### Create/Edit Mattermost Slash Command service + +Set Mattermost Slash Command for a project. + +``` +PUT /projects/:id/services/mattermost-slash-commands +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `token` | string | yes | The Mattermost token | + + +### Delete Mattermost Slash Command service + +Delete Mattermost Slash Command service for a project. + +``` +DELETE /projects/:id/services/mattermost-slash-commands +``` + +### Get Mattermost Slash Command service settings + +Get Mattermost Slash Command service settings for a project. + +``` +GET /projects/:id/services/mattermost-slash-commands +``` + +## Pipeline-Emails + +Get emails for GitLab CI pipelines. + +### Create/Edit Pipeline-Emails service + +Set Pipeline-Emails service for a project. + +``` +PUT /projects/:id/services/pipelines-email +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `recipients` | string | yes | Comma-separated list of recipient email addresses | +| `add_pusher` | boolean | no | Add pusher to recipients list | +| `notify_only_broken_builds` | boolean | no | Notify only broken pipelines | + + +### Delete Pipeline-Emails service + +Delete Pipeline-Emails service for a project. + +``` +DELETE /projects/:id/services/pipelines-email +``` + +### Get Pipeline-Emails service settings + +Get Pipeline-Emails service settings for a project. + +``` +GET /projects/:id/services/pipelines-email +``` + ## PivotalTracker Project Management Software (Source Commits Endpoint) diff --git a/doc/api/snippets.md b/doc/api/snippets.md new file mode 100644 index 00000000000..5a5dc162ffe --- /dev/null +++ b/doc/api/snippets.md @@ -0,0 +1,232 @@ +# Snippets + +> [Introduced][ce-6373] in GitLab 8.15. + +### Snippet visibility level + +Snippets in GitLab can be either private, internal, or public. +You can set it with the `visibility_level` field in the snippet. + +Constants for snippet visibility levels are: + +| Visibility | Visibility level | Description | +| ---------- | ---------------- | ----------- | +| Private | `0` | The snippet is visible only to the snippet creator | +| Internal | `10` | The snippet is visible for any logged in user | +| Public | `20` | The snippet can be accessed without any authentication | + +## List snippets + +Get a list of current user's snippets. + +``` +GET /snippets +``` + +## Single snippet + +Get a single snippet. + +``` +GET /snippets/:id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | Integer | yes | The ID of a snippet | + +``` bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/1 +``` + +Example response: + +``` json +{ + "id": 1, + "title": "test", + "file_name": "add.rb", + "author": { + "id": 1, + "username": "john_smith", + "email": "john@example.com", + "name": "John Smith", + "state": "active", + "created_at": "2012-05-23T08:00:58Z" + }, + "expires_at": null, + "updated_at": "2012-06-28T10:52:04Z", + "created_at": "2012-06-28T10:52:04Z", + "web_url": "http://example.com/snippets/1", +} +``` + +## Create new snippet + +Creates a new snippet. The user must have permission to create new snippets. + +``` +POST /snippets +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `title` | String | yes | The title of a snippet | +| `file_name` | String | yes | The name of a snippet file | +| `content` | String | yes | The content of a snippet | +| `visibility_level` | Integer | yes | The snippet's visibility | + + +``` bash +curl --request POST --data '{"title": "This is a snippet", "content": "Hello world", "file_name": "test.txt", "visibility_level": 10 }' --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets +``` + +Example response: + +``` json +{ + "id": 1, + "title": "This is a snippet", + "file_name": "test.txt", + "author": { + "id": 1, + "username": "john_smith", + "email": "john@example.com", + "name": "John Smith", + "state": "active", + "created_at": "2012-05-23T08:00:58Z" + }, + "expires_at": null, + "updated_at": "2012-06-28T10:52:04Z", + "created_at": "2012-06-28T10:52:04Z", + "web_url": "http://example.com/snippets/1", +} +``` + +## Update snippet + +Updates an existing snippet. The user must have permission to change an existing snippet. + +``` +PUT /snippets/:id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | Integer | yes | The ID of a snippet | +| `title` | String | no | The title of a snippet | +| `file_name` | String | no | The name of a snippet file | +| `content` | String | no | The content of a snippet | +| `visibility_level` | Integer | no | The snippet's visibility | + + +``` bash +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data '{"title": "foo", "content": "bar"}' https://gitlab.example.com/api/v3/snippets/1 +``` + +Example response: + +``` json +{ + "id": 1, + "title": "test", + "file_name": "add.rb", + "author": { + "id": 1, + "username": "john_smith", + "email": "john@example.com", + "name": "John Smith", + "state": "active", + "created_at": "2012-05-23T08:00:58Z" + }, + "expires_at": null, + "updated_at": "2012-06-28T10:52:04Z", + "created_at": "2012-06-28T10:52:04Z", + "web_url": "http://example.com/snippets/1", +} +``` + +## Delete snippet + +Deletes an existing snippet. + +``` +DELETE /snippets/:id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | Integer | yes | The ID of a snippet | + + +``` +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/snippets/1" +``` + +upon successful delete a `204 No content` HTTP code shall be expected, with no data, +but if the snippet is non-existent, a `404 Not Found` will be returned. + +## Explore all public snippets + +``` +GET /snippets/public +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `per_page` | Integer | no | number of snippets to return per page | +| `page` | Integer | no | the page to retrieve | + +``` bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/public?per_page=2&page=1 +``` + +Example response: + +``` json +[ + { + "author": { + "avatar_url": "http://www.gravatar.com/avatar/edaf55a9e363ea263e3b981d09e0f7f7?s=80&d=identicon", + "id": 12, + "name": "Libby Rolfson", + "state": "active", + "username": "elton_wehner", + "web_url": "http://localhost:3000/elton_wehner" + }, + "created_at": "2016-11-25T16:53:34.504Z", + "file_name": "oconnerrice.rb", + "id": 49, + "raw_url": "http://localhost:3000/snippets/49/raw", + "title": "Ratione cupiditate et laborum temporibus.", + "updated_at": "2016-11-25T16:53:34.504Z", + "web_url": "http://localhost:3000/snippets/49" + }, + { + "author": { + "avatar_url": "http://www.gravatar.com/avatar/36583b28626de71061e6e5a77972c3bd?s=80&d=identicon", + "id": 16, + "name": "Llewellyn Flatley", + "state": "active", + "username": "adaline", + "web_url": "http://localhost:3000/adaline" + }, + "created_at": "2016-11-25T16:53:34.479Z", + "file_name": "muellershields.rb", + "id": 48, + "raw_url": "http://localhost:3000/snippets/48/raw", + "title": "Minus similique nesciunt vel fugiat qui ullam sunt.", + "updated_at": "2016-11-25T16:53:34.479Z", + "web_url": "http://localhost:3000/snippets/48" + } +] +``` + diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 9dd84a5ff81..bad0233a9ce 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -86,6 +86,13 @@ will later see, is exposed in various places within GitLab. Each time a job that has an environment specified and succeeds, a deployment is recorded, remembering the Git SHA and environment name. +>**Note:** +Starting with GitLab 8.15, the environment name is exposed to the Runner in +two forms: `$CI_ENVIRONMENT_NAME`, and `$CI_ENVIRONMENT_SLUG`. The first is +the name given in `.gitlab-ci.yml` (with any variables expanded), while the +second is a "cleaned-up" version of the name, suitable for use in URLs, DNS, +etc. + To sum up, with the above `.gitlab-ci.yml` we have achieved that: - All branches will run the `test` and `build` jobs. @@ -157,7 +164,7 @@ that can be found in the deployments page job with the commit associated with it. >**Note:** -Bare in mind that your mileage will vary and it's entirely up to how you define +Bear in mind that your mileage will vary and it's entirely up to how you define the deployment process in the job's `script` whether the rollback succeeds or not. GitLab CI is just following orders. @@ -248,7 +255,7 @@ deploy_review: - echo "Deploy a review app" environment: name: review/$CI_BUILD_REF_NAME - url: https://$CI_BUILD_REF_NAME.example.com + url: https://$CI_BUILD_REF_SLUG.review.example.com only: - branches except: @@ -259,15 +266,25 @@ Let's break it down in pieces. The job's name is `deploy_review` and it runs on the `deploy` stage. The `script` at this point is fictional, you'd have to use your own based on your deployment. Then, we set the `environment` with the `environment:name` being `review/$CI_BUILD_REF_NAME`. Now that's an interesting -one. Since the [environment name][env-name] can contain also slashes (`/`), we -can use this pattern to distinguish between dynamic environments and the regular +one. Since the [environment name][env-name] can contain slashes (`/`), we can +use this pattern to distinguish between dynamic environments and the regular ones. So, the first part is `review`, followed by a `/` and then `$CI_BUILD_REF_NAME` -which takes the value of the branch name. We also use the same -`$CI_BUILD_REF_NAME` value in the `environment:url` so that the environment -can get a specific and distinct URL for each branch. Again, the way you set up -the webserver to serve these requests is based on your setup. +which takes the value of the branch name. Since `$CI_BUILD_REF_NAME` itself may +also contain `/`, or other characters that would be invalid in a domain name or +URL, we use `$CI_ENVIRONMENT_SLUG` in the `environment:url` so that the +environment can get a specific and distinct URL for each branch. In this case, +given a `$CI_BUILD_REF_NAME` of `100-Do-The-Thing`, the URL will be something +like `https://review-100-do-the-4f99a2.example.com`. Again, the way you set up +the web server to serve these requests is based on your setup. + +You could also use `$CI_BUILD_REF_SLUG` in `environment:url`, e.g.: +`https://$CI_BUILD_REF_SLUG.review.example.com`. We use `$CI_ENVIRONMENT_SLUG` +here because it is guaranteed to be unique, but if you're using a workflow like +[GitLab Flow][gitlab-flow], collisions are very unlikely, and you may prefer +environment names to be more closely based on the branch name - the example +above would give you an URL like `https://100-do-the-thing.review.example.com` Last but not least, we tell the job to run [`only`][only] on branches [`except`][only] master. @@ -299,7 +316,7 @@ deploy_review: - echo "Deploy a review app" environment: name: review/$CI_BUILD_REF_NAME - url: https://$CI_BUILD_REF_NAME.example.com + url: https://$CI_ENVIRONMENT_SLUG.example.com only: - branches except: @@ -329,16 +346,16 @@ deploy_prod: A more realistic example would include copying files to a location where a webserver (NGINX) could then read and serve. The example below will copy the -`public` directory to `/srv/nginx/$CI_BUILD_REF_NAME/public`: +`public` directory to `/srv/nginx/$CI_BUILD_REF_SLUG/public`: ```yaml review_app: stage: deploy script: - - rsync -av --delete public /srv/nginx/$CI_BUILD_REF_NAME + - rsync -av --delete public /srv/nginx/$CI_BUILD_REF_SLUG environment: name: review/$CI_BUILD_REF_NAME - url: https://$CI_BUILD_REF_NAME.example.com + url: https://$CI_BUILD_REF_SLUG.example.com ``` It is assumed that the user has already setup NGINX and GitLab Runner in the @@ -346,7 +363,7 @@ server this job will run on. >**Note:** Be sure to check out the [limitations](#limitations) section for some edge -cases regarding naming of you branches and Review Apps. +cases regarding naming of your branches and Review Apps. --- @@ -418,7 +435,7 @@ deploy_review: - echo "Deploy a review app" environment: name: review/$CI_BUILD_REF_NAME - url: https://$CI_BUILD_REF_NAME.example.com + url: https://$CI_ENVIRONMENT_SLUG.example.com on_stop: stop_review only: - branches @@ -480,9 +497,8 @@ exist, you should see something like: ## Checkout deployments locally -Since 8.13, a reference in the git repository is saved for each deployment. So -knowing what the state is of your current environments is only a `git fetch` -away. +Since 8.13, a reference in the git repository is saved for each deployment, so +knowing the state of your current environments is only a `git fetch` away. In your git config, append the `[remote "<your-remote>"]` block with an extra fetch line: @@ -493,10 +509,6 @@ fetch = +refs/environments/*:refs/remotes/origin/environments/* ## Limitations -1. If the branch name contains special characters (`/`), and you use the - `$CI_BUILD_REF_NAME` variable to dynamically create environments, there might - be complications during your Review Apps deployment. Follow the - [issue 22849][ce-22849] for more information. 1. You are limited to use only the [CI predefined variables][variables] in the `environment: name`. If you try to re-use variables defined inside `script` as part of the environment name, it will not work. @@ -520,6 +532,6 @@ Below are some links you may find interesting: [only]: yaml/README.md#only-and-except [onstop]: yaml/README.md#environment-on_stop [ce-7015]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7015 -[ce-22849]: https://gitlab.com/gitlab-org/gitlab-ce/issues/22849 +[gitlab-flow]: ../workflow/gitlab_flow.md [gitlab runner]: https://docs.gitlab.com/runner/ [git-strategy]: yaml/README.md#git-strategy diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md index a66165dc973..c679ea4e298 100644 --- a/doc/ci/review_apps/index.md +++ b/doc/ci/review_apps/index.md @@ -33,7 +33,7 @@ built and deployed under a dynamic environment and can be previewed with an also dynamically URL. The details of the Review Apps implementation depend widely on your real -technology stack and on your deployment process. The simplest case it to +technology stack and on your deployment process. The simplest case is to deploy a simple static HTML website, but it will not be that straightforward when your app is using a database for example. To make a branch be deployed on a temporary instance and booting up this instance with all required software diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index d142fe266a2..eb540a50606 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -40,6 +40,7 @@ version of Runner required. | **CI_BUILD_NAME** | all | 0.5 | The name of the build as defined in `.gitlab-ci.yml` | | **CI_BUILD_STAGE** | all | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` | | **CI_BUILD_REF_NAME** | all | all | The branch or tag name for which project is built | +| **CI_BUILD_REF_SLUG** | 8.15 | all | `$CI_BUILD_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. | | **CI_BUILD_REPO** | all | all | The URL to clone the Git repository | | **CI_BUILD_TRIGGERED** | all | 0.5 | The flag to indicate that build was [triggered] | | **CI_BUILD_MANUAL** | 8.12 | all | The flag to indicate that build was manually started | @@ -51,6 +52,8 @@ version of Runner required. | **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name | | **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project | | **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the build is run | +| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this build | +| **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. | | **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry | | **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project | | **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used | diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 5f88974d360..a62193700fc 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -690,18 +690,12 @@ The `stop_review_app` job is **required** to have the following keywords defined #### dynamic environments > [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6. + `$CI_ENVIRONMENT_SLUG` was [introduced][ce-7983] in GitLab 8.15 `environment` can also represent a configuration hash with `name` and `url`. These parameters can use any of the defined [CI variables](#variables) (including predefined, secure variables and `.gitlab-ci.yml` variables). ->**Note:** -Be aware than if the branch name contains special characters and you use the -`$CI_BUILD_REF_NAME` variable to dynamically create environments, there might -be complications during deployment. Follow the -[issue 22849](https://gitlab.com/gitlab-org/gitlab-ce/issues/22849) for more -information. - For example: ``` @@ -709,15 +703,17 @@ deploy as review app: stage: deploy script: make deploy environment: - name: review-apps/$CI_BUILD_REF_NAME - url: https://$CI_BUILD_REF_NAME.review.example.com/ + name: review/$CI_BUILD_REF_NAME + url: https://$CI_ENVIRONMENT_SLUG.example.com/ ``` The `deploy as review app` job will be marked as deployment to dynamically -create the `review-apps/$CI_BUILD_REF_NAME` environment, which `$CI_BUILD_REF_NAME` -is an [environment variable][variables] set by the Runner. If for example the -`deploy as review app` job was run in a branch named `pow`, this environment -should be accessible under `https://pow.review.example.com/`. +create the `review/$CI_BUILD_REF_NAME` environment, where `$CI_BUILD_REF_NAME` +is an [environment variable][variables] set by the Runner. The +`$CI_ENVIRONMENT_SLUG` variable is based on the environment name, but suitable +for inclusion in URLs. In this case, if the `deploy as review app` job was run +in a branch named `pow`, this environment would be accessible with an URL like +`https://review-pow-aaaaaa.example.com/`. This of course implies that the underlying server which hosts the application is properly configured. @@ -1246,3 +1242,4 @@ CI with various languages. [environment]: ../environments.md [ce-6669]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6669 [variables]: ../variables/README.md +[ce-7983]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7983 diff --git a/doc/development/ux_guide/basics.md b/doc/development/ux_guide/basics.md index 76b739386a5..e81729556d8 100644 --- a/doc/development/ux_guide/basics.md +++ b/doc/development/ux_guide/basics.md @@ -5,6 +5,7 @@ * [Typography](#typography) * [Icons](#icons) * [Color](#color) +* [Cursors](#cursors) --- @@ -59,3 +60,18 @@ GitLab uses Font Awesome icons throughout our interface. > TODO: Establish a perspective for color in terms of our personality and rationalize with Marketing usage. +--- + +## Cursors +The mouse cursor is key in helping users understand how to interact with elements on the screen. + +| | | +| :------: | :------- | +| ![Default cursor](img/cursors-default.png) | Default cursor | +| ![Pointer cursor](img/cursors-pointer.png) | Pointer cursor: used to indicate that you can click on an element to invoke a command or navigate, such as links and buttons | +| ![Move cursor](img/cursors-move.png) | Move cursor: used to indicate that you can move an element around on the screen | +| ![Pan opened cursor](img/cursors-panopened.png) | Pan cursor (opened): indicates that you can grab and move the entire canvas, affecting what is seen in the view port. | +| ![Pan closed cursor](img/cursors-panclosed.png) | Pan cursor (closed): indicates that you are actively panning the canvas. | +| ![I-beam cursor](img/cursors-ibeam.png) | I-beam cursor: indicates that this is either text that you can select and copy, or a text field that you can click into to enter text. | + + diff --git a/doc/development/ux_guide/img/cursors-default.png b/doc/development/ux_guide/img/cursors-default.png Binary files differnew file mode 100644 index 00000000000..c188ec4e351 --- /dev/null +++ b/doc/development/ux_guide/img/cursors-default.png diff --git a/doc/development/ux_guide/img/cursors-ibeam.png b/doc/development/ux_guide/img/cursors-ibeam.png Binary files differnew file mode 100644 index 00000000000..86f28639982 --- /dev/null +++ b/doc/development/ux_guide/img/cursors-ibeam.png diff --git a/doc/development/ux_guide/img/cursors-move.png b/doc/development/ux_guide/img/cursors-move.png Binary files differnew file mode 100644 index 00000000000..a9c610eaa88 --- /dev/null +++ b/doc/development/ux_guide/img/cursors-move.png diff --git a/doc/development/ux_guide/img/cursors-panclosed.png b/doc/development/ux_guide/img/cursors-panclosed.png Binary files differnew file mode 100644 index 00000000000..6d247a765ac --- /dev/null +++ b/doc/development/ux_guide/img/cursors-panclosed.png diff --git a/doc/development/ux_guide/img/cursors-panopened.png b/doc/development/ux_guide/img/cursors-panopened.png Binary files differnew file mode 100644 index 00000000000..76f2eeda831 --- /dev/null +++ b/doc/development/ux_guide/img/cursors-panopened.png diff --git a/doc/development/ux_guide/img/cursors-pointer.png b/doc/development/ux_guide/img/cursors-pointer.png Binary files differnew file mode 100644 index 00000000000..d86dd955fa7 --- /dev/null +++ b/doc/development/ux_guide/img/cursors-pointer.png diff --git a/doc/project_services/img/kubernetes_configuration.png b/doc/project_services/img/kubernetes_configuration.png Binary files differnew file mode 100644 index 00000000000..349a2dc8456 --- /dev/null +++ b/doc/project_services/img/kubernetes_configuration.png diff --git a/doc/project_services/kubernetes.md b/doc/project_services/kubernetes.md new file mode 100644 index 00000000000..cb577b608b4 --- /dev/null +++ b/doc/project_services/kubernetes.md @@ -0,0 +1,38 @@ +# GitLab Kubernetes / OpenShift integration + +GitLab can be configured to interact with Kubernetes, or other systems using the +Kubernetes API (such as OpenShift). + +Each project can be configured to connect to a different Kubernetes cluster, see +the [configuration](#configuration) section. + +If you have a single cluster that you want to use for all your projects, +you can pre-fill the settings page with a default template. To configure the +template, see the [Services Templates](services-templates.md) document. + +## Configuration + +![Kubernetes configuration settings](img/kubernetes_configuration.png) + +The Kubernetes service takes the following arguments: + +1. Kubernetes namespace +1. API URL +1. Service token +1. Custom CA bundle + +The API URL is the URL that GitLab uses to access the Kubernetes API. Kubernetes +exposes several APIs - we want the "base" URL that is common to all of them, +e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`. + +GitLab authenticates against Kubernetes using service tokens, which are +scoped to a particular `namespace`. If you don't have a service token yet, +you can follow the +[Kubernetes documentation](http://kubernetes.io/docs/user-guide/service-accounts/) +to create one. You can also view or create service tokens in the +[Kubernetes dashboard](http://kubernetes.io/docs/user-guide/ui/) - visit +`Config -> Secrets`. + +Fill in the service token and namespace according to the values you just got. +If the API is using a self-signed TLS certificate, you'll also need to include +the `ca.crt` contents as the `Custom CA bundle`. diff --git a/doc/project_services/mattermost_slash_commands.md b/doc/project_services/mattermost_slash_commands.md index 6fcbf6f1642..1a7c13a29b4 100644 --- a/doc/project_services/mattermost_slash_commands.md +++ b/doc/project_services/mattermost_slash_commands.md @@ -65,7 +65,7 @@ the administrator console. ### Step 3. Create a new custom slash command in Mattermost -Now that you have enabled the custom slash commands in Mattermost and opened +Now that you have enabled custom slash commands in Mattermost and opened the Mattermost slash commands service in GitLab, it's time to copy these values in a new slash command. @@ -128,20 +128,16 @@ GitLab using the Mattermost commands. ## Available slash commands -The available slash commands so far are: +The available slash commands are: | Command | Description | Example | | ------- | ----------- | ------- | -| `/<trigger> issue create <title>\n<description>` | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | `/trigger issue create We need to change the homepage` | -| `/<trigger> issue show <issue-number>` | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | `/trigger issue show 42` | -| `/<trigger> deploy <environment> to <environment>` | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | `/trigger deploy staging to production` | +| <kbd>/<trigger> issue new <title> <kbd>⇧ Shift</kbd>+<kbd>↵ Enter</kbd> <description></kbd> | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | <samp>/gitlab issue new We need to change the homepage</samp> | +| <kbd>/<trigger> issue show <issue-number></kbd> | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | <samp>/gitlab issue show 42</samp> | +| <kbd>/<trigger> deploy <environment> to <environment></kbd> | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | <samp>/gitlab deploy staging to production</samp> | -To see a list of available commands that can interact with GitLab, type the -trigger word followed by `help`: - -``` -/my-project help -``` +To see a list of available commands to interact with GitLab, type the +trigger word followed by <kbd>help</kbd>. Example: <samp>/gitlab help</samp> ![Mattermost bot available commands](img/mattermost_bot_available_commands.png) diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index 890f7525b0e..a7bcd186a8c 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -42,6 +42,7 @@ further configuration instructions and details. Contributions are welcome. | [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | | [JIRA](jira.md) | JIRA issue tracker | | JetBrains TeamCity CI | A continuous integration and build server | +| [Kubernetes](kubernetes.md) | A containerized deployment service | | [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands | | 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/update/8.13-to-8.14.md b/doc/update/8.13-to-8.14.md index a0e895773ce..c64d3407461 100644 --- a/doc/update/8.13-to-8.14.md +++ b/doc/update/8.13-to-8.14.md @@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-14-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch --all --tags -sudo -u git -H git checkout v4.0.0 +sudo -u git -H git checkout v4.0.3 ``` ### 6. Update gitlab-workhorse diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 4d24eb21976..f6484688721 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -319,6 +319,40 @@ Here's a sample video: ![Sample Video](img/markdown_video.mp4) +### Math + +> If this is not rendered correctly, see +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#math + +It is possible to have math written with the LaTeX syntax rendered using [KaTeX][katex]. + +Math written inside ```$``$``` will be rendered inline with the text. + +Math written inside triple back quotes, with the language declared as `math`, will be rendered on a separate line. + +Example: + + This math is inline $`a^2+b^2=c^2`$. + + This is on a separate line + ```math + a^2+b^2=c^2 + ``` + +Becomes: + +This math is inline $`a^2+b^2=c^2`$. + +This is on a separate line +```math +a^2+b^2=c^2 +``` + +_Be advised that KaTeX only supports a [subset][katex-subset] of LaTeX._ + +>**Note:** +This also works for the asciidoctor `:stem: latexmath`. For details see the [asciidoctor user manual][asciidoctor-manual]. + ## Standard Markdown ### Headers @@ -764,3 +798,6 @@ A link starting with a `/` is relative to the wiki root. [markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md [rouge]: http://rouge.jneen.net/ "Rouge website" [redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website" +[katex]: https://github.com/Khan/KaTeX "KaTeX website" +[katex-subset]: https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX "Macros supported by KaTeX" +[asciidoctor-manual]: http://asciidoctor.org/docs/user-manual/#activating-stem-support "Asciidoctor user manual"
\ No newline at end of file diff --git a/features/admin/active_tab.feature b/features/admin/active_tab.feature deleted file mode 100644 index f5bb06dea7d..00000000000 --- a/features/admin/active_tab.feature +++ /dev/null @@ -1,54 +0,0 @@ -@admin -Feature: Admin Active Tab - Background: - Given I sign in as an admin - - Scenario: On Admin Home - Given I visit admin page - Then the active main tab should be Overview - And no other main tabs should be active - - Scenario: On Admin Projects - Given I visit admin projects page - Then the active main tab should be Overview - And the active sub tab should be Projects - And no other main tabs should be active - And no other sub tabs should be active - - Scenario: On Admin Groups - Given I visit admin groups page - Then the active main tab should be Overview - And the active sub tab should be Groups - And no other main tabs should be active - And no other sub tabs should be active - - Scenario: On Admin Users - Given I visit admin users page - Then the active main tab should be Overview - And the active sub tab should be Users - And no other main tabs should be active - And no other sub tabs should be active - - Scenario: On Admin Logs - Given I visit admin logs page - Then the active main tab should be Monitoring - And the active sub tab should be Logs - And no other main tabs should be active - And no other sub tabs should be active - - Scenario: On Admin Messages - Given I visit admin messages page - Then the active main tab should be Messages - And no other main tabs should be active - - Scenario: On Admin Hooks - Given I visit admin hooks page - Then the active main tab should be Hooks - And no other main tabs should be active - - Scenario: On Admin Resque - Given I visit admin Resque page - Then the active main tab should be Monitoring - And the active sub tab should be Resque - And no other main tabs should be active - And no other sub tabs should be active diff --git a/features/admin/settings.feature b/features/admin/settings.feature deleted file mode 100644 index e38eea2cfed..00000000000 --- a/features/admin/settings.feature +++ /dev/null @@ -1,19 +0,0 @@ -@admin -Feature: Admin Settings - Background: - Given I sign in as an admin - And I visit admin settings page - - Scenario: Change application settings - When I modify settings and save form - Then I should see application settings saved - - Scenario: Change Slack Service Template settings - When I click on "Service Templates" - And I click on "Slack" service - And I fill out Slack settings - Then I check all events and submit form - And I should see service template settings saved - Then I click on "Slack" service - And I should see all checkboxes checked - And I should see Slack settings saved diff --git a/features/steps/admin/active_tab.rb b/features/steps/admin/active_tab.rb deleted file mode 100644 index 9b1689a8198..00000000000 --- a/features/steps/admin/active_tab.rb +++ /dev/null @@ -1,41 +0,0 @@ -class Spinach::Features::AdminActiveTab < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedActiveTab - - step 'the active main tab should be Overview' do - ensure_active_main_tab('Overview') - end - - step 'the active sub tab should be Projects' do - ensure_active_sub_tab('Projects') - end - - step 'the active sub tab should be Groups' do - ensure_active_sub_tab('Groups') - end - - step 'the active sub tab should be Users' do - ensure_active_sub_tab('Users') - end - - step 'the active main tab should be Hooks' do - ensure_active_main_tab('Hooks') - end - - step 'the active main tab should be Monitoring' do - ensure_active_main_tab('Monitoring') - end - - step 'the active sub tab should be Resque' do - ensure_active_sub_tab('Background Jobs') - end - - step 'the active sub tab should be Logs' do - ensure_active_sub_tab('Logs') - end - - step 'the active main tab should be Messages' do - ensure_active_main_tab('Messages') - end -end diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb index f5fddab357d..c1d1eca9116 100644 --- a/features/steps/group/milestones.rb +++ b/features/steps/group/milestones.rb @@ -131,5 +131,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps issue.labels << project.labels.find_by(title: 'bug') issue.labels << project.labels.find_by(title: 'feature') end + + current_user.refresh_authorized_projects end end diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb index 5e7d539add6..a3bebfa4b71 100644 --- a/features/steps/project/snippets.rb +++ b/features/steps/project/snippets.rb @@ -22,7 +22,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps end step 'I click link "New snippet"' do - click_link "New snippet" + first(:link, "New snippet").click end step 'I click link "Snippet one"' do diff --git a/features/steps/shared/authentication.rb b/features/steps/shared/authentication.rb index 735e0ef6108..5c3e724746b 100644 --- a/features/steps/shared/authentication.rb +++ b/features/steps/shared/authentication.rb @@ -33,6 +33,6 @@ module SharedAuthentication end def current_user - @user || User.first + @user || User.reorder(nil).first end end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index 2bd8ea745e4..82c07d4f536 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -168,7 +168,7 @@ module SharedPaths end step 'I visit admin projects page' do - visit admin_namespaces_projects_path + visit admin_projects_path end step 'I visit admin users page' do @@ -203,10 +203,6 @@ module SharedPaths visit admin_teams_path end - step 'I visit admin settings page' do - visit admin_application_settings_path - end - step 'I visit spam logs page' do visit admin_spam_logs_path end diff --git a/lib/api/api.rb b/lib/api/api.rb index 67109ceeef9..cec2702e44d 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -64,6 +64,7 @@ module API mount ::API::Session mount ::API::Settings mount ::API::SidekiqMetrics + mount ::API::Snippets mount ::API::Subscriptions mount ::API::SystemHooks mount ::API::Tags diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 73aed624ea7..0950c3d2e88 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -23,9 +23,9 @@ module API success Entities::RepoBranch end params do - requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch' + requires :branch, type: String, desc: 'The name of the branch' end - get ':id/repository/branches/:branch' do + get ':id/repository/branches/:branch', requirements: { branch: /.+/ } do branch = user_project.repository.find_branch(params[:branch]) not_found!("Branch") unless branch @@ -39,11 +39,11 @@ module API success Entities::RepoBranch end params do - requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch' + requires :branch, type: String, desc: 'The name of the branch' optional :developers_can_push, type: Boolean, desc: 'Flag if developers can push to that branch' optional :developers_can_merge, type: Boolean, desc: 'Flag if developers can merge to that branch' end - put ':id/repository/branches/:branch/protect' do + put ':id/repository/branches/:branch/protect', requirements: { branch: /.+/ } do authorize_admin_project branch = user_project.repository.find_branch(params[:branch]) @@ -76,9 +76,9 @@ module API success Entities::RepoBranch end params do - requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch' + requires :branch, type: String, desc: 'The name of the branch' end - put ':id/repository/branches/:branch/unprotect' do + put ':id/repository/branches/:branch/unprotect', requirements: { branch: /.+/ } do authorize_admin_project branch = user_project.repository.find_branch(params[:branch]) @@ -112,9 +112,9 @@ module API desc 'Delete a branch' params do - requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch' + requires :branch, type: String, desc: 'The name of the branch' end - delete ":id/repository/branches/:branch" do + delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do authorize_push_project result = DeleteBranchService.new(user_project, current_user). diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 2670a2d413a..cf2489dbb67 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -1,7 +1,6 @@ require 'mime/types' module API - # Projects commits API class Commits < Grape::API include PaginationParams @@ -121,6 +120,41 @@ module API present paginate(notes), with: Entities::CommitNote end + desc 'Cherry pick commit into a branch' do + detail 'This feature was introduced in GitLab 8.15' + success Entities::RepoCommit + end + params do + requires :sha, type: String, desc: 'A commit sha to be cherry picked' + requires :branch, type: String, desc: 'The name of the branch' + end + post ':id/repository/commits/:sha/cherry_pick' do + authorize! :push_code, user_project + + commit = user_project.commit(params[:sha]) + not_found!('Commit') unless commit + + branch = user_project.repository.find_branch(params[:branch]) + not_found!('Branch') unless branch + + commit_params = { + commit: commit, + create_merge_request: false, + source_project: user_project, + source_branch: commit.cherry_pick_branch_name, + target_branch: params[:branch] + } + + result = ::Commits::CherryPickService.new(user_project, current_user, commit_params).execute + + if result[:status] == :success + branch = user_project.repository.find_branch(params[:branch]) + present user_project.repository.commit(branch.dereferenced_target), with: Entities::RepoCommit + else + render_api_error!(result[:message], 400) + end + end + desc 'Post comment to commit' do success Entities::CommitNote end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 006d5f9f44e..dfbb3ab86dd 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -201,6 +201,19 @@ module API end end + class PersonalSnippet < Grape::Entity + expose :id, :title, :file_name + expose :author, using: Entities::UserBasic + expose :updated_at, :created_at + + expose :web_url do |snippet| + Gitlab::UrlBuilder.build(snippet) + end + expose :raw_url do |snippet| + Gitlab::UrlBuilder.build(snippet) + "/raw" + end + end + class ProjectEntity < Grape::Entity expose :id, :iid expose(:project_id) { |entity| entity.project.id } @@ -616,7 +629,7 @@ module API end class EnvironmentBasic < Grape::Entity - expose :id, :name, :external_url + expose :id, :name, :slug, :external_url end class Environment < EnvironmentBasic diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 80bbd9bb6e4..1a7e68f0528 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -1,6 +1,7 @@ module API # Environments RESTfull API endpoints class Environments < Grape::API + include ::API::Helpers::CustomValidators include PaginationParams before { authenticate! } @@ -29,6 +30,7 @@ module API params do requires :name, type: String, desc: 'The name of the environment to be created' optional :external_url, type: String, desc: 'URL on which this deployment is viewable' + optional :slug, absence: { message: "is automatically generated and cannot be changed" } end post ':id/environments' do authorize! :create_environment, user_project @@ -50,6 +52,7 @@ module API requires :environment_id, type: Integer, desc: 'The environment ID' optional :name, type: String, desc: 'The new environment name' optional :external_url, type: String, desc: 'The new URL on which this deployment is viewable' + optional :slug, absence: { message: "is automatically generated and cannot be changed" } end put ':id/environments/:environment_id' do authorize! :update_environment, user_project diff --git a/lib/api/groups.rb b/lib/api/groups.rb index fbf7513302b..9b9d3df7435 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -1,7 +1,7 @@ module API class Groups < Grape::API include PaginationParams - + before { authenticate! } helpers do @@ -117,12 +117,24 @@ module API success Entities::Project end params do + optional :archived, type: Boolean, default: false, desc: 'Limit by archived status' + optional :visibility, type: String, values: %w[public internal private], + desc: 'Limit by visibility' + optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria' + optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at], + default: 'created_at', desc: 'Return projects ordered by field' + optional :sort, type: String, values: %w[asc desc], default: 'desc', + desc: 'Return projects sorted in ascending and descending order' + optional :simple, type: Boolean, default: false, + desc: 'Return only the ID, URL, name, and path of each project' use :pagination end get ":id/projects" do group = find_group!(params[:id]) projects = GroupProjectsFinder.new(group).execute(current_user) - present paginate(projects), with: Entities::Project, user: current_user + projects = filter_projects(projects) + entity = params[:simple] ? Entities::BasicProjectDetails : Entities::Project + present paginate(projects), with: entity, user: current_user end desc 'Transfer a project to the group namespace. Available only for admin.' do diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 8b0f8deadfa..746849ef4c0 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -7,67 +7,23 @@ module API SUDO_HEADER = "HTTP_SUDO" SUDO_PARAM = :sudo - def private_token - params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER] - end - - def warden - env['warden'] - end - - # Check the Rails session for valid authentication details - # - # Until CSRF protection is added to the API, disallow this method for - # state-changing endpoints - def find_user_from_warden - warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD']) - end - def declared_params(options = {}) options = { include_parent_namespaces: false }.merge(options) declared(params, options).to_h.symbolize_keys end - def find_user_by_private_token - token = private_token - return nil unless token.present? - - User.find_by_authentication_token(token) || User.find_by_personal_access_token(token) - end - def current_user - @current_user ||= find_user_by_private_token - @current_user ||= doorkeeper_guard - @current_user ||= find_user_from_warden - - unless @current_user && Gitlab::UserAccess.new(@current_user).allowed? - return nil - end + return @current_user if defined?(@current_user) - identifier = sudo_identifier + @current_user = initial_current_user - if identifier - # We check for private_token because we cannot allow PAT to be used - forbidden!('Must be admin to use sudo') unless @current_user.is_admin? - forbidden!('Private token must be specified in order to use sudo') unless private_token_used? - - @impersonator = @current_user - @current_user = User.by_username_or_id(identifier) - not_found!("No user id or username for: #{identifier}") if @current_user.nil? - end + sudo! @current_user end - def sudo_identifier - identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER] - - # Regex for integers - if !!(identifier =~ /\A[0-9]+\z/) - identifier.to_i - else - identifier - end + def sudo? + initial_current_user != current_user end def user_project @@ -78,6 +34,14 @@ module API @available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute end + def find_user(id) + if id =~ /^\d+$/ + User.find_by(id: id) + else + User.find_by(username: id) + end + end + def find_project(id) if id =~ /^\d+$/ Project.find_by(id: id) @@ -96,17 +60,6 @@ module API end end - def project_service(project = user_project) - @project_service ||= project.find_or_initialize_service(params[:service_slug].underscore) - @project_service || not_found!("Service") - end - - def service_attributes - @service_attributes ||= project_service.fields.inject([]) do |arr, hash| - arr << hash[:name].to_sym - end - end - def find_group(id) if id =~ /^\d+$/ Group.find_by(id: id) @@ -354,6 +307,69 @@ module API private + def private_token + params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER] + end + + def warden + env['warden'] + end + + # Check the Rails session for valid authentication details + # + # Until CSRF protection is added to the API, disallow this method for + # state-changing endpoints + def find_user_from_warden + warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD']) + end + + def find_user_by_private_token + token = private_token + return nil unless token.present? + + User.find_by_authentication_token(token) || User.find_by_personal_access_token(token) + end + + def initial_current_user + return @initial_current_user if defined?(@initial_current_user) + + @initial_current_user ||= find_user_by_private_token + @initial_current_user ||= doorkeeper_guard + @initial_current_user ||= find_user_from_warden + + unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed? + @initial_current_user = nil + end + + @initial_current_user + end + + def sudo! + return unless sudo_identifier + return unless initial_current_user + + unless initial_current_user.is_admin? + forbidden!('Must be admin to use sudo') + end + + # Only private tokens should be used for the SUDO feature + unless private_token == initial_current_user.private_token + forbidden!('Private token must be specified in order to use sudo') + end + + sudoed_user = find_user(sudo_identifier) + + if sudoed_user + @current_user = sudoed_user + else + not_found!("No user id or username for: #{sudo_identifier}") + end + end + + def sudo_identifier + @sudo_identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER] + end + def add_pagination_headers(paginated_data) header 'X-Total', paginated_data.total_count.to_s header 'X-Total-Pages', paginated_data.total_pages.to_s @@ -386,10 +402,6 @@ module API links.join(', ') end - def private_token_used? - private_token == @current_user.private_token - end - def secret_token Gitlab::Shell.secret_token end diff --git a/lib/api/helpers/custom_validators.rb b/lib/api/helpers/custom_validators.rb new file mode 100644 index 00000000000..0a8f3073a50 --- /dev/null +++ b/lib/api/helpers/custom_validators.rb @@ -0,0 +1,14 @@ +module API + module Helpers + module CustomValidators + class Absence < Grape::Validations::Base + def validate_param!(attr_name, params) + return if params.respond_to?(:key?) && !params.key?(attr_name) + raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:absence) + end + end + end + end +end + +Grape::Validations.register_validator(:absence, ::API::Helpers::CustomValidators::Absence) diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 55bdbc6a47c..5d1fe22f2df 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -143,8 +143,8 @@ module API success Entities::MergeRequest end params do - optional :title, type: String, desc: 'The title of the merge request' - optional :target_branch, type: String, desc: 'The target branch' + optional :title, type: String, allow_blank: false, desc: 'The title of the merge request' + optional :target_branch, type: String, allow_blank: false, desc: 'The target branch' optional :state_event, type: String, values: %w[close reopen merge], desc: 'Status of the merge request' use :optional_params diff --git a/lib/api/services.rb b/lib/api/services.rb index bc427705777..b1e072b4f47 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -1,84 +1,630 @@ module API - # Projects API class Services < Grape::API + services = { + 'asana' => [ + { + required: true, + name: :api_key, + type: String, + desc: 'User API token' + }, + { + required: false, + name: :restrict_to_branch, + type: String, + desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches' + } + ], + 'assembla' => [ + { + required: true, + name: :token, + type: String, + desc: 'The authentication token' + }, + { + required: false, + name: :subdomain, + type: String, + desc: 'Subdomain setting' + } + ], + 'bamboo' => [ + { + required: true, + name: :bamboo_url, + type: String, + desc: 'Bamboo root URL like https://bamboo.example.com' + }, + { + required: true, + name: :build_key, + type: String, + desc: 'Bamboo build plan key like' + }, + { + required: true, + name: :username, + type: String, + desc: 'A user with API access, if applicable' + }, + { + required: true, + name: :password, + type: String, + desc: 'Passord of the user' + } + ], + 'bugzilla' => [ + { + required: true, + name: :new_issue_url, + type: String, + desc: 'New issue URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'Issues URL' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'Project URL' + }, + { + required: false, + name: :description, + type: String, + desc: 'Description' + }, + { + required: false, + name: :title, + type: String, + desc: 'Title' + } + ], + 'buildkite' => [ + { + required: true, + name: :token, + type: String, + desc: 'Buildkite project GitLab token' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'The buildkite project URL' + }, + { + required: false, + name: :enable_ssl_verification, + type: Boolean, + desc: 'Enable SSL verification for communication' + } + ], + 'builds-email' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Comma-separated list of recipient email addresses' + }, + { + required: false, + name: :add_pusher, + type: Boolean, + desc: 'Add pusher to recipients list' + }, + { + required: false, + name: :notify_only_broken_builds, + type: Boolean, + desc: 'Notify only broken builds' + } + ], + 'campfire' => [ + { + required: true, + name: :token, + type: String, + desc: 'Campfire token' + }, + { + required: false, + name: :subdomain, + type: String, + desc: 'Campfire subdomain' + }, + { + required: false, + name: :room, + type: String, + desc: 'Campfire room' + }, + ], + 'custom-issue-tracker' => [ + { + required: true, + name: :new_issue_url, + type: String, + desc: 'New issue URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'Issues URL' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'Project URL' + }, + { + required: false, + name: :description, + type: String, + desc: 'Description' + }, + { + required: false, + name: :title, + type: String, + desc: 'Title' + } + ], + 'drone-ci' => [ + { + required: true, + name: :token, + type: String, + desc: 'Drone CI token' + }, + { + required: true, + name: :drone_url, + type: String, + desc: 'Drone CI URL' + }, + { + required: false, + name: :enable_ssl_verification, + type: Boolean, + desc: 'Enable SSL verification for communication' + } + ], + 'emails-on-push' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Comma-separated list of recipient email addresses' + }, + { + required: false, + name: :disable_diffs, + type: Boolean, + desc: 'Disable code diffs' + }, + { + required: false, + name: :send_from_committer_email, + type: Boolean, + desc: 'Send from committer' + } + ], + 'external-wiki' => [ + { + required: true, + name: :external_wiki_url, + type: String, + desc: 'The URL of the external Wiki' + } + ], + 'flowdock' => [ + { + required: true, + name: :token, + type: String, + desc: 'Flowdock token' + } + ], + 'gemnasium' => [ + { + required: true, + name: :api_key, + type: String, + desc: 'Your personal API key on gemnasium.com' + }, + { + required: true, + name: :token, + type: String, + desc: "The project's slug on gemnasium.com" + } + ], + 'hipchat' => [ + { + required: true, + name: :token, + type: String, + desc: 'The room token' + }, + { + required: false, + name: :room, + type: String, + desc: 'The room name or ID' + }, + { + required: false, + name: :color, + type: String, + desc: 'The room color' + }, + { + required: false, + name: :notify, + type: Boolean, + desc: 'Enable notifications' + }, + { + required: false, + name: :api_version, + type: String, + desc: 'Leave blank for default (v2)' + }, + { + required: false, + name: :server, + type: String, + desc: 'Leave blank for default. https://hipchat.example.com' + } + ], + 'irker' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Recipients/channels separated by whitespaces' + }, + { + required: false, + name: :default_irc_uri, + type: String, + desc: 'Default: irc://irc.network.net:6697' + }, + { + required: false, + name: :server_host, + type: String, + desc: 'Server host. Default localhost' + }, + { + required: false, + name: :server_port, + type: Integer, + desc: 'Server port. Default 6659' + }, + { + required: false, + name: :colorize_messages, + type: Boolean, + desc: 'Colorize messages' + } + ], + 'jira' => [ + { + required: true, + name: :url, + type: String, + desc: 'The URL to the JIRA project which is being linked to this GitLab project, e.g., https://jira.example.com' + }, + { + required: true, + name: :project_key, + type: String, + desc: 'The short identifier for your JIRA project, all uppercase, e.g., PROJ' + }, + { + required: false, + name: :username, + type: String, + desc: 'The username of the user created to be used with GitLab/JIRA' + }, + { + required: false, + name: :password, + type: String, + desc: 'The password of the user created to be used with GitLab/JIRA' + }, + { + required: false, + name: :jira_issue_transition_id, + type: Integer, + desc: '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][trans]). By default, this ID is set to `2`' + } + ], + + 'kubernetes' => [ + { + required: true, + name: :namespace, + type: String, + desc: 'The Kubernetes namespace to use' + }, + { + required: true, + name: :api_url, + type: String, + desc: 'The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com' + }, + { + required: true, + name: :token, + type: String, + desc: 'The service token to authenticate against the Kubernetes cluster with' + }, + { + required: false, + name: :ca_pem, + type: String, + desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)' + }, + ], + + 'mattermost-slash-commands' => [ + { + required: true, + name: :token, + type: String, + desc: 'The Mattermost token' + } + ], + 'pipelines-email' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Comma-separated list of recipient email addresses' + }, + { + required: false, + name: :notify_only_broken_builds, + type: Boolean, + desc: 'Notify only broken builds' + } + ], + 'pivotaltracker' => [ + { + required: true, + name: :token, + type: String, + desc: 'The Pivotaltracker token' + }, + { + required: false, + name: :restrict_to_branch, + type: String, + desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.' + } + ], + 'pushover' => [ + { + required: true, + name: :api_key, + type: String, + desc: 'The application key' + }, + { + required: true, + name: :user_key, + type: String, + desc: 'The user key' + }, + { + required: true, + name: :priority, + type: String, + desc: 'The priority' + }, + { + required: true, + name: :device, + type: String, + desc: 'Leave blank for all active devices' + }, + { + required: true, + name: :sound, + type: String, + desc: 'The sound of the notification' + } + ], + 'redmine' => [ + { + required: true, + name: :new_issue_url, + type: String, + desc: 'The new issue URL' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'The project URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'The issues URL' + }, + { + required: false, + name: :description, + type: String, + desc: 'The description of the tracker' + } + ], + 'slack' => [ + { + required: true, + name: :webhook, + type: String, + desc: 'The Slack webhook. e.g. https://hooks.slack.com/services/...' + }, + { + required: false, + name: :new_issue_url, + type: String, + desc: 'The user name' + }, + { + required: false, + name: :channel, + type: String, + desc: 'The channel name' + } + ], + 'teamcity' => [ + { + required: true, + name: :teamcity_url, + type: String, + desc: 'TeamCity root URL like https://teamcity.example.com' + }, + { + required: true, + name: :build_type, + type: String, + desc: 'Build configuration ID' + }, + { + required: true, + name: :username, + type: String, + desc: 'A user with permissions to trigger a manual build' + }, + { + required: true, + name: :password, + type: String, + desc: 'The password of the user' + } + ] + }.freeze + + trigger_services = { + 'mattermost-slash-commands' => [ + { + name: :token, + type: String, + desc: 'The Mattermost token' + } + ] + }.freeze + resource :projects do before { authenticate! } before { authorize_admin_project } - # Set <service_slug> service for project - # - # Example Request: - # - # PUT /projects/:id/services/gitlab-ci - # - put ':id/services/:service_slug' do - if project_service - validators = project_service.class.validators.select do |s| - s.class == ActiveRecord::Validations::PresenceValidator && - s.attributes != [:project_id] + helpers do + def service_attributes(service) + service.fields.inject([]) do |arr, hash| + arr << hash[:name].to_sym end + end + end - required_attributes! validators.map(&:attributes).flatten.uniq - attrs = attributes_for_keys service_attributes + services.each do |service_slug, settings| + desc "Set #{service_slug} service for project" + params do + settings.each do |setting| + if setting[:required] + requires setting[:name], type: setting[:type], desc: setting[:desc] + else + optional setting[:name], type: setting[:type], desc: setting[:desc] + end + end + end + put ":id/services/#{service_slug}" do + service = user_project.find_or_initialize_service(service_slug.underscore) + service_params = declared_params(include_missing: false).merge(active: true) - if project_service.update_attributes(attrs.merge(active: true)) + if service.update_attributes(service_params) true else - not_found! + render_api_error!('400 Bad Request', 400) end end end - # Delete <service_slug> service for project - # - # Example Request: - # - # DELETE /project/:id/services/gitlab-ci - # - delete ':id/services/:service_slug' do - if project_service - attrs = service_attributes.inject({}) do |hash, key| - hash.merge!(key => nil) - end + desc "Delete a service for project" + params do + requires :service_slug, type: String, values: services.keys, desc: 'The name of the service' + end + delete ":id/services/:service_slug" do + service = user_project.find_or_initialize_service(params[:service_slug].underscore) - if project_service.update_attributes(attrs.merge(active: false)) - true - else - not_found! - end + attrs = service_attributes(service).inject({}) do |hash, key| + hash.merge!(key => nil) + end + + if service.update_attributes(attrs.merge(active: false)) + true + else + render_api_error!('400 Bad Request', 400) end end - # Get <service_slug> service settings for project - # - # Example Request: - # - # GET /project/:id/services/gitlab-ci - # - get ':id/services/:service_slug' do - present project_service, with: Entities::ProjectService, include_passwords: current_user.is_admin? + desc 'Get the service settings for project' do + success Entities::ProjectService + end + params do + requires :service_slug, type: String, values: services.keys, desc: 'The name of the service' + end + get ":id/services/:service_slug" do + service = user_project.find_or_initialize_service(params[:service_slug].underscore) + present service, with: Entities::ProjectService, include_passwords: current_user.is_admin? end end - resource :projects do - desc 'Trigger a slash command' do - detail 'Added in GitLab 8.13' + trigger_services.each do |service_slug, settings| + params do + requires :id, type: String, desc: 'The ID of a project' end - post ':id/services/:service_slug/trigger' do - project = find_project(params[:id]) + resource :projects do + desc "Trigger a slash command for #{service_slug}" do + detail 'Added in GitLab 8.13' + end + params do + settings.each do |setting| + requires setting[:name], type: setting[:type], desc: setting[:desc] + end + end + post ":id/services/#{service_slug.underscore}/trigger" do + project = find_project(params[:id]) - # This is not accurate, but done to prevent leakage of the project names - not_found!('Service') unless project + # This is not accurate, but done to prevent leakage of the project names + not_found!('Service') unless project - service = project_service(project) + service = project.find_or_initialize_service(service_slug.underscore) - result = service.try(:active?) && service.try(:trigger, params) + result = service.try(:active?) && service.try(:trigger, params) - if result - status result[:status] || 200 - present result - else - not_found!('Service') + if result + status result[:status] || 200 + present result + else + not_found!('Service') + end end end end diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb new file mode 100644 index 00000000000..e096e636806 --- /dev/null +++ b/lib/api/snippets.rb @@ -0,0 +1,137 @@ +module API + # Snippets API + class Snippets < Grape::API + include PaginationParams + + before { authenticate! } + + resource :snippets do + helpers do + def snippets_for_current_user + SnippetsFinder.new.execute(current_user, filter: :by_user, user: current_user) + end + + def public_snippets + SnippetsFinder.new.execute(current_user, filter: :public) + end + end + + desc 'Get a snippets list for authenticated user' do + detail 'This feature was introduced in GitLab 8.15.' + success Entities::PersonalSnippet + end + params do + use :pagination + end + get do + present paginate(snippets_for_current_user), with: Entities::PersonalSnippet + end + + desc 'List all public snippets current_user has access to' do + detail 'This feature was introduced in GitLab 8.15.' + success Entities::PersonalSnippet + end + params do + use :pagination + end + get 'public' do + present paginate(public_snippets), with: Entities::PersonalSnippet + end + + desc 'Get a single snippet' do + detail 'This feature was introduced in GitLab 8.15.' + success Entities::PersonalSnippet + end + params do + requires :id, type: Integer, desc: 'The ID of a snippet' + end + get ':id' do + snippet = snippets_for_current_user.find(params[:id]) + present snippet, with: Entities::PersonalSnippet + end + + desc 'Create new snippet' do + detail 'This feature was introduced in GitLab 8.15.' + success Entities::PersonalSnippet + end + params do + requires :title, type: String, desc: 'The title of a snippet' + requires :file_name, type: String, desc: 'The name of a snippet file' + requires :content, type: String, desc: 'The content of a snippet' + optional :visibility_level, type: Integer, + values: Gitlab::VisibilityLevel.values, + default: Gitlab::VisibilityLevel::INTERNAL, + desc: 'The visibility level of the snippet' + end + post do + attrs = declared_params(include_missing: false) + snippet = CreateSnippetService.new(nil, current_user, attrs).execute + + if snippet.persisted? + present snippet, with: Entities::PersonalSnippet + else + render_validation_error!(snippet) + end + end + + desc 'Update an existing snippet' do + detail 'This feature was introduced in GitLab 8.15.' + success Entities::PersonalSnippet + end + params do + requires :id, type: Integer, desc: 'The ID of a snippet' + optional :title, type: String, desc: 'The title of a snippet' + optional :file_name, type: String, desc: 'The name of a snippet file' + optional :content, type: String, desc: 'The content of a snippet' + optional :visibility_level, type: Integer, + values: Gitlab::VisibilityLevel.values, + desc: 'The visibility level of the snippet' + at_least_one_of :title, :file_name, :content, :visibility_level + end + put ':id' do + snippet = snippets_for_current_user.find_by(id: params.delete(:id)) + return not_found!('Snippet') unless snippet + authorize! :update_personal_snippet, snippet + + attrs = declared_params(include_missing: false) + + UpdateSnippetService.new(nil, current_user, snippet, attrs).execute + if snippet.persisted? + present snippet, with: Entities::PersonalSnippet + else + render_validation_error!(snippet) + end + end + + desc 'Remove snippet' do + detail 'This feature was introduced in GitLab 8.15.' + success Entities::PersonalSnippet + end + params do + requires :id, type: Integer, desc: 'The ID of a snippet' + end + delete ':id' do + snippet = snippets_for_current_user.find_by(id: params.delete(:id)) + return not_found!('Snippet') unless snippet + authorize! :destroy_personal_snippet, snippet + snippet.destroy + no_content! + end + + desc 'Get a raw snippet' do + detail 'This feature was introduced in GitLab 8.15.' + end + params do + requires :id, type: Integer, desc: 'The ID of a snippet' + end + get ":id/raw" do + snippet = snippets_for_current_user.find_by(id: params.delete(:id)) + return not_found!('Snippet') unless snippet + + env['api.format'] = :txt + content_type 'text/plain' + present snippet.content + end + end + end +end diff --git a/lib/api/users.rb b/lib/api/users.rb index 1dab799dd61..c7db2d71017 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -353,7 +353,7 @@ module API success Entities::UserPublic end get do - present current_user, with: @impersonator ? Entities::UserWithPrivateToken : Entities::UserPublic + present current_user, with: sudo? ? Entities::UserWithPrivateToken : Entities::UserPublic end desc "Get the currently authenticated user's SSH keys" do diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index d904a8bd4ae..fd74eeaebe7 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -248,7 +248,7 @@ module Banzai end def projects_relation_for_paths(paths) - Project.where_paths_in(paths).includes(:namespace) + Project.where_full_path_in(paths).includes(:namespace) end # Returns projects for the given paths. diff --git a/lib/banzai/filter/math_filter.rb b/lib/banzai/filter/math_filter.rb new file mode 100644 index 00000000000..cb037f89337 --- /dev/null +++ b/lib/banzai/filter/math_filter.rb @@ -0,0 +1,51 @@ +require 'uri' + +module Banzai + module Filter + # HTML filter that adds class="code math" and removes the dollar sign in $`2+2`$. + # + class MathFilter < HTML::Pipeline::Filter + # This picks out <code>...</code>. + INLINE_MATH = 'descendant-or-self::code'.freeze + + # Pick out a code block which is declared math + DISPLAY_MATH = "descendant-or-self::pre[contains(@class, 'math') and contains(@class, 'code')]".freeze + + # Attribute indicating inline or display math. + STYLE_ATTRIBUTE = 'data-math-style'.freeze + + # Class used for tagging elements that should be rendered + TAG_CLASS = 'js-render-math'.freeze + + INLINE_CLASSES = "code math #{TAG_CLASS}".freeze + + DOLLAR_SIGN = '$'.freeze + + def call + doc.xpath(INLINE_MATH).each do |code| + closing = code.next + opening = code.previous + + # We need a sibling before and after. + # They should end and start with $ respectively. + if closing && opening && + closing.content.first == DOLLAR_SIGN && + opening.content.last == DOLLAR_SIGN + + code[:class] = INLINE_CLASSES + code[STYLE_ATTRIBUTE] = 'inline' + closing.content = closing.content[1..-1] + opening.content = opening.content[0..-2] + end + end + + doc.xpath(DISPLAY_MATH).each do |el| + el[STYLE_ATTRIBUTE] = 'display' + el[:class] += " #{TAG_CLASS}" + end + + doc + end + end + end +end diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb index f09d78be0ce..9e23c8f8c55 100644 --- a/lib/banzai/filter/relative_link_filter.rb +++ b/lib/banzai/filter/relative_link_filter.rb @@ -46,7 +46,7 @@ module Banzai end def rebuild_relative_uri(uri) - file_path = relative_file_path(uri.path) + file_path = relative_file_path(uri) uri.path = [ relative_url_root, @@ -59,8 +59,10 @@ module Banzai uri end - def relative_file_path(path) - nested_path = build_relative_path(path, context[:requested_path]) + def relative_file_path(uri) + path = Addressable::URI.unescape(uri.path) + request_path = Addressable::URI.unescape(context[:requested_path]) + nested_path = build_relative_path(path, request_path) file_exists?(nested_path) ? nested_path : path end @@ -108,11 +110,7 @@ module Banzai end def uri_type(path) - @uri_types[path] ||= begin - unescaped_path = Addressable::URI.unescape(path) - - current_commit.uri_type(unescaped_path) - end + @uri_types[path] ||= current_commit.uri_type(path) end def current_commit diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 5da2d0b008c..5a1f873496c 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -6,6 +6,7 @@ module Banzai Filter::SyntaxHighlightFilter, Filter::SanitizationFilter, + Filter::MathFilter, Filter::UploadLinkFilter, Filter::VideoLinkFilter, Filter::ImageLinkFilter, diff --git a/lib/gitlab/allowable.rb b/lib/gitlab/allowable.rb new file mode 100644 index 00000000000..f48abcc86d5 --- /dev/null +++ b/lib/gitlab/allowable.rb @@ -0,0 +1,7 @@ +module Gitlab + module Allowable + def can?(user, action, subject) + Ability.allowed?(user, action, subject) + end + end +end diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb index 9667df4ffb8..fa234284361 100644 --- a/lib/gitlab/asciidoc.rb +++ b/lib/gitlab/asciidoc.rb @@ -1,4 +1,5 @@ require 'asciidoctor' +require 'asciidoctor/converter/html5' module Gitlab # Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters @@ -23,7 +24,7 @@ module Gitlab def self.render(input, context, asciidoc_opts = {}) asciidoc_opts.reverse_merge!( safe: :secure, - backend: :html5, + backend: :gitlab_html5, attributes: [] ) asciidoc_opts[:attributes].unshift(*DEFAULT_ADOC_ATTRS) @@ -34,5 +35,29 @@ module Gitlab html.html_safe end + + class Html5Converter < Asciidoctor::Converter::Html5Converter + extend Asciidoctor::Converter::Config + + register_for 'gitlab_html5' + + def stem(node) + return super unless node.style.to_sym == :latexmath + + %(<pre#{id_attribute(node)} class="code math js-render-math #{node.role}" data-math-style="display"><code>#{node.content}</code></pre>) + end + + def inline_quoted(node) + return super unless node.type.to_sym == :latexmath + + %(<code#{id_attribute(node)} class="code math js-render-math #{node.role}" data-math-style="inline">#{node.text}</code>) + end + + private + + def id_attribute(node) + node.id ? %( id="#{node.id}") : nil + end + end end end diff --git a/lib/gitlab/badge/build/status.rb b/lib/gitlab/badge/build/status.rb index 50aa45e5406..b762d85b6e5 100644 --- a/lib/gitlab/badge/build/status.rb +++ b/lib/gitlab/badge/build/status.rb @@ -20,8 +20,8 @@ module Gitlab def status @project.pipelines - .where(sha: @sha, ref: @ref) - .status || 'unknown' + .where(sha: @sha) + .latest_status(@ref) || 'unknown' end def metadata diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb index 1dba85c1b51..cefb6775db8 100644 --- a/lib/gitlab/chat_commands/issue_create.rb +++ b/lib/gitlab/chat_commands/issue_create.rb @@ -8,7 +8,7 @@ module Gitlab end def self.help_message - 'issue new <title>\n<description>' + 'issue new <title> *`⇧ Shift`*+*`↵ Enter`* <description>' end def self.allowed?(project, user) diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb new file mode 100644 index 00000000000..a979fe7d573 --- /dev/null +++ b/lib/gitlab/ci/status/build/cancelable.rb @@ -0,0 +1,37 @@ +module Gitlab + module Ci + module Status + module Build + class Cancelable < SimpleDelegator + include Status::Extended + + def has_action? + can?(user, :update_build, subject) + end + + def action_icon + 'ban' + end + + def action_path + cancel_namespace_project_build_path(subject.project.namespace, + subject.project, + subject) + end + + def action_method + :post + end + + def action_title + 'Cancel' + end + + def self.matches?(build, user) + build.cancelable? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb new file mode 100644 index 00000000000..3fec2c5d4db --- /dev/null +++ b/lib/gitlab/ci/status/build/common.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + module Build + module Common + def has_details? + can?(user, :read_build, subject) + end + + def details_path + namespace_project_build_path(subject.project.namespace, + subject.project, + subject) + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb new file mode 100644 index 00000000000..eee9a64120b --- /dev/null +++ b/lib/gitlab/ci/status/build/factory.rb @@ -0,0 +1,18 @@ +module Gitlab + module Ci + module Status + module Build + class Factory < Status::Factory + def self.extended_statuses + [Status::Build::Stop, Status::Build::Play, + Status::Build::Cancelable, Status::Build::Retryable] + end + + def self.common_helpers + Status::Build::Common + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb new file mode 100644 index 00000000000..5c506e6d59f --- /dev/null +++ b/lib/gitlab/ci/status/build/play.rb @@ -0,0 +1,53 @@ +module Gitlab + module Ci + module Status + module Build + class Play < SimpleDelegator + include Status::Extended + + def text + 'manual' + end + + def label + 'manual play action' + end + + def icon + 'icon_status_manual' + end + + def has_action? + can?(user, :update_build, subject) + end + + def action_icon + 'play' + end + + def action_title + 'Play' + end + + def action_class + 'ci-play-icon' + end + + def action_path + play_namespace_project_build_path(subject.project.namespace, + subject.project, + subject) + end + + def action_method + :post + end + + def self.matches?(build, user) + build.playable? && !build.stops_environment? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb new file mode 100644 index 00000000000..8e38d6a8523 --- /dev/null +++ b/lib/gitlab/ci/status/build/retryable.rb @@ -0,0 +1,37 @@ +module Gitlab + module Ci + module Status + module Build + class Retryable < SimpleDelegator + include Status::Extended + + def has_action? + can?(user, :update_build, subject) + end + + def action_icon + 'refresh' + end + + def action_title + 'Retry' + end + + def action_path + retry_namespace_project_build_path(subject.project.namespace, + subject.project, + subject) + end + + def action_method + :post + end + + def self.matches?(build, user) + build.retryable? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb new file mode 100644 index 00000000000..f8ffa95cde4 --- /dev/null +++ b/lib/gitlab/ci/status/build/stop.rb @@ -0,0 +1,49 @@ +module Gitlab + module Ci + module Status + module Build + class Stop < SimpleDelegator + include Status::Extended + + def text + 'manual' + end + + def label + 'manual stop action' + end + + def icon + 'icon_status_manual' + end + + def has_action? + can?(user, :update_build, subject) + end + + def action_icon + 'stop' + end + + def action_title + 'Stop' + end + + def action_path + play_namespace_project_build_path(subject.project.namespace, + subject.project, + subject) + end + + def action_method + :post + end + + def self.matches?(build, user) + build.playable? && build.stops_environment? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb index ce4108fdcf2..46fef8262c1 100644 --- a/lib/gitlab/ci/status/core.rb +++ b/lib/gitlab/ci/status/core.rb @@ -4,10 +4,14 @@ module Gitlab # Base abstract class fore core status # class Core - include Gitlab::Routing.url_helpers + include Gitlab::Routing + include Gitlab::Allowable - def initialize(subject) + attr_reader :subject, :user + + def initialize(subject, user) @subject = subject + @user = user end def icon @@ -18,10 +22,6 @@ module Gitlab raise NotImplementedError end - def title - "#{@subject.class.name.demodulize}: #{label}" - end - # Deprecation warning: this method is here because we need to maintain # backwards compatibility with legacy statuses. We often do something # like "ci-status ci-status-#{status}" to set CSS class. @@ -34,7 +34,7 @@ module Gitlab end def has_details? - raise NotImplementedError + false end def details_path @@ -42,16 +42,27 @@ module Gitlab end def has_action? - raise NotImplementedError + false end def action_icon raise NotImplementedError end + def action_class + end + def action_path raise NotImplementedError end + + def action_method + raise NotImplementedError + end + + def action_title + raise NotImplementedError + end end end end diff --git a/lib/gitlab/ci/status/extended.rb b/lib/gitlab/ci/status/extended.rb index 6bfb5d38c1f..d367c9bda69 100644 --- a/lib/gitlab/ci/status/extended.rb +++ b/lib/gitlab/ci/status/extended.rb @@ -2,8 +2,12 @@ module Gitlab module Ci module Status module Extended - def matches?(_subject) - raise NotImplementedError + extend ActiveSupport::Concern + + class_methods do + def matches?(_subject, _user) + raise NotImplementedError + end end end end diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb index b2f896f2211..ae9ef895df4 100644 --- a/lib/gitlab/ci/status/factory.rb +++ b/lib/gitlab/ci/status/factory.rb @@ -2,10 +2,9 @@ module Gitlab module Ci module Status class Factory - attr_reader :subject - - def initialize(subject) + def initialize(subject, user) @subject = subject + @user = user end def fabricate! @@ -16,27 +15,32 @@ module Gitlab end end + def self.extended_statuses + [] + end + + def self.common_helpers + Module.new + end + private - def subject_status - @subject_status ||= subject.status + def simple_status + @simple_status ||= @subject.status || :created end def core_status Gitlab::Ci::Status - .const_get(subject_status.capitalize) - .new(subject) + .const_get(simple_status.capitalize) + .new(@subject, @user) + .extend(self.class.common_helpers) end def extended_status - @extended ||= extended_statuses.find do |status| - status.matches?(subject) + @extended ||= self.class.extended_statuses.find do |status| + status.matches?(@subject, @user) end end - - def extended_statuses - [] - end end end end diff --git a/lib/gitlab/ci/status/pipeline/common.rb b/lib/gitlab/ci/status/pipeline/common.rb index 25e52bec3da..76bfd18bf40 100644 --- a/lib/gitlab/ci/status/pipeline/common.rb +++ b/lib/gitlab/ci/status/pipeline/common.rb @@ -4,13 +4,13 @@ module Gitlab module Pipeline module Common def has_details? - true + can?(user, :read_pipeline, subject) end def details_path - namespace_project_pipeline_path(@subject.project.namespace, - @subject.project, - @subject) + namespace_project_pipeline_path(subject.project.namespace, + subject.project, + subject) end def has_action? diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb index 4ac4ec671d0..16dcb326be9 100644 --- a/lib/gitlab/ci/status/pipeline/factory.rb +++ b/lib/gitlab/ci/status/pipeline/factory.rb @@ -3,14 +3,12 @@ module Gitlab module Status module Pipeline class Factory < Status::Factory - private - - def extended_statuses + def self.extended_statuses [Pipeline::SuccessWithWarnings] end - def core_status - super.extend(Status::Pipeline::Common) + def self.common_helpers + Status::Pipeline::Common end end end diff --git a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb index 4b040d60df8..a7c98f9e909 100644 --- a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb +++ b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb @@ -3,7 +3,7 @@ module Gitlab module Status module Pipeline class SuccessWithWarnings < SimpleDelegator - extend Status::Extended + include Status::Extended def text 'passed' @@ -21,7 +21,7 @@ module Gitlab 'success_with_warnings' end - def self.matches?(pipeline) + def self.matches?(pipeline, user) pipeline.success? && pipeline.has_warnings? end end diff --git a/lib/gitlab/ci/status/stage/common.rb b/lib/gitlab/ci/status/stage/common.rb index 14c437d2b98..7852f492e1d 100644 --- a/lib/gitlab/ci/status/stage/common.rb +++ b/lib/gitlab/ci/status/stage/common.rb @@ -4,14 +4,14 @@ module Gitlab module Stage module Common def has_details? - true + can?(user, :read_pipeline, subject.pipeline) end def details_path - namespace_project_pipeline_path(@subject.project.namespace, - @subject.project, - @subject.pipeline, - anchor: @subject.name) + namespace_project_pipeline_path(subject.project.namespace, + subject.project, + subject.pipeline, + anchor: subject.name) end def has_action? diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb index c6522d5ada1..689a5dd45bc 100644 --- a/lib/gitlab/ci/status/stage/factory.rb +++ b/lib/gitlab/ci/status/stage/factory.rb @@ -3,10 +3,8 @@ module Gitlab module Status module Stage class Factory < Status::Factory - private - - def core_status - super.extend(Status::Stage::Common) + def self.common_helpers + Status::Stage::Common end end end diff --git a/lib/gitlab/gfm/uploads_rewriter.rb b/lib/gitlab/gfm/uploads_rewriter.rb index abc8c8c55e6..8fab5489616 100644 --- a/lib/gitlab/gfm/uploads_rewriter.rb +++ b/lib/gitlab/gfm/uploads_rewriter.rb @@ -1,3 +1,5 @@ +require 'fileutils' + module Gitlab module Gfm ## @@ -22,7 +24,9 @@ module Gitlab return markdown unless file.try(:exists?) new_uploader = FileUploader.new(target_project) - new_uploader.store!(file) + with_link_in_tmp_dir(file.file) do |open_tmp_file| + new_uploader.store!(open_tmp_file) + end new_uploader.to_markdown end end @@ -46,6 +50,19 @@ module Gitlab uploader.retrieve_from_store!(file) uploader.file end + + # Because the uploaders use 'move_to_store' we must have a temporary + # file that is allowed to be (re)moved. + def with_link_in_tmp_dir(file) + dir = Dir.mktmpdir('UploadsRewriter', File.dirname(file)) + # The filename matters to Carrierwave so we make sure to preserve it + tmp_file = File.join(dir, File.basename(file)) + File.link(file, tmp_file) + # Open the file to placate Carrierwave + File.open(tmp_file) { |open_file| yield open_file } + ensure + FileUtils.rm_rf(dir) + end end end end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 2c21804fe7a..4d4e04e9e35 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -8,6 +8,8 @@ module Gitlab gon.shortcuts_path = help_page_path('shortcuts') gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class gon.award_menu_url = emojis_path + gon.katex_css_url = ActionController::Base.helpers.asset_path('katex.css') + gon.katex_js_url = ActionController::Base.helpers.asset_path('katex.js') if current_user gon.current_user_id = current_user.id diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb new file mode 100644 index 00000000000..65713e73a59 --- /dev/null +++ b/lib/gitlab/middleware/multipart.rb @@ -0,0 +1,99 @@ +# Gitlab::Middleware::Multipart - a Rack::Multipart replacement +# +# Rack::Multipart leaves behind tempfiles in /tmp and uses valuable Ruby +# process time to copy files around. This alternative solution uses +# gitlab-workhorse to clean up the tempfiles and puts the tempfiles in a +# location where copying should not be needed. +# +# When gitlab-workhorse finds files in a multipart MIME body it sends +# a signed message via a request header. This message lists the names of +# the multipart entries that gitlab-workhorse filtered out of the +# multipart structure and saved to tempfiles. Workhorse adds new entries +# in the multipart structure with paths to the tempfiles. +# +# The job of this Rack middleware is to detect and decode the message +# from workhorse. If present, it walks the Rack 'params' hash for the +# current request, opens the respective tempfiles, and inserts the open +# Ruby File objects in the params hash where Rack::Multipart would have +# put them. The goal is that application code deeper down can keep +# working the way it did with Rack::Multipart without changes. +# +# CAVEAT: the code that modifies the params hash is a bit complex. It is +# conceivable that certain Rack params structures will not be modified +# correctly. We are not aware of such bugs at this time though. +# + +module Gitlab + module Middleware + class Multipart + RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS' + + class Handler + def initialize(env, message) + @request = Rack::Request.new(env) + @rewritten_fields = message['rewritten_fields'] + @open_files = [] + end + + def with_open_files + @rewritten_fields.each do |field, tmp_path| + parsed_field = Rack::Utils.parse_nested_query(field) + raise "unexpected field: #{field.inspect}" unless parsed_field.count == 1 + + key, value = parsed_field.first + if value.nil? + value = File.open(tmp_path) + @open_files << value + else + value = decorate_params_value(value, @request.params[key], tmp_path) + end + @request.update_param(key, value) + end + + yield + ensure + @open_files.each(&:close) + end + + # This function calls itself recursively + def decorate_params_value(path_hash, value_hash, tmp_path) + unless path_hash.is_a?(Hash) && path_hash.count == 1 + raise "invalid path: #{path_hash.inspect}" + end + path_key, path_value = path_hash.first + + unless value_hash.is_a?(Hash) && value_hash[path_key] + raise "invalid value hash: #{value_hash.inspect}" + end + + case path_value + when nil + value_hash[path_key] = File.open(tmp_path) + @open_files << value_hash[path_key] + value_hash + when Hash + decorate_params_value(path_value, value_hash[path_key], tmp_path) + value_hash + else + raise "unexpected path value: #{path_value.inspect}" + end + end + end + + def initialize(app) + @app = app + end + + def call(env) + encoded_message = env.delete(RACK_ENV_KEY) + return @app.call(env) if encoded_message.blank? + + message = Gitlab::Workhorse.decode_jwt(encoded_message)[0] + + Handler.new(env, message).with_open_files do + @app.call(env) + end + end + end + end +end diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 66e6b29e798..6bdf3db9cb8 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -110,7 +110,7 @@ module Gitlab end def notes - @notes ||= project.notes.user.search(query, as_user: @current_user).order('updated_at DESC') + @notes ||= NotesFinder.new(project, @current_user, search: query).execute.user.order('updated_at DESC') end def commits diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index a06cf6a989c..9e0b0e5ea98 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -61,7 +61,7 @@ module Gitlab end def file_name_regex - @file_name_regex ||= /\A[a-zA-Z0-9_\-\.\@]*\z/.freeze + @file_name_regex ||= /\A[[[:alnum:]]_\-\.\@]*\z/.freeze end def file_name_regex_message @@ -69,7 +69,7 @@ module Gitlab end def file_path_regex - @file_path_regex ||= /\A[a-zA-Z0-9_\-\.\/\@]*\z/.freeze + @file_path_regex ||= /\A[[[:alnum:]]_\-\.\/\@]*\z/.freeze end def file_path_regex_message @@ -123,5 +123,22 @@ module Gitlab def environment_name_regex_message "can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.' and spaces" end + + def kubernetes_namespace_regex + /\A[a-z0-9]([-a-z0-9]*[a-z0-9])?\z/ + end + + def kubernetes_namespace_regex_message + "can contain only letters, digits or '-', and cannot start or end with '-'" + end + + def environment_slug_regex + @environment_slug_regex ||= /\A[a-z]([a-z0-9-]*[a-z0-9])?\z/.freeze + end + + def environment_slug_regex_message + "can contain only lowercase letters, digits, and '-'. " \ + "Must start with a letter, and cannot end with '-'" + end end end diff --git a/lib/gitlab/routing.rb b/lib/gitlab/routing.rb index 5132177de51..632e2d87500 100644 --- a/lib/gitlab/routing.rb +++ b/lib/gitlab/routing.rb @@ -1,5 +1,11 @@ module Gitlab module Routing + extend ActiveSupport::Concern + + included do + include Gitlab::Routing.url_helpers + end + # Returns the URL helpers Module. # # This method caches the output as Rails' "url_helpers" method creates an diff --git a/lib/gitlab/sql/union.rb b/lib/gitlab/sql/union.rb index 1cd89b3a9c4..222021e8802 100644 --- a/lib/gitlab/sql/union.rb +++ b/lib/gitlab/sql/union.rb @@ -22,9 +22,7 @@ module Gitlab # By using "unprepared_statements" we remove the usage of placeholders # (thus fixing this problem), at a slight performance cost. fragments = ActiveRecord::Base.connection.unprepared_statement do - @relations.map do |rel| - rel.reorder(nil).to_sql - end + @relations.map { |rel| rel.reorder(nil).to_sql }.reject(&:blank?) end fragments.join("\nUNION\n") diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index 99d0c28e749..ccb456bcc94 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -24,6 +24,8 @@ module Gitlab wiki_page_url when ProjectSnippet project_snippet_url(object) + when Snippet + personal_snippet_url(object) else raise NotImplementedError.new("No URL builder defined for #{object.class}") end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 594439a5d4b..aeb1a26e1ba 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -117,8 +117,12 @@ module Gitlab end def verify_api_request!(request_headers) + decode_jwt(request_headers[INTERNAL_API_REQUEST_HEADER]) + end + + def decode_jwt(encoded_message) JWT.decode( - request_headers[INTERNAL_API_REQUEST_HEADER], + encoded_message, secret, true, { iss: 'gitlab-workhorse', verify_iss: true, algorithm: 'HS256' }, diff --git a/lib/rouge/lexers/math.rb b/lib/rouge/lexers/math.rb new file mode 100644 index 00000000000..80784adfd76 --- /dev/null +++ b/lib/rouge/lexers/math.rb @@ -0,0 +1,21 @@ +module Rouge + module Lexers + class Math < Lexer + title "A passthrough lexer used for LaTeX input" + desc "A boring lexer that doesn't highlight anything" + + tag 'math' + mimetypes 'text/plain' + + default_options token: 'Text' + + def token + @token ||= Token[option :token] + end + + def stream_tokens(string, &b) + yield self.token, string + end + end + end +end diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake index 141a0b74ec0..f5caca3ddbf 100644 --- a/lib/tasks/migrate/setup_postgresql.rake +++ b/lib/tasks/migrate/setup_postgresql.rake @@ -1,8 +1,12 @@ +require Rails.root.join('lib/gitlab/database') +require Rails.root.join('lib/gitlab/database/migration_helpers') require Rails.root.join('db/migrate/20151007120511_namespaces_projects_path_lower_indexes') require Rails.root.join('db/migrate/20151008110232_add_users_lower_username_email_indexes') +require Rails.root.join('db/migrate/20161212142807_add_lower_path_index_to_routes') desc 'GitLab | Sets up PostgreSQL' task setup_postgresql: :environment do NamespacesProjectsPathLowerIndexes.new.up AddUsersLowerUsernameEmailIndexes.new.up + AddLowerPathIndexToRoutes.new.up end diff --git a/package.json b/package.json index 961989f8012..49b8210e427 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "scripts": { - "eslint": "eslint --ext .js,.js.es6 .", + "eslint": "eslint --max-warnings 0 --ext .js,.js.es6 .", "eslint-fix": "npm run eslint -- --fix", "eslint-report": "npm run eslint -- --format html --output-file ./eslint-report.html" }, diff --git a/public/404.html b/public/404.html index 11b29d09a82..b3b3a0fa3f3 100644 --- a/public/404.html +++ b/public/404.html @@ -46,6 +46,14 @@ margin: 40px auto; } + a { + line-height: 100px; + font-weight: normal; + color: #4A8BEE; + font-size: 18px; + text-decoration: none; + } + .container { margin: auto 20px; } @@ -63,6 +71,7 @@ <hr /> <p>Make sure the address is correct and that the page hasn't moved.</p> <p>Please contact your GitLab administrator if you think this is a mistake.</p> + <a href="javascript:history.back()">Go back</a> </div> </body> </html> diff --git a/public/422.html b/public/422.html index 9bd7cb4b7c8..119e54ad8bd 100644 --- a/public/422.html +++ b/public/422.html @@ -46,6 +46,14 @@ margin: 40px auto; } + a { + line-height: 100px; + font-weight: normal; + color: #4A8BEE; + font-size: 18px; + text-decoration: none; + } + .container { margin: auto 20px; } @@ -63,6 +71,7 @@ <hr /> <p>Make sure you have access to the thing you tried to change.</p> <p>Please contact your GitLab administrator if you think this is a mistake.</p> + <a href="javascript:history.back()">Go back</a> </div> </body> </html> diff --git a/public/500.html b/public/500.html index f92e8839f8d..226ef3c40ea 100644 --- a/public/500.html +++ b/public/500.html @@ -46,6 +46,14 @@ margin: 40px auto; } + a { + line-height: 100px; + font-weight: normal; + color: #4A8BEE; + font-size: 18px; + text-decoration: none; + } + .container { margin: auto 20px; } @@ -63,6 +71,7 @@ <hr /> <p>Try refreshing the page, or going back and attempting the action again.</p> <p>Please contact your GitLab administrator if this problem persists.</p> + <a href="javascript:history.back()">Go back</a> </div> </body> </html> diff --git a/public/502.html b/public/502.html index c2be4f130a9..f037b81bace 100644 --- a/public/502.html +++ b/public/502.html @@ -46,6 +46,14 @@ margin: 40px auto; } + a { + line-height: 100px; + font-weight: normal; + color: #4A8BEE; + font-size: 18px; + text-decoration: none; + } + .container { margin: auto 20px; } @@ -63,6 +71,7 @@ <hr /> <p>Try refreshing the page, or going back and attempting the action again.</p> <p>Please contact your GitLab administrator if this problem persists.</p> + <a href="javascript:history.back()">Go back</a> </div> </body> </html> diff --git a/public/503.html b/public/503.html index 8850ffce362..f946a087871 100644 --- a/public/503.html +++ b/public/503.html @@ -46,6 +46,14 @@ margin: 40px auto; } + a { + line-height: 100px; + font-weight: normal; + color: #4A8BEE; + font-size: 18px; + text-decoration: none; + } + .container { margin: auto 20px; } @@ -63,6 +71,7 @@ <hr /> <p>Try refreshing the page, or going back and attempting the action again.</p> <p>Please contact your GitLab administrator if this problem persists.</p> + <a href="javascript:history.back()">Go back</a> </div> </body> </html> diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb new file mode 100644 index 00000000000..9fd5c3b85f6 --- /dev/null +++ b/spec/controllers/projects/releases_controller_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe Projects::ReleasesController do + let!(:project) { create(:project) } + let!(:user) { create(:user) } + let!(:release) { create(:release, project: project) } + let!(:tag) { release.tag } + + before do + project.team << [user, :developer] + sign_in(user) + end + + describe 'GET #edit' do + it 'initializes a new release' do + tag_id = release.tag + project.releases.destroy_all + + get :edit, namespace_id: project.namespace.path, project_id: project.path, tag_id: tag_id + + release = assigns(:release) + expect(release).not_to be_nil + expect(release).not_to be_persisted + end + + it 'retrieves an existing release' do + get :edit, namespace_id: project.namespace.path, project_id: project.path, tag_id: release.tag + + release = assigns(:release) + expect(release).not_to be_nil + expect(release).to be_persisted + end + end + + describe 'PUT #update' do + it 'updates release note description' do + update_release('description updated') + + release = project.releases.find_by_tag(tag) + expect(release.description).to eq("description updated") + end + + it 'deletes release note when description is null' do + expect { update_release('') }.to change(project.releases, :count).by(-1) + end + end + + def update_release(description) + put :update, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + tag_id: release.tag, + release: { description: description } + end +end diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb new file mode 100644 index 00000000000..b7bb9290712 --- /dev/null +++ b/spec/controllers/search_controller_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe SearchController do + let(:user) { create(:user) } + let(:project) { create(:empty_project, :public) } + + before do + sign_in(user) + end + + it 'finds issue comments' do + project = create(:empty_project, :public) + note = create(:note_on_issue, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].first).to eq note + end + + context 'on restricted projects' do + context 'when signed out' do + before { sign_out(user) } + + it "doesn't expose comments on issues" do + project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_issue, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].count).to eq(0) + end + end + + it "doesn't expose comments on issues" do + project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_issue, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].count).to eq(0) + end + + it "doesn't expose comments on merge_requests" do + project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_merge_request, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].count).to eq(0) + end + + it "doesn't expose comments on snippets" do + project = create(:empty_project, :public, snippets_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_project_snippet, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].count).to eq(0) + end + end +end diff --git a/spec/db/production/settings.rb b/spec/db/production/settings.rb new file mode 100644 index 00000000000..a7c5283df94 --- /dev/null +++ b/spec/db/production/settings.rb @@ -0,0 +1,16 @@ +require 'spec_helper' +require 'rainbow/ext/string' + +describe 'seed production settings', lib: true do + context 'GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN is set in the environment' do + before do + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:[]).with('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN').and_return('013456789') + end + + it 'writes the token to the database' do + load(File.join(__dir__, '../../../db/fixtures/production/010_settings.rb')) + expect(ApplicationSetting.current.runners_registration_token).to eq('013456789') + end + end +end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index c443af09075..62466c06194 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -12,12 +12,14 @@ FactoryGirl.define do started_at 'Di 29. Okt 09:51:28 CET 2013' finished_at 'Di 29. Okt 09:53:28 CET 2013' commands 'ls -a' + options do { image: "ruby:2.1", services: ["postgres"] } end + yaml_variables do [ { key: :DB_NAME, value: 'postgres', public: true } @@ -60,15 +62,20 @@ FactoryGirl.define do end trait :teardown_environment do - options do - { environment: { action: 'stop' } } - end + environment 'staging' + options environment: { name: 'staging', + action: 'stop' } end trait :allowed_to_fail do allow_failure true end + trait :playable do + skipped + manual + end + after(:build) do |build, evaluator| build.project = build.pipeline.project end diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb index ebd3595ea64..ece6beb9fa9 100644 --- a/spec/factories/groups.rb +++ b/spec/factories/groups.rb @@ -19,5 +19,9 @@ FactoryGirl.define do trait :access_requestable do request_access_enabled true end + + trait :nested do + parent factory: :group + end end end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 6919002dedc..a10ba629760 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -67,7 +67,7 @@ FactoryGirl.define do end trait :on_project_snippet do - noteable { create(:snippet, project: project) } + noteable { create(:project_snippet, project: project) } end trait :system do diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 1166498ddff..0d072d6a690 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -133,4 +133,17 @@ FactoryGirl.define do ) end end + + factory :kubernetes_project, parent: :empty_project do + after :create do |project| + project.create_kubernetes_service( + active: true, + properties: { + namespace: project.path, + api_url: 'https://kubernetes.example.com/api', + token: 'a' * 40, + } + ) + end + end end diff --git a/spec/features/admin/admin_active_tab_spec.rb b/spec/features/admin/admin_active_tab_spec.rb new file mode 100644 index 00000000000..16064d60ce2 --- /dev/null +++ b/spec/features/admin/admin_active_tab_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +RSpec.describe 'admin active tab' do + before do + login_as :admin + end + + shared_examples 'page has active tab' do |title| + it "activates #{title} tab" do + expect(page).to have_selector('.layout-nav .nav-links > li.active', count: 1) + expect(page.find('.layout-nav li.active')).to have_content(title) + end + end + + shared_examples 'page has active sub tab' do |title| + it "activates #{title} sub tab" do + expect(page).to have_selector('.sub-nav li.active', count: 1) + expect(page.find('.sub-nav li.active')).to have_content(title) + end + end + + context 'on home page' do + before do + visit admin_root_path + end + + it_behaves_like 'page has active tab', 'Overview' + end + + context 'on projects' do + before do + visit admin_projects_path + end + + it_behaves_like 'page has active tab', 'Overview' + it_behaves_like 'page has active sub tab', 'Projects' + end + + context 'on groups' do + before do + visit admin_groups_path + end + + it_behaves_like 'page has active tab', 'Overview' + it_behaves_like 'page has active sub tab', 'Groups' + end + + context 'on users' do + before do + visit admin_users_path + end + + it_behaves_like 'page has active tab', 'Overview' + it_behaves_like 'page has active sub tab', 'Users' + end + + context 'on logs' do + before do + visit admin_logs_path + end + + it_behaves_like 'page has active tab', 'Monitoring' + it_behaves_like 'page has active sub tab', 'Logs' + end + + context 'on messages' do + before do + visit admin_broadcast_messages_path + end + + it_behaves_like 'page has active tab', 'Messages' + end + + context 'on hooks' do + before do + visit admin_hooks_path + end + + it_behaves_like 'page has active tab', 'Hooks' + end + + context 'on background jobs' do + before do + visit admin_background_jobs_path + end + + it_behaves_like 'page has active tab', 'Monitoring' + it_behaves_like 'page has active sub tab', 'Background Jobs' + end +end diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb index f6d625fa7f6..0aa01fc499a 100644 --- a/spec/features/admin/admin_groups_spec.rb +++ b/spec/features/admin/admin_groups_spec.rb @@ -21,7 +21,7 @@ feature 'Admin Groups', feature: true do scenario 'shows the visibility level radio populated with the group visibility_level value' do group = create(:group, :private) - visit edit_admin_group_path(group) + visit admin_group_edit_path(group) expect_selected_visibility(group.visibility_level) end diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index 30ded9202a4..a36bfd574cb 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -8,11 +8,11 @@ describe "Admin::Projects", feature: true do describe "GET /admin/projects" do before do - visit admin_namespaces_projects_path + visit admin_projects_path end it "is ok" do - expect(current_path).to eq(admin_namespaces_projects_path) + expect(current_path).to eq(admin_projects_path) end it "has projects list" do @@ -22,7 +22,7 @@ describe "Admin::Projects", feature: true do describe "GET /admin/projects/:id" do before do - visit admin_namespaces_projects_path + visit admin_projects_path click_link "#{@project.name}" end diff --git a/features/steps/admin/settings.rb b/spec/features/admin/admin_settings_spec.rb index 11dc7f580f0..8cd66f189be 100644 --- a/features/steps/admin/settings.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -1,62 +1,53 @@ -class Spinach::Features::AdminSettings < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedAdmin - include Gitlab::CurrentSettings +require 'spec_helper' - step 'I modify settings and save form' do +feature 'Admin updates settings', feature: true do + before(:each) do + login_as :admin + visit admin_application_settings_path + end + + scenario 'Change application settings' do uncheck 'Gravatar enabled' fill_in 'Home page URL', with: 'https://about.gitlab.com/' fill_in 'Help page text', with: 'Example text' click_button 'Save' - end - step 'I should see application settings saved' do expect(current_application_settings.gravatar_enabled).to be_falsey expect(current_application_settings.home_page_url).to eq "https://about.gitlab.com/" expect(page).to have_content "Application settings saved successfully" end - step 'I click on "Service Templates"' do + scenario 'Change Slack Service template settings' do click_link 'Service Templates' - end - - step 'I click on "Slack" service' do click_link 'Slack' - end - - step 'I check all events and submit form' do - page.check('Active') - page.check('Push') - page.check('Tag push') - page.check('Note') - page.check('Issue') - page.check('Merge request') - page.check('Build') - page.check('Pipeline') - click_on 'Save' - end - - step 'I fill out Slack settings' do fill_in 'Webhook', with: 'http://localhost' fill_in 'Username', with: 'test_user' fill_in 'service_push_channel', with: '#test_channel' page.check('Notify only broken builds') - end - step 'I should see service template settings saved' do + check_all_events + click_on 'Save' + expect(page).to have_content 'Application settings saved successfully' - end - step 'I should see all checkboxes checked' do + click_link 'Slack' + page.all('input[type=checkbox]').each do |checkbox| expect(checkbox).to be_checked end - end - - step 'I should see Slack settings saved' do expect(find_field('Webhook').value).to eq 'http://localhost' expect(find_field('Username').value).to eq 'test_user' expect(find('#service_push_channel').value).to eq '#test_channel' end + + def check_all_events + page.check('Active') + page.check('Push') + page.check('Tag push') + page.check('Note') + page.check('Issue') + page.check('Merge request') + page.check('Build') + page.check('Pipeline') + end end diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb index 365cb445df1..44dfc2dff45 100644 --- a/spec/features/dashboard/datetime_on_tooltips_spec.rb +++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb @@ -36,7 +36,7 @@ feature 'Tooltips on .timeago dates', feature: true, js: true do visit user_snippets_path(user) wait_for_ajax() - page.find('.js-timeago').hover + page.find('.js-timeago.snippet-created-ago').hover end it 'has the datetime formated correctly' do diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb index bc2c087c9b9..832757b24d4 100644 --- a/spec/features/issues/bulk_assignment_labels_spec.rb +++ b/spec/features/issues/bulk_assignment_labels_spec.rb @@ -9,6 +9,7 @@ feature 'Issues > Labels bulk assignment', feature: true do let!(:issue2) { create(:issue, project: project, title: "Issue 2") } let!(:bug) { create(:label, project: project, title: 'bug') } let!(:feature) { create(:label, project: project, title: 'feature') } + let!(:wontfix) { create(:label, project: project, title: 'wontfix') } context 'as an allowed user', js: true do before do @@ -291,6 +292,45 @@ feature 'Issues > Labels bulk assignment', feature: true do expect(find("#issue_#{issue1.id}")).not_to have_content 'feature' end end + + # Special case https://gitlab.com/gitlab-org/gitlab-ce/issues/24877 + context 'unmarking common label' do + before do + issue1.labels << bug + issue1.labels << feature + issue2.labels << bug + + visit namespace_project_issues_path(project.namespace, project) + end + + it 'applies label from filtered results' do + check 'check_all_issues' + + page.within('.issues_bulk_update') do + click_button 'Labels' + wait_for_ajax + + expect(find('.dropdown-menu-labels li', text: 'bug')).to have_css('.is-active') + expect(find('.dropdown-menu-labels li', text: 'feature')).to have_css('.is-indeterminate') + + click_link 'bug' + find('.dropdown-input-field', visible: true).set('wontfix') + click_link 'wontfix' + end + + update_issues + + page.within '.issues-holder' do + expect(find("#issue_#{issue1.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue1.id}")).to have_content 'feature' + expect(find("#issue_#{issue1.id}")).to have_content 'wontfix' + + expect(find("#issue_#{issue2.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue2.id}")).not_to have_content 'feature' + expect(find("#issue_#{issue2.id}")).to have_content 'wontfix' + end + end + end end context 'as a guest' do @@ -320,7 +360,7 @@ feature 'Issues > Labels bulk assignment', feature: true do def open_labels_dropdown(items = [], unmark = false) page.within('.issues_bulk_update') do - click_button 'Label' + click_button 'Labels' wait_for_ajax items.map do |item| click_link item diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index c421da97d76..da64827b377 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -2,8 +2,9 @@ require 'rails_helper' feature 'GFM autocomplete', feature: true, js: true do include WaitForAjax - let(:user) { create(:user) } + let(:user) { create(:user, username: 'someone.special') } let(:project) { create(:project) } + let(:label) { create(:label, project: project, title: 'special+') } let(:issue) { create(:issue, project: project) } before do @@ -23,19 +24,75 @@ feature 'GFM autocomplete', feature: true, js: true do expect(page).to have_selector('.atwho-container') end - it 'opens autocomplete menu when field is prefixed with non-text character' do + it 'doesnt open autocomplete menu character is prefixed with text' do page.within '.timeline-content-form' do - find('#note_note').native.send_keys('') + find('#note_note').native.send_keys('testing') find('#note_note').native.send_keys('@') end - expect(page).to have_selector('.atwho-container') + expect(page).not_to have_selector('.atwho-view') end - it 'doesnt open autocomplete menu character is prefixed with text' do + context 'if a selected value has special characters' do + it 'wraps the result in double quotes' do + note = find('#note_note') + page.within '.timeline-content-form' do + note.native.send_keys('') + note.native.send_keys("~#{label.title[0]}") + sleep 1 + note.click + end + + label_item = find('.atwho-view li', text: label.title) + + expect_to_wrap(true, label_item, note, label.title) + end + + it 'doesn\'t wrap for assignee values' do + note = find('#note_note') + page.within '.timeline-content-form' do + note.native.send_keys('') + note.native.send_keys("@#{user.username[0]}") + sleep 1 + note.click + end + + user_item = find('.atwho-view li', text: user.username) + + expect_to_wrap(false, user_item, note, user.username) + end + + it 'doesn\'t wrap for emoji values' do + note = find('#note_note') + page.within '.timeline-content-form' do + note.native.send_keys('') + note.native.send_keys(":cartwheel") + sleep 1 + note.click + end + + emoji_item = find('.atwho-view li', text: 'cartwheel_tone1') + + expect_to_wrap(false, emoji_item, note, 'cartwheel_tone1') + end + + def expect_to_wrap(should_wrap, item, note, value) + expect(item).to have_content(value) + expect(item).not_to have_content("\"#{value}\"") + + item.click + + if should_wrap + expect(note.value).to include("\"#{value}\"") + else + expect(note.value).not_to include("\"#{value}\"") + end + end + end + + it 'doesnt open autocomplete after non-word character' do page.within '.timeline-content-form' do - find('#note_note').native.send_keys('testing') - find('#note_note').native.send_keys('@') + find('#note_note').native.send_keys("@#{user.username[0..2]}!") end expect(page).not_to have_selector('.atwho-view') diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb index a0ccc472d11..8c4d4320dc5 100644 --- a/spec/features/projects/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -227,6 +227,43 @@ feature 'Builds', :feature do expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1') end end + + context 'when build starts environment' do + let(:environment) { create(:environment, project: project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + + context 'build is successfull and has deployment' do + let(:deployment) { create(:deployment) } + let(:build) { create(:ci_build, :success, environment: environment.name, deployments: [deployment], pipeline: pipeline) } + + it 'shows a link for the build' do + visit namespace_project_build_path(project.namespace, project, build) + + expect(page).to have_link environment.name + end + end + + context 'build is complete and not successfull' do + let(:build) { create(:ci_build, :failed, environment: environment.name, pipeline: pipeline) } + + it 'shows a link for the build' do + visit namespace_project_build_path(project.namespace, project, build) + + expect(page).to have_link environment.name + end + end + + context 'build creates a new deployment' do + let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) } + let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) } + + it 'shows a link to lastest deployment' do + visit namespace_project_build_path(project.namespace, project, build) + + expect(page).to have_link('latest deployment') + end + end + end end describe "POST /:project/builds/:id/cancel" do diff --git a/spec/features/projects/files/creating_a_file_spec.rb b/spec/features/projects/files/creating_a_file_spec.rb new file mode 100644 index 00000000000..ae448706130 --- /dev/null +++ b/spec/features/projects/files/creating_a_file_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +feature 'User wants to create a file', feature: true do + include WaitForAjax + + let(:project) { create(:project) } + let(:user) { create(:user) } + + background do + project.team << [user, :master] + login_as user + visit namespace_project_new_blob_path(project.namespace, project, project.default_branch) + end + + def submit_new_file(options) + file_name = find('#file_name') + file_name.set options[:file_name] || 'README.md' + + file_content = find('#file-content') + file_content.set options[:file_content] || 'Some content' + + click_button 'Commit Changes' + end + + scenario 'file name contains Chinese characters' do + submit_new_file(file_name: '测试.md') + expect(page).to have_content 'The file has been successfully created.' + end + + scenario 'directory name contains Chinese characters' do + submit_new_file(file_name: '中文/测试.md') + expect(page).to have_content 'The file has been successfully created.' + end + + scenario 'file name contains invalid characters' do + submit_new_file(file_name: '\\') + expect(page).to have_content 'Your changes could not be committed, because the file name can contain only' + end + + scenario 'file name contains directory traversal' do + submit_new_file(file_name: '../README.md') + expect(page).to have_content 'Your changes could not be committed, because the file name cannot include directory traversal.' + end +end diff --git a/spec/features/security/admin_access_spec.rb b/spec/features/security/admin_access_spec.rb index fe8cd7b7602..e180ca53eb5 100644 --- a/spec/features/security/admin_access_spec.rb +++ b/spec/features/security/admin_access_spec.rb @@ -4,7 +4,7 @@ describe "Admin::Projects", feature: true do include AccessMatchers describe "GET /admin/projects" do - subject { admin_namespaces_projects_path } + subject { admin_projects_path } it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for :user } diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 7f69e888f32..97737d7ddc7 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -10,24 +10,24 @@ describe IssuesFinder do let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone, title: 'gitlab') } let(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'gitlab') } let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2) } - let(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') } - let!(:label_link) { create(:label_link, label: label, target: issue2) } - - before do - project1.team << [user, :master] - project2.team << [user, :developer] - project2.team << [user2, :developer] - - issue1 - issue2 - issue3 - end describe '#execute' do + let(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') } + let!(:label_link) { create(:label_link, label: label, target: issue2) } let(:search_user) { user } let(:params) { {} } let(:issues) { IssuesFinder.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute } + before do + project1.team << [user, :master] + project2.team << [user, :developer] + project2.team << [user2, :developer] + + issue1 + issue2 + issue3 + end + context 'scope: all' do let(:scope) { 'all' } @@ -193,6 +193,15 @@ describe IssuesFinder do expect(issues).to contain_exactly(issue2, issue3) end end + + it 'finds issues user can access due to group' do + group = create(:group) + project = create(:empty_project, group: group) + issue = create(:issue, project: project) + group.add_user(user, :owner) + + expect(issues).to include(issue) + end end context 'personal scope' do @@ -210,5 +219,43 @@ describe IssuesFinder do end end end + + context 'when project restricts issues' do + let(:scope) { nil } + + it "doesn't return team-only issues to non team members" do + project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + issue = create(:issue, project: project) + + expect(issues).not_to include(issue) + end + + it "doesn't return issues if feature disabled" do + [project1, project2].each do |project| + project.project_feature.update!(issues_access_level: ProjectFeature::DISABLED) + end + + expect(issues.count).to eq 0 + end + end + end + + describe '.not_restricted_by_confidentiality' do + let(:authorized_user) { create(:user) } + let(:project) { create(:empty_project, namespace: authorized_user.namespace) } + let!(:public_issue) { create(:issue, project: project) } + let!(:confidential_issue) { create(:issue, project: project, confidential: true) } + + it 'returns non confidential issues for nil user' do + expect(IssuesFinder.send(:not_restricted_by_confidentiality, nil)).to include(public_issue) + end + + it 'returns non confidential issues for user not authorized for the issues projects' do + expect(IssuesFinder.send(:not_restricted_by_confidentiality, user)).to include(public_issue) + end + + it 'returns all issues for user authorized for the issues projects' do + expect(IssuesFinder.send(:not_restricted_by_confidentiality, authorized_user)).to include(public_issue, confidential_issue) + end end end diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index 7c6860372cc..4d21254323c 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -2,59 +2,203 @@ require 'spec_helper' describe NotesFinder do let(:user) { create :user } - let(:project) { create :project } - let(:note1) { create :note_on_commit, project: project } - let(:note2) { create :note_on_commit, project: project } - let(:commit) { note1.noteable } + let(:project) { create(:empty_project) } before do project.team << [user, :master] end describe '#execute' do - let(:params) { { target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } } + it 'finds notes on snippets when project is public and user isnt a member' - before do - note1 - note2 + it 'finds notes on merge requests' do + create(:note_on_merge_request, project: project) + + notes = described_class.new(project, user).execute + + expect(notes.count).to eq(1) end - it 'finds all notes' do - notes = NotesFinder.new.execute(project, user, params) - expect(notes.size).to eq(2) + it 'finds notes on snippets' do + create(:note_on_project_snippet, project: project) + + notes = described_class.new(project, user).execute + + expect(notes.count).to eq(1) end - it 'raises an exception for an invalid target_type' do - params.merge!(target_type: 'invalid') - expect { NotesFinder.new.execute(project, user, params) }.to raise_error('invalid target_type') + it "excludes notes on commits the author can't download" do + project = create(:project, :private) + note = create(:note_on_commit, project: project) + params = { target_type: 'commit', target_id: note.noteable.id } + + notes = described_class.new(project, create(:user), params).execute + + expect(notes.count).to eq(0) end - it 'filters out old notes' do - note2.update_attribute(:updated_at, 2.hours.ago) - notes = NotesFinder.new.execute(project, user, params) - expect(notes).to eq([note1]) + it 'succeeds when no notes found' do + notes = described_class.new(project, create(:user)).execute + + expect(notes.count).to eq(0) end - context 'confidential issue notes' do - let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } - let!(:confidential_note) { create(:note, noteable: confidential_issue, project: confidential_issue.project) } + context 'on restricted projects' do + let(:project) do + create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE, + snippets_access_level: ProjectFeature::PRIVATE, + merge_requests_access_level: ProjectFeature::PRIVATE) + end + + it 'publicly excludes notes on merge requests' do + create(:note_on_merge_request, project: project) + + notes = described_class.new(project, create(:user)).execute + + expect(notes.count).to eq(0) + end + + it 'publicly excludes notes on issues' do + create(:note_on_issue, project: project) + + notes = described_class.new(project, create(:user)).execute + + expect(notes.count).to eq(0) + end + + it 'publicly excludes notes on snippets' do + create(:note_on_project_snippet, project: project) + + notes = described_class.new(project, create(:user)).execute + + expect(notes.count).to eq(0) + end + end + + context 'for target' do + let(:project) { create(:project) } + let(:note1) { create :note_on_commit, project: project } + let(:note2) { create :note_on_commit, project: project } + let(:commit) { note1.noteable } + let(:params) { { target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } } + + before do + note1 + note2 + end + + it 'finds all notes' do + notes = described_class.new(project, user, params).execute + expect(notes.size).to eq(2) + end + + it 'finds notes on merge requests' do + note = create(:note_on_merge_request, project: project) + params = { target_type: 'merge_request', target_id: note.noteable.id } + + notes = described_class.new(project, user, params).execute + + expect(notes).to include(note) + end + + it 'finds notes on snippets' do + note = create(:note_on_project_snippet, project: project) + params = { target_type: 'snippet', target_id: note.noteable.id } - let(:params) { { target_id: confidential_issue.id, target_type: 'issue', last_fetched_at: 1.hour.ago.to_i } } + notes = described_class.new(project, user, params).execute - it 'returns notes if user can see the issue' do - expect(NotesFinder.new.execute(project, user, params)).to eq([confidential_note]) + expect(notes.count).to eq(1) end - it 'raises an error if user can not see the issue' do + it 'raises an exception for an invalid target_type' do + params.merge!(target_type: 'invalid') + expect { described_class.new(project, user, params).execute }.to raise_error('invalid target_type') + end + + it 'filters out old notes' do + note2.update_attribute(:updated_at, 2.hours.ago) + notes = described_class.new(project, user, params).execute + expect(notes).to eq([note1]) + end + + context 'confidential issue notes' do + let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } + let!(:confidential_note) { create(:note, noteable: confidential_issue, project: confidential_issue.project) } + + let(:params) { { target_id: confidential_issue.id, target_type: 'issue', last_fetched_at: 1.hour.ago.to_i } } + + it 'returns notes if user can see the issue' do + expect(described_class.new(project, user, params).execute).to eq([confidential_note]) + end + + it 'raises an error if user can not see the issue' do + user = create(:user) + expect { described_class.new(project, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises an error for project members with guest role' do + user = create(:user) + project.team << [user, :guest] + + expect { described_class.new(project, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end + end + + describe '.search' do + let(:project) { create(:empty_project, :public) } + let(:note) { create(:note_on_issue, note: 'WoW', project: project) } + + it 'returns notes with matching content' do + expect(described_class.new(note.project, nil, search: note.note).execute).to eq([note]) + end + + it 'returns notes with matching content regardless of the casing' do + expect(described_class.new(note.project, nil, search: 'WOW').execute).to eq([note]) + end + + it 'returns commit notes user can access' do + note = create(:note_on_commit, project: project) + + expect(described_class.new(note.project, create(:user), search: note.note).execute).to eq([note]) + end + + context "confidential issues" do + let(:user) { create(:user) } + let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } + let(:confidential_note) { create(:note, note: "Random", noteable: confidential_issue, project: confidential_issue.project) } + + it "returns notes with matching content if user can see the issue" do + expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to eq([confidential_note]) + end + + it "does not return notes with matching content if user can not see the issue" do user = create(:user) - expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound) + expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to be_empty end - it 'raises an error for project members with guest role' do + it "does not return notes with matching content for project members with guest role" do user = create(:user) project.team << [user, :guest] + expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to be_empty + end + + it "does not return notes with matching content for unauthenticated users" do + expect(described_class.new(confidential_note.project, nil, search: confidential_note.note).execute).to be_empty + end + end + + context 'inlines SQL filters on subqueries for performance' do + let(:sql) { described_class.new(note.project, nil, search: note.note).execute.to_sql } + let(:number_of_noteable_types) { 4 } + + specify 'project_id check' do + expect(sql.scan(/project_id/).count).to be >= (number_of_noteable_types + 2) + end - expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound) + specify 'search filter' do + expect(sql.scan(/LIKE/).count).to be >= number_of_noteable_types end end end diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index 28bdc18e840..975e99c5807 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -9,65 +9,74 @@ describe SnippetsFinder do let(:project2) { create(:empty_project, :private, group: group) } context ':all filter' do - before do - @snippet1 = create(:personal_snippet, :private) - @snippet2 = create(:personal_snippet, :internal) - @snippet3 = create(:personal_snippet, :public) - end + let!(:snippet1) { create(:personal_snippet, :private) } + let!(:snippet2) { create(:personal_snippet, :internal) } + let!(:snippet3) { create(:personal_snippet, :public) } it "returns all private and internal snippets" do snippets = SnippetsFinder.new.execute(user, filter: :all) - expect(snippets).to include(@snippet2, @snippet3) - expect(snippets).not_to include(@snippet1) + expect(snippets).to include(snippet2, snippet3) + expect(snippets).not_to include(snippet1) end it "returns all public snippets" do snippets = SnippetsFinder.new.execute(nil, filter: :all) - expect(snippets).to include(@snippet3) - expect(snippets).not_to include(@snippet1, @snippet2) + expect(snippets).to include(snippet3) + expect(snippets).not_to include(snippet1, snippet2) end end - context ':by_user filter' do - before do - @snippet1 = create(:personal_snippet, :private, author: user) - @snippet2 = create(:personal_snippet, :internal, author: user) - @snippet3 = create(:personal_snippet, :public, author: user) + context ':public filter' do + let!(:snippet1) { create(:personal_snippet, :private) } + let!(:snippet2) { create(:personal_snippet, :internal) } + let!(:snippet3) { create(:personal_snippet, :public) } + + it "returns public public snippets" do + snippets = SnippetsFinder.new.execute(nil, filter: :public) + + expect(snippets).to include(snippet3) + expect(snippets).not_to include(snippet1, snippet2) end + end + + context ':by_user filter' do + let!(:snippet1) { create(:personal_snippet, :private, author: user) } + let!(:snippet2) { create(:personal_snippet, :internal, author: user) } + let!(:snippet3) { create(:personal_snippet, :public, author: user) } it "returns all public and internal snippets" do snippets = SnippetsFinder.new.execute(user1, filter: :by_user, user: user) - expect(snippets).to include(@snippet2, @snippet3) - expect(snippets).not_to include(@snippet1) + expect(snippets).to include(snippet2, snippet3) + expect(snippets).not_to include(snippet1) end it "returns internal snippets" do snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_internal") - expect(snippets).to include(@snippet2) - expect(snippets).not_to include(@snippet1, @snippet3) + expect(snippets).to include(snippet2) + expect(snippets).not_to include(snippet1, snippet3) end it "returns private snippets" do snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_private") - expect(snippets).to include(@snippet1) - expect(snippets).not_to include(@snippet2, @snippet3) + expect(snippets).to include(snippet1) + expect(snippets).not_to include(snippet2, snippet3) end it "returns public snippets" do snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_public") - expect(snippets).to include(@snippet3) - expect(snippets).not_to include(@snippet1, @snippet2) + expect(snippets).to include(snippet3) + expect(snippets).not_to include(snippet1, snippet2) end it "returns all snippets" do snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user) - expect(snippets).to include(@snippet1, @snippet2, @snippet3) + expect(snippets).to include(snippet1, snippet2, snippet3) end it "returns only public snippets if unauthenticated user" do snippets = SnippetsFinder.new.execute(nil, filter: :by_user, user: user) - expect(snippets).to include(@snippet3) - expect(snippets).not_to include(@snippet2, @snippet1) + expect(snippets).to include(snippet3) + expect(snippets).not_to include(snippet2, snippet1) end end @@ -84,16 +93,39 @@ describe SnippetsFinder do expect(snippets).not_to include(@snippet1, @snippet2) end - it "returns public and internal snippets for none project members" do + it "returns public and internal snippets for non project members" do snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) expect(snippets).to include(@snippet2, @snippet3) expect(snippets).not_to include(@snippet1) end + it "returns public snippets for non project members" do + snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_public") + expect(snippets).to include(@snippet3) + expect(snippets).not_to include(@snippet1, @snippet2) + end + + it "returns internal snippets for non project members" do + snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_internal") + expect(snippets).to include(@snippet2) + expect(snippets).not_to include(@snippet1, @snippet3) + end + + it "does not return private snippets for non project members" do + snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_private") + expect(snippets).not_to include(@snippet1, @snippet2, @snippet3) + end + it "returns all snippets for project members" do project1.team << [user, :developer] snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) expect(snippets).to include(@snippet1, @snippet2, @snippet3) end + + it "returns private snippets for project members" do + project1.team << [user, :developer] + snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_private") + expect(snippets).to include(@snippet1) + end end end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 15863d444f8..92053e5a7c6 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe ApplicationHelper do + include UploadHelpers + describe 'current_controller?' do it 'returns true when controller matches argument' do stub_controller_name('foo') @@ -52,10 +54,8 @@ describe ApplicationHelper do end describe 'project_icon' do - let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') } - it 'returns an url for the avatar' do - project = create(:project, avatar: File.open(avatar_file_path)) + project = create(:project, avatar: File.open(uploaded_image_temp_path)) avatar_url = "http://#{Gitlab.config.gitlab.host}/uploads/project/avatar/#{project.id}/banana_sample.gif" expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s). @@ -74,10 +74,8 @@ describe ApplicationHelper do end describe 'avatar_icon' do - let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') } - it 'returns an url for the avatar' do - user = create(:user, avatar: File.open(avatar_file_path)) + user = create(:user, avatar: File.open(uploaded_image_temp_path)) expect(helper.avatar_icon(user.email).to_s). to match("/uploads/user/avatar/#{user.id}/banana_sample.gif") @@ -88,7 +86,7 @@ describe ApplicationHelper do # Must be stubbed after the stub above, and separately stub_config_setting(url: Settings.send(:build_gitlab_url)) - user = create(:user, avatar: File.open(avatar_file_path)) + user = create(:user, avatar: File.open(uploaded_image_temp_path)) expect(helper.avatar_icon(user.email).to_s). to match("/gitlab/uploads/user/avatar/#{user.id}/banana_sample.gif") @@ -102,7 +100,7 @@ describe ApplicationHelper do describe 'using a User' do it 'returns an URL for the avatar' do - user = create(:user, avatar: File.open(avatar_file_path)) + user = create(:user, avatar: File.open(uploaded_image_temp_path)) expect(helper.avatar_icon(user).to_s). to match("/uploads/user/avatar/#{user.id}/banana_sample.gif") diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6 index a3171353bfb..9e94c9d1d74 100644 --- a/spec/javascripts/abuse_reports_spec.js.es6 +++ b/spec/javascripts/abuse_reports_spec.js.es6 @@ -1,6 +1,6 @@ -/* eslint-disable */ -/*= require abuse_reports */ +/* eslint-disable space-before-function-paren, no-new, padded-blocks */ +/*= require abuse_reports */ /*= require jquery */ ((global) => { diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6 index 8640cd44085..192da4ee8d9 100644 --- a/spec/javascripts/activities_spec.js.es6 +++ b/spec/javascripts/activities_spec.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-unused-expressions, comma-spacing, prefer-const, no-prototype-builtins, semi, no-new, keyword-spacing, no-plusplus, no-shadow, max-len */ + /*= require js.cookie.js */ /*= require jquery.endless-scroll.js */ /*= require pager */ diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index ac1404f6e1c..89201c8cb8b 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, no-undef, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, padded-blocks, max-len */ +/* global AwardsHandler */ /*= require awards_handler */ /*= require jquery */ @@ -33,9 +34,9 @@ }; describe('AwardsHandler', function() { - fixture.preload('awards_handler.html'); + fixture.preload('issues/open-issue.html.raw'); beforeEach(function() { - fixture.load('awards_handler.html'); + fixture.load('issues/open-issue.html.raw'); awardsHandler = new AwardsHandler; spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) { return function(url, emoji, cb) { @@ -113,7 +114,7 @@ }); describe('::getAwardUrl', function() { return it('should return the url for request', function() { - return expect(awardsHandler.getAwardUrl()).toBe('/gitlab-org/gitlab-test/issues/8/toggle_award_emoji'); + return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/issues-project/issues/1/toggle_award_emoji'); }); }); describe('::addAward and ::checkMutuality', function() { @@ -209,7 +210,7 @@ $('.js-add-award').eq(0).click(); $menu = $('.emoji-menu'); $block = $('.js-awards-block'); - $emoji = $menu.find(".emoji-menu-list-item " + selector); + $emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + selector); expect($emoji.length).toBe(1); expect($block.find(selector).length).toBe(0); $emoji.click(); @@ -224,7 +225,7 @@ openEmojiMenuAndAddEmoji(); $('.js-add-award').eq(0).click(); $block = $('.js-awards-block'); - $emoji = $('.emoji-menu').find(".emoji-menu-list-item " + selector); + $emoji = $('.emoji-menu').find(".emoji-menu-list:not(.frequent-emojis) " + selector); $emoji.click(); return expect($block.find(selector).length).toBe(0); }); diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index efb1203eb2f..0f61000bc37 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, no-undef, jasmine/no-spec-dupes, new-cap, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, padded-blocks, max-len */ /*= require behaviors/quick_submit */ diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6 index b84dfc8197b..b3a1afa28a5 100644 --- a/spec/javascripts/boards/boards_store_spec.js.es6 +++ b/spec/javascripts/boards/boards_store_spec.js.es6 @@ -1,4 +1,11 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, one-var, no-unused-vars, indent */ +/* global Vue */ +/* global BoardService */ +/* global boardsMockInterceptor */ +/* global Cookies */ +/* global listObj */ +/* global listObjDuplicate */ + //= require jquery //= require jquery_ujs //= require js.cookie diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6 index 90cb8926545..c8a61a0a9b5 100644 --- a/spec/javascripts/boards/issue_spec.js.es6 +++ b/spec/javascripts/boards/issue_spec.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle */ +/* global BoardService */ +/* global ListIssue */ + //= require jquery //= require jquery_ujs //= require js.cookie diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index dfbcbe3a7c1..7d942ec3d65 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -1,4 +1,10 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle */ +/* global Vue */ +/* global boardsMockInterceptor */ +/* global BoardService */ +/* global List */ +/* global listObj */ + //= require jquery //= require jquery_ujs //= require js.cookie diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6 index fcb3d8f17d8..8d3e2237fda 100644 --- a/spec/javascripts/boards/mock_data.js.es6 +++ b/spec/javascripts/boards/mock_data.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, no-unused-vars, quote-props */ + const listObj = { id: 1, position: 0, diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6 index 93f73fa0e9a..aadf6f518a8 100644 --- a/spec/javascripts/dashboard_spec.js.es6 +++ b/spec/javascripts/dashboard_spec.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-new, padded-blocks */ + /*= require sidebar */ /*= require jquery */ /*= require js.cookie */ diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6 index 9fdbab3a9e9..8ece24555c5 100644 --- a/spec/javascripts/datetime_utility_spec.js.es6 +++ b/spec/javascripts/datetime_utility_spec.js.es6 @@ -1,5 +1,5 @@ -/* eslint-disable */ //= require lib/utils/datetime_utility + (() => { describe('Date time utils', () => { describe('get day name', () => { diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6 index 9b2845af608..18805d26ac0 100644 --- a/spec/javascripts/diff_comments_store_spec.js.es6 +++ b/spec/javascripts/diff_comments_store_spec.js.es6 @@ -1,8 +1,11 @@ -/* eslint-disable */ +/* eslint-disable no-extra-semi, jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */ +/* global CommentsStore */ + //= require vue //= require diff_notes/models/discussion //= require diff_notes/models/note //= require diff_notes/stores/comments + (() => { function createDiscussion(noteId = 1, resolved = true) { CommentsStore.create('a', noteId, true, resolved, 'test'); diff --git a/spec/javascripts/extensions/object_spec.js.es6 b/spec/javascripts/extensions/object_spec.js.es6 new file mode 100644 index 00000000000..3b71c255b30 --- /dev/null +++ b/spec/javascripts/extensions/object_spec.js.es6 @@ -0,0 +1,25 @@ +/*= require extensions/object */ + +describe('Object extensions', () => { + describe('assign', () => { + it('merges source object into target object', () => { + const targetObj = {}; + const sourceObj = { + foo: 'bar', + }; + Object.assign(targetObj, sourceObj); + expect(targetObj.foo).toBe('bar'); + }); + + it('merges object with the same properties', () => { + const targetObj = { + foo: 'bar', + }; + const sourceObj = { + foo: 'baz', + }; + Object.assign(targetObj, sourceObj); + expect(targetObj.foo).toBe('baz'); + }); + }); +}); diff --git a/spec/javascripts/fixtures/awards_handler.html.haml b/spec/javascripts/fixtures/awards_handler.html.haml deleted file mode 100644 index 1ef2e8f8624..00000000000 --- a/spec/javascripts/fixtures/awards_handler.html.haml +++ /dev/null @@ -1,52 +0,0 @@ -.issue-details.issuable-details - .detail-page-description.content-block - %h2.title Quibusdam sint officiis earum molestiae ipsa autem voluptatem nisi rem. - .description.js-task-list-container.is-task-list-enabled - .wiki - %p Qui exercitationem magnam optio quae fuga earum odio. - %textarea.hidden.js-task-list-field Qui exercitationem magnam optio quae fuga earum odio. - %small.edited-text - .content-block.content-block-small - .awards.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/issues/8/toggle_award_emoji"} - %button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"} - .icon.emoji-icon.emoji-1F44D{"data-aliases" => "", "data-emoji" => "thumbsup", "data-unicode-name" => "1F44D", :title => "thumbsup"} - %span.award-control-text.js-counter 0 - %button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"} - .icon.emoji-icon.emoji-1F44E{"data-aliases" => "", "data-emoji" => "thumbsdown", "data-unicode-name" => "1F44E", :title => "thumbsdown"} - %span.award-control-text.js-counter 0 - .award-menu-holder.js-award-holder - %button.btn.award-control.js-add-award{:type => "button"} - %i.fa.fa-smile-o.award-control-icon.award-control-icon-normal - %i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading - %span.award-control-text Add - %section.issuable-discussion - #notes - %ul#notes-list.notes.main-notes-list.timeline - %li#note_348.note.note-row-348.timeline-entry{"data-author-id" => "18", "data-editable" => ""} - .timeline-entry-inner - .timeline-icon - %a{:href => "/u/agustin"} - %img.avatar.s40{:alt => "", :src => "#"}/ - .timeline-content - .note-header - %a.author_link{:href => "/u/agustin"} - %span.author Brenna Stokes - .inline.note-headline-light - @agustin commented - %a{:href => "#note_348"} - %time 11 days ago - .note-actions - %span.note-role Reporter - %a.note-action-button.note-emoji-button.js-add-award.js-note-emoji{"data-position" => "right", :href => "#", :title => "Award Emoji"} - %i.fa.fa-spinner.fa-spin - %i.fa.fa-smile-o.link-highlight - .js-task-list-container.note-body.is-task-list-enabled - .note-text - %p Suscipit sunt quia quisquam sed eveniet ipsam. - .note-awards - .awards.hidden.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/notes/348/toggle_award_emoji"} - .award-menu-holder.js-award-holder - %button.btn.award-control.js-add-award{:type => "button"} - %i.fa.fa-smile-o.award-control-icon.award-control-icon-normal - %i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading - %span.award-control-text Add diff --git a/spec/javascripts/fixtures/pipeline_graph.html.haml b/spec/javascripts/fixtures/pipeline_graph.html.haml new file mode 100644 index 00000000000..deca50ceaa7 --- /dev/null +++ b/spec/javascripts/fixtures/pipeline_graph.html.haml @@ -0,0 +1,15 @@ +%div.pipeline-visualization.js-pipeline-graph + %ul.stage-column-list + %li.stage-column + .stage-name + %a{:href => "/"} + Test + .builds-container + %ul + %li.build + .curve + .build-content + %a + %svg + .ci-status-text + stop_review diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6 index 8ba238018cd..bfaf90e2aee 100644 --- a/spec/javascripts/gl_dropdown_spec.js.es6 +++ b/spec/javascripts/gl_dropdown_spec.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, prefer-const, no-param-reassign, no-plusplus, semi, no-unused-expressions, arrow-spacing, max-len */ +/* global Turbolinks */ + /*= require jquery */ /*= require gl_dropdown */ /*= require turbolinks */ diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6 index 0713e30e485..5018e87ad6c 100644 --- a/spec/javascripts/gl_field_errors_spec.js.es6 +++ b/spec/javascripts/gl_field_errors_spec.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable space-before-function-paren, arrow-body-style, indent, padded-blocks */ + //= require jquery //= require gl_field_errors diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js index a406e6cc36a..bc5cbeb6a40 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -1,4 +1,8 @@ -/* eslint-disable quotes, no-undef, indent, semi, object-curly-spacing, jasmine/no-suite-dupes, vars-on-top, no-var, padded-blocks, spaced-comment, max-len */ +/* eslint-disable quotes, indent, semi, object-curly-spacing, jasmine/no-suite-dupes, vars-on-top, no-var, padded-blocks, spaced-comment, max-len */ +/* global d3 */ +/* global ContributorsGraph */ +/* global ContributorsMasterGraph */ + //= require graphs/stat_graph_contributors_graph describe("ContributorsGraph", function () { diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js index 96f39abe13e..751f3d175e2 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable quotes, padded-blocks, no-var, camelcase, object-curly-spacing, semi, indent, object-property-newline, comma-dangle, comma-spacing, no-undef, spaced-comment, max-len, key-spacing, vars-on-top, quote-props, no-multi-spaces, max-len */ +/* eslint-disable quotes, padded-blocks, no-var, camelcase, object-curly-spacing, semi, indent, object-property-newline, comma-dangle, comma-spacing, spaced-comment, max-len, key-spacing, vars-on-top, quote-props, no-multi-spaces */ +/* global ContributorsStatGraphUtil */ + //= require graphs/stat_graph_contributors_util describe("ContributorsStatGraphUtil", function () { diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js index f78573b992b..0da124632ae 100644 --- a/spec/javascripts/graphs/stat_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable quotes, padded-blocks, no-undef, semi */ +/* eslint-disable quotes, padded-blocks, semi */ +/* global StatGraph */ + //= require graphs/stat_graph describe("StatGraph", function () { diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 14af6644de1..faab5ae00c2 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, indent, no-undef, no-trailing-spaces, comma-dangle, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, indent, no-trailing-spaces, comma-dangle, padded-blocks, max-len */ +/* global Issue */ /*= require lib/utils/text_utility */ /*= require issue */ @@ -70,7 +71,7 @@ $('input[type=checkbox]').attr('checked', true).trigger('change'); expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); }); - + it('submits an ajax request on tasklist:changed', function() { spyOn(jQuery, 'ajax').and.callFake(function(req) { expect(req.type).toBe('PATCH'); diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6 index 49687048eb5..0c48d04776f 100644 --- a/spec/javascripts/labels_issue_sidebar_spec.js.es6 +++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable no-new, no-plusplus, object-curly-spacing, prefer-const, semi */ +/* global IssuableContext */ +/* global LabelsSelect */ + //= require lib/utils/type_utility //= require jquery //= require bootstrap diff --git a/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 b/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 new file mode 100644 index 00000000000..3645dd70c55 --- /dev/null +++ b/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 @@ -0,0 +1,43 @@ +//= require lib/utils/custom_event_polyfill + +describe('Custom Event Polyfill', () => { + it('should be defined', () => { + expect(window.CustomEvent).toBeDefined(); + }); + + it('should create a `CustomEvent` instance', () => { + const e = new window.CustomEvent('foo'); + + expect(e.type).toEqual('foo'); + expect(e.bubbles).toBe(false); + expect(e.cancelable).toBe(false); + expect(e.detail).toBeFalsy(); + }); + + it('should create a `CustomEvent` instance with a `details` object', () => { + const e = new window.CustomEvent('bar', { detail: { foo: 'bar' } }); + + expect(e.type).toEqual('bar'); + expect(e.bubbles).toBe(false); + expect(e.cancelable).toBe(false); + expect(e.detail.foo).toEqual('bar'); + }); + + it('should create a `CustomEvent` instance with a `bubbles` boolean', () => { + const e = new window.CustomEvent('bar', { bubbles: true }); + + expect(e.type).toEqual('bar'); + expect(e.bubbles).toBe(true); + expect(e.cancelable).toBe(false); + expect(e.detail).toBeFalsy(); + }); + + it('should create a `CustomEvent` instance with a `cancelable` boolean', () => { + const e = new window.CustomEvent('bar', { cancelable: true }); + + expect(e.type).toEqual('bar'); + expect(e.bubbles).toBe(false); + expect(e.cancelable).toBe(true); + expect(e.detail).toBeFalsy(); + }); +}); diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js index b8b174a2e53..decdf583410 100644 --- a/spec/javascripts/line_highlighter_spec.js +++ b/spec/javascripts/line_highlighter_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-undef, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, no-plusplus, jasmine/no-spec-dupes, no-underscore-dangle, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, no-plusplus, jasmine/no-spec-dupes, no-underscore-dangle, padded-blocks, max-len */ +/* global LineHighlighter */ /*= require line_highlighter */ diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index cbe2634d3a4..4cf1693af1b 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, no-return-assign, no-undef, padded-blocks */ +/* eslint-disable space-before-function-paren, no-return-assign, padded-blocks */ +/* global MergeRequest */ /*= require merge_request */ diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index 8828970d984..a6cb9e47744 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, no-undef, quotes, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, padded-blocks, max-len */ +/* global NewBranchForm */ /*= require jquery-ui/autocomplete */ /*= require new_branch_form */ diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 2db182d702b..d3bfa7730fa 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable space-before-function-paren, no-unused-expressions, no-undef, no-var, object-shorthand, comma-dangle, semi, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, semi, padded-blocks, max-len */ +/* global Notes */ + /*= require notes */ /*= require autosize */ /*= require gl_form */ diff --git a/spec/javascripts/pipelines_spec.js.es6 b/spec/javascripts/pipelines_spec.js.es6 new file mode 100644 index 00000000000..85c9cf4b4f1 --- /dev/null +++ b/spec/javascripts/pipelines_spec.js.es6 @@ -0,0 +1,25 @@ +//= require pipelines + +(() => { + describe('Pipelines', () => { + fixture.preload('pipeline_graph'); + + beforeEach(() => { + fixture.load('pipeline_graph'); + }); + + it('should be defined', () => { + expect(window.gl.Pipelines).toBeDefined(); + }); + + it('should create a `Pipelines` instance without options', () => { + expect(() => { new window.gl.Pipelines(); }).not.toThrow(); //eslint-disable-line + }); + + it('should create a `Pipelines` instance with options', () => { + const pipelines = new window.gl.Pipelines({ foo: 'bar' }); + + expect(pipelines.pipelineGraph).toBeDefined(); + }); + }); +})(); diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index 49211a6b852..bb802a4b5e3 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-undef, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, jasmine/no-expect-in-setup-teardown, padded-blocks, max-len */ + +/* global Project */ /*= require bootstrap */ /*= require select2 */ @@ -21,16 +23,18 @@ return this.project = new Project(); }); return describe('project list', function() { + var fakeAjaxResponse = function fakeAjaxResponse(req) { + var d; + expect(req.url).toBe('/api/v3/projects.json?simple=true'); + d = $.Deferred(); + d.resolve(this.projects_data); + return d.promise(); + }; + beforeEach((function(_this) { return function() { _this.projects_data = fixture.load('projects.json')[0]; - return spyOn(jQuery, 'ajax').and.callFake(function(req) { - var d; - expect(req.url).toBe('/api/v3/projects.json?simple=true'); - d = $.Deferred(); - d.resolve(_this.projects_data); - return d.promise(); - }); + return spyOn(jQuery, 'ajax').and.callFake(fakeAjaxResponse.bind(_this)); }; })(this)); it('to show on toggle click', (function(_this) { diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 0a9bc546144..a083dbf033a 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-undef, no-return-assign, new-cap, vars-on-top, semi, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, semi, padded-blocks, max-len */ +/* global Sidebar */ /*= require right_sidebar */ /*= require jquery */ diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index e37816b0a8c..7bc898aed5d 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, no-return-assign, no-undef, no-var, quotes, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes, padded-blocks */ +/* global ShortcutsIssuable */ /*= require shortcuts_issuable */ diff --git a/spec/javascripts/smart_interval_spec.js.es6 b/spec/javascripts/smart_interval_spec.js.es6 index ed6166a25a8..1b7ca97cde4 100644 --- a/spec/javascripts/smart_interval_spec.js.es6 +++ b/spec/javascripts/smart_interval_spec.js.es6 @@ -14,8 +14,9 @@ startingInterval: DEFAULT_STARTING_INTERVAL, maxInterval: DEFAULT_MAX_INTERVAL, incrementByFactorOf: DEFAULT_INCREMENT_FACTOR, - delayStartBy: 0, lazyStart: false, + immediateExecution: false, + hiddenInterval: null, }; if (config) { @@ -114,14 +115,31 @@ expect(interval.state.intervalId).toBeTruthy(); // simulates triggering of visibilitychange event - interval.state.pageVisibility = 'hidden'; - interval.handleVisibilityChange(); + interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); expect(interval.state.intervalId).toBeUndefined(); done(); }, DEFAULT_SHORT_TIMEOUT); }); + it('should change to the hidden interval when page is not visible', function (done) { + const HIDDEN_INTERVAL = 1500; + const interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL }); + + setTimeout(() => { + expect(interval.state.intervalId).toBeTruthy(); + expect(interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL && + interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL).toBeTruthy(); + + // simulates triggering of visibilitychange event + interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); + + expect(interval.state.intervalId).toBeTruthy(); + expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL); + done(); + }, DEFAULT_SHORT_TIMEOUT); + }); + it('should resume when page is becomes visible at the previous interval', function (done) { const interval = this.smartInterval; @@ -129,14 +147,12 @@ expect(interval.state.intervalId).toBeTruthy(); // simulates triggering of visibilitychange event - interval.state.pageVisibility = 'hidden'; - interval.handleVisibilityChange(); + interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); expect(interval.state.intervalId).toBeUndefined(); // simulates triggering of visibilitychange event - interval.state.pageVisibility = 'visible'; - interval.handleVisibilityChange(); + interval.handleVisibilityChange({ target: { visibilityState: 'visible' } }); expect(interval.state.intervalId).toBeTruthy(); @@ -154,6 +170,11 @@ done(); }, DEFAULT_SHORT_TIMEOUT); }); + + it('should execute callback before first interval', function () { + const interval = createDefaultSmartInterval({ immediateExecution: true }); + expect(interval.cfg.immediateExecution).toBeFalsy(); + }); }); }); })(window.gl || (window.gl = {})); diff --git a/spec/javascripts/subbable_resource_spec.js.es6 b/spec/javascripts/subbable_resource_spec.js.es6 index df395296791..6a70dd856a7 100644 --- a/spec/javascripts/subbable_resource_spec.js.es6 +++ b/spec/javascripts/subbable_resource_spec.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable max-len, arrow-parens, comma-dangle, no-plusplus */ + //= vue //= vue-resource //= require jquery diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index 944df6d23f7..a8874ab12d3 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable space-before-function-paren, new-parens, no-undef, quotes, comma-dangle, no-var, one-var, one-var-declaration-per-line, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, new-parens, quotes, comma-dangle, no-var, one-var, one-var-declaration-per-line, padded-blocks, max-len */ +/* global MockU2FDevice */ +/* global U2FAuthenticate */ /*= require u2f/authenticate */ /*= require u2f/util */ diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index 0c73c5772bd..189592ea87a 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable space-before-function-paren, new-parens, no-undef, quotes, no-var, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, new-parens, quotes, no-var, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */ +/* global MockU2FDevice */ +/* global U2FRegister */ /*= require u2f/register */ /*= require u2f/util */ diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index b9acaaa5a0d..d80ce5a7f7e 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -1,4 +1,7 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-undef, object-shorthand, comma-dangle, no-return-assign, new-cap, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-return-assign, new-cap, padded-blocks, max-len */ +/* global Dropzone */ +/* global Mousetrap */ +/* global ZenMode */ /*= require zen_mode */ diff --git a/spec/lib/banzai/filter/math_filter_spec.rb b/spec/lib/banzai/filter/math_filter_spec.rb new file mode 100644 index 00000000000..3fe2c7f5d5d --- /dev/null +++ b/spec/lib/banzai/filter/math_filter_spec.rb @@ -0,0 +1,120 @@ +require 'spec_helper' + +describe Banzai::Filter::MathFilter, lib: true do + include FilterSpecHelper + + it 'leaves regular inline code unchanged' do + input = "<code>2+2</code>" + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'removes surrounding dollar signs and adds class code, math and js-render-math' do + doc = filter("$<code>2+2</code>$") + + expect(doc.to_s).to eq '<code class="code math js-render-math" data-math-style="inline">2+2</code>' + end + + it 'only removes surrounding dollar signs' do + doc = filter("test $<code>2+2</code>$ test") + before = doc.xpath('descendant-or-self::text()[1]').first + after = doc.xpath('descendant-or-self::text()[3]').first + + expect(before.to_s).to eq 'test ' + expect(after.to_s).to eq ' test' + end + + it 'only removes surrounding single dollar sign' do + doc = filter("test $$<code>2+2</code>$$ test") + before = doc.xpath('descendant-or-self::text()[1]').first + after = doc.xpath('descendant-or-self::text()[3]').first + + expect(before.to_s).to eq 'test $' + expect(after.to_s).to eq '$ test' + end + + it 'adds data-math-style inline attribute to inline math' do + doc = filter('$<code>2+2</code>$') + code = doc.xpath('descendant-or-self::code').first + + expect(code['data-math-style']).to eq 'inline' + end + + it 'adds class code and math to inline math' do + doc = filter('$<code>2+2</code>$') + code = doc.xpath('descendant-or-self::code').first + + expect(code[:class]).to include("code") + expect(code[:class]).to include("math") + end + + it 'adds js-render-math class to inline math' do + doc = filter('$<code>2+2</code>$') + code = doc.xpath('descendant-or-self::code').first + + expect(code[:class]).to include("js-render-math") + end + + # Cases with faulty syntax. Should be a no-op + + it 'ignores cases with missing dolar sign at the end' do + input = "test $<code>2+2</code> test" + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'ignores cases with missing dolar sign at the beginning' do + input = "test <code>2+2</code>$ test" + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'ignores dollar signs if it is not adjacent' do + input = '<p>We check strictly $<code>2+2</code> and <code>2+2</code>$ </p>' + doc = filter(input) + + expect(doc.to_s).to eq input + end + + # Display math + + it 'adds data-math-style display attribute to display math' do + doc = filter('<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>') + pre = doc.xpath('descendant-or-self::pre').first + + expect(pre['data-math-style']).to eq 'display' + end + + it 'adds js-render-math class to display math' do + doc = filter('<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>') + pre = doc.xpath('descendant-or-self::pre').first + + expect(pre[:class]).to include("js-render-math") + end + + it 'ignores code blocks that are not math' do + input = '<pre class="code highlight js-syntax-highlight plaintext" v-pre="true"><code>2+2</code></pre>' + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'requires the pre to contain both code and math' do + input = '<pre class="highlight js-syntax-highlight plaintext math" v-pre="true"><code>2+2</code></pre>' + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'dollar signs around to display math' do + doc = filter('$<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>$') + before = doc.xpath('descendant-or-self::text()[1]').first + after = doc.xpath('descendant-or-self::text()[3]').first + + expect(before.to_s).to eq '$' + expect(after.to_s).to eq '$' + end +end diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index 2bfa51deb20..df2dd173b57 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -175,7 +175,7 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do allow_any_instance_of(described_class).to receive(:uri_type).and_return(:raw) doc = filter(image(escaped)) - expect(doc.at_css('img')['src']).to match '/raw/' + expect(doc.at_css('img')['src']).to eq "/#{project_path}/raw/#{Addressable::URI.escape(ref)}/#{escaped}" end context 'when requested path is a file in the repo' do diff --git a/spec/lib/gitlab/allowable_spec.rb b/spec/lib/gitlab/allowable_spec.rb new file mode 100644 index 00000000000..87733d53e92 --- /dev/null +++ b/spec/lib/gitlab/allowable_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Gitlab::Allowable do + subject do + Class.new.include(described_class).new + end + + describe '#can?' do + let(:user) { create(:user) } + + context 'when user is allowed to do something' do + let(:project) { create(:empty_project, :public) } + + it 'reports correct ability to perform action' do + expect(subject.can?(user, :read_project, project)).to be true + end + end + + context 'when user is not allowed to do something' do + let(:project) { create(:empty_project, :private) } + + it 'reports correct ability to perform action' do + expect(subject.can?(user, :read_project, project)).to be false + end + end + end +end diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index 4aba783dc33..f3843ca64ff 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -11,7 +11,7 @@ module Gitlab it "converts the input using Asciidoctor and default options" do expected_asciidoc_opts = { safe: :secure, - backend: :html5, + backend: :gitlab_html5, attributes: described_class::DEFAULT_ADOC_ATTRS } @@ -27,7 +27,7 @@ module Gitlab it "merges the options with default ones" do expected_asciidoc_opts = { safe: :safe, - backend: :html5, + backend: :gitlab_html5, attributes: described_class::DEFAULT_ADOC_ATTRS + ['foo'] } diff --git a/spec/lib/gitlab/badge/build/status_spec.rb b/spec/lib/gitlab/badge/build/status_spec.rb index 38eebb2a176..70f03021d36 100644 --- a/spec/lib/gitlab/badge/build/status_spec.rb +++ b/spec/lib/gitlab/badge/build/status_spec.rb @@ -69,8 +69,8 @@ describe Gitlab::Badge::Build::Status do new_build.success! end - it 'reports the compound status' do - expect(badge.status).to eq 'failed' + it 'does not take outdated pipeline into account' do + expect(badge.status).to eq 'success' end end end diff --git a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb new file mode 100644 index 00000000000..9376bce17a1 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Cancelable do + let(:status) { double('core status') } + let(:user) { double('user') } + + subject do + described_class.new(status) + end + + describe '#text' do + it 'does not override status text' do + expect(status).to receive(:text) + + subject.text + end + end + + describe '#icon' do + it 'does not override status icon' do + expect(status).to receive(:icon) + + subject.icon + end + end + + describe '#label' do + it 'does not override status label' do + expect(status).to receive(:label) + + subject.label + end + end + + describe 'action details' do + let(:user) { create(:user) } + let(:build) { create(:ci_build) } + let(:status) { Gitlab::Ci::Status::Core.new(build, user) } + + describe '#has_action?' do + context 'when user is allowed to update build' do + before { build.project.team << [user, :developer] } + + it { is_expected.to have_action } + end + + context 'when user is not allowed to update build' do + it { is_expected.not_to have_action } + end + end + + describe '#action_path' do + it { expect(subject.action_path).to include "#{build.id}/cancel" } + end + + describe '#action_icon' do + it { expect(subject.action_icon).to eq 'ban' } + end + + describe '#action_title' do + it { expect(subject.action_title).to eq 'Cancel' } + end + end + + describe '.matches?' do + subject { described_class.matches?(build, user) } + + context 'when build is cancelable' do + let(:build) do + create(:ci_build, :running) + end + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when build is not cancelable' do + let(:build) { create(:ci_build, :success) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/common_spec.rb b/spec/lib/gitlab/ci/status/build/common_spec.rb new file mode 100644 index 00000000000..40b96b1807b --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/common_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Common do + let(:user) { create(:user) } + let(:build) { create(:ci_build) } + let(:project) { build.project } + + subject do + Gitlab::Ci::Status::Core + .new(build, user) + .extend(described_class) + end + + describe '#has_action?' do + it { is_expected.not_to have_action } + end + + describe '#has_details?' do + context 'when user has access to read build' do + before { project.team << [user, :developer] } + + it { is_expected.to have_details } + end + + context 'when user does not have access to read build' do + before { project.update(public_builds: false) } + + it { is_expected.not_to have_details } + end + end + + describe '#details_path' do + it 'links to the build details page' do + expect(subject.details_path).to include "builds/#{build.id}" + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb new file mode 100644 index 00000000000..dccb29b5ef6 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -0,0 +1,141 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Factory do + let(:user) { create(:user) } + let(:project) { build.project } + + subject { described_class.new(build, user) } + let(:status) { subject.fabricate! } + + before { project.team << [user, :developer] } + + context 'when build is successful' do + let(:build) { create(:ci_build, :success) } + + it 'fabricates a retryable build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Retryable + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'passed' + expect(status.icon).to eq 'icon_status_success' + expect(status.label).to eq 'passed' + expect(status).to have_details + expect(status).to have_action + end + end + + context 'when build is failed' do + let(:build) { create(:ci_build, :failed) } + + it 'fabricates a retryable build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Retryable + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'failed' + expect(status.icon).to eq 'icon_status_failed' + expect(status.label).to eq 'failed' + expect(status).to have_details + expect(status).to have_action + end + end + + context 'when build is a canceled' do + let(:build) { create(:ci_build, :canceled) } + + it 'fabricates a retryable build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Retryable + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'canceled' + expect(status.icon).to eq 'icon_status_canceled' + expect(status.label).to eq 'canceled' + expect(status).to have_details + expect(status).to have_action + end + end + + context 'when build is running' do + let(:build) { create(:ci_build, :running) } + + it 'fabricates a canceable build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'running' + expect(status.icon).to eq 'icon_status_running' + expect(status.label).to eq 'running' + expect(status).to have_details + expect(status).to have_action + end + end + + context 'when build is pending' do + let(:build) { create(:ci_build, :pending) } + + it 'fabricates a cancelable build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'pending' + expect(status.icon).to eq 'icon_status_pending' + expect(status.label).to eq 'pending' + expect(status).to have_details + expect(status).to have_action + end + end + + context 'when build is skipped' do + let(:build) { create(:ci_build, :skipped) } + + it 'fabricates a core skipped status' do + expect(status).to be_a Gitlab::Ci::Status::Skipped + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'skipped' + expect(status.icon).to eq 'icon_status_skipped' + expect(status.label).to eq 'skipped' + expect(status).to have_details + expect(status).not_to have_action + end + end + + context 'when build is a manual action' do + context 'when build is a play action' do + let(:build) { create(:ci_build, :playable) } + + it 'fabricates a core skipped status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Play + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'manual' + expect(status.icon).to eq 'icon_status_manual' + expect(status.label).to eq 'manual play action' + expect(status).to have_details + expect(status).to have_action + end + end + + context 'when build is an environment stop action' do + let(:build) { create(:ci_build, :playable, :teardown_environment) } + + it 'fabricates a core skipped status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Stop + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'manual' + expect(status.icon).to eq 'icon_status_manual' + expect(status.label).to eq 'manual stop action' + expect(status).to have_details + expect(status).to have_action + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb new file mode 100644 index 00000000000..4ddf04a8e11 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/play_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Play do + let(:status) { double('core') } + let(:user) { double('user') } + + subject { described_class.new(status) } + + describe '#text' do + it { expect(subject.text).to eq 'manual' } + end + + describe '#label' do + it { expect(subject.label).to eq 'manual play action' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_manual' } + end + + describe 'action details' do + let(:user) { create(:user) } + let(:build) { create(:ci_build) } + let(:status) { Gitlab::Ci::Status::Core.new(build, user) } + + describe '#has_action?' do + context 'when user is allowed to update build' do + before { build.project.team << [user, :developer] } + + it { is_expected.to have_action } + end + + context 'when user is not allowed to update build' do + it { is_expected.not_to have_action } + end + end + + describe '#action_path' do + it { expect(subject.action_path).to include "#{build.id}/play" } + end + + describe '#action_icon' do + it { expect(subject.action_icon).to eq 'play' } + end + + describe '#action_title' do + it { expect(subject.action_title).to eq 'Play' } + end + end + + describe '.matches?' do + subject { described_class.matches?(build, user) } + + context 'when build is playable' do + context 'when build stops an environment' do + let(:build) do + create(:ci_build, :playable, :teardown_environment) + end + + it 'does not match' do + expect(subject).to be false + end + end + + context 'when build does not stop an environment' do + let(:build) { create(:ci_build, :playable) } + + it 'is a correct match' do + expect(subject).to be true + end + end + end + + context 'when build is not playable' do + let(:build) { create(:ci_build) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb new file mode 100644 index 00000000000..d61e5bbaa6b --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Retryable do + let(:status) { double('core status') } + let(:user) { double('user') } + + subject do + described_class.new(status) + end + + describe '#text' do + it 'does not override status text' do + expect(status).to receive(:text) + + subject.text + end + end + + describe '#icon' do + it 'does not override status icon' do + expect(status).to receive(:icon) + + subject.icon + end + end + + describe '#label' do + it 'does not override status label' do + expect(status).to receive(:label) + + subject.label + end + end + + describe 'action details' do + let(:user) { create(:user) } + let(:build) { create(:ci_build) } + let(:status) { Gitlab::Ci::Status::Core.new(build, user) } + + describe '#has_action?' do + context 'when user is allowed to update build' do + before { build.project.team << [user, :developer] } + + it { is_expected.to have_action } + end + + context 'when user is not allowed to update build' do + it { is_expected.not_to have_action } + end + end + + describe '#action_path' do + it { expect(subject.action_path).to include "#{build.id}/retry" } + end + + describe '#action_icon' do + it { expect(subject.action_icon).to eq 'refresh' } + end + + describe '#action_title' do + it { expect(subject.action_title).to eq 'Retry' } + end + end + + describe '.matches?' do + subject { described_class.matches?(build, user) } + + context 'when build is retryable' do + let(:build) do + create(:ci_build, :success) + end + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when build is not retryable' do + let(:build) { create(:ci_build, :running) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb new file mode 100644 index 00000000000..59a85b55f90 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb @@ -0,0 +1,84 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Stop do + let(:status) { double('core status') } + let(:user) { double('user') } + + subject do + described_class.new(status) + end + + describe '#text' do + it { expect(subject.text).to eq 'manual' } + end + + describe '#label' do + it { expect(subject.label).to eq 'manual stop action' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_manual' } + end + + describe 'action details' do + let(:user) { create(:user) } + let(:build) { create(:ci_build) } + let(:status) { Gitlab::Ci::Status::Core.new(build, user) } + + describe '#has_action?' do + context 'when user is allowed to update build' do + before { build.project.team << [user, :developer] } + + it { is_expected.to have_action } + end + + context 'when user is not allowed to update build' do + it { is_expected.not_to have_action } + end + end + + describe '#action_path' do + it { expect(subject.action_path).to include "#{build.id}/play" } + end + + describe '#action_icon' do + it { expect(subject.action_icon).to eq 'stop' } + end + + describe '#action_title' do + it { expect(subject.action_title).to eq 'Stop' } + end + end + + describe '.matches?' do + subject { described_class.matches?(build, user) } + + context 'when build is playable' do + context 'when build stops an environment' do + let(:build) do + create(:ci_build, :playable, :teardown_environment) + end + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when build does not stop an environment' do + let(:build) { create(:ci_build, :playable) } + + it 'does not match' do + expect(subject).to be false + end + end + end + + context 'when build is not playable' do + let(:build) { create(:ci_build) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb index 619ecbcba67..4639278ad45 100644 --- a/spec/lib/gitlab/ci/status/canceled_spec.rb +++ b/spec/lib/gitlab/ci/status/canceled_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Canceled do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'canceled' } @@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Canceled do describe '#icon' do it { expect(subject.icon).to eq 'icon_status_canceled' } end - - describe '#title' do - it { expect(subject.title).to eq 'Double: canceled' } - end end diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb index 157302c65a8..2ce176a29d6 100644 --- a/spec/lib/gitlab/ci/status/created_spec.rb +++ b/spec/lib/gitlab/ci/status/created_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Created do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'created' } @@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Created do describe '#icon' do it { expect(subject.icon).to eq 'icon_status_created' } end - - describe '#title' do - it { expect(subject.title).to eq 'Double: created' } - end end diff --git a/spec/lib/gitlab/ci/status/extended_spec.rb b/spec/lib/gitlab/ci/status/extended_spec.rb index 120e121aae5..c2d74ca5cde 100644 --- a/spec/lib/gitlab/ci/status/extended_spec.rb +++ b/spec/lib/gitlab/ci/status/extended_spec.rb @@ -2,11 +2,11 @@ require 'spec_helper' describe Gitlab::Ci::Status::Extended do subject do - Class.new.extend(described_class) + Class.new.include(described_class) end it 'requires subclass to implement matcher' do - expect { subject.matches?(double) } + expect { subject.matches?(double, double) } .to raise_error(NotImplementedError) end end diff --git a/spec/lib/gitlab/ci/status/factory_spec.rb b/spec/lib/gitlab/ci/status/factory_spec.rb index d5bd7f7102b..f92a1c149bf 100644 --- a/spec/lib/gitlab/ci/status/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/factory_spec.rb @@ -2,15 +2,17 @@ require 'spec_helper' describe Gitlab::Ci::Status::Factory do subject do - described_class.new(object) + described_class.new(resource, user) end + let(:user) { create(:user) } + let(:status) { subject.fabricate! } context 'when object has a core status' do HasStatus::AVAILABLE_STATUSES.each do |core_status| context "when core status is #{core_status}" do - let(:object) { double(status: core_status) } + let(:resource) { double(status: core_status) } it "fabricates a core status #{core_status}" do expect(status).to be_a( diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb index 0b3cb8168e6..9d527e6a7ef 100644 --- a/spec/lib/gitlab/ci/status/failed_spec.rb +++ b/spec/lib/gitlab/ci/status/failed_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Failed do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'failed' } @@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Failed do describe '#icon' do it { expect(subject.icon).to eq 'icon_status_failed' } end - - describe '#title' do - it { expect(subject.title).to eq 'Double: failed' } - end end diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb index 57c901c1202..d03f595d3c7 100644 --- a/spec/lib/gitlab/ci/status/pending_spec.rb +++ b/spec/lib/gitlab/ci/status/pending_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Pending do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'pending' } @@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Pending do describe '#icon' do it { expect(subject.icon).to eq 'icon_status_pending' } end - - describe '#title' do - it { expect(subject.title).to eq 'Double: pending' } - end end diff --git a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb index 21adee3f8e7..d665674bf70 100644 --- a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb @@ -1,23 +1,36 @@ require 'spec_helper' describe Gitlab::Ci::Status::Pipeline::Common do - let(:pipeline) { create(:ci_pipeline) } + let(:user) { create(:user) } + let(:project) { create(:empty_project, :private) } + let(:pipeline) { create(:ci_pipeline, project: project) } subject do - Class.new(Gitlab::Ci::Status::Core) - .new(pipeline).extend(described_class) + Gitlab::Ci::Status::Core + .new(pipeline, user) + .extend(described_class) end - it 'does not have action' do - expect(subject).not_to have_action + describe '#has_action?' do + it { is_expected.not_to have_action } end - it 'has details' do - expect(subject).to have_details + describe '#has_details?' do + context 'when user has access to read pipeline' do + before { project.team << [user, :developer] } + + it { is_expected.to have_details } + end + + context 'when user does not have access to read pipeline' do + it { is_expected.not_to have_details } + end end - it 'links to the pipeline details page' do - expect(subject.details_path) - .to include "pipelines/#{pipeline.id}" + describe '#details_path' do + it 'links to the pipeline details page' do + expect(subject.details_path) + .to include "pipelines/#{pipeline.id}" + end end end diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb index d6243940f2e..d4a2dc7fcc1 100644 --- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb @@ -1,14 +1,21 @@ require 'spec_helper' describe Gitlab::Ci::Status::Pipeline::Factory do + let(:user) { create(:user) } + let(:project) { pipeline.project } + subject do - described_class.new(pipeline) + described_class.new(pipeline, user) end let(:status) do subject.fabricate! end + before do + project.team << [user, :developer] + end + context 'when pipeline has a core status' do HasStatus::AVAILABLE_STATUSES.each do |core_status| context "when core status is #{core_status}" do diff --git a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb index 02e526e3de2..7e3383c307f 100644 --- a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb @@ -29,13 +29,13 @@ describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do end it 'is a correct match' do - expect(described_class.matches?(pipeline)).to eq true + expect(described_class.matches?(pipeline, double)).to eq true end end context 'when pipeline does not have warnings' do it 'does not match' do - expect(described_class.matches?(pipeline)).to eq false + expect(described_class.matches?(pipeline, double)).to eq false end end end @@ -51,13 +51,13 @@ describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do end it 'does not match' do - expect(described_class.matches?(pipeline)).to eq false + expect(described_class.matches?(pipeline, double)).to eq false end end context 'when pipeline does not have warnings' do it 'does not match' do - expect(described_class.matches?(pipeline)).to eq false + expect(described_class.matches?(pipeline, double)).to eq false end end end diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb index c023f1872cc..9f47090d396 100644 --- a/spec/lib/gitlab/ci/status/running_spec.rb +++ b/spec/lib/gitlab/ci/status/running_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Running do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'running' } @@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Running do describe '#icon' do it { expect(subject.icon).to eq 'icon_status_running' } end - - describe '#title' do - it { expect(subject.title).to eq 'Double: running' } - end end diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb index d4f7f4b3b70..94601648a8d 100644 --- a/spec/lib/gitlab/ci/status/skipped_spec.rb +++ b/spec/lib/gitlab/ci/status/skipped_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Skipped do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'skipped' } @@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Skipped do describe '#icon' do it { expect(subject.icon).to eq 'icon_status_skipped' } end - - describe '#title' do - it { expect(subject.title).to eq 'Double: skipped' } - end end diff --git a/spec/lib/gitlab/ci/status/stage/common_spec.rb b/spec/lib/gitlab/ci/status/stage/common_spec.rb index f3259c6f23e..8814a7614a0 100644 --- a/spec/lib/gitlab/ci/status/stage/common_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/common_spec.rb @@ -1,26 +1,43 @@ require 'spec_helper' describe Gitlab::Ci::Status::Stage::Common do - let(:pipeline) { create(:ci_empty_pipeline) } - let(:stage) { build(:ci_stage, pipeline: pipeline, name: 'test') } + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:pipeline) { create(:ci_empty_pipeline, project: project) } + + let(:stage) do + build(:ci_stage, pipeline: pipeline, name: 'test') + end subject do Class.new(Gitlab::Ci::Status::Core) - .new(stage).extend(described_class) + .new(stage, user).extend(described_class) end it 'does not have action' do expect(subject).not_to have_action end - it 'has details' do - expect(subject).to have_details - end - it 'links to the pipeline details page' do expect(subject.details_path) .to include "pipelines/#{pipeline.id}" expect(subject.details_path) .to include "##{stage.name}" end + + context 'when user has permission to read pipeline' do + before do + project.team << [user, :master] + end + + it 'has details' do + expect(subject).to have_details + end + end + + context 'when user does not have permission to read pipeline' do + it 'does not have details' do + expect(subject).not_to have_details + end + end end diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb index 17929665c83..6f8721d30c2 100644 --- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb @@ -1,17 +1,26 @@ require 'spec_helper' describe Gitlab::Ci::Status::Stage::Factory do - let(:pipeline) { create(:ci_empty_pipeline) } - let(:stage) { build(:ci_stage, pipeline: pipeline, name: 'test') } + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:pipeline) { create(:ci_empty_pipeline, project: project) } + + let(:stage) do + build(:ci_stage, pipeline: pipeline, name: 'test') + end subject do - described_class.new(stage) + described_class.new(stage, user) end let(:status) do subject.fabricate! end + before do + project.team << [user, :developer] + end + context 'when stage has a core status' do HasStatus::AVAILABLE_STATUSES.each do |core_status| context "when core status is #{core_status}" do diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb index 9e261a3aa5f..90f9f615e0d 100644 --- a/spec/lib/gitlab/ci/status/success_spec.rb +++ b/spec/lib/gitlab/ci/status/success_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Success do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'passed' } @@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Success do describe '#icon' do it { expect(subject.icon).to eq 'icon_status_success' } end - - describe '#title' do - it { expect(subject.title).to eq 'Double: passed' } - end end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 8e1a28f2723..c4ee838b7c9 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -147,6 +147,7 @@ project: - bugzilla_service - gitlab_issue_tracker_service - external_wiki_service +- kubernetes_service - forked_project_link - forked_from_project - forked_project_links diff --git a/spec/lib/gitlab/import_export/avatar_restorer_spec.rb b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb index 5ae178414cc..08a42fd27a2 100644 --- a/spec/lib/gitlab/import_export/avatar_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb @@ -1,12 +1,14 @@ require 'spec_helper' describe Gitlab::ImportExport::AvatarRestorer, lib: true do + include UploadHelpers + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') } let(:project) { create(:empty_project) } before do allow_any_instance_of(described_class).to receive(:avatar_export_file) - .and_return(Rails.root + "spec/fixtures/dk.png") + .and_return(uploaded_image_temp_path) end after do diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb new file mode 100644 index 00000000000..c79c6494576 --- /dev/null +++ b/spec/lib/gitlab/middleware/multipart_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +require 'tempfile' + +describe Gitlab::Middleware::Multipart do + let(:app) { double(:app) } + let(:middleware) { described_class.new(app) } + + it 'opens top-level files' do + Tempfile.open do |tempfile| + env = post_env({ 'file' => tempfile.path }, { 'file.name' => 'filename' }, Gitlab::Workhorse.secret, 'gitlab-workhorse') + + expect(app).to receive(:call) do |env| + file = Rack::Request.new(env).params['file'] + expect(file).to be_a(File) + expect(file.path).to eq(tempfile.path) + end + + middleware.call(env) + end + end + + it 'rejects headers signed with the wrong secret' do + env = post_env({ 'file' => '/var/empty/nonesuch' }, {}, 'x' * 32, 'gitlab-workhorse') + + expect { middleware.call(env) }.to raise_error(JWT::VerificationError) + end + + it 'rejects headers signed with the wrong issuer' do + env = post_env({ 'file' => '/var/empty/nonesuch' }, {}, Gitlab::Workhorse.secret, 'acme-inc') + + expect { middleware.call(env) }.to raise_error(JWT::InvalidIssuerError) + end + + it 'opens files one level deep' do + Tempfile.open do |tempfile| + in_params = { 'user' => { 'avatar' => { '.name' => 'filename' } } } + env = post_env({ 'user[avatar]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse') + + expect(app).to receive(:call) do |env| + file = Rack::Request.new(env).params['user']['avatar'] + expect(file).to be_a(File) + expect(file.path).to eq(tempfile.path) + end + + middleware.call(env) + end + end + + it 'opens files two levels deep' do + Tempfile.open do |tempfile| + in_params = { 'project' => { 'milestone' => { 'themesong' => { '.name' => 'filename' } } } } + env = post_env({ 'project[milestone][themesong]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse') + + expect(app).to receive(:call) do |env| + file = Rack::Request.new(env).params['project']['milestone']['themesong'] + expect(file).to be_a(File) + expect(file.path).to eq(tempfile.path) + end + + middleware.call(env) + end + end + + def post_env(rewritten_fields, params, secret, issuer) + token = JWT.encode({ 'iss' => issuer, 'rewritten_fields' => rewritten_fields }, secret, 'HS256') + Rack::MockRequest.env_for( + '/', + method: 'post', + params: params, + described_class::RACK_ENV_KEY => token + ) + end +end diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 3cd9863ec6a..14ee386dba6 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -149,4 +149,33 @@ describe Gitlab::ProjectSearchResults, lib: true do expect(results.issues_count).to eq 3 end end + + describe 'notes search' do + it 'lists notes' do + project = create(:empty_project, :public) + note = create(:note, project: project) + + results = described_class.new(user, project, note.note) + + expect(results.objects('notes')).to include note + end + + it "doesn't list issue notes when access is restricted" do + project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_issue, project: project) + + results = described_class.new(user, project, note.note) + + expect(results.objects('notes')).not_to include note + end + + it "doesn't list merge_request notes when access is restricted" do + project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_merge_request, project: project) + + results = described_class.new(user, project, note.note) + + expect(results.objects('notes')).not_to include note + end + end end diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index c51b10bdc69..c78cd30157e 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -29,4 +29,20 @@ describe Gitlab::Regex, lib: true do describe 'file path regex' do it { expect('foo@/bar').to match(Gitlab::Regex.file_path_regex) } end + + describe 'environment slug regex' do + def be_matched + match(Gitlab::Regex.environment_slug_regex) + end + + it { expect('foo').to be_matched } + it { expect('foo-1').to be_matched } + + it { expect('FOO').not_to be_matched } + it { expect('foo/1').not_to be_matched } + it { expect('foo.1').not_to be_matched } + it { expect('foo*1').not_to be_matched } + it { expect('9foo').not_to be_matched } + it { expect('foo-').not_to be_matched } + end end diff --git a/spec/lib/gitlab/routing_spec.rb b/spec/lib/gitlab/routing_spec.rb new file mode 100644 index 00000000000..01d5acfc15b --- /dev/null +++ b/spec/lib/gitlab/routing_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::Routing do + context 'when module is included' do + subject do + Class.new.include(described_class).new + end + + it 'makes it possible to access url helpers' do + expect(subject).to respond_to(:namespace_project_path) + end + end + + context 'when module is not included' do + subject do + Class.new.include(described_class.url_helpers).new + end + + it 'exposes url helpers module through a method' do + expect(subject).to respond_to(:namespace_project_path) + end + end +end diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb index 0cdbab87544..849edb09476 100644 --- a/spec/lib/gitlab/sql/union_spec.rb +++ b/spec/lib/gitlab/sql/union_spec.rb @@ -1,16 +1,26 @@ require 'spec_helper' describe Gitlab::SQL::Union, lib: true do + let(:relation_1) { User.where(email: 'alice@example.com').select(:id) } + let(:relation_2) { User.where(email: 'bob@example.com').select(:id) } + + def to_sql(relation) + relation.reorder(nil).to_sql + end + describe '#to_sql' do it 'returns a String joining relations together using a UNION' do - rel1 = User.where(email: 'alice@example.com') - rel2 = User.where(email: 'bob@example.com') - union = described_class.new([rel1, rel2]) + union = described_class.new([relation_1, relation_2]) + + expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}") + end - sql1 = rel1.reorder(nil).to_sql - sql2 = rel2.reorder(nil).to_sql + it 'skips Model.none segements' do + empty_relation = User.none + union = described_class.new([empty_relation, relation_1, relation_2]) - expect(union.to_sql).to eq("#{sql1}\nUNION\n#{sql2}") + expect{User.where("users.id IN (#{union.to_sql})").to_a}.not_to raise_error + expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}") end end end diff --git a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb index 52428547a9f..6a93deb5412 100644 --- a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb +++ b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb @@ -1,3 +1,5 @@ +# encoding: utf-8 + require 'spec_helper' require Rails.root.join('db', 'migrate', '20161124141322_migrate_process_commit_worker_jobs.rb') @@ -59,6 +61,10 @@ describe MigrateProcessCommitWorkerJobs do Sidekiq.redis { |r| r.llen('queue:process_commit') } end + def pop_job + JSON.load(Sidekiq.redis { |r| r.lpop('queue:process_commit') }) + end + before do Sidekiq.redis do |redis| job = JSON.dump(args: [project.id, user.id, commit.oid]) @@ -92,11 +98,28 @@ describe MigrateProcessCommitWorkerJobs do expect(job_count).to eq(1) end + it 'encodes data to UTF-8' do + allow_any_instance_of(Rugged::Repository).to receive(:lookup). + with(commit.oid). + and_return(commit) + + allow(commit).to receive(:message). + and_return('김치'.force_encoding('BINARY')) + + migration.up + + job = pop_job + + # We don't care so much about what is being stored, instead we just want + # to make sure the encoding is right so that JSON encoding the data + # doesn't produce any errors. + expect(job['args'][2]['message'].encoding).to eq(Encoding::UTF_8) + end + context 'a migrated job' do let(:job) do migration.up - - JSON.load(Sidekiq.redis { |r| r.lpop('queue:process_commit') }) + pop_job end let(:commit_hash) do diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index d4970e38f7c..d5f2ffcff59 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -87,6 +87,26 @@ describe Ci::Build, models: true do end end + describe '#persisted_environment' do + before do + @environment = create(:environment, project: project, name: "foo-#{project.default_branch}") + end + + subject { build.persisted_environment } + + context 'referenced literally' do + let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}") } + + it { is_expected.to eq(@environment) } + end + + context 'referenced with a variable' do + let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_BUILD_REF_NAME") } + + it { is_expected.to eq(@environment) } + end + end + describe '#trace' do it { expect(build.trace).to be_nil } @@ -254,6 +274,24 @@ describe Ci::Build, models: true do end end + describe '#ref_slug' do + { + 'master' => 'master', + '1-foo' => '1-foo', + 'fix/1-foo' => 'fix-1-foo', + 'fix-1-foo' => 'fix-1-foo', + 'a' * 63 => 'a' * 63, + 'a' * 64 => 'a' * 63, + 'FOO' => 'foo', + }.each do |ref, slug| + it "transforms #{ref} to #{slug}" do + build.ref = ref + + expect(build.ref_slug).to eq(slug) + end + end + end + describe '#variables' do let(:container_registry_enabled) { false } let(:predefined_variables) do @@ -265,6 +303,7 @@ describe Ci::Build, models: true do { key: 'CI_BUILD_REF', value: build.sha, public: true }, { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true }, { key: 'CI_BUILD_REF_NAME', value: 'master', public: true }, + { key: 'CI_BUILD_REF_SLUG', value: 'master', public: true }, { key: 'CI_BUILD_NAME', value: 'test', public: true }, { key: 'CI_BUILD_STAGE', value: 'test', public: true }, { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, @@ -309,6 +348,22 @@ describe Ci::Build, models: true do it { user_variables.each { |v| is_expected.to include(v) } } end + context 'when build has an environment' do + before do + build.update(environment: 'production') + create(:environment, project: build.project, name: 'production', slug: 'prod-slug') + end + + let(:environment_variables) do + [ + { key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true }, + { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug', public: true } + ] + end + + it { environment_variables.each { |v| is_expected.to include(v) } } + end + context 'when build started manually' do before do build.update_attributes(when: :manual) @@ -899,21 +954,87 @@ describe Ci::Build, models: true do end end + describe '#cancelable?' do + subject { build } + + context 'when build is cancelable' do + context 'when build is pending' do + it { is_expected.to be_cancelable } + end + + context 'when build is running' do + before do + build.run! + end + + it { is_expected.to be_cancelable } + end + end + + context 'when build is not cancelable' do + context 'when build is successful' do + before do + build.success! + end + + it { is_expected.not_to be_cancelable } + end + + context 'when build is failed' do + before do + build.drop! + end + + it { is_expected.not_to be_cancelable } + end + end + end + describe '#retryable?' do - context 'when build is running' do - before do - build.run! + subject { build } + + context 'when build is retryable' do + context 'when build is successful' do + before do + build.success! + end + + it { is_expected.to be_retryable } + end + + context 'when build is failed' do + before do + build.drop! + end + + it { is_expected.to be_retryable } end - it { expect(build).not_to be_retryable } + context 'when build is canceled' do + before do + build.cancel! + end + + it { is_expected.to be_retryable } + end end - context 'when build is finished' do - before do - build.success! + context 'when build is not retryable' do + context 'when build is running' do + before do + build.run! + end + + it { is_expected.not_to be_retryable } end - it { expect(build).to be_retryable } + context 'when build is skipped' do + before do + build.skip! + end + + it { is_expected.not_to be_retryable } + end end end @@ -1180,4 +1301,13 @@ describe Ci::Build, models: true do it { is_expected.to eq('review/master') } end end + + describe '#detailed_status' do + let(:user) { create(:user) } + + it 'returns a detailed status' do + expect(build.detailed_status(user)) + .to be_a Gitlab::Ci::Status::Build::Cancelable + end + end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 8158e71dd55..52dd41065e9 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -381,6 +381,65 @@ describe Ci::Pipeline, models: true do end end + shared_context 'with some outdated pipelines' do + before do + create_pipeline(:canceled, 'ref', 'A') + create_pipeline(:success, 'ref', 'A') + create_pipeline(:failed, 'ref', 'B') + create_pipeline(:skipped, 'feature', 'C') + end + + def create_pipeline(status, ref, sha) + create(:ci_empty_pipeline, status: status, ref: ref, sha: sha) + end + end + + describe '.latest' do + include_context 'with some outdated pipelines' + + context 'when no ref is specified' do + let(:pipelines) { described_class.latest.all } + + it 'returns the latest pipeline for the same ref and different sha' do + expect(pipelines.map(&:sha)).to contain_exactly('A', 'B', 'C') + expect(pipelines.map(&:status)). + to contain_exactly('success', 'failed', 'skipped') + end + end + + context 'when ref is specified' do + let(:pipelines) { described_class.latest('ref').all } + + it 'returns the latest pipeline for ref and different sha' do + expect(pipelines.map(&:sha)).to contain_exactly('A', 'B') + expect(pipelines.map(&:status)). + to contain_exactly('success', 'failed') + end + end + end + + describe '.latest_status' do + include_context 'with some outdated pipelines' + + context 'when no ref is specified' do + let(:latest_status) { described_class.latest_status } + + it 'returns the latest status for the same ref and different sha' do + expect(latest_status).to eq(described_class.latest.status) + expect(latest_status).to eq('failed') + end + end + + context 'when ref is specified' do + let(:latest_status) { described_class.latest_status('ref') } + + it 'returns the latest status for ref and different sha' do + expect(latest_status).to eq(described_class.latest_status('ref')) + expect(latest_status).to eq('failed') + end + end + end + describe '#status' do let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') } @@ -442,11 +501,15 @@ describe Ci::Pipeline, models: true do end describe '#detailed_status' do + let(:user) { create(:user) } + + subject { pipeline.detailed_status(user) } + context 'when pipeline is created' do let(:pipeline) { create(:ci_pipeline, status: :created) } it 'returns detailed status for created pipeline' do - expect(pipeline.detailed_status.text).to eq 'created' + expect(subject.text).to eq 'created' end end @@ -454,7 +517,7 @@ describe Ci::Pipeline, models: true do let(:pipeline) { create(:ci_pipeline, status: :pending) } it 'returns detailed status for pending pipeline' do - expect(pipeline.detailed_status.text).to eq 'pending' + expect(subject.text).to eq 'pending' end end @@ -462,7 +525,7 @@ describe Ci::Pipeline, models: true do let(:pipeline) { create(:ci_pipeline, status: :running) } it 'returns detailed status for running pipeline' do - expect(pipeline.detailed_status.text).to eq 'running' + expect(subject.text).to eq 'running' end end @@ -470,7 +533,7 @@ describe Ci::Pipeline, models: true do let(:pipeline) { create(:ci_pipeline, status: :success) } it 'returns detailed status for successful pipeline' do - expect(pipeline.detailed_status.text).to eq 'passed' + expect(subject.text).to eq 'passed' end end @@ -478,7 +541,7 @@ describe Ci::Pipeline, models: true do let(:pipeline) { create(:ci_pipeline, status: :failed) } it 'returns detailed status for failed pipeline' do - expect(pipeline.detailed_status.text).to eq 'failed' + expect(subject.text).to eq 'failed' end end @@ -486,7 +549,7 @@ describe Ci::Pipeline, models: true do let(:pipeline) { create(:ci_pipeline, status: :canceled) } it 'returns detailed status for canceled pipeline' do - expect(pipeline.detailed_status.text).to eq 'canceled' + expect(subject.text).to eq 'canceled' end end @@ -494,7 +557,7 @@ describe Ci::Pipeline, models: true do let(:pipeline) { create(:ci_pipeline, status: :skipped) } it 'returns detailed status for skipped pipeline' do - expect(pipeline.detailed_status.text).to eq 'skipped' + expect(subject.text).to eq 'skipped' end end @@ -506,7 +569,7 @@ describe Ci::Pipeline, models: true do end it 'retruns detailed status for successful pipeline with warnings' do - expect(pipeline.detailed_status.label).to eq 'passed with warnings' + expect(subject.label).to eq 'passed with warnings' end end end diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index f232761dba2..8fff38f7cda 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -68,7 +68,9 @@ describe Ci::Stage, models: true do end describe '#detailed_status' do - subject { stage.detailed_status } + let(:user) { create(:user) } + + subject { stage.detailed_status(user) } context 'when build is created' do let!(:stage_build) { create_job(:ci_build, status: :created) } diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 0935fc0561c..74b50d2908d 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -213,23 +213,19 @@ eos end describe '#status' do - context 'without arguments for compound status' do - shared_examples 'giving the status from pipeline' do - it do - expect(commit.status).to eq(Ci::Pipeline.status) - end - end - - context 'with pipelines' do - let!(:pipeline) do - create(:ci_empty_pipeline, project: project, sha: commit.sha) + context 'without ref argument' do + before do + %w[success failed created pending].each do |status| + create(:ci_empty_pipeline, + project: project, + sha: commit.sha, + status: status) end - - it_behaves_like 'giving the status from pipeline' end - context 'without pipelines' do - it_behaves_like 'giving the status from pipeline' + it 'gives compound status from latest pipelines' do + expect(commit.status).to eq(Ci::Pipeline.latest_status) + expect(commit.status).to eq('pending') end end @@ -255,8 +251,9 @@ eos expect(commit.status('fix')).to eq(pipeline_from_fix.status) end - it 'gives compound status if ref is nil' do - expect(commit.status(nil)).to eq(commit.status) + it 'gives compound status from latest pipelines if ref is nil' do + expect(commit.status(nil)).to eq(Ci::Pipeline.latest_status) + expect(commit.status(nil)).to eq('failed') end end end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 1ec08c2a9d0..701f3323c0f 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -234,4 +234,13 @@ describe CommitStatus, models: true do end end end + + describe '#detailed_status' do + let(:user) { create(:user) } + + it 'returns a detailed status' do + expect(commit_status.detailed_status(user)) + .to be_a Gitlab::Ci::Status::Success + end + end end diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb index 0acefc0c1d5..b556135532f 100644 --- a/spec/models/concerns/routable_spec.rb +++ b/spec/models/concerns/routable_spec.rb @@ -3,6 +3,10 @@ require 'spec_helper' describe Group, 'Routable' do let!(:group) { create(:group) } + describe 'Validations' do + it { is_expected.to validate_presence_of(:route) } + end + describe 'Associations' do it { is_expected.to have_one(:route).dependent(:destroy) } end @@ -35,16 +39,16 @@ describe Group, 'Routable' do it { expect(described_class.find_by_full_path('unknown')).to eq(nil) } end - describe '.where_paths_in' do + describe '.where_full_path_in' do context 'without any paths' do it 'returns an empty relation' do - expect(described_class.where_paths_in([])).to eq([]) + expect(described_class.where_full_path_in([])).to eq([]) end end context 'without any valid paths' do it 'returns an empty relation' do - expect(described_class.where_paths_in(%w[unknown])).to eq([]) + expect(described_class.where_full_path_in(%w[unknown])).to eq([]) end end @@ -52,13 +56,13 @@ describe Group, 'Routable' do let!(:nested_group) { create(:group, parent: group) } it 'returns the projects matching the paths' do - result = described_class.where_paths_in([group.to_param, nested_group.to_param]) + result = described_class.where_full_path_in([group.to_param, nested_group.to_param]) expect(result).to contain_exactly(group, nested_group) end it 'returns projects regardless of the casing of paths' do - result = described_class.where_paths_in([group.to_param.upcase, nested_group.to_param.upcase]) + result = described_class.where_full_path_in([group.to_param.upcase, nested_group.to_param.upcase]) expect(result).to contain_exactly(group, nested_group) end diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb index eb64f3d0c83..4b0bfa43abf 100644 --- a/spec/models/concerns/token_authenticatable_spec.rb +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -6,6 +6,7 @@ shared_examples 'TokenAuthenticatable' do it { expect(described_class).to be_private_method_defined(:write_new_token) } it { expect(described_class).to respond_to("find_by_#{token_field}") } it { is_expected.to respond_to("ensure_#{token_field}") } + it { is_expected.to respond_to("set_#{token_field}") } it { is_expected.to respond_to("reset_#{token_field}!") } end end @@ -55,6 +56,12 @@ describe ApplicationSetting, 'TokenAuthenticatable' do end end + describe 'setting new token' do + subject { described_class.new.send("set_#{token_field}", '0123456789') } + + it { is_expected.to eq '0123456789' } + end + describe 'multiple token fields' do before do described_class.send(:add_authentication_token_field, :yet_another_token) diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index c8170164898..97cbb093ed2 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Environment, models: true do - let(:environment) { create(:environment) } + subject(:environment) { create(:environment) } it { is_expected.to belong_to(:project) } it { is_expected.to have_many(:deployments) } @@ -15,15 +15,11 @@ describe Environment, models: true do it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) } it { is_expected.to validate_length_of(:name).is_at_most(255) } - it { is_expected.to validate_length_of(:external_url).is_at_most(255) } - - # To circumvent a not null violation of the name column: - # https://github.com/thoughtbot/shoulda-matchers/issues/336 - it 'validates uniqueness of :external_url' do - create(:environment) + it { is_expected.to validate_uniqueness_of(:slug).scoped_to(:project_id) } + it { is_expected.to validate_length_of(:slug).is_at_most(24) } - is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) - end + it { is_expected.to validate_length_of(:external_url).is_at_most(255) } + it { is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) } describe '#nullify_external_url' do it 'replaces a blank url with nil' do @@ -199,4 +195,38 @@ describe Environment, models: true do expect(environment.actions_for('review/master')).to contain_exactly(review_action) end end + + describe '#slug' do + it "is automatically generated" do + expect(environment.slug).not_to be_nil + end + + it "is not regenerated if name changes" do + original_slug = environment.slug + environment.update_attributes!(name: environment.name.reverse) + + expect(environment.slug).to eq(original_slug) + end + end + + describe '#generate_slug' do + SUFFIX = "-[a-z0-9]{6}" + { + "staging-12345678901234567" => "staging-123456789" + SUFFIX, + "9-staging-123456789012345" => "env-9-staging-123" + SUFFIX, + "staging-1234567890123456" => "staging-1234567890123456", + "production" => "production", + "PRODUCTION" => "production" + SUFFIX, + "review/1-foo" => "review-1-foo" + SUFFIX, + "1-foo" => "env-1-foo" + SUFFIX, + "1/foo" => "env-1-foo" + SUFFIX, + "foo-" => "foo" + SUFFIX, + }.each do |name, matcher| + it "returns a slug matching #{matcher}, given #{name}" do + slug = described_class.new(name: name).generate_slug + + expect(slug).to match(/\A#{matcher}\z/) + end + end + end end diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb index 615cfe3142b..6004bfdb7b7 100644 --- a/spec/models/generic_commit_status_spec.rb +++ b/spec/models/generic_commit_status_spec.rb @@ -1,8 +1,11 @@ require 'spec_helper' describe GenericCommitStatus, models: true do - let(:pipeline) { FactoryGirl.create :ci_pipeline } - let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, pipeline: pipeline } + let(:pipeline) { create(:ci_pipeline) } + + let(:generic_commit_status) do + create(:generic_commit_status, pipeline: pipeline) + end describe '#context' do subject { generic_commit_status.context } @@ -17,6 +20,15 @@ describe GenericCommitStatus, models: true do it { is_expected.to eq([:external]) } end + describe '#detailed_status' do + let(:user) { create(:user) } + + it 'returns detailed status object' do + expect(generic_commit_status.detailed_status(user)) + .to be_a Gitlab::Ci::Status::Success + end + end + describe 'set_default_values' do before do generic_commit_status.context = nil diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 1613a586a2c..7d5ecfbaa64 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -50,9 +50,8 @@ describe Group, models: true do describe 'validations' do it { is_expected.to validate_presence_of :name } - it { is_expected.to validate_uniqueness_of(:name) } + it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) } it { is_expected.to validate_presence_of :path } - it { is_expected.to validate_uniqueness_of(:path) } it { is_expected.not_to validate_presence_of :owner } end @@ -271,4 +270,11 @@ describe Group, models: true do expect(group.web_url).to include("groups/#{group.name}") end end + + describe 'nested group' do + subject { build(:group, :nested) } + + it { is_expected.to be_valid } + it { expect(subject.parent).to be_kind_of(Group) } + end end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 24e216329a9..bb56e44db29 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -22,26 +22,6 @@ describe Issue, models: true do it { is_expected.to have_db_index(:deleted_at) } end - describe '.visible_to_user' do - let(:user) { create(:user) } - let(:authorized_user) { create(:user) } - let(:project) { create(:project, namespace: authorized_user.namespace) } - let!(:public_issue) { create(:issue, project: project) } - let!(:confidential_issue) { create(:issue, project: project, confidential: true) } - - it 'returns non confidential issues for nil user' do - expect(Issue.visible_to_user(nil).count).to be(1) - end - - it 'returns non confidential issues for user not authorized for the issues projects' do - expect(Issue.visible_to_user(user).count).to be(1) - end - - it 'returns all issues for user authorized for the issues projects' do - expect(Issue.visible_to_user(authorized_user).count).to be(2) - end - end - describe '#to_reference' do let(:project) { build(:empty_project, name: 'sample-project') } let(:issue) { build(:issue, iid: 1, project: project) } diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 8b730be91fd..1b71d00eb8f 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -205,7 +205,7 @@ describe MergeRequest, models: true do end end - describe "#mr_and_commit_notes" do + describe "#related_notes" do let!(:merge_request) { create(:merge_request) } before do @@ -217,7 +217,7 @@ describe MergeRequest, models: true do it "includes notes for commits" do expect(merge_request.commits).not_to be_empty - expect(merge_request.mr_and_commit_notes.count).to eq(2) + expect(merge_request.related_notes.count).to eq(2) end it "includes notes for commits from target project as well" do @@ -225,7 +225,7 @@ describe MergeRequest, models: true do project: merge_request.target_project) expect(merge_request.commits).not_to be_empty - expect(merge_request.mr_and_commit_notes.count).to eq(3) + expect(merge_request.related_notes.count).to eq(3) end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 0cc2efae5f9..064f29d2d66 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -24,8 +24,9 @@ describe Milestone, models: true do it { is_expected.to have_many(:issues) } end - let(:milestone) { create(:milestone) } - let(:issue) { create(:issue) } + let(:project) { create(:project, :public) } + let(:milestone) { create(:milestone, project: project) } + let(:issue) { create(:issue, project: project) } let(:user) { create(:user) } describe "#title" do @@ -110,8 +111,8 @@ describe Milestone, models: true do describe :items_count do before do - milestone.issues << create(:issue) - milestone.issues << create(:closed_issue) + milestone.issues << create(:issue, project: project) + milestone.issues << create(:closed_issue, project: project) milestone.merge_requests << create(:merge_request) end @@ -126,7 +127,7 @@ describe Milestone, models: true do describe '#total_items_count' do before do - create :closed_issue, milestone: milestone + create :closed_issue, milestone: milestone, project: project create :merge_request, milestone: milestone end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 7f82e85563b..9fd06bb6b23 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -6,20 +6,16 @@ describe Namespace, models: true do it { is_expected.to have_many :projects } it { is_expected.to validate_presence_of(:name) } - it { is_expected.to validate_uniqueness_of(:name) } + it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) } it { is_expected.to validate_length_of(:name).is_at_most(255) } it { is_expected.to validate_length_of(:description).is_at_most(255) } it { is_expected.to validate_presence_of(:path) } - it { is_expected.to validate_uniqueness_of(:path) } it { is_expected.to validate_length_of(:path).is_at_most(255) } it { is_expected.to validate_presence_of(:owner) } - describe "Mass assignment" do - end - describe "Respond to" do it { is_expected.to respond_to(:human_name) } it { is_expected.to respond_to(:to_param) } @@ -132,4 +128,26 @@ describe Namespace, models: true do it { expect(group.full_path).to eq(group.path) } it { expect(nested_group.full_path).to eq("#{group.path}/#{nested_group.path}") } end + + describe '#full_name' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + + it { expect(group.full_name).to eq(group.name) } + it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") } + end + + describe '#parents' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + let(:deep_nested_group) { create(:group, parent: nested_group) } + let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + + it 'returns the correct parents' do + expect(very_deep_nested_group.parents).to eq([group, nested_group, deep_nested_group]) + expect(deep_nested_group.parents).to eq([group, nested_group]) + expect(nested_group.parents).to eq([group]) + expect(group.parents).to eq([]) + end + end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 17a15b12dcb..310fecd8a5c 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -162,44 +162,6 @@ describe Note, models: true do end end - describe '.search' do - let(:note) { create(:note_on_issue, note: 'WoW') } - - it 'returns notes with matching content' do - expect(described_class.search(note.note)).to eq([note]) - end - - it 'returns notes with matching content regardless of the casing' do - expect(described_class.search('WOW')).to eq([note]) - end - - context "confidential issues" do - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } - let(:confidential_note) { create(:note, note: "Random", noteable: confidential_issue, project: confidential_issue.project) } - - it "returns notes with matching content if user can see the issue" do - expect(described_class.search(confidential_note.note, as_user: user)).to eq([confidential_note]) - end - - it "does not return notes with matching content if user can not see the issue" do - user = create(:user) - expect(described_class.search(confidential_note.note, as_user: user)).to be_empty - end - - it "does not return notes with matching content for project members with guest role" do - user = create(:user) - project.team << [user, :guest] - expect(described_class.search(confidential_note.note, as_user: user)).to be_empty - end - - it "does not return notes with matching content for unauthenticated users" do - expect(described_class.search(confidential_note.note)).to be_empty - end - end - end - describe "editable?" do it "returns true" do note = build(:note) diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb new file mode 100644 index 00000000000..ffb92012b89 --- /dev/null +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -0,0 +1,126 @@ +require 'spec_helper' + +describe KubernetesService, models: true do + let(:project) { create(:empty_project) } + + describe "Associations" do + it { is_expected.to belong_to :project } + end + + describe 'Validations' do + context 'when service is active' do + before { subject.active = true } + it { is_expected.to validate_presence_of(:namespace) } + it { is_expected.to validate_presence_of(:api_url) } + it { is_expected.to validate_presence_of(:token) } + + context 'namespace format' do + before do + subject.project = project + subject.api_url = "http://example.com" + subject.token = "test" + end + + { + 'foo' => true, + '1foo' => true, + 'foo1' => true, + 'foo-bar' => true, + '-foo' => false, + 'foo-' => false, + 'a' * 63 => true, + 'a' * 64 => false, + 'a.b' => false, + 'a*b' => false, + }.each do |namespace, validity| + it "should validate #{namespace} as #{validity ? 'valid' : 'invalid'}" do + subject.namespace = namespace + + expect(subject.valid?).to eq(validity) + end + end + end + end + + context 'when service is inactive' do + before { subject.active = false } + it { is_expected.not_to validate_presence_of(:namespace) } + it { is_expected.not_to validate_presence_of(:api_url) } + it { is_expected.not_to validate_presence_of(:token) } + end + end + + describe '#initialize_properties' do + context 'with a project' do + it 'defaults to the project name' do + expect(described_class.new(project: project).namespace).to eq(project.name) + end + end + + context 'without a project' do + it 'leaves the namespace unset' do + expect(described_class.new.namespace).to be_nil + end + end + end + + describe '#test' do + let(:project) { create(:kubernetes_project) } + let(:service) { project.kubernetes_service } + let(:discovery_url) { service.api_url + '/api/v1' } + + # JSON response body from Kubernetes GET /api/v1 request + let(:discovery_response) { { "kind" => "APIResourceList", "groupVersion" => "v1", "resources" => [] }.to_json } + + context 'with path prefix in api_url' do + let(:discovery_url) { 'https://kubernetes.example.com/prefix/api/v1' } + + before do + service.api_url = 'https://kubernetes.example.com/prefix/' + end + + it 'tests with the prefix' do + WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response) + + expect(service.test[:success]).to be_truthy + expect(WebMock).to have_requested(:get, discovery_url).once + end + end + + context 'with custom CA certificate' do + let(:certificate) { "CA PEM DATA" } + before do + service.update_attributes!(ca_pem: certificate) + end + + it 'is added to the certificate store' do + cert = double("certificate") + + expect(OpenSSL::X509::Certificate).to receive(:new).with(certificate).and_return(cert) + expect_any_instance_of(OpenSSL::X509::Store).to receive(:add_cert).with(cert) + WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response) + + expect(service.test[:success]).to be_truthy + expect(WebMock).to have_requested(:get, discovery_url).once + end + end + + context 'success' do + it 'reads the discovery endpoint' do + WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response) + + expect(service.test[:success]).to be_truthy + expect(WebMock).to have_requested(:get, discovery_url).once + end + end + + context 'failure' do + it 'fails to read the discovery endpoint' do + WebMock.stub_request(:get, discovery_url).to_return(status: 404) + + expect(service.test[:success]).to be_falsy + expect(WebMock).to have_requested(:get, discovery_url).once + end + end + end +end diff --git a/spec/models/project_services/slack_service/pipeline_message_spec.rb b/spec/models/project_services/slack_service/pipeline_message_spec.rb index 363138a9454..4098500122f 100644 --- a/spec/models/project_services/slack_service/pipeline_message_spec.rb +++ b/spec/models/project_services/slack_service/pipeline_message_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe SlackService::PipelineMessage do subject { SlackService::PipelineMessage.new(args) } + let(:user) { { name: 'hacker' } } let(:args) do { @@ -15,7 +16,7 @@ describe SlackService::PipelineMessage do }, project: { path_with_namespace: 'project_name', web_url: 'example.gitlab.com' }, - user: { name: 'hacker' } + user: user } end @@ -28,9 +29,7 @@ describe SlackService::PipelineMessage do let(:message) { build_message('passed') } it 'returns a message with information about succeeded build' do - expect(subject.pretext).to be_empty - expect(subject.fallback).to eq(message) - expect(subject.attachments).to eq([text: message, color: color]) + verify_message end end @@ -40,16 +39,29 @@ describe SlackService::PipelineMessage do let(:duration) { 10 } it 'returns a message with information about failed build' do - expect(subject.pretext).to be_empty - expect(subject.fallback).to eq(message) - expect(subject.attachments).to eq([text: message, color: color]) + verify_message end + + context 'when triggered by API therefore lacking user' do + let(:user) { nil } + let(:message) { build_message(status, 'API') } + + it 'returns a message stating it is by API' do + verify_message + end + end + end + + def verify_message + expect(subject.pretext).to be_empty + expect(subject.fallback).to eq(message) + expect(subject.attachments).to eq([text: message, color: color]) end - def build_message(status_text = status) + def build_message(status_text = status, name = user[:name]) "<example.gitlab.com|project_name>:" \ " Pipeline <example.gitlab.com/pipelines/123|#123>" \ " of <example.gitlab.com/commits/develop|develop> branch" \ - " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" + " by #{name} #{status_text} in #{duration} #{'second'.pluralize(duration)}" end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index bad6ed9e146..8b20ee81614 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -727,17 +727,6 @@ describe User, models: true do end end - describe 'by_username_or_id' do - let(:user1) { create(:user, username: 'foo') } - - it "gets the correct user" do - expect(User.by_username_or_id(user1.id)).to eq(user1) - expect(User.by_username_or_id('foo')).to eq(user1) - expect(User.by_username_or_id(-1)).to be_nil - expect(User.by_username_or_id('bar')).to be_nil - end - end - describe '.find_by_ssh_key_id' do context 'using an existing SSH key ID' do let(:user) { create(:user) } diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 55b8c8c0c69..2878e0cb59b 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -11,6 +11,7 @@ describe API::Branches, api: true do let!(:guest) { create(:project_member, :guest, user: user2, project: project) } let!(:branch_name) { 'feature' } let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } + let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") } describe "GET /projects/:id/repository/branches" do it "returns an array of project branches" do @@ -37,6 +38,13 @@ describe API::Branches, api: true do expect(json_response['developers_can_merge']).to eq(false) end + it "returns the branch information for a single branch with dots in the name" do + get api("/projects/#{project.id}/repository/branches/with.1.2.3", user) + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq("with.1.2.3") + end + context 'on a merged branch' do it "returns the branch information for a single branch" do get api("/projects/#{project.id}/repository/branches/merge-test", user) @@ -71,6 +79,14 @@ describe API::Branches, api: true do expect(json_response['developers_can_merge']).to eq(false) end + it "protects a single branch with dots in the name" do + put api("/projects/#{project.id}/repository/branches/with.1.2.3/protect", user) + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq("with.1.2.3") + expect(json_response['protected']).to eq(true) + end + it 'protects a single branch and developers can push' do put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_push: true @@ -220,6 +236,14 @@ describe API::Branches, api: true do expect(json_response['protected']).to eq(false) end + it "update branches with dots in branch name" do + put api("/projects/#{project.id}/repository/branches/with.1.2.3/unprotect", user) + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq("with.1.2.3") + expect(json_response['protected']).to eq(false) + end + it "returns success when unprotect branch" do put api("/projects/#{project.id}/repository/branches/unknown/unprotect", user) expect(response).to have_http_status(404) @@ -292,6 +316,13 @@ describe API::Branches, api: true do expect(json_response['branch_name']).to eq(branch_name) end + it "removes a branch with dots in the branch name" do + delete api("/projects/#{project.id}/repository/branches/with.1.2.3", user) + + expect(response).to have_http_status(200) + expect(json_response['branch_name']).to eq("with.1.2.3") + end + it 'returns 404 if branch not exists' do delete api("/projects/#{project.id}/repository/branches/foobar", user) expect(response).to have_http_status(404) diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index e497bce6943..964cded917c 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -456,6 +456,76 @@ describe API::Commits, api: true do end end + describe 'POST :id/repository/commits/:sha/cherry_pick' do + let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') } + + context 'authorized user' do + it 'cherry picks a commit' do + post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'master' + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq(master_pickable_commit.title) + expect(json_response['message']).to eq(master_pickable_commit.message) + expect(json_response['author_name']).to eq(master_pickable_commit.author_name) + expect(json_response['committer_name']).to eq(user.name) + end + + it 'returns 400 if commit is already included in the target branch' do + post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'markdown' + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq('Sorry, we cannot cherry-pick this commit automatically. + A cherry-pick may have already been performed with this commit, or a more recent commit may have updated some of its content.') + end + + it 'returns 400 if you are not allowed to push to the target branch' do + project.team << [user2, :developer] + protected_branch = create(:protected_branch, project: project, name: 'feature') + + post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user2), branch: protected_branch.name + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq('You are not allowed to push into this branch') + end + + it 'returns 400 for missing parameters' do + post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user) + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('branch is missing') + end + + it 'returns 404 if commit is not found' do + post api("/projects/#{project.id}/repository/commits/abcd0123/cherry_pick", user), branch: 'master' + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Commit Not Found') + end + + it 'returns 404 if branch is not found' do + post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'foo' + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Branch Not Found') + end + + it 'returns 400 for missing parameters' do + post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user) + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('branch is missing') + end + end + + context 'unauthorized user' do + it 'does not cherry pick the commit' do + post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick"), branch: 'master' + + expect(response).to have_http_status(401) + end + end + end + describe 'Post comment to commit' do context 'authorized user' do it 'returns comment' do diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 126496c43a5..b9d535bc314 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -46,6 +46,7 @@ describe API::Environments, api: true do expect(response).to have_http_status(201) expect(json_response['name']).to eq('mepmep') + expect(json_response['slug']).to eq('mepmep') expect(json_response['external']).to be nil end @@ -60,6 +61,13 @@ describe API::Environments, api: true do expect(response).to have_http_status(400) end + + it 'returns a 400 if slug is specified' do + post api("/projects/#{project.id}/environments", user), name: "foo", slug: "foo" + + expect(response).to have_http_status(400) + expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed") + end end context 'a non member' do @@ -86,6 +94,15 @@ describe API::Environments, api: true do expect(json_response['external_url']).to eq(url) end + it "won't allow slug to be changed" do + slug = environment.slug + api_url = api("/projects/#{project.id}/environments/#{environment.id}", user) + put api_url, slug: slug + "-foo" + + expect(response).to have_http_status(400) + expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed") + end + it "won't update the external_url if only the name is passed" do url = environment.external_url put api("/projects/#{project.id}/environments/#{environment.id}", user), diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 548ed8e1892..cdeb965b413 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -2,13 +2,13 @@ require 'spec_helper' describe API::Groups, api: true do include ApiHelpers + include UploadHelpers let(:user1) { create(:user, can_create_group: false) } let(:user2) { create(:user) } let(:user3) { create(:user) } let(:admin) { create(:admin) } - let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') } - let!(:group1) { create(:group, avatar: File.open(avatar_file_path)) } + let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) } let!(:group2) { create(:group, :private) } let!(:project1) { create(:project, namespace: group1) } let!(:project2) { create(:project, namespace: group2) } @@ -243,6 +243,28 @@ describe API::Groups, api: true do expect(json_response.length).to eq(2) project_names = json_response.map { |proj| proj['name' ] } expect(project_names).to match_array([project1.name, project3.name]) + expect(json_response.first['default_branch']).to be_present + end + + it "returns the group's projects with simple representation" do + get api("/groups/#{group1.id}/projects", user1), simple: true + + expect(response).to have_http_status(200) + expect(json_response.length).to eq(2) + project_names = json_response.map { |proj| proj['name' ] } + expect(project_names).to match_array([project1.name, project3.name]) + expect(json_response.first['default_branch']).not_to be_present + end + + it 'filters the groups projects' do + public_project = create(:project, :public, path: 'test1', group: group1) + + get api("/groups/#{group1.id}/projects", user1), visibility: 'public' + + expect(response).to have_http_status(200) + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(1) + expect(json_response.first['name']).to eq(public_project.name) end it "does not return a non existing group" do diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 3f34309f419..4035fd97af5 100644 --- a/spec/requests/api/api_helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -2,7 +2,6 @@ require 'spec_helper' describe API::Helpers, api: true do include API::Helpers - include ApiHelpers include SentryHelper let(:user) { create(:user) } @@ -13,18 +12,18 @@ describe API::Helpers, api: true do let(:env) { { 'REQUEST_METHOD' => 'GET' } } let(:request) { Rack::Request.new(env) } - def set_env(token_usr, identifier) + def set_env(user_or_token, identifier) clear_env clear_param - env[API::Helpers::PRIVATE_TOKEN_HEADER] = token_usr.private_token - env[API::Helpers::SUDO_HEADER] = identifier + env[API::Helpers::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token + env[API::Helpers::SUDO_HEADER] = identifier.to_s end - def set_param(token_usr, identifier) + def set_param(user_or_token, identifier) clear_env clear_param - params[API::Helpers::PRIVATE_TOKEN_PARAM] = token_usr.private_token - params[API::Helpers::SUDO_PARAM] = identifier + params[API::Helpers::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token + params[API::Helpers::SUDO_PARAM] = identifier.to_s end def clear_env @@ -163,6 +162,13 @@ describe API::Helpers, api: true do expect(current_user).to eq(user) end + it 'memoize the current_user: sudo permissions are not run against the sudoed user' do + set_env(admin, user.id) + + expect(current_user).to eq(user) + expect(current_user).to eq(user) + end + it 'handles sudo to oneself' do set_env(admin, admin.id) @@ -294,33 +300,48 @@ describe API::Helpers, api: true do end end - describe '.sudo_identifier' do - it "returns integers when input is an int" do - set_env(admin, '123') - expect(sudo_identifier).to eq(123) - set_env(admin, '0001234567890') - expect(sudo_identifier).to eq(1234567890) - - set_param(admin, '123') - expect(sudo_identifier).to eq(123) - set_param(admin, '0001234567890') - expect(sudo_identifier).to eq(1234567890) + describe '.sudo?' do + context 'when no sudo env or param is passed' do + before do + doorkeeper_guard_returns(nil) + end + + it 'returns false' do + expect(sudo?).to be_falsy + end end - it "returns string when input is an is not an int" do - set_env(admin, '12.30') - expect(sudo_identifier).to eq("12.30") - set_env(admin, 'hello') - expect(sudo_identifier).to eq('hello') - set_env(admin, ' 123') - expect(sudo_identifier).to eq(' 123') - - set_param(admin, '12.30') - expect(sudo_identifier).to eq("12.30") - set_param(admin, 'hello') - expect(sudo_identifier).to eq('hello') - set_param(admin, ' 123') - expect(sudo_identifier).to eq(' 123') + context 'when sudo env or param is passed', 'user is not an admin' do + before do + set_env(user, '123') + end + + it 'returns an 403 Forbidden' do + expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Must be admin to use sudo"}' + end + end + + context 'when sudo env or param is passed', 'user is admin' do + context 'personal access token is used' do + before do + personal_access_token = create(:personal_access_token, user: admin) + set_env(personal_access_token.token, user.id) + end + + it 'returns an 403 Forbidden' do + expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Private token must be specified in order to use sudo"}' + end + end + + context 'private access token is used' do + before do + set_env(admin.private_token, user.id) + end + + it 'returns true' do + expect(sudo?).to be_truthy + end + end end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 75b270aa93c..f032d1b683d 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -533,6 +533,22 @@ describe API::MergeRequests, api: true do expect(json_response['labels']).to include '?' expect(json_response['labels']).to include '&' end + + it 'does not update state when title is empty' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', title: nil + + merge_request.reload + expect(response).to have_http_status(400) + expect(merge_request.state).to eq('opened') + end + + it 'does not update state when target_branch is empty' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', target_branch: nil + + merge_request.reload + expect(response).to have_http_status(400) + expect(merge_request.state).to eq('opened') + end end describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index d30361f53d4..668e39f9dba 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -2,6 +2,7 @@ require "spec_helper" describe API::Services, api: true do include ApiHelpers + let(:user) { create(:user) } let(:admin) { create(:admin) } let(:user2) { create(:user) } @@ -98,7 +99,7 @@ describe API::Services, api: true do post api("/projects/#{project.id}/services/idonotexist/trigger") expect(response).to have_http_status(404) - expect(json_response["message"]).to eq("404 Service Not Found") + expect(json_response["error"]).to eq("404 Not Found") end end @@ -114,7 +115,7 @@ describe API::Services, api: true do end it 'when the service is inactive' do - post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger") + post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger"), params expect(response).to have_http_status(404) end diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb new file mode 100644 index 00000000000..f6fb6ea5506 --- /dev/null +++ b/spec/requests/api/snippets_spec.rb @@ -0,0 +1,157 @@ +require 'rails_helper' + +describe API::Snippets, api: true do + include ApiHelpers + let!(:user) { create(:user) } + + describe 'GET /snippets/' do + it 'returns snippets available' do + public_snippet = create(:personal_snippet, :public, author: user) + private_snippet = create(:personal_snippet, :private, author: user) + internal_snippet = create(:personal_snippet, :internal, author: user) + + get api("/snippets/", user) + + expect(response).to have_http_status(200) + expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly( + public_snippet.id, + internal_snippet.id, + private_snippet.id) + expect(json_response.last).to have_key('web_url') + expect(json_response.last).to have_key('raw_url') + end + + it 'hides private snippets from regular user' do + create(:personal_snippet, :private) + + get api("/snippets/", user) + expect(response).to have_http_status(200) + expect(json_response.size).to eq(0) + end + end + + describe 'GET /snippets/public' do + let!(:other_user) { create(:user) } + let!(:public_snippet) { create(:personal_snippet, :public, author: user) } + let!(:private_snippet) { create(:personal_snippet, :private, author: user) } + let!(:internal_snippet) { create(:personal_snippet, :internal, author: user) } + let!(:public_snippet_other) { create(:personal_snippet, :public, author: other_user) } + let!(:private_snippet_other) { create(:personal_snippet, :private, author: other_user) } + let!(:internal_snippet_other) { create(:personal_snippet, :internal, author: other_user) } + + it 'returns all snippets with public visibility from all users' do + get api("/snippets/public", user) + + expect(response).to have_http_status(200) + expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly( + public_snippet.id, + public_snippet_other.id) + expect(json_response.map{ |snippet| snippet['web_url']} ).to include( + "http://localhost/snippets/#{public_snippet.id}", + "http://localhost/snippets/#{public_snippet_other.id}") + expect(json_response.map{ |snippet| snippet['raw_url']} ).to include( + "http://localhost/snippets/#{public_snippet.id}/raw", + "http://localhost/snippets/#{public_snippet_other.id}/raw") + end + end + + describe 'GET /snippets/:id/raw' do + let(:snippet) { create(:personal_snippet, author: user) } + + it 'returns raw text' do + get api("/snippets/#{snippet.id}/raw", user) + + expect(response).to have_http_status(200) + expect(response.content_type).to eq 'text/plain' + expect(response.body).to eq(snippet.content) + end + + it 'returns 404 for invalid snippet id' do + delete api("/snippets/1234", user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Snippet Not Found') + end + end + + describe 'POST /snippets/' do + let(:params) do + { + title: 'Test Title', + file_name: 'test.rb', + content: 'puts "hello world"', + visibility_level: Gitlab::VisibilityLevel::PUBLIC + } + end + + it 'creates a new snippet' do + expect do + post api("/snippets/", user), params + end.to change { PersonalSnippet.count }.by(1) + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq(params[:title]) + expect(json_response['file_name']).to eq(params[:file_name]) + end + + it 'returns 400 for missing parameters' do + params.delete(:title) + + post api("/snippets/", user), params + + expect(response).to have_http_status(400) + end + end + + describe 'PUT /snippets/:id' do + let(:other_user) { create(:user) } + let(:public_snippet) { create(:personal_snippet, :public, author: user) } + it 'updates snippet' do + new_content = 'New content' + + put api("/snippets/#{public_snippet.id}", user), content: new_content + + expect(response).to have_http_status(200) + public_snippet.reload + expect(public_snippet.content).to eq(new_content) + end + + it 'returns 404 for invalid snippet id' do + put api("/snippets/1234", user), title: 'foo' + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Snippet Not Found') + end + + it "returns 404 for another user's snippet" do + put api("/snippets/#{public_snippet.id}", other_user), title: 'fubar' + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Snippet Not Found') + end + + it 'returns 400 for missing parameters' do + put api("/snippets/1234", user) + + expect(response).to have_http_status(400) + end + end + + describe 'DELETE /snippets/:id' do + let!(:public_snippet) { create(:personal_snippet, :public, author: user) } + it 'deletes snippet' do + expect do + delete api("/snippets/#{public_snippet.id}", user) + + expect(response).to have_http_status(204) + end.to change { PersonalSnippet.count }.by(-1) + end + + it 'returns 404 for invalid snippet id' do + delete api("/snippets/1234", user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Snippet Not Found') + end + end +end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index c37dbfa0a33..9e317f3a7e9 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -651,13 +651,12 @@ describe API::Users, api: true do end describe "GET /user" do - let(:personal_access_token) { create(:personal_access_token, user: user) } - let(:private_token) { user.private_token } + let(:personal_access_token) { create(:personal_access_token, user: user).token } context 'with regular user' do context 'with personal access token' do it 'returns 403 without private token when sudo is defined' do - get api("/user?private_token=#{personal_access_token.token}&sudo=#{user.id}") + get api("/user?private_token=#{personal_access_token}&sudo=123") expect(response).to have_http_status(403) end @@ -665,7 +664,7 @@ describe API::Users, api: true do context 'with private token' do it 'returns 403 without private token when sudo defined' do - get api("/user?private_token=#{private_token}&sudo=#{user.id}") + get api("/user?private_token=#{user.private_token}&sudo=123") expect(response).to have_http_status(403) end @@ -676,40 +675,44 @@ describe API::Users, api: true do expect(response).to have_http_status(200) expect(response).to match_response_schema('user/public') + expect(json_response['id']).to eq(user.id) end end context 'with admin' do - let(:user) { create(:admin) } + let(:admin_personal_access_token) { create(:personal_access_token, user: admin).token } context 'with personal access token' do it 'returns 403 without private token when sudo defined' do - get api("/user?private_token=#{personal_access_token.token}&sudo=#{user.id}") + get api("/user?private_token=#{admin_personal_access_token}&sudo=#{user.id}") expect(response).to have_http_status(403) end - it 'returns current user without private token when sudo not defined' do - get api("/user?private_token=#{personal_access_token.token}") + it 'returns initial current user without private token when sudo not defined' do + get api("/user?private_token=#{admin_personal_access_token}") expect(response).to have_http_status(200) expect(response).to match_response_schema('user/public') + expect(json_response['id']).to eq(admin.id) end end context 'with private token' do - it 'returns current user with private token when sudo defined' do - get api("/user?private_token=#{private_token}&sudo=#{user.id}") + it 'returns sudoed user with private token when sudo defined' do + get api("/user?private_token=#{admin.private_token}&sudo=#{user.id}") expect(response).to have_http_status(200) expect(response).to match_response_schema('user/login') + expect(json_response['id']).to eq(user.id) end - it 'returns current user without private token when sudo not defined' do - get api("/user?private_token=#{private_token}") + it 'returns initial current user without private token when sudo not defined' do + get api("/user?private_token=#{admin.private_token}") expect(response).to have_http_status(200) expect(response).to match_response_schema('user/public') + expect(json_response['id']).to eq(admin.id) end end end diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb index 69eeb45ed71..661b671301e 100644 --- a/spec/routing/admin_routing_spec.rb +++ b/spec/routing/admin_routing_spec.rb @@ -66,7 +66,8 @@ describe Admin::ProjectsController, "routing" do end it "to #show" do - expect(get("/admin/projects/gitlab")).to route_to('admin/projects#show', namespace_id: 'gitlab') + expect(get("/admin/projects/gitlab/gitlab-ce")).to route_to('admin/projects#show', namespace_id: 'gitlab', id: 'gitlab-ce') + expect(get("/admin/projects/gitlab/subgroup/gitlab-ce")).to route_to('admin/projects#show', namespace_id: 'gitlab/subgroup', id: 'gitlab-ce') end end @@ -119,3 +120,14 @@ describe Admin::HealthCheckController, "routing" do expect(get("/admin/health_check")).to route_to('admin/health_check#show') end end + +describe Admin::GroupsController, "routing" do + it "to #index" do + expect(get("/admin/groups")).to route_to('admin/groups#index') + end + + it "to #show" do + expect(get("/admin/groups/gitlab")).to route_to('admin/groups#show', id: 'gitlab') + expect(get("/admin/groups/gitlab/subgroup")).to route_to('admin/groups#show', id: 'gitlab/subgroup') + end +end diff --git a/spec/serializers/analytics_build_entity_spec.rb b/spec/serializers/analytics_build_entity_spec.rb index 6b33fe66a63..86e703a6448 100644 --- a/spec/serializers/analytics_build_entity_spec.rb +++ b/spec/serializers/analytics_build_entity_spec.rb @@ -13,6 +13,14 @@ describe AnalyticsBuildEntity do subject { entity.as_json } + before do + Timecop.freeze + end + + after do + Timecop.return + end + it 'contains the URL' do expect(subject).to include(:url) end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 4aadd009f3e..ceaca96e25b 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -210,5 +210,22 @@ describe Ci::CreatePipelineService, services: true do expect(result.manual_actions).not_to be_empty end end + + context 'with environment' do + before do + config = YAML.dump(deploy: { environment: { name: "review/$CI_BUILD_REF_NAME" }, script: 'ls' }) + stub_ci_pipeline_yaml_file(config) + end + + it 'creates the environment' do + result = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: [{ message: 'some msg' }]) + + expect(result).to be_persisted + expect(Environment.find_by(name: "review/master")).not_to be_nil + end + end end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 90b7e62bc6f..0e8adb68721 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -694,7 +694,7 @@ describe SystemNoteService, services: true do describe "existing reference" do before do - message = "[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\n'#{commit.title}'" + message = "[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\n'#{commit.title.chomp}'" allow_any_instance_of(JIRA::Resource::Issue).to receive(:comments).and_return([OpenStruct.new(body: message)]) end diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb index c0b3e83244d..ad1eed5b369 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -75,7 +75,8 @@ module LoginHelpers def logout find(".header-user-dropdown-toggle").click click_link "Sign out" - expect(page).to have_content('Signed out successfully') + # check the sign_in button + expect(page).to have_button('Sign in') end # Logout without JavaScript driver diff --git a/spec/support/services_shared_context.rb b/spec/support/services_shared_context.rb index d1c999cad4d..66c93890e31 100644 --- a/spec/support/services_shared_context.rb +++ b/spec/support/services_shared_context.rb @@ -16,8 +16,14 @@ Service.available_services_names.each do |service| hash.merge!(k => 'secrettoken') elsif k =~ /^(.*_url|url|webhook)/ hash.merge!(k => "http://example.com") + elsif service_klass.method_defined?("#{k}?") + hash.merge!(k => true) elsif service == 'irker' && k == :recipients hash.merge!(k => 'irc://irc.network.net:666/#channel') + elsif service == 'irker' && k == :server_port + hash.merge!(k => 1234) + elsif service == 'jira' && k == :jira_issue_transition_id + hash.merge!(k => 1234) else hash.merge!(k => "someword") end diff --git a/spec/support/upload_helpers.rb b/spec/support/upload_helpers.rb new file mode 100644 index 00000000000..5eead80c935 --- /dev/null +++ b/spec/support/upload_helpers.rb @@ -0,0 +1,16 @@ +require 'fileutils' + +module UploadHelpers + extend self + + def uploaded_image_temp_path + basename = 'banana_sample.gif' + orig_path = File.join(Rails.root, 'spec', 'fixtures', basename) + tmp_path = File.join(Rails.root, 'tmp', 'tests', basename) + # Because we use 'move_to_store' on all uploaders, we create a new + # tempfile on each call: the file we return here will be renamed in most + # cases. + FileUtils.copy(orig_path, tmp_path) + tmp_path + end +end diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb new file mode 100644 index 00000000000..6098be5cd45 --- /dev/null +++ b/spec/uploaders/attachment_uploader_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe AttachmentUploader do + let(:issue) { build(:issue) } + subject { described_class.new(issue) } + + describe '#move_to_cache' do + it 'is true' do + expect(subject.move_to_cache).to eq(true) + end + end + + describe '#move_to_store' do + it 'is true' do + expect(subject.move_to_store).to eq(true) + end + end +end diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb new file mode 100644 index 00000000000..1f0e8732587 --- /dev/null +++ b/spec/uploaders/avatar_uploader_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe AvatarUploader do + let(:user) { build(:user) } + subject { described_class.new(user) } + + describe '#move_to_cache' do + it 'is true' do + expect(subject.move_to_cache).to eq(true) + end + end + + describe '#move_to_store' do + it 'is true' do + expect(subject.move_to_store).to eq(true) + end + end +end diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb index e8300abed5d..bc86adfef68 100644 --- a/spec/uploaders/file_uploader_spec.rb +++ b/spec/uploaders/file_uploader_spec.rb @@ -42,4 +42,16 @@ describe FileUploader do expect(@uploader.image_or_video?).to be false end end + + describe '#move_to_cache' do + it 'is true' do + expect(@uploader.move_to_cache).to eq(true) + end + end + + describe '#move_to_store' do + it 'is true' do + expect(@uploader.move_to_store).to eq(true) + end + end end diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb index 16bf0698c4b..e741e3cf9b6 100644 --- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb +++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb @@ -13,7 +13,7 @@ describe 'projects/commit/_commit_box.html.haml' do it 'shows the commit SHA' do render - expect(rendered).to have_text("Commit #{Commit.truncate_sha(project.commit.sha)}") + expect(rendered).to have_text("#{Commit.truncate_sha(project.commit.sha)}") end it 'shows the last pipeline that ran for the commit' do diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb index bf027499c94..a066ea078e6 100644 --- a/spec/views/projects/pipelines/show.html.haml_spec.rb +++ b/spec/views/projects/pipelines/show.html.haml_spec.rb @@ -28,7 +28,7 @@ describe 'projects/pipelines/show' do it 'shows a graph with grouped stages' do render - expect(rendered).to have_css('.pipeline-graph') + expect(rendered).to have_css('.js-pipeline-graph') expect(rendered).to have_css('.grouped-pipeline-dropdown') # stages diff --git a/vendor/assets/fonts/KaTeX_AMS-Regular.eot b/vendor/assets/fonts/KaTeX_AMS-Regular.eot Binary files differnew file mode 100644 index 00000000000..784276a3cbf --- /dev/null +++ b/vendor/assets/fonts/KaTeX_AMS-Regular.eot diff --git a/vendor/assets/fonts/KaTeX_AMS-Regular.ttf b/vendor/assets/fonts/KaTeX_AMS-Regular.ttf Binary files differnew file mode 100644 index 00000000000..6f1e0be2028 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_AMS-Regular.ttf diff --git a/vendor/assets/fonts/KaTeX_AMS-Regular.woff b/vendor/assets/fonts/KaTeX_AMS-Regular.woff Binary files differnew file mode 100644 index 00000000000..4dded4733b3 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_AMS-Regular.woff diff --git a/vendor/assets/fonts/KaTeX_AMS-Regular.woff2 b/vendor/assets/fonts/KaTeX_AMS-Regular.woff2 Binary files differnew file mode 100644 index 00000000000..ea81079c4e2 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_AMS-Regular.woff2 diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.eot b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.eot Binary files differnew file mode 100644 index 00000000000..1a0db0c568e --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.eot diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.ttf b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.ttf Binary files differnew file mode 100644 index 00000000000..b94907dad11 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.ttf diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff Binary files differnew file mode 100644 index 00000000000..799fa8122ca --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff2 b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff2 Binary files differnew file mode 100644 index 00000000000..73bb5422878 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff2 diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.eot b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.eot Binary files differnew file mode 100644 index 00000000000..6cc83d0922c --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.eot diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.ttf b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.ttf Binary files differnew file mode 100644 index 00000000000..cf51e2021e4 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.ttf diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff Binary files differnew file mode 100644 index 00000000000..f5e5c623577 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff2 b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff2 Binary files differnew file mode 100644 index 00000000000..dd76d3488d5 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff2 diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.eot b/vendor/assets/fonts/KaTeX_Fraktur-Bold.eot Binary files differnew file mode 100644 index 00000000000..1960b106656 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Fraktur-Bold.eot diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.ttf b/vendor/assets/fonts/KaTeX_Fraktur-Bold.ttf Binary files differnew file mode 100644 index 00000000000..7b0790f1ae8 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Fraktur-Bold.ttf diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff Binary files differnew file mode 100644 index 00000000000..dc325713291 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff2 b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff2 Binary files differnew file mode 100644 index 00000000000..fdc429227ad --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff2 diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.eot b/vendor/assets/fonts/KaTeX_Fraktur-Regular.eot Binary files differnew file mode 100644 index 00000000000..e4e73796aea --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Fraktur-Regular.eot diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.ttf b/vendor/assets/fonts/KaTeX_Fraktur-Regular.ttf Binary files differnew file mode 100644 index 00000000000..063bc0263eb --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Fraktur-Regular.ttf diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff Binary files differnew file mode 100644 index 00000000000..c4b18d863f3 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff2 b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff2 Binary files differnew file mode 100644 index 00000000000..4318d938e26 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff2 diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.eot b/vendor/assets/fonts/KaTeX_Main-Bold.eot Binary files differnew file mode 100644 index 00000000000..80fbd022363 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Main-Bold.eot diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.ttf b/vendor/assets/fonts/KaTeX_Main-Bold.ttf Binary files differnew file mode 100644 index 00000000000..8e10722afae --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Main-Bold.ttf diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.woff b/vendor/assets/fonts/KaTeX_Main-Bold.woff Binary files differnew file mode 100644 index 00000000000..43b361a6005 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Main-Bold.woff diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.woff2 b/vendor/assets/fonts/KaTeX_Main-Bold.woff2 Binary files differnew file mode 100644 index 00000000000..af57a96c148 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Main-Bold.woff2 diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.eot b/vendor/assets/fonts/KaTeX_Main-Italic.eot Binary files differnew file mode 100644 index 00000000000..fc770166b5e --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Main-Italic.eot diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.ttf b/vendor/assets/fonts/KaTeX_Main-Italic.ttf Binary files differnew file mode 100644 index 00000000000..d124495d7b6 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Main-Italic.ttf diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.woff b/vendor/assets/fonts/KaTeX_Main-Italic.woff Binary files differnew file mode 100644 index 00000000000..e623236bc44 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Main-Italic.woff diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.woff2 b/vendor/assets/fonts/KaTeX_Main-Italic.woff2 Binary files differnew file mode 100644 index 00000000000..944e9740bdf --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Main-Italic.woff2 diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.eot b/vendor/assets/fonts/KaTeX_Main-Regular.eot Binary files differnew file mode 100644 index 00000000000..dc60c090c7a --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Main-Regular.eot diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.ttf b/vendor/assets/fonts/KaTeX_Main-Regular.ttf Binary files differnew file mode 100644 index 00000000000..da5797ffcce --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Main-Regular.ttf diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.woff b/vendor/assets/fonts/KaTeX_Main-Regular.woff Binary files differnew file mode 100644 index 00000000000..37db672e821 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Main-Regular.woff diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.woff2 b/vendor/assets/fonts/KaTeX_Main-Regular.woff2 Binary files differnew file mode 100644 index 00000000000..48820424893 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Main-Regular.woff2 diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.eot b/vendor/assets/fonts/KaTeX_Math-BoldItalic.eot Binary files differnew file mode 100644 index 00000000000..52c8b8c6b40 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Math-BoldItalic.eot diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.ttf b/vendor/assets/fonts/KaTeX_Math-BoldItalic.ttf Binary files differnew file mode 100644 index 00000000000..a8b527c7ef6 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Math-BoldItalic.ttf diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff Binary files differnew file mode 100644 index 00000000000..8940e0b5801 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff2 b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff2 Binary files differnew file mode 100644 index 00000000000..15cf56d3408 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff2 diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.eot b/vendor/assets/fonts/KaTeX_Math-Italic.eot Binary files differnew file mode 100644 index 00000000000..64c8992c477 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Math-Italic.eot diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.ttf b/vendor/assets/fonts/KaTeX_Math-Italic.ttf Binary files differnew file mode 100644 index 00000000000..06f39d3a299 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Math-Italic.ttf diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.woff b/vendor/assets/fonts/KaTeX_Math-Italic.woff Binary files differnew file mode 100644 index 00000000000..cf3b4b79e5b --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Math-Italic.woff diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.woff2 b/vendor/assets/fonts/KaTeX_Math-Italic.woff2 Binary files differnew file mode 100644 index 00000000000..5f8c4bfa455 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Math-Italic.woff2 diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.eot b/vendor/assets/fonts/KaTeX_Math-Regular.eot Binary files differnew file mode 100644 index 00000000000..5521e6a564d --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Math-Regular.eot diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.ttf b/vendor/assets/fonts/KaTeX_Math-Regular.ttf Binary files differnew file mode 100644 index 00000000000..73127082370 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Math-Regular.ttf diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.woff b/vendor/assets/fonts/KaTeX_Math-Regular.woff Binary files differnew file mode 100644 index 00000000000..0e2ebdf18af --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Math-Regular.woff diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.woff2 b/vendor/assets/fonts/KaTeX_Math-Regular.woff2 Binary files differnew file mode 100644 index 00000000000..ebe3d028a34 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Math-Regular.woff2 diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.eot b/vendor/assets/fonts/KaTeX_SansSerif-Bold.eot Binary files differnew file mode 100644 index 00000000000..1660e76a2b6 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_SansSerif-Bold.eot diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.ttf b/vendor/assets/fonts/KaTeX_SansSerif-Bold.ttf Binary files differnew file mode 100644 index 00000000000..dbeb7b92ab5 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_SansSerif-Bold.ttf diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff Binary files differnew file mode 100644 index 00000000000..8f144a8bb31 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff2 b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff2 Binary files differnew file mode 100644 index 00000000000..329e85557fa --- /dev/null +++ b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff2 diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.eot b/vendor/assets/fonts/KaTeX_SansSerif-Italic.eot Binary files differnew file mode 100644 index 00000000000..289ae3ff8b7 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_SansSerif-Italic.eot diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.ttf b/vendor/assets/fonts/KaTeX_SansSerif-Italic.ttf Binary files differnew file mode 100644 index 00000000000..b3a2f38f224 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_SansSerif-Italic.ttf diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff Binary files differnew file mode 100644 index 00000000000..bddf7ea6579 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff2 b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff2 Binary files differnew file mode 100644 index 00000000000..5fa767bddd6 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff2 diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.eot b/vendor/assets/fonts/KaTeX_SansSerif-Regular.eot Binary files differnew file mode 100644 index 00000000000..1b38b98a180 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_SansSerif-Regular.eot diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.ttf b/vendor/assets/fonts/KaTeX_SansSerif-Regular.ttf Binary files differnew file mode 100644 index 00000000000..e4712f84775 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_SansSerif-Regular.ttf diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff Binary files differnew file mode 100644 index 00000000000..33be368048f --- /dev/null +++ b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff2 b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff2 Binary files differnew file mode 100644 index 00000000000..4fcb2e29a05 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff2 diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.eot b/vendor/assets/fonts/KaTeX_Script-Regular.eot Binary files differnew file mode 100644 index 00000000000..7870d7f319b --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Script-Regular.eot diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.ttf b/vendor/assets/fonts/KaTeX_Script-Regular.ttf Binary files differnew file mode 100644 index 00000000000..da4d11308ae --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Script-Regular.ttf diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.woff b/vendor/assets/fonts/KaTeX_Script-Regular.woff Binary files differnew file mode 100644 index 00000000000..d6ae79f998a --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Script-Regular.woff diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.woff2 b/vendor/assets/fonts/KaTeX_Script-Regular.woff2 Binary files differnew file mode 100644 index 00000000000..1b43deb45a8 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Script-Regular.woff2 diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.eot b/vendor/assets/fonts/KaTeX_Size1-Regular.eot Binary files differnew file mode 100644 index 00000000000..29950f95ff6 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Size1-Regular.eot diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.ttf b/vendor/assets/fonts/KaTeX_Size1-Regular.ttf Binary files differnew file mode 100644 index 00000000000..194466a655d --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Size1-Regular.ttf diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.woff b/vendor/assets/fonts/KaTeX_Size1-Regular.woff Binary files differnew file mode 100644 index 00000000000..237f271edd1 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Size1-Regular.woff diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size1-Regular.woff2 Binary files differnew file mode 100644 index 00000000000..39b6f8f746c --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Size1-Regular.woff2 diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.eot b/vendor/assets/fonts/KaTeX_Size2-Regular.eot Binary files differnew file mode 100644 index 00000000000..b8b0536f967 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Size2-Regular.eot diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.ttf b/vendor/assets/fonts/KaTeX_Size2-Regular.ttf Binary files differnew file mode 100644 index 00000000000..b41b66a638f --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Size2-Regular.ttf diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.woff b/vendor/assets/fonts/KaTeX_Size2-Regular.woff Binary files differnew file mode 100644 index 00000000000..4a3055854ed --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Size2-Regular.woff diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size2-Regular.woff2 Binary files differnew file mode 100644 index 00000000000..3facec1ab89 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Size2-Regular.woff2 diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.eot b/vendor/assets/fonts/KaTeX_Size3-Regular.eot Binary files differnew file mode 100644 index 00000000000..576b864fae6 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Size3-Regular.eot diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.ttf b/vendor/assets/fonts/KaTeX_Size3-Regular.ttf Binary files differnew file mode 100644 index 00000000000..790ddbbc55f --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Size3-Regular.ttf diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.woff b/vendor/assets/fonts/KaTeX_Size3-Regular.woff Binary files differnew file mode 100644 index 00000000000..3a6d062e660 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Size3-Regular.woff diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size3-Regular.woff2 Binary files differnew file mode 100644 index 00000000000..2cffafe5018 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Size3-Regular.woff2 diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.eot b/vendor/assets/fonts/KaTeX_Size4-Regular.eot Binary files differnew file mode 100644 index 00000000000..c2b045fc3db --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Size4-Regular.eot diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.ttf b/vendor/assets/fonts/KaTeX_Size4-Regular.ttf Binary files differnew file mode 100644 index 00000000000..ce660aa7ff9 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Size4-Regular.ttf diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.woff b/vendor/assets/fonts/KaTeX_Size4-Regular.woff Binary files differnew file mode 100644 index 00000000000..7826c6c97a1 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Size4-Regular.woff diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size4-Regular.woff2 Binary files differnew file mode 100644 index 00000000000..c92189812d9 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Size4-Regular.woff2 diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.eot b/vendor/assets/fonts/KaTeX_Typewriter-Regular.eot Binary files differnew file mode 100644 index 00000000000..4c178f484a8 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Typewriter-Regular.eot diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.ttf b/vendor/assets/fonts/KaTeX_Typewriter-Regular.ttf Binary files differnew file mode 100644 index 00000000000..b0427ad0a56 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Typewriter-Regular.ttf diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff Binary files differnew file mode 100644 index 00000000000..78e990488a9 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff2 b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff2 Binary files differnew file mode 100644 index 00000000000..618de99d480 --- /dev/null +++ b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff2 diff --git a/vendor/assets/javascripts/katex.js b/vendor/assets/javascripts/katex.js new file mode 100644 index 00000000000..9596b839832 --- /dev/null +++ b/vendor/assets/javascripts/katex.js @@ -0,0 +1,8642 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.katex = f()}})(function(){var define,module,exports;return (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<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ +/* eslint no-console:0 */ +/** + * This is the main entry point for KaTeX. Here, we expose functions for + * rendering expressions either to DOM nodes or to markup strings. + * + * We also expose the ParseError class to check if errors thrown from KaTeX are + * errors in the expression, or errors in javascript handling. + */ + +var ParseError = require("./src/ParseError"); +var Settings = require("./src/Settings"); + +var buildTree = require("./src/buildTree"); +var parseTree = require("./src/parseTree"); +var utils = require("./src/utils"); + +/** + * Parse and build an expression, and place that expression in the DOM node + * given. + */ +var render = function(expression, baseNode, options) { + utils.clearNode(baseNode); + + var settings = new Settings(options); + + var tree = parseTree(expression, settings); + var node = buildTree(tree, expression, settings).toNode(); + + baseNode.appendChild(node); +}; + +// KaTeX's styles don't work properly in quirks mode. Print out an error, and +// disable rendering. +if (typeof document !== "undefined") { + if (document.compatMode !== "CSS1Compat") { + typeof console !== "undefined" && console.warn( + "Warning: KaTeX doesn't work in quirks mode. Make sure your " + + "website has a suitable doctype."); + + render = function() { + throw new ParseError("KaTeX doesn't work in quirks mode."); + }; + } +} + +/** + * Parse and build an expression, and return the markup for that. + */ +var renderToString = function(expression, options) { + var settings = new Settings(options); + + var tree = parseTree(expression, settings); + return buildTree(tree, expression, settings).toMarkup(); +}; + +/** + * Parse an expression and return the parse tree. + */ +var generateParseTree = function(expression, options) { + var settings = new Settings(options); + return parseTree(expression, settings); +}; + +module.exports = { + render: render, + renderToString: renderToString, + /** + * NOTE: This method is not currently recommended for public use. + * The internal tree representation is unstable and is very likely + * to change. Use at your own risk. + */ + __parse: generateParseTree, + ParseError: ParseError, +}; + +},{"./src/ParseError":6,"./src/Settings":8,"./src/buildTree":13,"./src/parseTree":22,"./src/utils":25}],2:[function(require,module,exports){ +/** @flow */ + +"use strict"; + +function getRelocatable(re) { + // In the future, this could use a WeakMap instead of an expando. + if (!re.__matchAtRelocatable) { + // Disjunctions are the lowest-precedence operator, so we can make any + // pattern match the empty string by appending `|()` to it: + // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-patterns + var source = re.source + "|()"; + + // We always make the new regex global. + var flags = "g" + (re.ignoreCase ? "i" : "") + (re.multiline ? "m" : "") + (re.unicode ? "u" : "") + // sticky (/.../y) doesn't make sense in conjunction with our relocation + // logic, so we ignore it here. + ; + + re.__matchAtRelocatable = new RegExp(source, flags); + } + return re.__matchAtRelocatable; +} + +function matchAt(re, str, pos) { + if (re.global || re.sticky) { + throw new Error("matchAt(...): Only non-global regexes are supported"); + } + var reloc = getRelocatable(re); + reloc.lastIndex = pos; + var match = reloc.exec(str); + // Last capturing group is our sentinel that indicates whether the regex + // matched at the given location. + if (match[match.length - 1] == null) { + // Original regex matched. + match.length = match.length - 1; + return match; + } else { + return null; + } +} + +module.exports = matchAt; +},{}],3:[function(require,module,exports){ +/** + * The Lexer class handles tokenizing the input in various ways. Since our + * parser expects us to be able to backtrack, the lexer allows lexing from any + * given starting point. + * + * Its main exposed function is the `lex` function, which takes a position to + * lex from and a type of token to lex. It defers to the appropriate `_innerLex` + * function. + * + * The various `_innerLex` functions perform the actual lexing of different + * kinds. + */ + +var matchAt = require("match-at"); + +var ParseError = require("./ParseError"); + +// The main lexer class +function Lexer(input) { + this.input = input; + this.pos = 0; +} + +/** + * The resulting token returned from `lex`. + * + * It consists of the token text plus some position information. + * The position information is essentially a range in an input string, + * but instead of referencing the bare input string, we refer to the lexer. + * That way it is possible to attach extra metadata to the input string, + * like for example a file name or similar. + * + * The position information (all three parameters) is optional, + * so it is OK to construct synthetic tokens if appropriate. + * Not providing available position information may lead to + * degraded error reporting, though. + * + * @param {string} text the text of this token + * @param {number=} start the start offset, zero-based inclusive + * @param {number=} end the end offset, zero-based exclusive + * @param {Lexer=} lexer the lexer which in turn holds the input string + */ +function Token(text, start, end, lexer) { + this.text = text; + this.start = start; + this.end = end; + this.lexer = lexer; +} + +/** + * Given a pair of tokens (this and endToken), compute a “Token” encompassing + * the whole input range enclosed by these two. + * + * @param {Token} endToken last token of the range, inclusive + * @param {string} text the text of the newly constructed token + */ +Token.prototype.range = function(endToken, text) { + if (endToken.lexer !== this.lexer) { + return new Token(text); // sorry, no position information available + } + return new Token(text, this.start, endToken.end, this.lexer); +}; + +/* The following tokenRegex + * - matches typical whitespace (but not NBSP etc.) using its first group + * - does not match any control character \x00-\x1f except whitespace + * - does not match a bare backslash + * - matches any ASCII character except those just mentioned + * - does not match the BMP private use area \uE000-\uF8FF + * - does not match bare surrogate code units + * - matches any BMP character except for those just described + * - matches any valid Unicode surrogate pair + * - matches a backslash followed by one or more letters + * - matches a backslash followed by any BMP character, including newline + * Just because the Lexer matches something doesn't mean it's valid input: + * If there is no matching function or symbol definition, the Parser will + * still reject the input. + */ +var tokenRegex = new RegExp( + "([ \r\n\t]+)|" + // whitespace + "([!-\\[\\]-\u2027\u202A-\uD7FF\uF900-\uFFFF]" + // single codepoint + "|[\uD800-\uDBFF][\uDC00-\uDFFF]" + // surrogate pair + "|\\\\(?:[a-zA-Z]+|[^\uD800-\uDFFF])" + // function name + ")" +); + +/** + * This function lexes a single token. + */ +Lexer.prototype.lex = function() { + var input = this.input; + var pos = this.pos; + if (pos === input.length) { + return new Token("EOF", pos, pos, this); + } + var match = matchAt(tokenRegex, input, pos); + if (match === null) { + throw new ParseError( + "Unexpected character: '" + input[pos] + "'", + new Token(input[pos], pos, pos + 1, this)); + } + var text = match[2] || " "; + var start = this.pos; + this.pos += match[0].length; + var end = this.pos; + return new Token(text, start, end, this); +}; + +module.exports = Lexer; + +},{"./ParseError":6,"match-at":2}],4:[function(require,module,exports){ +/** + * This file contains the “gullet” where macros are expanded + * until only non-macro tokens remain. + */ + +var Lexer = require("./Lexer"); + +function MacroExpander(input, macros) { + this.lexer = new Lexer(input); + this.macros = macros; + this.stack = []; // contains tokens in REVERSE order + this.discardedWhiteSpace = []; +} + +/** + * Recursively expand first token, then return first non-expandable token. + */ +MacroExpander.prototype.nextToken = function() { + for (;;) { + if (this.stack.length === 0) { + this.stack.push(this.lexer.lex()); + } + var topToken = this.stack.pop(); + var name = topToken.text; + if (!(name.charAt(0) === "\\" && this.macros.hasOwnProperty(name))) { + return topToken; + } + var expansion = this.macros[name]; + if (typeof expansion === "string") { + var bodyLexer = new Lexer(expansion); + expansion = []; + var tok = bodyLexer.lex(); + while (tok.text !== "EOF") { + expansion.push(tok); + tok = bodyLexer.lex(); + } + expansion.reverse(); // to fit in with stack using push and pop + this.macros[name] = expansion; + } + this.stack = this.stack.concat(expansion); + } +}; + +MacroExpander.prototype.get = function(ignoreSpace) { + this.discardedWhiteSpace = []; + var token = this.nextToken(); + if (ignoreSpace) { + while (token.text === " ") { + this.discardedWhiteSpace.push(token); + token = this.nextToken(); + } + } + return token; +}; + +/** + * Undo the effect of the preceding call to the get method. + * A call to this method MUST be immediately preceded and immediately followed + * by a call to get. Only used during mode switching, i.e. after one token + * was got in the old mode but should get got again in a new mode + * with possibly different whitespace handling. + */ +MacroExpander.prototype.unget = function(token) { + this.stack.push(token); + while (this.discardedWhiteSpace.length !== 0) { + this.stack.push(this.discardedWhiteSpace.pop()); + } +}; + +module.exports = MacroExpander; + +},{"./Lexer":3}],5:[function(require,module,exports){ +/** + * This file contains information about the options that the Parser carries + * around with it while parsing. Data is held in an `Options` object, and when + * recursing, a new `Options` object can be created with the `.with*` and + * `.reset` functions. + */ + +/** + * This is the main options class. It contains the style, size, color, and font + * of the current parse level. It also contains the style and size of the parent + * parse level, so size changes can be handled efficiently. + * + * Each of the `.with*` and `.reset` functions passes its current style and size + * as the parentStyle and parentSize of the new options class, so parent + * handling is taken care of automatically. + */ +function Options(data) { + this.style = data.style; + this.color = data.color; + this.size = data.size; + this.phantom = data.phantom; + this.font = data.font; + + if (data.parentStyle === undefined) { + this.parentStyle = data.style; + } else { + this.parentStyle = data.parentStyle; + } + + if (data.parentSize === undefined) { + this.parentSize = data.size; + } else { + this.parentSize = data.parentSize; + } +} + +/** + * Returns a new options object with the same properties as "this". Properties + * from "extension" will be copied to the new options object. + */ +Options.prototype.extend = function(extension) { + var data = { + style: this.style, + size: this.size, + color: this.color, + parentStyle: this.style, + parentSize: this.size, + phantom: this.phantom, + font: this.font, + }; + + for (var key in extension) { + if (extension.hasOwnProperty(key)) { + data[key] = extension[key]; + } + } + + return new Options(data); +}; + +/** + * Create a new options object with the given style. + */ +Options.prototype.withStyle = function(style) { + return this.extend({ + style: style, + }); +}; + +/** + * Create a new options object with the given size. + */ +Options.prototype.withSize = function(size) { + return this.extend({ + size: size, + }); +}; + +/** + * Create a new options object with the given color. + */ +Options.prototype.withColor = function(color) { + return this.extend({ + color: color, + }); +}; + +/** + * Create a new options object with "phantom" set to true. + */ +Options.prototype.withPhantom = function() { + return this.extend({ + phantom: true, + }); +}; + +/** + * Create a new options objects with the give font. + */ +Options.prototype.withFont = function(font) { + return this.extend({ + font: font, + }); +}; + +/** + * Create a new options object with the same style, size, and color. This is + * used so that parent style and size changes are handled correctly. + */ +Options.prototype.reset = function() { + return this.extend({}); +}; + +/** + * A map of color names to CSS colors. + * TODO(emily): Remove this when we have real macros + */ +var colorMap = { + "katex-blue": "#6495ed", + "katex-orange": "#ffa500", + "katex-pink": "#ff00af", + "katex-red": "#df0030", + "katex-green": "#28ae7b", + "katex-gray": "gray", + "katex-purple": "#9d38bd", + "katex-blueA": "#ccfaff", + "katex-blueB": "#80f6ff", + "katex-blueC": "#63d9ea", + "katex-blueD": "#11accd", + "katex-blueE": "#0c7f99", + "katex-tealA": "#94fff5", + "katex-tealB": "#26edd5", + "katex-tealC": "#01d1c1", + "katex-tealD": "#01a995", + "katex-tealE": "#208170", + "katex-greenA": "#b6ffb0", + "katex-greenB": "#8af281", + "katex-greenC": "#74cf70", + "katex-greenD": "#1fab54", + "katex-greenE": "#0d923f", + "katex-goldA": "#ffd0a9", + "katex-goldB": "#ffbb71", + "katex-goldC": "#ff9c39", + "katex-goldD": "#e07d10", + "katex-goldE": "#a75a05", + "katex-redA": "#fca9a9", + "katex-redB": "#ff8482", + "katex-redC": "#f9685d", + "katex-redD": "#e84d39", + "katex-redE": "#bc2612", + "katex-maroonA": "#ffbde0", + "katex-maroonB": "#ff92c6", + "katex-maroonC": "#ed5fa6", + "katex-maroonD": "#ca337c", + "katex-maroonE": "#9e034e", + "katex-purpleA": "#ddd7ff", + "katex-purpleB": "#c6b9fc", + "katex-purpleC": "#aa87ff", + "katex-purpleD": "#7854ab", + "katex-purpleE": "#543b78", + "katex-mintA": "#f5f9e8", + "katex-mintB": "#edf2df", + "katex-mintC": "#e0e5cc", + "katex-grayA": "#f6f7f7", + "katex-grayB": "#f0f1f2", + "katex-grayC": "#e3e5e6", + "katex-grayD": "#d6d8da", + "katex-grayE": "#babec2", + "katex-grayF": "#888d93", + "katex-grayG": "#626569", + "katex-grayH": "#3b3e40", + "katex-grayI": "#21242c", + "katex-kaBlue": "#314453", + "katex-kaGreen": "#71B307", +}; + +/** + * Gets the CSS color of the current options object, accounting for the + * `colorMap`. + */ +Options.prototype.getColor = function() { + if (this.phantom) { + return "transparent"; + } else { + return colorMap[this.color] || this.color; + } +}; + +module.exports = Options; + +},{}],6:[function(require,module,exports){ +/** + * This is the ParseError class, which is the main error thrown by KaTeX + * functions when something has gone wrong. This is used to distinguish internal + * errors from errors in the expression that the user provided. + * + * If possible, a caller should provide a Token or ParseNode with information + * about where in the source string the problem occurred. + * + * @param {string} message The error message + * @param {(Token|ParseNode)=} token An object providing position information + */ +function ParseError(message, token) { + var error = "KaTeX parse error: " + message; + var start; + var end; + + if (token && token.lexer && token.start <= token.end) { + // If we have the input and a position, make the error a bit fancier + + // Get the input + var input = token.lexer.input; + + // Prepend some information + start = token.start; + end = token.end; + if (start === input.length) { + error += " at end of input: "; + } else { + error += " at position " + (start + 1) + ": "; + } + + // Underline token in question using combining underscores + var underlined = input.slice(start, end).replace(/[^]/g, "$&\u0332"); + + // Extract some context from the input and add it to the error + var left; + if (start > 15) { + left = "…" + input.slice(start - 15, start); + } else { + left = input.slice(0, start); + } + var right; + if (end + 15 < input.length) { + right = input.slice(end, end + 15) + "…"; + } else { + right = input.slice(end); + } + error += left + underlined + right; + } + + // Some hackery to make ParseError a prototype of Error + // See http://stackoverflow.com/a/8460753 + var self = new Error(error); + self.name = "ParseError"; + self.__proto__ = ParseError.prototype; + + self.position = start; + return self; +} + +// More hackery +ParseError.prototype.__proto__ = Error.prototype; + +module.exports = ParseError; + +},{}],7:[function(require,module,exports){ +/* eslint no-constant-condition:0 */ +var functions = require("./functions"); +var environments = require("./environments"); +var MacroExpander = require("./MacroExpander"); +var symbols = require("./symbols"); +var utils = require("./utils"); +var cjkRegex = require("./unicodeRegexes").cjkRegex; + +var parseData = require("./parseData"); +var ParseError = require("./ParseError"); + +/** + * This file contains the parser used to parse out a TeX expression from the + * input. Since TeX isn't context-free, standard parsers don't work particularly + * well. + * + * The strategy of this parser is as such: + * + * The main functions (the `.parse...` ones) take a position in the current + * parse string to parse tokens from. The lexer (found in Lexer.js, stored at + * this.lexer) also supports pulling out tokens at arbitrary places. When + * individual tokens are needed at a position, the lexer is called to pull out a + * token, which is then used. + * + * The parser has a property called "mode" indicating the mode that + * the parser is currently in. Currently it has to be one of "math" or + * "text", which denotes whether the current environment is a math-y + * one or a text-y one (e.g. inside \text). Currently, this serves to + * limit the functions which can be used in text mode. + * + * The main functions then return an object which contains the useful data that + * was parsed at its given point, and a new position at the end of the parsed + * data. The main functions can call each other and continue the parsing by + * using the returned position as a new starting point. + * + * There are also extra `.handle...` functions, which pull out some reused + * functionality into self-contained functions. + * + * The earlier functions return ParseNodes. + * The later functions (which are called deeper in the parse) sometimes return + * ParseFuncOrArgument, which contain a ParseNode as well as some data about + * whether the parsed object is a function which is missing some arguments, or a + * standalone object which can be used as an argument to another function. + */ + +/** + * Main Parser class + */ +function Parser(input, settings) { + // Create a new macro expander (gullet) and (indirectly via that) also a + // new lexer (mouth) for this parser (stomach, in the language of TeX) + this.gullet = new MacroExpander(input, settings.macros); + // Store the settings for use in parsing + this.settings = settings; +} + +var ParseNode = parseData.ParseNode; + +/** + * An initial function (without its arguments), or an argument to a function. + * The `result` argument should be a ParseNode. + */ +function ParseFuncOrArgument(result, isFunction, token) { + this.result = result; + // Is this a function (i.e. is it something defined in functions.js)? + this.isFunction = isFunction; + this.token = token; +} + +/** + * Checks a result to make sure it has the right type, and throws an + * appropriate error otherwise. + * + * @param {boolean=} consume whether to consume the expected token, + * defaults to true + */ +Parser.prototype.expect = function(text, consume) { + if (this.nextToken.text !== text) { + throw new ParseError( + "Expected '" + text + "', got '" + this.nextToken.text + "'", + this.nextToken + ); + } + if (consume !== false) { + this.consume(); + } +}; + +/** + * Considers the current look ahead token as consumed, + * and fetches the one after that as the new look ahead. + */ +Parser.prototype.consume = function() { + this.nextToken = this.gullet.get(this.mode === "math"); +}; + +Parser.prototype.switchMode = function(newMode) { + this.gullet.unget(this.nextToken); + this.mode = newMode; + this.consume(); +}; + +/** + * Main parsing function, which parses an entire input. + * + * @return {?Array.<ParseNode>} + */ +Parser.prototype.parse = function() { + // Try to parse the input + this.mode = "math"; + this.consume(); + var parse = this.parseInput(); + return parse; +}; + +/** + * Parses an entire input tree. + */ +Parser.prototype.parseInput = function() { + // Parse an expression + var expression = this.parseExpression(false); + // If we succeeded, make sure there's an EOF at the end + this.expect("EOF", false); + return expression; +}; + +var endOfExpression = ["}", "\\end", "\\right", "&", "\\\\", "\\cr"]; + +/** + * Parses an "expression", which is a list of atoms. + * + * @param {boolean} breakOnInfix Should the parsing stop when we hit infix + * nodes? This happens when functions have higher precendence + * than infix nodes in implicit parses. + * + * @param {?string} breakOnTokenText The text of the token that the expression + * should end with, or `null` if something else should end the + * expression. + * + * @return {ParseNode} + */ +Parser.prototype.parseExpression = function(breakOnInfix, breakOnTokenText) { + var body = []; + // Keep adding atoms to the body until we can't parse any more atoms (either + // we reached the end, a }, or a \right) + while (true) { + var lex = this.nextToken; + if (endOfExpression.indexOf(lex.text) !== -1) { + break; + } + if (breakOnTokenText && lex.text === breakOnTokenText) { + break; + } + if (breakOnInfix && functions[lex.text] && functions[lex.text].infix) { + break; + } + var atom = this.parseAtom(); + if (!atom) { + if (!this.settings.throwOnError && lex.text[0] === "\\") { + var errorNode = this.handleUnsupportedCmd(); + body.push(errorNode); + continue; + } + + break; + } + body.push(atom); + } + return this.handleInfixNodes(body); +}; + +/** + * Rewrites infix operators such as \over with corresponding commands such + * as \frac. + * + * There can only be one infix operator per group. If there's more than one + * then the expression is ambiguous. This can be resolved by adding {}. + * + * @returns {Array} + */ +Parser.prototype.handleInfixNodes = function(body) { + var overIndex = -1; + var funcName; + + for (var i = 0; i < body.length; i++) { + var node = body[i]; + if (node.type === "infix") { + if (overIndex !== -1) { + throw new ParseError( + "only one infix operator per group", + node.value.token); + } + overIndex = i; + funcName = node.value.replaceWith; + } + } + + if (overIndex !== -1) { + var numerNode; + var denomNode; + + var numerBody = body.slice(0, overIndex); + var denomBody = body.slice(overIndex + 1); + + if (numerBody.length === 1 && numerBody[0].type === "ordgroup") { + numerNode = numerBody[0]; + } else { + numerNode = new ParseNode("ordgroup", numerBody, this.mode); + } + + if (denomBody.length === 1 && denomBody[0].type === "ordgroup") { + denomNode = denomBody[0]; + } else { + denomNode = new ParseNode("ordgroup", denomBody, this.mode); + } + + var value = this.callFunction( + funcName, [numerNode, denomNode], null); + return [new ParseNode(value.type, value, this.mode)]; + } else { + return body; + } +}; + +// The greediness of a superscript or subscript +var SUPSUB_GREEDINESS = 1; + +/** + * Handle a subscript or superscript with nice errors. + */ +Parser.prototype.handleSupSubscript = function(name) { + var symbolToken = this.nextToken; + var symbol = symbolToken.text; + this.consume(); + var group = this.parseGroup(); + + if (!group) { + if (!this.settings.throwOnError && this.nextToken.text[0] === "\\") { + return this.handleUnsupportedCmd(); + } else { + throw new ParseError( + "Expected group after '" + symbol + "'", + symbolToken + ); + } + } else if (group.isFunction) { + // ^ and _ have a greediness, so handle interactions with functions' + // greediness + var funcGreediness = functions[group.result].greediness; + if (funcGreediness > SUPSUB_GREEDINESS) { + return this.parseFunction(group); + } else { + throw new ParseError( + "Got function '" + group.result + "' with no arguments " + + "as " + name, symbolToken); + } + } else { + return group.result; + } +}; + +/** + * Converts the textual input of an unsupported command into a text node + * contained within a color node whose color is determined by errorColor + */ +Parser.prototype.handleUnsupportedCmd = function() { + var text = this.nextToken.text; + var textordArray = []; + + for (var i = 0; i < text.length; i++) { + textordArray.push(new ParseNode("textord", text[i], "text")); + } + + var textNode = new ParseNode( + "text", + { + body: textordArray, + type: "text", + }, + this.mode); + + var colorNode = new ParseNode( + "color", + { + color: this.settings.errorColor, + value: [textNode], + type: "color", + }, + this.mode); + + this.consume(); + return colorNode; +}; + +/** + * Parses a group with optional super/subscripts. + * + * @return {?ParseNode} + */ +Parser.prototype.parseAtom = function() { + // The body of an atom is an implicit group, so that things like + // \left(x\right)^2 work correctly. + var base = this.parseImplicitGroup(); + + // In text mode, we don't have superscripts or subscripts + if (this.mode === "text") { + return base; + } + + // Note that base may be empty (i.e. null) at this point. + + var superscript; + var subscript; + while (true) { + // Lex the first token + var lex = this.nextToken; + + if (lex.text === "\\limits" || lex.text === "\\nolimits") { + // We got a limit control + if (!base || base.type !== "op") { + throw new ParseError( + "Limit controls must follow a math operator", + lex); + } else { + var limits = lex.text === "\\limits"; + base.value.limits = limits; + base.value.alwaysHandleSupSub = true; + } + this.consume(); + } else if (lex.text === "^") { + // We got a superscript start + if (superscript) { + throw new ParseError("Double superscript", lex); + } + superscript = this.handleSupSubscript("superscript"); + } else if (lex.text === "_") { + // We got a subscript start + if (subscript) { + throw new ParseError("Double subscript", lex); + } + subscript = this.handleSupSubscript("subscript"); + } else if (lex.text === "'") { + // We got a prime + var prime = new ParseNode("textord", "\\prime", this.mode); + + // Many primes can be grouped together, so we handle this here + var primes = [prime]; + this.consume(); + // Keep lexing tokens until we get something that's not a prime + while (this.nextToken.text === "'") { + // For each one, add another prime to the list + primes.push(prime); + this.consume(); + } + // Put them into an ordgroup as the superscript + superscript = new ParseNode("ordgroup", primes, this.mode); + } else { + // If it wasn't ^, _, or ', stop parsing super/subscripts + break; + } + } + + if (superscript || subscript) { + // If we got either a superscript or subscript, create a supsub + return new ParseNode("supsub", { + base: base, + sup: superscript, + sub: subscript, + }, this.mode); + } else { + // Otherwise return the original body + return base; + } +}; + +// A list of the size-changing functions, for use in parseImplicitGroup +var sizeFuncs = [ + "\\tiny", "\\scriptsize", "\\footnotesize", "\\small", "\\normalsize", + "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge", +]; + +// A list of the style-changing functions, for use in parseImplicitGroup +var styleFuncs = [ + "\\displaystyle", "\\textstyle", "\\scriptstyle", "\\scriptscriptstyle", +]; + +/** + * Parses an implicit group, which is a group that starts at the end of a + * specified, and ends right before a higher explicit group ends, or at EOL. It + * is used for functions that appear to affect the current style, like \Large or + * \textrm, where instead of keeping a style we just pretend that there is an + * implicit grouping after it until the end of the group. E.g. + * small text {\Large large text} small text again + * It is also used for \left and \right to get the correct grouping. + * + * @return {?ParseNode} + */ +Parser.prototype.parseImplicitGroup = function() { + var start = this.parseSymbol(); + + if (start == null) { + // If we didn't get anything we handle, fall back to parseFunction + return this.parseFunction(); + } + + var func = start.result; + var body; + + if (func === "\\left") { + // If we see a left: + // Parse the entire left function (including the delimiter) + var left = this.parseFunction(start); + // Parse out the implicit body + body = this.parseExpression(false); + // Check the next token + this.expect("\\right", false); + var right = this.parseFunction(); + return new ParseNode("leftright", { + body: body, + left: left.value.value, + right: right.value.value, + }, this.mode); + } else if (func === "\\begin") { + // begin...end is similar to left...right + var begin = this.parseFunction(start); + var envName = begin.value.name; + if (!environments.hasOwnProperty(envName)) { + throw new ParseError( + "No such environment: " + envName, begin.value.nameGroup); + } + // Build the environment object. Arguments and other information will + // be made available to the begin and end methods using properties. + var env = environments[envName]; + var args = this.parseArguments("\\begin{" + envName + "}", env); + var context = { + mode: this.mode, + envName: envName, + parser: this, + positions: args.pop(), + }; + var result = env.handler(context, args); + this.expect("\\end", false); + var endNameToken = this.nextToken; + var end = this.parseFunction(); + if (end.value.name !== envName) { + throw new ParseError( + "Mismatch: \\begin{" + envName + "} matched " + + "by \\end{" + end.value.name + "}", + endNameToken); + } + result.position = end.position; + return result; + } else if (utils.contains(sizeFuncs, func)) { + // If we see a sizing function, parse out the implict body + body = this.parseExpression(false); + return new ParseNode("sizing", { + // Figure out what size to use based on the list of functions above + size: "size" + (utils.indexOf(sizeFuncs, func) + 1), + value: body, + }, this.mode); + } else if (utils.contains(styleFuncs, func)) { + // If we see a styling function, parse out the implict body + body = this.parseExpression(true); + return new ParseNode("styling", { + // Figure out what style to use by pulling out the style from + // the function name + style: func.slice(1, func.length - 5), + value: body, + }, this.mode); + } else { + // Defer to parseFunction if it's not a function we handle + return this.parseFunction(start); + } +}; + +/** + * Parses an entire function, including its base and all of its arguments. + * The base might either have been parsed already, in which case + * it is provided as an argument, or it's the next group in the input. + * + * @param {ParseFuncOrArgument=} baseGroup optional as described above + * @return {?ParseNode} + */ +Parser.prototype.parseFunction = function(baseGroup) { + if (!baseGroup) { + baseGroup = this.parseGroup(); + } + + if (baseGroup) { + if (baseGroup.isFunction) { + var func = baseGroup.result; + var funcData = functions[func]; + if (this.mode === "text" && !funcData.allowedInText) { + throw new ParseError( + "Can't use function '" + func + "' in text mode", + baseGroup.token); + } + + var args = this.parseArguments(func, funcData); + var token = baseGroup.token; + var result = this.callFunction(func, args, args.pop(), token); + return new ParseNode(result.type, result, this.mode); + } else { + return baseGroup.result; + } + } else { + return null; + } +}; + +/** + * Call a function handler with a suitable context and arguments. + */ +Parser.prototype.callFunction = function(name, args, positions, token) { + var context = { + funcName: name, + parser: this, + positions: positions, + token: token, + }; + return functions[name].handler(context, args); +}; + +/** + * Parses the arguments of a function or environment + * + * @param {string} func "\name" or "\begin{name}" + * @param {{numArgs:number,numOptionalArgs:number|undefined}} funcData + * @return the array of arguments, with the list of positions as last element + */ +Parser.prototype.parseArguments = function(func, funcData) { + var totalArgs = funcData.numArgs + funcData.numOptionalArgs; + if (totalArgs === 0) { + return [[this.pos]]; + } + + var baseGreediness = funcData.greediness; + var positions = [this.pos]; + var args = []; + + for (var i = 0; i < totalArgs; i++) { + var nextToken = this.nextToken; + var argType = funcData.argTypes && funcData.argTypes[i]; + var arg; + if (i < funcData.numOptionalArgs) { + if (argType) { + arg = this.parseGroupOfType(argType, true); + } else { + arg = this.parseGroup(true); + } + if (!arg) { + args.push(null); + positions.push(this.pos); + continue; + } + } else { + if (argType) { + arg = this.parseGroupOfType(argType); + } else { + arg = this.parseGroup(); + } + if (!arg) { + if (!this.settings.throwOnError && + this.nextToken.text[0] === "\\") { + arg = new ParseFuncOrArgument( + this.handleUnsupportedCmd(this.nextToken.text), + false); + } else { + throw new ParseError( + "Expected group after '" + func + "'", nextToken); + } + } + } + var argNode; + if (arg.isFunction) { + var argGreediness = + functions[arg.result].greediness; + if (argGreediness > baseGreediness) { + argNode = this.parseFunction(arg); + } else { + throw new ParseError( + "Got function '" + arg.result + "' as " + + "argument to '" + func + "'", nextToken); + } + } else { + argNode = arg.result; + } + args.push(argNode); + positions.push(this.pos); + } + + args.push(positions); + + return args; +}; + + +/** + * Parses a group when the mode is changing. + * + * @return {?ParseFuncOrArgument} + */ +Parser.prototype.parseGroupOfType = function(innerMode, optional) { + var outerMode = this.mode; + // Handle `original` argTypes + if (innerMode === "original") { + innerMode = outerMode; + } + + if (innerMode === "color") { + return this.parseColorGroup(optional); + } + if (innerMode === "size") { + return this.parseSizeGroup(optional); + } + + this.switchMode(innerMode); + if (innerMode === "text") { + // text mode is special because it should ignore the whitespace before + // it + while (this.nextToken.text === " ") { + this.consume(); + } + } + // By the time we get here, innerMode is one of "text" or "math". + // We switch the mode of the parser, recurse, then restore the old mode. + var res = this.parseGroup(optional); + this.switchMode(outerMode); + return res; +}; + +/** + * Parses a group, essentially returning the string formed by the + * brace-enclosed tokens plus some position information. + * + * @param {string} modeName Used to describe the mode in error messages + * @param {boolean=} optional Whether the group is optional or required + */ +Parser.prototype.parseStringGroup = function(modeName, optional) { + if (optional && this.nextToken.text !== "[") { + return null; + } + var outerMode = this.mode; + this.mode = "text"; + this.expect(optional ? "[" : "{"); + var str = ""; + var firstToken = this.nextToken; + var lastToken = firstToken; + while (this.nextToken.text !== (optional ? "]" : "}")) { + if (this.nextToken.text === "EOF") { + throw new ParseError( + "Unexpected end of input in " + modeName, + firstToken.range(this.nextToken, str)); + } + lastToken = this.nextToken; + str += lastToken.text; + this.consume(); + } + this.mode = outerMode; + this.expect(optional ? "]" : "}"); + return firstToken.range(lastToken, str); +}; + +/** + * Parses a color description. + */ +Parser.prototype.parseColorGroup = function(optional) { + var res = this.parseStringGroup("color", optional); + if (!res) { + return null; + } + var match = (/^(#[a-z0-9]+|[a-z]+)$/i).exec(res.text); + if (!match) { + throw new ParseError("Invalid color: '" + res.text + "'", res); + } + return new ParseFuncOrArgument( + new ParseNode("color", match[0], this.mode), + false); +}; + +/** + * Parses a size specification, consisting of magnitude and unit. + */ +Parser.prototype.parseSizeGroup = function(optional) { + var res = this.parseStringGroup("size", optional); + if (!res) { + return null; + } + var match = (/(-?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/).exec(res.text); + if (!match) { + throw new ParseError("Invalid size: '" + res.text + "'", res); + } + var data = { + number: +(match[1] + match[2]), // sign + magnitude, cast to number + unit: match[3], + }; + if (data.unit !== "em" && data.unit !== "ex") { + throw new ParseError("Invalid unit: '" + data.unit + "'", res); + } + return new ParseFuncOrArgument( + new ParseNode("color", data, this.mode), + false); +}; + +/** + * If the argument is false or absent, this parses an ordinary group, + * which is either a single nucleus (like "x") or an expression + * in braces (like "{x+y}"). + * If the argument is true, it parses either a bracket-delimited expression + * (like "[x+y]") or returns null to indicate the absence of a + * bracket-enclosed group. + * + * @param {boolean=} optional Whether the group is optional or required + * @return {?ParseFuncOrArgument} + */ +Parser.prototype.parseGroup = function(optional) { + var firstToken = this.nextToken; + // Try to parse an open brace + if (this.nextToken.text === (optional ? "[" : "{")) { + // If we get a brace, parse an expression + this.consume(); + var expression = this.parseExpression(false, optional ? "]" : null); + var lastToken = this.nextToken; + // Make sure we get a close brace + this.expect(optional ? "]" : "}"); + if (this.mode === "text") { + this.formLigatures(expression); + } + return new ParseFuncOrArgument( + new ParseNode("ordgroup", expression, this.mode, + firstToken, lastToken), + false); + } else { + // Otherwise, just return a nucleus, or nothing for an optional group + return optional ? null : this.parseSymbol(); + } +}; + +/** + * Form ligature-like combinations of characters for text mode. + * This includes inputs like "--", "---", "``" and "''". + * The result will simply replace multiple textord nodes with a single + * character in each value by a single textord node having multiple + * characters in its value. The representation is still ASCII source. + * + * @param {Array.<ParseNode>} group the nodes of this group, + * list will be moified in place + */ +Parser.prototype.formLigatures = function(group) { + var i; + var n = group.length - 1; + for (i = 0; i < n; ++i) { + var a = group[i]; + var v = a.value; + if (v === "-" && group[i + 1].value === "-") { + if (i + 1 < n && group[i + 2].value === "-") { + group.splice(i, 3, new ParseNode( + "textord", "---", "text", a, group[i + 2])); + n -= 2; + } else { + group.splice(i, 2, new ParseNode( + "textord", "--", "text", a, group[i + 1])); + n -= 1; + } + } + if ((v === "'" || v === "`") && group[i + 1].value === v) { + group.splice(i, 2, new ParseNode( + "textord", v + v, "text", a, group[i + 1])); + n -= 1; + } + } +}; + +/** + * Parse a single symbol out of the string. Here, we handle both the functions + * we have defined, as well as the single character symbols + * + * @return {?ParseFuncOrArgument} + */ +Parser.prototype.parseSymbol = function() { + var nucleus = this.nextToken; + + if (functions[nucleus.text]) { + this.consume(); + // If there exists a function with this name, we return the function and + // say that it is a function. + return new ParseFuncOrArgument( + nucleus.text, + true, nucleus); + } else if (symbols[this.mode][nucleus.text]) { + this.consume(); + // Otherwise if this is a no-argument function, find the type it + // corresponds to in the symbols map + return new ParseFuncOrArgument( + new ParseNode(symbols[this.mode][nucleus.text].group, + nucleus.text, this.mode, nucleus), + false, nucleus); + } else if (this.mode === "text" && cjkRegex.test(nucleus.text)) { + this.consume(); + return new ParseFuncOrArgument( + new ParseNode("textord", nucleus.text, this.mode, nucleus), + false, nucleus); + } else { + return null; + } +}; + +Parser.prototype.ParseNode = ParseNode; + +module.exports = Parser; + +},{"./MacroExpander":4,"./ParseError":6,"./environments":16,"./functions":19,"./parseData":21,"./symbols":23,"./unicodeRegexes":24,"./utils":25}],8:[function(require,module,exports){ +/** + * This is a module for storing settings passed into KaTeX. It correctly handles + * default settings. + */ + +/** + * Helper function for getting a default value if the value is undefined + */ +function get(option, defaultValue) { + return option === undefined ? defaultValue : option; +} + +/** + * The main Settings object + * + * The current options stored are: + * - displayMode: Whether the expression should be typeset by default in + * textstyle or displaystyle (default false) + */ +function Settings(options) { + // allow null options + options = options || {}; + this.displayMode = get(options.displayMode, false); + this.throwOnError = get(options.throwOnError, true); + this.errorColor = get(options.errorColor, "#cc0000"); + this.macros = options.macros || {}; +} + +module.exports = Settings; + +},{}],9:[function(require,module,exports){ +/** + * This file contains information and classes for the various kinds of styles + * used in TeX. It provides a generic `Style` class, which holds information + * about a specific style. It then provides instances of all the different kinds + * of styles possible, and provides functions to move between them and get + * information about them. + */ + +/** + * The main style class. Contains a unique id for the style, a size (which is + * the same for cramped and uncramped version of a style), a cramped flag, and a + * size multiplier, which gives the size difference between a style and + * textstyle. + */ +function Style(id, size, multiplier, cramped) { + this.id = id; + this.size = size; + this.cramped = cramped; + this.sizeMultiplier = multiplier; +} + +/** + * Get the style of a superscript given a base in the current style. + */ +Style.prototype.sup = function() { + return styles[sup[this.id]]; +}; + +/** + * Get the style of a subscript given a base in the current style. + */ +Style.prototype.sub = function() { + return styles[sub[this.id]]; +}; + +/** + * Get the style of a fraction numerator given the fraction in the current + * style. + */ +Style.prototype.fracNum = function() { + return styles[fracNum[this.id]]; +}; + +/** + * Get the style of a fraction denominator given the fraction in the current + * style. + */ +Style.prototype.fracDen = function() { + return styles[fracDen[this.id]]; +}; + +/** + * Get the cramped version of a style (in particular, cramping a cramped style + * doesn't change the style). + */ +Style.prototype.cramp = function() { + return styles[cramp[this.id]]; +}; + +/** + * HTML class name, like "displaystyle cramped" + */ +Style.prototype.cls = function() { + return sizeNames[this.size] + (this.cramped ? " cramped" : " uncramped"); +}; + +/** + * HTML Reset class name, like "reset-textstyle" + */ +Style.prototype.reset = function() { + return resetNames[this.size]; +}; + +// IDs of the different styles +var D = 0; +var Dc = 1; +var T = 2; +var Tc = 3; +var S = 4; +var Sc = 5; +var SS = 6; +var SSc = 7; + +// String names for the different sizes +var sizeNames = [ + "displaystyle textstyle", + "textstyle", + "scriptstyle", + "scriptscriptstyle", +]; + +// Reset names for the different sizes +var resetNames = [ + "reset-textstyle", + "reset-textstyle", + "reset-scriptstyle", + "reset-scriptscriptstyle", +]; + +// Instances of the different styles +var styles = [ + new Style(D, 0, 1.0, false), + new Style(Dc, 0, 1.0, true), + new Style(T, 1, 1.0, false), + new Style(Tc, 1, 1.0, true), + new Style(S, 2, 0.7, false), + new Style(Sc, 2, 0.7, true), + new Style(SS, 3, 0.5, false), + new Style(SSc, 3, 0.5, true), +]; + +// Lookup tables for switching from one style to another +var sup = [S, Sc, S, Sc, SS, SSc, SS, SSc]; +var sub = [Sc, Sc, Sc, Sc, SSc, SSc, SSc, SSc]; +var fracNum = [T, Tc, S, Sc, SS, SSc, SS, SSc]; +var fracDen = [Tc, Tc, Sc, Sc, SSc, SSc, SSc, SSc]; +var cramp = [Dc, Dc, Tc, Tc, Sc, Sc, SSc, SSc]; + +// We only export some of the styles. Also, we don't export the `Style` class so +// no more styles can be generated. +module.exports = { + DISPLAY: styles[D], + TEXT: styles[T], + SCRIPT: styles[S], + SCRIPTSCRIPT: styles[SS], +}; + +},{}],10:[function(require,module,exports){ +/* eslint no-console:0 */ +/** + * This module contains general functions that can be used for building + * different kinds of domTree nodes in a consistent manner. + */ + +var domTree = require("./domTree"); +var fontMetrics = require("./fontMetrics"); +var symbols = require("./symbols"); +var utils = require("./utils"); + +var greekCapitals = [ + "\\Gamma", + "\\Delta", + "\\Theta", + "\\Lambda", + "\\Xi", + "\\Pi", + "\\Sigma", + "\\Upsilon", + "\\Phi", + "\\Psi", + "\\Omega", +]; + +// The following have to be loaded from Main-Italic font, using class mainit +var mainitLetters = [ + "\u0131", // dotless i, \imath + "\u0237", // dotless j, \jmath + "\u00a3", // \pounds +]; + +/** + * Makes a symbolNode after translation via the list of symbols in symbols.js. + * Correctly pulls out metrics for the character, and optionally takes a list of + * classes to be attached to the node. + */ +var makeSymbol = function(value, style, mode, color, classes) { + // Replace the value with its replaced value from symbol.js + if (symbols[mode][value] && symbols[mode][value].replace) { + value = symbols[mode][value].replace; + } + + var metrics = fontMetrics.getCharacterMetrics(value, style); + + var symbolNode; + if (metrics) { + symbolNode = new domTree.symbolNode( + value, metrics.height, metrics.depth, metrics.italic, metrics.skew, + classes); + } else { + // TODO(emily): Figure out a good way to only print this in development + typeof console !== "undefined" && console.warn( + "No character metrics for '" + value + "' in style '" + + style + "'"); + symbolNode = new domTree.symbolNode(value, 0, 0, 0, 0, classes); + } + + if (color) { + symbolNode.style.color = color; + } + + return symbolNode; +}; + +/** + * Makes a symbol in Main-Regular or AMS-Regular. + * Used for rel, bin, open, close, inner, and punct. + */ +var mathsym = function(value, mode, color, classes) { + // Decide what font to render the symbol in by its entry in the symbols + // table. + // Have a special case for when the value = \ because the \ is used as a + // textord in unsupported command errors but cannot be parsed as a regular + // text ordinal and is therefore not present as a symbol in the symbols + // table for text + if (value === "\\" || symbols[mode][value].font === "main") { + return makeSymbol(value, "Main-Regular", mode, color, classes); + } else { + return makeSymbol( + value, "AMS-Regular", mode, color, classes.concat(["amsrm"])); + } +}; + +/** + * Makes a symbol in the default font for mathords and textords. + */ +var mathDefault = function(value, mode, color, classes, type) { + if (type === "mathord") { + return mathit(value, mode, color, classes); + } else if (type === "textord") { + return makeSymbol( + value, "Main-Regular", mode, color, classes.concat(["mathrm"])); + } else { + throw new Error("unexpected type: " + type + " in mathDefault"); + } +}; + +/** + * Makes a symbol in the italic math font. + */ +var mathit = function(value, mode, color, classes) { + if (/[0-9]/.test(value.charAt(0)) || + // glyphs for \imath and \jmath do not exist in Math-Italic so we + // need to use Main-Italic instead + utils.contains(mainitLetters, value) || + utils.contains(greekCapitals, value)) { + return makeSymbol( + value, "Main-Italic", mode, color, classes.concat(["mainit"])); + } else { + return makeSymbol( + value, "Math-Italic", mode, color, classes.concat(["mathit"])); + } +}; + +/** + * Makes either a mathord or textord in the correct font and color. + */ +var makeOrd = function(group, options, type) { + var mode = group.mode; + var value = group.value; + if (symbols[mode][value] && symbols[mode][value].replace) { + value = symbols[mode][value].replace; + } + + var classes = ["mord"]; + var color = options.getColor(); + + var font = options.font; + if (font) { + if (font === "mathit" || utils.contains(mainitLetters, value)) { + return mathit(value, mode, color, classes); + } else { + var fontName = fontMap[font].fontName; + if (fontMetrics.getCharacterMetrics(value, fontName)) { + return makeSymbol( + value, fontName, mode, color, classes.concat([font])); + } else { + return mathDefault(value, mode, color, classes, type); + } + } + } else { + return mathDefault(value, mode, color, classes, type); + } +}; + +/** + * Calculate the height, depth, and maxFontSize of an element based on its + * children. + */ +var sizeElementFromChildren = function(elem) { + var height = 0; + var depth = 0; + var maxFontSize = 0; + + if (elem.children) { + for (var i = 0; i < elem.children.length; i++) { + if (elem.children[i].height > height) { + height = elem.children[i].height; + } + if (elem.children[i].depth > depth) { + depth = elem.children[i].depth; + } + if (elem.children[i].maxFontSize > maxFontSize) { + maxFontSize = elem.children[i].maxFontSize; + } + } + } + + elem.height = height; + elem.depth = depth; + elem.maxFontSize = maxFontSize; +}; + +/** + * Makes a span with the given list of classes, list of children, and color. + */ +var makeSpan = function(classes, children, color) { + var span = new domTree.span(classes, children); + + sizeElementFromChildren(span); + + if (color) { + span.style.color = color; + } + + return span; +}; + +/** + * Makes a document fragment with the given list of children. + */ +var makeFragment = function(children) { + var fragment = new domTree.documentFragment(children); + + sizeElementFromChildren(fragment); + + return fragment; +}; + +/** + * Makes an element placed in each of the vlist elements to ensure that each + * element has the same max font size. To do this, we create a zero-width space + * with the correct font size. + */ +var makeFontSizer = function(options, fontSize) { + var fontSizeInner = makeSpan([], [new domTree.symbolNode("\u200b")]); + fontSizeInner.style.fontSize = + (fontSize / options.style.sizeMultiplier) + "em"; + + var fontSizer = makeSpan( + ["fontsize-ensurer", "reset-" + options.size, "size5"], + [fontSizeInner]); + + return fontSizer; +}; + +/** + * Makes a vertical list by stacking elements and kerns on top of each other. + * Allows for many different ways of specifying the positioning method. + * + * Arguments: + * - children: A list of child or kern nodes to be stacked on top of each other + * (i.e. the first element will be at the bottom, and the last at + * the top). Element nodes are specified as + * {type: "elem", elem: node} + * while kern nodes are specified as + * {type: "kern", size: size} + * - positionType: The method by which the vlist should be positioned. Valid + * values are: + * - "individualShift": The children list only contains elem + * nodes, and each node contains an extra + * "shift" value of how much it should be + * shifted (note that shifting is always + * moving downwards). positionData is + * ignored. + * - "top": The positionData specifies the topmost point of + * the vlist (note this is expected to be a height, + * so positive values move up) + * - "bottom": The positionData specifies the bottommost point + * of the vlist (note this is expected to be a + * depth, so positive values move down + * - "shift": The vlist will be positioned such that its + * baseline is positionData away from the baseline + * of the first child. Positive values move + * downwards. + * - "firstBaseline": The vlist will be positioned such that + * its baseline is aligned with the + * baseline of the first child. + * positionData is ignored. (this is + * equivalent to "shift" with + * positionData=0) + * - positionData: Data used in different ways depending on positionType + * - options: An Options object + * + */ +var makeVList = function(children, positionType, positionData, options) { + var depth; + var currPos; + var i; + if (positionType === "individualShift") { + var oldChildren = children; + children = [oldChildren[0]]; + + // Add in kerns to the list of children to get each element to be + // shifted to the correct specified shift + depth = -oldChildren[0].shift - oldChildren[0].elem.depth; + currPos = depth; + for (i = 1; i < oldChildren.length; i++) { + var diff = -oldChildren[i].shift - currPos - + oldChildren[i].elem.depth; + var size = diff - + (oldChildren[i - 1].elem.height + + oldChildren[i - 1].elem.depth); + + currPos = currPos + diff; + + children.push({type: "kern", size: size}); + children.push(oldChildren[i]); + } + } else if (positionType === "top") { + // We always start at the bottom, so calculate the bottom by adding up + // all the sizes + var bottom = positionData; + for (i = 0; i < children.length; i++) { + if (children[i].type === "kern") { + bottom -= children[i].size; + } else { + bottom -= children[i].elem.height + children[i].elem.depth; + } + } + depth = bottom; + } else if (positionType === "bottom") { + depth = -positionData; + } else if (positionType === "shift") { + depth = -children[0].elem.depth - positionData; + } else if (positionType === "firstBaseline") { + depth = -children[0].elem.depth; + } else { + depth = 0; + } + + // Make the fontSizer + var maxFontSize = 0; + for (i = 0; i < children.length; i++) { + if (children[i].type === "elem") { + maxFontSize = Math.max(maxFontSize, children[i].elem.maxFontSize); + } + } + var fontSizer = makeFontSizer(options, maxFontSize); + + // Create a new list of actual children at the correct offsets + var realChildren = []; + currPos = depth; + for (i = 0; i < children.length; i++) { + if (children[i].type === "kern") { + currPos += children[i].size; + } else { + var child = children[i].elem; + + var shift = -child.depth - currPos; + currPos += child.height + child.depth; + + var childWrap = makeSpan([], [fontSizer, child]); + childWrap.height -= shift; + childWrap.depth += shift; + childWrap.style.top = shift + "em"; + + realChildren.push(childWrap); + } + } + + // Add in an element at the end with no offset to fix the calculation of + // baselines in some browsers (namely IE, sometimes safari) + var baselineFix = makeSpan( + ["baseline-fix"], [fontSizer, new domTree.symbolNode("\u200b")]); + realChildren.push(baselineFix); + + var vlist = makeSpan(["vlist"], realChildren); + // Fix the final height and depth, in case there were kerns at the ends + // since the makeSpan calculation won't take that in to account. + vlist.height = Math.max(currPos, vlist.height); + vlist.depth = Math.max(-depth, vlist.depth); + return vlist; +}; + +// A table of size -> font size for the different sizing functions +var sizingMultiplier = { + size1: 0.5, + size2: 0.7, + size3: 0.8, + size4: 0.9, + size5: 1.0, + size6: 1.2, + size7: 1.44, + size8: 1.73, + size9: 2.07, + size10: 2.49, +}; + +// A map of spacing functions to their attributes, like size and corresponding +// CSS class +var spacingFunctions = { + "\\qquad": { + size: "2em", + className: "qquad", + }, + "\\quad": { + size: "1em", + className: "quad", + }, + "\\enspace": { + size: "0.5em", + className: "enspace", + }, + "\\;": { + size: "0.277778em", + className: "thickspace", + }, + "\\:": { + size: "0.22222em", + className: "mediumspace", + }, + "\\,": { + size: "0.16667em", + className: "thinspace", + }, + "\\!": { + size: "-0.16667em", + className: "negativethinspace", + }, +}; + +/** + * Maps TeX font commands to objects containing: + * - variant: string used for "mathvariant" attribute in buildMathML.js + * - fontName: the "style" parameter to fontMetrics.getCharacterMetrics + */ +// A map between tex font commands an MathML mathvariant attribute values +var fontMap = { + // styles + "mathbf": { + variant: "bold", + fontName: "Main-Bold", + }, + "mathrm": { + variant: "normal", + fontName: "Main-Regular", + }, + + // "mathit" is missing because it requires the use of two fonts: Main-Italic + // and Math-Italic. This is handled by a special case in makeOrd which ends + // up calling mathit. + + // families + "mathbb": { + variant: "double-struck", + fontName: "AMS-Regular", + }, + "mathcal": { + variant: "script", + fontName: "Caligraphic-Regular", + }, + "mathfrak": { + variant: "fraktur", + fontName: "Fraktur-Regular", + }, + "mathscr": { + variant: "script", + fontName: "Script-Regular", + }, + "mathsf": { + variant: "sans-serif", + fontName: "SansSerif-Regular", + }, + "mathtt": { + variant: "monospace", + fontName: "Typewriter-Regular", + }, +}; + +module.exports = { + fontMap: fontMap, + makeSymbol: makeSymbol, + mathsym: mathsym, + makeSpan: makeSpan, + makeFragment: makeFragment, + makeVList: makeVList, + makeOrd: makeOrd, + sizingMultiplier: sizingMultiplier, + spacingFunctions: spacingFunctions, +}; + +},{"./domTree":15,"./fontMetrics":17,"./symbols":23,"./utils":25}],11:[function(require,module,exports){ +/* eslint no-console:0 */ +/** + * This file does the main work of building a domTree structure from a parse + * tree. The entry point is the `buildHTML` function, which takes a parse tree. + * Then, the buildExpression, buildGroup, and various groupTypes functions are + * called, to produce a final HTML tree. + */ + +var ParseError = require("./ParseError"); +var Style = require("./Style"); + +var buildCommon = require("./buildCommon"); +var delimiter = require("./delimiter"); +var domTree = require("./domTree"); +var fontMetrics = require("./fontMetrics"); +var utils = require("./utils"); + +var makeSpan = buildCommon.makeSpan; + +/** + * Take a list of nodes, build them in order, and return a list of the built + * nodes. This function handles the `prev` node correctly, and passes the + * previous element from the list as the prev of the next element. + */ +var buildExpression = function(expression, options, prev) { + var groups = []; + for (var i = 0; i < expression.length; i++) { + var group = expression[i]; + groups.push(buildGroup(group, options, prev)); + prev = group; + } + return groups; +}; + +// List of types used by getTypeOfGroup, +// see https://github.com/Khan/KaTeX/wiki/Examining-TeX#group-types +var groupToType = { + mathord: "mord", + textord: "mord", + bin: "mbin", + rel: "mrel", + text: "mord", + open: "mopen", + close: "mclose", + inner: "minner", + genfrac: "mord", + array: "mord", + spacing: "mord", + punct: "mpunct", + ordgroup: "mord", + op: "mop", + katex: "mord", + overline: "mord", + underline: "mord", + rule: "mord", + leftright: "minner", + sqrt: "mord", + accent: "mord", +}; + +/** + * Gets the final math type of an expression, given its group type. This type is + * used to determine spacing between elements, and affects bin elements by + * causing them to change depending on what types are around them. This type + * must be attached to the outermost node of an element as a CSS class so that + * spacing with its surrounding elements works correctly. + * + * Some elements can be mapped one-to-one from group type to math type, and + * those are listed in the `groupToType` table. + * + * Others (usually elements that wrap around other elements) often have + * recursive definitions, and thus call `getTypeOfGroup` on their inner + * elements. + */ +var getTypeOfGroup = function(group) { + if (group == null) { + // Like when typesetting $^3$ + return groupToType.mathord; + } else if (group.type === "supsub") { + return getTypeOfGroup(group.value.base); + } else if (group.type === "llap" || group.type === "rlap") { + return getTypeOfGroup(group.value); + } else if (group.type === "color") { + return getTypeOfGroup(group.value.value); + } else if (group.type === "sizing") { + return getTypeOfGroup(group.value.value); + } else if (group.type === "styling") { + return getTypeOfGroup(group.value.value); + } else if (group.type === "delimsizing") { + return groupToType[group.value.delimType]; + } else { + return groupToType[group.type]; + } +}; + +/** + * Sometimes, groups perform special rules when they have superscripts or + * subscripts attached to them. This function lets the `supsub` group know that + * its inner element should handle the superscripts and subscripts instead of + * handling them itself. + */ +var shouldHandleSupSub = function(group, options) { + if (!group) { + return false; + } else if (group.type === "op") { + // Operators handle supsubs differently when they have limits + // (e.g. `\displaystyle\sum_2^3`) + return group.value.limits && + (options.style.size === Style.DISPLAY.size || + group.value.alwaysHandleSupSub); + } else if (group.type === "accent") { + return isCharacterBox(group.value.base); + } else { + return null; + } +}; + +/** + * Sometimes we want to pull out the innermost element of a group. In most + * cases, this will just be the group itself, but when ordgroups and colors have + * a single element, we want to pull that out. + */ +var getBaseElem = function(group) { + if (!group) { + return false; + } else if (group.type === "ordgroup") { + if (group.value.length === 1) { + return getBaseElem(group.value[0]); + } else { + return group; + } + } else if (group.type === "color") { + if (group.value.value.length === 1) { + return getBaseElem(group.value.value[0]); + } else { + return group; + } + } else if (group.type === "font") { + return getBaseElem(group.value.body); + } else { + return group; + } +}; + +/** + * TeXbook algorithms often reference "character boxes", which are simply groups + * with a single character in them. To decide if something is a character box, + * we find its innermost group, and see if it is a single character. + */ +var isCharacterBox = function(group) { + var baseElem = getBaseElem(group); + + // These are all they types of groups which hold single characters + return baseElem.type === "mathord" || + baseElem.type === "textord" || + baseElem.type === "bin" || + baseElem.type === "rel" || + baseElem.type === "inner" || + baseElem.type === "open" || + baseElem.type === "close" || + baseElem.type === "punct"; +}; + +var makeNullDelimiter = function(options) { + return makeSpan([ + "sizing", "reset-" + options.size, "size5", + options.style.reset(), Style.TEXT.cls(), + "nulldelimiter", + ]); +}; + +/** + * This is a map of group types to the function used to handle that type. + * Simpler types come at the beginning, while complicated types come afterwards. + */ +var groupTypes = {}; + +groupTypes.mathord = function(group, options, prev) { + return buildCommon.makeOrd(group, options, "mathord"); +}; + +groupTypes.textord = function(group, options, prev) { + return buildCommon.makeOrd(group, options, "textord"); +}; + +groupTypes.bin = function(group, options, prev) { + var className = "mbin"; + // Pull out the most recent element. Do some special handling to find + // things at the end of a \color group. Note that we don't use the same + // logic for ordgroups (which count as ords). + var prevAtom = prev; + while (prevAtom && prevAtom.type === "color") { + var atoms = prevAtom.value.value; + prevAtom = atoms[atoms.length - 1]; + } + // See TeXbook pg. 442-446, Rules 5 and 6, and the text before Rule 19. + // Here, we determine whether the bin should turn into an ord. We + // currently only apply Rule 5. + if (!prev || utils.contains(["mbin", "mopen", "mrel", "mop", "mpunct"], + getTypeOfGroup(prevAtom))) { + group.type = "textord"; + className = "mord"; + } + + return buildCommon.mathsym( + group.value, group.mode, options.getColor(), [className]); +}; + +groupTypes.rel = function(group, options, prev) { + return buildCommon.mathsym( + group.value, group.mode, options.getColor(), ["mrel"]); +}; + +groupTypes.open = function(group, options, prev) { + return buildCommon.mathsym( + group.value, group.mode, options.getColor(), ["mopen"]); +}; + +groupTypes.close = function(group, options, prev) { + return buildCommon.mathsym( + group.value, group.mode, options.getColor(), ["mclose"]); +}; + +groupTypes.inner = function(group, options, prev) { + return buildCommon.mathsym( + group.value, group.mode, options.getColor(), ["minner"]); +}; + +groupTypes.punct = function(group, options, prev) { + return buildCommon.mathsym( + group.value, group.mode, options.getColor(), ["mpunct"]); +}; + +groupTypes.ordgroup = function(group, options, prev) { + return makeSpan( + ["mord", options.style.cls()], + buildExpression(group.value, options.reset()) + ); +}; + +groupTypes.text = function(group, options, prev) { + return makeSpan(["text", "mord", options.style.cls()], + buildExpression(group.value.body, options.reset())); +}; + +groupTypes.color = function(group, options, prev) { + var elements = buildExpression( + group.value.value, + options.withColor(group.value.color), + prev + ); + + // \color isn't supposed to affect the type of the elements it contains. + // To accomplish this, we wrap the results in a fragment, so the inner + // elements will be able to directly interact with their neighbors. For + // example, `\color{red}{2 +} 3` has the same spacing as `2 + 3` + return new buildCommon.makeFragment(elements); +}; + +groupTypes.supsub = function(group, options, prev) { + // Superscript and subscripts are handled in the TeXbook on page + // 445-446, rules 18(a-f). + + // Here is where we defer to the inner group if it should handle + // superscripts and subscripts itself. + if (shouldHandleSupSub(group.value.base, options)) { + return groupTypes[group.value.base.type](group, options, prev); + } + + var base = buildGroup(group.value.base, options.reset()); + var supmid; + var submid; + var sup; + var sub; + + if (group.value.sup) { + sup = buildGroup(group.value.sup, + options.withStyle(options.style.sup())); + supmid = makeSpan( + [options.style.reset(), options.style.sup().cls()], [sup]); + } + + if (group.value.sub) { + sub = buildGroup(group.value.sub, + options.withStyle(options.style.sub())); + submid = makeSpan( + [options.style.reset(), options.style.sub().cls()], [sub]); + } + + // Rule 18a + var supShift; + var subShift; + if (isCharacterBox(group.value.base)) { + supShift = 0; + subShift = 0; + } else { + supShift = base.height - fontMetrics.metrics.supDrop; + subShift = base.depth + fontMetrics.metrics.subDrop; + } + + // Rule 18c + var minSupShift; + if (options.style === Style.DISPLAY) { + minSupShift = fontMetrics.metrics.sup1; + } else if (options.style.cramped) { + minSupShift = fontMetrics.metrics.sup3; + } else { + minSupShift = fontMetrics.metrics.sup2; + } + + // scriptspace is a font-size-independent size, so scale it + // appropriately + var multiplier = Style.TEXT.sizeMultiplier * + options.style.sizeMultiplier; + var scriptspace = + (0.5 / fontMetrics.metrics.ptPerEm) / multiplier + "em"; + + var supsub; + if (!group.value.sup) { + // Rule 18b + subShift = Math.max( + subShift, fontMetrics.metrics.sub1, + sub.height - 0.8 * fontMetrics.metrics.xHeight); + + supsub = buildCommon.makeVList([ + {type: "elem", elem: submid}, + ], "shift", subShift, options); + + supsub.children[0].style.marginRight = scriptspace; + + // Subscripts shouldn't be shifted by the base's italic correction. + // Account for that by shifting the subscript back the appropriate + // amount. Note we only do this when the base is a single symbol. + if (base instanceof domTree.symbolNode) { + supsub.children[0].style.marginLeft = -base.italic + "em"; + } + } else if (!group.value.sub) { + // Rule 18c, d + supShift = Math.max(supShift, minSupShift, + sup.depth + 0.25 * fontMetrics.metrics.xHeight); + + supsub = buildCommon.makeVList([ + {type: "elem", elem: supmid}, + ], "shift", -supShift, options); + + supsub.children[0].style.marginRight = scriptspace; + } else { + supShift = Math.max( + supShift, minSupShift, + sup.depth + 0.25 * fontMetrics.metrics.xHeight); + subShift = Math.max(subShift, fontMetrics.metrics.sub2); + + var ruleWidth = fontMetrics.metrics.defaultRuleThickness; + + // Rule 18e + if ((supShift - sup.depth) - (sub.height - subShift) < + 4 * ruleWidth) { + subShift = 4 * ruleWidth - (supShift - sup.depth) + sub.height; + var psi = 0.8 * fontMetrics.metrics.xHeight - + (supShift - sup.depth); + if (psi > 0) { + supShift += psi; + subShift -= psi; + } + } + + supsub = buildCommon.makeVList([ + {type: "elem", elem: submid, shift: subShift}, + {type: "elem", elem: supmid, shift: -supShift}, + ], "individualShift", null, options); + + // See comment above about subscripts not being shifted + if (base instanceof domTree.symbolNode) { + supsub.children[0].style.marginLeft = -base.italic + "em"; + } + + supsub.children[0].style.marginRight = scriptspace; + supsub.children[1].style.marginRight = scriptspace; + } + + // We ensure to wrap the supsub vlist in a span.msupsub to reset text-align + return makeSpan([getTypeOfGroup(group.value.base)], + [base, makeSpan(["msupsub"], [supsub])]); +}; + +groupTypes.genfrac = function(group, options, prev) { + // Fractions are handled in the TeXbook on pages 444-445, rules 15(a-e). + // Figure out what style this fraction should be in based on the + // function used + var fstyle = options.style; + if (group.value.size === "display") { + fstyle = Style.DISPLAY; + } else if (group.value.size === "text") { + fstyle = Style.TEXT; + } + + var nstyle = fstyle.fracNum(); + var dstyle = fstyle.fracDen(); + + var numer = buildGroup(group.value.numer, options.withStyle(nstyle)); + var numerreset = makeSpan([fstyle.reset(), nstyle.cls()], [numer]); + + var denom = buildGroup(group.value.denom, options.withStyle(dstyle)); + var denomreset = makeSpan([fstyle.reset(), dstyle.cls()], [denom]); + + var ruleWidth; + if (group.value.hasBarLine) { + ruleWidth = fontMetrics.metrics.defaultRuleThickness / + options.style.sizeMultiplier; + } else { + ruleWidth = 0; + } + + // Rule 15b + var numShift; + var clearance; + var denomShift; + if (fstyle.size === Style.DISPLAY.size) { + numShift = fontMetrics.metrics.num1; + if (ruleWidth > 0) { + clearance = 3 * ruleWidth; + } else { + clearance = 7 * fontMetrics.metrics.defaultRuleThickness; + } + denomShift = fontMetrics.metrics.denom1; + } else { + if (ruleWidth > 0) { + numShift = fontMetrics.metrics.num2; + clearance = ruleWidth; + } else { + numShift = fontMetrics.metrics.num3; + clearance = 3 * fontMetrics.metrics.defaultRuleThickness; + } + denomShift = fontMetrics.metrics.denom2; + } + + var frac; + if (ruleWidth === 0) { + // Rule 15c + var candiateClearance = + (numShift - numer.depth) - (denom.height - denomShift); + if (candiateClearance < clearance) { + numShift += 0.5 * (clearance - candiateClearance); + denomShift += 0.5 * (clearance - candiateClearance); + } + + frac = buildCommon.makeVList([ + {type: "elem", elem: denomreset, shift: denomShift}, + {type: "elem", elem: numerreset, shift: -numShift}, + ], "individualShift", null, options); + } else { + // Rule 15d + var axisHeight = fontMetrics.metrics.axisHeight; + + if ((numShift - numer.depth) - (axisHeight + 0.5 * ruleWidth) < + clearance) { + numShift += + clearance - ((numShift - numer.depth) - + (axisHeight + 0.5 * ruleWidth)); + } + + if ((axisHeight - 0.5 * ruleWidth) - (denom.height - denomShift) < + clearance) { + denomShift += + clearance - ((axisHeight - 0.5 * ruleWidth) - + (denom.height - denomShift)); + } + + var mid = makeSpan( + [options.style.reset(), Style.TEXT.cls(), "frac-line"]); + // Manually set the height of the line because its height is + // created in CSS + mid.height = ruleWidth; + + var midShift = -(axisHeight - 0.5 * ruleWidth); + + frac = buildCommon.makeVList([ + {type: "elem", elem: denomreset, shift: denomShift}, + {type: "elem", elem: mid, shift: midShift}, + {type: "elem", elem: numerreset, shift: -numShift}, + ], "individualShift", null, options); + } + + // Since we manually change the style sometimes (with \dfrac or \tfrac), + // account for the possible size change here. + frac.height *= fstyle.sizeMultiplier / options.style.sizeMultiplier; + frac.depth *= fstyle.sizeMultiplier / options.style.sizeMultiplier; + + // Rule 15e + var delimSize; + if (fstyle.size === Style.DISPLAY.size) { + delimSize = fontMetrics.metrics.delim1; + } else { + delimSize = fontMetrics.metrics.getDelim2(fstyle); + } + + var leftDelim; + var rightDelim; + if (group.value.leftDelim == null) { + leftDelim = makeNullDelimiter(options); + } else { + leftDelim = delimiter.customSizedDelim( + group.value.leftDelim, delimSize, true, + options.withStyle(fstyle), group.mode); + } + if (group.value.rightDelim == null) { + rightDelim = makeNullDelimiter(options); + } else { + rightDelim = delimiter.customSizedDelim( + group.value.rightDelim, delimSize, true, + options.withStyle(fstyle), group.mode); + } + + return makeSpan( + ["mord", options.style.reset(), fstyle.cls()], + [leftDelim, makeSpan(["mfrac"], [frac]), rightDelim], + options.getColor()); +}; + +groupTypes.array = function(group, options, prev) { + var r; + var c; + var nr = group.value.body.length; + var nc = 0; + var body = new Array(nr); + + // Horizontal spacing + var pt = 1 / fontMetrics.metrics.ptPerEm; + var arraycolsep = 5 * pt; // \arraycolsep in article.cls + + // Vertical spacing + var baselineskip = 12 * pt; // see size10.clo + // Default \arraystretch from lttab.dtx + // TODO(gagern): may get redefined once we have user-defined macros + var arraystretch = utils.deflt(group.value.arraystretch, 1); + var arrayskip = arraystretch * baselineskip; + var arstrutHeight = 0.7 * arrayskip; // \strutbox in ltfsstrc.dtx and + var arstrutDepth = 0.3 * arrayskip; // \@arstrutbox in lttab.dtx + + var totalHeight = 0; + for (r = 0; r < group.value.body.length; ++r) { + var inrow = group.value.body[r]; + var height = arstrutHeight; // \@array adds an \@arstrut + var depth = arstrutDepth; // to each tow (via the template) + + if (nc < inrow.length) { + nc = inrow.length; + } + + var outrow = new Array(inrow.length); + for (c = 0; c < inrow.length; ++c) { + var elt = buildGroup(inrow[c], options); + if (depth < elt.depth) { + depth = elt.depth; + } + if (height < elt.height) { + height = elt.height; + } + outrow[c] = elt; + } + + var gap = 0; + if (group.value.rowGaps[r]) { + gap = group.value.rowGaps[r].value; + switch (gap.unit) { + case "em": + gap = gap.number; + break; + case "ex": + gap = gap.number * fontMetrics.metrics.emPerEx; + break; + default: + console.error("Can't handle unit " + gap.unit); + gap = 0; + } + if (gap > 0) { // \@argarraycr + gap += arstrutDepth; + if (depth < gap) { + depth = gap; // \@xargarraycr + } + gap = 0; + } + } + + outrow.height = height; + outrow.depth = depth; + totalHeight += height; + outrow.pos = totalHeight; + totalHeight += depth + gap; // \@yargarraycr + body[r] = outrow; + } + + var offset = totalHeight / 2 + fontMetrics.metrics.axisHeight; + var colDescriptions = group.value.cols || []; + var cols = []; + var colSep; + var colDescrNum; + for (c = 0, colDescrNum = 0; + // Continue while either there are more columns or more column + // descriptions, so trailing separators don't get lost. + c < nc || colDescrNum < colDescriptions.length; + ++c, ++colDescrNum) { + + var colDescr = colDescriptions[colDescrNum] || {}; + + var firstSeparator = true; + while (colDescr.type === "separator") { + // If there is more than one separator in a row, add a space + // between them. + if (!firstSeparator) { + colSep = makeSpan(["arraycolsep"], []); + colSep.style.width = + fontMetrics.metrics.doubleRuleSep + "em"; + cols.push(colSep); + } + + if (colDescr.separator === "|") { + var separator = makeSpan( + ["vertical-separator"], + []); + separator.style.height = totalHeight + "em"; + separator.style.verticalAlign = + -(totalHeight - offset) + "em"; + + cols.push(separator); + } else { + throw new ParseError( + "Invalid separator type: " + colDescr.separator); + } + + colDescrNum++; + colDescr = colDescriptions[colDescrNum] || {}; + firstSeparator = false; + } + + if (c >= nc) { + continue; + } + + var sepwidth; + if (c > 0 || group.value.hskipBeforeAndAfter) { + sepwidth = utils.deflt(colDescr.pregap, arraycolsep); + if (sepwidth !== 0) { + colSep = makeSpan(["arraycolsep"], []); + colSep.style.width = sepwidth + "em"; + cols.push(colSep); + } + } + + var col = []; + for (r = 0; r < nr; ++r) { + var row = body[r]; + var elem = row[c]; + if (!elem) { + continue; + } + var shift = row.pos - offset; + elem.depth = row.depth; + elem.height = row.height; + col.push({type: "elem", elem: elem, shift: shift}); + } + + col = buildCommon.makeVList(col, "individualShift", null, options); + col = makeSpan( + ["col-align-" + (colDescr.align || "c")], + [col]); + cols.push(col); + + if (c < nc - 1 || group.value.hskipBeforeAndAfter) { + sepwidth = utils.deflt(colDescr.postgap, arraycolsep); + if (sepwidth !== 0) { + colSep = makeSpan(["arraycolsep"], []); + colSep.style.width = sepwidth + "em"; + cols.push(colSep); + } + } + } + body = makeSpan(["mtable"], cols); + return makeSpan(["mord"], [body], options.getColor()); +}; + +groupTypes.spacing = function(group, options, prev) { + if (group.value === "\\ " || group.value === "\\space" || + group.value === " " || group.value === "~") { + // Spaces are generated by adding an actual space. Each of these + // things has an entry in the symbols table, so these will be turned + // into appropriate outputs. + return makeSpan( + ["mord", "mspace"], + [buildCommon.mathsym(group.value, group.mode)] + ); + } else { + // Other kinds of spaces are of arbitrary width. We use CSS to + // generate these. + return makeSpan( + ["mord", "mspace", + buildCommon.spacingFunctions[group.value].className]); + } +}; + +groupTypes.llap = function(group, options, prev) { + var inner = makeSpan( + ["inner"], [buildGroup(group.value.body, options.reset())]); + var fix = makeSpan(["fix"], []); + return makeSpan( + ["llap", options.style.cls()], [inner, fix]); +}; + +groupTypes.rlap = function(group, options, prev) { + var inner = makeSpan( + ["inner"], [buildGroup(group.value.body, options.reset())]); + var fix = makeSpan(["fix"], []); + return makeSpan( + ["rlap", options.style.cls()], [inner, fix]); +}; + +groupTypes.op = function(group, options, prev) { + // Operators are handled in the TeXbook pg. 443-444, rule 13(a). + var supGroup; + var subGroup; + var hasLimits = false; + if (group.type === "supsub" ) { + // If we have limits, supsub will pass us its group to handle. Pull + // out the superscript and subscript and set the group to the op in + // its base. + supGroup = group.value.sup; + subGroup = group.value.sub; + group = group.value.base; + hasLimits = true; + } + + // Most operators have a large successor symbol, but these don't. + var noSuccessor = [ + "\\smallint", + ]; + + var large = false; + if (options.style.size === Style.DISPLAY.size && + group.value.symbol && + !utils.contains(noSuccessor, group.value.body)) { + + // Most symbol operators get larger in displaystyle (rule 13) + large = true; + } + + var base; + var baseShift = 0; + var slant = 0; + if (group.value.symbol) { + // If this is a symbol, create the symbol. + var style = large ? "Size2-Regular" : "Size1-Regular"; + base = buildCommon.makeSymbol( + group.value.body, style, "math", options.getColor(), + ["op-symbol", large ? "large-op" : "small-op", "mop"]); + + // Shift the symbol so its center lies on the axis (rule 13). It + // appears that our fonts have the centers of the symbols already + // almost on the axis, so these numbers are very small. Note we + // don't actually apply this here, but instead it is used either in + // the vlist creation or separately when there are no limits. + baseShift = (base.height - base.depth) / 2 - + fontMetrics.metrics.axisHeight * + options.style.sizeMultiplier; + + // The slant of the symbol is just its italic correction. + slant = base.italic; + } else { + // Otherwise, this is a text operator. Build the text from the + // operator's name. + // TODO(emily): Add a space in the middle of some of these + // operators, like \limsup + var output = []; + for (var i = 1; i < group.value.body.length; i++) { + output.push(buildCommon.mathsym(group.value.body[i], group.mode)); + } + base = makeSpan(["mop"], output, options.getColor()); + } + + if (hasLimits) { + // IE 8 clips \int if it is in a display: inline-block. We wrap it + // in a new span so it is an inline, and works. + base = makeSpan([], [base]); + + var supmid; + var supKern; + var submid; + var subKern; + // We manually have to handle the superscripts and subscripts. This, + // aside from the kern calculations, is copied from supsub. + if (supGroup) { + var sup = buildGroup( + supGroup, options.withStyle(options.style.sup())); + supmid = makeSpan( + [options.style.reset(), options.style.sup().cls()], [sup]); + + supKern = Math.max( + fontMetrics.metrics.bigOpSpacing1, + fontMetrics.metrics.bigOpSpacing3 - sup.depth); + } + + if (subGroup) { + var sub = buildGroup( + subGroup, options.withStyle(options.style.sub())); + submid = makeSpan( + [options.style.reset(), options.style.sub().cls()], + [sub]); + + subKern = Math.max( + fontMetrics.metrics.bigOpSpacing2, + fontMetrics.metrics.bigOpSpacing4 - sub.height); + } + + // Build the final group as a vlist of the possible subscript, base, + // and possible superscript. + var finalGroup; + var top; + var bottom; + if (!supGroup) { + top = base.height - baseShift; + + finalGroup = buildCommon.makeVList([ + {type: "kern", size: fontMetrics.metrics.bigOpSpacing5}, + {type: "elem", elem: submid}, + {type: "kern", size: subKern}, + {type: "elem", elem: base}, + ], "top", top, options); + + // Here, we shift the limits by the slant of the symbol. Note + // that we are supposed to shift the limits by 1/2 of the slant, + // but since we are centering the limits adding a full slant of + // margin will shift by 1/2 that. + finalGroup.children[0].style.marginLeft = -slant + "em"; + } else if (!subGroup) { + bottom = base.depth + baseShift; + + finalGroup = buildCommon.makeVList([ + {type: "elem", elem: base}, + {type: "kern", size: supKern}, + {type: "elem", elem: supmid}, + {type: "kern", size: fontMetrics.metrics.bigOpSpacing5}, + ], "bottom", bottom, options); + + // See comment above about slants + finalGroup.children[1].style.marginLeft = slant + "em"; + } else if (!supGroup && !subGroup) { + // This case probably shouldn't occur (this would mean the + // supsub was sending us a group with no superscript or + // subscript) but be safe. + return base; + } else { + bottom = fontMetrics.metrics.bigOpSpacing5 + + submid.height + submid.depth + + subKern + + base.depth + baseShift; + + finalGroup = buildCommon.makeVList([ + {type: "kern", size: fontMetrics.metrics.bigOpSpacing5}, + {type: "elem", elem: submid}, + {type: "kern", size: subKern}, + {type: "elem", elem: base}, + {type: "kern", size: supKern}, + {type: "elem", elem: supmid}, + {type: "kern", size: fontMetrics.metrics.bigOpSpacing5}, + ], "bottom", bottom, options); + + // See comment above about slants + finalGroup.children[0].style.marginLeft = -slant + "em"; + finalGroup.children[2].style.marginLeft = slant + "em"; + } + + return makeSpan(["mop", "op-limits"], [finalGroup]); + } else { + if (group.value.symbol) { + base.style.top = baseShift + "em"; + } + + return base; + } +}; + +groupTypes.katex = function(group, options, prev) { + // The KaTeX logo. The offsets for the K and a were chosen to look + // good, but the offsets for the T, E, and X were taken from the + // definition of \TeX in TeX (see TeXbook pg. 356) + var k = makeSpan( + ["k"], [buildCommon.mathsym("K", group.mode)]); + var a = makeSpan( + ["a"], [buildCommon.mathsym("A", group.mode)]); + + a.height = (a.height + 0.2) * 0.75; + a.depth = (a.height - 0.2) * 0.75; + + var t = makeSpan( + ["t"], [buildCommon.mathsym("T", group.mode)]); + var e = makeSpan( + ["e"], [buildCommon.mathsym("E", group.mode)]); + + e.height = (e.height - 0.2155); + e.depth = (e.depth + 0.2155); + + var x = makeSpan( + ["x"], [buildCommon.mathsym("X", group.mode)]); + + return makeSpan( + ["katex-logo", "mord"], [k, a, t, e, x], options.getColor()); +}; + +groupTypes.overline = function(group, options, prev) { + // Overlines are handled in the TeXbook pg 443, Rule 9. + + // Build the inner group in the cramped style. + var innerGroup = buildGroup(group.value.body, + options.withStyle(options.style.cramp())); + + var ruleWidth = fontMetrics.metrics.defaultRuleThickness / + options.style.sizeMultiplier; + + // Create the line above the body + var line = makeSpan( + [options.style.reset(), Style.TEXT.cls(), "overline-line"]); + line.height = ruleWidth; + line.maxFontSize = 1.0; + + // Generate the vlist, with the appropriate kerns + var vlist = buildCommon.makeVList([ + {type: "elem", elem: innerGroup}, + {type: "kern", size: 3 * ruleWidth}, + {type: "elem", elem: line}, + {type: "kern", size: ruleWidth}, + ], "firstBaseline", null, options); + + return makeSpan(["overline", "mord"], [vlist], options.getColor()); +}; + +groupTypes.underline = function(group, options, prev) { + // Underlines are handled in the TeXbook pg 443, Rule 10. + + // Build the inner group. + var innerGroup = buildGroup(group.value.body, options); + + var ruleWidth = fontMetrics.metrics.defaultRuleThickness / + options.style.sizeMultiplier; + + // Create the line above the body + var line = makeSpan( + [options.style.reset(), Style.TEXT.cls(), "underline-line"]); + line.height = ruleWidth; + line.maxFontSize = 1.0; + + // Generate the vlist, with the appropriate kerns + var vlist = buildCommon.makeVList([ + {type: "kern", size: ruleWidth}, + {type: "elem", elem: line}, + {type: "kern", size: 3 * ruleWidth}, + {type: "elem", elem: innerGroup}, + ], "top", innerGroup.height, options); + + return makeSpan(["underline", "mord"], [vlist], options.getColor()); +}; + +groupTypes.sqrt = function(group, options, prev) { + // Square roots are handled in the TeXbook pg. 443, Rule 11. + + // First, we do the same steps as in overline to build the inner group + // and line + var inner = buildGroup(group.value.body, + options.withStyle(options.style.cramp())); + + var ruleWidth = fontMetrics.metrics.defaultRuleThickness / + options.style.sizeMultiplier; + + var line = makeSpan( + [options.style.reset(), Style.TEXT.cls(), "sqrt-line"], [], + options.getColor()); + line.height = ruleWidth; + line.maxFontSize = 1.0; + + var phi = ruleWidth; + if (options.style.id < Style.TEXT.id) { + phi = fontMetrics.metrics.xHeight; + } + + // Calculate the clearance between the body and line + var lineClearance = ruleWidth + phi / 4; + + var innerHeight = + (inner.height + inner.depth) * options.style.sizeMultiplier; + var minDelimiterHeight = innerHeight + lineClearance + ruleWidth; + + // Create a \surd delimiter of the required minimum size + var delim = makeSpan(["sqrt-sign"], [ + delimiter.customSizedDelim("\\surd", minDelimiterHeight, + false, options, group.mode)], + options.getColor()); + + var delimDepth = (delim.height + delim.depth) - ruleWidth; + + // Adjust the clearance based on the delimiter size + if (delimDepth > inner.height + inner.depth + lineClearance) { + lineClearance = + (lineClearance + delimDepth - inner.height - inner.depth) / 2; + } + + // Shift the delimiter so that its top lines up with the top of the line + var delimShift = -(inner.height + lineClearance + ruleWidth) + delim.height; + delim.style.top = delimShift + "em"; + delim.height -= delimShift; + delim.depth += delimShift; + + // We add a special case here, because even when `inner` is empty, we + // still get a line. So, we use a simple heuristic to decide if we + // should omit the body entirely. (note this doesn't work for something + // like `\sqrt{\rlap{x}}`, but if someone is doing that they deserve for + // it not to work. + var body; + if (inner.height === 0 && inner.depth === 0) { + body = makeSpan(); + } else { + body = buildCommon.makeVList([ + {type: "elem", elem: inner}, + {type: "kern", size: lineClearance}, + {type: "elem", elem: line}, + {type: "kern", size: ruleWidth}, + ], "firstBaseline", null, options); + } + + if (!group.value.index) { + return makeSpan(["sqrt", "mord"], [delim, body]); + } else { + // Handle the optional root index + + // The index is always in scriptscript style + var root = buildGroup( + group.value.index, + options.withStyle(Style.SCRIPTSCRIPT)); + var rootWrap = makeSpan( + [options.style.reset(), Style.SCRIPTSCRIPT.cls()], + [root]); + + // Figure out the height and depth of the inner part + var innerRootHeight = Math.max(delim.height, body.height); + var innerRootDepth = Math.max(delim.depth, body.depth); + + // The amount the index is shifted by. This is taken from the TeX + // source, in the definition of `\r@@t`. + var toShift = 0.6 * (innerRootHeight - innerRootDepth); + + // Build a VList with the superscript shifted up correctly + var rootVList = buildCommon.makeVList( + [{type: "elem", elem: rootWrap}], + "shift", -toShift, options); + // Add a class surrounding it so we can add on the appropriate + // kerning + var rootVListWrap = makeSpan(["root"], [rootVList]); + + return makeSpan(["sqrt", "mord"], [rootVListWrap, delim, body]); + } +}; + +groupTypes.sizing = function(group, options, prev) { + // Handle sizing operators like \Huge. Real TeX doesn't actually allow + // these functions inside of math expressions, so we do some special + // handling. + var inner = buildExpression(group.value.value, + options.withSize(group.value.size), prev); + + var span = makeSpan(["mord"], + [makeSpan(["sizing", "reset-" + options.size, group.value.size, + options.style.cls()], + inner)]); + + // Calculate the correct maxFontSize manually + var fontSize = buildCommon.sizingMultiplier[group.value.size]; + span.maxFontSize = fontSize * options.style.sizeMultiplier; + + return span; +}; + +groupTypes.styling = function(group, options, prev) { + // Style changes are handled in the TeXbook on pg. 442, Rule 3. + + // Figure out what style we're changing to. + var style = { + "display": Style.DISPLAY, + "text": Style.TEXT, + "script": Style.SCRIPT, + "scriptscript": Style.SCRIPTSCRIPT, + }; + + var newStyle = style[group.value.style]; + + // Build the inner expression in the new style. + var inner = buildExpression( + group.value.value, options.withStyle(newStyle), prev); + + return makeSpan([options.style.reset(), newStyle.cls()], inner); +}; + +groupTypes.font = function(group, options, prev) { + var font = group.value.font; + return buildGroup(group.value.body, options.withFont(font), prev); +}; + +groupTypes.delimsizing = function(group, options, prev) { + var delim = group.value.value; + + if (delim === ".") { + // Empty delimiters still count as elements, even though they don't + // show anything. + return makeSpan([groupToType[group.value.delimType]]); + } + + // Use delimiter.sizedDelim to generate the delimiter. + return makeSpan( + [groupToType[group.value.delimType]], + [delimiter.sizedDelim( + delim, group.value.size, options, group.mode)]); +}; + +groupTypes.leftright = function(group, options, prev) { + // Build the inner expression + var inner = buildExpression(group.value.body, options.reset()); + + var innerHeight = 0; + var innerDepth = 0; + + // Calculate its height and depth + for (var i = 0; i < inner.length; i++) { + innerHeight = Math.max(inner[i].height, innerHeight); + innerDepth = Math.max(inner[i].depth, innerDepth); + } + + // The size of delimiters is the same, regardless of what style we are + // in. Thus, to correctly calculate the size of delimiter we need around + // a group, we scale down the inner size based on the size. + innerHeight *= options.style.sizeMultiplier; + innerDepth *= options.style.sizeMultiplier; + + var leftDelim; + if (group.value.left === ".") { + // Empty delimiters in \left and \right make null delimiter spaces. + leftDelim = makeNullDelimiter(options); + } else { + // Otherwise, use leftRightDelim to generate the correct sized + // delimiter. + leftDelim = delimiter.leftRightDelim( + group.value.left, innerHeight, innerDepth, options, + group.mode); + } + // Add it to the beginning of the expression + inner.unshift(leftDelim); + + var rightDelim; + // Same for the right delimiter + if (group.value.right === ".") { + rightDelim = makeNullDelimiter(options); + } else { + rightDelim = delimiter.leftRightDelim( + group.value.right, innerHeight, innerDepth, options, + group.mode); + } + // Add it to the end of the expression. + inner.push(rightDelim); + + return makeSpan( + ["minner", options.style.cls()], inner, options.getColor()); +}; + +groupTypes.rule = function(group, options, prev) { + // Make an empty span for the rule + var rule = makeSpan(["mord", "rule"], [], options.getColor()); + + // Calculate the shift, width, and height of the rule, and account for units + var shift = 0; + if (group.value.shift) { + shift = group.value.shift.number; + if (group.value.shift.unit === "ex") { + shift *= fontMetrics.metrics.xHeight; + } + } + + var width = group.value.width.number; + if (group.value.width.unit === "ex") { + width *= fontMetrics.metrics.xHeight; + } + + var height = group.value.height.number; + if (group.value.height.unit === "ex") { + height *= fontMetrics.metrics.xHeight; + } + + // The sizes of rules are absolute, so make it larger if we are in a + // smaller style. + shift /= options.style.sizeMultiplier; + width /= options.style.sizeMultiplier; + height /= options.style.sizeMultiplier; + + // Style the rule to the right size + rule.style.borderRightWidth = width + "em"; + rule.style.borderTopWidth = height + "em"; + rule.style.bottom = shift + "em"; + + // Record the height and width + rule.width = width; + rule.height = height + shift; + rule.depth = -shift; + + return rule; +}; + +groupTypes.kern = function(group, options, prev) { + // Make an empty span for the rule + var rule = makeSpan(["mord", "rule"], [], options.getColor()); + + var dimension = 0; + if (group.value.dimension) { + dimension = group.value.dimension.number; + if (group.value.dimension.unit === "ex") { + dimension *= fontMetrics.metrics.xHeight; + } + } + + dimension /= options.style.sizeMultiplier; + + rule.style.marginLeft = dimension + "em"; + + return rule; +}; + +groupTypes.accent = function(group, options, prev) { + // Accents are handled in the TeXbook pg. 443, rule 12. + var base = group.value.base; + + var supsubGroup; + if (group.type === "supsub") { + // If our base is a character box, and we have superscripts and + // subscripts, the supsub will defer to us. In particular, we want + // to attach the superscripts and subscripts to the inner body (so + // that the position of the superscripts and subscripts won't be + // affected by the height of the accent). We accomplish this by + // sticking the base of the accent into the base of the supsub, and + // rendering that, while keeping track of where the accent is. + + // The supsub group is the group that was passed in + var supsub = group; + // The real accent group is the base of the supsub group + group = supsub.value.base; + // The character box is the base of the accent group + base = group.value.base; + // Stick the character box into the base of the supsub group + supsub.value.base = base; + + // Rerender the supsub group with its new base, and store that + // result. + supsubGroup = buildGroup( + supsub, options.reset(), prev); + } + + // Build the base group + var body = buildGroup( + base, options.withStyle(options.style.cramp())); + + // Calculate the skew of the accent. This is based on the line "If the + // nucleus is not a single character, let s = 0; otherwise set s to the + // kern amount for the nucleus followed by the \skewchar of its font." + // Note that our skew metrics are just the kern between each character + // and the skewchar. + var skew; + if (isCharacterBox(base)) { + // If the base is a character box, then we want the skew of the + // innermost character. To do that, we find the innermost character: + var baseChar = getBaseElem(base); + // Then, we render its group to get the symbol inside it + var baseGroup = buildGroup( + baseChar, options.withStyle(options.style.cramp())); + // Finally, we pull the skew off of the symbol. + skew = baseGroup.skew; + // Note that we now throw away baseGroup, because the layers we + // removed with getBaseElem might contain things like \color which + // we can't get rid of. + // TODO(emily): Find a better way to get the skew + } else { + skew = 0; + } + + // calculate the amount of space between the body and the accent + var clearance = Math.min(body.height, fontMetrics.metrics.xHeight); + + // Build the accent + var accent = buildCommon.makeSymbol( + group.value.accent, "Main-Regular", "math", options.getColor()); + // Remove the italic correction of the accent, because it only serves to + // shift the accent over to a place we don't want. + accent.italic = 0; + + // The \vec character that the fonts use is a combining character, and + // thus shows up much too far to the left. To account for this, we add a + // specific class which shifts the accent over to where we want it. + // TODO(emily): Fix this in a better way, like by changing the font + var vecClass = group.value.accent === "\\vec" ? "accent-vec" : null; + + var accentBody = makeSpan(["accent-body", vecClass], [ + makeSpan([], [accent])]); + + accentBody = buildCommon.makeVList([ + {type: "elem", elem: body}, + {type: "kern", size: -clearance}, + {type: "elem", elem: accentBody}, + ], "firstBaseline", null, options); + + // Shift the accent over by the skew. Note we shift by twice the skew + // because we are centering the accent, so by adding 2*skew to the left, + // we shift it to the right by 1*skew. + accentBody.children[1].style.marginLeft = 2 * skew + "em"; + + var accentWrap = makeSpan(["mord", "accent"], [accentBody]); + + if (supsubGroup) { + // Here, we replace the "base" child of the supsub with our newly + // generated accent. + supsubGroup.children[0] = accentWrap; + + // Since we don't rerun the height calculation after replacing the + // accent, we manually recalculate height. + supsubGroup.height = Math.max(accentWrap.height, supsubGroup.height); + + // Accents should always be ords, even when their innards are not. + supsubGroup.classes[0] = "mord"; + + return supsubGroup; + } else { + return accentWrap; + } +}; + +groupTypes.phantom = function(group, options, prev) { + var elements = buildExpression( + group.value.value, + options.withPhantom(), + prev + ); + + // \phantom isn't supposed to affect the elements it contains. + // See "color" for more details. + return new buildCommon.makeFragment(elements); +}; + +/** + * buildGroup is the function that takes a group and calls the correct groupType + * function for it. It also handles the interaction of size and style changes + * between parents and children. + */ +var buildGroup = function(group, options, prev) { + if (!group) { + return makeSpan(); + } + + if (groupTypes[group.type]) { + // Call the groupTypes function + var groupNode = groupTypes[group.type](group, options, prev); + var multiplier; + + // If the style changed between the parent and the current group, + // account for the size difference + if (options.style !== options.parentStyle) { + multiplier = options.style.sizeMultiplier / + options.parentStyle.sizeMultiplier; + + groupNode.height *= multiplier; + groupNode.depth *= multiplier; + } + + // If the size changed between the parent and the current group, account + // for that size difference. + if (options.size !== options.parentSize) { + multiplier = buildCommon.sizingMultiplier[options.size] / + buildCommon.sizingMultiplier[options.parentSize]; + + groupNode.height *= multiplier; + groupNode.depth *= multiplier; + } + + return groupNode; + } else { + throw new ParseError( + "Got group of unknown type: '" + group.type + "'"); + } +}; + +/** + * Take an entire parse tree, and build it into an appropriate set of HTML + * nodes. + */ +var buildHTML = function(tree, options) { + // buildExpression is destructive, so we need to make a clone + // of the incoming tree so that it isn't accidentally changed + tree = JSON.parse(JSON.stringify(tree)); + + // Build the expression contained in the tree + var expression = buildExpression(tree, options); + var body = makeSpan(["base", options.style.cls()], expression); + + // Add struts, which ensure that the top of the HTML element falls at the + // height of the expression, and the bottom of the HTML element falls at the + // depth of the expression. + var topStrut = makeSpan(["strut"]); + var bottomStrut = makeSpan(["strut", "bottom"]); + + topStrut.style.height = body.height + "em"; + bottomStrut.style.height = (body.height + body.depth) + "em"; + // We'd like to use `vertical-align: top` but in IE 9 this lowers the + // baseline of the box to the bottom of this strut (instead staying in the + // normal place) so we use an absolute value for vertical-align instead + bottomStrut.style.verticalAlign = -body.depth + "em"; + + // Wrap the struts and body together + var htmlNode = makeSpan(["katex-html"], [topStrut, bottomStrut, body]); + + htmlNode.setAttribute("aria-hidden", "true"); + + return htmlNode; +}; + +module.exports = buildHTML; + +},{"./ParseError":6,"./Style":9,"./buildCommon":10,"./delimiter":14,"./domTree":15,"./fontMetrics":17,"./utils":25}],12:[function(require,module,exports){ +/** + * This file converts a parse tree into a cooresponding MathML tree. The main + * entry point is the `buildMathML` function, which takes a parse tree from the + * parser. + */ + +var buildCommon = require("./buildCommon"); +var fontMetrics = require("./fontMetrics"); +var mathMLTree = require("./mathMLTree"); +var ParseError = require("./ParseError"); +var symbols = require("./symbols"); +var utils = require("./utils"); + +var makeSpan = buildCommon.makeSpan; +var fontMap = buildCommon.fontMap; + +/** + * Takes a symbol and converts it into a MathML text node after performing + * optional replacement from symbols.js. + */ +var makeText = function(text, mode) { + if (symbols[mode][text] && symbols[mode][text].replace) { + text = symbols[mode][text].replace; + } + + return new mathMLTree.TextNode(text); +}; + +/** + * Returns the math variant as a string or null if none is required. + */ +var getVariant = function(group, options) { + var font = options.font; + if (!font) { + return null; + } + + var mode = group.mode; + if (font === "mathit") { + return "italic"; + } + + var value = group.value; + if (utils.contains(["\\imath", "\\jmath"], value)) { + return null; + } + + if (symbols[mode][value] && symbols[mode][value].replace) { + value = symbols[mode][value].replace; + } + + var fontName = fontMap[font].fontName; + if (fontMetrics.getCharacterMetrics(value, fontName)) { + return fontMap[options.font].variant; + } + + return null; +}; + +/** + * Functions for handling the different types of groups found in the parse + * tree. Each function should take a parse group and return a MathML node. + */ +var groupTypes = {}; + +groupTypes.mathord = function(group, options) { + var node = new mathMLTree.MathNode( + "mi", + [makeText(group.value, group.mode)]); + + var variant = getVariant(group, options); + if (variant) { + node.setAttribute("mathvariant", variant); + } + return node; +}; + +groupTypes.textord = function(group, options) { + var text = makeText(group.value, group.mode); + + var variant = getVariant(group, options) || "normal"; + + var node; + if (/[0-9]/.test(group.value)) { + // TODO(kevinb) merge adjacent <mn> nodes + // do it as a post processing step + node = new mathMLTree.MathNode("mn", [text]); + if (options.font) { + node.setAttribute("mathvariant", variant); + } + } else { + node = new mathMLTree.MathNode("mi", [text]); + node.setAttribute("mathvariant", variant); + } + + return node; +}; + +groupTypes.bin = function(group) { + var node = new mathMLTree.MathNode( + "mo", [makeText(group.value, group.mode)]); + + return node; +}; + +groupTypes.rel = function(group) { + var node = new mathMLTree.MathNode( + "mo", [makeText(group.value, group.mode)]); + + return node; +}; + +groupTypes.open = function(group) { + var node = new mathMLTree.MathNode( + "mo", [makeText(group.value, group.mode)]); + + return node; +}; + +groupTypes.close = function(group) { + var node = new mathMLTree.MathNode( + "mo", [makeText(group.value, group.mode)]); + + return node; +}; + +groupTypes.inner = function(group) { + var node = new mathMLTree.MathNode( + "mo", [makeText(group.value, group.mode)]); + + return node; +}; + +groupTypes.punct = function(group) { + var node = new mathMLTree.MathNode( + "mo", [makeText(group.value, group.mode)]); + + node.setAttribute("separator", "true"); + + return node; +}; + +groupTypes.ordgroup = function(group, options) { + var inner = buildExpression(group.value, options); + + var node = new mathMLTree.MathNode("mrow", inner); + + return node; +}; + +groupTypes.text = function(group, options) { + var inner = buildExpression(group.value.body, options); + + var node = new mathMLTree.MathNode("mtext", inner); + + return node; +}; + +groupTypes.color = function(group, options) { + var inner = buildExpression(group.value.value, options); + + var node = new mathMLTree.MathNode("mstyle", inner); + + node.setAttribute("mathcolor", group.value.color); + + return node; +}; + +groupTypes.supsub = function(group, options) { + var children = [buildGroup(group.value.base, options)]; + + if (group.value.sub) { + children.push(buildGroup(group.value.sub, options)); + } + + if (group.value.sup) { + children.push(buildGroup(group.value.sup, options)); + } + + var nodeType; + if (!group.value.sub) { + nodeType = "msup"; + } else if (!group.value.sup) { + nodeType = "msub"; + } else { + nodeType = "msubsup"; + } + + var node = new mathMLTree.MathNode(nodeType, children); + + return node; +}; + +groupTypes.genfrac = function(group, options) { + var node = new mathMLTree.MathNode( + "mfrac", + [buildGroup(group.value.numer, options), + buildGroup(group.value.denom, options)]); + + if (!group.value.hasBarLine) { + node.setAttribute("linethickness", "0px"); + } + + if (group.value.leftDelim != null || group.value.rightDelim != null) { + var withDelims = []; + + if (group.value.leftDelim != null) { + var leftOp = new mathMLTree.MathNode( + "mo", [new mathMLTree.TextNode(group.value.leftDelim)]); + + leftOp.setAttribute("fence", "true"); + + withDelims.push(leftOp); + } + + withDelims.push(node); + + if (group.value.rightDelim != null) { + var rightOp = new mathMLTree.MathNode( + "mo", [new mathMLTree.TextNode(group.value.rightDelim)]); + + rightOp.setAttribute("fence", "true"); + + withDelims.push(rightOp); + } + + var outerNode = new mathMLTree.MathNode("mrow", withDelims); + + return outerNode; + } + + return node; +}; + +groupTypes.array = function(group, options) { + return new mathMLTree.MathNode( + "mtable", group.value.body.map(function(row) { + return new mathMLTree.MathNode( + "mtr", row.map(function(cell) { + return new mathMLTree.MathNode( + "mtd", [buildGroup(cell, options)]); + })); + })); +}; + +groupTypes.sqrt = function(group, options) { + var node; + if (group.value.index) { + node = new mathMLTree.MathNode( + "mroot", [ + buildGroup(group.value.body, options), + buildGroup(group.value.index, options), + ]); + } else { + node = new mathMLTree.MathNode( + "msqrt", [buildGroup(group.value.body, options)]); + } + + return node; +}; + +groupTypes.leftright = function(group, options) { + var inner = buildExpression(group.value.body, options); + + if (group.value.left !== ".") { + var leftNode = new mathMLTree.MathNode( + "mo", [makeText(group.value.left, group.mode)]); + + leftNode.setAttribute("fence", "true"); + + inner.unshift(leftNode); + } + + if (group.value.right !== ".") { + var rightNode = new mathMLTree.MathNode( + "mo", [makeText(group.value.right, group.mode)]); + + rightNode.setAttribute("fence", "true"); + + inner.push(rightNode); + } + + var outerNode = new mathMLTree.MathNode("mrow", inner); + + return outerNode; +}; + +groupTypes.accent = function(group, options) { + var accentNode = new mathMLTree.MathNode( + "mo", [makeText(group.value.accent, group.mode)]); + + var node = new mathMLTree.MathNode( + "mover", + [buildGroup(group.value.base, options), + accentNode]); + + node.setAttribute("accent", "true"); + + return node; +}; + +groupTypes.spacing = function(group) { + var node; + + if (group.value === "\\ " || group.value === "\\space" || + group.value === " " || group.value === "~") { + node = new mathMLTree.MathNode( + "mtext", [new mathMLTree.TextNode("\u00a0")]); + } else { + node = new mathMLTree.MathNode("mspace"); + + node.setAttribute( + "width", buildCommon.spacingFunctions[group.value].size); + } + + return node; +}; + +groupTypes.op = function(group) { + var node; + + // TODO(emily): handle big operators using the `largeop` attribute + + if (group.value.symbol) { + // This is a symbol. Just add the symbol. + node = new mathMLTree.MathNode( + "mo", [makeText(group.value.body, group.mode)]); + } else { + // This is a text operator. Add all of the characters from the + // operator's name. + // TODO(emily): Add a space in the middle of some of these + // operators, like \limsup. + node = new mathMLTree.MathNode( + "mi", [new mathMLTree.TextNode(group.value.body.slice(1))]); + } + + return node; +}; + +groupTypes.katex = function(group) { + var node = new mathMLTree.MathNode( + "mtext", [new mathMLTree.TextNode("KaTeX")]); + + return node; +}; + +groupTypes.font = function(group, options) { + var font = group.value.font; + return buildGroup(group.value.body, options.withFont(font)); +}; + +groupTypes.delimsizing = function(group) { + var children = []; + + if (group.value.value !== ".") { + children.push(makeText(group.value.value, group.mode)); + } + + var node = new mathMLTree.MathNode("mo", children); + + if (group.value.delimType === "open" || + group.value.delimType === "close") { + // Only some of the delimsizing functions act as fences, and they + // return "open" or "close" delimTypes. + node.setAttribute("fence", "true"); + } else { + // Explicitly disable fencing if it's not a fence, to override the + // defaults. + node.setAttribute("fence", "false"); + } + + return node; +}; + +groupTypes.styling = function(group, options) { + var inner = buildExpression(group.value.value, options); + + var node = new mathMLTree.MathNode("mstyle", inner); + + var styleAttributes = { + "display": ["0", "true"], + "text": ["0", "false"], + "script": ["1", "false"], + "scriptscript": ["2", "false"], + }; + + var attr = styleAttributes[group.value.style]; + + node.setAttribute("scriptlevel", attr[0]); + node.setAttribute("displaystyle", attr[1]); + + return node; +}; + +groupTypes.sizing = function(group, options) { + var inner = buildExpression(group.value.value, options); + + var node = new mathMLTree.MathNode("mstyle", inner); + + // TODO(emily): This doesn't produce the correct size for nested size + // changes, because we don't keep state of what style we're currently + // in, so we can't reset the size to normal before changing it. Now + // that we're passing an options parameter we should be able to fix + // this. + node.setAttribute( + "mathsize", buildCommon.sizingMultiplier[group.value.size] + "em"); + + return node; +}; + +groupTypes.overline = function(group, options) { + var operator = new mathMLTree.MathNode( + "mo", [new mathMLTree.TextNode("\u203e")]); + operator.setAttribute("stretchy", "true"); + + var node = new mathMLTree.MathNode( + "mover", + [buildGroup(group.value.body, options), + operator]); + node.setAttribute("accent", "true"); + + return node; +}; + +groupTypes.underline = function(group, options) { + var operator = new mathMLTree.MathNode( + "mo", [new mathMLTree.TextNode("\u203e")]); + operator.setAttribute("stretchy", "true"); + + var node = new mathMLTree.MathNode( + "munder", + [buildGroup(group.value.body, options), + operator]); + node.setAttribute("accentunder", "true"); + + return node; +}; + +groupTypes.rule = function(group) { + // TODO(emily): Figure out if there's an actual way to draw black boxes + // in MathML. + var node = new mathMLTree.MathNode("mrow"); + + return node; +}; + +groupTypes.kern = function(group) { + // TODO(kevin): Figure out if there's a way to add space in MathML + var node = new mathMLTree.MathNode("mrow"); + + return node; +}; + +groupTypes.llap = function(group, options) { + var node = new mathMLTree.MathNode( + "mpadded", [buildGroup(group.value.body, options)]); + + node.setAttribute("lspace", "-1width"); + node.setAttribute("width", "0px"); + + return node; +}; + +groupTypes.rlap = function(group, options) { + var node = new mathMLTree.MathNode( + "mpadded", [buildGroup(group.value.body, options)]); + + node.setAttribute("width", "0px"); + + return node; +}; + +groupTypes.phantom = function(group, options, prev) { + var inner = buildExpression(group.value.value, options); + return new mathMLTree.MathNode("mphantom", inner); +}; + +/** + * Takes a list of nodes, builds them, and returns a list of the generated + * MathML nodes. A little simpler than the HTML version because we don't do any + * previous-node handling. + */ +var buildExpression = function(expression, options) { + var groups = []; + for (var i = 0; i < expression.length; i++) { + var group = expression[i]; + groups.push(buildGroup(group, options)); + } + return groups; +}; + +/** + * Takes a group from the parser and calls the appropriate groupTypes function + * on it to produce a MathML node. + */ +var buildGroup = function(group, options) { + if (!group) { + return new mathMLTree.MathNode("mrow"); + } + + if (groupTypes[group.type]) { + // Call the groupTypes function + return groupTypes[group.type](group, options); + } else { + throw new ParseError( + "Got group of unknown type: '" + group.type + "'"); + } +}; + +/** + * Takes a full parse tree and settings and builds a MathML representation of + * it. In particular, we put the elements from building the parse tree into a + * <semantics> tag so we can also include that TeX source as an annotation. + * + * Note that we actually return a domTree element with a `<math>` inside it so + * we can do appropriate styling. + */ +var buildMathML = function(tree, texExpression, options) { + var expression = buildExpression(tree, options); + + // Wrap up the expression in an mrow so it is presented in the semantics + // tag correctly. + var wrapper = new mathMLTree.MathNode("mrow", expression); + + // Build a TeX annotation of the source + var annotation = new mathMLTree.MathNode( + "annotation", [new mathMLTree.TextNode(texExpression)]); + + annotation.setAttribute("encoding", "application/x-tex"); + + var semantics = new mathMLTree.MathNode( + "semantics", [wrapper, annotation]); + + var math = new mathMLTree.MathNode("math", [semantics]); + + // You can't style <math> nodes, so we wrap the node in a span. + return makeSpan(["katex-mathml"], [math]); +}; + +module.exports = buildMathML; + +},{"./ParseError":6,"./buildCommon":10,"./fontMetrics":17,"./mathMLTree":20,"./symbols":23,"./utils":25}],13:[function(require,module,exports){ +var buildHTML = require("./buildHTML"); +var buildMathML = require("./buildMathML"); +var buildCommon = require("./buildCommon"); +var Options = require("./Options"); +var Settings = require("./Settings"); +var Style = require("./Style"); + +var makeSpan = buildCommon.makeSpan; + +var buildTree = function(tree, expression, settings) { + settings = settings || new Settings({}); + + var startStyle = Style.TEXT; + if (settings.displayMode) { + startStyle = Style.DISPLAY; + } + + // Setup the default options + var options = new Options({ + style: startStyle, + size: "size5", + }); + + // `buildHTML` sometimes messes with the parse tree (like turning bins -> + // ords), so we build the MathML version first. + var mathMLNode = buildMathML(tree, expression, options); + var htmlNode = buildHTML(tree, options); + + var katexNode = makeSpan(["katex"], [ + mathMLNode, htmlNode, + ]); + + if (settings.displayMode) { + return makeSpan(["katex-display"], [katexNode]); + } else { + return katexNode; + } +}; + +module.exports = buildTree; + +},{"./Options":5,"./Settings":8,"./Style":9,"./buildCommon":10,"./buildHTML":11,"./buildMathML":12}],14:[function(require,module,exports){ +/** + * This file deals with creating delimiters of various sizes. The TeXbook + * discusses these routines on page 441-442, in the "Another subroutine sets box + * x to a specified variable delimiter" paragraph. + * + * There are three main routines here. `makeSmallDelim` makes a delimiter in the + * normal font, but in either text, script, or scriptscript style. + * `makeLargeDelim` makes a delimiter in textstyle, but in one of the Size1, + * Size2, Size3, or Size4 fonts. `makeStackedDelim` makes a delimiter out of + * smaller pieces that are stacked on top of one another. + * + * The functions take a parameter `center`, which determines if the delimiter + * should be centered around the axis. + * + * Then, there are three exposed functions. `sizedDelim` makes a delimiter in + * one of the given sizes. This is used for things like `\bigl`. + * `customSizedDelim` makes a delimiter with a given total height+depth. It is + * called in places like `\sqrt`. `leftRightDelim` makes an appropriate + * delimiter which surrounds an expression of a given height an depth. It is + * used in `\left` and `\right`. + */ + +var ParseError = require("./ParseError"); +var Style = require("./Style"); + +var buildCommon = require("./buildCommon"); +var fontMetrics = require("./fontMetrics"); +var symbols = require("./symbols"); +var utils = require("./utils"); + +var makeSpan = buildCommon.makeSpan; + +/** + * Get the metrics for a given symbol and font, after transformation (i.e. + * after following replacement from symbols.js) + */ +var getMetrics = function(symbol, font) { + if (symbols.math[symbol] && symbols.math[symbol].replace) { + return fontMetrics.getCharacterMetrics( + symbols.math[symbol].replace, font); + } else { + return fontMetrics.getCharacterMetrics( + symbol, font); + } +}; + +/** + * Builds a symbol in the given font size (note size is an integer) + */ +var mathrmSize = function(value, size, mode) { + return buildCommon.makeSymbol(value, "Size" + size + "-Regular", mode); +}; + +/** + * Puts a delimiter span in a given style, and adds appropriate height, depth, + * and maxFontSizes. + */ +var styleWrap = function(delim, toStyle, options) { + var span = makeSpan( + ["style-wrap", options.style.reset(), toStyle.cls()], [delim]); + + var multiplier = toStyle.sizeMultiplier / options.style.sizeMultiplier; + + span.height *= multiplier; + span.depth *= multiplier; + span.maxFontSize = toStyle.sizeMultiplier; + + return span; +}; + +/** + * Makes a small delimiter. This is a delimiter that comes in the Main-Regular + * font, but is restyled to either be in textstyle, scriptstyle, or + * scriptscriptstyle. + */ +var makeSmallDelim = function(delim, style, center, options, mode) { + var text = buildCommon.makeSymbol(delim, "Main-Regular", mode); + + var span = styleWrap(text, style, options); + + if (center) { + var shift = + (1 - options.style.sizeMultiplier / style.sizeMultiplier) * + fontMetrics.metrics.axisHeight; + + span.style.top = shift + "em"; + span.height -= shift; + span.depth += shift; + } + + return span; +}; + +/** + * Makes a large delimiter. This is a delimiter that comes in the Size1, Size2, + * Size3, or Size4 fonts. It is always rendered in textstyle. + */ +var makeLargeDelim = function(delim, size, center, options, mode) { + var inner = mathrmSize(delim, size, mode); + + var span = styleWrap( + makeSpan(["delimsizing", "size" + size], + [inner], options.getColor()), + Style.TEXT, options); + + if (center) { + var shift = (1 - options.style.sizeMultiplier) * + fontMetrics.metrics.axisHeight; + + span.style.top = shift + "em"; + span.height -= shift; + span.depth += shift; + } + + return span; +}; + +/** + * Make an inner span with the given offset and in the given font. This is used + * in `makeStackedDelim` to make the stacking pieces for the delimiter. + */ +var makeInner = function(symbol, font, mode) { + var sizeClass; + // Apply the correct CSS class to choose the right font. + if (font === "Size1-Regular") { + sizeClass = "delim-size1"; + } else if (font === "Size4-Regular") { + sizeClass = "delim-size4"; + } + + var inner = makeSpan( + ["delimsizinginner", sizeClass], + [makeSpan([], [buildCommon.makeSymbol(symbol, font, mode)])]); + + // Since this will be passed into `makeVList` in the end, wrap the element + // in the appropriate tag that VList uses. + return {type: "elem", elem: inner}; +}; + +/** + * Make a stacked delimiter out of a given delimiter, with the total height at + * least `heightTotal`. This routine is mentioned on page 442 of the TeXbook. + */ +var makeStackedDelim = function(delim, heightTotal, center, options, mode) { + // There are four parts, the top, an optional middle, a repeated part, and a + // bottom. + var top; + var middle; + var repeat; + var bottom; + top = repeat = bottom = delim; + middle = null; + // Also keep track of what font the delimiters are in + var font = "Size1-Regular"; + + // We set the parts and font based on the symbol. Note that we use + // '\u23d0' instead of '|' and '\u2016' instead of '\\|' for the + // repeats of the arrows + if (delim === "\\uparrow") { + repeat = bottom = "\u23d0"; + } else if (delim === "\\Uparrow") { + repeat = bottom = "\u2016"; + } else if (delim === "\\downarrow") { + top = repeat = "\u23d0"; + } else if (delim === "\\Downarrow") { + top = repeat = "\u2016"; + } else if (delim === "\\updownarrow") { + top = "\\uparrow"; + repeat = "\u23d0"; + bottom = "\\downarrow"; + } else if (delim === "\\Updownarrow") { + top = "\\Uparrow"; + repeat = "\u2016"; + bottom = "\\Downarrow"; + } else if (delim === "[" || delim === "\\lbrack") { + top = "\u23a1"; + repeat = "\u23a2"; + bottom = "\u23a3"; + font = "Size4-Regular"; + } else if (delim === "]" || delim === "\\rbrack") { + top = "\u23a4"; + repeat = "\u23a5"; + bottom = "\u23a6"; + font = "Size4-Regular"; + } else if (delim === "\\lfloor") { + repeat = top = "\u23a2"; + bottom = "\u23a3"; + font = "Size4-Regular"; + } else if (delim === "\\lceil") { + top = "\u23a1"; + repeat = bottom = "\u23a2"; + font = "Size4-Regular"; + } else if (delim === "\\rfloor") { + repeat = top = "\u23a5"; + bottom = "\u23a6"; + font = "Size4-Regular"; + } else if (delim === "\\rceil") { + top = "\u23a4"; + repeat = bottom = "\u23a5"; + font = "Size4-Regular"; + } else if (delim === "(") { + top = "\u239b"; + repeat = "\u239c"; + bottom = "\u239d"; + font = "Size4-Regular"; + } else if (delim === ")") { + top = "\u239e"; + repeat = "\u239f"; + bottom = "\u23a0"; + font = "Size4-Regular"; + } else if (delim === "\\{" || delim === "\\lbrace") { + top = "\u23a7"; + middle = "\u23a8"; + bottom = "\u23a9"; + repeat = "\u23aa"; + font = "Size4-Regular"; + } else if (delim === "\\}" || delim === "\\rbrace") { + top = "\u23ab"; + middle = "\u23ac"; + bottom = "\u23ad"; + repeat = "\u23aa"; + font = "Size4-Regular"; + } else if (delim === "\\lgroup") { + top = "\u23a7"; + bottom = "\u23a9"; + repeat = "\u23aa"; + font = "Size4-Regular"; + } else if (delim === "\\rgroup") { + top = "\u23ab"; + bottom = "\u23ad"; + repeat = "\u23aa"; + font = "Size4-Regular"; + } else if (delim === "\\lmoustache") { + top = "\u23a7"; + bottom = "\u23ad"; + repeat = "\u23aa"; + font = "Size4-Regular"; + } else if (delim === "\\rmoustache") { + top = "\u23ab"; + bottom = "\u23a9"; + repeat = "\u23aa"; + font = "Size4-Regular"; + } else if (delim === "\\surd") { + top = "\ue001"; + bottom = "\u23b7"; + repeat = "\ue000"; + font = "Size4-Regular"; + } + + // Get the metrics of the four sections + var topMetrics = getMetrics(top, font); + var topHeightTotal = topMetrics.height + topMetrics.depth; + var repeatMetrics = getMetrics(repeat, font); + var repeatHeightTotal = repeatMetrics.height + repeatMetrics.depth; + var bottomMetrics = getMetrics(bottom, font); + var bottomHeightTotal = bottomMetrics.height + bottomMetrics.depth; + var middleHeightTotal = 0; + var middleFactor = 1; + if (middle !== null) { + var middleMetrics = getMetrics(middle, font); + middleHeightTotal = middleMetrics.height + middleMetrics.depth; + middleFactor = 2; // repeat symmetrically above and below middle + } + + // Calcuate the minimal height that the delimiter can have. + // It is at least the size of the top, bottom, and optional middle combined. + var minHeight = topHeightTotal + bottomHeightTotal + middleHeightTotal; + + // Compute the number of copies of the repeat symbol we will need + var repeatCount = Math.ceil( + (heightTotal - minHeight) / (middleFactor * repeatHeightTotal)); + + // Compute the total height of the delimiter including all the symbols + var realHeightTotal = + minHeight + repeatCount * middleFactor * repeatHeightTotal; + + // The center of the delimiter is placed at the center of the axis. Note + // that in this context, "center" means that the delimiter should be + // centered around the axis in the current style, while normally it is + // centered around the axis in textstyle. + var axisHeight = fontMetrics.metrics.axisHeight; + if (center) { + axisHeight *= options.style.sizeMultiplier; + } + // Calculate the depth + var depth = realHeightTotal / 2 - axisHeight; + + // Now, we start building the pieces that will go into the vlist + + // Keep a list of the inner pieces + var inners = []; + + // Add the bottom symbol + inners.push(makeInner(bottom, font, mode)); + + var i; + if (middle === null) { + // Add that many symbols + for (i = 0; i < repeatCount; i++) { + inners.push(makeInner(repeat, font, mode)); + } + } else { + // When there is a middle bit, we need the middle part and two repeated + // sections + for (i = 0; i < repeatCount; i++) { + inners.push(makeInner(repeat, font, mode)); + } + inners.push(makeInner(middle, font, mode)); + for (i = 0; i < repeatCount; i++) { + inners.push(makeInner(repeat, font, mode)); + } + } + + // Add the top symbol + inners.push(makeInner(top, font, mode)); + + // Finally, build the vlist + var inner = buildCommon.makeVList(inners, "bottom", depth, options); + + return styleWrap( + makeSpan(["delimsizing", "mult"], [inner], options.getColor()), + Style.TEXT, options); +}; + +// There are three kinds of delimiters, delimiters that stack when they become +// too large +var stackLargeDelimiters = [ + "(", ")", "[", "\\lbrack", "]", "\\rbrack", + "\\{", "\\lbrace", "\\}", "\\rbrace", + "\\lfloor", "\\rfloor", "\\lceil", "\\rceil", + "\\surd", +]; + +// delimiters that always stack +var stackAlwaysDelimiters = [ + "\\uparrow", "\\downarrow", "\\updownarrow", + "\\Uparrow", "\\Downarrow", "\\Updownarrow", + "|", "\\|", "\\vert", "\\Vert", + "\\lvert", "\\rvert", "\\lVert", "\\rVert", + "\\lgroup", "\\rgroup", "\\lmoustache", "\\rmoustache", +]; + +// and delimiters that never stack +var stackNeverDelimiters = [ + "<", ">", "\\langle", "\\rangle", "/", "\\backslash", "\\lt", "\\gt", +]; + +// Metrics of the different sizes. Found by looking at TeX's output of +// $\bigl| // \Bigl| \biggl| \Biggl| \showlists$ +// Used to create stacked delimiters of appropriate sizes in makeSizedDelim. +var sizeToMaxHeight = [0, 1.2, 1.8, 2.4, 3.0]; + +/** + * Used to create a delimiter of a specific size, where `size` is 1, 2, 3, or 4. + */ +var makeSizedDelim = function(delim, size, options, mode) { + // < and > turn into \langle and \rangle in delimiters + if (delim === "<" || delim === "\\lt") { + delim = "\\langle"; + } else if (delim === ">" || delim === "\\gt") { + delim = "\\rangle"; + } + + // Sized delimiters are never centered. + if (utils.contains(stackLargeDelimiters, delim) || + utils.contains(stackNeverDelimiters, delim)) { + return makeLargeDelim(delim, size, false, options, mode); + } else if (utils.contains(stackAlwaysDelimiters, delim)) { + return makeStackedDelim( + delim, sizeToMaxHeight[size], false, options, mode); + } else { + throw new ParseError("Illegal delimiter: '" + delim + "'"); + } +}; + +/** + * There are three different sequences of delimiter sizes that the delimiters + * follow depending on the kind of delimiter. This is used when creating custom + * sized delimiters to decide whether to create a small, large, or stacked + * delimiter. + * + * In real TeX, these sequences aren't explicitly defined, but are instead + * defined inside the font metrics. Since there are only three sequences that + * are possible for the delimiters that TeX defines, it is easier to just encode + * them explicitly here. + */ + +// Delimiters that never stack try small delimiters and large delimiters only +var stackNeverDelimiterSequence = [ + {type: "small", style: Style.SCRIPTSCRIPT}, + {type: "small", style: Style.SCRIPT}, + {type: "small", style: Style.TEXT}, + {type: "large", size: 1}, + {type: "large", size: 2}, + {type: "large", size: 3}, + {type: "large", size: 4}, +]; + +// Delimiters that always stack try the small delimiters first, then stack +var stackAlwaysDelimiterSequence = [ + {type: "small", style: Style.SCRIPTSCRIPT}, + {type: "small", style: Style.SCRIPT}, + {type: "small", style: Style.TEXT}, + {type: "stack"}, +]; + +// Delimiters that stack when large try the small and then large delimiters, and +// stack afterwards +var stackLargeDelimiterSequence = [ + {type: "small", style: Style.SCRIPTSCRIPT}, + {type: "small", style: Style.SCRIPT}, + {type: "small", style: Style.TEXT}, + {type: "large", size: 1}, + {type: "large", size: 2}, + {type: "large", size: 3}, + {type: "large", size: 4}, + {type: "stack"}, +]; + +/** + * Get the font used in a delimiter based on what kind of delimiter it is. + */ +var delimTypeToFont = function(type) { + if (type.type === "small") { + return "Main-Regular"; + } else if (type.type === "large") { + return "Size" + type.size + "-Regular"; + } else if (type.type === "stack") { + return "Size4-Regular"; + } +}; + +/** + * Traverse a sequence of types of delimiters to decide what kind of delimiter + * should be used to create a delimiter of the given height+depth. + */ +var traverseSequence = function(delim, height, sequence, options) { + // Here, we choose the index we should start at in the sequences. In smaller + // sizes (which correspond to larger numbers in style.size) we start earlier + // in the sequence. Thus, scriptscript starts at index 3-3=0, script starts + // at index 3-2=1, text starts at 3-1=2, and display starts at min(2,3-0)=2 + var start = Math.min(2, 3 - options.style.size); + for (var i = start; i < sequence.length; i++) { + if (sequence[i].type === "stack") { + // This is always the last delimiter, so we just break the loop now. + break; + } + + var metrics = getMetrics(delim, delimTypeToFont(sequence[i])); + var heightDepth = metrics.height + metrics.depth; + + // Small delimiters are scaled down versions of the same font, so we + // account for the style change size. + + if (sequence[i].type === "small") { + heightDepth *= sequence[i].style.sizeMultiplier; + } + + // Check if the delimiter at this size works for the given height. + if (heightDepth > height) { + return sequence[i]; + } + } + + // If we reached the end of the sequence, return the last sequence element. + return sequence[sequence.length - 1]; +}; + +/** + * Make a delimiter of a given height+depth, with optional centering. Here, we + * traverse the sequences, and create a delimiter that the sequence tells us to. + */ +var makeCustomSizedDelim = function(delim, height, center, options, mode) { + if (delim === "<" || delim === "\\lt") { + delim = "\\langle"; + } else if (delim === ">" || delim === "\\gt") { + delim = "\\rangle"; + } + + // Decide what sequence to use + var sequence; + if (utils.contains(stackNeverDelimiters, delim)) { + sequence = stackNeverDelimiterSequence; + } else if (utils.contains(stackLargeDelimiters, delim)) { + sequence = stackLargeDelimiterSequence; + } else { + sequence = stackAlwaysDelimiterSequence; + } + + // Look through the sequence + var delimType = traverseSequence(delim, height, sequence, options); + + // Depending on the sequence element we decided on, call the appropriate + // function. + if (delimType.type === "small") { + return makeSmallDelim(delim, delimType.style, center, options, mode); + } else if (delimType.type === "large") { + return makeLargeDelim(delim, delimType.size, center, options, mode); + } else if (delimType.type === "stack") { + return makeStackedDelim(delim, height, center, options, mode); + } +}; + +/** + * Make a delimiter for use with `\left` and `\right`, given a height and depth + * of an expression that the delimiters surround. + */ +var makeLeftRightDelim = function(delim, height, depth, options, mode) { + // We always center \left/\right delimiters, so the axis is always shifted + var axisHeight = + fontMetrics.metrics.axisHeight * options.style.sizeMultiplier; + + // Taken from TeX source, tex.web, function make_left_right + var delimiterFactor = 901; + var delimiterExtend = 5.0 / fontMetrics.metrics.ptPerEm; + + var maxDistFromAxis = Math.max( + height - axisHeight, depth + axisHeight); + + var totalHeight = Math.max( + // In real TeX, calculations are done using integral values which are + // 65536 per pt, or 655360 per em. So, the division here truncates in + // TeX but doesn't here, producing different results. If we wanted to + // exactly match TeX's calculation, we could do + // Math.floor(655360 * maxDistFromAxis / 500) * + // delimiterFactor / 655360 + // (To see the difference, compare + // x^{x^{\left(\rule{0.1em}{0.68em}\right)}} + // in TeX and KaTeX) + maxDistFromAxis / 500 * delimiterFactor, + 2 * maxDistFromAxis - delimiterExtend); + + // Finally, we defer to `makeCustomSizedDelim` with our calculated total + // height + return makeCustomSizedDelim(delim, totalHeight, true, options, mode); +}; + +module.exports = { + sizedDelim: makeSizedDelim, + customSizedDelim: makeCustomSizedDelim, + leftRightDelim: makeLeftRightDelim, +}; + +},{"./ParseError":6,"./Style":9,"./buildCommon":10,"./fontMetrics":17,"./symbols":23,"./utils":25}],15:[function(require,module,exports){ +/** + * These objects store the data about the DOM nodes we create, as well as some + * extra data. They can then be transformed into real DOM nodes with the + * `toNode` function or HTML markup using `toMarkup`. They are useful for both + * storing extra properties on the nodes, as well as providing a way to easily + * work with the DOM. + * + * Similar functions for working with MathML nodes exist in mathMLTree.js. + */ +var unicodeRegexes = require("./unicodeRegexes"); +var utils = require("./utils"); + +/** + * Create an HTML className based on a list of classes. In addition to joining + * with spaces, we also remove null or empty classes. + */ +var createClass = function(classes) { + classes = classes.slice(); + for (var i = classes.length - 1; i >= 0; i--) { + if (!classes[i]) { + classes.splice(i, 1); + } + } + + return classes.join(" "); +}; + +/** + * This node represents a span node, with a className, a list of children, and + * an inline style. It also contains information about its height, depth, and + * maxFontSize. + */ +function span(classes, children, height, depth, maxFontSize, style) { + this.classes = classes || []; + this.children = children || []; + this.height = height || 0; + this.depth = depth || 0; + this.maxFontSize = maxFontSize || 0; + this.style = style || {}; + this.attributes = {}; +} + +/** + * Sets an arbitrary attribute on the span. Warning: use this wisely. Not all + * browsers support attributes the same, and having too many custom attributes + * is probably bad. + */ +span.prototype.setAttribute = function(attribute, value) { + this.attributes[attribute] = value; +}; + +/** + * Convert the span into an HTML node + */ +span.prototype.toNode = function() { + var span = document.createElement("span"); + + // Apply the class + span.className = createClass(this.classes); + + // Apply inline styles + for (var style in this.style) { + if (Object.prototype.hasOwnProperty.call(this.style, style)) { + span.style[style] = this.style[style]; + } + } + + // Apply attributes + for (var attr in this.attributes) { + if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { + span.setAttribute(attr, this.attributes[attr]); + } + } + + // Append the children, also as HTML nodes + for (var i = 0; i < this.children.length; i++) { + span.appendChild(this.children[i].toNode()); + } + + return span; +}; + +/** + * Convert the span into an HTML markup string + */ +span.prototype.toMarkup = function() { + var markup = "<span"; + + // Add the class + if (this.classes.length) { + markup += " class=\""; + markup += utils.escape(createClass(this.classes)); + markup += "\""; + } + + var styles = ""; + + // Add the styles, after hyphenation + for (var style in this.style) { + if (this.style.hasOwnProperty(style)) { + styles += utils.hyphenate(style) + ":" + this.style[style] + ";"; + } + } + + if (styles) { + markup += " style=\"" + utils.escape(styles) + "\""; + } + + // Add the attributes + for (var attr in this.attributes) { + if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { + markup += " " + attr + "=\""; + markup += utils.escape(this.attributes[attr]); + markup += "\""; + } + } + + markup += ">"; + + // Add the markup of the children, also as markup + for (var i = 0; i < this.children.length; i++) { + markup += this.children[i].toMarkup(); + } + + markup += "</span>"; + + return markup; +}; + +/** + * This node represents a document fragment, which contains elements, but when + * placed into the DOM doesn't have any representation itself. Thus, it only + * contains children and doesn't have any HTML properties. It also keeps track + * of a height, depth, and maxFontSize. + */ +function documentFragment(children, height, depth, maxFontSize) { + this.children = children || []; + this.height = height || 0; + this.depth = depth || 0; + this.maxFontSize = maxFontSize || 0; +} + +/** + * Convert the fragment into a node + */ +documentFragment.prototype.toNode = function() { + // Create a fragment + var frag = document.createDocumentFragment(); + + // Append the children + for (var i = 0; i < this.children.length; i++) { + frag.appendChild(this.children[i].toNode()); + } + + return frag; +}; + +/** + * Convert the fragment into HTML markup + */ +documentFragment.prototype.toMarkup = function() { + var markup = ""; + + // Simply concatenate the markup for the children together + for (var i = 0; i < this.children.length; i++) { + markup += this.children[i].toMarkup(); + } + + return markup; +}; + +var iCombinations = { + 'î': '\u0131\u0302', + 'ï': '\u0131\u0308', + 'í': '\u0131\u0301', + // 'ī': '\u0131\u0304', // enable when we add Extended Latin + 'ì': '\u0131\u0300', +}; + +/** + * A symbol node contains information about a single symbol. It either renders + * to a single text node, or a span with a single text node in it, depending on + * whether it has CSS classes, styles, or needs italic correction. + */ +function symbolNode(value, height, depth, italic, skew, classes, style) { + this.value = value || ""; + this.height = height || 0; + this.depth = depth || 0; + this.italic = italic || 0; + this.skew = skew || 0; + this.classes = classes || []; + this.style = style || {}; + this.maxFontSize = 0; + + // Mark CJK characters with specific classes so that we can specify which + // fonts to use. This allows us to render these characters with a serif + // font in situations where the browser would either default to a sans serif + // or render a placeholder character. + if (unicodeRegexes.cjkRegex.test(value)) { + // I couldn't find any fonts that contained Hangul as well as all of + // the other characters we wanted to test there for it gets its own + // CSS class. + if (unicodeRegexes.hangulRegex.test(value)) { + this.classes.push('hangul_fallback'); + } else { + this.classes.push('cjk_fallback'); + } + } + + if (/[îïíì]/.test(this.value)) { // add ī when we add Extended Latin + this.value = iCombinations[this.value]; + } +} + +/** + * Creates a text node or span from a symbol node. Note that a span is only + * created if it is needed. + */ +symbolNode.prototype.toNode = function() { + var node = document.createTextNode(this.value); + var span = null; + + if (this.italic > 0) { + span = document.createElement("span"); + span.style.marginRight = this.italic + "em"; + } + + if (this.classes.length > 0) { + span = span || document.createElement("span"); + span.className = createClass(this.classes); + } + + for (var style in this.style) { + if (this.style.hasOwnProperty(style)) { + span = span || document.createElement("span"); + span.style[style] = this.style[style]; + } + } + + if (span) { + span.appendChild(node); + return span; + } else { + return node; + } +}; + +/** + * Creates markup for a symbol node. + */ +symbolNode.prototype.toMarkup = function() { + // TODO(alpert): More duplication than I'd like from + // span.prototype.toMarkup and symbolNode.prototype.toNode... + var needsSpan = false; + + var markup = "<span"; + + if (this.classes.length) { + needsSpan = true; + markup += " class=\""; + markup += utils.escape(createClass(this.classes)); + markup += "\""; + } + + var styles = ""; + + if (this.italic > 0) { + styles += "margin-right:" + this.italic + "em;"; + } + for (var style in this.style) { + if (this.style.hasOwnProperty(style)) { + styles += utils.hyphenate(style) + ":" + this.style[style] + ";"; + } + } + + if (styles) { + needsSpan = true; + markup += " style=\"" + utils.escape(styles) + "\""; + } + + var escaped = utils.escape(this.value); + if (needsSpan) { + markup += ">"; + markup += escaped; + markup += "</span>"; + return markup; + } else { + return escaped; + } +}; + +module.exports = { + span: span, + documentFragment: documentFragment, + symbolNode: symbolNode, +}; + +},{"./unicodeRegexes":24,"./utils":25}],16:[function(require,module,exports){ +/* eslint no-constant-condition:0 */ +var fontMetrics = require("./fontMetrics"); +var parseData = require("./parseData"); +var ParseError = require("./ParseError"); + +var ParseNode = parseData.ParseNode; + +/** + * Parse the body of the environment, with rows delimited by \\ and + * columns delimited by &, and create a nested list in row-major order + * with one group per cell. + */ +function parseArray(parser, result) { + var row = []; + var body = [row]; + var rowGaps = []; + while (true) { + var cell = parser.parseExpression(false, null); + row.push(new ParseNode("ordgroup", cell, parser.mode)); + var next = parser.nextToken.text; + if (next === "&") { + parser.consume(); + } else if (next === "\\end") { + break; + } else if (next === "\\\\" || next === "\\cr") { + var cr = parser.parseFunction(); + rowGaps.push(cr.value.size); + row = []; + body.push(row); + } else { + throw new ParseError("Expected & or \\\\ or \\end", + parser.nextToken); + } + } + result.body = body; + result.rowGaps = rowGaps; + return new ParseNode(result.type, result, parser.mode); +} + +/* + * An environment definition is very similar to a function definition: + * it is declared with a name or a list of names, a set of properties + * and a handler containing the actual implementation. + * + * The properties include: + * - numArgs: The number of arguments after the \begin{name} function. + * - argTypes: (optional) Just like for a function + * - allowedInText: (optional) Whether or not the environment is allowed inside + * text mode (default false) (not enforced yet) + * - numOptionalArgs: (optional) Just like for a function + * A bare number instead of that object indicates the numArgs value. + * + * The handler function will receive two arguments + * - context: information and references provided by the parser + * - args: an array of arguments passed to \begin{name} + * The context contains the following properties: + * - envName: the name of the environment, one of the listed names. + * - parser: the parser object + * - lexer: the lexer object + * - positions: the positions associated with these arguments from args. + * The handler must return a ParseResult. + */ + +function defineEnvironment(names, props, handler) { + if (typeof names === "string") { + names = [names]; + } + if (typeof props === "number") { + props = { numArgs: props }; + } + // Set default values of environments + var data = { + numArgs: props.numArgs || 0, + argTypes: props.argTypes, + greediness: 1, + allowedInText: !!props.allowedInText, + numOptionalArgs: props.numOptionalArgs || 0, + handler: handler, + }; + for (var i = 0; i < names.length; ++i) { + module.exports[names[i]] = data; + } +} + +// Arrays are part of LaTeX, defined in lttab.dtx so its documentation +// is part of the source2e.pdf file of LaTeX2e source documentation. +defineEnvironment("array", { + numArgs: 1, +}, function(context, args) { + var colalign = args[0]; + colalign = colalign.value.map ? colalign.value : [colalign]; + var cols = colalign.map(function(node) { + var ca = node.value; + if ("lcr".indexOf(ca) !== -1) { + return { + type: "align", + align: ca, + }; + } else if (ca === "|") { + return { + type: "separator", + separator: "|", + }; + } + throw new ParseError( + "Unknown column alignment: " + node.value, + node); + }); + var res = { + type: "array", + cols: cols, + hskipBeforeAndAfter: true, // \@preamble in lttab.dtx + }; + res = parseArray(context.parser, res); + return res; +}); + +// The matrix environments of amsmath builds on the array environment +// of LaTeX, which is discussed above. +defineEnvironment([ + "matrix", + "pmatrix", + "bmatrix", + "Bmatrix", + "vmatrix", + "Vmatrix", +], { +}, function(context) { + var delimiters = { + "matrix": null, + "pmatrix": ["(", ")"], + "bmatrix": ["[", "]"], + "Bmatrix": ["\\{", "\\}"], + "vmatrix": ["|", "|"], + "Vmatrix": ["\\Vert", "\\Vert"], + }[context.envName]; + var res = { + type: "array", + hskipBeforeAndAfter: false, // \hskip -\arraycolsep in amsmath + }; + res = parseArray(context.parser, res); + if (delimiters) { + res = new ParseNode("leftright", { + body: [res], + left: delimiters[0], + right: delimiters[1], + }, context.mode); + } + return res; +}); + +// A cases environment (in amsmath.sty) is almost equivalent to +// \def\arraystretch{1.2}% +// \left\{\begin{array}{@{}l@{\quad}l@{}} … \end{array}\right. +defineEnvironment("cases", { +}, function(context) { + var res = { + type: "array", + arraystretch: 1.2, + cols: [{ + type: "align", + align: "l", + pregap: 0, + postgap: fontMetrics.metrics.quad, + }, { + type: "align", + align: "l", + pregap: 0, + postgap: 0, + }], + }; + res = parseArray(context.parser, res); + res = new ParseNode("leftright", { + body: [res], + left: "\\{", + right: ".", + }, context.mode); + return res; +}); + +// An aligned environment is like the align* environment +// except it operates within math mode. +// Note that we assume \nomallineskiplimit to be zero, +// so that \strut@ is the same as \strut. +defineEnvironment("aligned", { +}, function(context) { + var res = { + type: "array", + cols: [], + }; + res = parseArray(context.parser, res); + var emptyGroup = new ParseNode("ordgroup", [], context.mode); + var numCols = 0; + res.value.body.forEach(function(row) { + var i; + for (i = 1; i < row.length; i += 2) { + row[i].value.unshift(emptyGroup); + } + if (numCols < row.length) { + numCols = row.length; + } + }); + for (var i = 0; i < numCols; ++i) { + var align = "r"; + var pregap = 0; + if (i % 2 === 1) { + align = "l"; + } else if (i > 0) { + pregap = 2; // one \qquad between columns + } + res.value.cols[i] = { + type: "align", + align: align, + pregap: pregap, + postgap: 0, + }; + } + return res; +}); + +},{"./ParseError":6,"./fontMetrics":17,"./parseData":21}],17:[function(require,module,exports){ +/* eslint no-unused-vars:0 */ + +var Style = require("./Style"); +var cjkRegex = require("./unicodeRegexes").cjkRegex; + +/** + * This file contains metrics regarding fonts and individual symbols. The sigma + * and xi variables, as well as the metricMap map contain data extracted from + * TeX, TeX font metrics, and the TTF files. These data are then exposed via the + * `metrics` variable and the getCharacterMetrics function. + */ + +// These font metrics are extracted from TeX by using +// \font\a=cmmi10 +// \showthe\fontdimenX\a +// where X is the corresponding variable number. These correspond to the font +// parameters of the symbol fonts. In TeX, there are actually three sets of +// dimensions, one for each of textstyle, scriptstyle, and scriptscriptstyle, +// but we only use the textstyle ones, and scale certain dimensions accordingly. +// See the TeXbook, page 441. +var sigma1 = 0.025; +var sigma2 = 0; +var sigma3 = 0; +var sigma4 = 0; +var sigma5 = 0.431; +var sigma6 = 1; +var sigma7 = 0; +var sigma8 = 0.677; +var sigma9 = 0.394; +var sigma10 = 0.444; +var sigma11 = 0.686; +var sigma12 = 0.345; +var sigma13 = 0.413; +var sigma14 = 0.363; +var sigma15 = 0.289; +var sigma16 = 0.150; +var sigma17 = 0.247; +var sigma18 = 0.386; +var sigma19 = 0.050; +var sigma20 = 2.390; +var sigma21 = 1.01; +var sigma21Script = 0.81; +var sigma21ScriptScript = 0.71; +var sigma22 = 0.250; + +// These font metrics are extracted from TeX by using +// \font\a=cmex10 +// \showthe\fontdimenX\a +// where X is the corresponding variable number. These correspond to the font +// parameters of the extension fonts (family 3). See the TeXbook, page 441. +var xi1 = 0; +var xi2 = 0; +var xi3 = 0; +var xi4 = 0; +var xi5 = 0.431; +var xi6 = 1; +var xi7 = 0; +var xi8 = 0.04; +var xi9 = 0.111; +var xi10 = 0.166; +var xi11 = 0.2; +var xi12 = 0.6; +var xi13 = 0.1; + +// This value determines how large a pt is, for metrics which are defined in +// terms of pts. +// This value is also used in katex.less; if you change it make sure the values +// match. +var ptPerEm = 10.0; + +// The space between adjacent `|` columns in an array definition. From +// `\showthe\doublerulesep` in LaTeX. +var doubleRuleSep = 2.0 / ptPerEm; + +/** + * This is just a mapping from common names to real metrics + */ +var metrics = { + xHeight: sigma5, + quad: sigma6, + num1: sigma8, + num2: sigma9, + num3: sigma10, + denom1: sigma11, + denom2: sigma12, + sup1: sigma13, + sup2: sigma14, + sup3: sigma15, + sub1: sigma16, + sub2: sigma17, + supDrop: sigma18, + subDrop: sigma19, + axisHeight: sigma22, + defaultRuleThickness: xi8, + bigOpSpacing1: xi9, + bigOpSpacing2: xi10, + bigOpSpacing3: xi11, + bigOpSpacing4: xi12, + bigOpSpacing5: xi13, + ptPerEm: ptPerEm, + emPerEx: sigma5 / sigma6, + doubleRuleSep: doubleRuleSep, + + // TODO(alpert): Missing parallel structure here. We should probably add + // style-specific metrics for all of these. + delim1: sigma20, + getDelim2: function(style) { + if (style.size === Style.TEXT.size) { + return sigma21; + } else if (style.size === Style.SCRIPT.size) { + return sigma21Script; + } else if (style.size === Style.SCRIPTSCRIPT.size) { + return sigma21ScriptScript; + } + throw new Error("Unexpected style size: " + style.size); + }, +}; + +// This map contains a mapping from font name and character code to character +// metrics, including height, depth, italic correction, and skew (kern from the +// character to the corresponding \skewchar) +// This map is generated via `make metrics`. It should not be changed manually. +var metricMap = require("./fontMetricsData"); + +// These are very rough approximations. We default to Times New Roman which +// should have Latin-1 and Cyrillic characters, but may not depending on the +// operating system. The metrics do not account for extra height from the +// accents. In the case of Cyrillic characters which have both ascenders and +// descenders we prefer approximations with ascenders, primarily to prevent +// the fraction bar or root line from intersecting the glyph. +// TODO(kevinb) allow union of multiple glyph metrics for better accuracy. +var extraCharacterMap = { + // Latin-1 + 'À': 'A', + 'Á': 'A', + 'Â': 'A', + 'Ã': 'A', + 'Ä': 'A', + 'Å': 'A', + 'Æ': 'A', + 'Ç': 'C', + 'È': 'E', + 'É': 'E', + 'Ê': 'E', + 'Ë': 'E', + 'Ì': 'I', + 'Í': 'I', + 'Î': 'I', + 'Ï': 'I', + 'Ð': 'D', + 'Ñ': 'N', + 'Ò': 'O', + 'Ó': 'O', + 'Ô': 'O', + 'Õ': 'O', + 'Ö': 'O', + 'Ø': 'O', + 'Ù': 'U', + 'Ú': 'U', + 'Û': 'U', + 'Ü': 'U', + 'Ý': 'Y', + 'Þ': 'o', + 'ß': 'B', + 'à': 'a', + 'á': 'a', + 'â': 'a', + 'ã': 'a', + 'ä': 'a', + 'å': 'a', + 'æ': 'a', + 'ç': 'c', + 'è': 'e', + 'é': 'e', + 'ê': 'e', + 'ë': 'e', + 'ì': 'i', + 'í': 'i', + 'î': 'i', + 'ï': 'i', + 'ð': 'd', + 'ñ': 'n', + 'ò': 'o', + 'ó': 'o', + 'ô': 'o', + 'õ': 'o', + 'ö': 'o', + 'ø': 'o', + 'ù': 'u', + 'ú': 'u', + 'û': 'u', + 'ü': 'u', + 'ý': 'y', + 'þ': 'o', + 'ÿ': 'y', + + // Cyrillic + 'А': 'A', + 'Б': 'B', + 'В': 'B', + 'Г': 'F', + 'Д': 'A', + 'Е': 'E', + 'Ж': 'K', + 'З': '3', + 'И': 'N', + 'Й': 'N', + 'К': 'K', + 'Л': 'N', + 'М': 'M', + 'Н': 'H', + 'О': 'O', + 'П': 'N', + 'Р': 'P', + 'С': 'C', + 'Т': 'T', + 'У': 'y', + 'Ф': 'O', + 'Х': 'X', + 'Ц': 'U', + 'Ч': 'h', + 'Ш': 'W', + 'Щ': 'W', + 'Ъ': 'B', + 'Ы': 'X', + 'Ь': 'B', + 'Э': '3', + 'Ю': 'X', + 'Я': 'R', + 'а': 'a', + 'б': 'b', + 'в': 'a', + 'г': 'r', + 'д': 'y', + 'е': 'e', + 'ж': 'm', + 'з': 'e', + 'и': 'n', + 'й': 'n', + 'к': 'n', + 'л': 'n', + 'м': 'm', + 'н': 'n', + 'о': 'o', + 'п': 'n', + 'р': 'p', + 'с': 'c', + 'т': 'o', + 'у': 'y', + 'ф': 'b', + 'х': 'x', + 'ц': 'n', + 'ч': 'n', + 'ш': 'w', + 'щ': 'w', + 'ъ': 'a', + 'ы': 'm', + 'ь': 'a', + 'э': 'e', + 'ю': 'm', + 'я': 'r', +}; + +/** + * This function is a convenience function for looking up information in the + * metricMap table. It takes a character as a string, and a style. + * + * Note: the `width` property may be undefined if fontMetricsData.js wasn't + * built using `Make extended_metrics`. + */ +var getCharacterMetrics = function(character, style) { + var ch = character.charCodeAt(0); + if (character[0] in extraCharacterMap) { + ch = extraCharacterMap[character[0]].charCodeAt(0); + } else if (cjkRegex.test(character[0])) { + ch = 'M'.charCodeAt(0); + } + var metrics = metricMap[style][ch]; + if (metrics) { + return { + depth: metrics[0], + height: metrics[1], + italic: metrics[2], + skew: metrics[3], + width: metrics[4], + }; + } +}; + +module.exports = { + metrics: metrics, + getCharacterMetrics: getCharacterMetrics, +}; + +},{"./Style":9,"./fontMetricsData":18,"./unicodeRegexes":24}],18:[function(require,module,exports){ +module.exports = { + "AMS-Regular": { + "65": [0, 0.68889, 0, 0], + "66": [0, 0.68889, 0, 0], + "67": [0, 0.68889, 0, 0], + "68": [0, 0.68889, 0, 0], + "69": [0, 0.68889, 0, 0], + "70": [0, 0.68889, 0, 0], + "71": [0, 0.68889, 0, 0], + "72": [0, 0.68889, 0, 0], + "73": [0, 0.68889, 0, 0], + "74": [0.16667, 0.68889, 0, 0], + "75": [0, 0.68889, 0, 0], + "76": [0, 0.68889, 0, 0], + "77": [0, 0.68889, 0, 0], + "78": [0, 0.68889, 0, 0], + "79": [0.16667, 0.68889, 0, 0], + "80": [0, 0.68889, 0, 0], + "81": [0.16667, 0.68889, 0, 0], + "82": [0, 0.68889, 0, 0], + "83": [0, 0.68889, 0, 0], + "84": [0, 0.68889, 0, 0], + "85": [0, 0.68889, 0, 0], + "86": [0, 0.68889, 0, 0], + "87": [0, 0.68889, 0, 0], + "88": [0, 0.68889, 0, 0], + "89": [0, 0.68889, 0, 0], + "90": [0, 0.68889, 0, 0], + "107": [0, 0.68889, 0, 0], + "165": [0, 0.675, 0.025, 0], + "174": [0.15559, 0.69224, 0, 0], + "240": [0, 0.68889, 0, 0], + "295": [0, 0.68889, 0, 0], + "710": [0, 0.825, 0, 0], + "732": [0, 0.9, 0, 0], + "770": [0, 0.825, 0, 0], + "771": [0, 0.9, 0, 0], + "989": [0.08167, 0.58167, 0, 0], + "1008": [0, 0.43056, 0.04028, 0], + "8245": [0, 0.54986, 0, 0], + "8463": [0, 0.68889, 0, 0], + "8487": [0, 0.68889, 0, 0], + "8498": [0, 0.68889, 0, 0], + "8502": [0, 0.68889, 0, 0], + "8503": [0, 0.68889, 0, 0], + "8504": [0, 0.68889, 0, 0], + "8513": [0, 0.68889, 0, 0], + "8592": [-0.03598, 0.46402, 0, 0], + "8594": [-0.03598, 0.46402, 0, 0], + "8602": [-0.13313, 0.36687, 0, 0], + "8603": [-0.13313, 0.36687, 0, 0], + "8606": [0.01354, 0.52239, 0, 0], + "8608": [0.01354, 0.52239, 0, 0], + "8610": [0.01354, 0.52239, 0, 0], + "8611": [0.01354, 0.52239, 0, 0], + "8619": [0, 0.54986, 0, 0], + "8620": [0, 0.54986, 0, 0], + "8621": [-0.13313, 0.37788, 0, 0], + "8622": [-0.13313, 0.36687, 0, 0], + "8624": [0, 0.69224, 0, 0], + "8625": [0, 0.69224, 0, 0], + "8630": [0, 0.43056, 0, 0], + "8631": [0, 0.43056, 0, 0], + "8634": [0.08198, 0.58198, 0, 0], + "8635": [0.08198, 0.58198, 0, 0], + "8638": [0.19444, 0.69224, 0, 0], + "8639": [0.19444, 0.69224, 0, 0], + "8642": [0.19444, 0.69224, 0, 0], + "8643": [0.19444, 0.69224, 0, 0], + "8644": [0.1808, 0.675, 0, 0], + "8646": [0.1808, 0.675, 0, 0], + "8647": [0.1808, 0.675, 0, 0], + "8648": [0.19444, 0.69224, 0, 0], + "8649": [0.1808, 0.675, 0, 0], + "8650": [0.19444, 0.69224, 0, 0], + "8651": [0.01354, 0.52239, 0, 0], + "8652": [0.01354, 0.52239, 0, 0], + "8653": [-0.13313, 0.36687, 0, 0], + "8654": [-0.13313, 0.36687, 0, 0], + "8655": [-0.13313, 0.36687, 0, 0], + "8666": [0.13667, 0.63667, 0, 0], + "8667": [0.13667, 0.63667, 0, 0], + "8669": [-0.13313, 0.37788, 0, 0], + "8672": [-0.064, 0.437, 0, 0], + "8674": [-0.064, 0.437, 0, 0], + "8705": [0, 0.825, 0, 0], + "8708": [0, 0.68889, 0, 0], + "8709": [0.08167, 0.58167, 0, 0], + "8717": [0, 0.43056, 0, 0], + "8722": [-0.03598, 0.46402, 0, 0], + "8724": [0.08198, 0.69224, 0, 0], + "8726": [0.08167, 0.58167, 0, 0], + "8733": [0, 0.69224, 0, 0], + "8736": [0, 0.69224, 0, 0], + "8737": [0, 0.69224, 0, 0], + "8738": [0.03517, 0.52239, 0, 0], + "8739": [0.08167, 0.58167, 0, 0], + "8740": [0.25142, 0.74111, 0, 0], + "8741": [0.08167, 0.58167, 0, 0], + "8742": [0.25142, 0.74111, 0, 0], + "8756": [0, 0.69224, 0, 0], + "8757": [0, 0.69224, 0, 0], + "8764": [-0.13313, 0.36687, 0, 0], + "8765": [-0.13313, 0.37788, 0, 0], + "8769": [-0.13313, 0.36687, 0, 0], + "8770": [-0.03625, 0.46375, 0, 0], + "8774": [0.30274, 0.79383, 0, 0], + "8776": [-0.01688, 0.48312, 0, 0], + "8778": [0.08167, 0.58167, 0, 0], + "8782": [0.06062, 0.54986, 0, 0], + "8783": [0.06062, 0.54986, 0, 0], + "8785": [0.08198, 0.58198, 0, 0], + "8786": [0.08198, 0.58198, 0, 0], + "8787": [0.08198, 0.58198, 0, 0], + "8790": [0, 0.69224, 0, 0], + "8791": [0.22958, 0.72958, 0, 0], + "8796": [0.08198, 0.91667, 0, 0], + "8806": [0.25583, 0.75583, 0, 0], + "8807": [0.25583, 0.75583, 0, 0], + "8808": [0.25142, 0.75726, 0, 0], + "8809": [0.25142, 0.75726, 0, 0], + "8812": [0.25583, 0.75583, 0, 0], + "8814": [0.20576, 0.70576, 0, 0], + "8815": [0.20576, 0.70576, 0, 0], + "8816": [0.30274, 0.79383, 0, 0], + "8817": [0.30274, 0.79383, 0, 0], + "8818": [0.22958, 0.72958, 0, 0], + "8819": [0.22958, 0.72958, 0, 0], + "8822": [0.1808, 0.675, 0, 0], + "8823": [0.1808, 0.675, 0, 0], + "8828": [0.13667, 0.63667, 0, 0], + "8829": [0.13667, 0.63667, 0, 0], + "8830": [0.22958, 0.72958, 0, 0], + "8831": [0.22958, 0.72958, 0, 0], + "8832": [0.20576, 0.70576, 0, 0], + "8833": [0.20576, 0.70576, 0, 0], + "8840": [0.30274, 0.79383, 0, 0], + "8841": [0.30274, 0.79383, 0, 0], + "8842": [0.13597, 0.63597, 0, 0], + "8843": [0.13597, 0.63597, 0, 0], + "8847": [0.03517, 0.54986, 0, 0], + "8848": [0.03517, 0.54986, 0, 0], + "8858": [0.08198, 0.58198, 0, 0], + "8859": [0.08198, 0.58198, 0, 0], + "8861": [0.08198, 0.58198, 0, 0], + "8862": [0, 0.675, 0, 0], + "8863": [0, 0.675, 0, 0], + "8864": [0, 0.675, 0, 0], + "8865": [0, 0.675, 0, 0], + "8872": [0, 0.69224, 0, 0], + "8873": [0, 0.69224, 0, 0], + "8874": [0, 0.69224, 0, 0], + "8876": [0, 0.68889, 0, 0], + "8877": [0, 0.68889, 0, 0], + "8878": [0, 0.68889, 0, 0], + "8879": [0, 0.68889, 0, 0], + "8882": [0.03517, 0.54986, 0, 0], + "8883": [0.03517, 0.54986, 0, 0], + "8884": [0.13667, 0.63667, 0, 0], + "8885": [0.13667, 0.63667, 0, 0], + "8888": [0, 0.54986, 0, 0], + "8890": [0.19444, 0.43056, 0, 0], + "8891": [0.19444, 0.69224, 0, 0], + "8892": [0.19444, 0.69224, 0, 0], + "8901": [0, 0.54986, 0, 0], + "8903": [0.08167, 0.58167, 0, 0], + "8905": [0.08167, 0.58167, 0, 0], + "8906": [0.08167, 0.58167, 0, 0], + "8907": [0, 0.69224, 0, 0], + "8908": [0, 0.69224, 0, 0], + "8909": [-0.03598, 0.46402, 0, 0], + "8910": [0, 0.54986, 0, 0], + "8911": [0, 0.54986, 0, 0], + "8912": [0.03517, 0.54986, 0, 0], + "8913": [0.03517, 0.54986, 0, 0], + "8914": [0, 0.54986, 0, 0], + "8915": [0, 0.54986, 0, 0], + "8916": [0, 0.69224, 0, 0], + "8918": [0.0391, 0.5391, 0, 0], + "8919": [0.0391, 0.5391, 0, 0], + "8920": [0.03517, 0.54986, 0, 0], + "8921": [0.03517, 0.54986, 0, 0], + "8922": [0.38569, 0.88569, 0, 0], + "8923": [0.38569, 0.88569, 0, 0], + "8926": [0.13667, 0.63667, 0, 0], + "8927": [0.13667, 0.63667, 0, 0], + "8928": [0.30274, 0.79383, 0, 0], + "8929": [0.30274, 0.79383, 0, 0], + "8934": [0.23222, 0.74111, 0, 0], + "8935": [0.23222, 0.74111, 0, 0], + "8936": [0.23222, 0.74111, 0, 0], + "8937": [0.23222, 0.74111, 0, 0], + "8938": [0.20576, 0.70576, 0, 0], + "8939": [0.20576, 0.70576, 0, 0], + "8940": [0.30274, 0.79383, 0, 0], + "8941": [0.30274, 0.79383, 0, 0], + "8994": [0.19444, 0.69224, 0, 0], + "8995": [0.19444, 0.69224, 0, 0], + "9416": [0.15559, 0.69224, 0, 0], + "9484": [0, 0.69224, 0, 0], + "9488": [0, 0.69224, 0, 0], + "9492": [0, 0.37788, 0, 0], + "9496": [0, 0.37788, 0, 0], + "9585": [0.19444, 0.68889, 0, 0], + "9586": [0.19444, 0.74111, 0, 0], + "9632": [0, 0.675, 0, 0], + "9633": [0, 0.675, 0, 0], + "9650": [0, 0.54986, 0, 0], + "9651": [0, 0.54986, 0, 0], + "9654": [0.03517, 0.54986, 0, 0], + "9660": [0, 0.54986, 0, 0], + "9661": [0, 0.54986, 0, 0], + "9664": [0.03517, 0.54986, 0, 0], + "9674": [0.11111, 0.69224, 0, 0], + "9733": [0.19444, 0.69224, 0, 0], + "10003": [0, 0.69224, 0, 0], + "10016": [0, 0.69224, 0, 0], + "10731": [0.11111, 0.69224, 0, 0], + "10846": [0.19444, 0.75583, 0, 0], + "10877": [0.13667, 0.63667, 0, 0], + "10878": [0.13667, 0.63667, 0, 0], + "10885": [0.25583, 0.75583, 0, 0], + "10886": [0.25583, 0.75583, 0, 0], + "10887": [0.13597, 0.63597, 0, 0], + "10888": [0.13597, 0.63597, 0, 0], + "10889": [0.26167, 0.75726, 0, 0], + "10890": [0.26167, 0.75726, 0, 0], + "10891": [0.48256, 0.98256, 0, 0], + "10892": [0.48256, 0.98256, 0, 0], + "10901": [0.13667, 0.63667, 0, 0], + "10902": [0.13667, 0.63667, 0, 0], + "10933": [0.25142, 0.75726, 0, 0], + "10934": [0.25142, 0.75726, 0, 0], + "10935": [0.26167, 0.75726, 0, 0], + "10936": [0.26167, 0.75726, 0, 0], + "10937": [0.26167, 0.75726, 0, 0], + "10938": [0.26167, 0.75726, 0, 0], + "10949": [0.25583, 0.75583, 0, 0], + "10950": [0.25583, 0.75583, 0, 0], + "10955": [0.28481, 0.79383, 0, 0], + "10956": [0.28481, 0.79383, 0, 0], + "57350": [0.08167, 0.58167, 0, 0], + "57351": [0.08167, 0.58167, 0, 0], + "57352": [0.08167, 0.58167, 0, 0], + "57353": [0, 0.43056, 0.04028, 0], + "57356": [0.25142, 0.75726, 0, 0], + "57357": [0.25142, 0.75726, 0, 0], + "57358": [0.41951, 0.91951, 0, 0], + "57359": [0.30274, 0.79383, 0, 0], + "57360": [0.30274, 0.79383, 0, 0], + "57361": [0.41951, 0.91951, 0, 0], + "57366": [0.25142, 0.75726, 0, 0], + "57367": [0.25142, 0.75726, 0, 0], + "57368": [0.25142, 0.75726, 0, 0], + "57369": [0.25142, 0.75726, 0, 0], + "57370": [0.13597, 0.63597, 0, 0], + "57371": [0.13597, 0.63597, 0, 0], + }, + "Caligraphic-Regular": { + "48": [0, 0.43056, 0, 0], + "49": [0, 0.43056, 0, 0], + "50": [0, 0.43056, 0, 0], + "51": [0.19444, 0.43056, 0, 0], + "52": [0.19444, 0.43056, 0, 0], + "53": [0.19444, 0.43056, 0, 0], + "54": [0, 0.64444, 0, 0], + "55": [0.19444, 0.43056, 0, 0], + "56": [0, 0.64444, 0, 0], + "57": [0.19444, 0.43056, 0, 0], + "65": [0, 0.68333, 0, 0.19445], + "66": [0, 0.68333, 0.03041, 0.13889], + "67": [0, 0.68333, 0.05834, 0.13889], + "68": [0, 0.68333, 0.02778, 0.08334], + "69": [0, 0.68333, 0.08944, 0.11111], + "70": [0, 0.68333, 0.09931, 0.11111], + "71": [0.09722, 0.68333, 0.0593, 0.11111], + "72": [0, 0.68333, 0.00965, 0.11111], + "73": [0, 0.68333, 0.07382, 0], + "74": [0.09722, 0.68333, 0.18472, 0.16667], + "75": [0, 0.68333, 0.01445, 0.05556], + "76": [0, 0.68333, 0, 0.13889], + "77": [0, 0.68333, 0, 0.13889], + "78": [0, 0.68333, 0.14736, 0.08334], + "79": [0, 0.68333, 0.02778, 0.11111], + "80": [0, 0.68333, 0.08222, 0.08334], + "81": [0.09722, 0.68333, 0, 0.11111], + "82": [0, 0.68333, 0, 0.08334], + "83": [0, 0.68333, 0.075, 0.13889], + "84": [0, 0.68333, 0.25417, 0], + "85": [0, 0.68333, 0.09931, 0.08334], + "86": [0, 0.68333, 0.08222, 0], + "87": [0, 0.68333, 0.08222, 0.08334], + "88": [0, 0.68333, 0.14643, 0.13889], + "89": [0.09722, 0.68333, 0.08222, 0.08334], + "90": [0, 0.68333, 0.07944, 0.13889], + }, + "Fraktur-Regular": { + "33": [0, 0.69141, 0, 0], + "34": [0, 0.69141, 0, 0], + "38": [0, 0.69141, 0, 0], + "39": [0, 0.69141, 0, 0], + "40": [0.24982, 0.74947, 0, 0], + "41": [0.24982, 0.74947, 0, 0], + "42": [0, 0.62119, 0, 0], + "43": [0.08319, 0.58283, 0, 0], + "44": [0, 0.10803, 0, 0], + "45": [0.08319, 0.58283, 0, 0], + "46": [0, 0.10803, 0, 0], + "47": [0.24982, 0.74947, 0, 0], + "48": [0, 0.47534, 0, 0], + "49": [0, 0.47534, 0, 0], + "50": [0, 0.47534, 0, 0], + "51": [0.18906, 0.47534, 0, 0], + "52": [0.18906, 0.47534, 0, 0], + "53": [0.18906, 0.47534, 0, 0], + "54": [0, 0.69141, 0, 0], + "55": [0.18906, 0.47534, 0, 0], + "56": [0, 0.69141, 0, 0], + "57": [0.18906, 0.47534, 0, 0], + "58": [0, 0.47534, 0, 0], + "59": [0.12604, 0.47534, 0, 0], + "61": [-0.13099, 0.36866, 0, 0], + "63": [0, 0.69141, 0, 0], + "65": [0, 0.69141, 0, 0], + "66": [0, 0.69141, 0, 0], + "67": [0, 0.69141, 0, 0], + "68": [0, 0.69141, 0, 0], + "69": [0, 0.69141, 0, 0], + "70": [0.12604, 0.69141, 0, 0], + "71": [0, 0.69141, 0, 0], + "72": [0.06302, 0.69141, 0, 0], + "73": [0, 0.69141, 0, 0], + "74": [0.12604, 0.69141, 0, 0], + "75": [0, 0.69141, 0, 0], + "76": [0, 0.69141, 0, 0], + "77": [0, 0.69141, 0, 0], + "78": [0, 0.69141, 0, 0], + "79": [0, 0.69141, 0, 0], + "80": [0.18906, 0.69141, 0, 0], + "81": [0.03781, 0.69141, 0, 0], + "82": [0, 0.69141, 0, 0], + "83": [0, 0.69141, 0, 0], + "84": [0, 0.69141, 0, 0], + "85": [0, 0.69141, 0, 0], + "86": [0, 0.69141, 0, 0], + "87": [0, 0.69141, 0, 0], + "88": [0, 0.69141, 0, 0], + "89": [0.18906, 0.69141, 0, 0], + "90": [0.12604, 0.69141, 0, 0], + "91": [0.24982, 0.74947, 0, 0], + "93": [0.24982, 0.74947, 0, 0], + "94": [0, 0.69141, 0, 0], + "97": [0, 0.47534, 0, 0], + "98": [0, 0.69141, 0, 0], + "99": [0, 0.47534, 0, 0], + "100": [0, 0.62119, 0, 0], + "101": [0, 0.47534, 0, 0], + "102": [0.18906, 0.69141, 0, 0], + "103": [0.18906, 0.47534, 0, 0], + "104": [0.18906, 0.69141, 0, 0], + "105": [0, 0.69141, 0, 0], + "106": [0, 0.69141, 0, 0], + "107": [0, 0.69141, 0, 0], + "108": [0, 0.69141, 0, 0], + "109": [0, 0.47534, 0, 0], + "110": [0, 0.47534, 0, 0], + "111": [0, 0.47534, 0, 0], + "112": [0.18906, 0.52396, 0, 0], + "113": [0.18906, 0.47534, 0, 0], + "114": [0, 0.47534, 0, 0], + "115": [0, 0.47534, 0, 0], + "116": [0, 0.62119, 0, 0], + "117": [0, 0.47534, 0, 0], + "118": [0, 0.52396, 0, 0], + "119": [0, 0.52396, 0, 0], + "120": [0.18906, 0.47534, 0, 0], + "121": [0.18906, 0.47534, 0, 0], + "122": [0.18906, 0.47534, 0, 0], + "8216": [0, 0.69141, 0, 0], + "8217": [0, 0.69141, 0, 0], + "58112": [0, 0.62119, 0, 0], + "58113": [0, 0.62119, 0, 0], + "58114": [0.18906, 0.69141, 0, 0], + "58115": [0.18906, 0.69141, 0, 0], + "58116": [0.18906, 0.47534, 0, 0], + "58117": [0, 0.69141, 0, 0], + "58118": [0, 0.62119, 0, 0], + "58119": [0, 0.47534, 0, 0], + }, + "Main-Bold": { + "33": [0, 0.69444, 0, 0], + "34": [0, 0.69444, 0, 0], + "35": [0.19444, 0.69444, 0, 0], + "36": [0.05556, 0.75, 0, 0], + "37": [0.05556, 0.75, 0, 0], + "38": [0, 0.69444, 0, 0], + "39": [0, 0.69444, 0, 0], + "40": [0.25, 0.75, 0, 0], + "41": [0.25, 0.75, 0, 0], + "42": [0, 0.75, 0, 0], + "43": [0.13333, 0.63333, 0, 0], + "44": [0.19444, 0.15556, 0, 0], + "45": [0, 0.44444, 0, 0], + "46": [0, 0.15556, 0, 0], + "47": [0.25, 0.75, 0, 0], + "48": [0, 0.64444, 0, 0], + "49": [0, 0.64444, 0, 0], + "50": [0, 0.64444, 0, 0], + "51": [0, 0.64444, 0, 0], + "52": [0, 0.64444, 0, 0], + "53": [0, 0.64444, 0, 0], + "54": [0, 0.64444, 0, 0], + "55": [0, 0.64444, 0, 0], + "56": [0, 0.64444, 0, 0], + "57": [0, 0.64444, 0, 0], + "58": [0, 0.44444, 0, 0], + "59": [0.19444, 0.44444, 0, 0], + "60": [0.08556, 0.58556, 0, 0], + "61": [-0.10889, 0.39111, 0, 0], + "62": [0.08556, 0.58556, 0, 0], + "63": [0, 0.69444, 0, 0], + "64": [0, 0.69444, 0, 0], + "65": [0, 0.68611, 0, 0], + "66": [0, 0.68611, 0, 0], + "67": [0, 0.68611, 0, 0], + "68": [0, 0.68611, 0, 0], + "69": [0, 0.68611, 0, 0], + "70": [0, 0.68611, 0, 0], + "71": [0, 0.68611, 0, 0], + "72": [0, 0.68611, 0, 0], + "73": [0, 0.68611, 0, 0], + "74": [0, 0.68611, 0, 0], + "75": [0, 0.68611, 0, 0], + "76": [0, 0.68611, 0, 0], + "77": [0, 0.68611, 0, 0], + "78": [0, 0.68611, 0, 0], + "79": [0, 0.68611, 0, 0], + "80": [0, 0.68611, 0, 0], + "81": [0.19444, 0.68611, 0, 0], + "82": [0, 0.68611, 0, 0], + "83": [0, 0.68611, 0, 0], + "84": [0, 0.68611, 0, 0], + "85": [0, 0.68611, 0, 0], + "86": [0, 0.68611, 0.01597, 0], + "87": [0, 0.68611, 0.01597, 0], + "88": [0, 0.68611, 0, 0], + "89": [0, 0.68611, 0.02875, 0], + "90": [0, 0.68611, 0, 0], + "91": [0.25, 0.75, 0, 0], + "92": [0.25, 0.75, 0, 0], + "93": [0.25, 0.75, 0, 0], + "94": [0, 0.69444, 0, 0], + "95": [0.31, 0.13444, 0.03194, 0], + "96": [0, 0.69444, 0, 0], + "97": [0, 0.44444, 0, 0], + "98": [0, 0.69444, 0, 0], + "99": [0, 0.44444, 0, 0], + "100": [0, 0.69444, 0, 0], + "101": [0, 0.44444, 0, 0], + "102": [0, 0.69444, 0.10903, 0], + "103": [0.19444, 0.44444, 0.01597, 0], + "104": [0, 0.69444, 0, 0], + "105": [0, 0.69444, 0, 0], + "106": [0.19444, 0.69444, 0, 0], + "107": [0, 0.69444, 0, 0], + "108": [0, 0.69444, 0, 0], + "109": [0, 0.44444, 0, 0], + "110": [0, 0.44444, 0, 0], + "111": [0, 0.44444, 0, 0], + "112": [0.19444, 0.44444, 0, 0], + "113": [0.19444, 0.44444, 0, 0], + "114": [0, 0.44444, 0, 0], + "115": [0, 0.44444, 0, 0], + "116": [0, 0.63492, 0, 0], + "117": [0, 0.44444, 0, 0], + "118": [0, 0.44444, 0.01597, 0], + "119": [0, 0.44444, 0.01597, 0], + "120": [0, 0.44444, 0, 0], + "121": [0.19444, 0.44444, 0.01597, 0], + "122": [0, 0.44444, 0, 0], + "123": [0.25, 0.75, 0, 0], + "124": [0.25, 0.75, 0, 0], + "125": [0.25, 0.75, 0, 0], + "126": [0.35, 0.34444, 0, 0], + "168": [0, 0.69444, 0, 0], + "172": [0, 0.44444, 0, 0], + "175": [0, 0.59611, 0, 0], + "176": [0, 0.69444, 0, 0], + "177": [0.13333, 0.63333, 0, 0], + "180": [0, 0.69444, 0, 0], + "215": [0.13333, 0.63333, 0, 0], + "247": [0.13333, 0.63333, 0, 0], + "305": [0, 0.44444, 0, 0], + "567": [0.19444, 0.44444, 0, 0], + "710": [0, 0.69444, 0, 0], + "711": [0, 0.63194, 0, 0], + "713": [0, 0.59611, 0, 0], + "714": [0, 0.69444, 0, 0], + "715": [0, 0.69444, 0, 0], + "728": [0, 0.69444, 0, 0], + "729": [0, 0.69444, 0, 0], + "730": [0, 0.69444, 0, 0], + "732": [0, 0.69444, 0, 0], + "768": [0, 0.69444, 0, 0], + "769": [0, 0.69444, 0, 0], + "770": [0, 0.69444, 0, 0], + "771": [0, 0.69444, 0, 0], + "772": [0, 0.59611, 0, 0], + "774": [0, 0.69444, 0, 0], + "775": [0, 0.69444, 0, 0], + "776": [0, 0.69444, 0, 0], + "778": [0, 0.69444, 0, 0], + "779": [0, 0.69444, 0, 0], + "780": [0, 0.63194, 0, 0], + "824": [0.19444, 0.69444, 0, 0], + "915": [0, 0.68611, 0, 0], + "916": [0, 0.68611, 0, 0], + "920": [0, 0.68611, 0, 0], + "923": [0, 0.68611, 0, 0], + "926": [0, 0.68611, 0, 0], + "928": [0, 0.68611, 0, 0], + "931": [0, 0.68611, 0, 0], + "933": [0, 0.68611, 0, 0], + "934": [0, 0.68611, 0, 0], + "936": [0, 0.68611, 0, 0], + "937": [0, 0.68611, 0, 0], + "8211": [0, 0.44444, 0.03194, 0], + "8212": [0, 0.44444, 0.03194, 0], + "8216": [0, 0.69444, 0, 0], + "8217": [0, 0.69444, 0, 0], + "8220": [0, 0.69444, 0, 0], + "8221": [0, 0.69444, 0, 0], + "8224": [0.19444, 0.69444, 0, 0], + "8225": [0.19444, 0.69444, 0, 0], + "8242": [0, 0.55556, 0, 0], + "8407": [0, 0.72444, 0.15486, 0], + "8463": [0, 0.69444, 0, 0], + "8465": [0, 0.69444, 0, 0], + "8467": [0, 0.69444, 0, 0], + "8472": [0.19444, 0.44444, 0, 0], + "8476": [0, 0.69444, 0, 0], + "8501": [0, 0.69444, 0, 0], + "8592": [-0.10889, 0.39111, 0, 0], + "8593": [0.19444, 0.69444, 0, 0], + "8594": [-0.10889, 0.39111, 0, 0], + "8595": [0.19444, 0.69444, 0, 0], + "8596": [-0.10889, 0.39111, 0, 0], + "8597": [0.25, 0.75, 0, 0], + "8598": [0.19444, 0.69444, 0, 0], + "8599": [0.19444, 0.69444, 0, 0], + "8600": [0.19444, 0.69444, 0, 0], + "8601": [0.19444, 0.69444, 0, 0], + "8636": [-0.10889, 0.39111, 0, 0], + "8637": [-0.10889, 0.39111, 0, 0], + "8640": [-0.10889, 0.39111, 0, 0], + "8641": [-0.10889, 0.39111, 0, 0], + "8656": [-0.10889, 0.39111, 0, 0], + "8657": [0.19444, 0.69444, 0, 0], + "8658": [-0.10889, 0.39111, 0, 0], + "8659": [0.19444, 0.69444, 0, 0], + "8660": [-0.10889, 0.39111, 0, 0], + "8661": [0.25, 0.75, 0, 0], + "8704": [0, 0.69444, 0, 0], + "8706": [0, 0.69444, 0.06389, 0], + "8707": [0, 0.69444, 0, 0], + "8709": [0.05556, 0.75, 0, 0], + "8711": [0, 0.68611, 0, 0], + "8712": [0.08556, 0.58556, 0, 0], + "8715": [0.08556, 0.58556, 0, 0], + "8722": [0.13333, 0.63333, 0, 0], + "8723": [0.13333, 0.63333, 0, 0], + "8725": [0.25, 0.75, 0, 0], + "8726": [0.25, 0.75, 0, 0], + "8727": [-0.02778, 0.47222, 0, 0], + "8728": [-0.02639, 0.47361, 0, 0], + "8729": [-0.02639, 0.47361, 0, 0], + "8730": [0.18, 0.82, 0, 0], + "8733": [0, 0.44444, 0, 0], + "8734": [0, 0.44444, 0, 0], + "8736": [0, 0.69224, 0, 0], + "8739": [0.25, 0.75, 0, 0], + "8741": [0.25, 0.75, 0, 0], + "8743": [0, 0.55556, 0, 0], + "8744": [0, 0.55556, 0, 0], + "8745": [0, 0.55556, 0, 0], + "8746": [0, 0.55556, 0, 0], + "8747": [0.19444, 0.69444, 0.12778, 0], + "8764": [-0.10889, 0.39111, 0, 0], + "8768": [0.19444, 0.69444, 0, 0], + "8771": [0.00222, 0.50222, 0, 0], + "8776": [0.02444, 0.52444, 0, 0], + "8781": [0.00222, 0.50222, 0, 0], + "8801": [0.00222, 0.50222, 0, 0], + "8804": [0.19667, 0.69667, 0, 0], + "8805": [0.19667, 0.69667, 0, 0], + "8810": [0.08556, 0.58556, 0, 0], + "8811": [0.08556, 0.58556, 0, 0], + "8826": [0.08556, 0.58556, 0, 0], + "8827": [0.08556, 0.58556, 0, 0], + "8834": [0.08556, 0.58556, 0, 0], + "8835": [0.08556, 0.58556, 0, 0], + "8838": [0.19667, 0.69667, 0, 0], + "8839": [0.19667, 0.69667, 0, 0], + "8846": [0, 0.55556, 0, 0], + "8849": [0.19667, 0.69667, 0, 0], + "8850": [0.19667, 0.69667, 0, 0], + "8851": [0, 0.55556, 0, 0], + "8852": [0, 0.55556, 0, 0], + "8853": [0.13333, 0.63333, 0, 0], + "8854": [0.13333, 0.63333, 0, 0], + "8855": [0.13333, 0.63333, 0, 0], + "8856": [0.13333, 0.63333, 0, 0], + "8857": [0.13333, 0.63333, 0, 0], + "8866": [0, 0.69444, 0, 0], + "8867": [0, 0.69444, 0, 0], + "8868": [0, 0.69444, 0, 0], + "8869": [0, 0.69444, 0, 0], + "8900": [-0.02639, 0.47361, 0, 0], + "8901": [-0.02639, 0.47361, 0, 0], + "8902": [-0.02778, 0.47222, 0, 0], + "8968": [0.25, 0.75, 0, 0], + "8969": [0.25, 0.75, 0, 0], + "8970": [0.25, 0.75, 0, 0], + "8971": [0.25, 0.75, 0, 0], + "8994": [-0.13889, 0.36111, 0, 0], + "8995": [-0.13889, 0.36111, 0, 0], + "9651": [0.19444, 0.69444, 0, 0], + "9657": [-0.02778, 0.47222, 0, 0], + "9661": [0.19444, 0.69444, 0, 0], + "9667": [-0.02778, 0.47222, 0, 0], + "9711": [0.19444, 0.69444, 0, 0], + "9824": [0.12963, 0.69444, 0, 0], + "9825": [0.12963, 0.69444, 0, 0], + "9826": [0.12963, 0.69444, 0, 0], + "9827": [0.12963, 0.69444, 0, 0], + "9837": [0, 0.75, 0, 0], + "9838": [0.19444, 0.69444, 0, 0], + "9839": [0.19444, 0.69444, 0, 0], + "10216": [0.25, 0.75, 0, 0], + "10217": [0.25, 0.75, 0, 0], + "10815": [0, 0.68611, 0, 0], + "10927": [0.19667, 0.69667, 0, 0], + "10928": [0.19667, 0.69667, 0, 0], + }, + "Main-Italic": { + "33": [0, 0.69444, 0.12417, 0], + "34": [0, 0.69444, 0.06961, 0], + "35": [0.19444, 0.69444, 0.06616, 0], + "37": [0.05556, 0.75, 0.13639, 0], + "38": [0, 0.69444, 0.09694, 0], + "39": [0, 0.69444, 0.12417, 0], + "40": [0.25, 0.75, 0.16194, 0], + "41": [0.25, 0.75, 0.03694, 0], + "42": [0, 0.75, 0.14917, 0], + "43": [0.05667, 0.56167, 0.03694, 0], + "44": [0.19444, 0.10556, 0, 0], + "45": [0, 0.43056, 0.02826, 0], + "46": [0, 0.10556, 0, 0], + "47": [0.25, 0.75, 0.16194, 0], + "48": [0, 0.64444, 0.13556, 0], + "49": [0, 0.64444, 0.13556, 0], + "50": [0, 0.64444, 0.13556, 0], + "51": [0, 0.64444, 0.13556, 0], + "52": [0.19444, 0.64444, 0.13556, 0], + "53": [0, 0.64444, 0.13556, 0], + "54": [0, 0.64444, 0.13556, 0], + "55": [0.19444, 0.64444, 0.13556, 0], + "56": [0, 0.64444, 0.13556, 0], + "57": [0, 0.64444, 0.13556, 0], + "58": [0, 0.43056, 0.0582, 0], + "59": [0.19444, 0.43056, 0.0582, 0], + "61": [-0.13313, 0.36687, 0.06616, 0], + "63": [0, 0.69444, 0.1225, 0], + "64": [0, 0.69444, 0.09597, 0], + "65": [0, 0.68333, 0, 0], + "66": [0, 0.68333, 0.10257, 0], + "67": [0, 0.68333, 0.14528, 0], + "68": [0, 0.68333, 0.09403, 0], + "69": [0, 0.68333, 0.12028, 0], + "70": [0, 0.68333, 0.13305, 0], + "71": [0, 0.68333, 0.08722, 0], + "72": [0, 0.68333, 0.16389, 0], + "73": [0, 0.68333, 0.15806, 0], + "74": [0, 0.68333, 0.14028, 0], + "75": [0, 0.68333, 0.14528, 0], + "76": [0, 0.68333, 0, 0], + "77": [0, 0.68333, 0.16389, 0], + "78": [0, 0.68333, 0.16389, 0], + "79": [0, 0.68333, 0.09403, 0], + "80": [0, 0.68333, 0.10257, 0], + "81": [0.19444, 0.68333, 0.09403, 0], + "82": [0, 0.68333, 0.03868, 0], + "83": [0, 0.68333, 0.11972, 0], + "84": [0, 0.68333, 0.13305, 0], + "85": [0, 0.68333, 0.16389, 0], + "86": [0, 0.68333, 0.18361, 0], + "87": [0, 0.68333, 0.18361, 0], + "88": [0, 0.68333, 0.15806, 0], + "89": [0, 0.68333, 0.19383, 0], + "90": [0, 0.68333, 0.14528, 0], + "91": [0.25, 0.75, 0.1875, 0], + "93": [0.25, 0.75, 0.10528, 0], + "94": [0, 0.69444, 0.06646, 0], + "95": [0.31, 0.12056, 0.09208, 0], + "97": [0, 0.43056, 0.07671, 0], + "98": [0, 0.69444, 0.06312, 0], + "99": [0, 0.43056, 0.05653, 0], + "100": [0, 0.69444, 0.10333, 0], + "101": [0, 0.43056, 0.07514, 0], + "102": [0.19444, 0.69444, 0.21194, 0], + "103": [0.19444, 0.43056, 0.08847, 0], + "104": [0, 0.69444, 0.07671, 0], + "105": [0, 0.65536, 0.1019, 0], + "106": [0.19444, 0.65536, 0.14467, 0], + "107": [0, 0.69444, 0.10764, 0], + "108": [0, 0.69444, 0.10333, 0], + "109": [0, 0.43056, 0.07671, 0], + "110": [0, 0.43056, 0.07671, 0], + "111": [0, 0.43056, 0.06312, 0], + "112": [0.19444, 0.43056, 0.06312, 0], + "113": [0.19444, 0.43056, 0.08847, 0], + "114": [0, 0.43056, 0.10764, 0], + "115": [0, 0.43056, 0.08208, 0], + "116": [0, 0.61508, 0.09486, 0], + "117": [0, 0.43056, 0.07671, 0], + "118": [0, 0.43056, 0.10764, 0], + "119": [0, 0.43056, 0.10764, 0], + "120": [0, 0.43056, 0.12042, 0], + "121": [0.19444, 0.43056, 0.08847, 0], + "122": [0, 0.43056, 0.12292, 0], + "126": [0.35, 0.31786, 0.11585, 0], + "163": [0, 0.69444, 0, 0], + "305": [0, 0.43056, 0, 0.02778], + "567": [0.19444, 0.43056, 0, 0.08334], + "768": [0, 0.69444, 0, 0], + "769": [0, 0.69444, 0.09694, 0], + "770": [0, 0.69444, 0.06646, 0], + "771": [0, 0.66786, 0.11585, 0], + "772": [0, 0.56167, 0.10333, 0], + "774": [0, 0.69444, 0.10806, 0], + "775": [0, 0.66786, 0.11752, 0], + "776": [0, 0.66786, 0.10474, 0], + "778": [0, 0.69444, 0, 0], + "779": [0, 0.69444, 0.1225, 0], + "780": [0, 0.62847, 0.08295, 0], + "915": [0, 0.68333, 0.13305, 0], + "916": [0, 0.68333, 0, 0], + "920": [0, 0.68333, 0.09403, 0], + "923": [0, 0.68333, 0, 0], + "926": [0, 0.68333, 0.15294, 0], + "928": [0, 0.68333, 0.16389, 0], + "931": [0, 0.68333, 0.12028, 0], + "933": [0, 0.68333, 0.11111, 0], + "934": [0, 0.68333, 0.05986, 0], + "936": [0, 0.68333, 0.11111, 0], + "937": [0, 0.68333, 0.10257, 0], + "8211": [0, 0.43056, 0.09208, 0], + "8212": [0, 0.43056, 0.09208, 0], + "8216": [0, 0.69444, 0.12417, 0], + "8217": [0, 0.69444, 0.12417, 0], + "8220": [0, 0.69444, 0.1685, 0], + "8221": [0, 0.69444, 0.06961, 0], + "8463": [0, 0.68889, 0, 0], + }, + "Main-Regular": { + "32": [0, 0, 0, 0], + "33": [0, 0.69444, 0, 0], + "34": [0, 0.69444, 0, 0], + "35": [0.19444, 0.69444, 0, 0], + "36": [0.05556, 0.75, 0, 0], + "37": [0.05556, 0.75, 0, 0], + "38": [0, 0.69444, 0, 0], + "39": [0, 0.69444, 0, 0], + "40": [0.25, 0.75, 0, 0], + "41": [0.25, 0.75, 0, 0], + "42": [0, 0.75, 0, 0], + "43": [0.08333, 0.58333, 0, 0], + "44": [0.19444, 0.10556, 0, 0], + "45": [0, 0.43056, 0, 0], + "46": [0, 0.10556, 0, 0], + "47": [0.25, 0.75, 0, 0], + "48": [0, 0.64444, 0, 0], + "49": [0, 0.64444, 0, 0], + "50": [0, 0.64444, 0, 0], + "51": [0, 0.64444, 0, 0], + "52": [0, 0.64444, 0, 0], + "53": [0, 0.64444, 0, 0], + "54": [0, 0.64444, 0, 0], + "55": [0, 0.64444, 0, 0], + "56": [0, 0.64444, 0, 0], + "57": [0, 0.64444, 0, 0], + "58": [0, 0.43056, 0, 0], + "59": [0.19444, 0.43056, 0, 0], + "60": [0.0391, 0.5391, 0, 0], + "61": [-0.13313, 0.36687, 0, 0], + "62": [0.0391, 0.5391, 0, 0], + "63": [0, 0.69444, 0, 0], + "64": [0, 0.69444, 0, 0], + "65": [0, 0.68333, 0, 0], + "66": [0, 0.68333, 0, 0], + "67": [0, 0.68333, 0, 0], + "68": [0, 0.68333, 0, 0], + "69": [0, 0.68333, 0, 0], + "70": [0, 0.68333, 0, 0], + "71": [0, 0.68333, 0, 0], + "72": [0, 0.68333, 0, 0], + "73": [0, 0.68333, 0, 0], + "74": [0, 0.68333, 0, 0], + "75": [0, 0.68333, 0, 0], + "76": [0, 0.68333, 0, 0], + "77": [0, 0.68333, 0, 0], + "78": [0, 0.68333, 0, 0], + "79": [0, 0.68333, 0, 0], + "80": [0, 0.68333, 0, 0], + "81": [0.19444, 0.68333, 0, 0], + "82": [0, 0.68333, 0, 0], + "83": [0, 0.68333, 0, 0], + "84": [0, 0.68333, 0, 0], + "85": [0, 0.68333, 0, 0], + "86": [0, 0.68333, 0.01389, 0], + "87": [0, 0.68333, 0.01389, 0], + "88": [0, 0.68333, 0, 0], + "89": [0, 0.68333, 0.025, 0], + "90": [0, 0.68333, 0, 0], + "91": [0.25, 0.75, 0, 0], + "92": [0.25, 0.75, 0, 0], + "93": [0.25, 0.75, 0, 0], + "94": [0, 0.69444, 0, 0], + "95": [0.31, 0.12056, 0.02778, 0], + "96": [0, 0.69444, 0, 0], + "97": [0, 0.43056, 0, 0], + "98": [0, 0.69444, 0, 0], + "99": [0, 0.43056, 0, 0], + "100": [0, 0.69444, 0, 0], + "101": [0, 0.43056, 0, 0], + "102": [0, 0.69444, 0.07778, 0], + "103": [0.19444, 0.43056, 0.01389, 0], + "104": [0, 0.69444, 0, 0], + "105": [0, 0.66786, 0, 0], + "106": [0.19444, 0.66786, 0, 0], + "107": [0, 0.69444, 0, 0], + "108": [0, 0.69444, 0, 0], + "109": [0, 0.43056, 0, 0], + "110": [0, 0.43056, 0, 0], + "111": [0, 0.43056, 0, 0], + "112": [0.19444, 0.43056, 0, 0], + "113": [0.19444, 0.43056, 0, 0], + "114": [0, 0.43056, 0, 0], + "115": [0, 0.43056, 0, 0], + "116": [0, 0.61508, 0, 0], + "117": [0, 0.43056, 0, 0], + "118": [0, 0.43056, 0.01389, 0], + "119": [0, 0.43056, 0.01389, 0], + "120": [0, 0.43056, 0, 0], + "121": [0.19444, 0.43056, 0.01389, 0], + "122": [0, 0.43056, 0, 0], + "123": [0.25, 0.75, 0, 0], + "124": [0.25, 0.75, 0, 0], + "125": [0.25, 0.75, 0, 0], + "126": [0.35, 0.31786, 0, 0], + "160": [0, 0, 0, 0], + "168": [0, 0.66786, 0, 0], + "172": [0, 0.43056, 0, 0], + "175": [0, 0.56778, 0, 0], + "176": [0, 0.69444, 0, 0], + "177": [0.08333, 0.58333, 0, 0], + "180": [0, 0.69444, 0, 0], + "215": [0.08333, 0.58333, 0, 0], + "247": [0.08333, 0.58333, 0, 0], + "305": [0, 0.43056, 0, 0], + "567": [0.19444, 0.43056, 0, 0], + "710": [0, 0.69444, 0, 0], + "711": [0, 0.62847, 0, 0], + "713": [0, 0.56778, 0, 0], + "714": [0, 0.69444, 0, 0], + "715": [0, 0.69444, 0, 0], + "728": [0, 0.69444, 0, 0], + "729": [0, 0.66786, 0, 0], + "730": [0, 0.69444, 0, 0], + "732": [0, 0.66786, 0, 0], + "768": [0, 0.69444, 0, 0], + "769": [0, 0.69444, 0, 0], + "770": [0, 0.69444, 0, 0], + "771": [0, 0.66786, 0, 0], + "772": [0, 0.56778, 0, 0], + "774": [0, 0.69444, 0, 0], + "775": [0, 0.66786, 0, 0], + "776": [0, 0.66786, 0, 0], + "778": [0, 0.69444, 0, 0], + "779": [0, 0.69444, 0, 0], + "780": [0, 0.62847, 0, 0], + "824": [0.19444, 0.69444, 0, 0], + "915": [0, 0.68333, 0, 0], + "916": [0, 0.68333, 0, 0], + "920": [0, 0.68333, 0, 0], + "923": [0, 0.68333, 0, 0], + "926": [0, 0.68333, 0, 0], + "928": [0, 0.68333, 0, 0], + "931": [0, 0.68333, 0, 0], + "933": [0, 0.68333, 0, 0], + "934": [0, 0.68333, 0, 0], + "936": [0, 0.68333, 0, 0], + "937": [0, 0.68333, 0, 0], + "8211": [0, 0.43056, 0.02778, 0], + "8212": [0, 0.43056, 0.02778, 0], + "8216": [0, 0.69444, 0, 0], + "8217": [0, 0.69444, 0, 0], + "8220": [0, 0.69444, 0, 0], + "8221": [0, 0.69444, 0, 0], + "8224": [0.19444, 0.69444, 0, 0], + "8225": [0.19444, 0.69444, 0, 0], + "8230": [0, 0.12, 0, 0], + "8242": [0, 0.55556, 0, 0], + "8407": [0, 0.71444, 0.15382, 0], + "8463": [0, 0.68889, 0, 0], + "8465": [0, 0.69444, 0, 0], + "8467": [0, 0.69444, 0, 0.11111], + "8472": [0.19444, 0.43056, 0, 0.11111], + "8476": [0, 0.69444, 0, 0], + "8501": [0, 0.69444, 0, 0], + "8592": [-0.13313, 0.36687, 0, 0], + "8593": [0.19444, 0.69444, 0, 0], + "8594": [-0.13313, 0.36687, 0, 0], + "8595": [0.19444, 0.69444, 0, 0], + "8596": [-0.13313, 0.36687, 0, 0], + "8597": [0.25, 0.75, 0, 0], + "8598": [0.19444, 0.69444, 0, 0], + "8599": [0.19444, 0.69444, 0, 0], + "8600": [0.19444, 0.69444, 0, 0], + "8601": [0.19444, 0.69444, 0, 0], + "8614": [0.011, 0.511, 0, 0], + "8617": [0.011, 0.511, 0, 0], + "8618": [0.011, 0.511, 0, 0], + "8636": [-0.13313, 0.36687, 0, 0], + "8637": [-0.13313, 0.36687, 0, 0], + "8640": [-0.13313, 0.36687, 0, 0], + "8641": [-0.13313, 0.36687, 0, 0], + "8652": [0.011, 0.671, 0, 0], + "8656": [-0.13313, 0.36687, 0, 0], + "8657": [0.19444, 0.69444, 0, 0], + "8658": [-0.13313, 0.36687, 0, 0], + "8659": [0.19444, 0.69444, 0, 0], + "8660": [-0.13313, 0.36687, 0, 0], + "8661": [0.25, 0.75, 0, 0], + "8704": [0, 0.69444, 0, 0], + "8706": [0, 0.69444, 0.05556, 0.08334], + "8707": [0, 0.69444, 0, 0], + "8709": [0.05556, 0.75, 0, 0], + "8711": [0, 0.68333, 0, 0], + "8712": [0.0391, 0.5391, 0, 0], + "8715": [0.0391, 0.5391, 0, 0], + "8722": [0.08333, 0.58333, 0, 0], + "8723": [0.08333, 0.58333, 0, 0], + "8725": [0.25, 0.75, 0, 0], + "8726": [0.25, 0.75, 0, 0], + "8727": [-0.03472, 0.46528, 0, 0], + "8728": [-0.05555, 0.44445, 0, 0], + "8729": [-0.05555, 0.44445, 0, 0], + "8730": [0.2, 0.8, 0, 0], + "8733": [0, 0.43056, 0, 0], + "8734": [0, 0.43056, 0, 0], + "8736": [0, 0.69224, 0, 0], + "8739": [0.25, 0.75, 0, 0], + "8741": [0.25, 0.75, 0, 0], + "8743": [0, 0.55556, 0, 0], + "8744": [0, 0.55556, 0, 0], + "8745": [0, 0.55556, 0, 0], + "8746": [0, 0.55556, 0, 0], + "8747": [0.19444, 0.69444, 0.11111, 0], + "8764": [-0.13313, 0.36687, 0, 0], + "8768": [0.19444, 0.69444, 0, 0], + "8771": [-0.03625, 0.46375, 0, 0], + "8773": [-0.022, 0.589, 0, 0], + "8776": [-0.01688, 0.48312, 0, 0], + "8781": [-0.03625, 0.46375, 0, 0], + "8784": [-0.133, 0.67, 0, 0], + "8800": [0.215, 0.716, 0, 0], + "8801": [-0.03625, 0.46375, 0, 0], + "8804": [0.13597, 0.63597, 0, 0], + "8805": [0.13597, 0.63597, 0, 0], + "8810": [0.0391, 0.5391, 0, 0], + "8811": [0.0391, 0.5391, 0, 0], + "8826": [0.0391, 0.5391, 0, 0], + "8827": [0.0391, 0.5391, 0, 0], + "8834": [0.0391, 0.5391, 0, 0], + "8835": [0.0391, 0.5391, 0, 0], + "8838": [0.13597, 0.63597, 0, 0], + "8839": [0.13597, 0.63597, 0, 0], + "8846": [0, 0.55556, 0, 0], + "8849": [0.13597, 0.63597, 0, 0], + "8850": [0.13597, 0.63597, 0, 0], + "8851": [0, 0.55556, 0, 0], + "8852": [0, 0.55556, 0, 0], + "8853": [0.08333, 0.58333, 0, 0], + "8854": [0.08333, 0.58333, 0, 0], + "8855": [0.08333, 0.58333, 0, 0], + "8856": [0.08333, 0.58333, 0, 0], + "8857": [0.08333, 0.58333, 0, 0], + "8866": [0, 0.69444, 0, 0], + "8867": [0, 0.69444, 0, 0], + "8868": [0, 0.69444, 0, 0], + "8869": [0, 0.69444, 0, 0], + "8872": [0.249, 0.75, 0, 0], + "8900": [-0.05555, 0.44445, 0, 0], + "8901": [-0.05555, 0.44445, 0, 0], + "8902": [-0.03472, 0.46528, 0, 0], + "8904": [0.005, 0.505, 0, 0], + "8942": [0.03, 0.9, 0, 0], + "8943": [-0.19, 0.31, 0, 0], + "8945": [-0.1, 0.82, 0, 0], + "8968": [0.25, 0.75, 0, 0], + "8969": [0.25, 0.75, 0, 0], + "8970": [0.25, 0.75, 0, 0], + "8971": [0.25, 0.75, 0, 0], + "8994": [-0.14236, 0.35764, 0, 0], + "8995": [-0.14236, 0.35764, 0, 0], + "9136": [0.244, 0.744, 0, 0], + "9137": [0.244, 0.744, 0, 0], + "9651": [0.19444, 0.69444, 0, 0], + "9657": [-0.03472, 0.46528, 0, 0], + "9661": [0.19444, 0.69444, 0, 0], + "9667": [-0.03472, 0.46528, 0, 0], + "9711": [0.19444, 0.69444, 0, 0], + "9824": [0.12963, 0.69444, 0, 0], + "9825": [0.12963, 0.69444, 0, 0], + "9826": [0.12963, 0.69444, 0, 0], + "9827": [0.12963, 0.69444, 0, 0], + "9837": [0, 0.75, 0, 0], + "9838": [0.19444, 0.69444, 0, 0], + "9839": [0.19444, 0.69444, 0, 0], + "10216": [0.25, 0.75, 0, 0], + "10217": [0.25, 0.75, 0, 0], + "10222": [0.244, 0.744, 0, 0], + "10223": [0.244, 0.744, 0, 0], + "10229": [0.011, 0.511, 0, 0], + "10230": [0.011, 0.511, 0, 0], + "10231": [0.011, 0.511, 0, 0], + "10232": [0.024, 0.525, 0, 0], + "10233": [0.024, 0.525, 0, 0], + "10234": [0.024, 0.525, 0, 0], + "10236": [0.011, 0.511, 0, 0], + "10815": [0, 0.68333, 0, 0], + "10927": [0.13597, 0.63597, 0, 0], + "10928": [0.13597, 0.63597, 0, 0], + }, + "Math-BoldItalic": { + "47": [0.19444, 0.69444, 0, 0], + "65": [0, 0.68611, 0, 0], + "66": [0, 0.68611, 0.04835, 0], + "67": [0, 0.68611, 0.06979, 0], + "68": [0, 0.68611, 0.03194, 0], + "69": [0, 0.68611, 0.05451, 0], + "70": [0, 0.68611, 0.15972, 0], + "71": [0, 0.68611, 0, 0], + "72": [0, 0.68611, 0.08229, 0], + "73": [0, 0.68611, 0.07778, 0], + "74": [0, 0.68611, 0.10069, 0], + "75": [0, 0.68611, 0.06979, 0], + "76": [0, 0.68611, 0, 0], + "77": [0, 0.68611, 0.11424, 0], + "78": [0, 0.68611, 0.11424, 0], + "79": [0, 0.68611, 0.03194, 0], + "80": [0, 0.68611, 0.15972, 0], + "81": [0.19444, 0.68611, 0, 0], + "82": [0, 0.68611, 0.00421, 0], + "83": [0, 0.68611, 0.05382, 0], + "84": [0, 0.68611, 0.15972, 0], + "85": [0, 0.68611, 0.11424, 0], + "86": [0, 0.68611, 0.25555, 0], + "87": [0, 0.68611, 0.15972, 0], + "88": [0, 0.68611, 0.07778, 0], + "89": [0, 0.68611, 0.25555, 0], + "90": [0, 0.68611, 0.06979, 0], + "97": [0, 0.44444, 0, 0], + "98": [0, 0.69444, 0, 0], + "99": [0, 0.44444, 0, 0], + "100": [0, 0.69444, 0, 0], + "101": [0, 0.44444, 0, 0], + "102": [0.19444, 0.69444, 0.11042, 0], + "103": [0.19444, 0.44444, 0.03704, 0], + "104": [0, 0.69444, 0, 0], + "105": [0, 0.69326, 0, 0], + "106": [0.19444, 0.69326, 0.0622, 0], + "107": [0, 0.69444, 0.01852, 0], + "108": [0, 0.69444, 0.0088, 0], + "109": [0, 0.44444, 0, 0], + "110": [0, 0.44444, 0, 0], + "111": [0, 0.44444, 0, 0], + "112": [0.19444, 0.44444, 0, 0], + "113": [0.19444, 0.44444, 0.03704, 0], + "114": [0, 0.44444, 0.03194, 0], + "115": [0, 0.44444, 0, 0], + "116": [0, 0.63492, 0, 0], + "117": [0, 0.44444, 0, 0], + "118": [0, 0.44444, 0.03704, 0], + "119": [0, 0.44444, 0.02778, 0], + "120": [0, 0.44444, 0, 0], + "121": [0.19444, 0.44444, 0.03704, 0], + "122": [0, 0.44444, 0.04213, 0], + "915": [0, 0.68611, 0.15972, 0], + "916": [0, 0.68611, 0, 0], + "920": [0, 0.68611, 0.03194, 0], + "923": [0, 0.68611, 0, 0], + "926": [0, 0.68611, 0.07458, 0], + "928": [0, 0.68611, 0.08229, 0], + "931": [0, 0.68611, 0.05451, 0], + "933": [0, 0.68611, 0.15972, 0], + "934": [0, 0.68611, 0, 0], + "936": [0, 0.68611, 0.11653, 0], + "937": [0, 0.68611, 0.04835, 0], + "945": [0, 0.44444, 0, 0], + "946": [0.19444, 0.69444, 0.03403, 0], + "947": [0.19444, 0.44444, 0.06389, 0], + "948": [0, 0.69444, 0.03819, 0], + "949": [0, 0.44444, 0, 0], + "950": [0.19444, 0.69444, 0.06215, 0], + "951": [0.19444, 0.44444, 0.03704, 0], + "952": [0, 0.69444, 0.03194, 0], + "953": [0, 0.44444, 0, 0], + "954": [0, 0.44444, 0, 0], + "955": [0, 0.69444, 0, 0], + "956": [0.19444, 0.44444, 0, 0], + "957": [0, 0.44444, 0.06898, 0], + "958": [0.19444, 0.69444, 0.03021, 0], + "959": [0, 0.44444, 0, 0], + "960": [0, 0.44444, 0.03704, 0], + "961": [0.19444, 0.44444, 0, 0], + "962": [0.09722, 0.44444, 0.07917, 0], + "963": [0, 0.44444, 0.03704, 0], + "964": [0, 0.44444, 0.13472, 0], + "965": [0, 0.44444, 0.03704, 0], + "966": [0.19444, 0.44444, 0, 0], + "967": [0.19444, 0.44444, 0, 0], + "968": [0.19444, 0.69444, 0.03704, 0], + "969": [0, 0.44444, 0.03704, 0], + "977": [0, 0.69444, 0, 0], + "981": [0.19444, 0.69444, 0, 0], + "982": [0, 0.44444, 0.03194, 0], + "1009": [0.19444, 0.44444, 0, 0], + "1013": [0, 0.44444, 0, 0], + }, + "Math-Italic": { + "47": [0.19444, 0.69444, 0, 0], + "65": [0, 0.68333, 0, 0.13889], + "66": [0, 0.68333, 0.05017, 0.08334], + "67": [0, 0.68333, 0.07153, 0.08334], + "68": [0, 0.68333, 0.02778, 0.05556], + "69": [0, 0.68333, 0.05764, 0.08334], + "70": [0, 0.68333, 0.13889, 0.08334], + "71": [0, 0.68333, 0, 0.08334], + "72": [0, 0.68333, 0.08125, 0.05556], + "73": [0, 0.68333, 0.07847, 0.11111], + "74": [0, 0.68333, 0.09618, 0.16667], + "75": [0, 0.68333, 0.07153, 0.05556], + "76": [0, 0.68333, 0, 0.02778], + "77": [0, 0.68333, 0.10903, 0.08334], + "78": [0, 0.68333, 0.10903, 0.08334], + "79": [0, 0.68333, 0.02778, 0.08334], + "80": [0, 0.68333, 0.13889, 0.08334], + "81": [0.19444, 0.68333, 0, 0.08334], + "82": [0, 0.68333, 0.00773, 0.08334], + "83": [0, 0.68333, 0.05764, 0.08334], + "84": [0, 0.68333, 0.13889, 0.08334], + "85": [0, 0.68333, 0.10903, 0.02778], + "86": [0, 0.68333, 0.22222, 0], + "87": [0, 0.68333, 0.13889, 0], + "88": [0, 0.68333, 0.07847, 0.08334], + "89": [0, 0.68333, 0.22222, 0], + "90": [0, 0.68333, 0.07153, 0.08334], + "97": [0, 0.43056, 0, 0], + "98": [0, 0.69444, 0, 0], + "99": [0, 0.43056, 0, 0.05556], + "100": [0, 0.69444, 0, 0.16667], + "101": [0, 0.43056, 0, 0.05556], + "102": [0.19444, 0.69444, 0.10764, 0.16667], + "103": [0.19444, 0.43056, 0.03588, 0.02778], + "104": [0, 0.69444, 0, 0], + "105": [0, 0.65952, 0, 0], + "106": [0.19444, 0.65952, 0.05724, 0], + "107": [0, 0.69444, 0.03148, 0], + "108": [0, 0.69444, 0.01968, 0.08334], + "109": [0, 0.43056, 0, 0], + "110": [0, 0.43056, 0, 0], + "111": [0, 0.43056, 0, 0.05556], + "112": [0.19444, 0.43056, 0, 0.08334], + "113": [0.19444, 0.43056, 0.03588, 0.08334], + "114": [0, 0.43056, 0.02778, 0.05556], + "115": [0, 0.43056, 0, 0.05556], + "116": [0, 0.61508, 0, 0.08334], + "117": [0, 0.43056, 0, 0.02778], + "118": [0, 0.43056, 0.03588, 0.02778], + "119": [0, 0.43056, 0.02691, 0.08334], + "120": [0, 0.43056, 0, 0.02778], + "121": [0.19444, 0.43056, 0.03588, 0.05556], + "122": [0, 0.43056, 0.04398, 0.05556], + "915": [0, 0.68333, 0.13889, 0.08334], + "916": [0, 0.68333, 0, 0.16667], + "920": [0, 0.68333, 0.02778, 0.08334], + "923": [0, 0.68333, 0, 0.16667], + "926": [0, 0.68333, 0.07569, 0.08334], + "928": [0, 0.68333, 0.08125, 0.05556], + "931": [0, 0.68333, 0.05764, 0.08334], + "933": [0, 0.68333, 0.13889, 0.05556], + "934": [0, 0.68333, 0, 0.08334], + "936": [0, 0.68333, 0.11, 0.05556], + "937": [0, 0.68333, 0.05017, 0.08334], + "945": [0, 0.43056, 0.0037, 0.02778], + "946": [0.19444, 0.69444, 0.05278, 0.08334], + "947": [0.19444, 0.43056, 0.05556, 0], + "948": [0, 0.69444, 0.03785, 0.05556], + "949": [0, 0.43056, 0, 0.08334], + "950": [0.19444, 0.69444, 0.07378, 0.08334], + "951": [0.19444, 0.43056, 0.03588, 0.05556], + "952": [0, 0.69444, 0.02778, 0.08334], + "953": [0, 0.43056, 0, 0.05556], + "954": [0, 0.43056, 0, 0], + "955": [0, 0.69444, 0, 0], + "956": [0.19444, 0.43056, 0, 0.02778], + "957": [0, 0.43056, 0.06366, 0.02778], + "958": [0.19444, 0.69444, 0.04601, 0.11111], + "959": [0, 0.43056, 0, 0.05556], + "960": [0, 0.43056, 0.03588, 0], + "961": [0.19444, 0.43056, 0, 0.08334], + "962": [0.09722, 0.43056, 0.07986, 0.08334], + "963": [0, 0.43056, 0.03588, 0], + "964": [0, 0.43056, 0.1132, 0.02778], + "965": [0, 0.43056, 0.03588, 0.02778], + "966": [0.19444, 0.43056, 0, 0.08334], + "967": [0.19444, 0.43056, 0, 0.05556], + "968": [0.19444, 0.69444, 0.03588, 0.11111], + "969": [0, 0.43056, 0.03588, 0], + "977": [0, 0.69444, 0, 0.08334], + "981": [0.19444, 0.69444, 0, 0.08334], + "982": [0, 0.43056, 0.02778, 0], + "1009": [0.19444, 0.43056, 0, 0.08334], + "1013": [0, 0.43056, 0, 0.05556], + }, + "Math-Regular": { + "65": [0, 0.68333, 0, 0.13889], + "66": [0, 0.68333, 0.05017, 0.08334], + "67": [0, 0.68333, 0.07153, 0.08334], + "68": [0, 0.68333, 0.02778, 0.05556], + "69": [0, 0.68333, 0.05764, 0.08334], + "70": [0, 0.68333, 0.13889, 0.08334], + "71": [0, 0.68333, 0, 0.08334], + "72": [0, 0.68333, 0.08125, 0.05556], + "73": [0, 0.68333, 0.07847, 0.11111], + "74": [0, 0.68333, 0.09618, 0.16667], + "75": [0, 0.68333, 0.07153, 0.05556], + "76": [0, 0.68333, 0, 0.02778], + "77": [0, 0.68333, 0.10903, 0.08334], + "78": [0, 0.68333, 0.10903, 0.08334], + "79": [0, 0.68333, 0.02778, 0.08334], + "80": [0, 0.68333, 0.13889, 0.08334], + "81": [0.19444, 0.68333, 0, 0.08334], + "82": [0, 0.68333, 0.00773, 0.08334], + "83": [0, 0.68333, 0.05764, 0.08334], + "84": [0, 0.68333, 0.13889, 0.08334], + "85": [0, 0.68333, 0.10903, 0.02778], + "86": [0, 0.68333, 0.22222, 0], + "87": [0, 0.68333, 0.13889, 0], + "88": [0, 0.68333, 0.07847, 0.08334], + "89": [0, 0.68333, 0.22222, 0], + "90": [0, 0.68333, 0.07153, 0.08334], + "97": [0, 0.43056, 0, 0], + "98": [0, 0.69444, 0, 0], + "99": [0, 0.43056, 0, 0.05556], + "100": [0, 0.69444, 0, 0.16667], + "101": [0, 0.43056, 0, 0.05556], + "102": [0.19444, 0.69444, 0.10764, 0.16667], + "103": [0.19444, 0.43056, 0.03588, 0.02778], + "104": [0, 0.69444, 0, 0], + "105": [0, 0.65952, 0, 0], + "106": [0.19444, 0.65952, 0.05724, 0], + "107": [0, 0.69444, 0.03148, 0], + "108": [0, 0.69444, 0.01968, 0.08334], + "109": [0, 0.43056, 0, 0], + "110": [0, 0.43056, 0, 0], + "111": [0, 0.43056, 0, 0.05556], + "112": [0.19444, 0.43056, 0, 0.08334], + "113": [0.19444, 0.43056, 0.03588, 0.08334], + "114": [0, 0.43056, 0.02778, 0.05556], + "115": [0, 0.43056, 0, 0.05556], + "116": [0, 0.61508, 0, 0.08334], + "117": [0, 0.43056, 0, 0.02778], + "118": [0, 0.43056, 0.03588, 0.02778], + "119": [0, 0.43056, 0.02691, 0.08334], + "120": [0, 0.43056, 0, 0.02778], + "121": [0.19444, 0.43056, 0.03588, 0.05556], + "122": [0, 0.43056, 0.04398, 0.05556], + "915": [0, 0.68333, 0.13889, 0.08334], + "916": [0, 0.68333, 0, 0.16667], + "920": [0, 0.68333, 0.02778, 0.08334], + "923": [0, 0.68333, 0, 0.16667], + "926": [0, 0.68333, 0.07569, 0.08334], + "928": [0, 0.68333, 0.08125, 0.05556], + "931": [0, 0.68333, 0.05764, 0.08334], + "933": [0, 0.68333, 0.13889, 0.05556], + "934": [0, 0.68333, 0, 0.08334], + "936": [0, 0.68333, 0.11, 0.05556], + "937": [0, 0.68333, 0.05017, 0.08334], + "945": [0, 0.43056, 0.0037, 0.02778], + "946": [0.19444, 0.69444, 0.05278, 0.08334], + "947": [0.19444, 0.43056, 0.05556, 0], + "948": [0, 0.69444, 0.03785, 0.05556], + "949": [0, 0.43056, 0, 0.08334], + "950": [0.19444, 0.69444, 0.07378, 0.08334], + "951": [0.19444, 0.43056, 0.03588, 0.05556], + "952": [0, 0.69444, 0.02778, 0.08334], + "953": [0, 0.43056, 0, 0.05556], + "954": [0, 0.43056, 0, 0], + "955": [0, 0.69444, 0, 0], + "956": [0.19444, 0.43056, 0, 0.02778], + "957": [0, 0.43056, 0.06366, 0.02778], + "958": [0.19444, 0.69444, 0.04601, 0.11111], + "959": [0, 0.43056, 0, 0.05556], + "960": [0, 0.43056, 0.03588, 0], + "961": [0.19444, 0.43056, 0, 0.08334], + "962": [0.09722, 0.43056, 0.07986, 0.08334], + "963": [0, 0.43056, 0.03588, 0], + "964": [0, 0.43056, 0.1132, 0.02778], + "965": [0, 0.43056, 0.03588, 0.02778], + "966": [0.19444, 0.43056, 0, 0.08334], + "967": [0.19444, 0.43056, 0, 0.05556], + "968": [0.19444, 0.69444, 0.03588, 0.11111], + "969": [0, 0.43056, 0.03588, 0], + "977": [0, 0.69444, 0, 0.08334], + "981": [0.19444, 0.69444, 0, 0.08334], + "982": [0, 0.43056, 0.02778, 0], + "1009": [0.19444, 0.43056, 0, 0.08334], + "1013": [0, 0.43056, 0, 0.05556], + }, + "SansSerif-Regular": { + "33": [0, 0.69444, 0, 0], + "34": [0, 0.69444, 0, 0], + "35": [0.19444, 0.69444, 0, 0], + "36": [0.05556, 0.75, 0, 0], + "37": [0.05556, 0.75, 0, 0], + "38": [0, 0.69444, 0, 0], + "39": [0, 0.69444, 0, 0], + "40": [0.25, 0.75, 0, 0], + "41": [0.25, 0.75, 0, 0], + "42": [0, 0.75, 0, 0], + "43": [0.08333, 0.58333, 0, 0], + "44": [0.125, 0.08333, 0, 0], + "45": [0, 0.44444, 0, 0], + "46": [0, 0.08333, 0, 0], + "47": [0.25, 0.75, 0, 0], + "48": [0, 0.65556, 0, 0], + "49": [0, 0.65556, 0, 0], + "50": [0, 0.65556, 0, 0], + "51": [0, 0.65556, 0, 0], + "52": [0, 0.65556, 0, 0], + "53": [0, 0.65556, 0, 0], + "54": [0, 0.65556, 0, 0], + "55": [0, 0.65556, 0, 0], + "56": [0, 0.65556, 0, 0], + "57": [0, 0.65556, 0, 0], + "58": [0, 0.44444, 0, 0], + "59": [0.125, 0.44444, 0, 0], + "61": [-0.13, 0.37, 0, 0], + "63": [0, 0.69444, 0, 0], + "64": [0, 0.69444, 0, 0], + "65": [0, 0.69444, 0, 0], + "66": [0, 0.69444, 0, 0], + "67": [0, 0.69444, 0, 0], + "68": [0, 0.69444, 0, 0], + "69": [0, 0.69444, 0, 0], + "70": [0, 0.69444, 0, 0], + "71": [0, 0.69444, 0, 0], + "72": [0, 0.69444, 0, 0], + "73": [0, 0.69444, 0, 0], + "74": [0, 0.69444, 0, 0], + "75": [0, 0.69444, 0, 0], + "76": [0, 0.69444, 0, 0], + "77": [0, 0.69444, 0, 0], + "78": [0, 0.69444, 0, 0], + "79": [0, 0.69444, 0, 0], + "80": [0, 0.69444, 0, 0], + "81": [0.125, 0.69444, 0, 0], + "82": [0, 0.69444, 0, 0], + "83": [0, 0.69444, 0, 0], + "84": [0, 0.69444, 0, 0], + "85": [0, 0.69444, 0, 0], + "86": [0, 0.69444, 0.01389, 0], + "87": [0, 0.69444, 0.01389, 0], + "88": [0, 0.69444, 0, 0], + "89": [0, 0.69444, 0.025, 0], + "90": [0, 0.69444, 0, 0], + "91": [0.25, 0.75, 0, 0], + "93": [0.25, 0.75, 0, 0], + "94": [0, 0.69444, 0, 0], + "95": [0.35, 0.09444, 0.02778, 0], + "97": [0, 0.44444, 0, 0], + "98": [0, 0.69444, 0, 0], + "99": [0, 0.44444, 0, 0], + "100": [0, 0.69444, 0, 0], + "101": [0, 0.44444, 0, 0], + "102": [0, 0.69444, 0.06944, 0], + "103": [0.19444, 0.44444, 0.01389, 0], + "104": [0, 0.69444, 0, 0], + "105": [0, 0.67937, 0, 0], + "106": [0.19444, 0.67937, 0, 0], + "107": [0, 0.69444, 0, 0], + "108": [0, 0.69444, 0, 0], + "109": [0, 0.44444, 0, 0], + "110": [0, 0.44444, 0, 0], + "111": [0, 0.44444, 0, 0], + "112": [0.19444, 0.44444, 0, 0], + "113": [0.19444, 0.44444, 0, 0], + "114": [0, 0.44444, 0.01389, 0], + "115": [0, 0.44444, 0, 0], + "116": [0, 0.57143, 0, 0], + "117": [0, 0.44444, 0, 0], + "118": [0, 0.44444, 0.01389, 0], + "119": [0, 0.44444, 0.01389, 0], + "120": [0, 0.44444, 0, 0], + "121": [0.19444, 0.44444, 0.01389, 0], + "122": [0, 0.44444, 0, 0], + "126": [0.35, 0.32659, 0, 0], + "305": [0, 0.44444, 0, 0], + "567": [0.19444, 0.44444, 0, 0], + "768": [0, 0.69444, 0, 0], + "769": [0, 0.69444, 0, 0], + "770": [0, 0.69444, 0, 0], + "771": [0, 0.67659, 0, 0], + "772": [0, 0.60889, 0, 0], + "774": [0, 0.69444, 0, 0], + "775": [0, 0.67937, 0, 0], + "776": [0, 0.67937, 0, 0], + "778": [0, 0.69444, 0, 0], + "779": [0, 0.69444, 0, 0], + "780": [0, 0.63194, 0, 0], + "915": [0, 0.69444, 0, 0], + "916": [0, 0.69444, 0, 0], + "920": [0, 0.69444, 0, 0], + "923": [0, 0.69444, 0, 0], + "926": [0, 0.69444, 0, 0], + "928": [0, 0.69444, 0, 0], + "931": [0, 0.69444, 0, 0], + "933": [0, 0.69444, 0, 0], + "934": [0, 0.69444, 0, 0], + "936": [0, 0.69444, 0, 0], + "937": [0, 0.69444, 0, 0], + "8211": [0, 0.44444, 0.02778, 0], + "8212": [0, 0.44444, 0.02778, 0], + "8216": [0, 0.69444, 0, 0], + "8217": [0, 0.69444, 0, 0], + "8220": [0, 0.69444, 0, 0], + "8221": [0, 0.69444, 0, 0], + }, + "Script-Regular": { + "65": [0, 0.7, 0.22925, 0], + "66": [0, 0.7, 0.04087, 0], + "67": [0, 0.7, 0.1689, 0], + "68": [0, 0.7, 0.09371, 0], + "69": [0, 0.7, 0.18583, 0], + "70": [0, 0.7, 0.13634, 0], + "71": [0, 0.7, 0.17322, 0], + "72": [0, 0.7, 0.29694, 0], + "73": [0, 0.7, 0.19189, 0], + "74": [0.27778, 0.7, 0.19189, 0], + "75": [0, 0.7, 0.31259, 0], + "76": [0, 0.7, 0.19189, 0], + "77": [0, 0.7, 0.15981, 0], + "78": [0, 0.7, 0.3525, 0], + "79": [0, 0.7, 0.08078, 0], + "80": [0, 0.7, 0.08078, 0], + "81": [0, 0.7, 0.03305, 0], + "82": [0, 0.7, 0.06259, 0], + "83": [0, 0.7, 0.19189, 0], + "84": [0, 0.7, 0.29087, 0], + "85": [0, 0.7, 0.25815, 0], + "86": [0, 0.7, 0.27523, 0], + "87": [0, 0.7, 0.27523, 0], + "88": [0, 0.7, 0.26006, 0], + "89": [0, 0.7, 0.2939, 0], + "90": [0, 0.7, 0.24037, 0], + }, + "Size1-Regular": { + "40": [0.35001, 0.85, 0, 0], + "41": [0.35001, 0.85, 0, 0], + "47": [0.35001, 0.85, 0, 0], + "91": [0.35001, 0.85, 0, 0], + "92": [0.35001, 0.85, 0, 0], + "93": [0.35001, 0.85, 0, 0], + "123": [0.35001, 0.85, 0, 0], + "125": [0.35001, 0.85, 0, 0], + "710": [0, 0.72222, 0, 0], + "732": [0, 0.72222, 0, 0], + "770": [0, 0.72222, 0, 0], + "771": [0, 0.72222, 0, 0], + "8214": [-0.00099, 0.601, 0, 0], + "8593": [1e-05, 0.6, 0, 0], + "8595": [1e-05, 0.6, 0, 0], + "8657": [1e-05, 0.6, 0, 0], + "8659": [1e-05, 0.6, 0, 0], + "8719": [0.25001, 0.75, 0, 0], + "8720": [0.25001, 0.75, 0, 0], + "8721": [0.25001, 0.75, 0, 0], + "8730": [0.35001, 0.85, 0, 0], + "8739": [-0.00599, 0.606, 0, 0], + "8741": [-0.00599, 0.606, 0, 0], + "8747": [0.30612, 0.805, 0.19445, 0], + "8748": [0.306, 0.805, 0.19445, 0], + "8749": [0.306, 0.805, 0.19445, 0], + "8750": [0.30612, 0.805, 0.19445, 0], + "8896": [0.25001, 0.75, 0, 0], + "8897": [0.25001, 0.75, 0, 0], + "8898": [0.25001, 0.75, 0, 0], + "8899": [0.25001, 0.75, 0, 0], + "8968": [0.35001, 0.85, 0, 0], + "8969": [0.35001, 0.85, 0, 0], + "8970": [0.35001, 0.85, 0, 0], + "8971": [0.35001, 0.85, 0, 0], + "9168": [-0.00099, 0.601, 0, 0], + "10216": [0.35001, 0.85, 0, 0], + "10217": [0.35001, 0.85, 0, 0], + "10752": [0.25001, 0.75, 0, 0], + "10753": [0.25001, 0.75, 0, 0], + "10754": [0.25001, 0.75, 0, 0], + "10756": [0.25001, 0.75, 0, 0], + "10758": [0.25001, 0.75, 0, 0], + }, + "Size2-Regular": { + "40": [0.65002, 1.15, 0, 0], + "41": [0.65002, 1.15, 0, 0], + "47": [0.65002, 1.15, 0, 0], + "91": [0.65002, 1.15, 0, 0], + "92": [0.65002, 1.15, 0, 0], + "93": [0.65002, 1.15, 0, 0], + "123": [0.65002, 1.15, 0, 0], + "125": [0.65002, 1.15, 0, 0], + "710": [0, 0.75, 0, 0], + "732": [0, 0.75, 0, 0], + "770": [0, 0.75, 0, 0], + "771": [0, 0.75, 0, 0], + "8719": [0.55001, 1.05, 0, 0], + "8720": [0.55001, 1.05, 0, 0], + "8721": [0.55001, 1.05, 0, 0], + "8730": [0.65002, 1.15, 0, 0], + "8747": [0.86225, 1.36, 0.44445, 0], + "8748": [0.862, 1.36, 0.44445, 0], + "8749": [0.862, 1.36, 0.44445, 0], + "8750": [0.86225, 1.36, 0.44445, 0], + "8896": [0.55001, 1.05, 0, 0], + "8897": [0.55001, 1.05, 0, 0], + "8898": [0.55001, 1.05, 0, 0], + "8899": [0.55001, 1.05, 0, 0], + "8968": [0.65002, 1.15, 0, 0], + "8969": [0.65002, 1.15, 0, 0], + "8970": [0.65002, 1.15, 0, 0], + "8971": [0.65002, 1.15, 0, 0], + "10216": [0.65002, 1.15, 0, 0], + "10217": [0.65002, 1.15, 0, 0], + "10752": [0.55001, 1.05, 0, 0], + "10753": [0.55001, 1.05, 0, 0], + "10754": [0.55001, 1.05, 0, 0], + "10756": [0.55001, 1.05, 0, 0], + "10758": [0.55001, 1.05, 0, 0], + }, + "Size3-Regular": { + "40": [0.95003, 1.45, 0, 0], + "41": [0.95003, 1.45, 0, 0], + "47": [0.95003, 1.45, 0, 0], + "91": [0.95003, 1.45, 0, 0], + "92": [0.95003, 1.45, 0, 0], + "93": [0.95003, 1.45, 0, 0], + "123": [0.95003, 1.45, 0, 0], + "125": [0.95003, 1.45, 0, 0], + "710": [0, 0.75, 0, 0], + "732": [0, 0.75, 0, 0], + "770": [0, 0.75, 0, 0], + "771": [0, 0.75, 0, 0], + "8730": [0.95003, 1.45, 0, 0], + "8968": [0.95003, 1.45, 0, 0], + "8969": [0.95003, 1.45, 0, 0], + "8970": [0.95003, 1.45, 0, 0], + "8971": [0.95003, 1.45, 0, 0], + "10216": [0.95003, 1.45, 0, 0], + "10217": [0.95003, 1.45, 0, 0], + }, + "Size4-Regular": { + "40": [1.25003, 1.75, 0, 0], + "41": [1.25003, 1.75, 0, 0], + "47": [1.25003, 1.75, 0, 0], + "91": [1.25003, 1.75, 0, 0], + "92": [1.25003, 1.75, 0, 0], + "93": [1.25003, 1.75, 0, 0], + "123": [1.25003, 1.75, 0, 0], + "125": [1.25003, 1.75, 0, 0], + "710": [0, 0.825, 0, 0], + "732": [0, 0.825, 0, 0], + "770": [0, 0.825, 0, 0], + "771": [0, 0.825, 0, 0], + "8730": [1.25003, 1.75, 0, 0], + "8968": [1.25003, 1.75, 0, 0], + "8969": [1.25003, 1.75, 0, 0], + "8970": [1.25003, 1.75, 0, 0], + "8971": [1.25003, 1.75, 0, 0], + "9115": [0.64502, 1.155, 0, 0], + "9116": [1e-05, 0.6, 0, 0], + "9117": [0.64502, 1.155, 0, 0], + "9118": [0.64502, 1.155, 0, 0], + "9119": [1e-05, 0.6, 0, 0], + "9120": [0.64502, 1.155, 0, 0], + "9121": [0.64502, 1.155, 0, 0], + "9122": [-0.00099, 0.601, 0, 0], + "9123": [0.64502, 1.155, 0, 0], + "9124": [0.64502, 1.155, 0, 0], + "9125": [-0.00099, 0.601, 0, 0], + "9126": [0.64502, 1.155, 0, 0], + "9127": [1e-05, 0.9, 0, 0], + "9128": [0.65002, 1.15, 0, 0], + "9129": [0.90001, 0, 0, 0], + "9130": [0, 0.3, 0, 0], + "9131": [1e-05, 0.9, 0, 0], + "9132": [0.65002, 1.15, 0, 0], + "9133": [0.90001, 0, 0, 0], + "9143": [0.88502, 0.915, 0, 0], + "10216": [1.25003, 1.75, 0, 0], + "10217": [1.25003, 1.75, 0, 0], + "57344": [-0.00499, 0.605, 0, 0], + "57345": [-0.00499, 0.605, 0, 0], + "57680": [0, 0.12, 0, 0], + "57681": [0, 0.12, 0, 0], + "57682": [0, 0.12, 0, 0], + "57683": [0, 0.12, 0, 0], + }, + "Typewriter-Regular": { + "33": [0, 0.61111, 0, 0], + "34": [0, 0.61111, 0, 0], + "35": [0, 0.61111, 0, 0], + "36": [0.08333, 0.69444, 0, 0], + "37": [0.08333, 0.69444, 0, 0], + "38": [0, 0.61111, 0, 0], + "39": [0, 0.61111, 0, 0], + "40": [0.08333, 0.69444, 0, 0], + "41": [0.08333, 0.69444, 0, 0], + "42": [0, 0.52083, 0, 0], + "43": [-0.08056, 0.53055, 0, 0], + "44": [0.13889, 0.125, 0, 0], + "45": [-0.08056, 0.53055, 0, 0], + "46": [0, 0.125, 0, 0], + "47": [0.08333, 0.69444, 0, 0], + "48": [0, 0.61111, 0, 0], + "49": [0, 0.61111, 0, 0], + "50": [0, 0.61111, 0, 0], + "51": [0, 0.61111, 0, 0], + "52": [0, 0.61111, 0, 0], + "53": [0, 0.61111, 0, 0], + "54": [0, 0.61111, 0, 0], + "55": [0, 0.61111, 0, 0], + "56": [0, 0.61111, 0, 0], + "57": [0, 0.61111, 0, 0], + "58": [0, 0.43056, 0, 0], + "59": [0.13889, 0.43056, 0, 0], + "60": [-0.05556, 0.55556, 0, 0], + "61": [-0.19549, 0.41562, 0, 0], + "62": [-0.05556, 0.55556, 0, 0], + "63": [0, 0.61111, 0, 0], + "64": [0, 0.61111, 0, 0], + "65": [0, 0.61111, 0, 0], + "66": [0, 0.61111, 0, 0], + "67": [0, 0.61111, 0, 0], + "68": [0, 0.61111, 0, 0], + "69": [0, 0.61111, 0, 0], + "70": [0, 0.61111, 0, 0], + "71": [0, 0.61111, 0, 0], + "72": [0, 0.61111, 0, 0], + "73": [0, 0.61111, 0, 0], + "74": [0, 0.61111, 0, 0], + "75": [0, 0.61111, 0, 0], + "76": [0, 0.61111, 0, 0], + "77": [0, 0.61111, 0, 0], + "78": [0, 0.61111, 0, 0], + "79": [0, 0.61111, 0, 0], + "80": [0, 0.61111, 0, 0], + "81": [0.13889, 0.61111, 0, 0], + "82": [0, 0.61111, 0, 0], + "83": [0, 0.61111, 0, 0], + "84": [0, 0.61111, 0, 0], + "85": [0, 0.61111, 0, 0], + "86": [0, 0.61111, 0, 0], + "87": [0, 0.61111, 0, 0], + "88": [0, 0.61111, 0, 0], + "89": [0, 0.61111, 0, 0], + "90": [0, 0.61111, 0, 0], + "91": [0.08333, 0.69444, 0, 0], + "92": [0.08333, 0.69444, 0, 0], + "93": [0.08333, 0.69444, 0, 0], + "94": [0, 0.61111, 0, 0], + "95": [0.09514, 0, 0, 0], + "96": [0, 0.61111, 0, 0], + "97": [0, 0.43056, 0, 0], + "98": [0, 0.61111, 0, 0], + "99": [0, 0.43056, 0, 0], + "100": [0, 0.61111, 0, 0], + "101": [0, 0.43056, 0, 0], + "102": [0, 0.61111, 0, 0], + "103": [0.22222, 0.43056, 0, 0], + "104": [0, 0.61111, 0, 0], + "105": [0, 0.61111, 0, 0], + "106": [0.22222, 0.61111, 0, 0], + "107": [0, 0.61111, 0, 0], + "108": [0, 0.61111, 0, 0], + "109": [0, 0.43056, 0, 0], + "110": [0, 0.43056, 0, 0], + "111": [0, 0.43056, 0, 0], + "112": [0.22222, 0.43056, 0, 0], + "113": [0.22222, 0.43056, 0, 0], + "114": [0, 0.43056, 0, 0], + "115": [0, 0.43056, 0, 0], + "116": [0, 0.55358, 0, 0], + "117": [0, 0.43056, 0, 0], + "118": [0, 0.43056, 0, 0], + "119": [0, 0.43056, 0, 0], + "120": [0, 0.43056, 0, 0], + "121": [0.22222, 0.43056, 0, 0], + "122": [0, 0.43056, 0, 0], + "123": [0.08333, 0.69444, 0, 0], + "124": [0.08333, 0.69444, 0, 0], + "125": [0.08333, 0.69444, 0, 0], + "126": [0, 0.61111, 0, 0], + "127": [0, 0.61111, 0, 0], + "305": [0, 0.43056, 0, 0], + "567": [0.22222, 0.43056, 0, 0], + "768": [0, 0.61111, 0, 0], + "769": [0, 0.61111, 0, 0], + "770": [0, 0.61111, 0, 0], + "771": [0, 0.61111, 0, 0], + "772": [0, 0.56555, 0, 0], + "774": [0, 0.61111, 0, 0], + "776": [0, 0.61111, 0, 0], + "778": [0, 0.61111, 0, 0], + "780": [0, 0.56597, 0, 0], + "915": [0, 0.61111, 0, 0], + "916": [0, 0.61111, 0, 0], + "920": [0, 0.61111, 0, 0], + "923": [0, 0.61111, 0, 0], + "926": [0, 0.61111, 0, 0], + "928": [0, 0.61111, 0, 0], + "931": [0, 0.61111, 0, 0], + "933": [0, 0.61111, 0, 0], + "934": [0, 0.61111, 0, 0], + "936": [0, 0.61111, 0, 0], + "937": [0, 0.61111, 0, 0], + "2018": [0, 0.61111, 0, 0], + "2019": [0, 0.61111, 0, 0], + "8242": [0, 0.61111, 0, 0], + }, +}; + +},{}],19:[function(require,module,exports){ +var utils = require("./utils"); +var ParseError = require("./ParseError"); + +/* This file contains a list of functions that we parse, identified by + * the calls to defineFunction. + * + * The first argument to defineFunction is a single name or a list of names. + * All functions named in such a list will share a single implementation. + * + * Each declared function can have associated properties, which + * include the following: + * + * - numArgs: The number of arguments the function takes. + * If this is the only property, it can be passed as a number + * instead of an element of a properties object. + * - argTypes: (optional) An array corresponding to each argument of the + * function, giving the type of argument that should be parsed. Its + * length should be equal to `numArgs + numOptionalArgs`. Valid + * types: + * - "size": A size-like thing, such as "1em" or "5ex" + * - "color": An html color, like "#abc" or "blue" + * - "original": The same type as the environment that the + * function being parsed is in (e.g. used for the + * bodies of functions like \color where the first + * argument is special and the second argument is + * parsed normally) + * Other possible types (probably shouldn't be used) + * - "text": Text-like (e.g. \text) + * - "math": Normal math + * If undefined, this will be treated as an appropriate length + * array of "original" strings + * - greediness: (optional) The greediness of the function to use ungrouped + * arguments. + * + * E.g. if you have an expression + * \sqrt \frac 1 2 + * since \frac has greediness=2 vs \sqrt's greediness=1, \frac + * will use the two arguments '1' and '2' as its two arguments, + * then that whole function will be used as the argument to + * \sqrt. On the other hand, the expressions + * \frac \frac 1 2 3 + * and + * \frac \sqrt 1 2 + * will fail because \frac and \frac have equal greediness + * and \sqrt has a lower greediness than \frac respectively. To + * make these parse, we would have to change them to: + * \frac {\frac 1 2} 3 + * and + * \frac {\sqrt 1} 2 + * + * The default value is `1` + * - allowedInText: (optional) Whether or not the function is allowed inside + * text mode (default false) + * - numOptionalArgs: (optional) The number of optional arguments the function + * should parse. If the optional arguments aren't found, + * `null` will be passed to the handler in their place. + * (default 0) + * - infix: (optional) Must be true if the function is an infix operator. + * + * The last argument is that implementation, the handler for the function(s). + * It is called to handle these functions and their arguments. + * It receives two arguments: + * - context contains information and references provided by the parser + * - args is an array of arguments obtained from TeX input + * The context contains the following properties: + * - funcName: the text (i.e. name) of the function, including \ + * - parser: the parser object + * - lexer: the lexer object + * - positions: the positions in the overall string of the function + * and the arguments. + * The latter three should only be used to produce error messages. + * + * The function should return an object with the following keys: + * - type: The type of element that this is. This is then used in + * buildHTML/buildMathML to determine which function + * should be called to build this node into a DOM node + * Any other data can be added to the object, which will be passed + * in to the function in buildHTML/buildMathML as `group.value`. + */ + +function defineFunction(names, props, handler) { + if (typeof names === "string") { + names = [names]; + } + if (typeof props === "number") { + props = { numArgs: props }; + } + // Set default values of functions + var data = { + numArgs: props.numArgs, + argTypes: props.argTypes, + greediness: (props.greediness === undefined) ? 1 : props.greediness, + allowedInText: !!props.allowedInText, + numOptionalArgs: props.numOptionalArgs || 0, + infix: !!props.infix, + handler: handler, + }; + for (var i = 0; i < names.length; ++i) { + module.exports[names[i]] = data; + } +} + +// A normal square root +defineFunction("\\sqrt", { + numArgs: 1, + numOptionalArgs: 1, +}, function(context, args) { + var index = args[0]; + var body = args[1]; + return { + type: "sqrt", + body: body, + index: index, + }; +}); + +// Some non-mathy text +defineFunction("\\text", { + numArgs: 1, + argTypes: ["text"], + greediness: 2, +}, function(context, args) { + var body = args[0]; + // Since the corresponding buildHTML/buildMathML function expects a + // list of elements, we normalize for different kinds of arguments + // TODO(emily): maybe this should be done somewhere else + var inner; + if (body.type === "ordgroup") { + inner = body.value; + } else { + inner = [body]; + } + + return { + type: "text", + body: inner, + }; +}); + +// A two-argument custom color +defineFunction("\\color", { + numArgs: 2, + allowedInText: true, + greediness: 3, + argTypes: ["color", "original"], +}, function(context, args) { + var color = args[0]; + var body = args[1]; + // Normalize the different kinds of bodies (see \text above) + var inner; + if (body.type === "ordgroup") { + inner = body.value; + } else { + inner = [body]; + } + + return { + type: "color", + color: color.value, + value: inner, + }; +}); + +// An overline +defineFunction("\\overline", { + numArgs: 1, +}, function(context, args) { + var body = args[0]; + return { + type: "overline", + body: body, + }; +}); + +// An underline +defineFunction("\\underline", { + numArgs: 1, +}, function(context, args) { + var body = args[0]; + return { + type: "underline", + body: body, + }; +}); + +// A box of the width and height +defineFunction("\\rule", { + numArgs: 2, + numOptionalArgs: 1, + argTypes: ["size", "size", "size"], +}, function(context, args) { + var shift = args[0]; + var width = args[1]; + var height = args[2]; + return { + type: "rule", + shift: shift && shift.value, + width: width.value, + height: height.value, + }; +}); + +defineFunction("\\kern", { + numArgs: 1, + argTypes: ["size"], +}, function(context, args) { + return { + type: "kern", + dimension: args[0].value, + }; +}); + +// A KaTeX logo +defineFunction("\\KaTeX", { + numArgs: 0, +}, function(context) { + return { + type: "katex", + }; +}); + +defineFunction("\\phantom", { + numArgs: 1, +}, function(context, args) { + var body = args[0]; + var inner; + if (body.type === "ordgroup") { + inner = body.value; + } else { + inner = [body]; + } + + return { + type: "phantom", + value: inner, + }; +}); + +// Extra data needed for the delimiter handler down below +var delimiterSizes = { + "\\bigl" : {type: "open", size: 1}, + "\\Bigl" : {type: "open", size: 2}, + "\\biggl": {type: "open", size: 3}, + "\\Biggl": {type: "open", size: 4}, + "\\bigr" : {type: "close", size: 1}, + "\\Bigr" : {type: "close", size: 2}, + "\\biggr": {type: "close", size: 3}, + "\\Biggr": {type: "close", size: 4}, + "\\bigm" : {type: "rel", size: 1}, + "\\Bigm" : {type: "rel", size: 2}, + "\\biggm": {type: "rel", size: 3}, + "\\Biggm": {type: "rel", size: 4}, + "\\big" : {type: "textord", size: 1}, + "\\Big" : {type: "textord", size: 2}, + "\\bigg" : {type: "textord", size: 3}, + "\\Bigg" : {type: "textord", size: 4}, +}; + +var delimiters = [ + "(", ")", "[", "\\lbrack", "]", "\\rbrack", + "\\{", "\\lbrace", "\\}", "\\rbrace", + "\\lfloor", "\\rfloor", "\\lceil", "\\rceil", + "<", ">", "\\langle", "\\rangle", "\\lt", "\\gt", + "\\lvert", "\\rvert", "\\lVert", "\\rVert", + "\\lgroup", "\\rgroup", "\\lmoustache", "\\rmoustache", + "/", "\\backslash", + "|", "\\vert", "\\|", "\\Vert", + "\\uparrow", "\\Uparrow", + "\\downarrow", "\\Downarrow", + "\\updownarrow", "\\Updownarrow", + ".", +]; + +var fontAliases = { + "\\Bbb": "\\mathbb", + "\\bold": "\\mathbf", + "\\frak": "\\mathfrak", +}; + +// Single-argument color functions +defineFunction([ + "\\blue", "\\orange", "\\pink", "\\red", + "\\green", "\\gray", "\\purple", + "\\blueA", "\\blueB", "\\blueC", "\\blueD", "\\blueE", + "\\tealA", "\\tealB", "\\tealC", "\\tealD", "\\tealE", + "\\greenA", "\\greenB", "\\greenC", "\\greenD", "\\greenE", + "\\goldA", "\\goldB", "\\goldC", "\\goldD", "\\goldE", + "\\redA", "\\redB", "\\redC", "\\redD", "\\redE", + "\\maroonA", "\\maroonB", "\\maroonC", "\\maroonD", "\\maroonE", + "\\purpleA", "\\purpleB", "\\purpleC", "\\purpleD", "\\purpleE", + "\\mintA", "\\mintB", "\\mintC", + "\\grayA", "\\grayB", "\\grayC", "\\grayD", "\\grayE", + "\\grayF", "\\grayG", "\\grayH", "\\grayI", + "\\kaBlue", "\\kaGreen", +], { + numArgs: 1, + allowedInText: true, + greediness: 3, +}, function(context, args) { + var body = args[0]; + var atoms; + if (body.type === "ordgroup") { + atoms = body.value; + } else { + atoms = [body]; + } + + return { + type: "color", + color: "katex-" + context.funcName.slice(1), + value: atoms, + }; +}); + +// There are 2 flags for operators; whether they produce limits in +// displaystyle, and whether they are symbols and should grow in +// displaystyle. These four groups cover the four possible choices. + +// No limits, not symbols +defineFunction([ + "\\arcsin", "\\arccos", "\\arctan", "\\arg", "\\cos", "\\cosh", + "\\cot", "\\coth", "\\csc", "\\deg", "\\dim", "\\exp", "\\hom", + "\\ker", "\\lg", "\\ln", "\\log", "\\sec", "\\sin", "\\sinh", + "\\tan", "\\tanh", +], { + numArgs: 0, +}, function(context) { + return { + type: "op", + limits: false, + symbol: false, + body: context.funcName, + }; +}); + +// Limits, not symbols +defineFunction([ + "\\det", "\\gcd", "\\inf", "\\lim", "\\liminf", "\\limsup", "\\max", + "\\min", "\\Pr", "\\sup", +], { + numArgs: 0, +}, function(context) { + return { + type: "op", + limits: true, + symbol: false, + body: context.funcName, + }; +}); + +// No limits, symbols +defineFunction([ + "\\int", "\\iint", "\\iiint", "\\oint", +], { + numArgs: 0, +}, function(context) { + return { + type: "op", + limits: false, + symbol: true, + body: context.funcName, + }; +}); + +// Limits, symbols +defineFunction([ + "\\coprod", "\\bigvee", "\\bigwedge", "\\biguplus", "\\bigcap", + "\\bigcup", "\\intop", "\\prod", "\\sum", "\\bigotimes", + "\\bigoplus", "\\bigodot", "\\bigsqcup", "\\smallint", +], { + numArgs: 0, +}, function(context) { + return { + type: "op", + limits: true, + symbol: true, + body: context.funcName, + }; +}); + +// Fractions +defineFunction([ + "\\dfrac", "\\frac", "\\tfrac", + "\\dbinom", "\\binom", "\\tbinom", +], { + numArgs: 2, + greediness: 2, +}, function(context, args) { + var numer = args[0]; + var denom = args[1]; + var hasBarLine; + var leftDelim = null; + var rightDelim = null; + var size = "auto"; + + switch (context.funcName) { + case "\\dfrac": + case "\\frac": + case "\\tfrac": + hasBarLine = true; + break; + case "\\dbinom": + case "\\binom": + case "\\tbinom": + hasBarLine = false; + leftDelim = "("; + rightDelim = ")"; + break; + default: + throw new Error("Unrecognized genfrac command"); + } + + switch (context.funcName) { + case "\\dfrac": + case "\\dbinom": + size = "display"; + break; + case "\\tfrac": + case "\\tbinom": + size = "text"; + break; + } + + return { + type: "genfrac", + numer: numer, + denom: denom, + hasBarLine: hasBarLine, + leftDelim: leftDelim, + rightDelim: rightDelim, + size: size, + }; +}); + +// Left and right overlap functions +defineFunction(["\\llap", "\\rlap"], { + numArgs: 1, + allowedInText: true, +}, function(context, args) { + var body = args[0]; + return { + type: context.funcName.slice(1), + body: body, + }; +}); + +// Delimiter functions +defineFunction([ + "\\bigl", "\\Bigl", "\\biggl", "\\Biggl", + "\\bigr", "\\Bigr", "\\biggr", "\\Biggr", + "\\bigm", "\\Bigm", "\\biggm", "\\Biggm", + "\\big", "\\Big", "\\bigg", "\\Bigg", + "\\left", "\\right", +], { + numArgs: 1, +}, function(context, args) { + var delim = args[0]; + if (!utils.contains(delimiters, delim.value)) { + throw new ParseError( + "Invalid delimiter: '" + delim.value + "' after '" + + context.funcName + "'", delim); + } + + // \left and \right are caught somewhere in Parser.js, which is + // why this data doesn't match what is in buildHTML. + if (context.funcName === "\\left" || context.funcName === "\\right") { + return { + type: "leftright", + value: delim.value, + }; + } else { + return { + type: "delimsizing", + size: delimiterSizes[context.funcName].size, + delimType: delimiterSizes[context.funcName].type, + value: delim.value, + }; + } +}); + +// Sizing functions (handled in Parser.js explicitly, hence no handler) +defineFunction([ + "\\tiny", "\\scriptsize", "\\footnotesize", "\\small", + "\\normalsize", "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge", +], 0, null); + +// Style changing functions (handled in Parser.js explicitly, hence no +// handler) +defineFunction([ + "\\displaystyle", "\\textstyle", "\\scriptstyle", + "\\scriptscriptstyle", +], 0, null); + +defineFunction([ + // styles + "\\mathrm", "\\mathit", "\\mathbf", + + // families + "\\mathbb", "\\mathcal", "\\mathfrak", "\\mathscr", "\\mathsf", + "\\mathtt", + + // aliases + "\\Bbb", "\\bold", "\\frak", +], { + numArgs: 1, + greediness: 2, +}, function(context, args) { + var body = args[0]; + var func = context.funcName; + if (func in fontAliases) { + func = fontAliases[func]; + } + return { + type: "font", + font: func.slice(1), + body: body, + }; +}); + +// Accents +defineFunction([ + "\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve", + "\\check", "\\hat", "\\vec", "\\dot", + // We don't support expanding accents yet + // "\\widetilde", "\\widehat" +], { + numArgs: 1, +}, function(context, args) { + var base = args[0]; + return { + type: "accent", + accent: context.funcName, + base: base, + }; +}); + +// Infix generalized fractions +defineFunction(["\\over", "\\choose"], { + numArgs: 0, + infix: true, +}, function(context) { + var replaceWith; + switch (context.funcName) { + case "\\over": + replaceWith = "\\frac"; + break; + case "\\choose": + replaceWith = "\\binom"; + break; + default: + throw new Error("Unrecognized infix genfrac command"); + } + return { + type: "infix", + replaceWith: replaceWith, + token: context.token, + }; +}); + +// Row breaks for aligned data +defineFunction(["\\\\", "\\cr"], { + numArgs: 0, + numOptionalArgs: 1, + argTypes: ["size"], +}, function(context, args) { + var size = args[0]; + return { + type: "cr", + size: size, + }; +}); + +// Environment delimiters +defineFunction(["\\begin", "\\end"], { + numArgs: 1, + argTypes: ["text"], +}, function(context, args) { + var nameGroup = args[0]; + if (nameGroup.type !== "ordgroup") { + throw new ParseError("Invalid environment name", nameGroup); + } + var name = ""; + for (var i = 0; i < nameGroup.value.length; ++i) { + name += nameGroup.value[i].value; + } + return { + type: "environment", + name: name, + nameGroup: nameGroup, + }; +}); + +},{"./ParseError":6,"./utils":25}],20:[function(require,module,exports){ +/** + * These objects store data about MathML nodes. This is the MathML equivalent + * of the types in domTree.js. Since MathML handles its own rendering, and + * since we're mainly using MathML to improve accessibility, we don't manage + * any of the styling state that the plain DOM nodes do. + * + * The `toNode` and `toMarkup` functions work simlarly to how they do in + * domTree.js, creating namespaced DOM nodes and HTML text markup respectively. + */ + +var utils = require("./utils"); + +/** + * This node represents a general purpose MathML node of any type. The + * constructor requires the type of node to create (for example, `"mo"` or + * `"mspace"`, corresponding to `<mo>` and `<mspace>` tags). + */ +function MathNode(type, children) { + this.type = type; + this.attributes = {}; + this.children = children || []; +} + +/** + * Sets an attribute on a MathML node. MathML depends on attributes to convey a + * semantic content, so this is used heavily. + */ +MathNode.prototype.setAttribute = function(name, value) { + this.attributes[name] = value; +}; + +/** + * Converts the math node into a MathML-namespaced DOM element. + */ +MathNode.prototype.toNode = function() { + var node = document.createElementNS( + "http://www.w3.org/1998/Math/MathML", this.type); + + for (var attr in this.attributes) { + if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { + node.setAttribute(attr, this.attributes[attr]); + } + } + + for (var i = 0; i < this.children.length; i++) { + node.appendChild(this.children[i].toNode()); + } + + return node; +}; + +/** + * Converts the math node into an HTML markup string. + */ +MathNode.prototype.toMarkup = function() { + var markup = "<" + this.type; + + // Add the attributes + for (var attr in this.attributes) { + if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { + markup += " " + attr + "=\""; + markup += utils.escape(this.attributes[attr]); + markup += "\""; + } + } + + markup += ">"; + + for (var i = 0; i < this.children.length; i++) { + markup += this.children[i].toMarkup(); + } + + markup += "</" + this.type + ">"; + + return markup; +}; + +/** + * This node represents a piece of text. + */ +function TextNode(text) { + this.text = text; +} + +/** + * Converts the text node into a DOM text node. + */ +TextNode.prototype.toNode = function() { + return document.createTextNode(this.text); +}; + +/** + * Converts the text node into HTML markup (which is just the text itself). + */ +TextNode.prototype.toMarkup = function() { + return utils.escape(this.text); +}; + +module.exports = { + MathNode: MathNode, + TextNode: TextNode, +}; + +},{"./utils":25}],21:[function(require,module,exports){ +/** + * The resulting parse tree nodes of the parse tree. + * + * It is possible to provide position information, so that a ParseNode can + * fulfil a role similar to a Token in error reporting. + * For details on the corresponding properties see Token constructor. + * Providing such information can lead to better error reporting. + * + * @param {string} type type of node, like e.g. "ordgroup" + * @param {?object} value type-specific representation of the node + * @param {string} mode parse mode in action for this node, + * "math" or "text" + * @param {Token=} firstToken first token of the input for this node, + * will omit position information if unset + * @param {Token=} lastToken last token of the input for this node, + * will default to firstToken if unset + */ +function ParseNode(type, value, mode, firstToken, lastToken) { + this.type = type; + this.value = value; + this.mode = mode; + if (firstToken && (!lastToken || lastToken.lexer === firstToken.lexer)) { + this.lexer = firstToken.lexer; + this.start = firstToken.start; + this.end = (lastToken || firstToken).end; + } +} + +module.exports = { + ParseNode: ParseNode, +}; + + +},{}],22:[function(require,module,exports){ +/** + * Provides a single function for parsing an expression using a Parser + * TODO(emily): Remove this + */ + +var Parser = require("./Parser"); + +/** + * Parses an expression using a Parser, then returns the parsed result. + */ +var parseTree = function(toParse, settings) { + if (!(typeof toParse === 'string' || toParse instanceof String)) { + throw new TypeError('KaTeX can only parse string typed expression'); + } + var parser = new Parser(toParse, settings); + + return parser.parse(); +}; + +module.exports = parseTree; + +},{"./Parser":7}],23:[function(require,module,exports){ +/** + * This file holds a list of all no-argument functions and single-character + * symbols (like 'a' or ';'). + * + * For each of the symbols, there are three properties they can have: + * - font (required): the font to be used for this symbol. Either "main" (the + normal font), or "ams" (the ams fonts). + * - group (required): the ParseNode group type the symbol should have (i.e. + "textord", "mathord", etc). + See https://github.com/Khan/KaTeX/wiki/Examining-TeX#group-types + * - replace: the character that this symbol or function should be + * replaced with (i.e. "\phi" has a replace value of "\u03d5", the phi + * character in the main font). + * + * The outermost map in the table indicates what mode the symbols should be + * accepted in (e.g. "math" or "text"). + */ + +module.exports = { + math: {}, + text: {}, +}; + +function defineSymbol(mode, font, group, replace, name) { + module.exports[mode][name] = { + font: font, + group: group, + replace: replace, + }; +} + +// Some abbreviations for commonly used strings. +// This helps minify the code, and also spotting typos using jshint. + +// modes: +var math = "math"; +var text = "text"; + +// fonts: +var main = "main"; +var ams = "ams"; + +// groups: +var accent = "accent"; +var bin = "bin"; +var close = "close"; +var inner = "inner"; +var mathord = "mathord"; +var op = "op"; +var open = "open"; +var punct = "punct"; +var rel = "rel"; +var spacing = "spacing"; +var textord = "textord"; + +// Now comes the symbol table + +// Relation Symbols +defineSymbol(math, main, rel, "\u2261", "\\equiv"); +defineSymbol(math, main, rel, "\u227a", "\\prec"); +defineSymbol(math, main, rel, "\u227b", "\\succ"); +defineSymbol(math, main, rel, "\u223c", "\\sim"); +defineSymbol(math, main, rel, "\u22a5", "\\perp"); +defineSymbol(math, main, rel, "\u2aaf", "\\preceq"); +defineSymbol(math, main, rel, "\u2ab0", "\\succeq"); +defineSymbol(math, main, rel, "\u2243", "\\simeq"); +defineSymbol(math, main, rel, "\u2223", "\\mid"); +defineSymbol(math, main, rel, "\u226a", "\\ll"); +defineSymbol(math, main, rel, "\u226b", "\\gg"); +defineSymbol(math, main, rel, "\u224d", "\\asymp"); +defineSymbol(math, main, rel, "\u2225", "\\parallel"); +defineSymbol(math, main, rel, "\u22c8", "\\bowtie"); +defineSymbol(math, main, rel, "\u2323", "\\smile"); +defineSymbol(math, main, rel, "\u2291", "\\sqsubseteq"); +defineSymbol(math, main, rel, "\u2292", "\\sqsupseteq"); +defineSymbol(math, main, rel, "\u2250", "\\doteq"); +defineSymbol(math, main, rel, "\u2322", "\\frown"); +defineSymbol(math, main, rel, "\u220b", "\\ni"); +defineSymbol(math, main, rel, "\u221d", "\\propto"); +defineSymbol(math, main, rel, "\u22a2", "\\vdash"); +defineSymbol(math, main, rel, "\u22a3", "\\dashv"); +defineSymbol(math, main, rel, "\u220b", "\\owns"); + +// Punctuation +defineSymbol(math, main, punct, "\u002e", "\\ldotp"); +defineSymbol(math, main, punct, "\u22c5", "\\cdotp"); + +// Misc Symbols +defineSymbol(math, main, textord, "\u0023", "\\#"); +defineSymbol(math, main, textord, "\u0026", "\\&"); +defineSymbol(math, main, textord, "\u2135", "\\aleph"); +defineSymbol(math, main, textord, "\u2200", "\\forall"); +defineSymbol(math, main, textord, "\u210f", "\\hbar"); +defineSymbol(math, main, textord, "\u2203", "\\exists"); +defineSymbol(math, main, textord, "\u2207", "\\nabla"); +defineSymbol(math, main, textord, "\u266d", "\\flat"); +defineSymbol(math, main, textord, "\u2113", "\\ell"); +defineSymbol(math, main, textord, "\u266e", "\\natural"); +defineSymbol(math, main, textord, "\u2663", "\\clubsuit"); +defineSymbol(math, main, textord, "\u2118", "\\wp"); +defineSymbol(math, main, textord, "\u266f", "\\sharp"); +defineSymbol(math, main, textord, "\u2662", "\\diamondsuit"); +defineSymbol(math, main, textord, "\u211c", "\\Re"); +defineSymbol(math, main, textord, "\u2661", "\\heartsuit"); +defineSymbol(math, main, textord, "\u2111", "\\Im"); +defineSymbol(math, main, textord, "\u2660", "\\spadesuit"); + +// Math and Text +defineSymbol(math, main, textord, "\u2020", "\\dag"); +defineSymbol(math, main, textord, "\u2021", "\\ddag"); + +// Large Delimiters +defineSymbol(math, main, close, "\u23b1", "\\rmoustache"); +defineSymbol(math, main, open, "\u23b0", "\\lmoustache"); +defineSymbol(math, main, close, "\u27ef", "\\rgroup"); +defineSymbol(math, main, open, "\u27ee", "\\lgroup"); + +// Binary Operators +defineSymbol(math, main, bin, "\u2213", "\\mp"); +defineSymbol(math, main, bin, "\u2296", "\\ominus"); +defineSymbol(math, main, bin, "\u228e", "\\uplus"); +defineSymbol(math, main, bin, "\u2293", "\\sqcap"); +defineSymbol(math, main, bin, "\u2217", "\\ast"); +defineSymbol(math, main, bin, "\u2294", "\\sqcup"); +defineSymbol(math, main, bin, "\u25ef", "\\bigcirc"); +defineSymbol(math, main, bin, "\u2219", "\\bullet"); +defineSymbol(math, main, bin, "\u2021", "\\ddagger"); +defineSymbol(math, main, bin, "\u2240", "\\wr"); +defineSymbol(math, main, bin, "\u2a3f", "\\amalg"); + +// Arrow Symbols +defineSymbol(math, main, rel, "\u27f5", "\\longleftarrow"); +defineSymbol(math, main, rel, "\u21d0", "\\Leftarrow"); +defineSymbol(math, main, rel, "\u27f8", "\\Longleftarrow"); +defineSymbol(math, main, rel, "\u27f6", "\\longrightarrow"); +defineSymbol(math, main, rel, "\u21d2", "\\Rightarrow"); +defineSymbol(math, main, rel, "\u27f9", "\\Longrightarrow"); +defineSymbol(math, main, rel, "\u2194", "\\leftrightarrow"); +defineSymbol(math, main, rel, "\u27f7", "\\longleftrightarrow"); +defineSymbol(math, main, rel, "\u21d4", "\\Leftrightarrow"); +defineSymbol(math, main, rel, "\u27fa", "\\Longleftrightarrow"); +defineSymbol(math, main, rel, "\u21a6", "\\mapsto"); +defineSymbol(math, main, rel, "\u27fc", "\\longmapsto"); +defineSymbol(math, main, rel, "\u2197", "\\nearrow"); +defineSymbol(math, main, rel, "\u21a9", "\\hookleftarrow"); +defineSymbol(math, main, rel, "\u21aa", "\\hookrightarrow"); +defineSymbol(math, main, rel, "\u2198", "\\searrow"); +defineSymbol(math, main, rel, "\u21bc", "\\leftharpoonup"); +defineSymbol(math, main, rel, "\u21c0", "\\rightharpoonup"); +defineSymbol(math, main, rel, "\u2199", "\\swarrow"); +defineSymbol(math, main, rel, "\u21bd", "\\leftharpoondown"); +defineSymbol(math, main, rel, "\u21c1", "\\rightharpoondown"); +defineSymbol(math, main, rel, "\u2196", "\\nwarrow"); +defineSymbol(math, main, rel, "\u21cc", "\\rightleftharpoons"); + +// AMS Negated Binary Relations +defineSymbol(math, ams, rel, "\u226e", "\\nless"); +defineSymbol(math, ams, rel, "\ue010", "\\nleqslant"); +defineSymbol(math, ams, rel, "\ue011", "\\nleqq"); +defineSymbol(math, ams, rel, "\u2a87", "\\lneq"); +defineSymbol(math, ams, rel, "\u2268", "\\lneqq"); +defineSymbol(math, ams, rel, "\ue00c", "\\lvertneqq"); +defineSymbol(math, ams, rel, "\u22e6", "\\lnsim"); +defineSymbol(math, ams, rel, "\u2a89", "\\lnapprox"); +defineSymbol(math, ams, rel, "\u2280", "\\nprec"); +defineSymbol(math, ams, rel, "\u22e0", "\\npreceq"); +defineSymbol(math, ams, rel, "\u22e8", "\\precnsim"); +defineSymbol(math, ams, rel, "\u2ab9", "\\precnapprox"); +defineSymbol(math, ams, rel, "\u2241", "\\nsim"); +defineSymbol(math, ams, rel, "\ue006", "\\nshortmid"); +defineSymbol(math, ams, rel, "\u2224", "\\nmid"); +defineSymbol(math, ams, rel, "\u22ac", "\\nvdash"); +defineSymbol(math, ams, rel, "\u22ad", "\\nvDash"); +defineSymbol(math, ams, rel, "\u22ea", "\\ntriangleleft"); +defineSymbol(math, ams, rel, "\u22ec", "\\ntrianglelefteq"); +defineSymbol(math, ams, rel, "\u228a", "\\subsetneq"); +defineSymbol(math, ams, rel, "\ue01a", "\\varsubsetneq"); +defineSymbol(math, ams, rel, "\u2acb", "\\subsetneqq"); +defineSymbol(math, ams, rel, "\ue017", "\\varsubsetneqq"); +defineSymbol(math, ams, rel, "\u226f", "\\ngtr"); +defineSymbol(math, ams, rel, "\ue00f", "\\ngeqslant"); +defineSymbol(math, ams, rel, "\ue00e", "\\ngeqq"); +defineSymbol(math, ams, rel, "\u2a88", "\\gneq"); +defineSymbol(math, ams, rel, "\u2269", "\\gneqq"); +defineSymbol(math, ams, rel, "\ue00d", "\\gvertneqq"); +defineSymbol(math, ams, rel, "\u22e7", "\\gnsim"); +defineSymbol(math, ams, rel, "\u2a8a", "\\gnapprox"); +defineSymbol(math, ams, rel, "\u2281", "\\nsucc"); +defineSymbol(math, ams, rel, "\u22e1", "\\nsucceq"); +defineSymbol(math, ams, rel, "\u22e9", "\\succnsim"); +defineSymbol(math, ams, rel, "\u2aba", "\\succnapprox"); +defineSymbol(math, ams, rel, "\u2246", "\\ncong"); +defineSymbol(math, ams, rel, "\ue007", "\\nshortparallel"); +defineSymbol(math, ams, rel, "\u2226", "\\nparallel"); +defineSymbol(math, ams, rel, "\u22af", "\\nVDash"); +defineSymbol(math, ams, rel, "\u22eb", "\\ntriangleright"); +defineSymbol(math, ams, rel, "\u22ed", "\\ntrianglerighteq"); +defineSymbol(math, ams, rel, "\ue018", "\\nsupseteqq"); +defineSymbol(math, ams, rel, "\u228b", "\\supsetneq"); +defineSymbol(math, ams, rel, "\ue01b", "\\varsupsetneq"); +defineSymbol(math, ams, rel, "\u2acc", "\\supsetneqq"); +defineSymbol(math, ams, rel, "\ue019", "\\varsupsetneqq"); +defineSymbol(math, ams, rel, "\u22ae", "\\nVdash"); +defineSymbol(math, ams, rel, "\u2ab5", "\\precneqq"); +defineSymbol(math, ams, rel, "\u2ab6", "\\succneqq"); +defineSymbol(math, ams, rel, "\ue016", "\\nsubseteqq"); +defineSymbol(math, ams, bin, "\u22b4", "\\unlhd"); +defineSymbol(math, ams, bin, "\u22b5", "\\unrhd"); + +// AMS Negated Arrows +defineSymbol(math, ams, rel, "\u219a", "\\nleftarrow"); +defineSymbol(math, ams, rel, "\u219b", "\\nrightarrow"); +defineSymbol(math, ams, rel, "\u21cd", "\\nLeftarrow"); +defineSymbol(math, ams, rel, "\u21cf", "\\nRightarrow"); +defineSymbol(math, ams, rel, "\u21ae", "\\nleftrightarrow"); +defineSymbol(math, ams, rel, "\u21ce", "\\nLeftrightarrow"); + +// AMS Misc +defineSymbol(math, ams, rel, "\u25b3", "\\vartriangle"); +defineSymbol(math, ams, textord, "\u210f", "\\hslash"); +defineSymbol(math, ams, textord, "\u25bd", "\\triangledown"); +defineSymbol(math, ams, textord, "\u25ca", "\\lozenge"); +defineSymbol(math, ams, textord, "\u24c8", "\\circledS"); +defineSymbol(math, ams, textord, "\u00ae", "\\circledR"); +defineSymbol(math, ams, textord, "\u2221", "\\measuredangle"); +defineSymbol(math, ams, textord, "\u2204", "\\nexists"); +defineSymbol(math, ams, textord, "\u2127", "\\mho"); +defineSymbol(math, ams, textord, "\u2132", "\\Finv"); +defineSymbol(math, ams, textord, "\u2141", "\\Game"); +defineSymbol(math, ams, textord, "\u006b", "\\Bbbk"); +defineSymbol(math, ams, textord, "\u2035", "\\backprime"); +defineSymbol(math, ams, textord, "\u25b2", "\\blacktriangle"); +defineSymbol(math, ams, textord, "\u25bc", "\\blacktriangledown"); +defineSymbol(math, ams, textord, "\u25a0", "\\blacksquare"); +defineSymbol(math, ams, textord, "\u29eb", "\\blacklozenge"); +defineSymbol(math, ams, textord, "\u2605", "\\bigstar"); +defineSymbol(math, ams, textord, "\u2222", "\\sphericalangle"); +defineSymbol(math, ams, textord, "\u2201", "\\complement"); +defineSymbol(math, ams, textord, "\u00f0", "\\eth"); +defineSymbol(math, ams, textord, "\u2571", "\\diagup"); +defineSymbol(math, ams, textord, "\u2572", "\\diagdown"); +defineSymbol(math, ams, textord, "\u25a1", "\\square"); +defineSymbol(math, ams, textord, "\u25a1", "\\Box"); +defineSymbol(math, ams, textord, "\u25ca", "\\Diamond"); +defineSymbol(math, ams, textord, "\u00a5", "\\yen"); +defineSymbol(math, ams, textord, "\u2713", "\\checkmark"); + +// AMS Hebrew +defineSymbol(math, ams, textord, "\u2136", "\\beth"); +defineSymbol(math, ams, textord, "\u2138", "\\daleth"); +defineSymbol(math, ams, textord, "\u2137", "\\gimel"); + +// AMS Greek +defineSymbol(math, ams, textord, "\u03dd", "\\digamma"); +defineSymbol(math, ams, textord, "\u03f0", "\\varkappa"); + +// AMS Delimiters +defineSymbol(math, ams, open, "\u250c", "\\ulcorner"); +defineSymbol(math, ams, close, "\u2510", "\\urcorner"); +defineSymbol(math, ams, open, "\u2514", "\\llcorner"); +defineSymbol(math, ams, close, "\u2518", "\\lrcorner"); + +// AMS Binary Relations +defineSymbol(math, ams, rel, "\u2266", "\\leqq"); +defineSymbol(math, ams, rel, "\u2a7d", "\\leqslant"); +defineSymbol(math, ams, rel, "\u2a95", "\\eqslantless"); +defineSymbol(math, ams, rel, "\u2272", "\\lesssim"); +defineSymbol(math, ams, rel, "\u2a85", "\\lessapprox"); +defineSymbol(math, ams, rel, "\u224a", "\\approxeq"); +defineSymbol(math, ams, bin, "\u22d6", "\\lessdot"); +defineSymbol(math, ams, rel, "\u22d8", "\\lll"); +defineSymbol(math, ams, rel, "\u2276", "\\lessgtr"); +defineSymbol(math, ams, rel, "\u22da", "\\lesseqgtr"); +defineSymbol(math, ams, rel, "\u2a8b", "\\lesseqqgtr"); +defineSymbol(math, ams, rel, "\u2251", "\\doteqdot"); +defineSymbol(math, ams, rel, "\u2253", "\\risingdotseq"); +defineSymbol(math, ams, rel, "\u2252", "\\fallingdotseq"); +defineSymbol(math, ams, rel, "\u223d", "\\backsim"); +defineSymbol(math, ams, rel, "\u22cd", "\\backsimeq"); +defineSymbol(math, ams, rel, "\u2ac5", "\\subseteqq"); +defineSymbol(math, ams, rel, "\u22d0", "\\Subset"); +defineSymbol(math, ams, rel, "\u228f", "\\sqsubset"); +defineSymbol(math, ams, rel, "\u227c", "\\preccurlyeq"); +defineSymbol(math, ams, rel, "\u22de", "\\curlyeqprec"); +defineSymbol(math, ams, rel, "\u227e", "\\precsim"); +defineSymbol(math, ams, rel, "\u2ab7", "\\precapprox"); +defineSymbol(math, ams, rel, "\u22b2", "\\vartriangleleft"); +defineSymbol(math, ams, rel, "\u22b4", "\\trianglelefteq"); +defineSymbol(math, ams, rel, "\u22a8", "\\vDash"); +defineSymbol(math, ams, rel, "\u22aa", "\\Vvdash"); +defineSymbol(math, ams, rel, "\u2323", "\\smallsmile"); +defineSymbol(math, ams, rel, "\u2322", "\\smallfrown"); +defineSymbol(math, ams, rel, "\u224f", "\\bumpeq"); +defineSymbol(math, ams, rel, "\u224e", "\\Bumpeq"); +defineSymbol(math, ams, rel, "\u2267", "\\geqq"); +defineSymbol(math, ams, rel, "\u2a7e", "\\geqslant"); +defineSymbol(math, ams, rel, "\u2a96", "\\eqslantgtr"); +defineSymbol(math, ams, rel, "\u2273", "\\gtrsim"); +defineSymbol(math, ams, rel, "\u2a86", "\\gtrapprox"); +defineSymbol(math, ams, bin, "\u22d7", "\\gtrdot"); +defineSymbol(math, ams, rel, "\u22d9", "\\ggg"); +defineSymbol(math, ams, rel, "\u2277", "\\gtrless"); +defineSymbol(math, ams, rel, "\u22db", "\\gtreqless"); +defineSymbol(math, ams, rel, "\u2a8c", "\\gtreqqless"); +defineSymbol(math, ams, rel, "\u2256", "\\eqcirc"); +defineSymbol(math, ams, rel, "\u2257", "\\circeq"); +defineSymbol(math, ams, rel, "\u225c", "\\triangleq"); +defineSymbol(math, ams, rel, "\u223c", "\\thicksim"); +defineSymbol(math, ams, rel, "\u2248", "\\thickapprox"); +defineSymbol(math, ams, rel, "\u2ac6", "\\supseteqq"); +defineSymbol(math, ams, rel, "\u22d1", "\\Supset"); +defineSymbol(math, ams, rel, "\u2290", "\\sqsupset"); +defineSymbol(math, ams, rel, "\u227d", "\\succcurlyeq"); +defineSymbol(math, ams, rel, "\u22df", "\\curlyeqsucc"); +defineSymbol(math, ams, rel, "\u227f", "\\succsim"); +defineSymbol(math, ams, rel, "\u2ab8", "\\succapprox"); +defineSymbol(math, ams, rel, "\u22b3", "\\vartriangleright"); +defineSymbol(math, ams, rel, "\u22b5", "\\trianglerighteq"); +defineSymbol(math, ams, rel, "\u22a9", "\\Vdash"); +defineSymbol(math, ams, rel, "\u2223", "\\shortmid"); +defineSymbol(math, ams, rel, "\u2225", "\\shortparallel"); +defineSymbol(math, ams, rel, "\u226c", "\\between"); +defineSymbol(math, ams, rel, "\u22d4", "\\pitchfork"); +defineSymbol(math, ams, rel, "\u221d", "\\varpropto"); +defineSymbol(math, ams, rel, "\u25c0", "\\blacktriangleleft"); +defineSymbol(math, ams, rel, "\u2234", "\\therefore"); +defineSymbol(math, ams, rel, "\u220d", "\\backepsilon"); +defineSymbol(math, ams, rel, "\u25b6", "\\blacktriangleright"); +defineSymbol(math, ams, rel, "\u2235", "\\because"); +defineSymbol(math, ams, rel, "\u22d8", "\\llless"); +defineSymbol(math, ams, rel, "\u22d9", "\\gggtr"); +defineSymbol(math, ams, bin, "\u22b2", "\\lhd"); +defineSymbol(math, ams, bin, "\u22b3", "\\rhd"); +defineSymbol(math, ams, rel, "\u2242", "\\eqsim"); +defineSymbol(math, main, rel, "\u22c8", "\\Join"); +defineSymbol(math, ams, rel, "\u2251", "\\Doteq"); + +// AMS Binary Operators +defineSymbol(math, ams, bin, "\u2214", "\\dotplus"); +defineSymbol(math, ams, bin, "\u2216", "\\smallsetminus"); +defineSymbol(math, ams, bin, "\u22d2", "\\Cap"); +defineSymbol(math, ams, bin, "\u22d3", "\\Cup"); +defineSymbol(math, ams, bin, "\u2a5e", "\\doublebarwedge"); +defineSymbol(math, ams, bin, "\u229f", "\\boxminus"); +defineSymbol(math, ams, bin, "\u229e", "\\boxplus"); +defineSymbol(math, ams, bin, "\u22c7", "\\divideontimes"); +defineSymbol(math, ams, bin, "\u22c9", "\\ltimes"); +defineSymbol(math, ams, bin, "\u22ca", "\\rtimes"); +defineSymbol(math, ams, bin, "\u22cb", "\\leftthreetimes"); +defineSymbol(math, ams, bin, "\u22cc", "\\rightthreetimes"); +defineSymbol(math, ams, bin, "\u22cf", "\\curlywedge"); +defineSymbol(math, ams, bin, "\u22ce", "\\curlyvee"); +defineSymbol(math, ams, bin, "\u229d", "\\circleddash"); +defineSymbol(math, ams, bin, "\u229b", "\\circledast"); +defineSymbol(math, ams, bin, "\u22c5", "\\centerdot"); +defineSymbol(math, ams, bin, "\u22ba", "\\intercal"); +defineSymbol(math, ams, bin, "\u22d2", "\\doublecap"); +defineSymbol(math, ams, bin, "\u22d3", "\\doublecup"); +defineSymbol(math, ams, bin, "\u22a0", "\\boxtimes"); + +// AMS Arrows +defineSymbol(math, ams, rel, "\u21e2", "\\dashrightarrow"); +defineSymbol(math, ams, rel, "\u21e0", "\\dashleftarrow"); +defineSymbol(math, ams, rel, "\u21c7", "\\leftleftarrows"); +defineSymbol(math, ams, rel, "\u21c6", "\\leftrightarrows"); +defineSymbol(math, ams, rel, "\u21da", "\\Lleftarrow"); +defineSymbol(math, ams, rel, "\u219e", "\\twoheadleftarrow"); +defineSymbol(math, ams, rel, "\u21a2", "\\leftarrowtail"); +defineSymbol(math, ams, rel, "\u21ab", "\\looparrowleft"); +defineSymbol(math, ams, rel, "\u21cb", "\\leftrightharpoons"); +defineSymbol(math, ams, rel, "\u21b6", "\\curvearrowleft"); +defineSymbol(math, ams, rel, "\u21ba", "\\circlearrowleft"); +defineSymbol(math, ams, rel, "\u21b0", "\\Lsh"); +defineSymbol(math, ams, rel, "\u21c8", "\\upuparrows"); +defineSymbol(math, ams, rel, "\u21bf", "\\upharpoonleft"); +defineSymbol(math, ams, rel, "\u21c3", "\\downharpoonleft"); +defineSymbol(math, ams, rel, "\u22b8", "\\multimap"); +defineSymbol(math, ams, rel, "\u21ad", "\\leftrightsquigarrow"); +defineSymbol(math, ams, rel, "\u21c9", "\\rightrightarrows"); +defineSymbol(math, ams, rel, "\u21c4", "\\rightleftarrows"); +defineSymbol(math, ams, rel, "\u21a0", "\\twoheadrightarrow"); +defineSymbol(math, ams, rel, "\u21a3", "\\rightarrowtail"); +defineSymbol(math, ams, rel, "\u21ac", "\\looparrowright"); +defineSymbol(math, ams, rel, "\u21b7", "\\curvearrowright"); +defineSymbol(math, ams, rel, "\u21bb", "\\circlearrowright"); +defineSymbol(math, ams, rel, "\u21b1", "\\Rsh"); +defineSymbol(math, ams, rel, "\u21ca", "\\downdownarrows"); +defineSymbol(math, ams, rel, "\u21be", "\\upharpoonright"); +defineSymbol(math, ams, rel, "\u21c2", "\\downharpoonright"); +defineSymbol(math, ams, rel, "\u21dd", "\\rightsquigarrow"); +defineSymbol(math, ams, rel, "\u21dd", "\\leadsto"); +defineSymbol(math, ams, rel, "\u21db", "\\Rrightarrow"); +defineSymbol(math, ams, rel, "\u21be", "\\restriction"); + +defineSymbol(math, main, textord, "\u2018", "`"); +defineSymbol(math, main, textord, "$", "\\$"); +defineSymbol(math, main, textord, "%", "\\%"); +defineSymbol(math, main, textord, "_", "\\_"); +defineSymbol(math, main, textord, "\u2220", "\\angle"); +defineSymbol(math, main, textord, "\u221e", "\\infty"); +defineSymbol(math, main, textord, "\u2032", "\\prime"); +defineSymbol(math, main, textord, "\u25b3", "\\triangle"); +defineSymbol(math, main, textord, "\u0393", "\\Gamma"); +defineSymbol(math, main, textord, "\u0394", "\\Delta"); +defineSymbol(math, main, textord, "\u0398", "\\Theta"); +defineSymbol(math, main, textord, "\u039b", "\\Lambda"); +defineSymbol(math, main, textord, "\u039e", "\\Xi"); +defineSymbol(math, main, textord, "\u03a0", "\\Pi"); +defineSymbol(math, main, textord, "\u03a3", "\\Sigma"); +defineSymbol(math, main, textord, "\u03a5", "\\Upsilon"); +defineSymbol(math, main, textord, "\u03a6", "\\Phi"); +defineSymbol(math, main, textord, "\u03a8", "\\Psi"); +defineSymbol(math, main, textord, "\u03a9", "\\Omega"); +defineSymbol(math, main, textord, "\u00ac", "\\neg"); +defineSymbol(math, main, textord, "\u00ac", "\\lnot"); +defineSymbol(math, main, textord, "\u22a4", "\\top"); +defineSymbol(math, main, textord, "\u22a5", "\\bot"); +defineSymbol(math, main, textord, "\u2205", "\\emptyset"); +defineSymbol(math, ams, textord, "\u2205", "\\varnothing"); +defineSymbol(math, main, mathord, "\u03b1", "\\alpha"); +defineSymbol(math, main, mathord, "\u03b2", "\\beta"); +defineSymbol(math, main, mathord, "\u03b3", "\\gamma"); +defineSymbol(math, main, mathord, "\u03b4", "\\delta"); +defineSymbol(math, main, mathord, "\u03f5", "\\epsilon"); +defineSymbol(math, main, mathord, "\u03b6", "\\zeta"); +defineSymbol(math, main, mathord, "\u03b7", "\\eta"); +defineSymbol(math, main, mathord, "\u03b8", "\\theta"); +defineSymbol(math, main, mathord, "\u03b9", "\\iota"); +defineSymbol(math, main, mathord, "\u03ba", "\\kappa"); +defineSymbol(math, main, mathord, "\u03bb", "\\lambda"); +defineSymbol(math, main, mathord, "\u03bc", "\\mu"); +defineSymbol(math, main, mathord, "\u03bd", "\\nu"); +defineSymbol(math, main, mathord, "\u03be", "\\xi"); +defineSymbol(math, main, mathord, "o", "\\omicron"); +defineSymbol(math, main, mathord, "\u03c0", "\\pi"); +defineSymbol(math, main, mathord, "\u03c1", "\\rho"); +defineSymbol(math, main, mathord, "\u03c3", "\\sigma"); +defineSymbol(math, main, mathord, "\u03c4", "\\tau"); +defineSymbol(math, main, mathord, "\u03c5", "\\upsilon"); +defineSymbol(math, main, mathord, "\u03d5", "\\phi"); +defineSymbol(math, main, mathord, "\u03c7", "\\chi"); +defineSymbol(math, main, mathord, "\u03c8", "\\psi"); +defineSymbol(math, main, mathord, "\u03c9", "\\omega"); +defineSymbol(math, main, mathord, "\u03b5", "\\varepsilon"); +defineSymbol(math, main, mathord, "\u03d1", "\\vartheta"); +defineSymbol(math, main, mathord, "\u03d6", "\\varpi"); +defineSymbol(math, main, mathord, "\u03f1", "\\varrho"); +defineSymbol(math, main, mathord, "\u03c2", "\\varsigma"); +defineSymbol(math, main, mathord, "\u03c6", "\\varphi"); +defineSymbol(math, main, bin, "\u2217", "*"); +defineSymbol(math, main, bin, "+", "+"); +defineSymbol(math, main, bin, "\u2212", "-"); +defineSymbol(math, main, bin, "\u22c5", "\\cdot"); +defineSymbol(math, main, bin, "\u2218", "\\circ"); +defineSymbol(math, main, bin, "\u00f7", "\\div"); +defineSymbol(math, main, bin, "\u00b1", "\\pm"); +defineSymbol(math, main, bin, "\u00d7", "\\times"); +defineSymbol(math, main, bin, "\u2229", "\\cap"); +defineSymbol(math, main, bin, "\u222a", "\\cup"); +defineSymbol(math, main, bin, "\u2216", "\\setminus"); +defineSymbol(math, main, bin, "\u2227", "\\land"); +defineSymbol(math, main, bin, "\u2228", "\\lor"); +defineSymbol(math, main, bin, "\u2227", "\\wedge"); +defineSymbol(math, main, bin, "\u2228", "\\vee"); +defineSymbol(math, main, textord, "\u221a", "\\surd"); +defineSymbol(math, main, open, "(", "("); +defineSymbol(math, main, open, "[", "["); +defineSymbol(math, main, open, "\u27e8", "\\langle"); +defineSymbol(math, main, open, "\u2223", "\\lvert"); +defineSymbol(math, main, open, "\u2225", "\\lVert"); +defineSymbol(math, main, close, ")", ")"); +defineSymbol(math, main, close, "]", "]"); +defineSymbol(math, main, close, "?", "?"); +defineSymbol(math, main, close, "!", "!"); +defineSymbol(math, main, close, "\u27e9", "\\rangle"); +defineSymbol(math, main, close, "\u2223", "\\rvert"); +defineSymbol(math, main, close, "\u2225", "\\rVert"); +defineSymbol(math, main, rel, "=", "="); +defineSymbol(math, main, rel, "<", "<"); +defineSymbol(math, main, rel, ">", ">"); +defineSymbol(math, main, rel, ":", ":"); +defineSymbol(math, main, rel, "\u2248", "\\approx"); +defineSymbol(math, main, rel, "\u2245", "\\cong"); +defineSymbol(math, main, rel, "\u2265", "\\ge"); +defineSymbol(math, main, rel, "\u2265", "\\geq"); +defineSymbol(math, main, rel, "\u2190", "\\gets"); +defineSymbol(math, main, rel, ">", "\\gt"); +defineSymbol(math, main, rel, "\u2208", "\\in"); +defineSymbol(math, main, rel, "\u2209", "\\notin"); +defineSymbol(math, main, rel, "\u2282", "\\subset"); +defineSymbol(math, main, rel, "\u2283", "\\supset"); +defineSymbol(math, main, rel, "\u2286", "\\subseteq"); +defineSymbol(math, main, rel, "\u2287", "\\supseteq"); +defineSymbol(math, ams, rel, "\u2288", "\\nsubseteq"); +defineSymbol(math, ams, rel, "\u2289", "\\nsupseteq"); +defineSymbol(math, main, rel, "\u22a8", "\\models"); +defineSymbol(math, main, rel, "\u2190", "\\leftarrow"); +defineSymbol(math, main, rel, "\u2264", "\\le"); +defineSymbol(math, main, rel, "\u2264", "\\leq"); +defineSymbol(math, main, rel, "<", "\\lt"); +defineSymbol(math, main, rel, "\u2260", "\\ne"); +defineSymbol(math, main, rel, "\u2260", "\\neq"); +defineSymbol(math, main, rel, "\u2192", "\\rightarrow"); +defineSymbol(math, main, rel, "\u2192", "\\to"); +defineSymbol(math, ams, rel, "\u2271", "\\ngeq"); +defineSymbol(math, ams, rel, "\u2270", "\\nleq"); +defineSymbol(math, main, spacing, null, "\\!"); +defineSymbol(math, main, spacing, "\u00a0", "\\ "); +defineSymbol(math, main, spacing, "\u00a0", "~"); +defineSymbol(math, main, spacing, null, "\\,"); +defineSymbol(math, main, spacing, null, "\\:"); +defineSymbol(math, main, spacing, null, "\\;"); +defineSymbol(math, main, spacing, null, "\\enspace"); +defineSymbol(math, main, spacing, null, "\\qquad"); +defineSymbol(math, main, spacing, null, "\\quad"); +defineSymbol(math, main, spacing, "\u00a0", "\\space"); +defineSymbol(math, main, punct, ",", ","); +defineSymbol(math, main, punct, ";", ";"); +defineSymbol(math, main, punct, ":", "\\colon"); +defineSymbol(math, ams, bin, "\u22bc", "\\barwedge"); +defineSymbol(math, ams, bin, "\u22bb", "\\veebar"); +defineSymbol(math, main, bin, "\u2299", "\\odot"); +defineSymbol(math, main, bin, "\u2295", "\\oplus"); +defineSymbol(math, main, bin, "\u2297", "\\otimes"); +defineSymbol(math, main, textord, "\u2202", "\\partial"); +defineSymbol(math, main, bin, "\u2298", "\\oslash"); +defineSymbol(math, ams, bin, "\u229a", "\\circledcirc"); +defineSymbol(math, ams, bin, "\u22a1", "\\boxdot"); +defineSymbol(math, main, bin, "\u25b3", "\\bigtriangleup"); +defineSymbol(math, main, bin, "\u25bd", "\\bigtriangledown"); +defineSymbol(math, main, bin, "\u2020", "\\dagger"); +defineSymbol(math, main, bin, "\u22c4", "\\diamond"); +defineSymbol(math, main, bin, "\u22c6", "\\star"); +defineSymbol(math, main, bin, "\u25c3", "\\triangleleft"); +defineSymbol(math, main, bin, "\u25b9", "\\triangleright"); +defineSymbol(math, main, open, "{", "\\{"); +defineSymbol(math, main, close, "}", "\\}"); +defineSymbol(math, main, open, "{", "\\lbrace"); +defineSymbol(math, main, close, "}", "\\rbrace"); +defineSymbol(math, main, open, "[", "\\lbrack"); +defineSymbol(math, main, close, "]", "\\rbrack"); +defineSymbol(math, main, open, "\u230a", "\\lfloor"); +defineSymbol(math, main, close, "\u230b", "\\rfloor"); +defineSymbol(math, main, open, "\u2308", "\\lceil"); +defineSymbol(math, main, close, "\u2309", "\\rceil"); +defineSymbol(math, main, textord, "\\", "\\backslash"); +defineSymbol(math, main, textord, "\u2223", "|"); +defineSymbol(math, main, textord, "\u2223", "\\vert"); +defineSymbol(math, main, textord, "\u2225", "\\|"); +defineSymbol(math, main, textord, "\u2225", "\\Vert"); +defineSymbol(math, main, rel, "\u2191", "\\uparrow"); +defineSymbol(math, main, rel, "\u21d1", "\\Uparrow"); +defineSymbol(math, main, rel, "\u2193", "\\downarrow"); +defineSymbol(math, main, rel, "\u21d3", "\\Downarrow"); +defineSymbol(math, main, rel, "\u2195", "\\updownarrow"); +defineSymbol(math, main, rel, "\u21d5", "\\Updownarrow"); +defineSymbol(math, math, op, "\u2210", "\\coprod"); +defineSymbol(math, math, op, "\u22c1", "\\bigvee"); +defineSymbol(math, math, op, "\u22c0", "\\bigwedge"); +defineSymbol(math, math, op, "\u2a04", "\\biguplus"); +defineSymbol(math, math, op, "\u22c2", "\\bigcap"); +defineSymbol(math, math, op, "\u22c3", "\\bigcup"); +defineSymbol(math, math, op, "\u222b", "\\int"); +defineSymbol(math, math, op, "\u222b", "\\intop"); +defineSymbol(math, math, op, "\u222c", "\\iint"); +defineSymbol(math, math, op, "\u222d", "\\iiint"); +defineSymbol(math, math, op, "\u220f", "\\prod"); +defineSymbol(math, math, op, "\u2211", "\\sum"); +defineSymbol(math, math, op, "\u2a02", "\\bigotimes"); +defineSymbol(math, math, op, "\u2a01", "\\bigoplus"); +defineSymbol(math, math, op, "\u2a00", "\\bigodot"); +defineSymbol(math, math, op, "\u222e", "\\oint"); +defineSymbol(math, math, op, "\u2a06", "\\bigsqcup"); +defineSymbol(math, math, op, "\u222b", "\\smallint"); +defineSymbol(math, main, inner, "\u2026", "\\ldots"); +defineSymbol(math, main, inner, "\u22ef", "\\cdots"); +defineSymbol(math, main, inner, "\u22f1", "\\ddots"); +defineSymbol(math, main, textord, "\u22ee", "\\vdots"); +defineSymbol(math, main, accent, "\u00b4", "\\acute"); +defineSymbol(math, main, accent, "\u0060", "\\grave"); +defineSymbol(math, main, accent, "\u00a8", "\\ddot"); +defineSymbol(math, main, accent, "\u007e", "\\tilde"); +defineSymbol(math, main, accent, "\u00af", "\\bar"); +defineSymbol(math, main, accent, "\u02d8", "\\breve"); +defineSymbol(math, main, accent, "\u02c7", "\\check"); +defineSymbol(math, main, accent, "\u005e", "\\hat"); +defineSymbol(math, main, accent, "\u20d7", "\\vec"); +defineSymbol(math, main, accent, "\u02d9", "\\dot"); +defineSymbol(math, main, mathord, "\u0131", "\\imath"); +defineSymbol(math, main, mathord, "\u0237", "\\jmath"); + +defineSymbol(text, main, textord, "\u2013", "--"); +defineSymbol(text, main, textord, "\u2014", "---"); +defineSymbol(text, main, textord, "\u2018", "`"); +defineSymbol(text, main, textord, "\u2019", "'"); +defineSymbol(text, main, textord, "\u201c", "``"); +defineSymbol(text, main, textord, "\u201d", "''"); +defineSymbol(math, main, textord, "\u00b0", "\\degree"); +defineSymbol(text, main, textord, "\u00b0", "\\degree"); +defineSymbol(math, main, mathord, "\u00a3", "\\pounds"); +defineSymbol(math, ams, textord, "\u2720", "\\maltese"); +defineSymbol(text, ams, textord, "\u2720", "\\maltese"); + +defineSymbol(text, main, spacing, "\u00a0", "\\ "); +defineSymbol(text, main, spacing, "\u00a0", " "); +defineSymbol(text, main, spacing, "\u00a0", "~"); + +// There are lots of symbols which are the same, so we add them in afterwards. +var i; +var ch; + +// All of these are textords in math mode +var mathTextSymbols = "0123456789/@.\""; +for (i = 0; i < mathTextSymbols.length; i++) { + ch = mathTextSymbols.charAt(i); + defineSymbol(math, main, textord, ch, ch); +} + +// All of these are textords in text mode +var textSymbols = "0123456789!@*()-=+[]\";:?/.,"; +for (i = 0; i < textSymbols.length; i++) { + ch = textSymbols.charAt(i); + defineSymbol(text, main, textord, ch, ch); +} + +// All of these are textords in text mode, and mathords in math mode +var letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; +for (i = 0; i < letters.length; i++) { + ch = letters.charAt(i); + defineSymbol(math, main, mathord, ch, ch); + defineSymbol(text, main, textord, ch, ch); +} + +// Latin-1 letters +for (i = 0x00C0; i <= 0x00D6; i++) { + ch = String.fromCharCode(i); + defineSymbol(text, main, textord, ch, ch); +} + +for (i = 0x00D8; i <= 0x00F6; i++) { + ch = String.fromCharCode(i); + defineSymbol(text, main, textord, ch, ch); +} + +for (i = 0x00F8; i <= 0x00FF; i++) { + ch = String.fromCharCode(i); + defineSymbol(text, main, textord, ch, ch); +} + +// Cyrillic +for (i = 0x0410; i <= 0x044F; i++) { + ch = String.fromCharCode(i); + defineSymbol(text, main, textord, ch, ch); +} + +},{}],24:[function(require,module,exports){ +var hangulRegex = /[\uAC00-\uD7AF]/; + +// This regex combines +// - Hiragana: [\u3040-\u309F] +// - Katakana: [\u30A0-\u30FF] +// - CJK ideograms: [\u4E00-\u9FAF] +// - Hangul syllables: [\uAC00-\uD7AF] +// Notably missing are halfwidth Katakana and Romanji glyphs. +var cjkRegex = + /[\u3040-\u309F]|[\u30A0-\u30FF]|[\u4E00-\u9FAF]|[\uAC00-\uD7AF]/; + +module.exports = { + cjkRegex: cjkRegex, + hangulRegex: hangulRegex, +}; + +},{}],25:[function(require,module,exports){ +/** + * This file contains a list of utility functions which are useful in other + * files. + */ + +/** + * Provide an `indexOf` function which works in IE8, but defers to native if + * possible. + */ +var nativeIndexOf = Array.prototype.indexOf; +var indexOf = function(list, elem) { + if (list == null) { + return -1; + } + if (nativeIndexOf && list.indexOf === nativeIndexOf) { + return list.indexOf(elem); + } + var i = 0; + var l = list.length; + for (; i < l; i++) { + if (list[i] === elem) { + return i; + } + } + return -1; +}; + +/** + * Return whether an element is contained in a list + */ +var contains = function(list, elem) { + return indexOf(list, elem) !== -1; +}; + +/** + * Provide a default value if a setting is undefined + */ +var deflt = function(setting, defaultIfUndefined) { + return setting === undefined ? defaultIfUndefined : setting; +}; + +// hyphenate and escape adapted from Facebook's React under Apache 2 license + +var uppercase = /([A-Z])/g; +var hyphenate = function(str) { + return str.replace(uppercase, "-$1").toLowerCase(); +}; + +var ESCAPE_LOOKUP = { + "&": "&", + ">": ">", + "<": "<", + "\"": """, + "'": "'", +}; + +var ESCAPE_REGEX = /[&><"']/g; + +function escaper(match) { + return ESCAPE_LOOKUP[match]; +} + +/** + * Escapes text to prevent scripting attacks. + * + * @param {*} text Text value to escape. + * @return {string} An escaped string. + */ +function escape(text) { + return ("" + text).replace(ESCAPE_REGEX, escaper); +} + +/** + * A function to set the text content of a DOM element in all supported + * browsers. Note that we don't define this if there is no document. + */ +var setTextContent; +if (typeof document !== "undefined") { + var testNode = document.createElement("span"); + if ("textContent" in testNode) { + setTextContent = function(node, text) { + node.textContent = text; + }; + } else { + setTextContent = function(node, text) { + node.innerText = text; + }; + } +} + +/** + * A function to clear a node. + */ +function clearNode(node) { + setTextContent(node, ""); +} + +module.exports = { + contains: contains, + deflt: deflt, + escape: escape, + hyphenate: hyphenate, + indexOf: indexOf, + setTextContent: setTextContent, + clearNode: clearNode, +}; + +},{}]},{},[1])(1) +});
\ No newline at end of file diff --git a/vendor/assets/stylesheets/katex.css b/vendor/assets/stylesheets/katex.css new file mode 100644 index 00000000000..e684d697072 --- /dev/null +++ b/vendor/assets/stylesheets/katex.css @@ -0,0 +1,934 @@ +@font-face { + font-family: 'KaTeX_AMS'; + src: url('KaTeX_AMS-Regular.eot'); + src: url('KaTeX_AMS-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_AMS-Regular.woff2') format('woff2'), url('KaTeX_AMS-Regular.woff') format('woff'), url('KaTeX_AMS-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Caligraphic'; + src: url('KaTeX_Caligraphic-Bold.eot'); + src: url('KaTeX_Caligraphic-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Caligraphic-Bold.woff2') format('woff2'), url('KaTeX_Caligraphic-Bold.woff') format('woff'), url('KaTeX_Caligraphic-Bold.ttf') format('truetype'); + font-weight: bold; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Caligraphic'; + src: url('KaTeX_Caligraphic-Regular.eot'); + src: url('KaTeX_Caligraphic-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Caligraphic-Regular.woff2') format('woff2'), url('KaTeX_Caligraphic-Regular.woff') format('woff'), url('KaTeX_Caligraphic-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Fraktur'; + src: url('KaTeX_Fraktur-Bold.eot'); + src: url('KaTeX_Fraktur-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Fraktur-Bold.woff2') format('woff2'), url('KaTeX_Fraktur-Bold.woff') format('woff'), url('KaTeX_Fraktur-Bold.ttf') format('truetype'); + font-weight: bold; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Fraktur'; + src: url('KaTeX_Fraktur-Regular.eot'); + src: url('KaTeX_Fraktur-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Fraktur-Regular.woff2') format('woff2'), url('KaTeX_Fraktur-Regular.woff') format('woff'), url('KaTeX_Fraktur-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Main'; + src: url('KaTeX_Main-Bold.eot'); + src: url('KaTeX_Main-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Bold.woff2') format('woff2'), url('KaTeX_Main-Bold.woff') format('woff'), url('KaTeX_Main-Bold.ttf') format('truetype'); + font-weight: bold; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Main'; + src: url('KaTeX_Main-Italic.eot'); + src: url('KaTeX_Main-Italic.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Italic.woff2') format('woff2'), url('KaTeX_Main-Italic.woff') format('woff'), url('KaTeX_Main-Italic.ttf') format('truetype'); + font-weight: normal; + font-style: italic; +} +@font-face { + font-family: 'KaTeX_Main'; + src: url('KaTeX_Main-Regular.eot'); + src: url('KaTeX_Main-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Regular.woff2') format('woff2'), url('KaTeX_Main-Regular.woff') format('woff'), url('KaTeX_Main-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Math'; + src: url('KaTeX_Math-Italic.eot'); + src: url('KaTeX_Math-Italic.eot#iefix') format('embedded-opentype'), url('KaTeX_Math-Italic.woff2') format('woff2'), url('KaTeX_Math-Italic.woff') format('woff'), url('KaTeX_Math-Italic.ttf') format('truetype'); + font-weight: normal; + font-style: italic; +} +@font-face { + font-family: 'KaTeX_SansSerif'; + src: url('KaTeX_SansSerif-Regular.eot'); + src: url('KaTeX_SansSerif-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_SansSerif-Regular.woff2') format('woff2'), url('KaTeX_SansSerif-Regular.woff') format('woff'), url('KaTeX_SansSerif-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Script'; + src: url('KaTeX_Script-Regular.eot'); + src: url('KaTeX_Script-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Script-Regular.woff2') format('woff2'), url('KaTeX_Script-Regular.woff') format('woff'), url('KaTeX_Script-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Size1'; + src: url('KaTeX_Size1-Regular.eot'); + src: url('KaTeX_Size1-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size1-Regular.woff2') format('woff2'), url('KaTeX_Size1-Regular.woff') format('woff'), url('KaTeX_Size1-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Size2'; + src: url('KaTeX_Size2-Regular.eot'); + src: url('KaTeX_Size2-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size2-Regular.woff2') format('woff2'), url('KaTeX_Size2-Regular.woff') format('woff'), url('KaTeX_Size2-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Size3'; + src: url('KaTeX_Size3-Regular.eot'); + src: url('KaTeX_Size3-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size3-Regular.woff2') format('woff2'), url('KaTeX_Size3-Regular.woff') format('woff'), url('KaTeX_Size3-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Size4'; + src: url('KaTeX_Size4-Regular.eot'); + src: url('KaTeX_Size4-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size4-Regular.woff2') format('woff2'), url('KaTeX_Size4-Regular.woff') format('woff'), url('KaTeX_Size4-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Typewriter'; + src: url('KaTeX_Typewriter-Regular.eot'); + src: url('KaTeX_Typewriter-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Typewriter-Regular.woff2') format('woff2'), url('KaTeX_Typewriter-Regular.woff') format('woff'), url('KaTeX_Typewriter-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +.katex-display { + display: block; + margin: 1em 0; + text-align: center; +} +.katex-display > .katex { + display: inline-block; + text-align: initial; +} +.katex { + font: normal 1.21em KaTeX_Main, Times New Roman, serif; + line-height: 1.2; + white-space: nowrap; + text-indent: 0; +} +.katex .katex-html { + display: inline-block; +} +.katex .katex-mathml { + position: absolute; + clip: rect(1px, 1px, 1px, 1px); + padding: 0; + border: 0; + height: 1px; + width: 1px; + overflow: hidden; +} +.katex .base { + display: inline-block; +} +.katex .strut { + display: inline-block; +} +.katex .mathit { + font-family: KaTeX_Math; + font-style: italic; +} +.katex .mathbf { + font-family: KaTeX_Main; + font-weight: bold; +} +.katex .amsrm { + font-family: KaTeX_AMS; +} +.katex .mathbb { + font-family: KaTeX_AMS; +} +.katex .mathcal { + font-family: KaTeX_Caligraphic; +} +.katex .mathfrak { + font-family: KaTeX_Fraktur; +} +.katex .mathtt { + font-family: KaTeX_Typewriter; +} +.katex .mathscr { + font-family: KaTeX_Script; +} +.katex .mathsf { + font-family: KaTeX_SansSerif; +} +.katex .mainit { + font-family: KaTeX_Main; + font-style: italic; +} +.katex .textstyle > .mord + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .mord + .mbin { + margin-left: 0.22222em; +} +.katex .textstyle > .mord + .mrel { + margin-left: 0.27778em; +} +.katex .textstyle > .mord + .minner { + margin-left: 0.16667em; +} +.katex .textstyle > .mop + .mord { + margin-left: 0.16667em; +} +.katex .textstyle > .mop + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .mop + .mrel { + margin-left: 0.27778em; +} +.katex .textstyle > .mop + .minner { + margin-left: 0.16667em; +} +.katex .textstyle > .mbin + .mord { + margin-left: 0.22222em; +} +.katex .textstyle > .mbin + .mop { + margin-left: 0.22222em; +} +.katex .textstyle > .mbin + .mopen { + margin-left: 0.22222em; +} +.katex .textstyle > .mbin + .minner { + margin-left: 0.22222em; +} +.katex .textstyle > .mrel + .mord { + margin-left: 0.27778em; +} +.katex .textstyle > .mrel + .mop { + margin-left: 0.27778em; +} +.katex .textstyle > .mrel + .mopen { + margin-left: 0.27778em; +} +.katex .textstyle > .mrel + .minner { + margin-left: 0.27778em; +} +.katex .textstyle > .mclose + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .mclose + .mbin { + margin-left: 0.22222em; +} +.katex .textstyle > .mclose + .mrel { + margin-left: 0.27778em; +} +.katex .textstyle > .mclose + .minner { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mord { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mrel { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mopen { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mclose { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mpunct { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .minner { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .mord { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .mbin { + margin-left: 0.22222em; +} +.katex .textstyle > .minner + .mrel { + margin-left: 0.27778em; +} +.katex .textstyle > .minner + .mopen { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .mpunct { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .minner { + margin-left: 0.16667em; +} +.katex .mord + .mop { + margin-left: 0.16667em; +} +.katex .mop + .mord { + margin-left: 0.16667em; +} +.katex .mop + .mop { + margin-left: 0.16667em; +} +.katex .mclose + .mop { + margin-left: 0.16667em; +} +.katex .minner + .mop { + margin-left: 0.16667em; +} +.katex .reset-textstyle.textstyle { + font-size: 1em; +} +.katex .reset-textstyle.scriptstyle { + font-size: 0.7em; +} +.katex .reset-textstyle.scriptscriptstyle { + font-size: 0.5em; +} +.katex .reset-scriptstyle.textstyle { + font-size: 1.42857em; +} +.katex .reset-scriptstyle.scriptstyle { + font-size: 1em; +} +.katex .reset-scriptstyle.scriptscriptstyle { + font-size: 0.71429em; +} +.katex .reset-scriptscriptstyle.textstyle { + font-size: 2em; +} +.katex .reset-scriptscriptstyle.scriptstyle { + font-size: 1.4em; +} +.katex .reset-scriptscriptstyle.scriptscriptstyle { + font-size: 1em; +} +.katex .style-wrap { + position: relative; +} +.katex .vlist { + display: inline-block; +} +.katex .vlist > span { + display: block; + height: 0; + position: relative; +} +.katex .vlist > span > span { + display: inline-block; +} +.katex .vlist .baseline-fix { + display: inline-table; + table-layout: fixed; +} +.katex .msupsub { + text-align: left; +} +.katex .mfrac > span > span { + text-align: center; +} +.katex .mfrac .frac-line { + width: 100%; +} +.katex .mfrac .frac-line:before { + border-bottom-style: solid; + border-bottom-width: 1px; + content: ""; + display: block; +} +.katex .mfrac .frac-line:after { + border-bottom-style: solid; + border-bottom-width: 0.04em; + content: ""; + display: block; + margin-top: -1px; +} +.katex .mspace { + display: inline-block; +} +.katex .mspace.negativethinspace { + margin-left: -0.16667em; +} +.katex .mspace.thinspace { + width: 0.16667em; +} +.katex .mspace.mediumspace { + width: 0.22222em; +} +.katex .mspace.thickspace { + width: 0.27778em; +} +.katex .mspace.enspace { + width: 0.5em; +} +.katex .mspace.quad { + width: 1em; +} +.katex .mspace.qquad { + width: 2em; +} +.katex .llap, +.katex .rlap { + width: 0; + position: relative; +} +.katex .llap > .inner, +.katex .rlap > .inner { + position: absolute; +} +.katex .llap > .fix, +.katex .rlap > .fix { + display: inline-block; +} +.katex .llap > .inner { + right: 0; +} +.katex .rlap > .inner { + left: 0; +} +.katex .katex-logo .a { + font-size: 0.75em; + margin-left: -0.32em; + position: relative; + top: -0.2em; +} +.katex .katex-logo .t { + margin-left: -0.23em; +} +.katex .katex-logo .e { + margin-left: -0.1667em; + position: relative; + top: 0.2155em; +} +.katex .katex-logo .x { + margin-left: -0.125em; +} +.katex .rule { + display: inline-block; + border: solid 0; + position: relative; +} +.katex .overline .overline-line, +.katex .underline .underline-line { + width: 100%; +} +.katex .overline .overline-line:before, +.katex .underline .underline-line:before { + border-bottom-style: solid; + border-bottom-width: 1px; + content: ""; + display: block; +} +.katex .overline .overline-line:after, +.katex .underline .underline-line:after { + border-bottom-style: solid; + border-bottom-width: 0.04em; + content: ""; + display: block; + margin-top: -1px; +} +.katex .sqrt > .sqrt-sign { + position: relative; +} +.katex .sqrt .sqrt-line { + width: 100%; +} +.katex .sqrt .sqrt-line:before { + border-bottom-style: solid; + border-bottom-width: 1px; + content: ""; + display: block; +} +.katex .sqrt .sqrt-line:after { + border-bottom-style: solid; + border-bottom-width: 0.04em; + content: ""; + display: block; + margin-top: -1px; +} +.katex .sqrt > .root { + margin-left: 0.27777778em; + margin-right: -0.55555556em; +} +.katex .sizing, +.katex .fontsize-ensurer { + display: inline-block; +} +.katex .sizing.reset-size1.size1, +.katex .fontsize-ensurer.reset-size1.size1 { + font-size: 1em; +} +.katex .sizing.reset-size1.size2, +.katex .fontsize-ensurer.reset-size1.size2 { + font-size: 1.4em; +} +.katex .sizing.reset-size1.size3, +.katex .fontsize-ensurer.reset-size1.size3 { + font-size: 1.6em; +} +.katex .sizing.reset-size1.size4, +.katex .fontsize-ensurer.reset-size1.size4 { + font-size: 1.8em; +} +.katex .sizing.reset-size1.size5, +.katex .fontsize-ensurer.reset-size1.size5 { + font-size: 2em; +} +.katex .sizing.reset-size1.size6, +.katex .fontsize-ensurer.reset-size1.size6 { + font-size: 2.4em; +} +.katex .sizing.reset-size1.size7, +.katex .fontsize-ensurer.reset-size1.size7 { + font-size: 2.88em; +} +.katex .sizing.reset-size1.size8, +.katex .fontsize-ensurer.reset-size1.size8 { + font-size: 3.46em; +} +.katex .sizing.reset-size1.size9, +.katex .fontsize-ensurer.reset-size1.size9 { + font-size: 4.14em; +} +.katex .sizing.reset-size1.size10, +.katex .fontsize-ensurer.reset-size1.size10 { + font-size: 4.98em; +} +.katex .sizing.reset-size2.size1, +.katex .fontsize-ensurer.reset-size2.size1 { + font-size: 0.71428571em; +} +.katex .sizing.reset-size2.size2, +.katex .fontsize-ensurer.reset-size2.size2 { + font-size: 1em; +} +.katex .sizing.reset-size2.size3, +.katex .fontsize-ensurer.reset-size2.size3 { + font-size: 1.14285714em; +} +.katex .sizing.reset-size2.size4, +.katex .fontsize-ensurer.reset-size2.size4 { + font-size: 1.28571429em; +} +.katex .sizing.reset-size2.size5, +.katex .fontsize-ensurer.reset-size2.size5 { + font-size: 1.42857143em; +} +.katex .sizing.reset-size2.size6, +.katex .fontsize-ensurer.reset-size2.size6 { + font-size: 1.71428571em; +} +.katex .sizing.reset-size2.size7, +.katex .fontsize-ensurer.reset-size2.size7 { + font-size: 2.05714286em; +} +.katex .sizing.reset-size2.size8, +.katex .fontsize-ensurer.reset-size2.size8 { + font-size: 2.47142857em; +} +.katex .sizing.reset-size2.size9, +.katex .fontsize-ensurer.reset-size2.size9 { + font-size: 2.95714286em; +} +.katex .sizing.reset-size2.size10, +.katex .fontsize-ensurer.reset-size2.size10 { + font-size: 3.55714286em; +} +.katex .sizing.reset-size3.size1, +.katex .fontsize-ensurer.reset-size3.size1 { + font-size: 0.625em; +} +.katex .sizing.reset-size3.size2, +.katex .fontsize-ensurer.reset-size3.size2 { + font-size: 0.875em; +} +.katex .sizing.reset-size3.size3, +.katex .fontsize-ensurer.reset-size3.size3 { + font-size: 1em; +} +.katex .sizing.reset-size3.size4, +.katex .fontsize-ensurer.reset-size3.size4 { + font-size: 1.125em; +} +.katex .sizing.reset-size3.size5, +.katex .fontsize-ensurer.reset-size3.size5 { + font-size: 1.25em; +} +.katex .sizing.reset-size3.size6, +.katex .fontsize-ensurer.reset-size3.size6 { + font-size: 1.5em; +} +.katex .sizing.reset-size3.size7, +.katex .fontsize-ensurer.reset-size3.size7 { + font-size: 1.8em; +} +.katex .sizing.reset-size3.size8, +.katex .fontsize-ensurer.reset-size3.size8 { + font-size: 2.1625em; +} +.katex .sizing.reset-size3.size9, +.katex .fontsize-ensurer.reset-size3.size9 { + font-size: 2.5875em; +} +.katex .sizing.reset-size3.size10, +.katex .fontsize-ensurer.reset-size3.size10 { + font-size: 3.1125em; +} +.katex .sizing.reset-size4.size1, +.katex .fontsize-ensurer.reset-size4.size1 { + font-size: 0.55555556em; +} +.katex .sizing.reset-size4.size2, +.katex .fontsize-ensurer.reset-size4.size2 { + font-size: 0.77777778em; +} +.katex .sizing.reset-size4.size3, +.katex .fontsize-ensurer.reset-size4.size3 { + font-size: 0.88888889em; +} +.katex .sizing.reset-size4.size4, +.katex .fontsize-ensurer.reset-size4.size4 { + font-size: 1em; +} +.katex .sizing.reset-size4.size5, +.katex .fontsize-ensurer.reset-size4.size5 { + font-size: 1.11111111em; +} +.katex .sizing.reset-size4.size6, +.katex .fontsize-ensurer.reset-size4.size6 { + font-size: 1.33333333em; +} +.katex .sizing.reset-size4.size7, +.katex .fontsize-ensurer.reset-size4.size7 { + font-size: 1.6em; +} +.katex .sizing.reset-size4.size8, +.katex .fontsize-ensurer.reset-size4.size8 { + font-size: 1.92222222em; +} +.katex .sizing.reset-size4.size9, +.katex .fontsize-ensurer.reset-size4.size9 { + font-size: 2.3em; +} +.katex .sizing.reset-size4.size10, +.katex .fontsize-ensurer.reset-size4.size10 { + font-size: 2.76666667em; +} +.katex .sizing.reset-size5.size1, +.katex .fontsize-ensurer.reset-size5.size1 { + font-size: 0.5em; +} +.katex .sizing.reset-size5.size2, +.katex .fontsize-ensurer.reset-size5.size2 { + font-size: 0.7em; +} +.katex .sizing.reset-size5.size3, +.katex .fontsize-ensurer.reset-size5.size3 { + font-size: 0.8em; +} +.katex .sizing.reset-size5.size4, +.katex .fontsize-ensurer.reset-size5.size4 { + font-size: 0.9em; +} +.katex .sizing.reset-size5.size5, +.katex .fontsize-ensurer.reset-size5.size5 { + font-size: 1em; +} +.katex .sizing.reset-size5.size6, +.katex .fontsize-ensurer.reset-size5.size6 { + font-size: 1.2em; +} +.katex .sizing.reset-size5.size7, +.katex .fontsize-ensurer.reset-size5.size7 { + font-size: 1.44em; +} +.katex .sizing.reset-size5.size8, +.katex .fontsize-ensurer.reset-size5.size8 { + font-size: 1.73em; +} +.katex .sizing.reset-size5.size9, +.katex .fontsize-ensurer.reset-size5.size9 { + font-size: 2.07em; +} +.katex .sizing.reset-size5.size10, +.katex .fontsize-ensurer.reset-size5.size10 { + font-size: 2.49em; +} +.katex .sizing.reset-size6.size1, +.katex .fontsize-ensurer.reset-size6.size1 { + font-size: 0.41666667em; +} +.katex .sizing.reset-size6.size2, +.katex .fontsize-ensurer.reset-size6.size2 { + font-size: 0.58333333em; +} +.katex .sizing.reset-size6.size3, +.katex .fontsize-ensurer.reset-size6.size3 { + font-size: 0.66666667em; +} +.katex .sizing.reset-size6.size4, +.katex .fontsize-ensurer.reset-size6.size4 { + font-size: 0.75em; +} +.katex .sizing.reset-size6.size5, +.katex .fontsize-ensurer.reset-size6.size5 { + font-size: 0.83333333em; +} +.katex .sizing.reset-size6.size6, +.katex .fontsize-ensurer.reset-size6.size6 { + font-size: 1em; +} +.katex .sizing.reset-size6.size7, +.katex .fontsize-ensurer.reset-size6.size7 { + font-size: 1.2em; +} +.katex .sizing.reset-size6.size8, +.katex .fontsize-ensurer.reset-size6.size8 { + font-size: 1.44166667em; +} +.katex .sizing.reset-size6.size9, +.katex .fontsize-ensurer.reset-size6.size9 { + font-size: 1.725em; +} +.katex .sizing.reset-size6.size10, +.katex .fontsize-ensurer.reset-size6.size10 { + font-size: 2.075em; +} +.katex .sizing.reset-size7.size1, +.katex .fontsize-ensurer.reset-size7.size1 { + font-size: 0.34722222em; +} +.katex .sizing.reset-size7.size2, +.katex .fontsize-ensurer.reset-size7.size2 { + font-size: 0.48611111em; +} +.katex .sizing.reset-size7.size3, +.katex .fontsize-ensurer.reset-size7.size3 { + font-size: 0.55555556em; +} +.katex .sizing.reset-size7.size4, +.katex .fontsize-ensurer.reset-size7.size4 { + font-size: 0.625em; +} +.katex .sizing.reset-size7.size5, +.katex .fontsize-ensurer.reset-size7.size5 { + font-size: 0.69444444em; +} +.katex .sizing.reset-size7.size6, +.katex .fontsize-ensurer.reset-size7.size6 { + font-size: 0.83333333em; +} +.katex .sizing.reset-size7.size7, +.katex .fontsize-ensurer.reset-size7.size7 { + font-size: 1em; +} +.katex .sizing.reset-size7.size8, +.katex .fontsize-ensurer.reset-size7.size8 { + font-size: 1.20138889em; +} +.katex .sizing.reset-size7.size9, +.katex .fontsize-ensurer.reset-size7.size9 { + font-size: 1.4375em; +} +.katex .sizing.reset-size7.size10, +.katex .fontsize-ensurer.reset-size7.size10 { + font-size: 1.72916667em; +} +.katex .sizing.reset-size8.size1, +.katex .fontsize-ensurer.reset-size8.size1 { + font-size: 0.28901734em; +} +.katex .sizing.reset-size8.size2, +.katex .fontsize-ensurer.reset-size8.size2 { + font-size: 0.40462428em; +} +.katex .sizing.reset-size8.size3, +.katex .fontsize-ensurer.reset-size8.size3 { + font-size: 0.46242775em; +} +.katex .sizing.reset-size8.size4, +.katex .fontsize-ensurer.reset-size8.size4 { + font-size: 0.52023121em; +} +.katex .sizing.reset-size8.size5, +.katex .fontsize-ensurer.reset-size8.size5 { + font-size: 0.57803468em; +} +.katex .sizing.reset-size8.size6, +.katex .fontsize-ensurer.reset-size8.size6 { + font-size: 0.69364162em; +} +.katex .sizing.reset-size8.size7, +.katex .fontsize-ensurer.reset-size8.size7 { + font-size: 0.83236994em; +} +.katex .sizing.reset-size8.size8, +.katex .fontsize-ensurer.reset-size8.size8 { + font-size: 1em; +} +.katex .sizing.reset-size8.size9, +.katex .fontsize-ensurer.reset-size8.size9 { + font-size: 1.19653179em; +} +.katex .sizing.reset-size8.size10, +.katex .fontsize-ensurer.reset-size8.size10 { + font-size: 1.43930636em; +} +.katex .sizing.reset-size9.size1, +.katex .fontsize-ensurer.reset-size9.size1 { + font-size: 0.24154589em; +} +.katex .sizing.reset-size9.size2, +.katex .fontsize-ensurer.reset-size9.size2 { + font-size: 0.33816425em; +} +.katex .sizing.reset-size9.size3, +.katex .fontsize-ensurer.reset-size9.size3 { + font-size: 0.38647343em; +} +.katex .sizing.reset-size9.size4, +.katex .fontsize-ensurer.reset-size9.size4 { + font-size: 0.43478261em; +} +.katex .sizing.reset-size9.size5, +.katex .fontsize-ensurer.reset-size9.size5 { + font-size: 0.48309179em; +} +.katex .sizing.reset-size9.size6, +.katex .fontsize-ensurer.reset-size9.size6 { + font-size: 0.57971014em; +} +.katex .sizing.reset-size9.size7, +.katex .fontsize-ensurer.reset-size9.size7 { + font-size: 0.69565217em; +} +.katex .sizing.reset-size9.size8, +.katex .fontsize-ensurer.reset-size9.size8 { + font-size: 0.83574879em; +} +.katex .sizing.reset-size9.size9, +.katex .fontsize-ensurer.reset-size9.size9 { + font-size: 1em; +} +.katex .sizing.reset-size9.size10, +.katex .fontsize-ensurer.reset-size9.size10 { + font-size: 1.20289855em; +} +.katex .sizing.reset-size10.size1, +.katex .fontsize-ensurer.reset-size10.size1 { + font-size: 0.20080321em; +} +.katex .sizing.reset-size10.size2, +.katex .fontsize-ensurer.reset-size10.size2 { + font-size: 0.2811245em; +} +.katex .sizing.reset-size10.size3, +.katex .fontsize-ensurer.reset-size10.size3 { + font-size: 0.32128514em; +} +.katex .sizing.reset-size10.size4, +.katex .fontsize-ensurer.reset-size10.size4 { + font-size: 0.36144578em; +} +.katex .sizing.reset-size10.size5, +.katex .fontsize-ensurer.reset-size10.size5 { + font-size: 0.40160643em; +} +.katex .sizing.reset-size10.size6, +.katex .fontsize-ensurer.reset-size10.size6 { + font-size: 0.48192771em; +} +.katex .sizing.reset-size10.size7, +.katex .fontsize-ensurer.reset-size10.size7 { + font-size: 0.57831325em; +} +.katex .sizing.reset-size10.size8, +.katex .fontsize-ensurer.reset-size10.size8 { + font-size: 0.69477912em; +} +.katex .sizing.reset-size10.size9, +.katex .fontsize-ensurer.reset-size10.size9 { + font-size: 0.8313253em; +} +.katex .sizing.reset-size10.size10, +.katex .fontsize-ensurer.reset-size10.size10 { + font-size: 1em; +} +.katex .delimsizing.size1 { + font-family: KaTeX_Size1; +} +.katex .delimsizing.size2 { + font-family: KaTeX_Size2; +} +.katex .delimsizing.size3 { + font-family: KaTeX_Size3; +} +.katex .delimsizing.size4 { + font-family: KaTeX_Size4; +} +.katex .delimsizing.mult .delim-size1 > span { + font-family: KaTeX_Size1; +} +.katex .delimsizing.mult .delim-size4 > span { + font-family: KaTeX_Size4; +} +.katex .nulldelimiter { + display: inline-block; + width: 0.12em; +} +.katex .op-symbol { + position: relative; +} +.katex .op-symbol.small-op { + font-family: KaTeX_Size1; +} +.katex .op-symbol.large-op { + font-family: KaTeX_Size2; +} +.katex .op-limits > .vlist > span { + text-align: center; +} +.katex .accent > .vlist > span { + text-align: center; +} +.katex .accent .accent-body > span { + width: 0; +} +.katex .accent .accent-body.accent-vec > span { + position: relative; + left: 0.326em; +} +.katex .mtable .vertical-separator { + display: inline-block; + margin: 0 -0.025em; + border-right: 0.05em solid black; +} +.katex .mtable .arraycolsep { + display: inline-block; +} +.katex .mtable .col-align-c > .vlist { + text-align: center; +} +.katex .mtable .col-align-l > .vlist { + text-align: left; +} +.katex .mtable .col-align-r > .vlist { + text-align: right; +} |