From a59786be2e3113f931c520ebc7f309556bd6940e Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sat, 4 Jun 2016 10:09:01 -0400 Subject: [PATCH 001/825] Change single-doc setup to use data-attribute instead of inline script --- assets/javascripts/app/app.coffee | 3 ++- test/app_test.rb | 4 ++-- views/other.erb | 3 +-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/assets/javascripts/app/app.coffee b/assets/javascripts/app/app.coffee index e8f083a2..7a9f925b 100644 --- a/assets/javascripts/app/app.coffee +++ b/assets/javascripts/app/app.coffee @@ -27,7 +27,8 @@ @document = new app.views.Document @mobile = new app.views.Mobile if @isMobile() - if @DOC + if document.body.hasAttribute('data-doc') + @DOC = JSON.parse(document.body.getAttribute('data-doc')) @bootOne() else if @DOCS @bootAll() diff --git a/test/app_test.rb b/test/app_test.rb index eebab3aa..50cf78e4 100644 --- a/test/app_test.rb +++ b/test/app_test.rb @@ -185,13 +185,13 @@ class AppTest < MiniTest::Spec it "works when the doc exists" do get '/html~4-foo-bar_42/' assert last_response.ok? - assert_includes last_response.body, 'app.DOC = {"name":"HTML","slug":"html~4"' + assert_includes last_response.body, 'data-doc="{"name":"HTML","slug":"html~4"' end it "works when the doc has no version in the path and a version exists" do get '/html-foo-bar_42/' assert last_response.ok? - assert_includes last_response.body, 'app.DOC = {"name":"HTML","slug":"html~5"' + assert_includes last_response.body, 'data-doc="{"name":"HTML","slug":"html~5"' end it "returns 404 when the type is blank" do diff --git a/views/other.erb b/views/other.erb index f1b6247c..4c8df481 100644 --- a/views/other.erb +++ b/views/other.erb @@ -13,9 +13,8 @@ <%= javascript_tag 'application', asset_host: false %><% unless App.production? %> <%= javascript_tag 'debug' %><% end %> - - + <%= erb :app %> From fd6683f3b234760fbe9446c0872612f770a877e6 Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sat, 4 Jun 2016 10:14:28 -0400 Subject: [PATCH 002/825] Move tracking scripts inside the JS bundle --- assets/javascripts/application.js.coffee | 2 ++ assets/javascripts/tracking.js | 20 ++++++++++++++++++++ views/app.erb | 14 -------------- 3 files changed, 22 insertions(+), 14 deletions(-) create mode 100644 assets/javascripts/tracking.js diff --git a/assets/javascripts/application.js.coffee b/assets/javascripts/application.js.coffee index ae85f852..6bf87f1c 100644 --- a/assets/javascripts/application.js.coffee +++ b/assets/javascripts/application.js.coffee @@ -18,6 +18,8 @@ #= require_tree ./templates +#= require tracking + init = -> document.removeEventListener 'DOMContentLoaded', init, false diff --git a/assets/javascripts/tracking.js b/assets/javascripts/tracking.js new file mode 100644 index 00000000..6f505c5d --- /dev/null +++ b/assets/javascripts/tracking.js @@ -0,0 +1,20 @@ +try { + if (app.config.env == 'production') { + (function() { + (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); + ga('create', 'UA-5544833-12', 'devdocs.io'); + ga('send', 'pageview'); + })(); + + (function() { + var _gauges=_gauges||[];!function(){var a=document.createElement("script"); + a.type="text/javascript",a.async=!0,a.id="gauges-tracker", + a.setAttribute("data-site-id","51c15f82613f5d7819000067"), + a.src="https://secure.gaug.es/track.js";var b=document.getElementsByTagName("script")[0]; + b.parentNode.insertBefore(a,b)}(); + })(); + } +} catch(e) { } diff --git a/views/app.erb b/views/app.erb index c034d735..c0ed5056 100644 --- a/views/app.erb +++ b/views/app.erb @@ -30,20 +30,6 @@
-<% if App.production? %><% end %> From ce8079fff36415a2c181a751998b8e30c3753fcc Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sat, 25 Feb 2017 09:16:28 -0500 Subject: [PATCH 470/825] Fix tabindex attribute breaking default text cursor in Firefox --- views/app.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/app.erb b/views/app.erb index e11411cb..0b3104e0 100644 --- a/views/app.erb +++ b/views/app.erb @@ -35,7 +35,7 @@
-
+
From 88ba685441f1da0630d52c0c62872e3953853072 Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sun, 26 Feb 2017 10:06:20 -0500 Subject: [PATCH 472/825] Remove legacy code --- assets/javascripts/app/app.coffee | 10 ++-------- assets/javascripts/app/settings.coffee | 15 +-------------- assets/javascripts/news.json | 3 --- .../templates/pages/offline_tmpl.coffee | 2 -- 4 files changed, 3 insertions(+), 27 deletions(-) diff --git a/assets/javascripts/app/app.coffee b/assets/javascripts/app/app.coffee index 427e31e0..50190069 100644 --- a/assets/javascripts/app/app.coffee +++ b/assets/javascripts/app/app.coffee @@ -15,7 +15,7 @@ @el = $('._app') @localStorage = new LocalStorageStore @appCache = new app.AppCache if app.AppCache.isEnabled() - @settings = new app.Settings @localStorage + @settings = new app.Settings @db = new app.DB() @docs = new app.collections.Docs @@ -106,7 +106,6 @@ @hideLoading() @welcomeBack() unless @doc @removeEvent 'ready bootError' - try navigator.mozApps?.getSelf().onsuccess = -> app.mozApp = true catch return initDoc: (doc) -> @@ -117,12 +116,7 @@ migrateDocs: -> for slug in @settings.getDocs() when not @docs.findBy('slug', slug) needsSaving = true - doc = @disabledDocs.findBy('slug', 'codeigniter~3') if slug == 'codeigniter~3.0' - doc = @disabledDocs.findBy('slug', 'node~4_lts') if slug == 'node~4.2_lts' - doc = @disabledDocs.findBy('slug', 'xslt_xpath') if slug == 'xpath' - doc = @disabledDocs.findBy('slug', 'angular~2_typescript') if slug == 'angular~2.0_typescript' - doc = @disabledDocs.findBy('slug', "angularjs~#{match[1]}") if match = /^angular~(1\.\d)$/.exec(slug) - doc ||= @disabledDocs.findBy('slug_without_version', slug) + doc = @disabledDocs.findBy('slug_without_version', slug) if doc @disabledDocs.remove(doc) @docs.add(doc) diff --git a/assets/javascripts/app/settings.coffee b/assets/javascripts/app/settings.coffee index 96f923bd..ec651b32 100644 --- a/assets/javascripts/app/settings.coffee +++ b/assets/javascripts/app/settings.coffee @@ -13,21 +13,8 @@ class app.Settings manualUpdate: false schema: 1 - constructor: (legacyStore) -> + constructor: -> @store = new CookieStore - @importLegacyValues(legacyStore) - - importLegacyValues: (legacyStore) -> - return unless settings = legacyStore.get('settings') - for key, value of settings - if key == 'autoUpdate' - key = 'manualUpdate' - value = !value - else if key == 'tips' - value = value.join('/') - @store.set(key, value) - legacyStore.del('settings') - return set: (key, value) -> @store.set(key, value) diff --git a/assets/javascripts/news.json b/assets/javascripts/news.json index a67185d7..e28e6f25 100644 --- a/assets/javascripts/news.json +++ b/assets/javascripts/news.json @@ -90,9 +90,6 @@ "2015-07-26", "Added search abbreviations (e.g. $ is an alias for jQuery).\nClick here to see the full list. Feel free to suggest more on GitHub.", "Added shift + ↓/↑ shortcut for scrolling (same as alt + ↓/↑)." - ], [ - "2015-07-12", - "New sponsors: JetBrains and Code School\nIf you like DevDocs, please take a moment to check out their products — they're awesome!" ], [ "2015-07-05", "New documentations: Drupal, Vue.js, Phaser and webpack" diff --git a/assets/javascripts/templates/pages/offline_tmpl.coffee b/assets/javascripts/templates/pages/offline_tmpl.coffee index d60d9968..752031d2 100644 --- a/assets/javascripts/templates/pages/offline_tmpl.coffee +++ b/assets/javascripts/templates/pages/offline_tmpl.coffee @@ -42,8 +42,6 @@ app.templates.offlinePage = (docs) -> """ canICloseTheTab = -> if app.AppCache.isEnabled() """ Yes! Even offline, you can open a new tab, go to devdocs.io, and everything will work as if you were online (provided you installed all the documentations you want to use beforehand). """ - else if app.mozApp - """ Yes! Even offline, you can open the app and everything will work as if you were online (provided you installed all the documentations you want to use beforehand). """ else """ No. AppCache isn't available in your browser (or is disabled) so loading devdocs.io offline won't work.
The current tab will continue to work, though (provided you installed all the documentations you want to use beforehand). """ From 29ec9db263f897287df7960366efe98c22c56d4f Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sun, 26 Feb 2017 11:56:30 -0500 Subject: [PATCH 473/825] UI tweaks --- .../templates/pages/offline_tmpl.coffee | 2 +- assets/stylesheets/components/_content.scss | 19 ++++++++----------- assets/stylesheets/components/_header.scss | 2 +- assets/stylesheets/components/_mobile.scss | 1 + assets/stylesheets/components/_settings.scss | 16 ++++++++++------ assets/stylesheets/components/_sidebar.scss | 1 + assets/stylesheets/global/_base.scss | 1 + assets/stylesheets/pages/_support_tables.scss | 1 - assets/stylesheets/pages/_yii.scss | 2 -- views/app.erb | 5 ++--- 10 files changed, 25 insertions(+), 25 deletions(-) diff --git a/assets/javascripts/templates/pages/offline_tmpl.coffee b/assets/javascripts/templates/pages/offline_tmpl.coffee index 752031d2..2429ec65 100644 --- a/assets/javascripts/templates/pages/offline_tmpl.coffee +++ b/assets/javascripts/templates/pages/offline_tmpl.coffee @@ -52,7 +52,7 @@ app.templates.offlineDoc = (doc, status) -> html = """ #{doc.fullName} - #{Math.ceil(doc.db_size / 100000) / 10} MB + #{Math.ceil(doc.db_size / 100000) / 10} MB """ html += if !(status and status.installed) diff --git a/assets/stylesheets/components/_content.scss b/assets/stylesheets/components/_content.scss index f9dde186..b5744001 100644 --- a/assets/stylesheets/components/_content.scss +++ b/assets/stylesheets/components/_content.scss @@ -38,17 +38,12 @@ margin-top: $headerHeight; } - &:after { // padding bottom - content: ''; - display: block; - margin-bottom: 1.25rem; - } - &::-webkit-scrollbar { -webkit-appearance: none; } &::-webkit-scrollbar:vertical { width: 14px; } &::-webkit-scrollbar:horizontal { height: 14px } &::-webkit-scrollbar-button { display: none; } - &::-webkit-scrollbar-track { background: $contentBackground; } + &::-webkit-scrollbar-track, + &::-webkit-scrollbar-corner { background: $contentBackground; } &::-webkit-scrollbar-thumb { min-height: 2rem; background: $scrollbarColor; @@ -56,6 +51,7 @@ border: 5px solid $contentBackground; border-radius: 10px; + &:hover, &:active { background-color: $scrollbarColorHover; border-width: 4px; @@ -229,8 +225,6 @@ ._credits { width: 100%; - - td:first-child, td:last-child { white-space: nowrap; } } // @@ -244,7 +238,6 @@ th, td { width: 1%; - white-space: nowrap; &:first-child { width: auto; } &:last-child { width: 12rem; } @@ -259,7 +252,11 @@ @extend %doc-icon; } -._docs-size { text-align: right; } +._docs-size { + text-align: right; + + > small { color: $textColorLight; } +} ._docs-tools { overflow: hidden; diff --git a/assets/stylesheets/components/_header.scss b/assets/stylesheets/components/_header.scss index 50c756c2..5dc13256 100644 --- a/assets/stylesheets/components/_header.scss +++ b/assets/stylesheets/components/_header.scss @@ -146,7 +146,7 @@ &:before { position: absolute; z-index: 1; - top: 1rem; + top: calc(1rem - 1px); left: 1rem; opacity: .4; pointer-events: none; diff --git a/assets/stylesheets/components/_mobile.scss b/assets/stylesheets/components/_mobile.scss index 7039eda0..25b24de7 100644 --- a/assets/stylesheets/components/_mobile.scss +++ b/assets/stylesheets/components/_mobile.scss @@ -42,6 +42,7 @@ ._settings { position: static; } ._settings ._sidebar { padding-bottom: $headerHeight; } ._settings-tabs { display: block; } + ._header ._settings-btn { width: auto; } // Header diff --git a/assets/stylesheets/components/_settings.scss b/assets/stylesheets/components/_settings.scss index 089e0732..ec69f95d 100644 --- a/assets/stylesheets/components/_settings.scss +++ b/assets/stylesheets/components/_settings.scss @@ -22,6 +22,10 @@ } } +// +// Settings page +// + ._settings-fieldset { display: flex; margin: 1.5rem 0; @@ -68,12 +72,13 @@ vertical-align: top; margin-left: .375rem; - &[data-behavior=reset] { - font-size: .75rem; - color: $textColorRed; - } + &[data-behavior="reset"] { color: $textColorRed; } } +// +// Footer +// + ._footer { position: absolute; z-index: $headerZ; @@ -92,6 +97,7 @@ ._settings-btn { display: block; + width: 100%; height: 100%; line-height: 1.5rem; padding: 0 .75rem; @@ -113,8 +119,6 @@ } } -._save-btn { width: 100%; } - // // Header tabs // diff --git a/assets/stylesheets/components/_sidebar.scss b/assets/stylesheets/components/_sidebar.scss index 1e6b6d52..a02cba41 100644 --- a/assets/stylesheets/components/_sidebar.scss +++ b/assets/stylesheets/components/_sidebar.scss @@ -27,6 +27,7 @@ border: 3px solid $contentBackground; border-radius: 5px; + &:hover, &:active { background-color: $scrollbarColorHover; border-width: 2px; diff --git a/assets/stylesheets/global/_base.scss b/assets/stylesheets/global/_base.scss index 58012df4..9615e117 100644 --- a/assets/stylesheets/global/_base.scss +++ b/assets/stylesheets/global/_base.scss @@ -126,6 +126,7 @@ th, td { padding-bottom: -webkit-calc(.3em + 1px); padding-bottom: calc(.3em + 1px); text-align: left; + white-space: normal !important; } th { diff --git a/assets/stylesheets/pages/_support_tables.scss b/assets/stylesheets/pages/_support_tables.scss index 8fc21fac..0ef0b3d3 100644 --- a/assets/stylesheets/pages/_support_tables.scss +++ b/assets/stylesheets/pages/_support_tables.scss @@ -30,7 +30,6 @@ td { padding: .125rem .25rem; - white-space: nowrap; cursor: default; } diff --git a/assets/stylesheets/pages/_yii.scss b/assets/stylesheets/pages/_yii.scss index e27623dd..e2d5f0b7 100644 --- a/assets/stylesheets/pages/_yii.scss +++ b/assets/stylesheets/pages/_yii.scss @@ -9,6 +9,4 @@ float: right; color: $textColorLight; } - - .param-type-col { white-space: nowrap; } } diff --git a/views/app.erb b/views/app.erb index 92d83a61..accac693 100644 --- a/views/app.erb +++ b/views/app.erb @@ -44,13 +44,12 @@ Back
From 71a3c311455bd3144651973a94dc5c1d3d32c3eb Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sun, 26 Feb 2017 12:10:35 -0500 Subject: [PATCH 474/825] Add setting to disable smooth scrolling --- assets/javascripts/app/settings.coffee | 7 ------- .../javascripts/templates/pages/settings_tmpl.coffee | 10 ++++++++++ assets/javascripts/views/content/content.coffee | 5 ++++- assets/javascripts/views/content/settings_page.coffee | 11 +++++++++-- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/assets/javascripts/app/settings.coffee b/assets/javascripts/app/settings.coffee index ec651b32..b9a9e1c3 100644 --- a/assets/javascripts/app/settings.coffee +++ b/assets/javascripts/app/settings.coffee @@ -40,13 +40,6 @@ class app.Settings @store.set TIPS_KEY, tips.join('/') return - setDark: (value) -> - @store.set DARK_KEY, !!value - return - - getDark: -> - @store.get DARK_KEY - setLayout: (name, enable) -> layout = (@store.get(LAYOUT_KEY) || '').split(' ') $.arrayDelete(layout, '') diff --git a/assets/javascripts/templates/pages/settings_tmpl.coffee b/assets/javascripts/templates/pages/settings_tmpl.coffee index 31cc0e72..10e3761b 100644 --- a/assets/javascripts/templates/pages/settings_tmpl.coffee +++ b/assets/javascripts/templates/pages/settings_tmpl.coffee @@ -17,6 +17,16 @@ app.templates.settingsPage = (settings) -> """ +
+

Scrolling:

+ +
+ +
+
+

Advanced:

diff --git a/assets/javascripts/views/content/content.coffee b/assets/javascripts/views/content/content.coffee index 404938b4..ecbd8f15 100644 --- a/assets/javascripts/views/content/content.coffee +++ b/assets/javascripts/views/content/content.coffee @@ -64,7 +64,10 @@ class app.views.Content extends app.View return smoothScrollTo: (value) -> - $.smoothScroll @scrollEl, value or 0 + if app.settings.get('fastScroll') + @scrollTo value + else + $.smoothScroll @scrollEl, value or 0 return scrollBy: (n) -> diff --git a/assets/javascripts/views/content/settings_page.coffee b/assets/javascripts/views/content/settings_page.coffee index 7b74e6bf..b4be96e0 100644 --- a/assets/javascripts/views/content/settings_page.coffee +++ b/assets/javascripts/views/content/settings_page.coffee @@ -13,7 +13,8 @@ class app.views.SettingsPage extends app.View currentSettings: -> settings = {} - settings.dark = app.settings.getDark() + settings.dark = app.settings.get('dark') + settings.smoothScroll = !app.settings.get('fastScroll') settings[layout] = app.settings.hasLayout(layout) for layout in LAYOUTS settings @@ -25,7 +26,7 @@ class app.views.SettingsPage extends app.View alt = css.getAttribute('data-alt') css.setAttribute('data-alt', css.getAttribute('href')) css.setAttribute('href', alt) - app.settings.setDark(enable) + app.settings.set('dark', !!enable) app.appCache?.updateInBackground() return @@ -35,6 +36,10 @@ class app.views.SettingsPage extends app.View app.appCache?.updateInBackground() return + toggleSmoothScroll: (enable) -> + app.settings.set('fastScroll', !enable) + return + onChange: (event) => input = event.target switch input.name @@ -42,6 +47,8 @@ class app.views.SettingsPage extends app.View @toggleDark input.checked when 'layout' @toggleLayout input.value, input.checked + when 'smoothScroll' + @toggleSmoothScroll input.checked return onRoute: (route) => From 4984f0064f55694bc42d7b49ec3b5751db67e120 Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sun, 26 Feb 2017 12:33:20 -0500 Subject: [PATCH 475/825] Add setting for swapping arrow keys behavior Closes #296. --- assets/javascripts/app/shortcuts.coffee | 15 ++++++++++++--- .../templates/pages/settings_tmpl.coffee | 4 ++++ .../views/content/settings_page.coffee | 7 +++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/assets/javascripts/app/shortcuts.coffee b/assets/javascripts/app/shortcuts.coffee index cef7df39..11140667 100644 --- a/assets/javascripts/app/shortcuts.coffee +++ b/assets/javascripts/app/shortcuts.coffee @@ -15,6 +15,9 @@ class app.Shortcuts $.off document, 'keypress', @onKeypress return + swapArrowKeysBehavior: -> + app.settings.get('arrowScroll') + showTip: -> app.showTip('KeyNav') @showTip = null @@ -40,7 +43,9 @@ class app.Shortcuts event.preventDefault() if result is false return - handleKeydownEvent: (event) -> + handleKeydownEvent: (event, _force) -> + return @handleKeydownAltEvent(event, true) if not _force and event.which in [37, 38, 39, 40] and @swapArrowKeysBehavior() + if not event.target.form and (48 <= event.which <= 57 or 65 <= event.which <= 90) @trigger 'typing' return @@ -100,7 +105,9 @@ class app.Shortcuts @trigger 'pageBottom' false - handleKeydownShiftEvent: (event) -> + handleKeydownShiftEvent: (event, _force) -> + return @handleKeydownEvent(event, true) if not _force and event.which in [37, 38, 39, 40] and @swapArrowKeysBehavior() + if not event.target.form and 65 <= event.which <= 90 @trigger 'typing' return @@ -118,7 +125,9 @@ class app.Shortcuts @trigger 'altDown' false - handleKeydownAltEvent: (event) -> + handleKeydownAltEvent: (event, _force) -> + return @handleKeydownEvent(event, true) if not _force and event.which in [37, 38, 39, 40] and @swapArrowKeysBehavior() + switch event.which when 9 @trigger 'altRight', event diff --git a/assets/javascripts/templates/pages/settings_tmpl.coffee b/assets/javascripts/templates/pages/settings_tmpl.coffee index 10e3761b..62a8f5a1 100644 --- a/assets/javascripts/templates/pages/settings_tmpl.coffee +++ b/assets/javascripts/templates/pages/settings_tmpl.coffee @@ -24,6 +24,10 @@ app.templates.settingsPage = (settings) -> """ +
diff --git a/assets/javascripts/views/content/settings_page.coffee b/assets/javascripts/views/content/settings_page.coffee index b4be96e0..c3671c29 100644 --- a/assets/javascripts/views/content/settings_page.coffee +++ b/assets/javascripts/views/content/settings_page.coffee @@ -15,6 +15,7 @@ class app.views.SettingsPage extends app.View settings = {} settings.dark = app.settings.get('dark') settings.smoothScroll = !app.settings.get('fastScroll') + settings.arrowScroll = app.settings.get('arrowScroll') settings[layout] = app.settings.hasLayout(layout) for layout in LAYOUTS settings @@ -40,6 +41,10 @@ class app.views.SettingsPage extends app.View app.settings.set('fastScroll', !enable) return + toggle: (name, enable) -> + app.settings.set(name, enable) + return + onChange: (event) => input = event.target switch input.name @@ -49,6 +54,8 @@ class app.views.SettingsPage extends app.View @toggleLayout input.value, input.checked when 'smoothScroll' @toggleSmoothScroll input.checked + else + @toggle input.name, input.checked return onRoute: (route) => From 031b62485fd674d8f99b3890dbb213a4d606355d Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sun, 26 Feb 2017 12:47:10 -0500 Subject: [PATCH 476/825] Add keyboard shortcut to open preferences --- assets/javascripts/app/shortcuts.coffee | 3 ++ .../templates/pages/help_tmpl.coffee | 32 +++++++++++-------- .../javascripts/views/layout/document.coffee | 17 +++++++--- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/assets/javascripts/app/shortcuts.coffee b/assets/javascripts/app/shortcuts.coffee index 11140667..8ea37a24 100644 --- a/assets/javascripts/app/shortcuts.coffee +++ b/assets/javascripts/app/shortcuts.coffee @@ -104,6 +104,9 @@ class app.Shortcuts when 40 @trigger 'pageBottom' false + when 188 + @trigger 'preferences' + false handleKeydownShiftEvent: (event, _force) -> return @handleKeydownEvent(event, true) if not _force and event.which in [37, 38, 39, 40] and @swapArrowKeysBehavior() diff --git a/assets/javascripts/templates/pages/help_tmpl.coffee b/assets/javascripts/templates/pages/help_tmpl.coffee index afee16ea..c6489c1d 100644 --- a/assets/javascripts/templates/pages/help_tmpl.coffee +++ b/assets/javascripts/templates/pages/help_tmpl.coffee @@ -45,7 +45,7 @@ app.templates.helpPage = """

Keyboard Shortcuts

-

Selection

+

Sidebar

@@ -61,8 +61,11 @@ app.templates.helpPage = """
#{ctrlKey} + enter
Open selection in a new tab +
+ alt + r +
Reveal current page in sidebar
-

Navigation

+

Browsing

#{navKey} + ← @@ -83,15 +86,24 @@ app.templates.helpPage = """ #{ctrlKey} + ↑ #{ctrlKey} + ↓
Scroll to the top/bottom -
-

Misc

-
alt + f
Focus first link in the content area
(press tab to focus the other links) +
+

App

+
- alt + r -
Reveal current page in sidebar + ctrl + , +
Open preferences +
+ escape +
Reset UI +
+ ? +
Show this page +
+

Miscellaneous

+
alt + o
Open original page @@ -101,12 +113,6 @@ app.templates.helpPage = """
alt + s
Search on Stack Overflow -
- escape -
Reset
(press twice in single doc mode) -
- ? -
Show this page

Tip: If the cursor is no longer in the search field, press / or diff --git a/assets/javascripts/views/layout/document.coffee b/assets/javascripts/views/layout/document.coffee index 6cc507dc..8a541361 100644 --- a/assets/javascripts/views/layout/document.coffee +++ b/assets/javascripts/views/layout/document.coffee @@ -5,10 +5,11 @@ class app.views.Document extends app.View visibilitychange: 'onVisibilityChange' @shortcuts: - help: 'onHelp' - escape: 'onEscape' - superLeft: 'onBack' - superRight: 'onForward' + help: 'onHelp' + preferences: 'onPreferences' + escape: 'onEscape' + superLeft: 'onBack' + superRight: 'onForward' @routes: after: 'afterRoute' @@ -46,6 +47,11 @@ class app.views.Document extends app.View onHelp: -> app.router.show '/help#shortcuts' + return + + onPreferences: -> + app.router.show '/settings' + return onEscape: -> path = if !app.isSingleDoc() or location.pathname is app.doc.fullPath() @@ -54,12 +60,15 @@ class app.views.Document extends app.View app.doc.fullPath() app.router.show(path) + return onBack: -> history.back() + return onForward: -> history.forward() + return onClick: (event) -> return unless event.target.hasAttribute('data-behavior') From bde3a70480f999dba493caf3f13e1e5371bb9a6f Mon Sep 17 00:00:00 2001 From: Rhys Powell Date: Thu, 9 Feb 2017 16:14:00 +1100 Subject: [PATCH 477/825] Added mask icon for Safari This is used for pinned tabs and the touchbar on the new MacBook Pro --- public/images/webkit-mask-icon.svg | 4 ++++ views/index.erb | 1 + 2 files changed, 5 insertions(+) create mode 100644 public/images/webkit-mask-icon.svg diff --git a/public/images/webkit-mask-icon.svg b/public/images/webkit-mask-icon.svg new file mode 100644 index 00000000..239900ee --- /dev/null +++ b/public/images/webkit-mask-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/views/index.erb b/views/index.erb index 7e347d5d..261b5d53 100644 --- a/views/index.erb +++ b/views/index.erb @@ -28,6 +28,7 @@ + <%= javascript_tag 'application', asset_host: false %> <%= javascript_tag 'docs' %><% unless App.production? %> From 214622b41e7443486184afac5e9bdaae66a4f434 Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sun, 26 Feb 2017 14:46:45 -0500 Subject: [PATCH 478/825] Only use super + left/right for back/forward navigation on Mac Closes #529. --- assets/javascripts/app/shortcuts.coffee | 10 +++++----- assets/javascripts/lib/util.coffee | 3 --- assets/javascripts/templates/pages/help_tmpl.coffee | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/assets/javascripts/app/shortcuts.coffee b/assets/javascripts/app/shortcuts.coffee index 8ea37a24..57c96d8f 100644 --- a/assets/javascripts/app/shortcuts.coffee +++ b/assets/javascripts/app/shortcuts.coffee @@ -2,7 +2,7 @@ class app.Shortcuts $.extend @prototype, Events constructor: -> - @isWindows = $.isWindows() + @isMac = $.isMac() @start() start: -> @@ -91,14 +91,14 @@ class app.Shortcuts when 13 @trigger 'superEnter' when 37 - unless @isWindows + if @isMac @trigger 'superLeft' false when 38 @trigger 'pageTop' false when 39 - unless @isWindows + if @isMac @trigger 'superRight' false when 40 @@ -135,14 +135,14 @@ class app.Shortcuts when 9 @trigger 'altRight', event when 37 - if @isWindows + unless @isMac @trigger 'superLeft' false when 38 @trigger 'altUp' false when 39 - if @isWindows + unless @isMac @trigger 'superRight' false when 40 diff --git a/assets/javascripts/lib/util.coffee b/assets/javascripts/lib/util.coffee index dae644c3..aba9f038 100644 --- a/assets/javascripts/lib/util.coffee +++ b/assets/javascripts/lib/util.coffee @@ -336,9 +336,6 @@ $.popup = (value) -> window.open value.href or value, '_blank' return -$.isWindows = -> - navigator.platform?.indexOf('Win') >= 0 - $.isMac = -> navigator.userAgent?.indexOf('Mac') >= 0 diff --git a/assets/javascripts/templates/pages/help_tmpl.coffee b/assets/javascripts/templates/pages/help_tmpl.coffee index c6489c1d..51831d63 100644 --- a/assets/javascripts/templates/pages/help_tmpl.coffee +++ b/assets/javascripts/templates/pages/help_tmpl.coffee @@ -1,5 +1,5 @@ ctrlKey = if $.isMac() then 'cmd' else 'ctrl' -navKey = if $.isWindows() then 'alt' else ctrlKey +navKey = if $.isMac() then 'cmd' else 'alt' app.templates.helpPage = """

@@ -15,14 +15,13 @@ app.templates.helpPage = """

- The search is case-insensitive and supports fuzzy matching (for queries longer than two characters). - For example, searching bgcp brings up background-clip.
- Abbreviations are also supported (full list below). - For example, $ is an alias for jQuery. + The search is case-insensitive. It supports fuzzy matching + (e.g. bgcp matches background-clip) + and aliases (full list below).

- You can scope the search to a single documentation by typing its name (or an abbreviation), + You can scope the search to a single documentation by typing its name (or an abbreviation) and pressing Tab (Space on mobile devices). For example, to search the JavaScript documentation, enter javascript or js, then Tab.
@@ -30,16 +29,16 @@ app.templates.helpPage = """
The search field can be prefilled from the URL by visiting devdocs.io/#q=keyword. - Characters after #q= will be used as search string.
+ Characters after #q= will be used as search query.
To search a single documentation, add its name and a space before the keyword: devdocs.io/#q=js date.
- DevDocs supports OpenSearch, meaning it can easily be installed as a search engine on most web browsers. + DevDocs supports OpenSearch. It can easily be installed as a search engine on most web browsers:
  • On Chrome, the setup is done automatically. Simply press Tab when devdocs.io is autocompleted in the omnibox (to set a custom keyword, click Manage search engines\u2026 in Chrome's settings). -
  • On Firefox, open the search engine list (icon in the search bar) and select Add "DevDocs Search". +
  • On Firefox, open the search engine list (icon in the search bar) and click Add "DevDocs Search". DevDocs is now available in the search bar. You can also search from the location bar by following these instructions.
@@ -114,16 +113,16 @@ app.templates.helpPage = """ alt + s
Search on Stack Overflow -

+

Tip: If the cursor is no longer in the search field, press / or continue to type and it will refocus the search field and start showing new results. -

Abbreviations

-

Feel free to suggest new abbreviations on GitHub. - +

Search Aliases

+
Word Alias - #{("
#{key}#{value}" for key, value of app.models.Entry.ALIASES).join('')} + #{("
#{key}#{value}" for key, value of app.models.Entry.ALIASES).join('')}
+

Feel free to suggest new aliases on GitHub. """ diff --git a/assets/stylesheets/components/_content.scss b/assets/stylesheets/components/_content.scss index b5744001..341be1a1 100644 --- a/assets/stylesheets/components/_content.scss +++ b/assets/stylesheets/components/_content.scss @@ -357,12 +357,6 @@ @extend %label; } -// -// Abbreviations -// - -._abbreviations td { @extend %code; } - // // Utilities // @@ -370,6 +364,7 @@ ._note { @extend %note; } ._note-green { @extend %note-green; } ._label { @extend %label; } +._code { @extend %code; } ._highlight { background: $highlightBackground !important; } ._pre-clip { From aa312b953a894ca5c82854d16e71458b9e67652e Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sun, 26 Feb 2017 18:19:47 -0500 Subject: [PATCH 488/825] Update D3.js documentation (4.6.0, 3.5.17) --- lib/docs/filters/d3/clean_html.rb | 2 +- lib/docs/scrapers/d3.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/docs/filters/d3/clean_html.rb b/lib/docs/filters/d3/clean_html.rb index afba6cb3..ff009c64 100644 --- a/lib/docs/filters/d3/clean_html.rb +++ b/lib/docs/filters/d3/clean_html.rb @@ -16,7 +16,7 @@ module Docs parent = node.parent parent.name = 'h6' parent['id'] = (node['name'] || node['href'].remove(/\A.+#/)).remove('user-content-') - parent.css('a[name], a:contains("#")').remove + parent.css('a[name], a:contains("#"), a:contains("†")').remove node.remove end diff --git a/lib/docs/scrapers/d3.rb b/lib/docs/scrapers/d3.rb index 57f516a6..65e4337c 100644 --- a/lib/docs/scrapers/d3.rb +++ b/lib/docs/scrapers/d3.rb @@ -16,7 +16,7 @@ module Docs HTML version '4' do - self.release = '4.5.0' + self.release = '4.6.0' self.base_url = 'https://github.com/d3/' self.root_path = 'd3/blob/master/API.md' From fd46dc5d18412e3d08270bee704ccaccace3db21 Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sun, 26 Feb 2017 18:20:36 -0500 Subject: [PATCH 489/825] Update Async documentation (2.1.5) --- assets/javascripts/templates/pages/about_tmpl.coffee | 2 +- lib/docs/scrapers/async.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/javascripts/templates/pages/about_tmpl.coffee b/assets/javascripts/templates/pages/about_tmpl.coffee index 9d664124..f489d492 100644 --- a/assets/javascripts/templates/pages/about_tmpl.coffee +++ b/assets/javascripts/templates/pages/about_tmpl.coffee @@ -101,7 +101,7 @@ credits = [ 'https://www.apache.org/licenses/LICENSE-2.0' ], [ 'Async', - '2010-2016 Caolan McMahon', + '2010-2017 Caolan McMahon', 'MIT', 'https://raw.githubusercontent.com/caolan/async/master/LICENSE' ], [ diff --git a/lib/docs/scrapers/async.rb b/lib/docs/scrapers/async.rb index 896fb4cf..c33d3a77 100644 --- a/lib/docs/scrapers/async.rb +++ b/lib/docs/scrapers/async.rb @@ -1,7 +1,7 @@ module Docs class Async < UrlScraper self.type = 'async' - self.release = '2.1.4' + self.release = '2.1.5' self.base_url = 'https://caolan.github.io/async/' self.root_path = 'docs.html' self.links = { @@ -14,7 +14,7 @@ module Docs options[:skip_links] = true options[:attribution] = <<-HTML - © 2010–2016 Caolan McMahon
+ © 2010–2017 Caolan McMahon
Licensed under the MIT License. HTML end From 38540115fc889515408e2650411501be66bcd99a Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sun, 26 Feb 2017 18:21:46 -0500 Subject: [PATCH 490/825] Update RequireJS documentation (2.3.3) --- lib/docs/scrapers/requirejs.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/docs/scrapers/requirejs.rb b/lib/docs/scrapers/requirejs.rb index edc29288..ae4f478f 100644 --- a/lib/docs/scrapers/requirejs.rb +++ b/lib/docs/scrapers/requirejs.rb @@ -2,7 +2,7 @@ module Docs class Requirejs < UrlScraper self.name = 'RequireJS' self.type = 'requirejs' - self.release = '2.3.1' + self.release = '2.3.3' self.base_url = 'http://requirejs.org/docs/' self.links = { home: 'http://requirejs.org/', From 909fd1cd1c35a94ad303f7640ef1a2e53782b771 Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sun, 26 Feb 2017 18:28:18 -0500 Subject: [PATCH 491/825] Update Crystal documentation (0.21.0) --- assets/javascripts/templates/pages/about_tmpl.coffee | 2 +- lib/docs/scrapers/crystal.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/javascripts/templates/pages/about_tmpl.coffee b/assets/javascripts/templates/pages/about_tmpl.coffee index f489d492..9fc9bd52 100644 --- a/assets/javascripts/templates/pages/about_tmpl.coffee +++ b/assets/javascripts/templates/pages/about_tmpl.coffee @@ -186,7 +186,7 @@ credits = [ 'https://creativecommons.org/licenses/by-sa/2.5/' ], [ 'Crystal', - '2012-2016 Manas Technology Solutions', + '2012-2017 Manas Technology Solutions', 'Apache', 'https://raw.githubusercontent.com/crystal-lang/crystal/master/LICENSE' ], [ diff --git a/lib/docs/scrapers/crystal.rb b/lib/docs/scrapers/crystal.rb index 1cfac14f..03a89b99 100644 --- a/lib/docs/scrapers/crystal.rb +++ b/lib/docs/scrapers/crystal.rb @@ -1,7 +1,7 @@ module Docs class Crystal < UrlScraper self.type = 'crystal' - self.release = '0.20.3' + self.release = '0.21.0' self.base_url = 'https://crystal-lang.org/' self.root_path = "api/#{release}/index.html" self.initial_paths = %w(docs/index.html) @@ -29,7 +29,7 @@ module Docs HTML else <<-HTML - © 2012–2016 Manas Technology Solutions.
+ © 2012–2017 Manas Technology Solutions.
Licensed under the Apache License, Version 2.0. HTML end From 105ac36000121a46212de36d4de976c49fc6d906 Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sun, 26 Feb 2017 18:29:54 -0500 Subject: [PATCH 492/825] Update Marionette.js documentation (3.2.0, 2.4.7) --- assets/javascripts/templates/pages/about_tmpl.coffee | 2 +- lib/docs/scrapers/marionette.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/javascripts/templates/pages/about_tmpl.coffee b/assets/javascripts/templates/pages/about_tmpl.coffee index 9fc9bd52..1a0919cb 100644 --- a/assets/javascripts/templates/pages/about_tmpl.coffee +++ b/assets/javascripts/templates/pages/about_tmpl.coffee @@ -336,7 +336,7 @@ credits = [ 'http://www.gnu.org/copyleft/fdl.html' ], [ 'Marionette.js', - '2016 Muted Solutions, LLC', + '2017 Muted Solutions, LLC', 'MIT', 'https://mutedsolutions.mit-license.org/' ], [ diff --git a/lib/docs/scrapers/marionette.rb b/lib/docs/scrapers/marionette.rb index 2145c340..62d3cef2 100644 --- a/lib/docs/scrapers/marionette.rb +++ b/lib/docs/scrapers/marionette.rb @@ -14,12 +14,12 @@ module Docs options[:container] = '.docs__content' options[:attribution] = <<-HTML - © 2016 Muted Solutions, LLC
+ © 2017 Muted Solutions, LLC
Licensed under the MIT License. HTML version '3' do - self.release = '3.1.0' + self.release = '3.2.0' self.base_url = "https://marionettejs.com/docs/v#{release}/" html_filters.push 'marionette/entries_v3' From 3920b44059a37a47f0ea03a48fb0b508b8e3afd6 Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sun, 26 Feb 2017 18:38:15 -0500 Subject: [PATCH 493/825] Update Vue.js documentation (2.2.1, 1.0.28) --- assets/javascripts/templates/pages/about_tmpl.coffee | 2 +- lib/docs/filters/vue/clean_html.rb | 1 + lib/docs/filters/vue/entries.rb | 6 +++++- lib/docs/scrapers/vue.rb | 6 +++--- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/assets/javascripts/templates/pages/about_tmpl.coffee b/assets/javascripts/templates/pages/about_tmpl.coffee index 1a0919cb..531201e8 100644 --- a/assets/javascripts/templates/pages/about_tmpl.coffee +++ b/assets/javascripts/templates/pages/about_tmpl.coffee @@ -586,7 +586,7 @@ credits = [ 'https://raw.githubusercontent.com/mitchellh/vagrant/master/LICENSE' ], [ 'Vue.js', - '2013-2016 Evan You, Vue.js contributors', + '2013-2017 Evan You, Vue.js contributors', 'MIT', 'https://raw.githubusercontent.com/vuejs/vue/master/LICENSE' ], [ diff --git a/lib/docs/filters/vue/clean_html.rb b/lib/docs/filters/vue/clean_html.rb index 30910df7..84c5020f 100644 --- a/lib/docs/filters/vue/clean_html.rb +++ b/lib/docs/filters/vue/clean_html.rb @@ -5,6 +5,7 @@ module Docs @doc = at_css('.content') at_css('h1').content = 'Vue.js' if root_page? + doc.child.before('

Vue.js API

') if slug == 'api/' css('.demo', '.guide-links', '.footer', '#ad').remove diff --git a/lib/docs/filters/vue/entries.rb b/lib/docs/filters/vue/entries.rb index 95002faa..012a2d52 100644 --- a/lib/docs/filters/vue/entries.rb +++ b/lib/docs/filters/vue/entries.rb @@ -2,7 +2,11 @@ module Docs class Vue class EntriesFilter < Docs::EntriesFilter def get_name - at_css('h1').try(:content).presence || 'API' + if slug == 'api/' + 'API' + else + at_css('h1').content + end end def get_type diff --git a/lib/docs/scrapers/vue.rb b/lib/docs/scrapers/vue.rb index ebbde63d..b58ddc67 100644 --- a/lib/docs/scrapers/vue.rb +++ b/lib/docs/scrapers/vue.rb @@ -13,15 +13,15 @@ module Docs options[:only_patterns] = [/guide\//, /api\//] options[:attribution] = <<-HTML - © 2013–2016 Evan You, Vue.js contributors
+ © 2013–2017 Evan You, Vue.js contributors
Licensed under the MIT License. HTML version '2' do - self.release = '2.1.8' + self.release = '2.2.1' self.base_url = 'https://vuejs.org/v2/' self.root_path = 'guide/index.html' - self.initial_paths = %w(api/index.html) + self.initial_paths = %w(api/) end version '1' do From 03403bde6c4d6c02825d26b58d6d8b78f666c3a4 Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sun, 26 Feb 2017 18:46:24 -0500 Subject: [PATCH 494/825] Update Node.js documentation (7.6.0, 6.10.0, 4.8.0) --- lib/docs/filters/node/entries.rb | 15 ++++++++------- lib/docs/scrapers/node.rb | 6 +++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/docs/filters/node/entries.rb b/lib/docs/filters/node/entries.rb index de0f22f6..fca49f45 100644 --- a/lib/docs/filters/node/entries.rb +++ b/lib/docs/filters/node/entries.rb @@ -7,13 +7,14 @@ module Docs 'modules' => 'module' } REPLACE_TYPES = { - 'Addons' => 'Miscellaneous', - 'Debugger' => 'Miscellaneous', - 'os' => 'OS', - 'StringDecoder' => 'String Decoder', - 'TLS (SSL)' => 'TLS/SSL', - 'UDP / Datagram Sockets' => 'UDP/Datagram', - 'Executing JavaScript' => 'VM' } + 'Addons' => 'Miscellaneous', + 'Debugger' => 'Miscellaneous', + 'os' => 'OS', + 'StringDecoder' => 'String Decoder', + 'TLS (SSL)' => 'TLS/SSL', + 'UDP / Datagram Sockets' => 'UDP/Datagram', + 'VM (Executing JavaScript)' => 'VM', + 'Executing JavaScript' => 'VM' } def get_name REPLACE_NAMES[slug] || slug diff --git a/lib/docs/scrapers/node.rb b/lib/docs/scrapers/node.rb index f9fc544e..f7b9a899 100644 --- a/lib/docs/scrapers/node.rb +++ b/lib/docs/scrapers/node.rb @@ -23,17 +23,17 @@ module Docs HTML version do - self.release = '7.5.0' + self.release = '7.6.0' self.base_url = 'https://nodejs.org/dist/latest-v7.x/docs/api/' end version '6 LTS' do - self.release = '6.9.5' + self.release = '6.10.0' self.base_url = 'https://nodejs.org/dist/latest-v6.x/docs/api/' end version '4 LTS' do - self.release = '4.7.3' + self.release = '4.8.0' self.base_url = 'https://nodejs.org/dist/latest-v4.x/docs/api/' end end From 57a995d2b2d74406fc299e6c3c413048bbd4857a Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sun, 26 Feb 2017 18:47:32 -0500 Subject: [PATCH 495/825] Improve escape key behavior in single doc mode --- assets/javascripts/views/sidebar/doc_list.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/javascripts/views/sidebar/doc_list.coffee b/assets/javascripts/views/sidebar/doc_list.coffee index 19820d7f..7a896bbb 100644 --- a/assets/javascripts/views/sidebar/doc_list.coffee +++ b/assets/javascripts/views/sidebar/doc_list.coffee @@ -84,7 +84,7 @@ class app.views.DocList extends app.View @listSelect.deselect() @listFocus?.blur() @listFold.reset() - @revealCurrent() if options.revealCurrent + @revealCurrent() if options.revealCurrent || app.isSingleDoc() return onOpen: (event) => From c097cdecd293bbe4aeb7d3e57743c3ccec4e96e1 Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sun, 26 Feb 2017 19:06:08 -0500 Subject: [PATCH 496/825] Update changelog --- assets/javascripts/news.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/assets/javascripts/news.json b/assets/javascripts/news.json index 02cc7605..29c5b058 100644 --- a/assets/javascripts/news.json +++ b/assets/javascripts/news.json @@ -1,5 +1,9 @@ [ [ + "2017-02-26", + "Refreshed design.", + "Added Preferences." + ], [ "2017-01-22", "New HTTP documentation (thanks Mozilla)" ], [ From 639ca70819f6a561d760c5d24a675803b8a451ad Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sat, 4 Mar 2017 09:55:02 -0500 Subject: [PATCH 497/825] Fix menu overflow #592. --- assets/stylesheets/components/_header.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/assets/stylesheets/components/_header.scss b/assets/stylesheets/components/_header.scss index 5dc13256..26218db4 100644 --- a/assets/stylesheets/components/_header.scss +++ b/assets/stylesheets/components/_header.scss @@ -62,8 +62,11 @@ z-index: 1; top: .25rem; right: .25rem; - width: 8rem; + width: 8.5rem; height: calc(13.75rem + 1px); + white-space: nowrap; + word-wrap: normal; + overflow-wrap: normal; font-size: .875rem; background: $contentBackground; border: 1px solid $headerBorder; From 7980abde69457641d034618fbb9a8341f55331d3 Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sat, 4 Mar 2017 10:20:44 -0500 Subject: [PATCH 498/825] Fix sidebar not resizable when set to show/hide automatically Fixes #592. --- assets/javascripts/views/sidebar/sidebar.coffee | 4 ++-- assets/stylesheets/components/_sidebar.scss | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/assets/javascripts/views/sidebar/sidebar.coffee b/assets/javascripts/views/sidebar/sidebar.coffee index 50d88dec..b2d76464 100644 --- a/assets/javascripts/views/sidebar/sidebar.coffee +++ b/assets/javascripts/views/sidebar/sidebar.coffee @@ -27,11 +27,11 @@ class app.views.Sidebar extends app.View return display: -> - @el.style.display = 'block' + @addClass 'show' return resetDisplay: -> - @el.style.display = '' unless @el.style.display is 'none' + @removeClass 'show' return showView: (view) -> diff --git a/assets/stylesheets/components/_sidebar.scss b/assets/stylesheets/components/_sidebar.scss index 5fa619a1..df847886 100644 --- a/assets/stylesheets/components/_sidebar.scss +++ b/assets/stylesheets/components/_sidebar.scss @@ -40,9 +40,8 @@ } } - ._sidebar-hidden & { - display: none; - } + ._sidebar-hidden & { display: none; } + &.show { display: block; } } ._resizer { @@ -56,6 +55,7 @@ cursor: col-resize; ._sidebar-hidden & { display: none; } + ._sidebar-hidden ._sidebar.show ~ & { display: block; } } // From c01782a02ec51e131226799b23bd279ec729cc49 Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sat, 4 Mar 2017 10:21:15 -0500 Subject: [PATCH 499/825] Improve automatic show/hide of sidebar --- assets/javascripts/views/sidebar/sidebar.coffee | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/assets/javascripts/views/sidebar/sidebar.coffee b/assets/javascripts/views/sidebar/sidebar.coffee index b2d76464..de74261f 100644 --- a/assets/javascripts/views/sidebar/sidebar.coffee +++ b/assets/javascripts/views/sidebar/sidebar.coffee @@ -6,6 +6,9 @@ class app.views.Sidebar extends app.View select: 'onSelect' click: 'onClick' + @routes: + after: 'afterRoute' + @shortcuts: altR: 'onAltR' escape: 'onEscape' @@ -133,3 +136,7 @@ class app.views.Sidebar extends app.View @docList.onEnabled() @reset() return + + afterRoute: => + @resetDisplay() + return From b36f3f8095e297e6196bb1e7268af1864312d248 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Wed, 18 Jan 2017 11:43:52 +0100 Subject: [PATCH 500/825] core/doc: make sure name is usable as slug. --- lib/docs/core/doc.rb | 5 ++++- test/lib/docs/core/doc_test.rb | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/docs/core/doc.rb b/lib/docs/core/doc.rb index de826959..5baa9973 100644 --- a/lib/docs/core/doc.rb +++ b/lib/docs/core/doc.rb @@ -48,7 +48,10 @@ module Docs end def slug - slug = @slug || name.try(:downcase) + slug = @slug || ( + raise "Slug must be set explicitly when name (#{name}) consists of anything else than [\\w\\.%]" if /[^\w\.%]/ =~ name + name.try(:downcase) + ) version? ? "#{slug}~#{version_slug}" : slug end diff --git a/test/lib/docs/core/doc_test.rb b/test/lib/docs/core/doc_test.rb index 8c3937e2..ebf44664 100644 --- a/test/lib/docs/core/doc_test.rb +++ b/test/lib/docs/core/doc_test.rb @@ -55,6 +55,18 @@ class DocsDocTest < MiniTest::Spec doc.version = '42' assert_equal 'foo~42', doc.slug end + + it "returns 'foobar' when #name has been set to 'FooBar'" do + doc.name = 'FooBar' + assert_equal 'foobar', doc.slug + end + + it "raises error when #name has unsluggable characters" do + assert_raises do + doc.name = 'Foo-Bar' + doc.slug + end + end end describe ".slug=" do From c1ebb7a0b963cc547521743a35a97b595d9321bd Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sat, 4 Mar 2017 10:58:05 -0500 Subject: [PATCH 501/825] Improve Doc#name and Doc#slug --- lib/docs/core/doc.rb | 12 +++++++----- test/lib/docs/core/manifest_test.rb | 4 +++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/docs/core/doc.rb b/lib/docs/core/doc.rb index 5baa9973..73323fec 100644 --- a/lib/docs/core/doc.rb +++ b/lib/docs/core/doc.rb @@ -44,14 +44,11 @@ module Docs end def name - @name || super.try(:demodulize) + @name || super.demodulize end def slug - slug = @slug || ( - raise "Slug must be set explicitly when name (#{name}) consists of anything else than [\\w\\.%]" if /[^\w\.%]/ =~ name - name.try(:downcase) - ) + slug = @slug || default_slug || raise('slug is required') version? ? "#{slug}~#{version_slug}" : slug end @@ -119,6 +116,11 @@ module Docs private + def default_slug + return if name =~ /[^A-Za-z0-9_]/ + name.downcase + end + def store_page?(page) page[:entries].present? end diff --git a/test/lib/docs/core/manifest_test.rb b/test/lib/docs/core/manifest_test.rb index 1138b9c6..ccf8d247 100644 --- a/test/lib/docs/core/manifest_test.rb +++ b/test/lib/docs/core/manifest_test.rb @@ -3,7 +3,9 @@ require 'docs' class ManifestTest < MiniTest::Spec let :doc do - Class.new Docs::Doc + doc = Class.new Docs::Doc + doc.name = 'TestDoc' + doc end let :store do From 2db57fdf8245bc41251cadfbd3a6f8529010741e Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sat, 4 Mar 2017 11:30:42 -0500 Subject: [PATCH 502/825] Bump Raven.js --- assets/javascripts/vendor/raven.js | 958 +++++++++++++++++------------ 1 file changed, 557 insertions(+), 401 deletions(-) diff --git a/assets/javascripts/vendor/raven.js b/assets/javascripts/vendor/raven.js index 6ffb870c..8e956570 100644 --- a/assets/javascripts/vendor/raven.js +++ b/assets/javascripts/vendor/raven.js @@ -1,10 +1,10 @@ -/*! Raven.js 3.5.1 (bef9fa7) | github.com/getsentry/raven-js */ +/*! Raven.js 3.11.0 (cb87941) | github.com/getsentry/raven-js */ /* * Includes TraceKit * https://github.com/getsentry/TraceKit * - * Copyright 2016 Matt Robenolt and other contributors + * Copyright 2017 Matt Robenolt and other contributors * Released under the BSD license * https://github.com/getsentry/raven-js/blob/master/LICENSE * @@ -91,30 +91,14 @@ module.exports = { }; },{}],4:[function(_dereq_,module,exports){ -/*global XDomainRequest:false*/ +(function (global){ +/*global XDomainRequest:false, __DEV__:false*/ 'use strict'; -var TraceKit = _dereq_(7); +var TraceKit = _dereq_(6); var RavenConfigError = _dereq_(2); -var utils = _dereq_(6); var stringify = _dereq_(1); -var isFunction = utils.isFunction; -var isUndefined = utils.isUndefined; -var isError = utils.isError; -var isEmptyObject = utils.isEmptyObject; -var hasKey = utils.hasKey; -var joinRegExp = utils.joinRegExp; -var each = utils.each; -var objectMerge = utils.objectMerge; -var truncate = utils.truncate; -var urlencode = utils.urlencode; -var uuid4 = utils.uuid4; -var htmlTreeAsString = utils.htmlTreeAsString; -var parseUrl = utils.parseUrl; -var isString = utils.isString; -var fill = utils.fill; - var wrapConsoleMethod = _dereq_(3).wrapMethod; var dsnKeys = 'source protocol user pass host port path'.split(' '), @@ -124,6 +108,13 @@ function now() { return +new Date(); } +// This is to be defensive in environments where window does not exist (see https://github.com/getsentry/raven-js/pull/785) +var _window = typeof window !== 'undefined' ? window + : typeof global !== 'undefined' ? global + : typeof self !== 'undefined' ? self + : {}; +var _document = _window.document; +var _navigator = _window.navigator; // First, check for JSON support // If there is no JSON, we no-op the core features of Raven @@ -131,8 +122,10 @@ function now() { function Raven() { this._hasJSON = !!(typeof JSON === 'object' && JSON.stringify); // Raven can run in contexts where there's no document (react-native) - this._hasDocument = typeof document !== 'undefined'; + this._hasDocument = !isUndefined(_document); + this._hasNavigator = !isUndefined(_navigator); this._lastCapturedException = null; + this._lastData = null; this._lastEventId = null; this._globalServer = null; this._globalKey = null; @@ -155,7 +148,7 @@ function Raven() { this._originalErrorStackTraceLimit = Error.stackTraceLimit; // capture references to window.console *and* all its methods first // before the console plugin has a chance to monkey patch - this._originalConsole = window.console || {}; + this._originalConsole = _window.console || {}; this._originalConsoleMethods = {}; this._plugins = []; this._startTime = now(); @@ -163,8 +156,9 @@ function Raven() { this._breadcrumbs = []; this._lastCapturedEvent = null; this._keypressTimeout; - this._location = window.location; + this._location = _window.location; this._lastHref = this._location && this._location.href; + this._resetBackoff(); for (var method in this._originalConsole) { // eslint-disable-line guard-for-in this._originalConsoleMethods[method] = this._originalConsole[method]; @@ -182,7 +176,7 @@ Raven.prototype = { // webpack (using a build step causes webpack #1617). Grunt verifies that // this value matches package.json during build. // See: https://github.com/getsentry/raven-js/issues/465 - VERSION: '3.5.1', + VERSION: '3.11.0', debug: false, @@ -198,41 +192,39 @@ Raven.prototype = { config: function(dsn, options) { var self = this; - if (this._globalServer) { + if (self._globalServer) { this._logDebug('error', 'Error: Raven has already been configured'); - return this; + return self; } - if (!dsn) return this; + if (!dsn) return self; + + var globalOptions = self._globalOptions; // merge in options if (options) { each(options, function(key, value){ // tags and extra are special and need to be put into context - if (key === 'tags' || key === 'extra') { + if (key === 'tags' || key === 'extra' || key === 'user') { self._globalContext[key] = value; } else { - self._globalOptions[key] = value; + globalOptions[key] = value; } }); } - var uri = this._parseDSN(dsn), - lastSlash = uri.path.lastIndexOf('/'), - path = uri.path.substr(1, lastSlash); - - this._dsn = dsn; + self.setDSN(dsn); // "Script error." is hard coded into browsers for errors that it can't read. // this is the result of a script being pulled in from an external domain and CORS. - this._globalOptions.ignoreErrors.push(/^Script error\.?$/); - this._globalOptions.ignoreErrors.push(/^Javascript error: Script error\.? on line 0$/); + globalOptions.ignoreErrors.push(/^Script error\.?$/); + globalOptions.ignoreErrors.push(/^Javascript error: Script error\.? on line 0$/); // join regexp rules into one big rule - this._globalOptions.ignoreErrors = joinRegExp(this._globalOptions.ignoreErrors); - this._globalOptions.ignoreUrls = this._globalOptions.ignoreUrls.length ? joinRegExp(this._globalOptions.ignoreUrls) : false; - this._globalOptions.whitelistUrls = this._globalOptions.whitelistUrls.length ? joinRegExp(this._globalOptions.whitelistUrls) : false; - this._globalOptions.includePaths = joinRegExp(this._globalOptions.includePaths); - this._globalOptions.maxBreadcrumbs = Math.max(0, Math.min(this._globalOptions.maxBreadcrumbs || 100, 100)); // default and hard limit is 100 + globalOptions.ignoreErrors = joinRegExp(globalOptions.ignoreErrors); + globalOptions.ignoreUrls = globalOptions.ignoreUrls.length ? joinRegExp(globalOptions.ignoreUrls) : false; + globalOptions.whitelistUrls = globalOptions.whitelistUrls.length ? joinRegExp(globalOptions.whitelistUrls) : false; + globalOptions.includePaths = joinRegExp(globalOptions.includePaths); + globalOptions.maxBreadcrumbs = Math.max(0, Math.min(globalOptions.maxBreadcrumbs || 100, 100)); // default and hard limit is 100 var autoBreadcrumbDefaults = { xhr: true, @@ -241,27 +233,18 @@ Raven.prototype = { location: true }; - var autoBreadcrumbs = this._globalOptions.autoBreadcrumbs; + var autoBreadcrumbs = globalOptions.autoBreadcrumbs; if ({}.toString.call(autoBreadcrumbs) === '[object Object]') { autoBreadcrumbs = objectMerge(autoBreadcrumbDefaults, autoBreadcrumbs); } else if (autoBreadcrumbs !== false) { autoBreadcrumbs = autoBreadcrumbDefaults; } - this._globalOptions.autoBreadcrumbs = autoBreadcrumbs; - - this._globalKey = uri.user; - this._globalSecret = uri.pass && uri.pass.substr(1); - this._globalProject = uri.path.substr(lastSlash + 1); - - this._globalServer = this._getGlobalServer(uri); + globalOptions.autoBreadcrumbs = autoBreadcrumbs; - this._globalEndpoint = this._globalServer + - '/' + path + 'api/' + this._globalProject + '/store/'; - - TraceKit.collectWindowErrors = !!this._globalOptions.collectWindowErrors; + TraceKit.collectWindowErrors = !!globalOptions.collectWindowErrors; // return for chaining - return this; + return self; }, /* @@ -274,24 +257,50 @@ Raven.prototype = { */ install: function() { var self = this; - if (this.isSetup() && !this._isRavenInstalled) { + if (self.isSetup() && !self._isRavenInstalled) { TraceKit.report.subscribe(function () { self._handleOnErrorStackInfo.apply(self, arguments); }); - this._instrumentTryCatch(); + self._instrumentTryCatch(); if (self._globalOptions.autoBreadcrumbs) - this._instrumentBreadcrumbs(); + self._instrumentBreadcrumbs(); // Install all of the plugins - this._drainPlugins(); + self._drainPlugins(); - this._isRavenInstalled = true; + self._isRavenInstalled = true; } - Error.stackTraceLimit = this._globalOptions.stackTraceLimit; + Error.stackTraceLimit = self._globalOptions.stackTraceLimit; return this; }, + /* + * Set the DSN (can be called multiple time unlike config) + * + * @param {string} dsn The public Sentry DSN + */ + setDSN: function(dsn) { + var self = this, + uri = self._parseDSN(dsn), + lastSlash = uri.path.lastIndexOf('/'), + path = uri.path.substr(1, lastSlash); + + self._dsn = dsn; + self._globalKey = uri.user; + self._globalSecret = uri.pass && uri.pass.substr(1); + self._globalProject = uri.path.substr(lastSlash + 1); + + self._globalServer = self._getGlobalServer(uri); + + self._globalEndpoint = self._globalServer + + '/' + path + 'api/' + self._globalProject + '/store/'; + + // Reset backoff state since we may be pointing at a + // new project/server + this._resetBackoff(); + }, + /* * Wrap code within a context so Raven can capture errors * reliably across domains that is executed immediately. @@ -343,18 +352,18 @@ Raven.prototype = { if (func.__raven__) { return func; } + + // If this has already been wrapped in the past, return that + if (func.__raven_wrapper__ ){ + return func.__raven_wrapper__ ; + } } catch (e) { - // Just accessing the __raven__ prop in some Selenium environments + // Just accessing custom props in some Selenium environments // can cause a "Permission denied" exception (see raven-js#495). // Bail on wrapping and return the function as-is (defers to window.onerror). return func; } - // If this has already been wrapped in the past, return that - if (func.__raven_wrapper__ ){ - return func.__raven_wrapper__ ; - } - function wrapped() { var args = [], i = arguments.length, deep = !options || options && options.deep !== false; @@ -368,6 +377,10 @@ Raven.prototype = { while(i--) args[i] = deep ? self.wrap(options, arguments[i]) : arguments[i]; try { + // Attempt to invoke user-land function + // NOTE: If you are a Sentry user, and you are seeing this stack frame, it + // means Raven caught an error invoking your application code. This is + // expected behavior and NOT indicative of a bug with Raven.js. return func.apply(this, args); } catch(e) { self._ignoreNextOnError(); @@ -418,7 +431,12 @@ Raven.prototype = { */ captureException: function(ex, options) { // If not an Error is passed through, recall as a message instead - if (!isError(ex)) return this.captureMessage(ex, options); + if (!isError(ex)) { + return this.captureMessage(ex, objectMerge({ + trimHeadFrames: 1, + stacktrace: true // if we fall back to captureMessage, default to attempting a new trace + }, options)); + } // Store the raw exception object for potential debugging and introspection this._lastCapturedException = ex; @@ -455,12 +473,43 @@ Raven.prototype = { return; } + options = options || {}; + + var data = objectMerge({ + message: msg + '' // Make sure it's actually a string + }, options); + + if (this._globalOptions.stacktrace || (options && options.stacktrace)) { + var ex; + // create a stack trace from this point; just trim + // off extra frames so they don't include this function call (or + // earlier Raven.js library fn calls) + try { + throw new Error(msg); + } catch (ex1) { + ex = ex1; + } + + // null exception name so `Error` isn't prefixed to msg + ex.name = null; + + options = objectMerge({ + // fingerprint on msg, not stack trace (legacy behavior, could be + // revisited) + fingerprint: msg, + trimHeadFrames: (options.trimHeadFrames || 0) + 1 + }, options); + + var stack = TraceKit.computeStackTrace(ex); + var frames = this._prepareFrames(stack, options); + data.stacktrace = { + // Sentry expects frames oldest to newest + frames: frames.reverse() + } + } + // Fire away! - this._send( - objectMerge({ - message: msg + '' // Make sure it's actually a string - }, options) - ); + this._send(data); return this; }, @@ -470,14 +519,25 @@ Raven.prototype = { timestamp: now() / 1000 }, obj); + if (isFunction(this._globalOptions.breadcrumbCallback)) { + var result = this._globalOptions.breadcrumbCallback(crumb); + + if (isObject(result) && !isEmptyObject(result)) { + crumb = result; + } else if (result === false) { + return this; + } + } + this._breadcrumbs.push(crumb); if (this._breadcrumbs.length > this._globalOptions.maxBreadcrumbs) { this._breadcrumbs.shift(); } + return this; }, addPlugin: function(plugin /*arg1, arg2, ... argN*/) { - var pluginArgs = Array.prototype.slice.call(arguments, 1); + var pluginArgs = [].slice.call(arguments, 1); this._plugins.push([plugin, pluginArgs]); if (this._isRavenInstalled) { @@ -586,6 +646,22 @@ Raven.prototype = { return this; }, + /* + * Set the breadcrumbCallback option + * + * @param {function} callback The callback to run which allows filtering + * or mutating breadcrumbs + * @return {Raven} + */ + setBreadcrumbCallback: function(callback) { + var original = this._globalOptions.breadcrumbCallback; + this._globalOptions.breadcrumbCallback = isFunction(callback) + ? function (data) { return callback(data, original); } + : callback; + + return this; + }, + /* * Set the shouldSendCallback option * @@ -656,14 +732,14 @@ Raven.prototype = { // TODO: remove window dependence? // Attempt to initialize Raven on load - var RavenConfig = window.RavenConfig; + var RavenConfig = _window.RavenConfig; if (RavenConfig) { this.config(RavenConfig.dsn, RavenConfig.config).install(); } }, showReportDialog: function (options) { - if (!window.document) // doesn't work without a document (React native) + if (!_document) // doesn't work without a document (React native) return; options = options || {}; @@ -691,10 +767,10 @@ Raven.prototype = { var globalServer = this._getGlobalServer(this._parseDSN(dsn)); - var script = document.createElement('script'); + var script = _document.createElement('script'); script.async = true; script.src = globalServer + '/api/embed/error-page/' + qs; - (document.head || document.body).appendChild(script); + (_document.head || _document.body).appendChild(script); }, /**** Private functions ****/ @@ -718,11 +794,11 @@ Raven.prototype = { eventType = 'raven' + eventType.substr(0,1).toUpperCase() + eventType.substr(1); - if (document.createEvent) { - evt = document.createEvent('HTMLEvents'); + if (_document.createEvent) { + evt = _document.createEvent('HTMLEvents'); evt.initEvent(eventType, true, true); } else { - evt = document.createEventObject(); + evt = _document.createEventObject(); evt.eventType = eventType; } @@ -730,14 +806,14 @@ Raven.prototype = { evt[key] = options[key]; } - if (document.createEvent) { + if (_document.createEvent) { // IE9 if standards - document.dispatchEvent(evt); + _document.dispatchEvent(evt); } else { // IE8 regardless of Quirks or Standards // IE9 if quirks try { - document.fireEvent('on' + evt.eventType.toLowerCase(), evt); + _document.fireEvent('on' + evt.eventType.toLowerCase(), evt); } catch(e) { // Do nothing } @@ -765,14 +841,14 @@ Raven.prototype = { return; self._lastCapturedEvent = evt; - var elem = evt.target; + // try/catch both: + // - accessing evt.target (see getsentry/raven-js#838, #768) + // - `htmlTreeAsString` because it's complex, and just accessing the DOM incorrectly + // can throw an exception in some circumstances. var target; - - // try/catch htmlTreeAsString because it's particularly complicated, and - // just accessing the DOM incorrectly can throw an exception in some circumstances. try { - target = htmlTreeAsString(elem); + target = htmlTreeAsString(evt.target); } catch (e) { target = ''; } @@ -796,15 +872,21 @@ Raven.prototype = { // TODO: if somehow user switches keypress target before // debounce timeout is triggered, we will only capture // a single breadcrumb from the FIRST target (acceptable?) - return function (evt) { - var target = evt.target, - tagName = target && target.tagName; + var target; + try { + target = evt.target; + } catch (e) { + // just accessing event properties can throw an exception in some rare circumstances + // see: https://github.com/getsentry/raven-js/issues/838 + return; + } + var tagName = target && target.tagName; // only consider keypress events on actual input elements // this will disregard keypresses targeting body (e.g. tabbing // through elements, hotkeys, etc) - if (!tagName || tagName !== 'INPUT' && tagName !== 'TEXTAREA') + if (!tagName || tagName !== 'INPUT' && tagName !== 'TEXTAREA' && !target.isContentEditable) return; // record first keypress in a series, but ignore subsequent @@ -815,7 +897,7 @@ Raven.prototype = { } clearTimeout(timeout); self._keypressTimeout = setTimeout(function () { - self._keypressTimeout = null; + self._keypressTimeout = null; }, debounceDuration); }; }, @@ -887,7 +969,7 @@ Raven.prototype = { var autoBreadcrumbs = this._globalOptions.autoBreadcrumbs; function wrapEventTarget(global) { - var proto = window[global] && window[global].prototype; + var proto = _window[global] && _window[global].prototype; if (proto && proto.hasOwnProperty && proto.hasOwnProperty('addEventListener')) { fill(proto, 'addEventListener', function(orig) { return function (evtName, fn, capture, secure) { // preserve arity @@ -901,30 +983,55 @@ Raven.prototype = { // More breadcrumb DOM capture ... done here and not in `_instrumentBreadcrumbs` // so that we don't have more than one wrapper function - var before; + var before, + clickHandler, + keypressHandler; + if (autoBreadcrumbs && autoBreadcrumbs.dom && (global === 'EventTarget' || global === 'Node')) { - if (evtName === 'click'){ - before = self._breadcrumbEventHandler(evtName); - } else if (evtName === 'keypress') { - before = self._keypressEventHandler(); - } + // NOTE: generating multiple handlers per addEventListener invocation, should + // revisit and verify we can just use one (almost certainly) + clickHandler = self._breadcrumbEventHandler('click'); + keypressHandler = self._keypressEventHandler(); + before = function (evt) { + // need to intercept every DOM event in `before` argument, in case that + // same wrapped method is re-used for different events (e.g. mousemove THEN click) + // see #724 + if (!evt) return; + + var eventType; + try { + eventType = evt.type + } catch (e) { + // just accessing event properties can throw an exception in some rare circumstances + // see: https://github.com/getsentry/raven-js/issues/838 + return; + } + if (eventType === 'click') + return clickHandler(evt); + else if (eventType === 'keypress') + return keypressHandler(evt); + }; } return orig.call(this, evtName, self.wrap(fn, undefined, before), capture, secure); }; }, wrappedBuiltIns); fill(proto, 'removeEventListener', function (orig) { return function (evt, fn, capture, secure) { - fn = fn && (fn.__raven_wrapper__ ? fn.__raven_wrapper__ : fn); + try { + fn = fn && (fn.__raven_wrapper__ ? fn.__raven_wrapper__ : fn); + } catch (e) { + // ignore, accessing __raven_wrapper__ will throw in some Selenium environments + } return orig.call(this, evt, fn, capture, secure); }; }, wrappedBuiltIns); } } - fill(window, 'setTimeout', wrapTimeFn, wrappedBuiltIns); - fill(window, 'setInterval', wrapTimeFn, wrappedBuiltIns); - if (window.requestAnimationFrame) { - fill(window, 'requestAnimationFrame', function (orig) { + fill(_window, 'setTimeout', wrapTimeFn, wrappedBuiltIns); + fill(_window, 'setInterval', wrapTimeFn, wrappedBuiltIns); + if (_window.requestAnimationFrame) { + fill(_window, 'requestAnimationFrame', function (orig) { return function (cb) { return orig(self.wrap(cb)); }; @@ -937,15 +1044,6 @@ Raven.prototype = { for (var i = 0; i < eventTargets.length; i++) { wrapEventTarget(eventTargets[i]); } - - var $ = window.jQuery || window.$; - if ($ && $.fn && $.fn.ready) { - fill($.fn, 'ready', function (orig) { - return function (fn) { - return orig.call(this, self.wrap(fn)); - }; - }, wrappedBuiltIns); - } }, @@ -972,7 +1070,7 @@ Raven.prototype = { } } - if (autoBreadcrumbs.xhr && 'XMLHttpRequest' in window) { + if (autoBreadcrumbs.xhr && 'XMLHttpRequest' in _window) { var xhrproto = XMLHttpRequest.prototype; fill(xhrproto, 'open', function(origOpen) { return function (method, url) { // preserve arity @@ -1029,17 +1127,54 @@ Raven.prototype = { }, wrappedBuiltIns); } + if (autoBreadcrumbs.xhr && 'fetch' in _window) { + fill(_window, 'fetch', function(origFetch) { + return function (fn, t) { // preserve arity + // Make a copy of the arguments to prevent deoptimization + // https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#32-leaking-arguments + var args = new Array(arguments.length); + for(var i = 0; i < args.length; ++i) { + args[i] = arguments[i]; + } + + var method = 'GET'; + + if (args[1] && args[1].method) { + method = args[1].method; + } + + var fetchData = { + method: method, + url: args[0], + status_code: null + }; + + self.captureBreadcrumb({ + type: 'http', + category: 'fetch', + data: fetchData + }); + + return origFetch.apply(this, args).then(function (response) { + fetchData.status_code = response.status; + + return response; + }); + }; + }, wrappedBuiltIns); + } + // Capture breadcrumbs from any click that is unhandled / bubbled up all the way // to the document. Do this before we instrument addEventListener. if (autoBreadcrumbs.dom && this._hasDocument) { - if (document.addEventListener) { - document.addEventListener('click', self._breadcrumbEventHandler('click'), false); - document.addEventListener('keypress', self._keypressEventHandler(), false); + if (_document.addEventListener) { + _document.addEventListener('click', self._breadcrumbEventHandler('click'), false); + _document.addEventListener('keypress', self._keypressEventHandler(), false); } else { // IE8 Compatibility - document.attachEvent('onclick', self._breadcrumbEventHandler('click')); - document.attachEvent('onkeypress', self._keypressEventHandler()); + _document.attachEvent('onclick', self._breadcrumbEventHandler('click')); + _document.attachEvent('onkeypress', self._keypressEventHandler()); } } @@ -1047,13 +1182,13 @@ Raven.prototype = { // NOTE: in Chrome App environment, touching history.pushState, *even inside // a try/catch block*, will cause Chrome to output an error to console.error // borrowed from: https://github.com/angular/angular.js/pull/13945/files - var chrome = window.chrome; + var chrome = _window.chrome; var isChromePackagedApp = chrome && chrome.app && chrome.app.runtime; - var hasPushState = !isChromePackagedApp && window.history && history.pushState; + var hasPushState = !isChromePackagedApp && _window.history && history.pushState; if (autoBreadcrumbs.location && hasPushState) { // TODO: remove onpopstate handler on uninstall() - var oldOnPopState = window.onpopstate; - window.onpopstate = function () { + var oldOnPopState = _window.onpopstate; + _window.onpopstate = function () { var currentHref = self._location.href; self._captureUrlChange(self._lastHref, currentHref); @@ -1079,7 +1214,7 @@ Raven.prototype = { }, wrappedBuiltIns); } - if (autoBreadcrumbs.console && 'console' in window && console.log) { + if (autoBreadcrumbs.console && 'console' in _window && console.log) { // console var consoleMethodCallback = function (msg, data) { self.captureBreadcrumb({ @@ -1158,17 +1293,7 @@ Raven.prototype = { }, _handleStackInfo: function(stackInfo, options) { - var self = this; - var frames = []; - - if (stackInfo.stack && stackInfo.stack.length) { - each(stackInfo.stack, function(i, stack) { - var frame = self._normalizeFrame(stack); - if (frame) { - frames.push(frame); - } - }); - } + var frames = this._prepareFrames(stackInfo, options); this._triggerEvent('handle', { stackInfo: stackInfo, @@ -1180,11 +1305,34 @@ Raven.prototype = { stackInfo.message, stackInfo.url, stackInfo.lineno, - frames.slice(0, this._globalOptions.stackTraceLimit), + frames, options ); }, + _prepareFrames: function(stackInfo, options) { + var self = this; + var frames = []; + if (stackInfo.stack && stackInfo.stack.length) { + each(stackInfo.stack, function(i, stack) { + var frame = self._normalizeFrame(stack); + if (frame) { + frames.push(frame); + } + }); + + // e.g. frames captured via captureMessage throw + if (options && options.trimHeadFrames) { + for (var j = 0; j < options.trimHeadFrames && j < frames.length; j++) { + frames[j].in_app = false; + } + } + } + frames = frames.slice(0, this._globalOptions.stackTraceLimit); + return frames; + }, + + _normalizeFrame: function(frame) { if (!frame.url) return; @@ -1210,7 +1358,6 @@ Raven.prototype = { _processException: function(type, message, fileurl, lineno, frames, options) { var stacktrace; - if (!!this._globalOptions.ignoreErrors.test && this._globalOptions.ignoreErrors.test(message)) return; message += ''; @@ -1266,25 +1413,99 @@ Raven.prototype = { }, _getHttpData: function() { - if (!this._hasDocument || !document.location || !document.location.href) { - return; + if (!this._hasNavigator && !this._hasDocument) return; + var httpData = {}; + + if (this._hasNavigator && _navigator.userAgent) { + httpData.headers = { + 'User-Agent': navigator.userAgent + }; } - var httpData = { - headers: { - 'User-Agent': navigator.userAgent + if (this._hasDocument) { + if (_document.location && _document.location.href) { + httpData.url = _document.location.href; } - }; + if (_document.referrer) { + if (!httpData.headers) httpData.headers = {}; + httpData.headers.Referer = _document.referrer; + } + } - httpData.url = document.location.href; + return httpData; + }, + + _resetBackoff: function() { + this._backoffDuration = 0; + this._backoffStart = null; + }, + + _shouldBackoff: function() { + return this._backoffDuration && now() - this._backoffStart < this._backoffDuration; + }, + + /** + * Returns true if the in-process data payload matches the signature + * of the previously-sent data + * + * NOTE: This has to be done at this level because TraceKit can generate + * data from window.onerror WITHOUT an exception object (IE8, IE9, + * other old browsers). This can take the form of an "exception" + * data object with a single frame (derived from the onerror args). + */ + _isRepeatData: function (current) { + var last = this._lastData; - if (document.referrer) { - httpData.headers.Referer = document.referrer; + if (!last || + current.message !== last.message || // defined for captureMessage + current.culprit !== last.culprit) // defined for captureException/onerror + return false; + + // Stacktrace interface (i.e. from captureMessage) + if (current.stacktrace || last.stacktrace) { + return isSameStacktrace(current.stacktrace, last.stacktrace); + } + // Exception interface (i.e. from captureException/onerror) + else if (current.exception || last.exception) { + return isSameException(current.exception, last.exception); } - return httpData; + return true; }, + _setBackoffState: function(request) { + // If we are already in a backoff state, don't change anything + if (this._shouldBackoff()) { + return; + } + + var status = request.status; + + // 400 - project_id doesn't exist or some other fatal + // 401 - invalid/revoked dsn + // 429 - too many requests + if (!(status === 400 || status === 401 || status === 429)) + return; + + var retry; + try { + // If Retry-After is not in Access-Control-Expose-Headers, most + // browsers will throw an exception trying to access it + retry = request.getResponseHeader('Retry-After'); + retry = parseInt(retry, 10) * 1000; // Retry-After is returned in seconds + } catch (e) { + /* eslint no-empty:0 */ + } + + + this._backoffDuration = retry + // If Sentry server returned a Retry-After value, use it + ? retry + // Otherwise, double the last backoff duration (starts at 1 sec) + : this._backoffDuration * 2 || 1000; + + this._backoffStart = now(); + }, _send: function(data) { var globalOptions = this._globalOptions; @@ -1299,6 +1520,9 @@ Raven.prototype = { baseData.request = httpData; } + // HACK: delete `trimHeadFrames` to prevent from appearing in outbound payload + if (data.trimHeadFrames) delete data.trimHeadFrames; + data = objectMerge(baseData, data); // Merge in the tags and extra separately since objectMerge doesn't handle a deep merge @@ -1347,24 +1571,46 @@ Raven.prototype = { return; } + // Backoff state: Sentry server previously responded w/ an error (e.g. 429 - too many requests), + // so drop requests until "cool-off" period has elapsed. + if (this._shouldBackoff()) { + this._logDebug('warn', 'Raven dropped error due to backoff: ', data); + return; + } + this._sendProcessedPayload(data); }, + _getUuid: function () { + return uuid4(); + }, + _sendProcessedPayload: function(data, callback) { var self = this; var globalOptions = this._globalOptions; + if (!this.isSetup()) return; + // Send along an event_id if not explicitly passed. // This event_id can be used to reference the error within Sentry itself. // Set lastEventId after we know the error should actually be sent - this._lastEventId = data.event_id || (data.event_id = uuid4()); + this._lastEventId = data.event_id || (data.event_id = this._getUuid()); // Try and clean up the packet before sending by truncating long values data = this._trimPacket(data); - this._logDebug('debug', 'Raven about to send:', data); + // ideally duplicate error testing should occur *before* dataCallback/shouldSendCallback, + // but this would require copying an un-truncated copy of the data packet, which can be + // arbitrarily deep (extra_data) -- could be worthwhile? will revisit + if (!this._globalOptions.allowDuplicates && this._isRepeatData(data)) { + this._logDebug('warn', 'Raven dropped repeat event: ', data); + return; + } - if (!this.isSetup()) return; + // Store outbound payload after trim + this._lastData = data; + + this._logDebug('debug', 'Raven about to send:', data); var auth = { sentry_version: '7', @@ -1392,6 +1638,8 @@ Raven.prototype = { data: data, options: globalOptions, onSuccess: function success() { + self._resetBackoff(); + self._triggerEvent('success', { data: data, src: url @@ -1399,6 +1647,12 @@ Raven.prototype = { callback && callback(); }, onError: function failure(error) { + self._logDebug('error', 'Raven transport failed to send: ', error); + + if (error.request) { + self._setBackoffState(error.request); + } + self._triggerEvent('failure', { data: data, src: url @@ -1426,7 +1680,9 @@ Raven.prototype = { opts.onSuccess(); } } else if (opts.onError) { - opts.onError(new Error('Sentry error code: ' + request.status)); + var err = new Error('Sentry error code: ' + request.status); + err.request = request; + opts.onError(err); } } @@ -1473,46 +1729,12 @@ Raven.prototype = { } }; -// Deprecations -Raven.prototype.setUser = Raven.prototype.setUserContext; -Raven.prototype.setReleaseContext = Raven.prototype.setRelease; - -module.exports = Raven; - -},{"1":1,"2":2,"3":3,"6":6,"7":7}],5:[function(_dereq_,module,exports){ -/** - * Enforces a single instance of the Raven client, and the - * main entry point for Raven. If you are a consumer of the - * Raven library, you SHOULD load this file (vs raven.js). - **/ - -'use strict'; - -var RavenConstructor = _dereq_(4); - -var _Raven = window.Raven; - -var Raven = new RavenConstructor(); - -/* - * Allow multiple versions of Raven to be installed. - * Strip Raven from the global context and returns the instance. +/*------------------------------------------------ + * utils * - * @return {Raven} + * conditionally exported for test via Raven.utils + ================================================= */ -Raven.noConflict = function () { - window.Raven = _Raven; - return Raven; -}; - -Raven.afterLoad(); - -module.exports = Raven; - -},{"4":4}],6:[function(_dereq_,module,exports){ -/*eslint no-extra-parens:0*/ -'use strict'; - var objectPrototype = Object.prototype; function isUndefined(what) { @@ -1638,7 +1860,7 @@ function parseUrl(url) { }; } function uuid4() { - var crypto = window.crypto || window.msCrypto; + var crypto = _window.crypto || _window.msCrypto; if (!isUndefined(crypto) && crypto.getRandomValues) { // Use window.crypto API if available @@ -1678,6 +1900,7 @@ function uuid4() { * @returns {string} */ function htmlTreeAsString(elem) { + /* eslint no-extra-parens:0*/ var MAX_TRAVERSE_HEIGHT = 5, MAX_OUTPUT_LEN = 80, out = [], @@ -1732,7 +1955,7 @@ function htmlElementAsString(elem) { className = elem.className; if (className && isString(className)) { - classes = className.split(' '); + classes = className.split(/\s+/); for (i = 0; i < classes.length; i++) { out.push('.' + classes[i]); } @@ -1748,6 +1971,58 @@ function htmlElementAsString(elem) { return out.join(''); } +/** + * Returns true if either a OR b is truthy, but not both + */ +function isOnlyOneTruthy(a, b) { + return !!(!!a ^ !!b); +} + +/** + * Returns true if the two input exception interfaces have the same content + */ +function isSameException(ex1, ex2) { + if (isOnlyOneTruthy(ex1, ex2)) + return false; + + ex1 = ex1.values[0]; + ex2 = ex2.values[0]; + + if (ex1.type !== ex2.type || + ex1.value !== ex2.value) + return false; + + return isSameStacktrace(ex1.stacktrace, ex2.stacktrace); +} + +/** + * Returns true if the two input stack trace interfaces have the same content + */ +function isSameStacktrace(stack1, stack2) { + if (isOnlyOneTruthy(stack1, stack2)) + return false; + + var frames1 = stack1.frames; + var frames2 = stack2.frames; + + // Exit early if frame count differs + if (frames1.length !== frames2.length) + return false; + + // Iterate through every frame; bail out if anything differs + var a, b; + for (var i = 0; i < frames1.length; i++) { + a = frames1[i]; + b = frames2[i]; + if (a.filename !== b.filename || + a.lineno !== b.lineno || + a.colno !== b.colno || + a['function'] !== b['function']) + return false; + } + return true; +} + /** * Polyfill a method * @param obj object e.g. `document` @@ -1763,37 +2038,83 @@ function fill(obj, name, replacement, track) { } } -module.exports = { - isUndefined: isUndefined, - isFunction: isFunction, - isString: isString, - isObject: isObject, - isEmptyObject: isEmptyObject, - isError: isError, - each: each, - objectMerge: objectMerge, - truncate: truncate, - hasKey: hasKey, - joinRegExp: joinRegExp, - urlencode: urlencode, - uuid4: uuid4, - htmlTreeAsString: htmlTreeAsString, - htmlElementAsString: htmlElementAsString, - parseUrl: parseUrl, - fill: fill +if (typeof __DEV__ !== 'undefined' && __DEV__) { + Raven.utils = { + isUndefined: isUndefined, + isFunction: isFunction, + isString: isString, + isObject: isObject, + isEmptyObject: isEmptyObject, + isError: isError, + each: each, + objectMerge: objectMerge, + truncate: truncate, + hasKey: hasKey, + joinRegExp: joinRegExp, + urlencode: urlencode, + uuid4: uuid4, + htmlTreeAsString: htmlTreeAsString, + htmlElementAsString: htmlElementAsString, + parseUrl: parseUrl, + fill: fill + }; }; -},{}],7:[function(_dereq_,module,exports){ +// Deprecations +Raven.prototype.setUser = Raven.prototype.setUserContext; +Raven.prototype.setReleaseContext = Raven.prototype.setRelease; + +module.exports = Raven; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"1":1,"2":2,"3":3,"6":6}],5:[function(_dereq_,module,exports){ +(function (global){ +/** + * Enforces a single instance of the Raven client, and the + * main entry point for Raven. If you are a consumer of the + * Raven library, you SHOULD load this file (vs raven.js). + **/ + 'use strict'; -var utils = _dereq_(6); +var RavenConstructor = _dereq_(4); + +// This is to be defensive in environments where window does not exist (see https://github.com/getsentry/raven-js/pull/785) +var _window = typeof window !== 'undefined' ? window + : typeof global !== 'undefined' ? global + : typeof self !== 'undefined' ? self + : {}; +var _Raven = _window.Raven; + +var Raven = new RavenConstructor(); -var hasKey = utils.hasKey; -var isString = utils.isString; -var isUndefined = utils.isUndefined; +/* + * Allow multiple versions of Raven to be installed. + * Strip Raven from the global context and returns the instance. + * + * @return {Raven} + */ +Raven.noConflict = function () { + _window.Raven = _Raven; + return Raven; +}; + +Raven.afterLoad(); + +module.exports = Raven; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"4":4}],6:[function(_dereq_,module,exports){ +(function (global){ +'use strict'; /* - TraceKit - Cross brower stack traces - github.com/occ/TraceKit + TraceKit - Cross brower stack traces + + This was originally forked from github.com/occ/TraceKit, but has since been + largely re-written and is now maintained as part of raven-js. Tests for + this are in test/vendor. + MIT license */ @@ -1802,6 +2123,12 @@ var TraceKit = { debug: false }; +// This is to be defensive in environments where window does not exist (see https://github.com/getsentry/raven-js/pull/785) +var _window = typeof window !== 'undefined' ? window + : typeof global !== 'undefined' ? global + : typeof self !== 'undefined' ? self + : {}; + // global reference to slice var _slice = [].slice; var UNKNOWN_FUNCTION = '?'; @@ -1810,7 +2137,7 @@ var UNKNOWN_FUNCTION = '?'; var ERROR_TYPES_RE = /^(?:Uncaught (?:exception: )?)?((?:Eval|Internal|Range|Reference|Syntax|Type|URI)Error): ?(.*)$/; function getLocationHref() { - if (typeof document === 'undefined') + if (typeof document === 'undefined' || typeof document.location === 'undefined') return ''; return document.location.href; @@ -1900,7 +2227,7 @@ TraceKit.report = (function reportModuleWrapper() { return; } for (var i in handlers) { - if (hasKey(handlers, i)) { + if (handlers.hasOwnProperty(i)) { try { handlers[i].apply(null, [stack].concat(_slice.call(arguments, 2))); } catch (inner) { @@ -1949,7 +2276,7 @@ TraceKit.report = (function reportModuleWrapper() { var name = undefined; var msg = message; // must be new var or will modify original `arguments` var groups; - if (isString(message)) { + if ({}.toString.call(message) === '[object String]') { var groups = message.match(ERROR_TYPES_RE); if (groups) { name = groups[1]; @@ -1980,8 +2307,8 @@ TraceKit.report = (function reportModuleWrapper() { if (_onErrorHandlerInstalled) { return; } - _oldOnerrorHandler = window.onerror; - window.onerror = traceKitWindowOnError; + _oldOnerrorHandler = _window.onerror; + _window.onerror = traceKitWindowOnError; _onErrorHandlerInstalled = true; } @@ -1990,7 +2317,7 @@ TraceKit.report = (function reportModuleWrapper() { if (!_onErrorHandlerInstalled) { return; } - window.onerror = _oldOnerrorHandler; + _window.onerror = _oldOnerrorHandler; _onErrorHandlerInstalled = false; _oldOnerrorHandler = undefined; } @@ -2030,7 +2357,7 @@ TraceKit.report = (function reportModuleWrapper() { // slow slow IE to see if onerror occurs or not before reporting // this exception; otherwise, we will end up with an incomplete // stack trace - window.setTimeout(function () { + setTimeout(function () { if (lastException === ex) { processLastException(); } @@ -2164,10 +2491,10 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { * @return {?Object.} Stack trace information. */ function computeStackTraceFromStackProp(ex) { - if (isUndefined(ex.stack) || !ex.stack) return; + if (typeof ex.stack === 'undefined' || !ex.stack) return; var chrome = /^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i, - gecko = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|\[native).*?)(?::(\d+))?(?::(\d+))?\s*$/i, + gecko = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|resource|\[native).*?)(?::(\d+))?(?::(\d+))?\s*$/i, winjs = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i, lines = ex.stack.split('\n'), stack = [], @@ -2216,7 +2543,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { return null; } - if (!stack[0].column && !isUndefined(ex.columnNumber)) { + if (!stack[0].column && typeof ex.columnNumber !== 'undefined') { // FireFox uses this awesome columnNumber property for its top frame // Also note, Firefox's column number is 0-based and everything else expects 1-based, // so adding 1 @@ -2231,153 +2558,6 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { }; } - /** - * Computes stack trace information from the stacktrace property. - * Opera 10 uses this property. - * @param {Error} ex - * @return {?Object.} Stack trace information. - */ - function computeStackTraceFromStacktraceProp(ex) { - // Access and store the stacktrace property before doing ANYTHING - // else to it because Opera is not very good at providing it - // reliably in other circumstances. - var stacktrace = ex.stacktrace; - if (isUndefined(ex.stacktrace) || !ex.stacktrace) return; - - var opera10Regex = / line (\d+).*script (?:in )?(\S+)(?:: in function (\S+))?$/i, - opera11Regex = / line (\d+), column (\d+)\s*(?:in (?:]+)>|([^\)]+))\((.*)\))? in (.*):\s*$/i, - lines = stacktrace.split('\n'), - stack = [], - parts; - - for (var line = 0; line < lines.length; line += 2) { - var element = null; - if ((parts = opera10Regex.exec(lines[line]))) { - element = { - 'url': parts[2], - 'line': +parts[1], - 'column': null, - 'func': parts[3], - 'args':[] - }; - } else if ((parts = opera11Regex.exec(lines[line]))) { - element = { - 'url': parts[6], - 'line': +parts[1], - 'column': +parts[2], - 'func': parts[3] || parts[4], - 'args': parts[5] ? parts[5].split(',') : [] - }; - } - - if (element) { - if (!element.func && element.line) { - element.func = UNKNOWN_FUNCTION; - } - - stack.push(element); - } - } - - if (!stack.length) { - return null; - } - - return { - 'name': ex.name, - 'message': ex.message, - 'url': getLocationHref(), - 'stack': stack - }; - } - - /** - * NOT TESTED. - * Computes stack trace information from an error message that includes - * the stack trace. - * Opera 9 and earlier use this method if the option to show stack - * traces is turned on in opera:config. - * @param {Error} ex - * @return {?Object.} Stack information. - */ - function computeStackTraceFromOperaMultiLineMessage(ex) { - // Opera includes a stack trace into the exception message. An example is: - // - // Statement on line 3: Undefined variable: undefinedFunc - // Backtrace: - // Line 3 of linked script file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.js: In function zzz - // undefinedFunc(a); - // Line 7 of inline#1 script in file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.html: In function yyy - // zzz(x, y, z); - // Line 3 of inline#1 script in file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.html: In function xxx - // yyy(a, a, a); - // Line 1 of function script - // try { xxx('hi'); return false; } catch(ex) { TraceKit.report(ex); } - // ... - - var lines = ex.message.split('\n'); - if (lines.length < 4) { - return null; - } - - var lineRE1 = /^\s*Line (\d+) of linked script ((?:file|https?|blob)\S+)(?:: in function (\S+))?\s*$/i, - lineRE2 = /^\s*Line (\d+) of inline#(\d+) script in ((?:file|https?|blob)\S+)(?:: in function (\S+))?\s*$/i, - lineRE3 = /^\s*Line (\d+) of function script\s*$/i, - stack = [], - scripts = document.getElementsByTagName('script'), - parts; - - for (var line = 2; line < lines.length; line += 2) { - var item = null; - if ((parts = lineRE1.exec(lines[line]))) { - item = { - 'url': parts[2], - 'func': parts[3], - 'args': [], - 'line': +parts[1], - 'column': null - }; - } else if ((parts = lineRE2.exec(lines[line]))) { - item = { - 'url': parts[3], - 'func': parts[4], - 'args': [], - 'line': +parts[1], - 'column': null // TODO: Check to see if inline#1 (+parts[2]) points to the script number or column number. - }; - var relativeLine = (+parts[1]); // relative to the start of the