From 39f0d4d349b50a2d4294406101ee4a49132a456d Mon Sep 17 00:00:00 2001
From: rond A Vite-native unit test framework. It's fast! Collection of Vue Composition Utilities Next Generation Frontend Tooling dtd+S`MbIgptYP(ROJi)
zie2Xx38VK>u&ataRqn+LyZXo0crM3$0<9Q)1APP@9OuG$0GMi4Y_H3nVIi7b4WQl!
z(y7@MK|Gcb>O+28wQu4drpX&$2zCDAC&|NnWr7F=_|P%fy7oJZCQ^2PxQ93|8BLwS
zIr?mi$3S+IrSr7JkiEli{(bVav(Dm1a>}u!g%GzUpEZjtkDKfTy5TVM%T{BqKPe_*
zmmz{d*78JW+R@5L0bQbtihv8B*4+p0Pe^#8#7Yh=#w`pzVelj&a!wkf!2?uaVBliS
zULbUa
- © 2011–2022 The Bootstrap Authors
+ © 2011–2023 The Bootstrap Authors
Code licensed under the MIT License.
Documentation licensed under the Creative Commons Attribution License v3.0.
HTML
@@ -23,9 +22,8 @@ class Bootstrap < UrlScraper
html_filters.push 'bootstrap/entries_v5', 'bootstrap/clean_html_v5'
- options[:only_patterns] = [
- /\Agetting-started\//, /\Alayout\//, /\Acontent\//,
- /\Acomponents\//, /\Autilities\/.+/, /\Ahelpers\//
+ options[:skip_patterns] = [
+ /\Aabout\//
]
options[:replace_paths] = {
From a32e3d0522fbe466647b070d62be92d6aff99d9a Mon Sep 17 00:00:00 2001
From: Simon Legner '
+ super
+ end
end
end
From 6100912e469261a531ecd271cba5dc47679dbfb5 Mon Sep 17 00:00:00 2001
From: Simon Legner
Vite
' if root_page?
- @doc = at_css('.content > div')
- else
- return 'Vue.js
' if root_page?
- @doc = at_css(version == '3' ? 'main > div > div' : '.content')
- end
+ return 'Vue.js
' if root_page?
+ @doc = at_css(version == '3' ? 'main > div > div' : '.content')
at_css('h1').content = 'Vue.js' if root_page?
doc.child.before('Vue.js API
') if slug == 'api/' || slug == 'api/index'
diff --git a/lib/docs/scrapers/vue.rb b/lib/docs/scrapers/vue.rb
index 3b2bde9f47..53f0723ac4 100644
--- a/lib/docs/scrapers/vue.rb
+++ b/lib/docs/scrapers/vue.rb
@@ -29,6 +29,7 @@ class Vue < UrlScraper
self.release = '2.7.14'
self.base_url = 'https://v2.vuejs.org/v2/'
self.initial_paths = %w(api/)
+ self.root_path = 'guide/'
html_filters.push 'vue/entries', 'vue/clean_html'
end
From 751dcc4087e049450280813b1d762b4c7a547b94 Mon Sep 17 00:00:00 2001
From: ClasherKasten
+ © 2010–2023 Jeremy Ashkenas, DocumentCloud
Licensed under the MIT License.
HTML
From f0e52f8f757b78df01d85af10c42881b3e8fb529 Mon Sep 17 00:00:00 2001
From: Simon Legner
+ © 2009–2023 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
HTML
+ version '1.9' do
+ self.release = '1.9.2'
+ self.base_url = "https://docs.julialang.org/en/v#{version}/"
+ self.type = 'julia'
+
+ html_filters.push 'julia/entries', 'julia/clean_html'
+
+ options[:container] = '.docs-main'
+ options[:only_patterns] = [/\Amanual\//, /\Abase\//, /\Astdlib\//]
+ end
+
version '1.8' do
self.release = '1.8.5'
self.base_url = "https://docs.julialang.org/en/v#{version}/"
From f7caff282d5f9960b4f556bde6dfe27489e6bfdf Mon Sep 17 00:00:00 2001
From: Simon Legner Vitest
VueUse
Vite
' if root_page?
+ return 'Vite
+ © 2012–2023 Scott Chacon and others
Licensed under the MIT License.
HTML
From 58446f23fb5070e81575c5b1ca1c235c9d8f7e0c Mon Sep 17 00:00:00 2001
From: Simon Legner
+ © 2023 Willy Tarreau, HAProxy contributors
Licensed under the GNU General Public License version 2.
HTML
+ version '2.8' do
+ self.release = '2.8.0'
+ self.base_url = "https://docs.haproxy.org/#{self.version}/"
+ end
+
version '2.7' do
self.release = '2.7.0'
self.base_url = "https://docs.haproxy.org/#{self.version}/"
From d638585fea8e383143773c0c031e5769445ba9c1 Mon Sep 17 00:00:00 2001
From: Simon Legner
+ © 2023 MariaDB
Licensed under the Creative Commons Attribution 3.0 Unported License and the GNU Free Documentation License.
HTML
From 05c8c21cae146720323ac89fdf1a1dbb9ed7a8e0 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Sat, 5 Aug 2023 00:28:52 +0000
Subject: [PATCH 0033/1110] chore(deps): update dependency rack to v2.2.8
---
Gemfile.lock | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 481293f89b..1bc44105c1 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -70,7 +70,7 @@ GEM
byebug (~> 11.0)
pry (>= 0.13, < 0.15)
racc (1.6.2)
- rack (2.2.7)
+ rack (2.2.8)
rack-protection (3.0.6)
rack
rack-ssl-enforcer (0.2.9)
From 34c456cd4ec2f5241608b60c6062f26c633aad54 Mon Sep 17 00:00:00 2001
From: Luckas <101930730+luckasRanarison@users.noreply.github.com>
Date: Fri, 11 Aug 2023 04:34:43 +0300
Subject: [PATCH 0034/1110] Add nvim-devdocs to related projects
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index b5e117738e..a5a190fd4e 100644
--- a/README.md
+++ b/README.md
@@ -174,6 +174,7 @@ Made something cool? Feel free to open a PR to add a new row to this table! You
| [mdh34/quickDocs](https://github.com/mdh34/quickDocs) | Vala/Python based viewer |  |  |
| [romainl/vim-devdocs](https://github.com/romainl/vim-devdocs) | Vim plugin |  |  |
| [waiting-for-dev/vim-www](https://github.com/waiting-for-dev/vim-www) | Vim plugin |  |  |
+| [luckasRanarison/nvim-devdocs](https://github.com/luckasRanarison/nvim-devdocs) | Neovim plugin |  |  |
## Copyright / License
From 69357cd89d8120f1eae032d49a67569a0197aec6 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Sat, 12 Aug 2023 00:17:40 +0000
Subject: [PATCH 0035/1110] chore(deps): update dependency activesupport to
v7.0.7
---
Gemfile.lock | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 1bc44105c1..0d6099cfaf 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,7 +1,7 @@
GEM
remote: https://rubygems.org/
specs:
- activesupport (7.0.6)
+ activesupport (7.0.7)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
From 4f81d61f276fa229b87cdde7c5cc91f97dc2a2e0 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Sat, 12 Aug 2023 04:18:37 +0000
Subject: [PATCH 0036/1110] chore(deps): update dependency nokogiri to v1.15.4
---
Gemfile.lock | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 0d6099cfaf..e2548a850f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -46,7 +46,7 @@ GEM
image_size (3.2.0)
in_threads (1.6.0)
method_source (1.0.0)
- mini_portile2 (2.8.2)
+ mini_portile2 (2.8.4)
minitest (5.18.1)
multi_json (1.15.0)
mustermann (3.0.0)
@@ -55,7 +55,7 @@ GEM
net-ssh (>= 5.0.0, < 8.0.0)
net-ssh (7.0.1)
newrelic_rpm (8.16.0)
- nokogiri (1.15.3)
+ nokogiri (1.15.4)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
options (2.3.2)
From 27ca1658c4411224e8b923ee79664ade532faa24 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Sat, 12 Aug 2023 04:19:06 +0000
Subject: [PATCH 0037/1110] chore(deps): update dependency rss to v0.3.0
---
Gemfile.lock | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 0d6099cfaf..2dc2716aec 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -81,10 +81,10 @@ GEM
rb-inotify (0.10.1)
ffi (~> 1.0)
redcarpet (3.6.0)
- rexml (3.2.5)
+ rexml (3.2.6)
rouge (1.11.1)
rr (3.1.0)
- rss (0.2.9)
+ rss (0.3.0)
rexml
ruby2_keywords (0.0.5)
sass (3.7.4)
From 0619e0fdb5a0e5b5689d78662224d90cd94f645d Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Sat, 12 Aug 2023 07:18:19 +0000
Subject: [PATCH 0038/1110] chore(deps): update dependency sinatra to v3.1.0
---
Gemfile.lock | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index e2548a850f..1b31c4e751 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -122,7 +122,7 @@ GEM
eventmachine (~> 1.0, >= 1.0.4)
rack (>= 1, < 3)
thor (1.2.2)
- tilt (2.0.11)
+ tilt (2.2.0)
tty-pager (0.14.0)
strings (~> 0.2.0)
tty-screen (~> 0.8)
From e1999ca288a60d79ce6caa6382069f2d500435c5 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Sat, 12 Aug 2023 07:18:33 +0000
Subject: [PATCH 0039/1110] chore(deps): update dependency sinatra-contrib to
v3.1.0
---
Gemfile.lock | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index e2548a850f..dca06e5c7f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -71,8 +71,8 @@ GEM
pry (>= 0.13, < 0.15)
racc (1.6.2)
rack (2.2.8)
- rack-protection (3.0.6)
- rack
+ rack-protection (3.1.0)
+ rack (~> 2.2, >= 2.2.4)
rack-ssl-enforcer (0.2.9)
rack-test (2.1.0)
rack (>= 1.3)
@@ -92,16 +92,16 @@ GEM
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
- sinatra (3.0.6)
+ sinatra (3.1.0)
mustermann (~> 3.0)
rack (~> 2.2, >= 2.2.4)
- rack-protection (= 3.0.6)
+ rack-protection (= 3.1.0)
tilt (~> 2.0)
- sinatra-contrib (3.0.6)
+ sinatra-contrib (3.1.0)
multi_json
mustermann (~> 3.0)
- rack-protection (= 3.0.6)
- sinatra (= 3.0.6)
+ rack-protection (= 3.1.0)
+ sinatra (= 3.1.0)
tilt (~> 2.0)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
@@ -122,7 +122,7 @@ GEM
eventmachine (~> 1.0, >= 1.0.4)
rack (>= 1, < 3)
thor (1.2.2)
- tilt (2.0.11)
+ tilt (2.2.0)
tty-pager (0.14.0)
strings (~> 0.2.0)
tty-screen (~> 0.8)
From acbf684186c47325aa972368b488f045cdeea9cb Mon Sep 17 00:00:00 2001
From: Simon Legner
+ Licensed under the MIT License.
+ HTML
+
+ def get_latest_version(opts)
+ get_npm_version("fluture", opts)
+ end
+ end
+end
diff --git a/public/icons/docs/fluture/16.png b/public/icons/docs/fluture/16.png
new file mode 100644
index 0000000000000000000000000000000000000000..75fca6cd7002304e9462f01eaaa652b733559e1a
GIT binary patch
literal 3743
zcma)82{@E%`+sMMNn^{-V2CVbjNMEWG8j99ti>?nj4*S?Oc>cSwk$~
+ © 2016 Plaid Technologies, Inc.
+ Licensed under the MIT License.
+ HTML
+
+ def get_latest_version(opts)
+ get_npm_version("sanctuary-def", opts)
+ end
+ end
+end
diff --git a/public/icons/docs/sanctuary_def/16.png b/public/icons/docs/sanctuary_def/16.png
new file mode 100644
index 0000000000000000000000000000000000000000..df0bbd4f6294ce35b1cd30a7c6fe8c85b675c9fa
GIT binary patch
literal 564
zcmV-40?Yl0P)
+ © 2016 Plaid Technologies, Inc.
+ Licensed under the MIT License.
+ HTML
+
+ def get_latest_version(opts)
+ get-npm-version("sanctuary-type-classes", opts)
+ end
+ end
+end
diff --git a/public/icons/docs/sanctuary_type_classes/16.png b/public/icons/docs/sanctuary_type_classes/16.png
new file mode 100644
index 0000000000000000000000000000000000000000..df0bbd4f6294ce35b1cd30a7c6fe8c85b675c9fa
GIT binary patch
literal 564
zcmV-40?Yl0P)
+ © 2016 Plaid Technologies, Inc.
+ Copyright © 2011-2022, Project contributors Copyright © 2011-2020, Sideway Inc Copyright © 2011-2014, Walmart
+ Copyright © 2011, Yahoo Inc.
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+ Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+ The names of any contributors may not be used to endorse or promote products derived from this software without specific prior written permission.
+ HTML
+
+ def get_latest_version(opts)
+ get_npm_version("@hapi/hapi", opts)
+ end
+
+ private
+
+ end
+
+end
diff --git a/public/icons/docs/hapi/16.png b/public/icons/docs/hapi/16.png
new file mode 100644
index 0000000000000000000000000000000000000000..9f5080f1e0bd0ab2ab2198859a86b2d3da424cd8
GIT binary patch
literal 466
zcmV;@0WJQCP)
+ Licensed under the Apache License, Version 2.0.
HTML
self.release = '3.1.3'
From 74c7f624b1f336f2313dab5b5140915cc31f1fbe Mon Sep 17 00:00:00 2001
From: Jan Christoph Ebersbach
- © 2016 Plaid Technologies, Inc.
Copyright © 2011-2022, Project contributors Copyright © 2011-2020, Sideway Inc Copyright © 2011-2014, Walmart
Copyright © 2011, Yahoo Inc.
All rights reserved.
From cb6a65e482f3b3896ad0985698364a9cead62f83 Mon Sep 17 00:00:00 2001
From: Jan Christoph Ebersbach M)2^*8QP(`ZzrAHi7U^~<0T@f?0#$FxG
zB9HDLirzJ{YswZ8voeLmj6QQo1$5?!ocp0yYQB}izMFw4zNKqq@LN6naTt98b6Dhk
zhU_}B7xb#eEbS3*<+Fu2Z~yYEdGV{DI~G;G
+ Copyright © 2006–2020 Varnish Software AS
+ Licensed under the BSD-2-Clause License.
+ HTML
+
+ version do
+ self.release = '7.3.0'
+ self.base_url = 'https://varnish-cache.org/docs/7.3/'
+ end
+
+ def get_latest_version(opts)
+ contents = get_github_file_contents('varnishcache', 'varnish-cache', 'doc/changes.rst', opts)
+ contents.scan(/Varnish\s+Cache\s+([0-9.]+)/)[0][0]
+ end
+
+ end
+end
diff --git a/public/icons/docs/varnish/16.png b/public/icons/docs/varnish/16.png
new file mode 100644
index 0000000000000000000000000000000000000000..49c7e76e6dae555af006cc10dc501ac02e73ac8c
GIT binary patch
literal 1428
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk?
zp1FzXsX?iUDV2pMQ*9U+m{l`FB1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+*
zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn
zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxOgGuk#6rQ`QqR!L
z#K720N5ROz&{E&PLf_C>*TB%qz|6|jTmcG{fVLH-q*(>IxIyg#@@$ndN=gc>^!3Zj
z%k|2Q_413-^$jg8fo2%#8yV>WRp=I1=9MH?=;jqG!%T2VElw`VEGWs$&r<-In3$Ab
zT4JjNbScCOxdm`z^NOLt1Pn0!io^naLp=kKmtYEgeeo;J&4sHjE(uCSxEHIz#UYgi
zsro^w#rdU0$-sz9QwCX8VC7ttnpl!w6q28x0}I7~jQo=P;*9(P1tXx76f}GjlQZ)`
zBAQ?=zP?tTdBr7(dC94sF1AWQL-aB;Q>+XPObty<-7HNU&D{(QU7akPj7*$d&5Yb!
zoGcAZ-5g
+ Copyright © 2011, Yahoo Inc.
+ Licensed under the BSD 3-clause License.
HTML
def get_latest_version(opts)
From 2dce213f6acf726a150af06919a2c09578e2441a Mon Sep 17 00:00:00 2001
From: Simon Legner
+ Licensed under the BSD 3-clause License.
HTML
def get_latest_version(opts)
From 19445c34fe1d88d0f4fb3aa6be9255ff4645d738 Mon Sep 17 00:00:00 2001
From: Simon Legner
Copyright © 2006–2020 Varnish Software AS
@@ -27,8 +22,8 @@ class Varnish < UrlScraper
HTML
version do
- self.release = '7.3.0'
- self.base_url = 'https://varnish-cache.org/docs/7.3/'
+ self.release = '7.4'
+ self.base_url = "https://varnish-cache.org/docs/#{release}/"
end
def get_latest_version(opts)
From 136a2f168203776b4c87744d256cfcfc1e288e7f Mon Sep 17 00:00:00 2001
From: Simon Legner
+ © 2010 Pallets
Licensed under the BSD 3-clause License.
HTML
+ version '3.0' do
+ self.release = '3.0.x'
+ self.base_url = "https://flask.palletsprojects.com/en/#{self.release}/"
+ end
+
version '2.3' do
self.release = '2.3.x'
self.base_url = "https://flask.palletsprojects.com/en/#{self.release}/"
From 0166105a4518aa7978d027b4098b6dc08f3d856e Mon Sep 17 00:00:00 2001
From: Simon Legner
+ © 2007 Pallets
Licensed under the BSD 3-clause License.
HTML
+ version '3.0' do
+ self.release = '3.0.x'
+ self.base_url = "https://werkzeug.palletsprojects.com/en/#{self.release}/"
+ end
+
version '2.3' do
self.release = '2.3.x'
self.base_url = "https://werkzeug.palletsprojects.com/en/#{self.release}/"
From 0ac54123585e40bab507cdf213bd36c214677702 Mon Sep 17 00:00:00 2001
From: Simon Legner
+ © 2016–2023 Rich Harris and contributors
Licensed under the MIT License.
HTML
options[:skip] = %w(team.html plugins/)
- self.release = '3.55.0'
- self.base_url = 'https://svelte.dev/docs'
+ self.base_url = 'https://svelte.dev/docs/'
html_filters.push 'svelte/entries', 'svelte/clean_html'
+
+ version do
+ self.release = '4.2.1'
+ end
+
+ version '3' do
+ self.release = '3.55.0'
+ end
def get_latest_version(opts)
get_npm_version('svelte', opts)
From 17d492b4856499831ff2ec32056f43ab6b55c7bb Mon Sep 17 00:00:00 2001
From: Simon Legner
+ Licensed under the MIT License.
HTML
def get_latest_version(opts)
From 94bf820267559db42f35a9ce59c15fcea607a68d Mon Sep 17 00:00:00 2001
From: Simon Legner
\n?/, "\n").gsub("\n
hkMTnu9m5GIwv7xqsft7(l z+PuZhC>nC}Q!>*kff@|Wbq&mQjer^qtV~U<3@soU%5F@p1ZvQL+fb63n_66wm|K8u aiKUgHDMXKCUC{=h9tKZWKbLh*2~7Z<^G!zp literal 0 HcmV?d00001 diff --git a/public/icons/docs/bazel/16@2x.png b/public/icons/docs/bazel/16@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a8e26f2fbaa4a390bcb756ecf6117333761bb798 GIT binary patch literal 2418 zcmZ9OcTm%56UTpqUPS@nuH m}9>uOa1HhvvKh zz5okW5hjF_ R^N1h#k7f2ZX2RJb_EhZHOC<^>SW31__z03-&4cbezMP@k?U$v#ZMm*z z=asZOSbWQ#2wy1S<#ShkYZoU&%6Klj-a*gDMonutse7n#<|XE{5aV4F5~fJB*Z t#gV0_W_wS2Fy*_zrb+8&8~V#&ofF%2M-(rnyVz0;@8O!dAVN zttn(PuZR@V)iRBFH5*Zehe}+dxU@sT8xHo~daPly6jwLqb2R7GRp?zxJn3!c#CO|q zWid8tnOM^MT^$Utls!q&u$~GhW<9uD?Rt2vZaP`RqR6RJU?3#CHJlj0kui1GM`cxr zyel@K%K4~q757rBctG8XnZ4L-@~>fosoS1;P1OtFk+tXx>C3ty2ai;0c?{ N4G;aZV$=l2G2@26I^-sdxU z4?lS=7F)nywB*FdP6Y*9`E_xeyuH(FkEiGmsF{LD&O^WvxfkX;tbH&2L3?fXOpW_A zsy7LAaWe4_c^K@^^E&lV<|gN`n4LZ9J7h+t%FT96pB=Q?Wjoko*byvApsra>n%I|r zk&iNMZFH&qh+7^(PkHx_@2x~tnu;chCiV@byM@TKj>1G>`3d?l7NDckq(;260KGBs zBCl37@IF3$V6VEn5Zw2LqsGSOsr(bl(*6w}hW>u(No(OJ6Zr{RTNHdjuH!r=z_x!V z sL;TwDvx{TA 96!Q0w-$@i?)&lyN;aL2x8*n0al*rm z0@?$wUAXD!FH|tTQ 4pSn0>WwiJ9z7yvqyO?@uPSm@wWE-&iA3CbWRa1A$kt6}ScOgegEGfY_`vj{yi zen*)O^p!=ZgFQ*N!#557_Jd(nO2NsT#$cgbu73NX R?^NF`aS3J&a)FgNZNwyIWbepH?PP!rFfQlF!J ^_EmO3hGR zI9ubNUE!H_Yc4DJMIVDm(9|hNcm9@*`UD=(fR;vbh_}GRs~Q56qwMqi{J^_^e$V)9 zmz#Ja%jogLS0eFe8+glC7YCORqGZd^X nW0W8N*TK|q z*fm`aMa^FhwE}j_=ZGbLT`!Q>E|+XgM#=bImP@v+w4~OT^KB3tt;Q0Wpg~T=X8wz* z45Xpwdk+<+G^}@musLFVUqyXHfBU6pr0P*o*#>bM9Hqf;b)&uZtM?s!-C(8N+C%K^ zx%(UmT+#J(U8>$%&BeGEXKvF_s7Z%ty{{+F-@I{4znympM7V1!CEu66YAU%fI(Bx) zHSG3zHLRgr3>^-%E2=9{5JC~Ugzua^tB>B;bF}oL&EXmh5ZBbs<7ahR%0l?}!OSFX z5s(a<*fHWOfjWM9ewOeY!lvrqv{)Ja`1iCoL#K*djrgGFzXqel_AQm8pV7GinJckt zTC>Brwy*n@;irT8yHt4BELRv==PFe?TdVNJe4QYIUMg;Z-iez|z*fmo@0_et>gq@e zwM@Ai@kp8!m~(IaAaG=4%R9z{3#623{srmZ>|Qo`HzWZo9!g=Ps=z5uX=|yc$j>IT z mnu_;{`}UFg*f{STNc8MEWUCkc{*golt$Xesj1&;+mQj=So?W z)+`9%WGr7@C^BzgZAhpynKaV46mj ;(#M)3nvn?2bV z va5tFE=wns;mA}H!`1H; zKjZa7O}^~9CCZOp&okba=d*-;M69iWiBXGW_w=C(KlK`Yn888~ikSPnGu?lNedb)4 zs!teZcF_YsQ^L1A^nf9qEi?Df2+IZ2nfEXM$U?r!mbst{^7JQ(clOni^KSZ|3(hwf z@Ne*jj)UBpn2+d}-Z53(GK#5Wc5;e#6YR8Y#-<3~a7$5sTt8RvI h7<}0 zD%1D+7}C_h_oe4`MhxfcQkMvHSB7ClJw^(XuQ?xAXt2ImTpH(98It>x*tJ#UdC2>{ z!gKLDQf~FC8FO2M(&v8iJ!RJoxb8ELP~2XJuj#Xxx^D-1pfCgdHF6FKKhrKXyECiy z0`pzULD)I^97PpWbo~2^*T+Uj+HD~bgtQ5Aa}L77T?4Q*0Tdt#in34zSt!gBdIPQm zgDXP85C|LsaW5*1`yar^&kg4uP6MDSvJe$n1sVW>D=WjHuzv#~bGhL(K Date: Wed, 3 Jan 2024 18:19:30 +0900 Subject: [PATCH 0102/1110] Upgrade minitest from 5.18.1 to 5.20.0 --- Gemfile.lock | 2 +- test/app_test.rb | 2 +- test/lib/docs/core/doc_test.rb | 2 +- test/lib/docs/core/entry_index_test.rb | 2 +- test/lib/docs/core/filter_test.rb | 2 +- test/lib/docs/core/instrumentable_test.rb | 2 +- test/lib/docs/core/manifest_test.rb | 2 +- test/lib/docs/core/models/entry_test.rb | 2 +- test/lib/docs/core/models/type_test.rb | 2 +- test/lib/docs/core/parser_test.rb | 2 +- test/lib/docs/core/request_test.rb | 2 +- test/lib/docs/core/requester_test.rb | 2 +- test/lib/docs/core/response_test.rb | 2 +- test/lib/docs/core/scraper_test.rb | 2 +- test/lib/docs/core/scrapers/file_scraper_test.rb | 2 +- test/lib/docs/core/scrapers/url_scraper_test.rb | 2 +- test/lib/docs/core/url_test.rb | 2 +- test/lib/docs/filters/core/apply_base_url_test.rb | 2 +- test/lib/docs/filters/core/clean_html_test.rb | 2 +- test/lib/docs/filters/core/clean_text_test.rb | 2 +- test/lib/docs/filters/core/container_test.rb | 2 +- test/lib/docs/filters/core/entries_test.rb | 2 +- test/lib/docs/filters/core/inner_html_test.rb | 2 +- test/lib/docs/filters/core/internal_urls_test.rb | 2 +- test/lib/docs/filters/core/normalize_paths_test.rb | 2 +- test/lib/docs/filters/core/normalize_urls_test.rb | 2 +- test/lib/docs/filters/core/parse_cf_email_test.rb | 2 +- test/lib/docs/filters/core/title_test.rb | 2 +- test/lib/docs/storage/abstract_store_test.rb | 2 +- test/lib/docs/storage/file_store_test.rb | 2 +- test/test_helper.rb | 2 +- 31 files changed, 31 insertions(+), 31 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 68930c1d03..115d6860fd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -47,7 +47,7 @@ GEM in_threads (1.6.0) method_source (1.0.0) mini_portile2 (2.8.4) - minitest (5.18.1) + minitest (5.20.0) multi_json (1.15.0) mustermann (3.0.0) ruby2_keywords (~> 0.0.1) diff --git a/test/app_test.rb b/test/app_test.rb index 8e9e369b5e..44fca64aab 100644 --- a/test/app_test.rb +++ b/test/app_test.rb @@ -2,7 +2,7 @@ require 'rack/test' require 'app' -class AppTest < MiniTest::Spec +class AppTest < Minitest::Spec include Rack::Test::Methods MODERN_BROWSER = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:39.0) Gecko/20100101 Firefox/39.0' diff --git a/test/lib/docs/core/doc_test.rb b/test/lib/docs/core/doc_test.rb index b0d2341f84..6b41cdd9c8 100644 --- a/test/lib/docs/core/doc_test.rb +++ b/test/lib/docs/core/doc_test.rb @@ -1,7 +1,7 @@ require_relative '../../../test_helper' require_relative '../../../../lib/docs' -class DocsDocTest < MiniTest::Spec +class DocsDocTest < Minitest::Spec let :doc do Class.new Docs::Doc do self.name = 'name' diff --git a/test/lib/docs/core/entry_index_test.rb b/test/lib/docs/core/entry_index_test.rb index 187f5f4ad2..95495eb90a 100644 --- a/test/lib/docs/core/entry_index_test.rb +++ b/test/lib/docs/core/entry_index_test.rb @@ -1,7 +1,7 @@ require_relative '../../../test_helper' require_relative '../../../../lib/docs' -class DocsEntryIndexTest < MiniTest::Spec +class DocsEntryIndexTest < Minitest::Spec let :entry do Docs::Entry.new 'name', 'path', 'type' end diff --git a/test/lib/docs/core/filter_test.rb b/test/lib/docs/core/filter_test.rb index 38c5019a3f..10872c7beb 100644 --- a/test/lib/docs/core/filter_test.rb +++ b/test/lib/docs/core/filter_test.rb @@ -1,7 +1,7 @@ require_relative '../../../test_helper' require_relative '../../../../lib/docs' -class DocsFilterTest < MiniTest::Spec +class DocsFilterTest < Minitest::Spec include FilterTestHelper self.filter_class = Docs::Filter diff --git a/test/lib/docs/core/instrumentable_test.rb b/test/lib/docs/core/instrumentable_test.rb index c3fe2b8b6b..5a244d5199 100644 --- a/test/lib/docs/core/instrumentable_test.rb +++ b/test/lib/docs/core/instrumentable_test.rb @@ -1,7 +1,7 @@ require_relative '../../../test_helper' require_relative '../../../../lib/docs' -class DocsInstrumentableTest < MiniTest::Spec +class DocsInstrumentableTest < Minitest::Spec let :extended_class do Class.new.tap { |klass| klass.send :extend, Docs::Instrumentable } end diff --git a/test/lib/docs/core/manifest_test.rb b/test/lib/docs/core/manifest_test.rb index f121e69b21..636885cd1a 100644 --- a/test/lib/docs/core/manifest_test.rb +++ b/test/lib/docs/core/manifest_test.rb @@ -1,7 +1,7 @@ require_relative '../../../test_helper' require_relative '../../../../lib/docs' -class ManifestTest < MiniTest::Spec +class ManifestTest < Minitest::Spec let :doc do doc = Class.new Docs::Scraper doc.name = 'TestDoc' diff --git a/test/lib/docs/core/models/entry_test.rb b/test/lib/docs/core/models/entry_test.rb index 7487340ff2..56de796661 100644 --- a/test/lib/docs/core/models/entry_test.rb +++ b/test/lib/docs/core/models/entry_test.rb @@ -1,7 +1,7 @@ require_relative '../../../../test_helper' require_relative '../../../../../lib/docs' -class DocsEntryTest < MiniTest::Spec +class DocsEntryTest < Minitest::Spec Entry = Docs::Entry let :entry do diff --git a/test/lib/docs/core/models/type_test.rb b/test/lib/docs/core/models/type_test.rb index a01d8e6140..e3f3b3368f 100644 --- a/test/lib/docs/core/models/type_test.rb +++ b/test/lib/docs/core/models/type_test.rb @@ -1,7 +1,7 @@ require_relative '../../../../test_helper' require_relative '../../../../../lib/docs' -class DocsTypeTest < MiniTest::Spec +class DocsTypeTest < Minitest::Spec Type = Docs::Type describe ".new" do diff --git a/test/lib/docs/core/parser_test.rb b/test/lib/docs/core/parser_test.rb index 440516a8e9..2edacacb99 100644 --- a/test/lib/docs/core/parser_test.rb +++ b/test/lib/docs/core/parser_test.rb @@ -1,7 +1,7 @@ require_relative '../../../test_helper' require_relative '../../../../lib/docs' -class DocsParserTest < MiniTest::Spec +class DocsParserTest < Minitest::Spec def parser(content) Docs::Parser.new(content) end diff --git a/test/lib/docs/core/request_test.rb b/test/lib/docs/core/request_test.rb index d05dd1ffe9..4e7353465d 100644 --- a/test/lib/docs/core/request_test.rb +++ b/test/lib/docs/core/request_test.rb @@ -1,7 +1,7 @@ require_relative '../../../test_helper' require_relative '../../../../lib/docs' -class DocsRequestTest < MiniTest::Spec +class DocsRequestTest < Minitest::Spec let :url do 'http://example.com' end diff --git a/test/lib/docs/core/requester_test.rb b/test/lib/docs/core/requester_test.rb index acf8d0a9a2..18a17ec3fc 100644 --- a/test/lib/docs/core/requester_test.rb +++ b/test/lib/docs/core/requester_test.rb @@ -1,7 +1,7 @@ require_relative '../../../test_helper' require_relative '../../../../lib/docs' -class DocsRequesterTest < MiniTest::Spec +class DocsRequesterTest < Minitest::Spec def stub_request(url) Typhoeus.stub(url).and_return(Typhoeus::Response.new) end diff --git a/test/lib/docs/core/response_test.rb b/test/lib/docs/core/response_test.rb index 9e92c034fc..5c06dc0cea 100644 --- a/test/lib/docs/core/response_test.rb +++ b/test/lib/docs/core/response_test.rb @@ -1,7 +1,7 @@ require_relative '../../../test_helper' require_relative '../../../../lib/docs' -class DocsResponseTest < MiniTest::Spec +class DocsResponseTest < Minitest::Spec let :response do Typhoeus::Response.new(options).tap do |response| response.extend Docs::Response diff --git a/test/lib/docs/core/scraper_test.rb b/test/lib/docs/core/scraper_test.rb index 3d49320743..7b5522b3cb 100644 --- a/test/lib/docs/core/scraper_test.rb +++ b/test/lib/docs/core/scraper_test.rb @@ -1,7 +1,7 @@ require_relative '../../../test_helper' require_relative '../../../../lib/docs' -class DocsScraperTest < MiniTest::Spec +class DocsScraperTest < Minitest::Spec class Scraper < Docs::Scraper self.type = 'scraper' self.base_url = 'http://example.com/' diff --git a/test/lib/docs/core/scrapers/file_scraper_test.rb b/test/lib/docs/core/scrapers/file_scraper_test.rb index 46dad3c780..93b2ba43b4 100644 --- a/test/lib/docs/core/scrapers/file_scraper_test.rb +++ b/test/lib/docs/core/scrapers/file_scraper_test.rb @@ -1,7 +1,7 @@ require_relative '../../../../test_helper' require_relative '../../../../../lib/docs' -class FileScraperTest < MiniTest::Spec +class FileScraperTest < Minitest::Spec ROOT_PATH = File.expand_path('../../../../../../', __FILE__) class Scraper < Docs::FileScraper diff --git a/test/lib/docs/core/scrapers/url_scraper_test.rb b/test/lib/docs/core/scrapers/url_scraper_test.rb index 9a687256ab..77fc235d57 100644 --- a/test/lib/docs/core/scrapers/url_scraper_test.rb +++ b/test/lib/docs/core/scrapers/url_scraper_test.rb @@ -1,7 +1,7 @@ require_relative '../../../../test_helper' require_relative '../../../../../lib/docs' -class DocsUrlScraperTest < MiniTest::Spec +class DocsUrlScraperTest < Minitest::Spec class Scraper < Docs::UrlScraper self.base_url = 'http://example.com' self.html_filters = Docs::FilterStack.new diff --git a/test/lib/docs/core/url_test.rb b/test/lib/docs/core/url_test.rb index 7d4bda43de..8f08b1404e 100644 --- a/test/lib/docs/core/url_test.rb +++ b/test/lib/docs/core/url_test.rb @@ -1,7 +1,7 @@ require_relative '../../../test_helper' require_relative '../../../../lib/docs' -class DocsUrlTest < MiniTest::Spec +class DocsUrlTest < Minitest::Spec URL = Docs::URL describe ".new" do diff --git a/test/lib/docs/filters/core/apply_base_url_test.rb b/test/lib/docs/filters/core/apply_base_url_test.rb index ba2764e7a8..ce070018e9 100644 --- a/test/lib/docs/filters/core/apply_base_url_test.rb +++ b/test/lib/docs/filters/core/apply_base_url_test.rb @@ -1,7 +1,7 @@ require_relative '../../../../test_helper' require_relative '../../../../../lib/docs' -class ApplyBaseUrlFilterTest < MiniTest::Spec +class ApplyBaseUrlFilterTest < Minitest::Spec include FilterTestHelper self.filter_class = Docs::ApplyBaseUrlFilter self.filter_type = 'html' diff --git a/test/lib/docs/filters/core/clean_html_test.rb b/test/lib/docs/filters/core/clean_html_test.rb index 2d3c9f8add..a1a47013d2 100644 --- a/test/lib/docs/filters/core/clean_html_test.rb +++ b/test/lib/docs/filters/core/clean_html_test.rb @@ -1,7 +1,7 @@ require_relative '../../../../test_helper' require_relative '../../../../../lib/docs' -class CleanHtmlFilterTest < MiniTest::Spec +class CleanHtmlFilterTest < Minitest::Spec include FilterTestHelper self.filter_class = Docs::CleanHtmlFilter diff --git a/test/lib/docs/filters/core/clean_text_test.rb b/test/lib/docs/filters/core/clean_text_test.rb index 7babf15e48..4126b88686 100644 --- a/test/lib/docs/filters/core/clean_text_test.rb +++ b/test/lib/docs/filters/core/clean_text_test.rb @@ -1,7 +1,7 @@ require_relative '../../../../test_helper' require_relative '../../../../../lib/docs' -class CleanTextFilterTest < MiniTest::Spec +class CleanTextFilterTest < Minitest::Spec include FilterTestHelper self.filter_class = Docs::CleanTextFilter diff --git a/test/lib/docs/filters/core/container_test.rb b/test/lib/docs/filters/core/container_test.rb index 7d4993e8b4..d1cc3ab437 100644 --- a/test/lib/docs/filters/core/container_test.rb +++ b/test/lib/docs/filters/core/container_test.rb @@ -1,7 +1,7 @@ require_relative '../../../../test_helper' require_relative '../../../../../lib/docs' -class ContainerFilterTest < MiniTest::Spec +class ContainerFilterTest < Minitest::Spec include FilterTestHelper self.filter_class = Docs::ContainerFilter self.filter_type = 'html' diff --git a/test/lib/docs/filters/core/entries_test.rb b/test/lib/docs/filters/core/entries_test.rb index e33f778d01..95951e1144 100644 --- a/test/lib/docs/filters/core/entries_test.rb +++ b/test/lib/docs/filters/core/entries_test.rb @@ -1,7 +1,7 @@ require_relative '../../../../test_helper' require_relative '../../../../../lib/docs' -class EntriesFilterTest < MiniTest::Spec +class EntriesFilterTest < Minitest::Spec include FilterTestHelper self.filter_class = Docs::EntriesFilter diff --git a/test/lib/docs/filters/core/inner_html_test.rb b/test/lib/docs/filters/core/inner_html_test.rb index bbd4095848..74fbc9c72f 100644 --- a/test/lib/docs/filters/core/inner_html_test.rb +++ b/test/lib/docs/filters/core/inner_html_test.rb @@ -1,7 +1,7 @@ require_relative '../../../../test_helper' require_relative '../../../../../lib/docs' -class InnerHtmlFilterTest < MiniTest::Spec +class InnerHtmlFilterTest < Minitest::Spec include FilterTestHelper self.filter_class = Docs::InnerHtmlFilter diff --git a/test/lib/docs/filters/core/internal_urls_test.rb b/test/lib/docs/filters/core/internal_urls_test.rb index c9194e09e0..9888c9fe67 100644 --- a/test/lib/docs/filters/core/internal_urls_test.rb +++ b/test/lib/docs/filters/core/internal_urls_test.rb @@ -1,7 +1,7 @@ require_relative '../../../../test_helper' require_relative '../../../../../lib/docs' -class InternalUrlsFilterTest < MiniTest::Spec +class InternalUrlsFilterTest < Minitest::Spec include FilterTestHelper self.filter_class = Docs::InternalUrlsFilter diff --git a/test/lib/docs/filters/core/normalize_paths_test.rb b/test/lib/docs/filters/core/normalize_paths_test.rb index 6d407f2b0b..1fc8272128 100644 --- a/test/lib/docs/filters/core/normalize_paths_test.rb +++ b/test/lib/docs/filters/core/normalize_paths_test.rb @@ -1,7 +1,7 @@ require_relative '../../../../test_helper' require_relative '../../../../../lib/docs' -class NormalizePathsFilterTest < MiniTest::Spec +class NormalizePathsFilterTest < Minitest::Spec include FilterTestHelper self.filter_class = Docs::NormalizePathsFilter diff --git a/test/lib/docs/filters/core/normalize_urls_test.rb b/test/lib/docs/filters/core/normalize_urls_test.rb index 804b05b159..0122c8fcc7 100644 --- a/test/lib/docs/filters/core/normalize_urls_test.rb +++ b/test/lib/docs/filters/core/normalize_urls_test.rb @@ -1,7 +1,7 @@ require_relative '../../../../test_helper' require_relative '../../../../../lib/docs' -class NormalizeUrlsFilterTest < MiniTest::Spec +class NormalizeUrlsFilterTest < Minitest::Spec include FilterTestHelper self.filter_class = Docs::NormalizeUrlsFilter diff --git a/test/lib/docs/filters/core/parse_cf_email_test.rb b/test/lib/docs/filters/core/parse_cf_email_test.rb index d6124fedac..7f2485822b 100644 --- a/test/lib/docs/filters/core/parse_cf_email_test.rb +++ b/test/lib/docs/filters/core/parse_cf_email_test.rb @@ -1,7 +1,7 @@ require_relative '../../../../test_helper' require_relative '../../../../../lib/docs' -class ParseCfEmailFilterTest < MiniTest::Spec +class ParseCfEmailFilterTest < Minitest::Spec include FilterTestHelper self.filter_class = Docs::ParseCfEmailFilter diff --git a/test/lib/docs/filters/core/title_test.rb b/test/lib/docs/filters/core/title_test.rb index 9a28bd3bee..24189555a0 100644 --- a/test/lib/docs/filters/core/title_test.rb +++ b/test/lib/docs/filters/core/title_test.rb @@ -1,7 +1,7 @@ require_relative '../../../../test_helper' require_relative '../../../../../lib/docs' -class TitleFilterTest < MiniTest::Spec +class TitleFilterTest < Minitest::Spec include FilterTestHelper self.filter_class = Docs::TitleFilter diff --git a/test/lib/docs/storage/abstract_store_test.rb b/test/lib/docs/storage/abstract_store_test.rb index d63e95f226..2bdf391e70 100644 --- a/test/lib/docs/storage/abstract_store_test.rb +++ b/test/lib/docs/storage/abstract_store_test.rb @@ -1,7 +1,7 @@ require_relative '../../../test_helper' require_relative '../../../../lib/docs' -class DocsAbstractStoreTest < MiniTest::Spec +class DocsAbstractStoreTest < Minitest::Spec InvalidPathError = Docs::AbstractStore::InvalidPathError LockError = Docs::AbstractStore::LockError diff --git a/test/lib/docs/storage/file_store_test.rb b/test/lib/docs/storage/file_store_test.rb index aa3ce3f8ec..1068b7d745 100644 --- a/test/lib/docs/storage/file_store_test.rb +++ b/test/lib/docs/storage/file_store_test.rb @@ -1,7 +1,7 @@ require_relative '../../../test_helper' require_relative '../../../../lib/docs' -class DocsFileStoreTest < MiniTest::Spec +class DocsFileStoreTest < Minitest::Spec let :store do Docs::FileStore.new(tmp_path) end diff --git a/test/test_helper.rb b/test/test_helper.rb index 2bcaa9d543..8ae02364c6 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -18,7 +18,7 @@ ActiveSupport::TestCase.test_order = :random -class MiniTest::Spec +class Minitest::Spec include ActiveSupport::Testing::Assertions module DSL From 7049a8316a61ec304b3a9731ae0bfd81afad0b00 Mon Sep 17 00:00:00 2001 From: Nicholas La Roux Date: Wed, 3 Jan 2024 20:12:16 +0900 Subject: [PATCH 0103/1110] Update Ruby, Rails, and Minitest docs --- lib/docs/scrapers/rdoc/minitest.rb | 2 +- lib/docs/scrapers/rdoc/rails.rb | 6 +++++- lib/docs/scrapers/rdoc/ruby.rb | 12 ++++++++---- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/docs/scrapers/rdoc/minitest.rb b/lib/docs/scrapers/rdoc/minitest.rb index 884dc0a65b..2ff12cf496 100644 --- a/lib/docs/scrapers/rdoc/minitest.rb +++ b/lib/docs/scrapers/rdoc/minitest.rb @@ -8,7 +8,7 @@ class Minitest < Rdoc self.name = 'Ruby / Minitest' self.slug = 'minitest' - self.release = '5.17.0' + self.release = '5.20.0' self.links = { code: 'https://github.com/minitest/minitest' } diff --git a/lib/docs/scrapers/rdoc/rails.rb b/lib/docs/scrapers/rdoc/rails.rb index 68381b7984..03c9e4f2d5 100644 --- a/lib/docs/scrapers/rdoc/rails.rb +++ b/lib/docs/scrapers/rdoc/rails.rb @@ -75,8 +75,12 @@ class Rails < Rdoc end end + version '7.1' do + self.release = '7.1.2' + end + version '7.0' do - self.release = '7.0.4' + self.release = '7.0.8' end version '6.1' do diff --git a/lib/docs/scrapers/rdoc/ruby.rb b/lib/docs/scrapers/rdoc/ruby.rb index 2850061a5f..eef26f4182 100644 --- a/lib/docs/scrapers/rdoc/ruby.rb +++ b/lib/docs/scrapers/rdoc/ruby.rb @@ -69,16 +69,20 @@ class Ruby < Rdoc Licensed under their own licenses. HTML + version '3.3' do + self.release = '3.3.0' + end + version '3.2' do - self.release = '3.2.0' + self.release = '3.2.2' end - + version '3.1' do - self.release = '3.1.3' + self.release = '3.1.4' end version '3' do - self.release = '3.0.0' + self.release = '3.0.6' end version '2.7' do From 4862e15775c45cc7a63fa8d1f048e0952405bdce Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Fri, 5 Jan 2024 17:54:31 +0100 Subject: [PATCH 0104/1110] Update Python documentation (3.12.1) --- lib/docs/scrapers/python.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/docs/scrapers/python.rb b/lib/docs/scrapers/python.rb index 52b2505fd6..3988bba3b0 100644 --- a/lib/docs/scrapers/python.rb +++ b/lib/docs/scrapers/python.rb @@ -28,14 +28,14 @@ class Python < FileScraper HTML version '3.12' do - self.release = '3.12.0' + self.release = '3.12.1' self.base_url = "https://docs.python.org/#{self.version}/" html_filters.push 'python/entries_v3', 'sphinx/clean_html', 'python/clean_html' end version '3.11' do - self.release = '3.11.5' + self.release = '3.11.7' self.base_url = "https://docs.python.org/#{self.version}/" html_filters.push 'python/entries_v3', 'sphinx/clean_html', 'python/clean_html' From 9d0d3ec933ae2cdca8165b9f259215ba7d01b083 Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Fri, 5 Jan 2024 19:33:59 +0100 Subject: [PATCH 0105/1110] Update Go documentation (1.21.5) --- lib/docs/scrapers/go.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/docs/scrapers/go.rb b/lib/docs/scrapers/go.rb index d811f3c444..c94a5455fb 100644 --- a/lib/docs/scrapers/go.rb +++ b/lib/docs/scrapers/go.rb @@ -1,7 +1,7 @@ module Docs class Go < UrlScraper self.type = 'go' - self.release = '1.21.0' + self.release = '1.21.5' self.base_url = 'https://golang.org/pkg/' self.links = { home: 'https://golang.org/', @@ -10,10 +10,10 @@ class Go < UrlScraper # Run godoc locally, since https://golang.org/pkg/ redirects to https://pkg.go.dev/std with rate limiting / scraping protection. - # docker run --expose=6060 --rm -it docker.io/golang:1.18.0 - #docker# go install golang.org/x/tools/cmd/godoc@latest - #docker# rm -r /usr/local/go/test/ - #docker# godoc -http 0.0.0.0:6060 -v + # podman run --net host --rm -it docker.io/golang:1.21.5 + #podman# go install golang.org/x/tools/cmd/godoc@latest + #podman# rm -r /usr/local/go/test/ + #podman# godoc -http 0.0.0.0:6060 -v self.base_url = 'http://localhost:6060/pkg/' html_filters.push 'clean_local_urls' From dbf59d21382e92b014b9e42802d65804ea0385da Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Fri, 5 Jan 2024 20:09:49 +0100 Subject: [PATCH 0106/1110] file-scrapers: update rails --- docs/file-scrapers.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/file-scrapers.md b/docs/file-scrapers.md index 8c2741dae9..adf084fc14 100644 --- a/docs/file-scrapers.md +++ b/docs/file-scrapers.md @@ -242,11 +242,12 @@ done ### Ruby / Minitest ### Ruby on Rails * Download a release at https://github.com/rails/rails/releases or clone https://github.com/rails/rails.git (checkout to the branch of the rails' version that is going to be scraped) -* Open "railties/lib/rails/api/task.rb" and comment out any code related to sdoc ("configure_sdoc") -* Run "bundle install --without db && bundle exec rake rdoc" (in the Rails directory) -* Run "cd guides && bundle exec rake guides:generate:html" -* Copy the "guides/output" directory to "html/guides" -* Copy the "html" directory to "docs/rails~[version]" +* Open `railties/lib/rails/api/task.rb` and comment out any code related to sdoc (`configure_sdoc`) +* Run `bundle config set --local without 'db job'` (in the Rails directory) +* Run `bundle install && bundle exec rake rdoc` (in the Rails directory) +* Run `cd guides && bundle exec rake guides:generate:html` +* Copy the `guides/output` directory to `html/guides` +* Copy the `html` directory to `docs/rails~[version]` ### Ruby Download the tarball of Ruby from https://www.ruby-lang.org/en/downloads/, extract it, run From c9dd0ed106920f05e78cec3745d580ffdbc8382e Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Fri, 5 Jan 2024 20:10:29 +0100 Subject: [PATCH 0107/1110] rails: fix get_name --- lib/docs/filters/rails/entries.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/docs/filters/rails/entries.rb b/lib/docs/filters/rails/entries.rb index 0b29a4afbb..343f85952f 100644 --- a/lib/docs/filters/rails/entries.rb +++ b/lib/docs/filters/rails/entries.rb @@ -40,7 +40,7 @@ class EntriesFilter < Docs::Rdoc::EntriesFilter def get_name if slug.start_with?('guides') - name = at_css('#feature h2').content.strip + name = at_css('#mainCol h2').content.strip name.remove! %r{\s\(.+\)\z} return name end From 0bb3f3779f1d8f6d2ddd8e88c6a0172d21034bb5 Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Fri, 5 Jan 2024 20:15:27 +0100 Subject: [PATCH 0108/1110] bazel: CSS styling for article > h1 --- assets/stylesheets/components/_page.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assets/stylesheets/components/_page.scss b/assets/stylesheets/components/_page.scss index e3c735dcc0..900ed6050e 100644 --- a/assets/stylesheets/components/_page.scss +++ b/assets/stylesheets/components/_page.scss @@ -9,11 +9,13 @@ &._page-error { position: static; } > h1, + > article > h1, > header > h1, > section > h1 { @extend ._lined-heading; } > h1:first-child, + > article:first-of-type > h1, > header:first-of-type > h1, > section:first-of-type > h1 { margin-top: 0; From 0e10f2462467ca91c33cb7d3e3bf93b70ef2f45f Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Fri, 5 Jan 2024 20:17:05 +0100 Subject: [PATCH 0109/1110] bazel: add news entry --- assets/javascripts/news.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/assets/javascripts/news.json b/assets/javascripts/news.json index dfe15ffcb8..a0a009fae2 100644 --- a/assets/javascripts/news.json +++ b/assets/javascripts/news.json @@ -1,4 +1,8 @@ [ + [ + "2024-01-05", + "New documentation: Bazel" + ], [ "2023-10-09", "New documentations: hapi, joi, Nushell, Varnish" From 8697de85672a5bf572cb7b897edbee57d60b585d Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Fri, 5 Jan 2024 20:30:27 +0100 Subject: [PATCH 0110/1110] thor updates: fix Terminal::Table from terminal-table The newest Thor also contains a Terminal module, causing "uninitialized constant Thor::Shell::Terminal::Table (NameError)" --- lib/tasks/updates.thor | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/tasks/updates.thor b/lib/tasks/updates.thor index b3fb28bb9e..48486b9f20 100644 --- a/lib/tasks/updates.thor +++ b/lib/tasks/updates.thor @@ -123,7 +123,7 @@ class UpdatesCLI < Thor headings = ['Documentation', 'Scraper version', 'Latest version'] rows = results.map {|result| [result[:name], result[:scraper_version], result[:latest_version]]} - table = Terminal::Table.new :title => title, :headings => headings, :rows => rows + table = ::Terminal::Table.new :title => title, :headings => headings, :rows => rows puts table end @@ -132,7 +132,7 @@ class UpdatesCLI < Thor headings = %w(Documentation Reason) rows = results.map {|result| [result[:name], result[:error]]} - table = Terminal::Table.new :title => title, :headings => headings, :rows => rows + table = ::Terminal::Table.new :title => title, :headings => headings, :rows => rows puts table end From 6b1bbd6e9f44512bbdf7c89350919707a00d3dd9 Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Fri, 5 Jan 2024 20:51:06 +0100 Subject: [PATCH 0111/1110] Update SQLite documentation (3.44.2) --- lib/docs/filters/sqlite/clean_html.rb | 1 + lib/docs/scrapers/sqlite.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/docs/filters/sqlite/clean_html.rb b/lib/docs/filters/sqlite/clean_html.rb index 2f7fb46c89..ae74228431 100644 --- a/lib/docs/filters/sqlite/clean_html.rb +++ b/lib/docs/filters/sqlite/clean_html.rb @@ -88,6 +88,7 @@ def call end css('svg *[style], svg *[fill]').each do |node| + next if slug == 'geopoly' # transform style in SVG diagrams, e.g. on https://sqlite.org/lang_insert.html if node['style'] == 'fill:rgb(0,0,0)' or node['fill'] == 'rgb(0,0,0)' node.add_class('fill') diff --git a/lib/docs/scrapers/sqlite.rb b/lib/docs/scrapers/sqlite.rb index 92521b1ae1..062d907c0c 100644 --- a/lib/docs/scrapers/sqlite.rb +++ b/lib/docs/scrapers/sqlite.rb @@ -2,7 +2,7 @@ module Docs class Sqlite < UrlScraper self.name = 'SQLite' self.type = 'sqlite' - self.release = '3.43.0' + self.release = '3.44.2' self.base_url = 'https://sqlite.org/' self.root_path = 'docs.html' self.initial_paths = %w(keyword_index.html) From 667a75467af79ed924b9c6587d67b001e9e5e043 Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Fri, 5 Jan 2024 21:01:56 +0100 Subject: [PATCH 0112/1110] Update Vue documentation (3.4.5) --- lib/docs/scrapers/vue.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/docs/scrapers/vue.rb b/lib/docs/scrapers/vue.rb index 53f0723ac4..7fac3ce64e 100644 --- a/lib/docs/scrapers/vue.rb +++ b/lib/docs/scrapers/vue.rb @@ -19,7 +19,7 @@ class Vue < UrlScraper HTML version '3' do - self.release = '3.3.4' + self.release = '3.4.5' self.base_url = 'https://vuejs.org/' self.initial_paths = %w(guide/introduction.html) html_filters.push 'vue/entries_v3', 'vue/clean_html' From 88544ab3023cb65b5f0b00b041e774377992c587 Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Fri, 5 Jan 2024 21:10:36 +0100 Subject: [PATCH 0113/1110] Update PHP documentation (8.3) --- docs/file-scrapers.md | 3 +-- lib/docs/scrapers/php.rb | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/file-scrapers.md b/docs/file-scrapers.md index adf084fc14..621a66c00b 100644 --- a/docs/file-scrapers.md +++ b/docs/file-scrapers.md @@ -193,8 +193,7 @@ Click the link under the "Many HTML files" column on https://www.php.net/downloa Or run the following commands in your terminal: ```sh -curl https://www.php.net/distributions/manual/php_manual_en.tar.gz > php.tar; \ -tar -xf php.tar; mv php-chunked-xhtml/ docs/php/ +curl https://www.php.net/distributions/manual/php_manual_en.tar.gz | tar xz; mv php-chunked-xhtml/ docs/php/ ``` ## Python 3.6+ diff --git a/lib/docs/scrapers/php.rb b/lib/docs/scrapers/php.rb index 5c02d6c5f2..6d38c70c22 100644 --- a/lib/docs/scrapers/php.rb +++ b/lib/docs/scrapers/php.rb @@ -5,7 +5,7 @@ class Php < FileScraper self.name = 'PHP' self.type = 'php' - self.release = '8.2' + self.release = '8.3' self.base_url = 'https://www.php.net/manual/en/' self.root_path = 'index.html' self.initial_paths = %w( @@ -62,7 +62,7 @@ class Php < FileScraper options[:skip_patterns] = [/mysqlnd/, /xdevapi/i] options[:attribution] = <<-HTML - © 1997–2022 The PHP Documentation Group
+ © 1997–2023 The PHP Documentation Group
Licensed under the Creative Commons Attribution License v3.0 or later. HTML From a80293ae9878723e6a8f9fbf30e58384348ff49e Mon Sep 17 00:00:00 2001 From: Simon LegnerDate: Fri, 5 Jan 2024 21:46:02 +0100 Subject: [PATCH 0114/1110] Update OpenJDK documentation (21) --- docs/file-scrapers.md | 10 +++++----- lib/docs/scrapers/openjdk.rb | 18 ++++-------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/docs/file-scrapers.md b/docs/file-scrapers.md index 621a66c00b..478099b4c2 100644 --- a/docs/file-scrapers.md +++ b/docs/file-scrapers.md @@ -166,11 +166,11 @@ Search 'Openjdk' in https://www.debian.org/distrib/packages, find the `openjdk-$ download it, extract it with `dpkg -x $PACKAGE ./` and move `./usr/share/doc/openjdk-16-jre-headless/api/` to `path/to/devdocs/docs/openjdk~$VERSION` -``` -curl http://ftp.at.debian.org/debian/pool/main/o/openjdk-19/openjdk-19-doc_19+36-2_all.deb && -tar xf openjdk-19-doc_19+36-2_all.deb +```sh +curl -O http://ftp.at.debian.org/debian/pool/main/o/openjdk-21/openjdk-21-doc_21.0.1+12-3_all.deb +tar xf openjdk-21-doc_21.0.1+12-3_all.deb tar xf data.tar.xz -mv ./usr/share/doc/openjdk-19-jre-headless/api/ path/to/devdocs/docs/openjdk~$VERSION +mv ./usr/share/doc/openjdk-21-jre-headless/api/ docs/openjdk~$VERSION ``` If you use or have access to a Debian-based GNU/Linux distribution you can run the following command: @@ -178,7 +178,7 @@ If you use or have access to a Debian-based GNU/Linux distribution you can run t apt download openjdk-$VERSION-doc dpkg -x $PACKAGE ./ # previous command makes a directory called 'usr' in the current directory -mv ./usr/share/doc/openjdk-16-jre-headless/api/ path/to/devdocs/docs/openjdk~$VERSION +mv ./usr/share/doc/openjdk-16-jre-headless/api/ docs/openjdk~$VERSION ``` ## Pandas diff --git a/lib/docs/scrapers/openjdk.rb b/lib/docs/scrapers/openjdk.rb index 2ede8d5dda..d96f96617b 100644 --- a/lib/docs/scrapers/openjdk.rb +++ b/lib/docs/scrapers/openjdk.rb @@ -28,7 +28,7 @@ class Openjdk < FileScraper ] options[:attribution] = <<-HTML - © 1993, 2022, Oracle and/or its affiliates. All rights reserved.
+ © 1993, 2023, Oracle and/or its affiliates. All rights reserved.
Documentation extracted from Debian's OpenJDK Development Kit package.
Licensed under the GNU General Public License, version 2, with the Classpath Exception.
Various third party code in OpenJDK is licensed under different licenses (see Debian package).
@@ -37,20 +37,10 @@ class Openjdk < FileScraper NEWFILTERS = ['openjdk/entries_new', 'openjdk/clean_html_new'] - version '19' do - self.release = '19' + version '21' do + self.release = '21' self.root_path = 'index.html' - self.base_url = 'https://docs.oracle.com/en/java/javase/19/docs/api/' - - html_filters.push NEWFILTERS - - options[:container] = 'main' - end - - version '18' do - self.release = '18' - self.root_path = 'index.html' - self.base_url = 'https://docs.oracle.com/en/java/javase/18/docs/api/' + self.base_url = 'https://docs.oracle.com/en/java/javase/21/docs/api/' html_filters.push NEWFILTERS From fd086dd55fca58e7a3e3de05af85de3594ffba67 Mon Sep 17 00:00:00 2001 From: Simon LegnerDate: Sat, 6 Jan 2024 09:39:05 +0100 Subject: [PATCH 0115/1110] schedule-doc-report: add workflow_dispatch https://docs.github.com/en/actions/using-workflows/manually-running-a-workflow --- .github/workflows/schedule-doc-report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/schedule-doc-report.yml b/.github/workflows/schedule-doc-report.yml index 005666405e..386683d70e 100644 --- a/.github/workflows/schedule-doc-report.yml +++ b/.github/workflows/schedule-doc-report.yml @@ -2,6 +2,7 @@ name: Generate documentation versions report on: schedule: - cron: '17 4 1 * *' + workflow_dispatch: jobs: report: From a8e7fb78d8d3591a2d6b55596500a48f6ad55e45 Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Sat, 6 Jan 2024 09:45:39 +0100 Subject: [PATCH 0116/1110] Update Axios documentation (1.6.5) --- lib/docs/scrapers/axios.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/docs/scrapers/axios.rb b/lib/docs/scrapers/axios.rb index 13d7802873..0a78e940a5 100755 --- a/lib/docs/scrapers/axios.rb +++ b/lib/docs/scrapers/axios.rb @@ -5,7 +5,7 @@ class Axios < UrlScraper home: 'hthttps://axios-http.com/', code: 'https://github.com/axios/axios' } - self.release = '1.4.0' + self.release = '1.6.5' self.base_url = "https://axios-http.com/docs/" self.initial_paths = %w(index intro) From 880202d6a60a6baa1022fdd36b697648b5d7e266 Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Sat, 6 Jan 2024 09:57:01 +0100 Subject: [PATCH 0117/1110] Update Crystal documentation (1.10.1) --- lib/docs/scrapers/crystal.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/docs/scrapers/crystal.rb b/lib/docs/scrapers/crystal.rb index 5c4a782e79..fefe62d66e 100644 --- a/lib/docs/scrapers/crystal.rb +++ b/lib/docs/scrapers/crystal.rb @@ -2,7 +2,7 @@ module Docs class Crystal < UrlScraper include MultipleBaseUrls self.type = 'crystal' - self.release = '1.9.2' + self.release = '1.10.1' self.base_urls = [ "https://crystal-lang.org/api/#{release}/", "https://crystal-lang.org/reference/#{release[0..2]}/", From 85798a8d420f4185cc5953fcefc3d5ec216204ad Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Sat, 6 Jan 2024 09:57:04 +0100 Subject: [PATCH 0118/1110] Update Vite documentation (5.0.11) --- lib/docs/filters/vite/entries.rb | 2 +- lib/docs/scrapers/vite.rb | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/docs/filters/vite/entries.rb b/lib/docs/filters/vite/entries.rb index db28ea7c40..606a19bb17 100644 --- a/lib/docs/filters/vite/entries.rb +++ b/lib/docs/filters/vite/entries.rb @@ -8,7 +8,7 @@ def get_name end def get_type - at_css('aside nav .is-active').content.strip + name end def additional_entries diff --git a/lib/docs/scrapers/vite.rb b/lib/docs/scrapers/vite.rb index ff82da017b..46309fa742 100644 --- a/lib/docs/scrapers/vite.rb +++ b/lib/docs/scrapers/vite.rb @@ -15,16 +15,22 @@ class Vite < UrlScraper Licensed under the MIT License. HTML - options[:skip] = %w(team.html plugins/) + options[:skip] = %w(team.html team) + options[:skip_patterns] = [/\Ablog/, /\Aplugins/] self.initial_paths = %w(guide/) html_filters.push 'vite/entries', 'vite/clean_html' version do - self.release = '4.4.8' + self.release = '5.0.11' self.base_url = 'https://vitejs.dev/' end + version '4' do + self.release = '4.5.1' + self.base_url = 'https://v4.vitejs.dev/' + end + version '3' do self.release = '3.2.5' self.base_url = 'https://v3.vitejs.dev/' From 263c8a57f685822ffd3786a3794446e1bf663d86 Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Sat, 6 Jan 2024 09:58:27 +0100 Subject: [PATCH 0119/1110] Update Fish documentation (3.7.0) --- lib/docs/scrapers/fish.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/docs/scrapers/fish.rb b/lib/docs/scrapers/fish.rb index bf2f09dbf8..8e9c3326e0 100644 --- a/lib/docs/scrapers/fish.rb +++ b/lib/docs/scrapers/fish.rb @@ -16,6 +16,14 @@ class Fish < UrlScraper Licensed under the GNU General Public License, version 2. HTML + version '3.7' do + self.release = '3.7.0' + self.base_url = "https://fishshell.com/docs/#{version}/" + + options[:skip].concat %w(genindex.html relnotes.html) + html_filters.push 'sphinx/clean_html', 'fish/clean_html_sphinx', 'fish/entries_sphinx' + end + version '3.6' do self.release = '3.6.0' self.base_url = "https://fishshell.com/docs/#{version}/" From 9d538b4aace797fa782efccf0bbd2109261179a6 Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Sat, 6 Jan 2024 10:01:05 +0100 Subject: [PATCH 0120/1110] Update Julia documentation (1.10.0.2) --- lib/docs/scrapers/julia.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/docs/scrapers/julia.rb b/lib/docs/scrapers/julia.rb index 97a5dc579c..c31cdaea90 100644 --- a/lib/docs/scrapers/julia.rb +++ b/lib/docs/scrapers/julia.rb @@ -11,6 +11,17 @@ class Julia < UrlScraper Licensed under the MIT License. HTML + version '1.10' do + self.release = '1.10.0.2' + self.base_url = "https://docs.julialang.org/en/v#{version}/" + self.type = 'julia' + + html_filters.push 'julia/entries', 'julia/clean_html' + + options[:container] = '.docs-main' + options[:only_patterns] = [/\Amanual\//, /\Abase\//, /\Astdlib\//] + end + version '1.9' do self.release = '1.9.2' self.base_url = "https://docs.julialang.org/en/v#{version}/" From 9bf2311eced479ea1d0b8e3d4ff6dfe17a445ebe Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Sat, 6 Jan 2024 10:16:37 +0100 Subject: [PATCH 0121/1110] Update Angular documentation (17.0.8) --- lib/docs/filters/angular/clean_html.rb | 5 +++++ lib/docs/scrapers/angular.rb | 10 ++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/docs/filters/angular/clean_html.rb b/lib/docs/filters/angular/clean_html.rb index 751127105b..562b6d259e 100644 --- a/lib/docs/filters/angular/clean_html.rb +++ b/lib/docs/filters/angular/clean_html.rb @@ -94,6 +94,11 @@ def call node.before(node.children).remove end + css('details.overloads > summary').each do |node| + node.css('.actions').remove + node.content = node.content + end + doc end end diff --git a/lib/docs/scrapers/angular.rb b/lib/docs/scrapers/angular.rb index 0a85b9c845..dcbdd0ea70 100644 --- a/lib/docs/scrapers/angular.rb +++ b/lib/docs/scrapers/angular.rb @@ -83,14 +83,20 @@ def url_for(path) end version do - self.release = '16.1.3' + self.release = '17.0.8' self.base_url = 'https://angular.io/' include Docs::Angular::Since12 end + version '16' do + self.release = '16.2.12' + self.base_url = 'https://v16.angular.io/' + include Docs::Angular::Since12 + end + version '15' do self.release = '15.2.9' - self.base_url = 'https://angular.io/' + self.base_url = 'https://v15.angular.io/' include Docs::Angular::Since12 end From 43ddae5560e491dd4ba3922a58f8ae03174b9824 Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Sat, 6 Jan 2024 10:25:52 +0100 Subject: [PATCH 0122/1110] Update esbuild documentation (0.19.11) --- lib/docs/scrapers/esbuild.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/docs/scrapers/esbuild.rb b/lib/docs/scrapers/esbuild.rb index 9250b4fa70..e0b6c861aa 100644 --- a/lib/docs/scrapers/esbuild.rb +++ b/lib/docs/scrapers/esbuild.rb @@ -16,7 +16,7 @@ class Esbuild < UrlScraper Licensed under the MIT License. HTML - self.release = '0.18.17' + self.release = '0.19.11' self.base_url = 'https://esbuild.github.io/' html_filters.push 'esbuild/clean_html', 'esbuild/entries' From 32443faf2828b4ace5cb14a8af3731ee8bbf324c Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Sat, 6 Jan 2024 10:26:23 +0100 Subject: [PATCH 0123/1110] Update Vitest documentation (1.1.3) --- lib/docs/scrapers/vitest.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/docs/scrapers/vitest.rb b/lib/docs/scrapers/vitest.rb index 199e9c9af8..820b9eef80 100644 --- a/lib/docs/scrapers/vitest.rb +++ b/lib/docs/scrapers/vitest.rb @@ -16,7 +16,7 @@ class Vitest < UrlScraper Licensed under the MIT License. HTML - self.release = '0.32.4' + self.release = '1.1.3' self.base_url = 'https://vitest.dev/' self.initial_paths = %w(guide/) html_filters.push 'vitest/entries', 'vite/clean_html' From 93cc8b3978911b60091d339a3825c72295813260 Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Sat, 6 Jan 2024 10:42:10 +0100 Subject: [PATCH 0124/1110] Update Rust documentation (1.75.0) --- lib/docs/scrapers/rust.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/docs/scrapers/rust.rb b/lib/docs/scrapers/rust.rb index 5c2b1f10c9..536aaefd9f 100644 --- a/lib/docs/scrapers/rust.rb +++ b/lib/docs/scrapers/rust.rb @@ -3,7 +3,7 @@ module Docs class Rust < UrlScraper self.type = 'rust' - self.release = '1.73.0' + self.release = '1.75.0' self.base_url = 'https://doc.rust-lang.org/' self.root_path = 'book/index.html' self.initial_paths = %w( From bf89445d87b720d2b35a358f483d5210957d12e0 Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Sat, 6 Jan 2024 11:03:52 +0100 Subject: [PATCH 0125/1110] Update npm documentation (10.2.5) --- lib/docs/filters/npm/clean_html.rb | 16 +++++----------- lib/docs/filters/npm/entries.rb | 3 ++- lib/docs/scrapers/npm.rb | 7 +++++-- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/lib/docs/filters/npm/clean_html.rb b/lib/docs/filters/npm/clean_html.rb index ad07c476a2..b951976805 100644 --- a/lib/docs/filters/npm/clean_html.rb +++ b/lib/docs/filters/npm/clean_html.rb @@ -2,18 +2,13 @@ module Docs class Npm class CleanHtmlFilter < Filter def call - - at_css('#___gatsby').before(at_css('h1')) + @doc = at_css('main') css('details').remove - - css('.dZYhXG', '.fONtKn').remove - - css('.kSYjyK').remove - - css('.cDvIaH').remove - - css('.jRndWL').remove_attribute('style') + css('nav[aria-label="Breadcrumbs"]').remove + css('.gtWOdv').remove # Select CLI Version + css('.ezMiXD').remove # Navbox + css('.gOhcvK').remove # Edit this page on GitHub css('pre').each do |node| node.content = node.css('.token-line').map(&:content).join("\n") @@ -21,7 +16,6 @@ def call end doc - end end end diff --git a/lib/docs/filters/npm/entries.rb b/lib/docs/filters/npm/entries.rb index 8291d7a71c..d754b1d4d2 100644 --- a/lib/docs/filters/npm/entries.rb +++ b/lib/docs/filters/npm/entries.rb @@ -7,7 +7,7 @@ def get_name end def get_type - at_css('.active').content + at_css('nav[aria-label="Breadcrumbs"] li').content end def additional_entries @@ -21,6 +21,7 @@ def additional_entries if name == 'package.json' css('h3').each do |node| + next unless node['id'] entries << [node['id'], slug + '#' + node['id'], 'Package.json Settings'] end end diff --git a/lib/docs/scrapers/npm.rb b/lib/docs/scrapers/npm.rb index ce2be86b2a..1cb2c6e2e4 100644 --- a/lib/docs/scrapers/npm.rb +++ b/lib/docs/scrapers/npm.rb @@ -2,7 +2,7 @@ module Docs class Npm < UrlScraper self.name = 'npm' self.type = 'npm' - self.release = '8.3.0' + self.release = '10.2.5' self.base_url = 'https://docs.npmjs.com/' self.force_gzip = true self.links = { @@ -12,7 +12,8 @@ class Npm < UrlScraper html_filters.push 'npm/entries', 'npm/clean_html' - options[:max_image_size] = 130_000 + options[:download_images] = false + # options[:max_image_size] = 130_000 options[:skip] = [ 'all', @@ -31,6 +32,8 @@ class Npm < UrlScraper /\Apolicies/, /cli\/v6/, /cli\/v7/, + /cli\/v8/, + /cli\/v9/, /\/\Z/ # avoid pages with a trailing slash, those pages mess up the entries ] From 6cc430ffc41ed1eaee031858059cb5ec359da939 Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Sat, 6 Jan 2024 11:46:56 +0100 Subject: [PATCH 0126/1110] decaffeinate: Rename app.coffee and 75 other files from .coffee to .js --- assets/javascripts/app/{app.coffee => app.js} | 0 assets/javascripts/app/{db.coffee => db.js} | 0 assets/javascripts/app/{router.coffee => router.js} | 0 assets/javascripts/app/{searcher.coffee => searcher.js} | 0 assets/javascripts/app/{serviceworker.coffee => serviceworker.js} | 0 assets/javascripts/app/{settings.coffee => settings.js} | 0 assets/javascripts/app/{shortcuts.coffee => shortcuts.js} | 0 .../javascripts/app/{update_checker.coffee => update_checker.js} | 0 assets/javascripts/{application.js.coffee => application.js.js} | 0 .../javascripts/collections/{collection.coffee => collection.js} | 0 assets/javascripts/collections/{docs.coffee => docs.js} | 0 assets/javascripts/collections/{entries.coffee => entries.js} | 0 assets/javascripts/collections/{types.coffee => types.js} | 0 assets/javascripts/{debug.js.coffee => debug.js.js} | 0 assets/javascripts/lib/{ajax.coffee => ajax.js} | 0 assets/javascripts/lib/{cookies_store.coffee => cookies_store.js} | 0 assets/javascripts/lib/{events.coffee => events.js} | 0 assets/javascripts/lib/{favicon.coffee => favicon.js} | 0 assets/javascripts/lib/{license.coffee => license.js} | 0 .../lib/{local_storage_store.coffee => local_storage_store.js} | 0 assets/javascripts/lib/{page.coffee => page.js} | 0 assets/javascripts/lib/{util.coffee => util.js} | 0 assets/javascripts/models/{doc.coffee => doc.js} | 0 assets/javascripts/models/{entry.coffee => entry.js} | 0 assets/javascripts/models/{model.coffee => model.js} | 0 assets/javascripts/models/{type.coffee => type.js} | 0 assets/javascripts/templates/{base.coffee => base.js} | 0 assets/javascripts/templates/{error_tmpl.coffee => error_tmpl.js} | 0 .../javascripts/templates/{notice_tmpl.coffee => notice_tmpl.js} | 0 assets/javascripts/templates/{notif_tmpl.coffee => notif_tmpl.js} | 0 .../templates/pages/{about_tmpl.coffee => about_tmpl.js} | 0 .../templates/pages/{help_tmpl.coffee => help_tmpl.js} | 0 .../templates/pages/{offline_tmpl.coffee => offline_tmpl.js} | 0 .../templates/pages/{settings_tmpl.coffee => settings_tmpl.js} | 0 .../templates/pages/{type_tmpl.coffee => type_tmpl.js} | 0 assets/javascripts/templates/{path_tmpl.coffee => path_tmpl.js} | 0 .../templates/{sidebar_tmpl.coffee => sidebar_tmpl.js} | 0 assets/javascripts/templates/{tip_tmpl.coffee => tip_tmpl.js} | 0 assets/javascripts/views/content/{content.coffee => content.js} | 0 .../views/content/{entry_page.coffee => entry_page.js} | 0 .../views/content/{offline_page.coffee => offline_page.js} | 0 .../javascripts/views/content/{root_page.coffee => root_page.js} | 0 .../views/content/{settings_page.coffee => settings_page.js} | 0 .../views/content/{static_page.coffee => static_page.js} | 0 .../javascripts/views/content/{type_page.coffee => type_page.js} | 0 assets/javascripts/views/layout/{document.coffee => document.js} | 0 assets/javascripts/views/layout/{menu.coffee => menu.js} | 0 assets/javascripts/views/layout/{mobile.coffee => mobile.js} | 0 assets/javascripts/views/layout/{path.coffee => path.js} | 0 assets/javascripts/views/layout/{resizer.coffee => resizer.js} | 0 assets/javascripts/views/layout/{settings.coffee => settings.js} | 0 .../javascripts/views/list/{list_focus.coffee => list_focus.js} | 0 assets/javascripts/views/list/{list_fold.coffee => list_fold.js} | 0 .../javascripts/views/list/{list_select.coffee => list_select.js} | 0 .../views/list/{paginated_list.coffee => paginated_list.js} | 0 assets/javascripts/views/misc/{news.coffee => news.js} | 0 assets/javascripts/views/misc/{notice.coffee => notice.js} | 0 assets/javascripts/views/misc/{notif.coffee => notif.js} | 0 assets/javascripts/views/misc/{tip.coffee => tip.js} | 0 assets/javascripts/views/misc/{updates.coffee => updates.js} | 0 assets/javascripts/views/pages/{base.coffee => base.js} | 0 assets/javascripts/views/pages/{hidden.coffee => hidden.js} | 0 assets/javascripts/views/pages/{jquery.coffee => jquery.js} | 0 assets/javascripts/views/pages/{rdoc.coffee => rdoc.js} | 0 assets/javascripts/views/pages/{sqlite.coffee => sqlite.js} | 0 .../views/pages/{support_tables.coffee => support_tables.js} | 0 assets/javascripts/views/search/{search.coffee => search.js} | 0 .../views/search/{search_scope.coffee => search_scope.js} | 0 assets/javascripts/views/sidebar/{doc_list.coffee => doc_list.js} | 0 .../views/sidebar/{doc_picker.coffee => doc_picker.js} | 0 .../views/sidebar/{entry_list.coffee => entry_list.js} | 0 assets/javascripts/views/sidebar/{results.coffee => results.js} | 0 assets/javascripts/views/sidebar/{sidebar.coffee => sidebar.js} | 0 .../views/sidebar/{sidebar_hover.coffee => sidebar_hover.js} | 0 .../javascripts/views/sidebar/{type_list.coffee => type_list.js} | 0 assets/javascripts/views/{view.coffee => view.js} | 0 76 files changed, 0 insertions(+), 0 deletions(-) rename assets/javascripts/app/{app.coffee => app.js} (100%) rename assets/javascripts/app/{db.coffee => db.js} (100%) rename assets/javascripts/app/{router.coffee => router.js} (100%) rename assets/javascripts/app/{searcher.coffee => searcher.js} (100%) rename assets/javascripts/app/{serviceworker.coffee => serviceworker.js} (100%) rename assets/javascripts/app/{settings.coffee => settings.js} (100%) rename assets/javascripts/app/{shortcuts.coffee => shortcuts.js} (100%) rename assets/javascripts/app/{update_checker.coffee => update_checker.js} (100%) rename assets/javascripts/{application.js.coffee => application.js.js} (100%) rename assets/javascripts/collections/{collection.coffee => collection.js} (100%) rename assets/javascripts/collections/{docs.coffee => docs.js} (100%) rename assets/javascripts/collections/{entries.coffee => entries.js} (100%) rename assets/javascripts/collections/{types.coffee => types.js} (100%) rename assets/javascripts/{debug.js.coffee => debug.js.js} (100%) rename assets/javascripts/lib/{ajax.coffee => ajax.js} (100%) rename assets/javascripts/lib/{cookies_store.coffee => cookies_store.js} (100%) rename assets/javascripts/lib/{events.coffee => events.js} (100%) rename assets/javascripts/lib/{favicon.coffee => favicon.js} (100%) rename assets/javascripts/lib/{license.coffee => license.js} (100%) rename assets/javascripts/lib/{local_storage_store.coffee => local_storage_store.js} (100%) rename assets/javascripts/lib/{page.coffee => page.js} (100%) rename assets/javascripts/lib/{util.coffee => util.js} (100%) rename assets/javascripts/models/{doc.coffee => doc.js} (100%) rename assets/javascripts/models/{entry.coffee => entry.js} (100%) rename assets/javascripts/models/{model.coffee => model.js} (100%) rename assets/javascripts/models/{type.coffee => type.js} (100%) rename assets/javascripts/templates/{base.coffee => base.js} (100%) rename assets/javascripts/templates/{error_tmpl.coffee => error_tmpl.js} (100%) rename assets/javascripts/templates/{notice_tmpl.coffee => notice_tmpl.js} (100%) rename assets/javascripts/templates/{notif_tmpl.coffee => notif_tmpl.js} (100%) rename assets/javascripts/templates/pages/{about_tmpl.coffee => about_tmpl.js} (100%) rename assets/javascripts/templates/pages/{help_tmpl.coffee => help_tmpl.js} (100%) rename assets/javascripts/templates/pages/{offline_tmpl.coffee => offline_tmpl.js} (100%) rename assets/javascripts/templates/pages/{settings_tmpl.coffee => settings_tmpl.js} (100%) rename assets/javascripts/templates/pages/{type_tmpl.coffee => type_tmpl.js} (100%) rename assets/javascripts/templates/{path_tmpl.coffee => path_tmpl.js} (100%) rename assets/javascripts/templates/{sidebar_tmpl.coffee => sidebar_tmpl.js} (100%) rename assets/javascripts/templates/{tip_tmpl.coffee => tip_tmpl.js} (100%) rename assets/javascripts/views/content/{content.coffee => content.js} (100%) rename assets/javascripts/views/content/{entry_page.coffee => entry_page.js} (100%) rename assets/javascripts/views/content/{offline_page.coffee => offline_page.js} (100%) rename assets/javascripts/views/content/{root_page.coffee => root_page.js} (100%) rename assets/javascripts/views/content/{settings_page.coffee => settings_page.js} (100%) rename assets/javascripts/views/content/{static_page.coffee => static_page.js} (100%) rename assets/javascripts/views/content/{type_page.coffee => type_page.js} (100%) rename assets/javascripts/views/layout/{document.coffee => document.js} (100%) rename assets/javascripts/views/layout/{menu.coffee => menu.js} (100%) rename assets/javascripts/views/layout/{mobile.coffee => mobile.js} (100%) rename assets/javascripts/views/layout/{path.coffee => path.js} (100%) rename assets/javascripts/views/layout/{resizer.coffee => resizer.js} (100%) rename assets/javascripts/views/layout/{settings.coffee => settings.js} (100%) rename assets/javascripts/views/list/{list_focus.coffee => list_focus.js} (100%) rename assets/javascripts/views/list/{list_fold.coffee => list_fold.js} (100%) rename assets/javascripts/views/list/{list_select.coffee => list_select.js} (100%) rename assets/javascripts/views/list/{paginated_list.coffee => paginated_list.js} (100%) rename assets/javascripts/views/misc/{news.coffee => news.js} (100%) rename assets/javascripts/views/misc/{notice.coffee => notice.js} (100%) rename assets/javascripts/views/misc/{notif.coffee => notif.js} (100%) rename assets/javascripts/views/misc/{tip.coffee => tip.js} (100%) rename assets/javascripts/views/misc/{updates.coffee => updates.js} (100%) rename assets/javascripts/views/pages/{base.coffee => base.js} (100%) rename assets/javascripts/views/pages/{hidden.coffee => hidden.js} (100%) rename assets/javascripts/views/pages/{jquery.coffee => jquery.js} (100%) rename assets/javascripts/views/pages/{rdoc.coffee => rdoc.js} (100%) rename assets/javascripts/views/pages/{sqlite.coffee => sqlite.js} (100%) rename assets/javascripts/views/pages/{support_tables.coffee => support_tables.js} (100%) rename assets/javascripts/views/search/{search.coffee => search.js} (100%) rename assets/javascripts/views/search/{search_scope.coffee => search_scope.js} (100%) rename assets/javascripts/views/sidebar/{doc_list.coffee => doc_list.js} (100%) rename assets/javascripts/views/sidebar/{doc_picker.coffee => doc_picker.js} (100%) rename assets/javascripts/views/sidebar/{entry_list.coffee => entry_list.js} (100%) rename assets/javascripts/views/sidebar/{results.coffee => results.js} (100%) rename assets/javascripts/views/sidebar/{sidebar.coffee => sidebar.js} (100%) rename assets/javascripts/views/sidebar/{sidebar_hover.coffee => sidebar_hover.js} (100%) rename assets/javascripts/views/sidebar/{type_list.coffee => type_list.js} (100%) rename assets/javascripts/views/{view.coffee => view.js} (100%) diff --git a/assets/javascripts/app/app.coffee b/assets/javascripts/app/app.js similarity index 100% rename from assets/javascripts/app/app.coffee rename to assets/javascripts/app/app.js diff --git a/assets/javascripts/app/db.coffee b/assets/javascripts/app/db.js similarity index 100% rename from assets/javascripts/app/db.coffee rename to assets/javascripts/app/db.js diff --git a/assets/javascripts/app/router.coffee b/assets/javascripts/app/router.js similarity index 100% rename from assets/javascripts/app/router.coffee rename to assets/javascripts/app/router.js diff --git a/assets/javascripts/app/searcher.coffee b/assets/javascripts/app/searcher.js similarity index 100% rename from assets/javascripts/app/searcher.coffee rename to assets/javascripts/app/searcher.js diff --git a/assets/javascripts/app/serviceworker.coffee b/assets/javascripts/app/serviceworker.js similarity index 100% rename from assets/javascripts/app/serviceworker.coffee rename to assets/javascripts/app/serviceworker.js diff --git a/assets/javascripts/app/settings.coffee b/assets/javascripts/app/settings.js similarity index 100% rename from assets/javascripts/app/settings.coffee rename to assets/javascripts/app/settings.js diff --git a/assets/javascripts/app/shortcuts.coffee b/assets/javascripts/app/shortcuts.js similarity index 100% rename from assets/javascripts/app/shortcuts.coffee rename to assets/javascripts/app/shortcuts.js diff --git a/assets/javascripts/app/update_checker.coffee b/assets/javascripts/app/update_checker.js similarity index 100% rename from assets/javascripts/app/update_checker.coffee rename to assets/javascripts/app/update_checker.js diff --git a/assets/javascripts/application.js.coffee b/assets/javascripts/application.js.js similarity index 100% rename from assets/javascripts/application.js.coffee rename to assets/javascripts/application.js.js diff --git a/assets/javascripts/collections/collection.coffee b/assets/javascripts/collections/collection.js similarity index 100% rename from assets/javascripts/collections/collection.coffee rename to assets/javascripts/collections/collection.js diff --git a/assets/javascripts/collections/docs.coffee b/assets/javascripts/collections/docs.js similarity index 100% rename from assets/javascripts/collections/docs.coffee rename to assets/javascripts/collections/docs.js diff --git a/assets/javascripts/collections/entries.coffee b/assets/javascripts/collections/entries.js similarity index 100% rename from assets/javascripts/collections/entries.coffee rename to assets/javascripts/collections/entries.js diff --git a/assets/javascripts/collections/types.coffee b/assets/javascripts/collections/types.js similarity index 100% rename from assets/javascripts/collections/types.coffee rename to assets/javascripts/collections/types.js diff --git a/assets/javascripts/debug.js.coffee b/assets/javascripts/debug.js.js similarity index 100% rename from assets/javascripts/debug.js.coffee rename to assets/javascripts/debug.js.js diff --git a/assets/javascripts/lib/ajax.coffee b/assets/javascripts/lib/ajax.js similarity index 100% rename from assets/javascripts/lib/ajax.coffee rename to assets/javascripts/lib/ajax.js diff --git a/assets/javascripts/lib/cookies_store.coffee b/assets/javascripts/lib/cookies_store.js similarity index 100% rename from assets/javascripts/lib/cookies_store.coffee rename to assets/javascripts/lib/cookies_store.js diff --git a/assets/javascripts/lib/events.coffee b/assets/javascripts/lib/events.js similarity index 100% rename from assets/javascripts/lib/events.coffee rename to assets/javascripts/lib/events.js diff --git a/assets/javascripts/lib/favicon.coffee b/assets/javascripts/lib/favicon.js similarity index 100% rename from assets/javascripts/lib/favicon.coffee rename to assets/javascripts/lib/favicon.js diff --git a/assets/javascripts/lib/license.coffee b/assets/javascripts/lib/license.js similarity index 100% rename from assets/javascripts/lib/license.coffee rename to assets/javascripts/lib/license.js diff --git a/assets/javascripts/lib/local_storage_store.coffee b/assets/javascripts/lib/local_storage_store.js similarity index 100% rename from assets/javascripts/lib/local_storage_store.coffee rename to assets/javascripts/lib/local_storage_store.js diff --git a/assets/javascripts/lib/page.coffee b/assets/javascripts/lib/page.js similarity index 100% rename from assets/javascripts/lib/page.coffee rename to assets/javascripts/lib/page.js diff --git a/assets/javascripts/lib/util.coffee b/assets/javascripts/lib/util.js similarity index 100% rename from assets/javascripts/lib/util.coffee rename to assets/javascripts/lib/util.js diff --git a/assets/javascripts/models/doc.coffee b/assets/javascripts/models/doc.js similarity index 100% rename from assets/javascripts/models/doc.coffee rename to assets/javascripts/models/doc.js diff --git a/assets/javascripts/models/entry.coffee b/assets/javascripts/models/entry.js similarity index 100% rename from assets/javascripts/models/entry.coffee rename to assets/javascripts/models/entry.js diff --git a/assets/javascripts/models/model.coffee b/assets/javascripts/models/model.js similarity index 100% rename from assets/javascripts/models/model.coffee rename to assets/javascripts/models/model.js diff --git a/assets/javascripts/models/type.coffee b/assets/javascripts/models/type.js similarity index 100% rename from assets/javascripts/models/type.coffee rename to assets/javascripts/models/type.js diff --git a/assets/javascripts/templates/base.coffee b/assets/javascripts/templates/base.js similarity index 100% rename from assets/javascripts/templates/base.coffee rename to assets/javascripts/templates/base.js diff --git a/assets/javascripts/templates/error_tmpl.coffee b/assets/javascripts/templates/error_tmpl.js similarity index 100% rename from assets/javascripts/templates/error_tmpl.coffee rename to assets/javascripts/templates/error_tmpl.js diff --git a/assets/javascripts/templates/notice_tmpl.coffee b/assets/javascripts/templates/notice_tmpl.js similarity index 100% rename from assets/javascripts/templates/notice_tmpl.coffee rename to assets/javascripts/templates/notice_tmpl.js diff --git a/assets/javascripts/templates/notif_tmpl.coffee b/assets/javascripts/templates/notif_tmpl.js similarity index 100% rename from assets/javascripts/templates/notif_tmpl.coffee rename to assets/javascripts/templates/notif_tmpl.js diff --git a/assets/javascripts/templates/pages/about_tmpl.coffee b/assets/javascripts/templates/pages/about_tmpl.js similarity index 100% rename from assets/javascripts/templates/pages/about_tmpl.coffee rename to assets/javascripts/templates/pages/about_tmpl.js diff --git a/assets/javascripts/templates/pages/help_tmpl.coffee b/assets/javascripts/templates/pages/help_tmpl.js similarity index 100% rename from assets/javascripts/templates/pages/help_tmpl.coffee rename to assets/javascripts/templates/pages/help_tmpl.js diff --git a/assets/javascripts/templates/pages/offline_tmpl.coffee b/assets/javascripts/templates/pages/offline_tmpl.js similarity index 100% rename from assets/javascripts/templates/pages/offline_tmpl.coffee rename to assets/javascripts/templates/pages/offline_tmpl.js diff --git a/assets/javascripts/templates/pages/settings_tmpl.coffee b/assets/javascripts/templates/pages/settings_tmpl.js similarity index 100% rename from assets/javascripts/templates/pages/settings_tmpl.coffee rename to assets/javascripts/templates/pages/settings_tmpl.js diff --git a/assets/javascripts/templates/pages/type_tmpl.coffee b/assets/javascripts/templates/pages/type_tmpl.js similarity index 100% rename from assets/javascripts/templates/pages/type_tmpl.coffee rename to assets/javascripts/templates/pages/type_tmpl.js diff --git a/assets/javascripts/templates/path_tmpl.coffee b/assets/javascripts/templates/path_tmpl.js similarity index 100% rename from assets/javascripts/templates/path_tmpl.coffee rename to assets/javascripts/templates/path_tmpl.js diff --git a/assets/javascripts/templates/sidebar_tmpl.coffee b/assets/javascripts/templates/sidebar_tmpl.js similarity index 100% rename from assets/javascripts/templates/sidebar_tmpl.coffee rename to assets/javascripts/templates/sidebar_tmpl.js diff --git a/assets/javascripts/templates/tip_tmpl.coffee b/assets/javascripts/templates/tip_tmpl.js similarity index 100% rename from assets/javascripts/templates/tip_tmpl.coffee rename to assets/javascripts/templates/tip_tmpl.js diff --git a/assets/javascripts/views/content/content.coffee b/assets/javascripts/views/content/content.js similarity index 100% rename from assets/javascripts/views/content/content.coffee rename to assets/javascripts/views/content/content.js diff --git a/assets/javascripts/views/content/entry_page.coffee b/assets/javascripts/views/content/entry_page.js similarity index 100% rename from assets/javascripts/views/content/entry_page.coffee rename to assets/javascripts/views/content/entry_page.js diff --git a/assets/javascripts/views/content/offline_page.coffee b/assets/javascripts/views/content/offline_page.js similarity index 100% rename from assets/javascripts/views/content/offline_page.coffee rename to assets/javascripts/views/content/offline_page.js diff --git a/assets/javascripts/views/content/root_page.coffee b/assets/javascripts/views/content/root_page.js similarity index 100% rename from assets/javascripts/views/content/root_page.coffee rename to assets/javascripts/views/content/root_page.js diff --git a/assets/javascripts/views/content/settings_page.coffee b/assets/javascripts/views/content/settings_page.js similarity index 100% rename from assets/javascripts/views/content/settings_page.coffee rename to assets/javascripts/views/content/settings_page.js diff --git a/assets/javascripts/views/content/static_page.coffee b/assets/javascripts/views/content/static_page.js similarity index 100% rename from assets/javascripts/views/content/static_page.coffee rename to assets/javascripts/views/content/static_page.js diff --git a/assets/javascripts/views/content/type_page.coffee b/assets/javascripts/views/content/type_page.js similarity index 100% rename from assets/javascripts/views/content/type_page.coffee rename to assets/javascripts/views/content/type_page.js diff --git a/assets/javascripts/views/layout/document.coffee b/assets/javascripts/views/layout/document.js similarity index 100% rename from assets/javascripts/views/layout/document.coffee rename to assets/javascripts/views/layout/document.js diff --git a/assets/javascripts/views/layout/menu.coffee b/assets/javascripts/views/layout/menu.js similarity index 100% rename from assets/javascripts/views/layout/menu.coffee rename to assets/javascripts/views/layout/menu.js diff --git a/assets/javascripts/views/layout/mobile.coffee b/assets/javascripts/views/layout/mobile.js similarity index 100% rename from assets/javascripts/views/layout/mobile.coffee rename to assets/javascripts/views/layout/mobile.js diff --git a/assets/javascripts/views/layout/path.coffee b/assets/javascripts/views/layout/path.js similarity index 100% rename from assets/javascripts/views/layout/path.coffee rename to assets/javascripts/views/layout/path.js diff --git a/assets/javascripts/views/layout/resizer.coffee b/assets/javascripts/views/layout/resizer.js similarity index 100% rename from assets/javascripts/views/layout/resizer.coffee rename to assets/javascripts/views/layout/resizer.js diff --git a/assets/javascripts/views/layout/settings.coffee b/assets/javascripts/views/layout/settings.js similarity index 100% rename from assets/javascripts/views/layout/settings.coffee rename to assets/javascripts/views/layout/settings.js diff --git a/assets/javascripts/views/list/list_focus.coffee b/assets/javascripts/views/list/list_focus.js similarity index 100% rename from assets/javascripts/views/list/list_focus.coffee rename to assets/javascripts/views/list/list_focus.js diff --git a/assets/javascripts/views/list/list_fold.coffee b/assets/javascripts/views/list/list_fold.js similarity index 100% rename from assets/javascripts/views/list/list_fold.coffee rename to assets/javascripts/views/list/list_fold.js diff --git a/assets/javascripts/views/list/list_select.coffee b/assets/javascripts/views/list/list_select.js similarity index 100% rename from assets/javascripts/views/list/list_select.coffee rename to assets/javascripts/views/list/list_select.js diff --git a/assets/javascripts/views/list/paginated_list.coffee b/assets/javascripts/views/list/paginated_list.js similarity index 100% rename from assets/javascripts/views/list/paginated_list.coffee rename to assets/javascripts/views/list/paginated_list.js diff --git a/assets/javascripts/views/misc/news.coffee b/assets/javascripts/views/misc/news.js similarity index 100% rename from assets/javascripts/views/misc/news.coffee rename to assets/javascripts/views/misc/news.js diff --git a/assets/javascripts/views/misc/notice.coffee b/assets/javascripts/views/misc/notice.js similarity index 100% rename from assets/javascripts/views/misc/notice.coffee rename to assets/javascripts/views/misc/notice.js diff --git a/assets/javascripts/views/misc/notif.coffee b/assets/javascripts/views/misc/notif.js similarity index 100% rename from assets/javascripts/views/misc/notif.coffee rename to assets/javascripts/views/misc/notif.js diff --git a/assets/javascripts/views/misc/tip.coffee b/assets/javascripts/views/misc/tip.js similarity index 100% rename from assets/javascripts/views/misc/tip.coffee rename to assets/javascripts/views/misc/tip.js diff --git a/assets/javascripts/views/misc/updates.coffee b/assets/javascripts/views/misc/updates.js similarity index 100% rename from assets/javascripts/views/misc/updates.coffee rename to assets/javascripts/views/misc/updates.js diff --git a/assets/javascripts/views/pages/base.coffee b/assets/javascripts/views/pages/base.js similarity index 100% rename from assets/javascripts/views/pages/base.coffee rename to assets/javascripts/views/pages/base.js diff --git a/assets/javascripts/views/pages/hidden.coffee b/assets/javascripts/views/pages/hidden.js similarity index 100% rename from assets/javascripts/views/pages/hidden.coffee rename to assets/javascripts/views/pages/hidden.js diff --git a/assets/javascripts/views/pages/jquery.coffee b/assets/javascripts/views/pages/jquery.js similarity index 100% rename from assets/javascripts/views/pages/jquery.coffee rename to assets/javascripts/views/pages/jquery.js diff --git a/assets/javascripts/views/pages/rdoc.coffee b/assets/javascripts/views/pages/rdoc.js similarity index 100% rename from assets/javascripts/views/pages/rdoc.coffee rename to assets/javascripts/views/pages/rdoc.js diff --git a/assets/javascripts/views/pages/sqlite.coffee b/assets/javascripts/views/pages/sqlite.js similarity index 100% rename from assets/javascripts/views/pages/sqlite.coffee rename to assets/javascripts/views/pages/sqlite.js diff --git a/assets/javascripts/views/pages/support_tables.coffee b/assets/javascripts/views/pages/support_tables.js similarity index 100% rename from assets/javascripts/views/pages/support_tables.coffee rename to assets/javascripts/views/pages/support_tables.js diff --git a/assets/javascripts/views/search/search.coffee b/assets/javascripts/views/search/search.js similarity index 100% rename from assets/javascripts/views/search/search.coffee rename to assets/javascripts/views/search/search.js diff --git a/assets/javascripts/views/search/search_scope.coffee b/assets/javascripts/views/search/search_scope.js similarity index 100% rename from assets/javascripts/views/search/search_scope.coffee rename to assets/javascripts/views/search/search_scope.js diff --git a/assets/javascripts/views/sidebar/doc_list.coffee b/assets/javascripts/views/sidebar/doc_list.js similarity index 100% rename from assets/javascripts/views/sidebar/doc_list.coffee rename to assets/javascripts/views/sidebar/doc_list.js diff --git a/assets/javascripts/views/sidebar/doc_picker.coffee b/assets/javascripts/views/sidebar/doc_picker.js similarity index 100% rename from assets/javascripts/views/sidebar/doc_picker.coffee rename to assets/javascripts/views/sidebar/doc_picker.js diff --git a/assets/javascripts/views/sidebar/entry_list.coffee b/assets/javascripts/views/sidebar/entry_list.js similarity index 100% rename from assets/javascripts/views/sidebar/entry_list.coffee rename to assets/javascripts/views/sidebar/entry_list.js diff --git a/assets/javascripts/views/sidebar/results.coffee b/assets/javascripts/views/sidebar/results.js similarity index 100% rename from assets/javascripts/views/sidebar/results.coffee rename to assets/javascripts/views/sidebar/results.js diff --git a/assets/javascripts/views/sidebar/sidebar.coffee b/assets/javascripts/views/sidebar/sidebar.js similarity index 100% rename from assets/javascripts/views/sidebar/sidebar.coffee rename to assets/javascripts/views/sidebar/sidebar.js diff --git a/assets/javascripts/views/sidebar/sidebar_hover.coffee b/assets/javascripts/views/sidebar/sidebar_hover.js similarity index 100% rename from assets/javascripts/views/sidebar/sidebar_hover.coffee rename to assets/javascripts/views/sidebar/sidebar_hover.js diff --git a/assets/javascripts/views/sidebar/type_list.coffee b/assets/javascripts/views/sidebar/type_list.js similarity index 100% rename from assets/javascripts/views/sidebar/type_list.coffee rename to assets/javascripts/views/sidebar/type_list.js diff --git a/assets/javascripts/views/view.coffee b/assets/javascripts/views/view.js similarity index 100% rename from assets/javascripts/views/view.coffee rename to assets/javascripts/views/view.js From e4fbca722bab61f27b9f6512d8981ae3915eff5b Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Sat, 6 Jan 2024 11:47:09 +0100 Subject: [PATCH 0127/1110] decaffeinate: Convert app.coffee and 75 other files to JS --- assets/javascripts/app/app.js | 635 +++++++------ assets/javascripts/app/db.js | 868 +++++++++-------- assets/javascripts/app/router.js | 351 ++++--- assets/javascripts/app/searcher.js | 665 +++++++------ assets/javascripts/app/serviceworker.js | 105 ++- assets/javascripts/app/settings.js | 389 ++++---- assets/javascripts/app/shortcuts.js | 452 +++++---- assets/javascripts/app/update_checker.js | 79 +- assets/javascripts/application.js.js | 49 +- assets/javascripts/collections/collection.js | 130 +-- assets/javascripts/collections/docs.js | 184 ++-- assets/javascripts/collections/entries.js | 13 +- assets/javascripts/collections/types.js | 56 +- assets/javascripts/debug.js.js | 195 ++-- assets/javascripts/lib/ajax.js | 256 ++--- assets/javascripts/lib/cookies_store.js | 94 +- assets/javascripts/lib/events.js | 73 +- assets/javascripts/lib/favicon.js | 165 ++-- assets/javascripts/lib/license.js | 4 +- assets/javascripts/lib/local_storage_store.js | 50 +- assets/javascripts/lib/page.js | 495 +++++----- assets/javascripts/lib/util.js | 886 ++++++++++-------- assets/javascripts/models/doc.js | 315 ++++--- assets/javascripts/models/entry.js | 179 ++-- assets/javascripts/models/model.js | 8 +- assets/javascripts/models/type.js | 32 +- assets/javascripts/templates/base.js | 28 +- assets/javascripts/templates/error_tmpl.js | 142 +-- assets/javascripts/templates/notice_tmpl.js | 19 +- assets/javascripts/templates/notif_tmpl.js | 127 +-- .../javascripts/templates/pages/about_tmpl.js | 181 ++-- .../javascripts/templates/pages/help_tmpl.js | 334 ++++--- .../templates/pages/offline_tmpl.js | 149 +-- .../templates/pages/settings_tmpl.js | 143 +-- .../javascripts/templates/pages/type_tmpl.js | 13 +- assets/javascripts/templates/path_tmpl.js | 18 +- assets/javascripts/templates/sidebar_tmpl.js | 119 +-- assets/javascripts/templates/tip_tmpl.js | 25 +- assets/javascripts/views/content/content.js | 448 +++++---- .../javascripts/views/content/entry_page.js | 391 ++++---- .../javascripts/views/content/offline_page.js | 190 ++-- assets/javascripts/views/content/root_page.js | 98 +- .../views/content/settings_page.js | 267 +++--- .../javascripts/views/content/static_page.js | 57 +- assets/javascripts/views/content/type_page.js | 45 +- assets/javascripts/views/layout/document.js | 194 ++-- assets/javascripts/views/layout/menu.js | 54 +- assets/javascripts/views/layout/mobile.js | 346 ++++--- assets/javascripts/views/layout/path.js | 93 +- assets/javascripts/views/layout/resizer.js | 110 ++- assets/javascripts/views/layout/settings.js | 210 +++-- assets/javascripts/views/list/list_focus.js | 301 +++--- assets/javascripts/views/list/list_fold.js | 146 +-- assets/javascripts/views/list/list_select.js | 108 ++- .../javascripts/views/list/paginated_list.js | 211 +++-- assets/javascripts/views/misc/news.js | 89 +- assets/javascripts/views/misc/notice.js | 53 +- assets/javascripts/views/misc/notif.js | 123 ++- assets/javascripts/views/misc/tip.js | 27 +- assets/javascripts/views/misc/updates.js | 90 +- assets/javascripts/views/pages/base.js | 92 +- assets/javascripts/views/pages/hidden.js | 38 +- assets/javascripts/views/pages/jquery.js | 138 +-- assets/javascripts/views/pages/rdoc.js | 33 +- assets/javascripts/views/pages/sqlite.js | 47 +- .../javascripts/views/pages/support_tables.js | 31 +- assets/javascripts/views/search/search.js | 393 ++++---- .../javascripts/views/search/search_scope.js | 315 ++++--- assets/javascripts/views/sidebar/doc_list.js | 421 +++++---- .../javascripts/views/sidebar/doc_picker.js | 218 +++-- .../javascripts/views/sidebar/entry_list.js | 34 +- assets/javascripts/views/sidebar/results.js | 135 +-- assets/javascripts/views/sidebar/sidebar.js | 379 ++++---- .../views/sidebar/sidebar_hover.js | 243 +++-- assets/javascripts/views/sidebar/type_list.js | 130 +-- assets/javascripts/views/view.js | 386 ++++---- 76 files changed, 8404 insertions(+), 6306 deletions(-) diff --git a/assets/javascripts/app/app.js b/assets/javascripts/app/app.js index b55e552c8b..b79b9e1886 100644 --- a/assets/javascripts/app/app.js +++ b/assets/javascripts/app/app.js @@ -1,283 +1,352 @@ -@app = - _$: $ - _$$: $$ - _page: page - collections: {} - models: {} - templates: {} - views: {} - - init: -> - try @initErrorTracking() catch - return unless @browserCheck() - - @el = $('._app') - @localStorage = new LocalStorageStore - @serviceWorker = new app.ServiceWorker if app.ServiceWorker.isEnabled() - @settings = new app.Settings - @db = new app.DB() - - @settings.initLayout() - - @docs = new app.collections.Docs - @disabledDocs = new app.collections.Docs - @entries = new app.collections.Entries - - @router = new app.Router - @shortcuts = new app.Shortcuts - @document = new app.views.Document - @mobile = new app.views.Mobile if @isMobile() - - if document.body.hasAttribute('data-doc') - @DOC = JSON.parse(document.body.getAttribute('data-doc')) - @bootOne() - else if @DOCS - @bootAll() - else - @onBootError() - return - - browserCheck: -> - return true if @isSupportedBrowser() - document.body.innerHTML = app.templates.unsupportedBrowser - @hideLoadingScreen() - false - - initErrorTracking: -> - # Show a warning message and don't track errors when the app is loaded - # from a domain other than our own, because things are likely to break. - # (e.g. cross-domain requests) - if @isInvalidLocation() - new app.views.Notif 'InvalidLocation' - else - if @config.sentry_dsn - Raven.config @config.sentry_dsn, - release: @config.release - whitelistUrls: [/devdocs/] - includePaths: [/devdocs/] - ignoreErrors: [/NPObject/, /NS_ERROR/, /^null$/, /EvalError/] - tags: - mode: if @isSingleDoc() then 'single' else 'full' - iframe: (window.top isnt window).toString() - electron: (!!window.process?.versions?.electron).toString() - shouldSendCallback: => - try - if @isInjectionError() - @onInjectionError() - return false - if @isAndroidWebview() - return false - true - dataCallback: (data) -> - try - $.extend(data.user ||= {}, app.settings.dump()) - data.user.docs = data.user.docs.split('/') if data.user.docs - data.user.lastIDBTransaction = app.lastIDBTransaction if app.lastIDBTransaction - data.tags.scriptCount = document.scripts.length - data - .install() - @previousErrorHandler = onerror - window.onerror = @onWindowError.bind(@) - CookiesStore.onBlocked = @onCookieBlocked - return - - bootOne: -> - @doc = new app.models.Doc @DOC - @docs.reset [@doc] - @doc.load @start.bind(@), @onBootError.bind(@), readCache: true - new app.views.Notice 'singleDoc', @doc - delete @DOC - return - - bootAll: -> - docs = @settings.getDocs() - for doc in @DOCS - (if docs.indexOf(doc.slug) >= 0 then @docs else @disabledDocs).add(doc) - @migrateDocs() - @docs.load @start.bind(@), @onBootError.bind(@), readCache: true, writeCache: true - delete @DOCS - return - - start: -> - @entries.add doc.toEntry() for doc in @docs.all() - @entries.add doc.toEntry() for doc in @disabledDocs.all() - @initDoc(doc) for doc in @docs.all() - @trigger 'ready' - @router.start() - @hideLoadingScreen() - setTimeout => - @welcomeBack() unless @doc - @removeEvent 'ready bootError' - , 50 - return - - initDoc: (doc) -> - doc.entries.add type.toEntry() for type in doc.types.all() - @entries.add doc.entries.all() - return - - migrateDocs: -> - for slug in @settings.getDocs() when not @docs.findBy('slug', slug) - needsSaving = true - doc = @disabledDocs.findBy('slug', 'webpack') if slug == 'webpack~2' - doc = @disabledDocs.findBy('slug', 'angular') if slug == 'angular~4_typescript' - doc = @disabledDocs.findBy('slug', 'angular~2') if slug == 'angular~2_typescript' - doc ||= @disabledDocs.findBy('slug_without_version', slug) - if doc - @disabledDocs.remove(doc) - @docs.add(doc) - - @saveDocs() if needsSaving - return - - enableDoc: (doc, _onSuccess, onError) -> - return if @docs.contains(doc) - - onSuccess = => - return if @docs.contains(doc) - @disabledDocs.remove(doc) - @docs.add(doc) - @docs.sort() - @initDoc(doc) - @saveDocs() - if app.settings.get('autoInstall') - doc.install(_onSuccess, onError) - else - _onSuccess() - return - - doc.load onSuccess, onError, writeCache: true - return - - saveDocs: -> - @settings.setDocs(doc.slug for doc in @docs.all()) - @db.migrate() - @serviceWorker?.updateInBackground() - - welcomeBack: -> - visitCount = @settings.get('count') - @settings.set 'count', ++visitCount - new app.views.Notif 'Share', autoHide: null if visitCount is 5 - new app.views.News() - new app.views.Updates() - @updateChecker = new app.UpdateChecker() - - reboot: -> - if location.pathname isnt '/' and location.pathname isnt '/settings' - window.location = "/##{location.pathname}" - else - window.location = '/' - return - - reload: -> - @docs.clearCache() - @disabledDocs.clearCache() - if @serviceWorker then @serviceWorker.reload() else @reboot() - return - - reset: -> - @localStorage.reset() - @settings.reset() - @db?.reset() - @serviceWorker?.update() - window.location = '/' - return - - showTip: (tip) -> - return if @isSingleDoc() - tips = @settings.getTips() - if tips.indexOf(tip) is -1 - tips.push(tip) - @settings.setTips(tips) - new app.views.Tip(tip) - return - - hideLoadingScreen: -> - document.body.classList.add '_overlay-scrollbars' if $.overlayScrollbarsEnabled() - document.documentElement.classList.remove '_booting' - return - - indexHost: -> - # Can't load the index files from the host/CDN when service worker is - # enabled because it doesn't support caching URLs that use CORS. - @config[if @serviceWorker and @settings.hasDocs() then 'index_path' else 'docs_origin'] - - onBootError: (args...) -> - @trigger 'bootError' - @hideLoadingScreen() - return - - onQuotaExceeded: -> - return if @quotaExceeded - @quotaExceeded = true - new app.views.Notif 'QuotaExceeded', autoHide: null - return - - onCookieBlocked: (key, value, actual) -> - return if @cookieBlocked - @cookieBlocked = true - new app.views.Notif 'CookieBlocked', autoHide: null - Raven.captureMessage "CookieBlocked/#{key}", level: 'warning', extra: {value, actual} - return - - onWindowError: (args...) -> - return if @cookieBlocked - if @isInjectionError args... - @onInjectionError() - else if @isAppError args... - @previousErrorHandler? args... - @hideLoadingScreen() - @errorNotif or= new app.views.Notif 'Error' - @errorNotif.show() - return - - onInjectionError: -> - unless @injectionError - @injectionError = true - alert """ - JavaScript code has been injected in the page which prevents DevDocs from running correctly. - Please check your browser extensions/addons. """ - Raven.captureMessage 'injection error', level: 'info' - return - - isInjectionError: -> - # Some browser extensions expect the entire web to use jQuery. - # I gave up trying to fight back. - window.$ isnt app._$ or window.$$ isnt app._$$ or window.page isnt app._page or typeof $.empty isnt 'function' or typeof page.show isnt 'function' - - isAppError: (error, file) -> - # Ignore errors from external scripts. - file and file.indexOf('devdocs') isnt -1 and file.indexOf('.js') is file.length - 3 - - isSupportedBrowser: -> - try - features = - bind: !!Function::bind - pushState: !!history.pushState - matchMedia: !!window.matchMedia - insertAdjacentHTML: !!document.body.insertAdjacentHTML - defaultPrevented: document.createEvent('CustomEvent').defaultPrevented is false - cssVariables: !!CSS?.supports?('(--t: 0)') - - for key, value of features when not value - Raven.captureMessage "unsupported/#{key}", level: 'info' - return false - - true - catch error - Raven.captureMessage 'unsupported/exception', level: 'info', extra: { error: error } - false - - isSingleDoc: -> - document.body.hasAttribute('data-doc') - - isMobile: -> - @_isMobile ?= app.views.Mobile.detect() - - isAndroidWebview: -> - @_isAndroidWebview ?= app.views.Mobile.detectAndroidWebview() - - isInvalidLocation: -> - @config.env is 'production' and location.host.indexOf(app.config.production_host) isnt 0 - -$.extend app, Events +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining + * DS207: Consider shorter variations of null checks + * DS208: Avoid top-level this + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +this.app = { + _$: $, + _$$: $$, + _page: page, + collections: {}, + models: {}, + templates: {}, + views: {}, + + init() { + try { this.initErrorTracking(); } catch (error) {} + if (!this.browserCheck()) { return; } + + this.el = $('._app'); + this.localStorage = new LocalStorageStore; + if (app.ServiceWorker.isEnabled()) { this.serviceWorker = new app.ServiceWorker; } + this.settings = new app.Settings; + this.db = new app.DB(); + + this.settings.initLayout(); + + this.docs = new app.collections.Docs; + this.disabledDocs = new app.collections.Docs; + this.entries = new app.collections.Entries; + + this.router = new app.Router; + this.shortcuts = new app.Shortcuts; + this.document = new app.views.Document; + if (this.isMobile()) { this.mobile = new app.views.Mobile; } + + if (document.body.hasAttribute('data-doc')) { + this.DOC = JSON.parse(document.body.getAttribute('data-doc')); + this.bootOne(); + } else if (this.DOCS) { + this.bootAll(); + } else { + this.onBootError(); + } + }, + + browserCheck() { + if (this.isSupportedBrowser()) { return true; } + document.body.innerHTML = app.templates.unsupportedBrowser; + this.hideLoadingScreen(); + return false; + }, + + initErrorTracking() { + // Show a warning message and don't track errors when the app is loaded + // from a domain other than our own, because things are likely to break. + // (e.g. cross-domain requests) + if (this.isInvalidLocation()) { + new app.views.Notif('InvalidLocation'); + } else { + if (this.config.sentry_dsn) { + Raven.config(this.config.sentry_dsn, { + release: this.config.release, + whitelistUrls: [/devdocs/], + includePaths: [/devdocs/], + ignoreErrors: [/NPObject/, /NS_ERROR/, /^null$/, /EvalError/], + tags: { + mode: this.isSingleDoc() ? 'single' : 'full', + iframe: (window.top !== window).toString(), + electron: (!!__guard__(window.process != null ? window.process.versions : undefined, x => x.electron)).toString() + }, + shouldSendCallback: () => { + try { + if (this.isInjectionError()) { + this.onInjectionError(); + return false; + } + if (this.isAndroidWebview()) { + return false; + } + } catch (error) {} + return true; + }, + dataCallback(data) { + try { + $.extend(data.user || (data.user = {}), app.settings.dump()); + if (data.user.docs) { data.user.docs = data.user.docs.split('/'); } + if (app.lastIDBTransaction) { data.user.lastIDBTransaction = app.lastIDBTransaction; } + data.tags.scriptCount = document.scripts.length; + } catch (error) {} + return data; + } + }).install(); + } + this.previousErrorHandler = onerror; + window.onerror = this.onWindowError.bind(this); + CookiesStore.onBlocked = this.onCookieBlocked; + } + }, + + bootOne() { + this.doc = new app.models.Doc(this.DOC); + this.docs.reset([this.doc]); + this.doc.load(this.start.bind(this), this.onBootError.bind(this), {readCache: true}); + new app.views.Notice('singleDoc', this.doc); + delete this.DOC; + }, + + bootAll() { + const docs = this.settings.getDocs(); + for (var doc of Array.from(this.DOCS)) { + (docs.indexOf(doc.slug) >= 0 ? this.docs : this.disabledDocs).add(doc); + } + this.migrateDocs(); + this.docs.load(this.start.bind(this), this.onBootError.bind(this), {readCache: true, writeCache: true}); + delete this.DOCS; + }, + + start() { + let doc; + for (doc of Array.from(this.docs.all())) { this.entries.add(doc.toEntry()); } + for (doc of Array.from(this.disabledDocs.all())) { this.entries.add(doc.toEntry()); } + for (doc of Array.from(this.docs.all())) { this.initDoc(doc); } + this.trigger('ready'); + this.router.start(); + this.hideLoadingScreen(); + setTimeout(() => { + if (!this.doc) { this.welcomeBack(); } + return this.removeEvent('ready bootError'); + } + , 50); + }, + + initDoc(doc) { + for (var type of Array.from(doc.types.all())) { doc.entries.add(type.toEntry()); } + this.entries.add(doc.entries.all()); + }, + + migrateDocs() { + let needsSaving; + for (var slug of Array.from(this.settings.getDocs())) { + if (!this.docs.findBy('slug', slug)) {var doc; + + needsSaving = true; + if (slug === 'webpack~2') { doc = this.disabledDocs.findBy('slug', 'webpack'); } + if (slug === 'angular~4_typescript') { doc = this.disabledDocs.findBy('slug', 'angular'); } + if (slug === 'angular~2_typescript') { doc = this.disabledDocs.findBy('slug', 'angular~2'); } + if (!doc) { doc = this.disabledDocs.findBy('slug_without_version', slug); } + if (doc) { + this.disabledDocs.remove(doc); + this.docs.add(doc); + } + } + } + + if (needsSaving) { this.saveDocs(); } + }, + + enableDoc(doc, _onSuccess, onError) { + if (this.docs.contains(doc)) { return; } + + const onSuccess = () => { + if (this.docs.contains(doc)) { return; } + this.disabledDocs.remove(doc); + this.docs.add(doc); + this.docs.sort(); + this.initDoc(doc); + this.saveDocs(); + if (app.settings.get('autoInstall')) { + doc.install(_onSuccess, onError); + } else { + _onSuccess(); + } + }; + + doc.load(onSuccess, onError, {writeCache: true}); + }, + + saveDocs() { + this.settings.setDocs(Array.from(this.docs.all()).map((doc) => doc.slug)); + this.db.migrate(); + return (this.serviceWorker != null ? this.serviceWorker.updateInBackground() : undefined); + }, + + welcomeBack() { + let visitCount = this.settings.get('count'); + this.settings.set('count', ++visitCount); + if (visitCount === 5) { new app.views.Notif('Share', {autoHide: null}); } + new app.views.News(); + new app.views.Updates(); + return this.updateChecker = new app.UpdateChecker(); + }, + + reboot() { + if ((location.pathname !== '/') && (location.pathname !== '/settings')) { + window.location = `/#${location.pathname}`; + } else { + window.location = '/'; + } + }, + + reload() { + this.docs.clearCache(); + this.disabledDocs.clearCache(); + if (this.serviceWorker) { this.serviceWorker.reload(); } else { this.reboot(); } + }, + + reset() { + this.localStorage.reset(); + this.settings.reset(); + if (this.db != null) { + this.db.reset(); + } + if (this.serviceWorker != null) { + this.serviceWorker.update(); + } + window.location = '/'; + }, + + showTip(tip) { + if (this.isSingleDoc()) { return; } + const tips = this.settings.getTips(); + if (tips.indexOf(tip) === -1) { + tips.push(tip); + this.settings.setTips(tips); + new app.views.Tip(tip); + } + }, + + hideLoadingScreen() { + if ($.overlayScrollbarsEnabled()) { document.body.classList.add('_overlay-scrollbars'); } + document.documentElement.classList.remove('_booting'); + }, + + indexHost() { + // Can't load the index files from the host/CDN when service worker is + // enabled because it doesn't support caching URLs that use CORS. + return this.config[this.serviceWorker && this.settings.hasDocs() ? 'index_path' : 'docs_origin']; + }, + + onBootError(...args) { + this.trigger('bootError'); + this.hideLoadingScreen(); + }, + + onQuotaExceeded() { + if (this.quotaExceeded) { return; } + this.quotaExceeded = true; + new app.views.Notif('QuotaExceeded', {autoHide: null}); + }, + + onCookieBlocked(key, value, actual) { + if (this.cookieBlocked) { return; } + this.cookieBlocked = true; + new app.views.Notif('CookieBlocked', {autoHide: null}); + Raven.captureMessage(`CookieBlocked/${key}`, {level: 'warning', extra: {value, actual}}); + }, + + onWindowError(...args) { + if (this.cookieBlocked) { return; } + if (this.isInjectionError(...Array.from(args || []))) { + this.onInjectionError(); + } else if (this.isAppError(...Array.from(args || []))) { + if (typeof this.previousErrorHandler === 'function') { + this.previousErrorHandler(...Array.from(args || [])); + } + this.hideLoadingScreen(); + if (!this.errorNotif) { this.errorNotif = new app.views.Notif('Error'); } + this.errorNotif.show(); + } + }, + + onInjectionError() { + if (!this.injectionError) { + this.injectionError = true; + alert(`\ +JavaScript code has been injected in the page which prevents DevDocs from running correctly. +Please check your browser extensions/addons. ` + ); + Raven.captureMessage('injection error', {level: 'info'}); + } + }, + + isInjectionError() { + // Some browser extensions expect the entire web to use jQuery. + // I gave up trying to fight back. + return (window.$ !== app._$) || (window.$$ !== app._$$) || (window.page !== app._page) || (typeof $.empty !== 'function') || (typeof page.show !== 'function'); + }, + + isAppError(error, file) { + // Ignore errors from external scripts. + return file && (file.indexOf('devdocs') !== -1) && (file.indexOf('.js') === (file.length - 3)); + }, + + isSupportedBrowser() { + try { + const features = { + bind: !!Function.prototype.bind, + pushState: !!history.pushState, + matchMedia: !!window.matchMedia, + insertAdjacentHTML: !!document.body.insertAdjacentHTML, + defaultPrevented: document.createEvent('CustomEvent').defaultPrevented === false, + cssVariables: !!__guardMethod__(CSS, 'supports', o => o.supports('(--t: 0)')) + }; + + for (var key in features) { + var value = features[key]; + if (!value) { + Raven.captureMessage(`unsupported/${key}`, {level: 'info'}); + return false; + } + } + + return true; + } catch (error) { + Raven.captureMessage('unsupported/exception', {level: 'info', extra: { error }}); + return false; + } + }, + + isSingleDoc() { + return document.body.hasAttribute('data-doc'); + }, + + isMobile() { + return this._isMobile != null ? this._isMobile : (this._isMobile = app.views.Mobile.detect()); + }, + + isAndroidWebview() { + return this._isAndroidWebview != null ? this._isAndroidWebview : (this._isAndroidWebview = app.views.Mobile.detectAndroidWebview()); + }, + + isInvalidLocation() { + return (this.config.env === 'production') && (location.host.indexOf(app.config.production_host) !== 0); + } +}; + +$.extend(app, Events); + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} +function __guardMethod__(obj, methodName, transform) { + if (typeof obj !== 'undefined' && obj !== null && typeof obj[methodName] === 'function') { + return transform(obj, methodName); + } else { + return undefined; + } +} \ No newline at end of file diff --git a/assets/javascripts/app/db.js b/assets/javascripts/app/db.js index 28e4b0eaeb..ab84896488 100644 --- a/assets/javascripts/app/db.js +++ b/assets/javascripts/app/db.js @@ -1,382 +1,486 @@ -class app.DB - NAME = 'docs' - VERSION = 15 - - constructor: -> - @versionMultipler = if $.isIE() then 1e5 else 1e9 - @useIndexedDB = @useIndexedDB() - @callbacks = [] - - db: (fn) -> - return fn() unless @useIndexedDB - @callbacks.push(fn) if fn - return if @open - - try - @open = true - req = indexedDB.open(NAME, VERSION * @versionMultipler + @userVersion()) - req.onsuccess = @onOpenSuccess - req.onerror = @onOpenError - req.onupgradeneeded = @onUpgradeNeeded - catch error - @fail 'exception', error - return - - onOpenSuccess: (event) => - db = event.target.result - - if db.objectStoreNames.length is 0 - try db.close() - @open = false - @fail 'empty' - else if error = @buggyIDB(db) - try db.close() - @open = false - @fail 'buggy', error - else - @runCallbacks(db) - @open = false - db.close() - return - - onOpenError: (event) => - event.preventDefault() - @open = false - error = event.target.error - - switch error.name - when 'QuotaExceededError' - @onQuotaExceededError() - when 'VersionError' - @onVersionError() - when 'InvalidStateError' - @fail 'private_mode' - else - @fail 'cant_open', error - return - - fail: (reason, error) -> - @cachedDocs = null - @useIndexedDB = false - @reason or= reason - @error or= error - console.error? 'IDB error', error if error - @runCallbacks() - if error and reason is 'cant_open' - Raven.captureMessage "#{error.name}: #{error.message}", level: 'warning', fingerprint: [error.name] - return - - onQuotaExceededError: -> - @reset() - @db() - app.onQuotaExceeded() - Raven.captureMessage 'QuotaExceededError', level: 'warning' - return - - onVersionError: -> - req = indexedDB.open(NAME) - req.onsuccess = (event) => - @handleVersionMismatch event.target.result.version - req.onerror = (event) -> - event.preventDefault() - @fail 'cant_open', error - return - - handleVersionMismatch: (actualVersion) -> - if Math.floor(actualVersion / @versionMultipler) isnt VERSION - @fail 'version' - else - @setUserVersion actualVersion - VERSION * @versionMultipler - @db() - return - - buggyIDB: (db) -> - return if @checkedBuggyIDB - @checkedBuggyIDB = true - try - @idbTransaction(db, stores: $.makeArray(db.objectStoreNames)[0..1], mode: 'readwrite').abort() # https://bugs.webkit.org/show_bug.cgi?id=136937 - return - catch error - return error - - runCallbacks: (db) -> - fn(db) while fn = @callbacks.shift() - return - - onUpgradeNeeded: (event) -> - return unless db = event.target.result - - objectStoreNames = $.makeArray(db.objectStoreNames) - - unless $.arrayDelete(objectStoreNames, 'docs') - try db.createObjectStore('docs') - - for doc in app.docs.all() when not $.arrayDelete(objectStoreNames, doc.slug) - try db.createObjectStore(doc.slug) - - for name in objectStoreNames - try db.deleteObjectStore(name) - return - - store: (doc, data, onSuccess, onError, _retry = true) -> - @db (db) => - unless db - onError() - return - - txn = @idbTransaction db, stores: ['docs', doc.slug], mode: 'readwrite', ignoreError: false - txn.oncomplete = => - @cachedDocs?[doc.slug] = doc.mtime - onSuccess() - return - txn.onerror = (event) => - event.preventDefault() - if txn.error?.name is 'NotFoundError' and _retry - @migrate() - setTimeout => - @store(doc, data, onSuccess, onError, false) - , 0 - else - onError(event) - return - - store = txn.objectStore(doc.slug) - store.clear() - store.add(content, path) for path, content of data - - store = txn.objectStore('docs') - store.put(doc.mtime, doc.slug) - return - return - - unstore: (doc, onSuccess, onError, _retry = true) -> - @db (db) => - unless db - onError() - return - - txn = @idbTransaction db, stores: ['docs', doc.slug], mode: 'readwrite', ignoreError: false - txn.oncomplete = => - delete @cachedDocs?[doc.slug] - onSuccess() - return - txn.onerror = (event) -> - event.preventDefault() - if txn.error?.name is 'NotFoundError' and _retry - @migrate() - setTimeout => - @unstore(doc, onSuccess, onError, false) - , 0 - else - onError(event) - return - - store = txn.objectStore('docs') - store.delete(doc.slug) - - store = txn.objectStore(doc.slug) - store.clear() - return - return - - version: (doc, fn) -> - if (version = @cachedVersion(doc))? - fn(version) - return - - @db (db) => - unless db - fn(false) - return - - txn = @idbTransaction db, stores: ['docs'], mode: 'readonly' - store = txn.objectStore('docs') - - req = store.get(doc.slug) - req.onsuccess = -> - fn(req.result) - return - req.onerror = (event) -> - event.preventDefault() - fn(false) - return - return - return - - cachedVersion: (doc) -> - return unless @cachedDocs - @cachedDocs[doc.slug] or false - - versions: (docs, fn) -> - if versions = @cachedVersions(docs) - fn(versions) - return - - @db (db) => - unless db - fn(false) - return - - txn = @idbTransaction db, stores: ['docs'], mode: 'readonly' - txn.oncomplete = -> - fn(result) - return - store = txn.objectStore('docs') - result = {} - - docs.forEach (doc) -> - req = store.get(doc.slug) - req.onsuccess = -> - result[doc.slug] = req.result - return - req.onerror = (event) -> - event.preventDefault() - result[doc.slug] = false - return - return - return - - cachedVersions: (docs) -> - return unless @cachedDocs - result = {} - result[doc.slug] = @cachedVersion(doc) for doc in docs - result - - load: (entry, onSuccess, onError) -> - if @shouldLoadWithIDB(entry) - onError = @loadWithXHR.bind(@, entry, onSuccess, onError) - @loadWithIDB entry, onSuccess, onError - else - @loadWithXHR entry, onSuccess, onError - - loadWithXHR: (entry, onSuccess, onError) -> - ajax - url: entry.fileUrl() - dataType: 'html' - success: onSuccess - error: onError - - loadWithIDB: (entry, onSuccess, onError) -> - @db (db) => - unless db - onError() - return - - unless db.objectStoreNames.contains(entry.doc.slug) - onError() - @loadDocsCache(db) - return - - txn = @idbTransaction db, stores: [entry.doc.slug], mode: 'readonly' - store = txn.objectStore(entry.doc.slug) - - req = store.get(entry.dbPath()) - req.onsuccess = -> - if req.result then onSuccess(req.result) else onError() - return - req.onerror = (event) -> - event.preventDefault() - onError() - return - @loadDocsCache(db) - return - - loadDocsCache: (db) -> - return if @cachedDocs - @cachedDocs = {} - - txn = @idbTransaction db, stores: ['docs'], mode: 'readonly' - txn.oncomplete = => - setTimeout(@checkForCorruptedDocs, 50) - return - - req = txn.objectStore('docs').openCursor() - req.onsuccess = (event) => - return unless cursor = event.target.result - @cachedDocs[cursor.key] = cursor.value - cursor.continue() - return - req.onerror = (event) -> - event.preventDefault() - return - return - - checkForCorruptedDocs: => - @db (db) => - @corruptedDocs = [] - docs = (key for key, value of @cachedDocs when value) - return if docs.length is 0 - - for slug in docs when not app.docs.findBy('slug', slug) - @corruptedDocs.push(slug) - - for slug in @corruptedDocs - $.arrayDelete(docs, slug) - - if docs.length is 0 - setTimeout(@deleteCorruptedDocs, 0) - return - - txn = @idbTransaction(db, stores: docs, mode: 'readonly', ignoreError: false) - txn.oncomplete = => - setTimeout(@deleteCorruptedDocs, 0) if @corruptedDocs.length > 0 - return - - for doc in docs - txn.objectStore(doc).get('index').onsuccess = (event) => - @corruptedDocs.push(event.target.source.name) unless event.target.result - return - return - return - - deleteCorruptedDocs: => - @db (db) => - txn = @idbTransaction(db, stores: ['docs'], mode: 'readwrite', ignoreError: false) - store = txn.objectStore('docs') - while doc = @corruptedDocs.pop() - @cachedDocs[doc] = false - store.delete(doc) - return - Raven.captureMessage 'corruptedDocs', level: 'info', extra: { docs: @corruptedDocs.join(',') } - return - - shouldLoadWithIDB: (entry) -> - @useIndexedDB and (not @cachedDocs or @cachedDocs[entry.doc.slug]) - - idbTransaction: (db, options) -> - app.lastIDBTransaction = [options.stores, options.mode] - txn = db.transaction(options.stores, options.mode) - unless options.ignoreError is false - txn.onerror = (event) -> - event.preventDefault() - return - unless options.ignoreAbort is false - txn.onabort = (event) -> - event.preventDefault() - return - txn - - reset: -> - try indexedDB?.deleteDatabase(NAME) catch - return - - useIndexedDB: -> - try - if !app.isSingleDoc() and window.indexedDB - true - else - @reason = 'not_supported' - false - catch - false - - migrate: -> - app.settings.set('schema', @userVersion() + 1) - return - - setUserVersion: (version) -> - app.settings.set('schema', version) - return - - userVersion: -> - app.settings.get('schema') +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +(function() { + let NAME = undefined; + let VERSION = undefined; + const Cls = (app.DB = class DB { + static initClass() { + NAME = 'docs'; + VERSION = 15; + } + + constructor() { + this.onOpenSuccess = this.onOpenSuccess.bind(this); + this.onOpenError = this.onOpenError.bind(this); + this.checkForCorruptedDocs = this.checkForCorruptedDocs.bind(this); + this.deleteCorruptedDocs = this.deleteCorruptedDocs.bind(this); + this.versionMultipler = $.isIE() ? 1e5 : 1e9; + this.useIndexedDB = this.useIndexedDB(); + this.callbacks = []; + } + + db(fn) { + if (!this.useIndexedDB) { return fn(); } + if (fn) { this.callbacks.push(fn); } + if (this.open) { return; } + + try { + this.open = true; + const req = indexedDB.open(NAME, (VERSION * this.versionMultipler) + this.userVersion()); + req.onsuccess = this.onOpenSuccess; + req.onerror = this.onOpenError; + req.onupgradeneeded = this.onUpgradeNeeded; + } catch (error) { + this.fail('exception', error); + } + } + + onOpenSuccess(event) { + let error; + const db = event.target.result; + + if (db.objectStoreNames.length === 0) { + try { db.close(); } catch (error1) {} + this.open = false; + this.fail('empty'); + } else if (error = this.buggyIDB(db)) { + try { db.close(); } catch (error2) {} + this.open = false; + this.fail('buggy', error); + } else { + this.runCallbacks(db); + this.open = false; + db.close(); + } + } + + onOpenError(event) { + event.preventDefault(); + this.open = false; + const { + error + } = event.target; + + switch (error.name) { + case 'QuotaExceededError': + this.onQuotaExceededError(); + break; + case 'VersionError': + this.onVersionError(); + break; + case 'InvalidStateError': + this.fail('private_mode'); + break; + default: + this.fail('cant_open', error); + } + } + + fail(reason, error) { + this.cachedDocs = null; + this.useIndexedDB = false; + if (!this.reason) { this.reason = reason; } + if (!this.error) { this.error = error; } + if (error) { if (typeof console.error === 'function') { + console.error('IDB error', error); + } } + this.runCallbacks(); + if (error && (reason === 'cant_open')) { + Raven.captureMessage(`${error.name}: ${error.message}`, {level: 'warning', fingerprint: [error.name]}); + } + } + + onQuotaExceededError() { + this.reset(); + this.db(); + app.onQuotaExceeded(); + Raven.captureMessage('QuotaExceededError', {level: 'warning'}); + } + + onVersionError() { + const req = indexedDB.open(NAME); + req.onsuccess = event => { + return this.handleVersionMismatch(event.target.result.version); + }; + req.onerror = function(event) { + event.preventDefault(); + return this.fail('cant_open', error); + }; + } + + handleVersionMismatch(actualVersion) { + if (Math.floor(actualVersion / this.versionMultipler) !== VERSION) { + this.fail('version'); + } else { + this.setUserVersion(actualVersion - (VERSION * this.versionMultipler)); + this.db(); + } + } + + buggyIDB(db) { + if (this.checkedBuggyIDB) { return; } + this.checkedBuggyIDB = true; + try { + this.idbTransaction(db, {stores: $.makeArray(db.objectStoreNames).slice(0, 2), mode: 'readwrite'}).abort(); // https://bugs.webkit.org/show_bug.cgi?id=136937 + return; + } catch (error) { + return error; + } + } + + runCallbacks(db) { + let fn; + while ((fn = this.callbacks.shift())) { fn(db); } + } + + onUpgradeNeeded(event) { + let db; + if (!(db = event.target.result)) { return; } + + const objectStoreNames = $.makeArray(db.objectStoreNames); + + if (!$.arrayDelete(objectStoreNames, 'docs')) { + try { db.createObjectStore('docs'); } catch (error) {} + } + + for (var doc of Array.from(app.docs.all())) { + if (!$.arrayDelete(objectStoreNames, doc.slug)) { + try { db.createObjectStore(doc.slug); } catch (error1) {} + } + } + + for (var name of Array.from(objectStoreNames)) { + try { db.deleteObjectStore(name); } catch (error2) {} + } + } + + store(doc, data, onSuccess, onError, _retry) { + if (_retry == null) { _retry = true; } + this.db(db => { + if (!db) { + onError(); + return; + } + + const txn = this.idbTransaction(db, {stores: ['docs', doc.slug], mode: 'readwrite', ignoreError: false}); + txn.oncomplete = () => { + if (this.cachedDocs != null) { + this.cachedDocs[doc.slug] = doc.mtime; + } + onSuccess(); + }; + txn.onerror = event => { + event.preventDefault(); + if (((txn.error != null ? txn.error.name : undefined) === 'NotFoundError') && _retry) { + this.migrate(); + setTimeout(() => { + return this.store(doc, data, onSuccess, onError, false); + } + , 0); + } else { + onError(event); + } + }; + + let store = txn.objectStore(doc.slug); + store.clear(); + for (var path in data) { var content = data[path]; store.add(content, path); } + + store = txn.objectStore('docs'); + store.put(doc.mtime, doc.slug); + }); + } + + unstore(doc, onSuccess, onError, _retry) { + if (_retry == null) { _retry = true; } + this.db(db => { + if (!db) { + onError(); + return; + } + + const txn = this.idbTransaction(db, {stores: ['docs', doc.slug], mode: 'readwrite', ignoreError: false}); + txn.oncomplete = () => { + if (this.cachedDocs != null) { + delete this.cachedDocs[doc.slug]; + } + onSuccess(); + }; + txn.onerror = function(event) { + event.preventDefault(); + if (((txn.error != null ? txn.error.name : undefined) === 'NotFoundError') && _retry) { + this.migrate(); + setTimeout(() => { + return this.unstore(doc, onSuccess, onError, false); + } + , 0); + } else { + onError(event); + } + }; + + let store = txn.objectStore('docs'); + store.delete(doc.slug); + + store = txn.objectStore(doc.slug); + store.clear(); + }); + } + + version(doc, fn) { + let version; + if ((version = this.cachedVersion(doc)) != null) { + fn(version); + return; + } + + this.db(db => { + if (!db) { + fn(false); + return; + } + + const txn = this.idbTransaction(db, {stores: ['docs'], mode: 'readonly'}); + const store = txn.objectStore('docs'); + + const req = store.get(doc.slug); + req.onsuccess = function() { + fn(req.result); + }; + req.onerror = function(event) { + event.preventDefault(); + fn(false); + }; + }); + } + + cachedVersion(doc) { + if (!this.cachedDocs) { return; } + return this.cachedDocs[doc.slug] || false; + } + + versions(docs, fn) { + let versions; + if (versions = this.cachedVersions(docs)) { + fn(versions); + return; + } + + return this.db(db => { + if (!db) { + fn(false); + return; + } + + const txn = this.idbTransaction(db, {stores: ['docs'], mode: 'readonly'}); + txn.oncomplete = function() { + fn(result); + }; + const store = txn.objectStore('docs'); + var result = {}; + + docs.forEach(function(doc) { + const req = store.get(doc.slug); + req.onsuccess = function() { + result[doc.slug] = req.result; + }; + req.onerror = function(event) { + event.preventDefault(); + result[doc.slug] = false; + }; + }); + }); + } + + cachedVersions(docs) { + if (!this.cachedDocs) { return; } + const result = {}; + for (var doc of Array.from(docs)) { result[doc.slug] = this.cachedVersion(doc); } + return result; + } + + load(entry, onSuccess, onError) { + if (this.shouldLoadWithIDB(entry)) { + onError = this.loadWithXHR.bind(this, entry, onSuccess, onError); + return this.loadWithIDB(entry, onSuccess, onError); + } else { + return this.loadWithXHR(entry, onSuccess, onError); + } + } + + loadWithXHR(entry, onSuccess, onError) { + return ajax({ + url: entry.fileUrl(), + dataType: 'html', + success: onSuccess, + error: onError + }); + } + + loadWithIDB(entry, onSuccess, onError) { + return this.db(db => { + if (!db) { + onError(); + return; + } + + if (!db.objectStoreNames.contains(entry.doc.slug)) { + onError(); + this.loadDocsCache(db); + return; + } + + const txn = this.idbTransaction(db, {stores: [entry.doc.slug], mode: 'readonly'}); + const store = txn.objectStore(entry.doc.slug); + + const req = store.get(entry.dbPath()); + req.onsuccess = function() { + if (req.result) { onSuccess(req.result); } else { onError(); } + }; + req.onerror = function(event) { + event.preventDefault(); + onError(); + }; + this.loadDocsCache(db); + }); + } + + loadDocsCache(db) { + if (this.cachedDocs) { return; } + this.cachedDocs = {}; + + const txn = this.idbTransaction(db, {stores: ['docs'], mode: 'readonly'}); + txn.oncomplete = () => { + setTimeout(this.checkForCorruptedDocs, 50); + }; + + const req = txn.objectStore('docs').openCursor(); + req.onsuccess = event => { + let cursor; + if (!(cursor = event.target.result)) { return; } + this.cachedDocs[cursor.key] = cursor.value; + cursor.continue(); + }; + req.onerror = function(event) { + event.preventDefault(); + }; + } + + checkForCorruptedDocs() { + this.db(db => { + let slug; + this.corruptedDocs = []; + const docs = ((() => { + const result = []; + for (var key in this.cachedDocs) { + var value = this.cachedDocs[key]; + if (value) { + result.push(key); + } + } + return result; + })()); + if (docs.length === 0) { return; } + + for (slug of Array.from(docs)) { + if (!app.docs.findBy('slug', slug)) { + this.corruptedDocs.push(slug); + } + } + + for (slug of Array.from(this.corruptedDocs)) { + $.arrayDelete(docs, slug); + } + + if (docs.length === 0) { + setTimeout(this.deleteCorruptedDocs, 0); + return; + } + + const txn = this.idbTransaction(db, {stores: docs, mode: 'readonly', ignoreError: false}); + txn.oncomplete = () => { + if (this.corruptedDocs.length > 0) { setTimeout(this.deleteCorruptedDocs, 0); } + }; + + for (var doc of Array.from(docs)) { + txn.objectStore(doc).get('index').onsuccess = event => { + if (!event.target.result) { this.corruptedDocs.push(event.target.source.name); } + }; + } + }); + } + + deleteCorruptedDocs() { + this.db(db => { + let doc; + const txn = this.idbTransaction(db, {stores: ['docs'], mode: 'readwrite', ignoreError: false}); + const store = txn.objectStore('docs'); + while ((doc = this.corruptedDocs.pop())) { + this.cachedDocs[doc] = false; + store.delete(doc); + } + }); + Raven.captureMessage('corruptedDocs', {level: 'info', extra: { docs: this.corruptedDocs.join(',') }}); + } + + shouldLoadWithIDB(entry) { + return this.useIndexedDB && (!this.cachedDocs || this.cachedDocs[entry.doc.slug]); + } + + idbTransaction(db, options) { + app.lastIDBTransaction = [options.stores, options.mode]; + const txn = db.transaction(options.stores, options.mode); + if (options.ignoreError !== false) { + txn.onerror = function(event) { + event.preventDefault(); + }; + } + if (options.ignoreAbort !== false) { + txn.onabort = function(event) { + event.preventDefault(); + }; + } + return txn; + } + + reset() { + try { if (typeof indexedDB !== 'undefined' && indexedDB !== null) { + indexedDB.deleteDatabase(NAME); + } } catch (error) {} + } + + useIndexedDB() { + try { + if (!app.isSingleDoc() && window.indexedDB) { + return true; + } else { + this.reason = 'not_supported'; + return false; + } + } catch (error) { + return false; + } + } + + migrate() { + app.settings.set('schema', this.userVersion() + 1); + } + + setUserVersion(version) { + app.settings.set('schema', version); + } + + userVersion() { + return app.settings.get('schema'); + } + }); + Cls.initClass(); + return Cls; +})(); diff --git a/assets/javascripts/app/router.js b/assets/javascripts/app/router.js index ba25148acd..8cd0607e9e 100644 --- a/assets/javascripts/app/router.js +++ b/assets/javascripts/app/router.js @@ -1,154 +1,199 @@ -class app.Router - $.extend @prototype, Events - - @routes: [ - ['*', 'before' ] - ['/', 'root' ] - ['/settings', 'settings' ] - ['/offline', 'offline' ] - ['/about', 'about' ] - ['/news', 'news' ] - ['/help', 'help' ] - ['/:doc-:type/', 'type' ] - ['/:doc/', 'doc' ] - ['/:doc/:path(*)', 'entry' ] - ['*', 'notFound' ] - ] - - constructor: -> - for [path, method] in @constructor.routes - page path, @[method].bind(@) - @setInitialPath() - - start: -> - page.start() - return - - show: (path) -> - page.show(path) - return - - triggerRoute: (name) -> - @trigger name, @context - @trigger 'after', name, @context - return - - before: (context, next) -> - previousContext = @context - @context = context - @trigger 'before', context - - if res = next() - @context = previousContext - return res - else - return - - doc: (context, next) -> - if doc = app.docs.findBySlug(context.params.doc) or app.disabledDocs.findBySlug(context.params.doc) - context.doc = doc - context.entry = doc.toEntry() - @triggerRoute 'entry' - return - else - return next() - - type: (context, next) -> - doc = app.docs.findBySlug(context.params.doc) - - if type = doc?.types.findBy 'slug', context.params.type - context.doc = doc - context.type = type - @triggerRoute 'type' - return - else - return next() - - entry: (context, next) -> - doc = app.docs.findBySlug(context.params.doc) - return next() unless doc - path = context.params.path - hash = context.hash - - if entry = doc.findEntryByPathAndHash(path, hash) - context.doc = doc - context.entry = entry - @triggerRoute 'entry' - return - else if path.slice(-6) is '/index' - path = path.substr(0, path.length - 6) - return entry.fullPath() if entry = doc.findEntryByPathAndHash(path, hash) - else - path = "#{path}/index" - return entry.fullPath() if entry = doc.findEntryByPathAndHash(path, hash) - - return next() - - root: -> - return '/' if app.isSingleDoc() - @triggerRoute 'root' - return - - settings: (context) -> - return "/#/#{context.path}" if app.isSingleDoc() - @triggerRoute 'settings' - return - - offline: (context)-> - return "/#/#{context.path}" if app.isSingleDoc() - @triggerRoute 'offline' - return - - about: (context) -> - return "/#/#{context.path}" if app.isSingleDoc() - context.page = 'about' - @triggerRoute 'page' - return - - news: (context) -> - return "/#/#{context.path}" if app.isSingleDoc() - context.page = 'news' - @triggerRoute 'page' - return - - help: (context) -> - return "/#/#{context.path}" if app.isSingleDoc() - context.page = 'help' - @triggerRoute 'page' - return - - notFound: (context) -> - @triggerRoute 'notFound' - return - - isIndex: -> - @context?.path is '/' or (app.isSingleDoc() and @context?.entry?.isIndex()) - - isSettings: -> - @context?.path is '/settings' - - setInitialPath: -> - # Remove superfluous forward slashes at the beginning of the path - if (path = location.pathname.replace /^\/{2,}/g, '/') isnt location.pathname - page.replace path + location.search + location.hash, null, true - - if location.pathname is '/' - if path = @getInitialPathFromHash() - page.replace path + location.search, null, true - else if path = @getInitialPathFromCookie() - page.replace path + location.search + location.hash, null, true - return - - getInitialPathFromHash: -> - try - (new RegExp "#/(.+)").exec(decodeURIComponent location.hash)?[1] - catch - - getInitialPathFromCookie: -> - if path = Cookies.get('initial_path') - Cookies.expire('initial_path') +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const Cls = (app.Router = class Router { + static initClass() { + $.extend(this.prototype, Events); + + this.routes = [ + ['*', 'before' ], + ['/', 'root' ], + ['/settings', 'settings' ], + ['/offline', 'offline' ], + ['/about', 'about' ], + ['/news', 'news' ], + ['/help', 'help' ], + ['/:doc-:type/', 'type' ], + ['/:doc/', 'doc' ], + ['/:doc/:path(*)', 'entry' ], + ['*', 'notFound' ] + ]; + } + + constructor() { + for (var [path, method] of Array.from(this.constructor.routes)) { + page(path, this[method].bind(this)); + } + this.setInitialPath(); + } + + start() { + page.start(); + } + + show(path) { + page.show(path); + } + + triggerRoute(name) { + this.trigger(name, this.context); + this.trigger('after', name, this.context); + } + + before(context, next) { + let res; + const previousContext = this.context; + this.context = context; + this.trigger('before', context); + + if (res = next()) { + this.context = previousContext; + return res; + } else { + return; + } + } + + doc(context, next) { + let doc; + if (doc = app.docs.findBySlug(context.params.doc) || app.disabledDocs.findBySlug(context.params.doc)) { + context.doc = doc; + context.entry = doc.toEntry(); + this.triggerRoute('entry'); + return; + } else { + return next(); + } + } + + type(context, next) { + let type; + const doc = app.docs.findBySlug(context.params.doc); + + if (type = doc != null ? doc.types.findBy('slug', context.params.type) : undefined) { + context.doc = doc; + context.type = type; + this.triggerRoute('type'); + return; + } else { + return next(); + } + } + + entry(context, next) { + let entry; + const doc = app.docs.findBySlug(context.params.doc); + if (!doc) { return next(); } + let { path - - replaceHash: (hash) -> - page.replace location.pathname + location.search + (hash or ''), null, true - return + } = context.params; + const { + hash + } = context; + + if (entry = doc.findEntryByPathAndHash(path, hash)) { + context.doc = doc; + context.entry = entry; + this.triggerRoute('entry'); + return; + } else if (path.slice(-6) === '/index') { + path = path.substr(0, path.length - 6); + if (entry = doc.findEntryByPathAndHash(path, hash)) { return entry.fullPath(); } + } else { + path = `${path}/index`; + if (entry = doc.findEntryByPathAndHash(path, hash)) { return entry.fullPath(); } + } + + return next(); + } + + root() { + if (app.isSingleDoc()) { return '/'; } + this.triggerRoute('root'); + } + + settings(context) { + if (app.isSingleDoc()) { return `/#/${context.path}`; } + this.triggerRoute('settings'); + } + + offline(context){ + if (app.isSingleDoc()) { return `/#/${context.path}`; } + this.triggerRoute('offline'); + } + + about(context) { + if (app.isSingleDoc()) { return `/#/${context.path}`; } + context.page = 'about'; + this.triggerRoute('page'); + } + + news(context) { + if (app.isSingleDoc()) { return `/#/${context.path}`; } + context.page = 'news'; + this.triggerRoute('page'); + } + + help(context) { + if (app.isSingleDoc()) { return `/#/${context.path}`; } + context.page = 'help'; + this.triggerRoute('page'); + } + + notFound(context) { + this.triggerRoute('notFound'); + } + + isIndex() { + return ((this.context != null ? this.context.path : undefined) === '/') || (app.isSingleDoc() && __guard__(this.context != null ? this.context.entry : undefined, x => x.isIndex())); + } + + isSettings() { + return (this.context != null ? this.context.path : undefined) === '/settings'; + } + + setInitialPath() { + // Remove superfluous forward slashes at the beginning of the path + let path; + if ((path = location.pathname.replace(/^\/{2,}/g, '/')) !== location.pathname) { + page.replace(path + location.search + location.hash, null, true); + } + + if (location.pathname === '/') { + if (path = this.getInitialPathFromHash()) { + page.replace(path + location.search, null, true); + } else if (path = this.getInitialPathFromCookie()) { + page.replace(path + location.search + location.hash, null, true); + } + } + } + + getInitialPathFromHash() { + try { + return __guard__((new RegExp("#/(.+)")).exec(decodeURIComponent(location.hash)), x => x[1]); + } catch (error) {} + } + + getInitialPathFromCookie() { + let path; + if (path = Cookies.get('initial_path')) { + Cookies.expire('initial_path'); + return path; + } + } + + replaceHash(hash) { + page.replace(location.pathname + location.search + (hash || ''), null, true); + } +}); +Cls.initClass(); + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/assets/javascripts/app/searcher.js b/assets/javascripts/app/searcher.js index 79f6a304c3..27555a1619 100644 --- a/assets/javascripts/app/searcher.js +++ b/assets/javascripts/app/searcher.js @@ -1,292 +1,373 @@ -# -# Match functions -# - -SEPARATOR = '.' - -query = -queryLength = -value = -valueLength = -matcher = # current match function -fuzzyRegexp = # query fuzzy regexp -index = # position of the query in the string being matched -lastIndex = # last position of the query in the string being matched -match = # regexp match data -matchIndex = -matchLength = -score = # score for the current match -separators = # counter -i = null # cursor - -`function exactMatch() {` -index = value.indexOf(query) -return unless index >= 0 - -lastIndex = value.lastIndexOf(query) - -if index isnt lastIndex - return Math.max(scoreExactMatch(), ((index = lastIndex) and scoreExactMatch()) or 0) -else - return scoreExactMatch() -`}` - -`function scoreExactMatch() {` -# Remove one point for each unmatched character. -score = 100 - (valueLength - queryLength) - -if index > 0 - # If the character preceding the query is a dot, assign the same score - # as if the query was found at the beginning of the string, minus one. - if value.charAt(index - 1) is SEPARATOR - score += index - 1 - # Don't match a single-character query unless it's found at the beginning - # of the string or is preceded by a dot. - else if queryLength is 1 - return - # (1) Remove one point for each unmatched character up to the nearest - # preceding dot or the beginning of the string. - # (2) Remove one point for each unmatched character following the query. - else - i = index - 2 - i-- while i >= 0 and value.charAt(i) isnt SEPARATOR - score -= (index - i) + # (1) - (valueLength - queryLength - index) # (2) - - # Remove one point for each dot preceding the query, except for the one - # immediately before the query. - separators = 0 - i = index - 2 - while i >= 0 - separators++ if value.charAt(i) is SEPARATOR - i-- - score -= separators - -# Remove five points for each dot following the query. -separators = 0 -i = valueLength - queryLength - index - 1 -while i >= 0 - separators++ if value.charAt(index + queryLength + i) is SEPARATOR - i-- -score -= separators * 5 - -return Math.max 1, score -`}` - -`function fuzzyMatch() {` -return if valueLength <= queryLength or value.indexOf(query) >= 0 -return unless match = fuzzyRegexp.exec(value) -matchIndex = match.index -matchLength = match[0].length -score = scoreFuzzyMatch() -if match = fuzzyRegexp.exec(value.slice(i = value.lastIndexOf(SEPARATOR) + 1)) - matchIndex = i + match.index - matchLength = match[0].length - return Math.max(score, scoreFuzzyMatch()) -else - return score -`}` - -`function scoreFuzzyMatch() {` -# When the match is at the beginning of the string or preceded by a dot. -if matchIndex is 0 or value.charAt(matchIndex - 1) is SEPARATOR - return Math.max 66, 100 - matchLength -# When the match is at the end of the string. -else if matchIndex + matchLength is valueLength - return Math.max 33, 67 - matchLength -# When the match is in the middle of the string. -else - return Math.max 1, 34 - matchLength -`}` - -# -# Searchers -# - -class app.Searcher - $.extend @prototype, Events - - CHUNK_SIZE = 20000 - - DEFAULTS = - max_results: app.config.max_results - fuzzy_min_length: 3 - - SEPARATORS_REGEXP = /#|::|:-|->|\$(?=\w)|\-(?=\w)|\:(?=\w)|\ [\/\-&]\ |:\ |\ /g - EOS_SEPARATORS_REGEXP = /(\w)[\-:]$/ - INFO_PARANTHESES_REGEXP = /\ \(\w+?\)$/ - EMPTY_PARANTHESES_REGEXP = /\(\)/ - EVENT_REGEXP = /\ event$/ - DOT_REGEXP = /\.+/g - WHITESPACE_REGEXP = /\s/g - - EMPTY_STRING = '' - ELLIPSIS = '...' - STRING = 'string' - - @normalizeString: (string) -> - string - .toLowerCase() - .replace ELLIPSIS, EMPTY_STRING - .replace EVENT_REGEXP, EMPTY_STRING - .replace INFO_PARANTHESES_REGEXP, EMPTY_STRING - .replace SEPARATORS_REGEXP, SEPARATOR - .replace DOT_REGEXP, SEPARATOR - .replace EMPTY_PARANTHESES_REGEXP, EMPTY_STRING - .replace WHITESPACE_REGEXP, EMPTY_STRING - - @normalizeQuery: (string) -> - string = @normalizeString(string) - string.replace EOS_SEPARATORS_REGEXP, '$1.' - - constructor: (options = {}) -> - @options = $.extend {}, DEFAULTS, options - - find: (data, attr, q) -> - @kill() - - @data = data - @attr = attr - @query = q - @setup() - - if @isValid() then @match() else @end() - return - - setup: -> - query = @query = @constructor.normalizeQuery(@query) - queryLength = query.length - @dataLength = @data.length - @matchers = [exactMatch] - @totalResults = 0 - @setupFuzzy() - return - - setupFuzzy: -> - if queryLength >= @options.fuzzy_min_length - fuzzyRegexp = @queryToFuzzyRegexp(query) - @matchers.push(fuzzyMatch) - else - fuzzyRegexp = null - return - - isValid: -> - queryLength > 0 and query isnt SEPARATOR - - end: -> - @triggerResults [] unless @totalResults - @trigger 'end' - @free() - return - - kill: -> - if @timeout - clearTimeout @timeout - @free() - return - - free: -> - @data = @attr = @dataLength = @matchers = @matcher = @query = - @totalResults = @scoreMap = @cursor = @timeout = null - return - - match: => - if not @foundEnough() and @matcher = @matchers.shift() - @setupMatcher() - @matchChunks() - else - @end() - return - - setupMatcher: -> - @cursor = 0 - @scoreMap = new Array(101) - return - - matchChunks: => - @matchChunk() - - if @cursor is @dataLength or @scoredEnough() - @delay @match - @sendResults() - else - @delay @matchChunks - return - - matchChunk: -> - matcher = @matcher - for [0...@chunkSize()] - value = @data[@cursor][@attr] - if value.split # string - valueLength = value.length - @addResult(@data[@cursor], score) if score = matcher() - else # array - score = 0 - for value in @data[@cursor][@attr] - valueLength = value.length - score = Math.max(score, matcher() || 0) - @addResult(@data[@cursor], score) if score > 0 - @cursor++ - return - - chunkSize: -> - if @cursor + CHUNK_SIZE > @dataLength - @dataLength % CHUNK_SIZE - else - CHUNK_SIZE - - scoredEnough: -> - @scoreMap[100]?.length >= @options.max_results - - foundEnough: -> - @totalResults >= @options.max_results - - addResult: (object, score) -> - (@scoreMap[Math.round(score)] or= []).push(object) - @totalResults++ - return - - getResults: -> - results = [] - for objects in @scoreMap by -1 when objects - results.push.apply results, objects - results[0...@options.max_results] - - sendResults: -> - results = @getResults() - @triggerResults results if results.length - return - - triggerResults: (results) -> - @trigger 'results', results - return - - delay: (fn) -> - @timeout = setTimeout(fn, 1) - - queryToFuzzyRegexp: (string) -> - chars = string.split '' - chars[i] = $.escapeRegexp(char) for char, i in chars - new RegExp chars.join('.*?') # abc -> /a.*?b.*?c.*?/ - -class app.SynchronousSearcher extends app.Searcher - match: => - if @matcher - @allResults or= [] - @allResults.push.apply @allResults, @getResults() - super - - free: -> - @allResults = null - super - - end: -> - @sendResults true - super - - sendResults: (end) -> - if end and @allResults?.length - @triggerResults @allResults - - delay: (fn) -> - fn() +/* + * decaffeinate suggestions: + * DS002: Fix invalid constructor + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS202: Simplify dynamic range loops + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * DS209: Avoid top-level return + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +// +// Match functions +// + +let fuzzyRegexp, i, index, lastIndex, match, matcher, matchIndex, matchLength, queryLength, score, separators, value, valueLength; +const SEPARATOR = '.'; + +let query = +(queryLength = +(value = +(valueLength = +(matcher = // current match function +(fuzzyRegexp = // query fuzzy regexp +(index = // position of the query in the string being matched +(lastIndex = // last position of the query in the string being matched +(match = // regexp match data +(matchIndex = +(matchLength = +(score = // score for the current match +(separators = // counter +(i = null))))))))))))); // cursor + +function exactMatch() { +index = value.indexOf(query); +if (!(index >= 0)) { return; } + +lastIndex = value.lastIndexOf(query); + +if (index !== lastIndex) { + return Math.max(scoreExactMatch(), ((index = lastIndex) && scoreExactMatch()) || 0); +} else { + return scoreExactMatch(); +} +} + +function scoreExactMatch() { +// Remove one point for each unmatched character. +score = 100 - (valueLength - queryLength); + +if (index > 0) { + // If the character preceding the query is a dot, assign the same score + // as if the query was found at the beginning of the string, minus one. + if (value.charAt(index - 1) === SEPARATOR) { + score += index - 1; + // Don't match a single-character query unless it's found at the beginning + // of the string or is preceded by a dot. + } else if (queryLength === 1) { + return; + // (1) Remove one point for each unmatched character up to the nearest + // preceding dot or the beginning of the string. + // (2) Remove one point for each unmatched character following the query. + } else { + i = index - 2; + while ((i >= 0) && (value.charAt(i) !== SEPARATOR)) { i--; } + score -= (index - i) + // (1) + (valueLength - queryLength - index); // (2) + } + + // Remove one point for each dot preceding the query, except for the one + // immediately before the query. + separators = 0; + i = index - 2; + while (i >= 0) { + if (value.charAt(i) === SEPARATOR) { separators++; } + i--; + } + score -= separators; +} + +// Remove five points for each dot following the query. +separators = 0; +i = valueLength - queryLength - index - 1; +while (i >= 0) { + if (value.charAt(index + queryLength + i) === SEPARATOR) { separators++; } + i--; +} +score -= separators * 5; + +return Math.max(1, score); +} + +function fuzzyMatch() { +if ((valueLength <= queryLength) || (value.indexOf(query) >= 0)) { return; } +if (!(match = fuzzyRegexp.exec(value))) { return; } +matchIndex = match.index; +matchLength = match[0].length; +score = scoreFuzzyMatch(); +if (match = fuzzyRegexp.exec(value.slice(i = value.lastIndexOf(SEPARATOR) + 1))) { + matchIndex = i + match.index; + matchLength = match[0].length; + return Math.max(score, scoreFuzzyMatch()); +} else { + return score; +} +} + +function scoreFuzzyMatch() { +// When the match is at the beginning of the string or preceded by a dot. +if ((matchIndex === 0) || (value.charAt(matchIndex - 1) === SEPARATOR)) { + return Math.max(66, 100 - matchLength); +// When the match is at the end of the string. +} else if ((matchIndex + matchLength) === valueLength) { + return Math.max(33, 67 - matchLength); +// When the match is in the middle of the string. +} else { + return Math.max(1, 34 - matchLength); +} +} + +// +// Searchers +// + +(function() { + let CHUNK_SIZE = undefined; + let DEFAULTS = undefined; + let SEPARATORS_REGEXP = undefined; + let EOS_SEPARATORS_REGEXP = undefined; + let INFO_PARANTHESES_REGEXP = undefined; + let EMPTY_PARANTHESES_REGEXP = undefined; + let EVENT_REGEXP = undefined; + let DOT_REGEXP = undefined; + let WHITESPACE_REGEXP = undefined; + let EMPTY_STRING = undefined; + let ELLIPSIS = undefined; + let STRING = undefined; + const Cls = (app.Searcher = class Searcher { + static initClass() { + $.extend(this.prototype, Events); + + CHUNK_SIZE = 20000; + + DEFAULTS = { + max_results: app.config.max_results, + fuzzy_min_length: 3 + }; + + SEPARATORS_REGEXP = /#|::|:-|->|\$(?=\w)|\-(?=\w)|\:(?=\w)|\ [\/\-&]\ |:\ |\ /g; + EOS_SEPARATORS_REGEXP = /(\w)[\-:]$/; + INFO_PARANTHESES_REGEXP = /\ \(\w+?\)$/; + EMPTY_PARANTHESES_REGEXP = /\(\)/; + EVENT_REGEXP = /\ event$/; + DOT_REGEXP = /\.+/g; + WHITESPACE_REGEXP = /\s/g; + + EMPTY_STRING = ''; + ELLIPSIS = '...'; + STRING = 'string'; + } + + static normalizeString(string) { + return string + .toLowerCase() + .replace(ELLIPSIS, EMPTY_STRING) + .replace(EVENT_REGEXP, EMPTY_STRING) + .replace(INFO_PARANTHESES_REGEXP, EMPTY_STRING) + .replace(SEPARATORS_REGEXP, SEPARATOR) + .replace(DOT_REGEXP, SEPARATOR) + .replace(EMPTY_PARANTHESES_REGEXP, EMPTY_STRING) + .replace(WHITESPACE_REGEXP, EMPTY_STRING); + } + + static normalizeQuery(string) { + string = this.normalizeString(string); + return string.replace(EOS_SEPARATORS_REGEXP, '$1.'); + } + + constructor(options) { + this.match = this.match.bind(this); + this.matchChunks = this.matchChunks.bind(this); + if (options == null) { options = {}; } + this.options = $.extend({}, DEFAULTS, options); + } + + find(data, attr, q) { + this.kill(); + + this.data = data; + this.attr = attr; + this.query = q; + this.setup(); + + if (this.isValid()) { this.match(); } else { this.end(); } + } + + setup() { + query = (this.query = this.constructor.normalizeQuery(this.query)); + queryLength = query.length; + this.dataLength = this.data.length; + this.matchers = [exactMatch]; + this.totalResults = 0; + this.setupFuzzy(); + } + + setupFuzzy() { + if (queryLength >= this.options.fuzzy_min_length) { + fuzzyRegexp = this.queryToFuzzyRegexp(query); + this.matchers.push(fuzzyMatch); + } else { + fuzzyRegexp = null; + } + } + + isValid() { + return (queryLength > 0) && (query !== SEPARATOR); + } + + end() { + if (!this.totalResults) { this.triggerResults([]); } + this.trigger('end'); + this.free(); + } + + kill() { + if (this.timeout) { + clearTimeout(this.timeout); + this.free(); + } + } + + free() { + this.data = (this.attr = (this.dataLength = (this.matchers = (this.matcher = (this.query = + (this.totalResults = (this.scoreMap = (this.cursor = (this.timeout = null))))))))); + } + + match() { + if (!this.foundEnough() && (this.matcher = this.matchers.shift())) { + this.setupMatcher(); + this.matchChunks(); + } else { + this.end(); + } + } + + setupMatcher() { + this.cursor = 0; + this.scoreMap = new Array(101); + } + + matchChunks() { + this.matchChunk(); + + if ((this.cursor === this.dataLength) || this.scoredEnough()) { + this.delay(this.match); + this.sendResults(); + } else { + this.delay(this.matchChunks); + } + } + + matchChunk() { + ({ + matcher + } = this); + for (let j = 0, end = this.chunkSize(), asc = 0 <= end; asc ? j < end : j > end; asc ? j++ : j--) { + value = this.data[this.cursor][this.attr]; + if (value.split) { // string + valueLength = value.length; + if (score = matcher()) { this.addResult(this.data[this.cursor], score); } + } else { // array + score = 0; + for (value of Array.from(this.data[this.cursor][this.attr])) { + valueLength = value.length; + score = Math.max(score, matcher() || 0); + } + if (score > 0) { this.addResult(this.data[this.cursor], score); } + } + this.cursor++; + } + } + + chunkSize() { + if ((this.cursor + CHUNK_SIZE) > this.dataLength) { + return this.dataLength % CHUNK_SIZE; + } else { + return CHUNK_SIZE; + } + } + + scoredEnough() { + return (this.scoreMap[100] != null ? this.scoreMap[100].length : undefined) >= this.options.max_results; + } + + foundEnough() { + return this.totalResults >= this.options.max_results; + } + + addResult(object, score) { + let name; + (this.scoreMap[name = Math.round(score)] || (this.scoreMap[name] = [])).push(object); + this.totalResults++; + } + + getResults() { + const results = []; + for (let j = this.scoreMap.length - 1; j >= 0; j--) { + var objects = this.scoreMap[j]; + if (objects) { + results.push.apply(results, objects); + } + } + return results.slice(0, this.options.max_results); + } + + sendResults() { + const results = this.getResults(); + if (results.length) { this.triggerResults(results); } + } + + triggerResults(results) { + this.trigger('results', results); + } + + delay(fn) { + return this.timeout = setTimeout(fn, 1); + } + + queryToFuzzyRegexp(string) { + const chars = string.split(''); + for (i = 0; i < chars.length; i++) { var char = chars[i]; chars[i] = $.escapeRegexp(char); } + return new RegExp(chars.join('.*?')); + } + }); + Cls.initClass(); + return Cls; // abc -> /a.*?b.*?c.*?/ +})(); + +app.SynchronousSearcher = class SynchronousSearcher extends app.Searcher { + constructor(...args) { + this.match = this.match.bind(this); + super(...args); + } + + match() { + if (this.matcher) { + if (!this.allResults) { this.allResults = []; } + this.allResults.push.apply(this.allResults, this.getResults()); + } + return super.match(...arguments); + } + + free() { + this.allResults = null; + return super.free(...arguments); + } + + end() { + this.sendResults(true); + return super.end(...arguments); + } + + sendResults(end) { + if (end && (this.allResults != null ? this.allResults.length : undefined)) { + return this.triggerResults(this.allResults); + } + } + + delay(fn) { + return fn(); + } +}; diff --git a/assets/javascripts/app/serviceworker.js b/assets/javascripts/app/serviceworker.js index 4023556658..388bdcbf1f 100644 --- a/assets/javascripts/app/serviceworker.js +++ b/assets/javascripts/app/serviceworker.js @@ -1,49 +1,66 @@ -class app.ServiceWorker - $.extend @prototype, Events +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const Cls = (app.ServiceWorker = class ServiceWorker { + static initClass() { + $.extend(this.prototype, Events); + } - @isEnabled: -> - !!navigator.serviceWorker and app.config.service_worker_enabled + static isEnabled() { + return !!navigator.serviceWorker && app.config.service_worker_enabled; + } - constructor: -> - @registration = null - @notifyUpdate = true + constructor() { + this.onUpdateFound = this.onUpdateFound.bind(this); + this.onStateChange = this.onStateChange.bind(this); + this.registration = null; + this.notifyUpdate = true; navigator.serviceWorker.register(app.config.service_worker_path, {scope: '/'}) .then( - (registration) => @updateRegistration(registration), - (error) -> console.error('Could not register service worker:', error) - ) - - update: -> - return unless @registration - @notifyUpdate = true - return @registration.update().catch(->) - - updateInBackground: -> - return unless @registration - @notifyUpdate = false - return @registration.update().catch(->) - - reload: -> - return @updateInBackground().then(() -> app.reboot()) - - updateRegistration: (registration) -> - @registration = registration - $.on @registration, 'updatefound', @onUpdateFound - return - - onUpdateFound: => - $.off @installingRegistration, 'statechange', @onStateChange() if @installingRegistration - @installingRegistration = @registration.installing - $.on @installingRegistration, 'statechange', @onStateChange - return - - onStateChange: => - if @installingRegistration and @installingRegistration.state == 'installed' and navigator.serviceWorker.controller - @installingRegistration = null - @onUpdateReady() - return - - onUpdateReady: -> - @trigger 'updateready' if @notifyUpdate - return + registration => this.updateRegistration(registration), + error => console.error('Could not register service worker:', error)); + } + + update() { + if (!this.registration) { return; } + this.notifyUpdate = true; + return this.registration.update().catch(function() {}); + } + + updateInBackground() { + if (!this.registration) { return; } + this.notifyUpdate = false; + return this.registration.update().catch(function() {}); + } + + reload() { + return this.updateInBackground().then(() => app.reboot()); + } + + updateRegistration(registration) { + this.registration = registration; + $.on(this.registration, 'updatefound', this.onUpdateFound); + } + + onUpdateFound() { + if (this.installingRegistration) { $.off(this.installingRegistration, 'statechange', this.onStateChange()); } + this.installingRegistration = this.registration.installing; + $.on(this.installingRegistration, 'statechange', this.onStateChange); + } + + onStateChange() { + if (this.installingRegistration && (this.installingRegistration.state === 'installed') && navigator.serviceWorker.controller) { + this.installingRegistration = null; + this.onUpdateReady(); + } + } + + onUpdateReady() { + if (this.notifyUpdate) { this.trigger('updateready'); } + } +}); +Cls.initClass(); diff --git a/assets/javascripts/app/settings.js b/assets/javascripts/app/settings.js index 74e32a658c..aed1135fc9 100644 --- a/assets/javascripts/app/settings.js +++ b/assets/javascripts/app/settings.js @@ -1,170 +1,219 @@ -class app.Settings - PREFERENCE_KEYS = [ - 'hideDisabled' - 'hideIntro' - 'manualUpdate' - 'fastScroll' - 'arrowScroll' - 'analyticsConsent' - 'docs' - 'dark' # legacy - 'theme' - 'layout' - 'size' - 'tips' - 'noAutofocus' - 'autoInstall' - 'spaceScroll' - 'spaceTimeout' - ] - - INTERNAL_KEYS = [ - 'count' - 'schema' - 'version' - 'news' - ] - - LAYOUTS: [ - '_max-width' - '_sidebar-hidden' - '_native-scrollbars' - '_text-justify-hyphenate' - ] - - @defaults: - count: 0 - hideDisabled: false - hideIntro: false - news: 0 - manualUpdate: false - schema: 1 - analyticsConsent: false - theme: 'auto' - spaceScroll: 1 - spaceTimeout: 0.5 - - constructor: -> - @store = new CookiesStore - @cache = {} - @autoSupported = window.matchMedia('(prefers-color-scheme)').media != 'not all' - if @autoSupported - @darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)') - @darkModeQuery.addListener => @setTheme(@get('theme')) - - - get: (key) -> - return @cache[key] if @cache.hasOwnProperty(key) - @cache[key] = @store.get(key) ? @constructor.defaults[key] - if key == 'theme' and @cache[key] == 'auto' and !@darkModeQuery - @cache[key] = 'default' - else - @cache[key] - - set: (key, value) -> - @store.set(key, value) - delete @cache[key] - @setTheme(value) if key == 'theme' - return - - del: (key) -> - @store.del(key) - delete @cache[key] - return - - hasDocs: -> - try !!@store.get('docs') - - getDocs: -> - @store.get('docs')?.split('/') or app.config.default_docs - - setDocs: (docs) -> - @set 'docs', docs.join('/') - return - - getTips: -> - @store.get('tips')?.split('/') or [] - - setTips: (tips) -> - @set 'tips', tips.join('/') - return - - setLayout: (name, enable) -> - @toggleLayout(name, enable) - - layout = (@store.get('layout') || '').split(' ') - $.arrayDelete(layout, '') - - if enable - layout.push(name) if layout.indexOf(name) is -1 - else - $.arrayDelete(layout, name) - - if layout.length > 0 - @set 'layout', layout.join(' ') - else - @del 'layout' - return - - hasLayout: (name) -> - layout = (@store.get('layout') || '').split(' ') - layout.indexOf(name) isnt -1 - - setSize: (value) -> - @set 'size', value - return - - dump: -> - @store.dump() - - export: -> - data = @dump() - delete data[key] for key in INTERNAL_KEYS - data - - import: (data) -> - for key, value of @export() - @del key unless data.hasOwnProperty(key) - for key, value of data - @set key, value if PREFERENCE_KEYS.indexOf(key) isnt -1 - return - - reset: -> - @store.reset() - @cache = {} - return - - initLayout: -> - if @get('dark') is 1 - @set('theme', 'dark') - @del 'dark' - @setTheme(@get('theme')) - @toggleLayout(layout, @hasLayout(layout)) for layout in @LAYOUTS - @initSidebarWidth() - return - - setTheme: (theme) -> - if theme is 'auto' - theme = if @darkModeQuery.matches then 'dark' else 'default' - classList = document.documentElement.classList - classList.remove('_theme-default', '_theme-dark') - classList.add('_theme-' + theme) - @updateColorMeta() - return - - updateColorMeta: -> - color = getComputedStyle(document.documentElement).getPropertyValue('--headerBackground').trim() - $('meta[name=theme-color]').setAttribute('content', color) - return - - toggleLayout: (layout, enable) -> - classList = document.body.classList - # sidebar is always shown for settings; its state is updated in app.views.Settings - classList.toggle(layout, enable) unless layout is '_sidebar-hidden' and app.router?.isSettings - classList.toggle('_overlay-scrollbars', $.overlayScrollbarsEnabled()) - return - - initSidebarWidth: -> - size = @get('size') - document.documentElement.style.setProperty('--sidebarWidth', size + 'px') if size - return +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining + * DS104: Avoid inline assignments + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +(function() { + let PREFERENCE_KEYS = undefined; + let INTERNAL_KEYS = undefined; + const Cls = (app.Settings = class Settings { + static initClass() { + PREFERENCE_KEYS = [ + 'hideDisabled', + 'hideIntro', + 'manualUpdate', + 'fastScroll', + 'arrowScroll', + 'analyticsConsent', + 'docs', + 'dark', // legacy + 'theme', + 'layout', + 'size', + 'tips', + 'noAutofocus', + 'autoInstall', + 'spaceScroll', + 'spaceTimeout' + ]; + + INTERNAL_KEYS = [ + 'count', + 'schema', + 'version', + 'news' + ]; + + this.prototype.LAYOUTS = [ + '_max-width', + '_sidebar-hidden', + '_native-scrollbars', + '_text-justify-hyphenate' + ]; + + this.defaults = { + count: 0, + hideDisabled: false, + hideIntro: false, + news: 0, + manualUpdate: false, + schema: 1, + analyticsConsent: false, + theme: 'auto', + spaceScroll: 1, + spaceTimeout: 0.5 + }; + } + + constructor() { + this.store = new CookiesStore; + this.cache = {}; + this.autoSupported = window.matchMedia('(prefers-color-scheme)').media !== 'not all'; + if (this.autoSupported) { + this.darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)'); + this.darkModeQuery.addListener(() => this.setTheme(this.get('theme'))); + } + } + + + get(key) { + let left; + if (this.cache.hasOwnProperty(key)) { return this.cache[key]; } + this.cache[key] = (left = this.store.get(key)) != null ? left : this.constructor.defaults[key]; + if ((key === 'theme') && (this.cache[key] === 'auto') && !this.darkModeQuery) { + return this.cache[key] = 'default'; + } else { + return this.cache[key]; + } + } + + set(key, value) { + this.store.set(key, value); + delete this.cache[key]; + if (key === 'theme') { this.setTheme(value); } + } + + del(key) { + this.store.del(key); + delete this.cache[key]; + } + + hasDocs() { + try { return !!this.store.get('docs'); } catch (error) {} + } + + getDocs() { + return __guard__(this.store.get('docs'), x => x.split('/')) || app.config.default_docs; + } + + setDocs(docs) { + this.set('docs', docs.join('/')); + } + + getTips() { + return __guard__(this.store.get('tips'), x => x.split('/')) || []; + } + + setTips(tips) { + this.set('tips', tips.join('/')); + } + + setLayout(name, enable) { + this.toggleLayout(name, enable); + + const layout = (this.store.get('layout') || '').split(' '); + $.arrayDelete(layout, ''); + + if (enable) { + if (layout.indexOf(name) === -1) { layout.push(name); } + } else { + $.arrayDelete(layout, name); + } + + if (layout.length > 0) { + this.set('layout', layout.join(' ')); + } else { + this.del('layout'); + } + } + + hasLayout(name) { + const layout = (this.store.get('layout') || '').split(' '); + return layout.indexOf(name) !== -1; + } + + setSize(value) { + this.set('size', value); + } + + dump() { + return this.store.dump(); + } + + export() { + const data = this.dump(); + for (var key of Array.from(INTERNAL_KEYS)) { delete data[key]; } + return data; + } + + import(data) { + let key, value; + const object = this.export(); + for (key in object) { + value = object[key]; + if (!data.hasOwnProperty(key)) { this.del(key); } + } + for (key in data) { + value = data[key]; + if (PREFERENCE_KEYS.indexOf(key) !== -1) { this.set(key, value); } + } + } + + reset() { + this.store.reset(); + this.cache = {}; + } + + initLayout() { + if (this.get('dark') === 1) { + this.set('theme', 'dark'); + this.del('dark'); + } + this.setTheme(this.get('theme')); + for (var layout of Array.from(this.LAYOUTS)) { this.toggleLayout(layout, this.hasLayout(layout)); } + this.initSidebarWidth(); + } + + setTheme(theme) { + if (theme === 'auto') { + theme = this.darkModeQuery.matches ? 'dark' : 'default'; + } + const { + classList + } = document.documentElement; + classList.remove('_theme-default', '_theme-dark'); + classList.add('_theme-' + theme); + this.updateColorMeta(); + } + + updateColorMeta() { + const color = getComputedStyle(document.documentElement).getPropertyValue('--headerBackground').trim(); + $('meta[name=theme-color]').setAttribute('content', color); + } + + toggleLayout(layout, enable) { + const { + classList + } = document.body; + // sidebar is always shown for settings; its state is updated in app.views.Settings + if ((layout !== '_sidebar-hidden') || !(app.router != null ? app.router.isSettings : undefined)) { classList.toggle(layout, enable); } + classList.toggle('_overlay-scrollbars', $.overlayScrollbarsEnabled()); + } + + initSidebarWidth() { + const size = this.get('size'); + if (size) { document.documentElement.style.setProperty('--sidebarWidth', size + 'px'); } + } + }); + Cls.initClass(); + return Cls; +})(); + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/assets/javascripts/app/shortcuts.js b/assets/javascripts/app/shortcuts.js index 28ddf0b8a2..5ae46708ea 100644 --- a/assets/javascripts/app/shortcuts.js +++ b/assets/javascripts/app/shortcuts.js @@ -1,193 +1,259 @@ -class app.Shortcuts - $.extend @prototype, Events - - constructor: -> - @isMac = $.isMac() - @start() - - start: -> - $.on document, 'keydown', @onKeydown - $.on document, 'keypress', @onKeypress - return - - stop: -> - $.off document, 'keydown', @onKeydown - $.off document, 'keypress', @onKeypress - return - - swapArrowKeysBehavior: -> - app.settings.get('arrowScroll') - - spaceScroll: -> - app.settings.get('spaceScroll') - - showTip: -> - app.showTip('KeyNav') - @showTip = null - - spaceTimeout: -> - app.settings.get('spaceTimeout') - - onKeydown: (event) => - return if @buggyEvent(event) - result = if event.ctrlKey or event.metaKey - @handleKeydownSuperEvent event unless event.altKey or event.shiftKey - else if event.shiftKey - @handleKeydownShiftEvent event unless event.altKey - else if event.altKey - @handleKeydownAltEvent event - else - @handleKeydownEvent event - - event.preventDefault() if result is false - return - - onKeypress: (event) => - return if @buggyEvent(event) or (event.charCode == 63 and document.activeElement.tagName == 'INPUT') - unless event.ctrlKey or event.metaKey - result = @handleKeypressEvent event - event.preventDefault() if result is false - return - - 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 - - switch event.which - when 8 - @trigger 'typing' unless event.target.form - when 13 - @trigger 'enter' - when 27 - @trigger 'escape' - false - when 32 - if event.target.type is 'search' and @spaceScroll() and (not @lastKeypress or @lastKeypress < Date.now() - (@spaceTimeout() * 1000)) - @trigger 'pageDown' - false - when 33 - @trigger 'pageUp' - when 34 - @trigger 'pageDown' - when 35 - @trigger 'pageBottom' unless event.target.form - when 36 - @trigger 'pageTop' unless event.target.form - when 37 - @trigger 'left' unless event.target.value - when 38 - @trigger 'up' - @showTip?() - false - when 39 - @trigger 'right' unless event.target.value - when 40 - @trigger 'down' - @showTip?() - false - when 191 - unless event.target.form - @trigger 'typing' - false - - handleKeydownSuperEvent: (event) -> - switch event.which - when 13 - @trigger 'superEnter' - when 37 - if @isMac - @trigger 'superLeft' - false - when 38 - @trigger 'pageTop' - false - when 39 - if @isMac - @trigger 'superRight' - false - 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() - - if not event.target.form and 65 <= event.which <= 90 - @trigger 'typing' - return - - switch event.which - when 32 - @trigger 'pageUp' - false - when 38 - unless getSelection()?.toString() - @trigger 'altUp' - false - when 40 - unless getSelection()?.toString() - @trigger 'altDown' - false - - 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 - when 37 - unless @isMac - @trigger 'superLeft' - false - when 38 - @trigger 'altUp' - false - when 39 - unless @isMac - @trigger 'superRight' - false - when 40 - @trigger 'altDown' - false - when 67 - @trigger 'altC' - false - when 68 - @trigger 'altD' - false - when 70 - @trigger 'altF', event - when 71 - @trigger 'altG' - false - when 79 - @trigger 'altO' - false - when 82 - @trigger 'altR' - false - when 83 - @trigger 'altS' - false - - handleKeypressEvent: (event) -> - if event.which is 63 and not event.target.value - @trigger 'help' - false - else - @lastKeypress = Date.now() - - buggyEvent: (event) -> - try - event.target - event.ctrlKey - event.which - return false - catch - return true +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining + * DS205: Consider reworking code to avoid use of IIFEs + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const Cls = (app.Shortcuts = class Shortcuts { + static initClass() { + $.extend(this.prototype, Events); + } + + constructor() { + this.onKeydown = this.onKeydown.bind(this); + this.onKeypress = this.onKeypress.bind(this); + this.isMac = $.isMac(); + this.start(); + } + + start() { + $.on(document, 'keydown', this.onKeydown); + $.on(document, 'keypress', this.onKeypress); + } + + stop() { + $.off(document, 'keydown', this.onKeydown); + $.off(document, 'keypress', this.onKeypress); + } + + swapArrowKeysBehavior() { + return app.settings.get('arrowScroll'); + } + + spaceScroll() { + return app.settings.get('spaceScroll'); + } + + showTip() { + app.showTip('KeyNav'); + return this.showTip = null; + } + + spaceTimeout() { + return app.settings.get('spaceTimeout'); + } + + onKeydown(event) { + if (this.buggyEvent(event)) { return; } + const result = (() => { + if (event.ctrlKey || event.metaKey) { + if (!event.altKey && !event.shiftKey) { return this.handleKeydownSuperEvent(event); } + } else if (event.shiftKey) { + if (!event.altKey) { return this.handleKeydownShiftEvent(event); } + } else if (event.altKey) { + return this.handleKeydownAltEvent(event); + } else { + return this.handleKeydownEvent(event); + } + })(); + + if (result === false) { event.preventDefault(); } + } + + onKeypress(event) { + if (this.buggyEvent(event) || ((event.charCode === 63) && (document.activeElement.tagName === 'INPUT'))) { return; } + if (!event.ctrlKey && !event.metaKey) { + const result = this.handleKeypressEvent(event); + if (result === false) { event.preventDefault(); } + } + } + + handleKeydownEvent(event, _force) { + if (!_force && [37, 38, 39, 40].includes(event.which) && this.swapArrowKeysBehavior()) { return this.handleKeydownAltEvent(event, true); } + + if (!event.target.form && ((48 <= event.which && event.which <= 57) || (65 <= event.which && event.which <= 90))) { + this.trigger('typing'); + return; + } + + switch (event.which) { + case 8: + if (!event.target.form) { return this.trigger('typing'); } + break; + case 13: + return this.trigger('enter'); + case 27: + this.trigger('escape'); + return false; + case 32: + if ((event.target.type === 'search') && this.spaceScroll() && (!this.lastKeypress || (this.lastKeypress < (Date.now() - (this.spaceTimeout() * 1000))))) { + this.trigger('pageDown'); + return false; + } + break; + case 33: + return this.trigger('pageUp'); + case 34: + return this.trigger('pageDown'); + case 35: + if (!event.target.form) { return this.trigger('pageBottom'); } + break; + case 36: + if (!event.target.form) { return this.trigger('pageTop'); } + break; + case 37: + if (!event.target.value) { return this.trigger('left'); } + break; + case 38: + this.trigger('up'); + if (typeof this.showTip === 'function') { + this.showTip(); + } + return false; + case 39: + if (!event.target.value) { return this.trigger('right'); } + break; + case 40: + this.trigger('down'); + if (typeof this.showTip === 'function') { + this.showTip(); + } + return false; + case 191: + if (!event.target.form) { + this.trigger('typing'); + return false; + } + break; + } + } + + handleKeydownSuperEvent(event) { + switch (event.which) { + case 13: + return this.trigger('superEnter'); + case 37: + if (this.isMac) { + this.trigger('superLeft'); + return false; + } + break; + case 38: + this.trigger('pageTop'); + return false; + case 39: + if (this.isMac) { + this.trigger('superRight'); + return false; + } + break; + case 40: + this.trigger('pageBottom'); + return false; + case 188: + this.trigger('preferences'); + return false; + } + } + + handleKeydownShiftEvent(event, _force) { + if (!_force && [37, 38, 39, 40].includes(event.which) && this.swapArrowKeysBehavior()) { return this.handleKeydownEvent(event, true); } + + if (!event.target.form && (65 <= event.which && event.which <= 90)) { + this.trigger('typing'); + return; + } + + switch (event.which) { + case 32: + this.trigger('pageUp'); + return false; + case 38: + if (!__guard__(getSelection(), x => x.toString())) { + this.trigger('altUp'); + return false; + } + break; + case 40: + if (!__guard__(getSelection(), x1 => x1.toString())) { + this.trigger('altDown'); + return false; + } + break; + } + } + + handleKeydownAltEvent(event, _force) { + if (!_force && [37, 38, 39, 40].includes(event.which) && this.swapArrowKeysBehavior()) { return this.handleKeydownEvent(event, true); } + + switch (event.which) { + case 9: + return this.trigger('altRight', event); + case 37: + if (!this.isMac) { + this.trigger('superLeft'); + return false; + } + break; + case 38: + this.trigger('altUp'); + return false; + case 39: + if (!this.isMac) { + this.trigger('superRight'); + return false; + } + break; + case 40: + this.trigger('altDown'); + return false; + case 67: + this.trigger('altC'); + return false; + case 68: + this.trigger('altD'); + return false; + case 70: + return this.trigger('altF', event); + case 71: + this.trigger('altG'); + return false; + case 79: + this.trigger('altO'); + return false; + case 82: + this.trigger('altR'); + return false; + case 83: + this.trigger('altS'); + return false; + } + } + + handleKeypressEvent(event) { + if ((event.which === 63) && !event.target.value) { + this.trigger('help'); + return false; + } else { + return this.lastKeypress = Date.now(); + } + } + + buggyEvent(event) { + try { + event.target; + event.ctrlKey; + event.which; + return false; + } catch (error) { + return true; + } + } +}); +Cls.initClass(); + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/assets/javascripts/app/update_checker.js b/assets/javascripts/app/update_checker.js index 3558d6bc4e..09538adced 100644 --- a/assets/javascripts/app/update_checker.js +++ b/assets/javascripts/app/update_checker.js @@ -1,39 +1,54 @@ -class app.UpdateChecker - constructor: -> - @lastCheck = Date.now() +/* + * decaffeinate suggestions: + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +app.UpdateChecker = class UpdateChecker { + constructor() { + this.checkDocs = this.checkDocs.bind(this); + this.onFocus = this.onFocus.bind(this); + this.lastCheck = Date.now(); - $.on window, 'focus', @onFocus - app.serviceWorker?.on 'updateready', @onUpdateReady + $.on(window, 'focus', this.onFocus); + if (app.serviceWorker != null) { + app.serviceWorker.on('updateready', this.onUpdateReady); + } - setTimeout @checkDocs, 0 + setTimeout(this.checkDocs, 0); + } - check: -> - if app.serviceWorker - app.serviceWorker.update() - else - ajax - url: $('script[src*="application"]').getAttribute('src') - dataType: 'application/javascript' - error: (_, xhr) => @onUpdateReady() if xhr.status is 404 - return + check() { + if (app.serviceWorker) { + app.serviceWorker.update(); + } else { + ajax({ + url: $('script[src*="application"]').getAttribute('src'), + dataType: 'application/javascript', + error: (_, xhr) => { if (xhr.status === 404) { return this.onUpdateReady(); } } + }); + } + } - onUpdateReady: -> - new app.views.Notif 'UpdateReady', autoHide: null - return + onUpdateReady() { + new app.views.Notif('UpdateReady', {autoHide: null}); + } - checkDocs: => - unless app.settings.get('manualUpdate') - app.docs.updateInBackground() - else - app.docs.checkForUpdates (i) => @onDocsUpdateReady() if i > 0 - return + checkDocs() { + if (!app.settings.get('manualUpdate')) { + app.docs.updateInBackground(); + } else { + app.docs.checkForUpdates(i => { if (i > 0) { return this.onDocsUpdateReady(); } }); + } + } - onDocsUpdateReady: -> - new app.views.Notif 'UpdateDocs', autoHide: null - return + onDocsUpdateReady() { + new app.views.Notif('UpdateDocs', {autoHide: null}); + } - onFocus: => - if Date.now() - @lastCheck > 21600e3 - @lastCheck = Date.now() - @check() - return + onFocus() { + if ((Date.now() - this.lastCheck) > 21600e3) { + this.lastCheck = Date.now(); + this.check(); + } + } +}; diff --git a/assets/javascripts/application.js.js b/assets/javascripts/application.js.js index 6bf87f1ce8..c06b434dd2 100644 --- a/assets/javascripts/application.js.js +++ b/assets/javascripts/application.js.js @@ -1,31 +1,38 @@ -#= require_tree ./vendor +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +//= require_tree ./vendor -#= require lib/license -#= require_tree ./lib +//= require lib/license +//= require_tree ./lib -#= require app/app -#= require app/config -#= require_tree ./app +//= require app/app +//= require app/config +//= require_tree ./app -#= require collections/collection -#= require_tree ./collections +//= require collections/collection +//= require_tree ./collections -#= require models/model -#= require_tree ./models +//= require models/model +//= require_tree ./models -#= require views/view -#= require_tree ./views +//= require views/view +//= require_tree ./views -#= require_tree ./templates +//= require_tree ./templates -#= require tracking +//= require tracking -init = -> - document.removeEventListener 'DOMContentLoaded', init, false +var init = function() { + document.removeEventListener('DOMContentLoaded', init, false); - if document.body - app.init() - else - setTimeout(init, 42) + if (document.body) { + return app.init(); + } else { + return setTimeout(init, 42); + } +}; -document.addEventListener 'DOMContentLoaded', init, false +document.addEventListener('DOMContentLoaded', init, false); diff --git a/assets/javascripts/collections/collection.js b/assets/javascripts/collections/collection.js index b902a498ef..2b857c7311 100644 --- a/assets/javascripts/collections/collection.js +++ b/assets/javascripts/collections/collection.js @@ -1,55 +1,75 @@ -class app.Collection - constructor: (objects = []) -> - @reset objects - - model: -> - app.models[@constructor.model] - - reset: (objects = []) -> - @models = [] - @add object for object in objects - return - - add: (object) -> - if object instanceof app.Model - @models.push object - else if object instanceof Array - @add obj for obj in object - else if object instanceof app.Collection - @models.push object.all()... - else - @models.push new (@model())(object) - return - - remove: (model) -> - @models.splice @models.indexOf(model), 1 - return - - size: -> - @models.length - - isEmpty: -> - @models.length is 0 - - each: (fn) -> - fn(model) for model in @models - return - - all: -> - @models - - contains: (model) -> - @models.indexOf(model) >= 0 - - findBy: (attr, value) -> - for model in @models - return model if model[attr] is value - return - - findAllBy: (attr, value) -> - model for model in @models when model[attr] is value - - countAllBy: (attr, value) -> - i = 0 - i += 1 for model in @models when model[attr] is value - i +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +app.Collection = class Collection { + constructor(objects) { + if (objects == null) { objects = []; } + this.reset(objects); + } + + model() { + return app.models[this.constructor.model]; + } + + reset(objects) { + if (objects == null) { objects = []; } + this.models = []; + for (var object of Array.from(objects)) { this.add(object); } + } + + add(object) { + if (object instanceof app.Model) { + this.models.push(object); + } else if (object instanceof Array) { + for (var obj of Array.from(object)) { this.add(obj); } + } else if (object instanceof app.Collection) { + this.models.push(...Array.from(object.all() || [])); + } else { + this.models.push(new (this.model())(object)); + } + } + + remove(model) { + this.models.splice(this.models.indexOf(model), 1); + } + + size() { + return this.models.length; + } + + isEmpty() { + return this.models.length === 0; + } + + each(fn) { + for (var model of Array.from(this.models)) { fn(model); } + } + + all() { + return this.models; + } + + contains(model) { + return this.models.indexOf(model) >= 0; + } + + findBy(attr, value) { + for (var model of Array.from(this.models)) { + if (model[attr] === value) { return model; } + } + } + + findAllBy(attr, value) { + return Array.from(this.models).filter((model) => model[attr] === value); + } + + countAllBy(attr, value) { + let i = 0; + for (var model of Array.from(this.models)) { if (model[attr] === value) { i += 1; } } + return i; + } +}; diff --git a/assets/javascripts/collections/docs.js b/assets/javascripts/collections/docs.js index d76e0f07ef..99ef32745d 100644 --- a/assets/javascripts/collections/docs.js +++ b/assets/javascripts/collections/docs.js @@ -1,85 +1,117 @@ -class app.collections.Docs extends app.Collection - @model: 'Doc' +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS202: Simplify dynamic range loops + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +(function() { + let NORMALIZE_VERSION_RGX = undefined; + let NORMALIZE_VERSION_SUB = undefined; + let CONCURRENCY = undefined; + const Cls = (app.collections.Docs = class Docs extends app.Collection { + static initClass() { + this.model = 'Doc'; + + NORMALIZE_VERSION_RGX = /\.(\d)$/; + NORMALIZE_VERSION_SUB = '.0$1'; + + // Load models concurrently. + // It's not pretty but I didn't want to import a promise library only for this. + CONCURRENCY = 3; + } - findBySlug: (slug) -> - @findBy('slug', slug) or @findBy('slug_without_version', slug) + findBySlug(slug) { + return this.findBy('slug', slug) || this.findBy('slug_without_version', slug); + } + sort() { + return this.models.sort(function(a, b) { + if (a.name === b.name) { + if (!a.version || (a.version.replace(NORMALIZE_VERSION_RGX, NORMALIZE_VERSION_SUB) > b.version.replace(NORMALIZE_VERSION_RGX, NORMALIZE_VERSION_SUB))) { + return -1; + } else { + return 1; + } + } else if (a.name.toLowerCase() > b.name.toLowerCase()) { + return 1; + } else { + return -1; + } + }); + } + load(onComplete, onError, options) { + let i = 0; - NORMALIZE_VERSION_RGX = /\.(\d)$/ - NORMALIZE_VERSION_SUB = '.0$1' - sort: -> - @models.sort (a, b) -> - if a.name is b.name - if not a.version or a.version.replace(NORMALIZE_VERSION_RGX, NORMALIZE_VERSION_SUB) > b.version.replace(NORMALIZE_VERSION_RGX, NORMALIZE_VERSION_SUB) - -1 - else - 1 - else if a.name.toLowerCase() > b.name.toLowerCase() - 1 - else - -1 + var next = () => { + if (i < this.models.length) { + this.models[i].load(next, fail, options); + } else if (i === ((this.models.length + CONCURRENCY) - 1)) { + onComplete(); + } + i++; + }; - # Load models concurrently. - # It's not pretty but I didn't want to import a promise library only for this. - CONCURRENCY = 3 - load: (onComplete, onError, options) -> - i = 0 + var fail = function(...args) { + if (onError) { + onError(...Array.from(args || [])); + onError = null; + } + next(); + }; - next = => - if i < @models.length - @models[i].load(next, fail, options) - else if i is @models.length + CONCURRENCY - 1 - onComplete() - i++ - return + for (let j = 0, end = CONCURRENCY, asc = 0 <= end; asc ? j < end : j > end; asc ? j++ : j--) { next(); } + } - fail = (args...) -> - if onError - onError(args...) - onError = null - next() - return + clearCache() { + for (var doc of Array.from(this.models)) { doc.clearCache(); } + } - next() for [0...CONCURRENCY] - return + uninstall(callback) { + let i = 0; + var next = () => { + if (i < this.models.length) { + this.models[i++].uninstall(next, next); + } else { + callback(); + } + }; + next(); + } - clearCache: -> - doc.clearCache() for doc in @models - return + getInstallStatuses(callback) { + app.db.versions(this.models, function(statuses) { + if (statuses) { + for (var key in statuses) { + var value = statuses[key]; + statuses[key] = {installed: !!value, mtime: value}; + } + } + callback(statuses); + }); + } - uninstall: (callback) -> - i = 0 - next = => - if i < @models.length - @models[i++].uninstall(next, next) - else - callback() - return - next() - return + checkForUpdates(callback) { + this.getInstallStatuses(statuses => { + let i = 0; + if (statuses) { + for (var slug in statuses) { var status = statuses[slug]; if (this.findBy('slug', slug).isOutdated(status)) { i += 1; } } + } + callback(i); + }); + } - getInstallStatuses: (callback) -> - app.db.versions @models, (statuses) -> - if statuses - for key, value of statuses - statuses[key] = installed: !!value, mtime: value - callback(statuses) - return - return - - checkForUpdates: (callback) -> - @getInstallStatuses (statuses) => - i = 0 - if statuses - i += 1 for slug, status of statuses when @findBy('slug', slug).isOutdated(status) - callback(i) - return - return - - updateInBackground: -> - @getInstallStatuses (statuses) => - return unless statuses - for slug, status of statuses - doc = @findBy 'slug', slug - doc.install($.noop, $.noop) if doc.isOutdated(status) - return - return + updateInBackground() { + this.getInstallStatuses(statuses => { + if (!statuses) { return; } + for (var slug in statuses) { + var status = statuses[slug]; + var doc = this.findBy('slug', slug); + if (doc.isOutdated(status)) { doc.install($.noop, $.noop); } + } + }); + } + }); + Cls.initClass(); + return Cls; +})(); diff --git a/assets/javascripts/collections/entries.js b/assets/javascripts/collections/entries.js index f978b68be4..9a1b6dd887 100644 --- a/assets/javascripts/collections/entries.js +++ b/assets/javascripts/collections/entries.js @@ -1,2 +1,11 @@ -class app.collections.Entries extends app.Collection - @model: 'Entry' +/* + * decaffeinate suggestions: + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const Cls = (app.collections.Entries = class Entries extends app.Collection { + static initClass() { + this.model = 'Entry'; + } +}); +Cls.initClass(); diff --git a/assets/javascripts/collections/types.js b/assets/javascripts/collections/types.js index 8e76eeab34..2d34f2410b 100644 --- a/assets/javascripts/collections/types.js +++ b/assets/javascripts/collections/types.js @@ -1,19 +1,41 @@ -class app.collections.Types extends app.Collection - @model: 'Type' +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +(function() { + let GUIDES_RGX = undefined; + let APPENDIX_RGX = undefined; + const Cls = (app.collections.Types = class Types extends app.Collection { + static initClass() { + this.model = 'Type'; + + GUIDES_RGX = /(^|\()(guides?|tutorials?|reference|book|getting\ started|manual|examples)($|[\):])/i; + APPENDIX_RGX = /appendix/i; + } - groups: -> - result = [] - for type in @models - (result[@_groupFor(type)] ||= []).push(type) - result.filter (e) -> e.length > 0 + groups() { + const result = []; + for (var type of Array.from(this.models)) { + var name; + (result[name = this._groupFor(type)] || (result[name] = [])).push(type); + } + return result.filter(e => e.length > 0); + } - GUIDES_RGX = /(^|\()(guides?|tutorials?|reference|book|getting\ started|manual|examples)($|[\):])/i - APPENDIX_RGX = /appendix/i - - _groupFor: (type) -> - if GUIDES_RGX.test(type.name) - 0 - else if APPENDIX_RGX.test(type.name) - 2 - else - 1 + _groupFor(type) { + if (GUIDES_RGX.test(type.name)) { + return 0; + } else if (APPENDIX_RGX.test(type.name)) { + return 2; + } else { + return 1; + } + } + }); + Cls.initClass(); + return Cls; +})(); diff --git a/assets/javascripts/debug.js.js b/assets/javascripts/debug.js.js index 032d93ac15..3273ac066f 100644 --- a/assets/javascripts/debug.js.js +++ b/assets/javascripts/debug.js.js @@ -1,85 +1,110 @@ -return unless console?.time and console.groupCollapsed - -# -# App -# - -_init = app.init -app.init = -> - console.time 'Init' - _init.call(app) - console.timeEnd 'Init' - console.time 'Load' - -_start = app.start -app.start = -> - console.timeEnd 'Load' - console.time 'Start' - _start.call(app, arguments...) - console.timeEnd 'Start' - -# -# Searcher -# - -_super = app.Searcher -_proto = app.Searcher.prototype - -app.Searcher = -> - _super.apply @, arguments - - _setup = @setup.bind(@) - @setup = -> - console.groupCollapsed "Search: #{@query}" - console.time 'Total' - _setup() - - _match = @match.bind(@) - @match = => - console.timeEnd @matcher.name if @matcher - _match() - - _setupMatcher = @setupMatcher.bind(@) - @setupMatcher = -> - console.time @matcher.name - _setupMatcher() - - _end = @end.bind(@) - @end = -> - console.log "Results: #{@totalResults}" - console.timeEnd 'Total' - console.groupEnd() - _end() - - _kill = @kill.bind(@) - @kill = -> - if @timeout - console.timeEnd @matcher.name if @matcher - console.groupEnd() - console.timeEnd 'Total' - console.warn 'Killed' - _kill() - - return - -$.extend(app.Searcher, _super) -_proto.constructor = app.Searcher -app.Searcher.prototype = _proto - -# -# View tree -# - -@viewTree = (view = app.document, level = 0, visited = []) -> - return if visited.indexOf(view) >= 0 - visited.push(view) - - console.log "%c #{Array(level + 1).join(' ')}#{view.constructor.name}: #{!!view.activated}", - 'color:' + (view.activated and 'green' or 'red') - - for own key, value of view when key isnt 'view' and value - if typeof value is 'object' and value.setupElement - @viewTree(value, level + 1, visited) - else if value.constructor.toString().match(/Object\(\)/) - @viewTree(v, level + 1, visited) for own k, v of value when v and typeof v is 'object' and v.setupElement - return +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS203: Remove `|| {}` from converted for-own loops + * DS207: Consider shorter variations of null checks + * DS208: Avoid top-level this + * DS209: Avoid top-level return + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +if (!(typeof console !== 'undefined' && console !== null ? console.time : undefined) || !console.groupCollapsed) { return; } + +// +// App +// + +const _init = app.init; +app.init = function() { + console.time('Init'); + _init.call(app); + console.timeEnd('Init'); + return console.time('Load'); +}; + +const _start = app.start; +app.start = function() { + console.timeEnd('Load'); + console.time('Start'); + _start.call(app, ...arguments); + return console.timeEnd('Start'); +}; + +// +// Searcher +// + +const _super = app.Searcher; +const _proto = app.Searcher.prototype; + +app.Searcher = function() { + _super.apply(this, arguments); + + const _setup = this.setup.bind(this); + this.setup = function() { + console.groupCollapsed(`Search: ${this.query}`); + console.time('Total'); + return _setup(); + }; + + const _match = this.match.bind(this); + this.match = () => { + if (this.matcher) { console.timeEnd(this.matcher.name); } + return _match(); + }; + + const _setupMatcher = this.setupMatcher.bind(this); + this.setupMatcher = function() { + console.time(this.matcher.name); + return _setupMatcher(); + }; + + const _end = this.end.bind(this); + this.end = function() { + console.log(`Results: ${this.totalResults}`); + console.timeEnd('Total'); + console.groupEnd(); + return _end(); + }; + + const _kill = this.kill.bind(this); + this.kill = function() { + if (this.timeout) { + if (this.matcher) { console.timeEnd(this.matcher.name); } + console.groupEnd(); + console.timeEnd('Total'); + console.warn('Killed'); + } + return _kill(); + }; + +}; + +$.extend(app.Searcher, _super); +_proto.constructor = app.Searcher; +app.Searcher.prototype = _proto; + +// +// View tree +// + +this.viewTree = function(view, level, visited) { + if (view == null) { view = app.document; } + if (level == null) { level = 0; } + if (visited == null) { visited = []; } + if (visited.indexOf(view) >= 0) { return; } + visited.push(view); + + console.log(`%c ${Array(level + 1).join(' ')}${view.constructor.name}: ${!!view.activated}`, + 'color:' + ((view.activated && 'green') || 'red')); + + for (var key of Object.keys(view || {})) { + var value = view[key]; + if ((key !== 'view') && value) { + if ((typeof value === 'object') && value.setupElement) { + this.viewTree(value, level + 1, visited); + } else if (value.constructor.toString().match(/Object\(\)/)) { + for (var k of Object.keys(value || {})) { var v = value[k]; if (v && (typeof v === 'object') && v.setupElement) { this.viewTree(v, level + 1, visited); } } + } + } + } +}; diff --git a/assets/javascripts/lib/ajax.js b/assets/javascripts/lib/ajax.js index 4138ce7b0f..8019f9fd62 100644 --- a/assets/javascripts/lib/ajax.js +++ b/assets/javascripts/lib/ajax.js @@ -1,118 +1,154 @@ -MIME_TYPES = - json: 'application/json' +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * DS208: Avoid top-level this + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const MIME_TYPES = { + json: 'application/json', html: 'text/html' +}; -@ajax = (options) -> - applyDefaults(options) - serializeData(options) +this.ajax = function(options) { + applyDefaults(options); + serializeData(options); - xhr = new XMLHttpRequest() - xhr.open(options.type, options.url, options.async) + const xhr = new XMLHttpRequest(); + xhr.open(options.type, options.url, options.async); - applyCallbacks(xhr, options) - applyHeaders(xhr, options) + applyCallbacks(xhr, options); + applyHeaders(xhr, options); - xhr.send(options.data) + xhr.send(options.data); - if options.async - abort: abort.bind(undefined, xhr) - else - parseResponse(xhr, options) + if (options.async) { + return {abort: abort.bind(undefined, xhr)}; + } else { + return parseResponse(xhr, options); + } +}; -ajax.defaults = - async: true - dataType: 'json' - timeout: 30 +ajax.defaults = { + async: true, + dataType: 'json', + timeout: 30, type: 'GET' - # contentType - # context - # data - # error - # headers - # progress - # success - # url - -applyDefaults = (options) -> - for key of ajax.defaults - options[key] ?= ajax.defaults[key] - return - -serializeData = (options) -> - return unless options.data - - if options.type is 'GET' - options.url += '?' + serializeParams(options.data) - options.data = null - else - options.data = serializeParams(options.data) - return - -serializeParams = (params) -> - ("#{encodeURIComponent key}=#{encodeURIComponent value}" for key, value of params).join '&' - -applyCallbacks = (xhr, options) -> - return unless options.async - - xhr.timer = setTimeout onTimeout.bind(undefined, xhr, options), options.timeout * 1000 - xhr.onprogress = options.progress if options.progress - xhr.onreadystatechange = -> - if xhr.readyState is 4 - clearTimeout(xhr.timer) - onComplete(xhr, options) - return - return - -applyHeaders = (xhr, options) -> - options.headers or= {} - - if options.contentType - options.headers['Content-Type'] = options.contentType - - if not options.headers['Content-Type'] and options.data and options.type isnt 'GET' - options.headers['Content-Type'] = 'application/x-www-form-urlencoded' - - if options.dataType - options.headers['Accept'] = MIME_TYPES[options.dataType] or options.dataType - - for key, value of options.headers - xhr.setRequestHeader(key, value) - return - -onComplete = (xhr, options) -> - if 200 <= xhr.status < 300 - if (response = parseResponse(xhr, options))? - onSuccess response, xhr, options - else - onError 'invalid', xhr, options - else - onError 'error', xhr, options - return - -onSuccess = (response, xhr, options) -> - options.success?.call options.context, response, xhr, options - return - -onError = (type, xhr, options) -> - options.error?.call options.context, type, xhr, options - return - -onTimeout = (xhr, options) -> - xhr.abort() - onError 'timeout', xhr, options - return - -abort = (xhr) -> - clearTimeout(xhr.timer) - xhr.onreadystatechange = null - xhr.abort() - return - -parseResponse = (xhr, options) -> - if options.dataType is 'json' - parseJSON(xhr.responseText) - else - xhr.responseText - -parseJSON = (json) -> - try JSON.parse(json) catch +}; + // contentType + // context + // data + // error + // headers + // progress + // success + // url + +var applyDefaults = function(options) { + for (var key in ajax.defaults) { + if (options[key] == null) { options[key] = ajax.defaults[key]; } + } +}; + +var serializeData = function(options) { + if (!options.data) { return; } + + if (options.type === 'GET') { + options.url += '?' + serializeParams(options.data); + options.data = null; + } else { + options.data = serializeParams(options.data); + } +}; + +var serializeParams = params => ((() => { + const result = []; + for (var key in params) { + var value = params[key]; + result.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`); + } + return result; +})()).join('&'); + +var applyCallbacks = function(xhr, options) { + if (!options.async) { return; } + + xhr.timer = setTimeout(onTimeout.bind(undefined, xhr, options), options.timeout * 1000); + if (options.progress) { xhr.onprogress = options.progress; } + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + clearTimeout(xhr.timer); + onComplete(xhr, options); + } + }; +}; + +var applyHeaders = function(xhr, options) { + if (!options.headers) { options.headers = {}; } + + if (options.contentType) { + options.headers['Content-Type'] = options.contentType; + } + + if (!options.headers['Content-Type'] && options.data && (options.type !== 'GET')) { + options.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + } + + if (options.dataType) { + options.headers['Accept'] = MIME_TYPES[options.dataType] || options.dataType; + } + + for (var key in options.headers) { + var value = options.headers[key]; + xhr.setRequestHeader(key, value); + } +}; + +var onComplete = function(xhr, options) { + if (200 <= xhr.status && xhr.status < 300) { + let response; + if ((response = parseResponse(xhr, options)) != null) { + onSuccess(response, xhr, options); + } else { + onError('invalid', xhr, options); + } + } else { + onError('error', xhr, options); + } +}; + +var onSuccess = function(response, xhr, options) { + if (options.success != null) { + options.success.call(options.context, response, xhr, options); + } +}; + +var onError = function(type, xhr, options) { + if (options.error != null) { + options.error.call(options.context, type, xhr, options); + } +}; + +var onTimeout = function(xhr, options) { + xhr.abort(); + onError('timeout', xhr, options); +}; + +var abort = function(xhr) { + clearTimeout(xhr.timer); + xhr.onreadystatechange = null; + xhr.abort(); +}; + +var parseResponse = function(xhr, options) { + if (options.dataType === 'json') { + return parseJSON(xhr.responseText); + } else { + return xhr.responseText; + } +}; + +var parseJSON = function(json) { + try { return JSON.parse(json); } catch (error) {} +}; diff --git a/assets/javascripts/lib/cookies_store.js b/assets/javascripts/lib/cookies_store.js index eaf1bd4f7c..a1a8f0341b 100644 --- a/assets/javascripts/lib/cookies_store.js +++ b/assets/javascripts/lib/cookies_store.js @@ -1,42 +1,66 @@ -class @CookiesStore - # Intentionally called CookiesStore instead of CookieStore - # Calling it CookieStore causes issues when the Experimental Web Platform features flag is enabled in Chrome - # Related issue: https://github.com/freeCodeCamp/devdocs/issues/932 +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +(function() { + let INT = undefined; + const Cls = (this.CookiesStore = class CookiesStore { + static initClass() { + // Intentionally called CookiesStore instead of CookieStore + // Calling it CookieStore causes issues when the Experimental Web Platform features flag is enabled in Chrome + // Related issue: https://github.com/freeCodeCamp/devdocs/issues/932 + + INT = /^\d+$/; + } - INT = /^\d+$/ + static onBlocked() {} - @onBlocked: -> + get(key) { + let value = Cookies.get(key); + if ((value != null) && INT.test(value)) { value = parseInt(value, 10); } + return value; + } - get: (key) -> - value = Cookies.get(key) - value = parseInt(value, 10) if value? and INT.test(value) - value + set(key, value) { + if (value === false) { + this.del(key); + return; + } - set: (key, value) -> - if value == false - @del(key) - return + if (value === true) { value = 1; } + if (value && (typeof INT.test === 'function' ? INT.test(value) : undefined)) { value = parseInt(value, 10); } + Cookies.set(key, '' + value, {path: '/', expires: 1e8}); + if (this.get(key) !== value) { this.constructor.onBlocked(key, value, this.get(key)); } + } - value = 1 if value == true - value = parseInt(value, 10) if value and INT.test?(value) - Cookies.set(key, '' + value, path: '/', expires: 1e8) - @constructor.onBlocked(key, value, @get(key)) if @get(key) != value - return + del(key) { + Cookies.expire(key); + } - del: (key) -> - Cookies.expire(key) - return + reset() { + try { + for (var cookie of Array.from(document.cookie.split(/;\s?/))) { + Cookies.expire(cookie.split('=')[0]); + } + return; + } catch (error) {} + } - reset: -> - try - for cookie in document.cookie.split(/;\s?/) - Cookies.expire(cookie.split('=')[0]) - return - catch - - dump: -> - result = {} - for cookie in document.cookie.split(/;\s?/) when cookie[0] isnt '_' - cookie = cookie.split('=') - result[cookie[0]] = cookie[1] - result + dump() { + const result = {}; + for (var cookie of Array.from(document.cookie.split(/;\s?/))) { + if (cookie[0] !== '_') { + cookie = cookie.split('='); + result[cookie[0]] = cookie[1]; + } + } + return result; + } + }); + Cls.initClass(); + return Cls; +})(); diff --git a/assets/javascripts/lib/events.js b/assets/javascripts/lib/events.js index 0593607659..9e007e9c3c 100644 --- a/assets/javascripts/lib/events.js +++ b/assets/javascripts/lib/events.js @@ -1,28 +1,51 @@ -@Events = - on: (event, callback) -> - if event.indexOf(' ') >= 0 - @on name, callback for name in event.split(' ') - else - ((@_callbacks ?= {})[event] ?= []).push callback - @ +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS207: Consider shorter variations of null checks + * DS208: Avoid top-level this + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +this.Events = { + on(event, callback) { + if (event.indexOf(' ') >= 0) { + for (var name of Array.from(event.split(' '))) { this.on(name, callback); } + } else { + let base; + (((base = this._callbacks != null ? this._callbacks : (this._callbacks = {})))[event] != null ? base[event] : (base[event] = [])).push(callback); + } + return this; + }, - off: (event, callback) -> - if event.indexOf(' ') >= 0 - @off name, callback for name in event.split(' ') - else if (callbacks = @_callbacks?[event]) and (index = callbacks.indexOf callback) >= 0 - callbacks.splice index, 1 - delete @_callbacks[event] unless callbacks.length - @ + off(event, callback) { + let callbacks, index; + if (event.indexOf(' ') >= 0) { + for (var name of Array.from(event.split(' '))) { this.off(name, callback); } + } else if ((callbacks = this._callbacks != null ? this._callbacks[event] : undefined) && ((index = callbacks.indexOf(callback)) >= 0)) { + callbacks.splice(index, 1); + if (!callbacks.length) { delete this._callbacks[event]; } + } + return this; + }, - trigger: (event, args...) -> - @eventInProgress = { name: event, args: args } - if callbacks = @_callbacks?[event] - callback? args... for callback in callbacks.slice(0) - @eventInProgress = null - @trigger 'all', event, args... unless event is 'all' - @ + trigger(event, ...args) { + let callbacks; + this.eventInProgress = { name: event, args }; + if (callbacks = this._callbacks != null ? this._callbacks[event] : undefined) { + for (var callback of Array.from(callbacks.slice(0))) { if (typeof callback === 'function') { + callback(...Array.from(args || [])); + } } + } + this.eventInProgress = null; + if (event !== 'all') { this.trigger('all', event, ...Array.from(args)); } + return this; + }, - removeEvent: (event) -> - if @_callbacks? - delete @_callbacks[name] for name in event.split(' ') - @ + removeEvent(event) { + if (this._callbacks != null) { + for (var name of Array.from(event.split(' '))) { delete this._callbacks[name]; } + } + return this; + } +}; diff --git a/assets/javascripts/lib/favicon.js b/assets/javascripts/lib/favicon.js index 428eae453a..726fb15a28 100644 --- a/assets/javascripts/lib/favicon.js +++ b/assets/javascripts/lib/favicon.js @@ -1,76 +1,89 @@ -defaultUrl = null -currentSlug = null - -imageCache = {} -urlCache = {} - -withImage = (url, action) -> - if imageCache[url] - action(imageCache[url]) - else - img = new Image() - img.crossOrigin = 'anonymous' - img.src = url - img.onload = () => - imageCache[url] = img - action(img) - -@setFaviconForDoc = (doc) -> - return if currentSlug == doc.slug - - favicon = $('link[rel="icon"]') - - if defaultUrl == null - defaultUrl = favicon.href - - if urlCache[doc.slug] - favicon.href = urlCache[doc.slug] - currentSlug = doc.slug - return - - iconEl = $("._icon-#{doc.slug.split('~')[0]}") - return if iconEl == null - - styles = window.getComputedStyle(iconEl, ':before') - - backgroundPositionX = styles['background-position-x'] - backgroundPositionY = styles['background-position-y'] - return if backgroundPositionX == undefined || backgroundPositionY == undefined - - bgUrl = app.config.favicon_spritesheet - sourceSize = 16 - sourceX = Math.abs(parseInt(backgroundPositionX.slice(0, -2))) - sourceY = Math.abs(parseInt(backgroundPositionY.slice(0, -2))) - - withImage(bgUrl, (docImg) -> - withImage(defaultUrl, (defaultImg) -> - size = defaultImg.width - - canvas = document.createElement('canvas') - ctx = canvas.getContext('2d') - - canvas.width = size - canvas.height = size - ctx.drawImage(defaultImg, 0, 0) - - docIconPercentage = 65 - destinationCoords = size / 100 * (100 - docIconPercentage) - destinationSize = size / 100 * docIconPercentage - - ctx.drawImage(docImg, sourceX, sourceY, sourceSize, sourceSize, destinationCoords, destinationCoords, destinationSize, destinationSize) - - try - urlCache[doc.slug] = canvas.toDataURL() - favicon.href = urlCache[doc.slug] - - currentSlug = doc.slug - catch error - Raven.captureException error, { level: 'info' } - @resetFavicon() - ) - ) - -@resetFavicon = () -> - if defaultUrl != null and currentSlug != null - $('link[rel="icon"]').href = defaultUrl - currentSlug = null +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS208: Avoid top-level this + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +let defaultUrl = null; +let currentSlug = null; + +const imageCache = {}; +const urlCache = {}; + +const withImage = function(url, action) { + if (imageCache[url]) { + return action(imageCache[url]); + } else { + const img = new Image(); + img.crossOrigin = 'anonymous'; + img.src = url; + return img.onload = () => { + imageCache[url] = img; + return action(img); + }; + } +}; + +this.setFaviconForDoc = function(doc) { + if (currentSlug === doc.slug) { return; } + + const favicon = $('link[rel="icon"]'); + + if (defaultUrl === null) { + defaultUrl = favicon.href; + } + + if (urlCache[doc.slug]) { + favicon.href = urlCache[doc.slug]; + currentSlug = doc.slug; + return; + } + + const iconEl = $(`._icon-${doc.slug.split('~')[0]}`); + if (iconEl === null) { return; } + + const styles = window.getComputedStyle(iconEl, ':before'); + + const backgroundPositionX = styles['background-position-x']; + const backgroundPositionY = styles['background-position-y']; + if ((backgroundPositionX === undefined) || (backgroundPositionY === undefined)) { return; } + + const bgUrl = app.config.favicon_spritesheet; + const sourceSize = 16; + const sourceX = Math.abs(parseInt(backgroundPositionX.slice(0, -2))); + const sourceY = Math.abs(parseInt(backgroundPositionY.slice(0, -2))); + + return withImage(bgUrl, docImg => withImage(defaultUrl, function(defaultImg) { + const size = defaultImg.width; + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + canvas.width = size; + canvas.height = size; + ctx.drawImage(defaultImg, 0, 0); + + const docIconPercentage = 65; + const destinationCoords = (size / 100) * (100 - docIconPercentage); + const destinationSize = (size / 100) * docIconPercentage; + + ctx.drawImage(docImg, sourceX, sourceY, sourceSize, sourceSize, destinationCoords, destinationCoords, destinationSize, destinationSize); + + try { + urlCache[doc.slug] = canvas.toDataURL(); + favicon.href = urlCache[doc.slug]; + + return currentSlug = doc.slug; + } catch (error) { + Raven.captureException(error, { level: 'info' }); + return this.resetFavicon(); + } + })); +}; + +this.resetFavicon = function() { + if ((defaultUrl !== null) && (currentSlug !== null)) { + $('link[rel="icon"]').href = defaultUrl; + return currentSlug = null; + } +}; diff --git a/assets/javascripts/lib/license.js b/assets/javascripts/lib/license.js index c397b93b19..9292ef7ee8 100644 --- a/assets/javascripts/lib/license.js +++ b/assets/javascripts/lib/license.js @@ -1,7 +1,7 @@ -### +/* * Copyright 2013-2023 Thibaut Courouble and other contributors * * This source code is licensed under the terms of the Mozilla * Public License, v. 2.0, a copy of which may be obtained at: * http://mozilla.org/MPL/2.0/ -### +*/ diff --git a/assets/javascripts/lib/local_storage_store.js b/assets/javascripts/lib/local_storage_store.js index f4438c86a7..07bc4870fb 100644 --- a/assets/javascripts/lib/local_storage_store.js +++ b/assets/javascripts/lib/local_storage_store.js @@ -1,23 +1,33 @@ -class @LocalStorageStore - get: (key) -> - try - JSON.parse localStorage.getItem(key) - catch +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +this.LocalStorageStore = class LocalStorageStore { + get(key) { + try { + return JSON.parse(localStorage.getItem(key)); + } catch (error) {} + } - set: (key, value) -> - try - localStorage.setItem(key, JSON.stringify(value)) - true - catch + set(key, value) { + try { + localStorage.setItem(key, JSON.stringify(value)); + return true; + } catch (error) {} + } - del: (key) -> - try - localStorage.removeItem(key) - true - catch + del(key) { + try { + localStorage.removeItem(key); + return true; + } catch (error) {} + } - reset: -> - try - localStorage.clear() - true - catch + reset() { + try { + localStorage.clear(); + return true; + } catch (error) {} + } +}; diff --git a/assets/javascripts/lib/page.js b/assets/javascripts/lib/page.js index 5ad89b32ae..c24750421b 100644 --- a/assets/javascripts/lib/page.js +++ b/assets/javascripts/lib/page.js @@ -1,223 +1,280 @@ -### +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * DS208: Avoid top-level this + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +/* * Based on github.com/visionmedia/page.js * Licensed under the MIT license * Copyright 2012 TJ Holowaychuk -### - -running = false -currentState = null -callbacks = [] - -@page = (value, fn) -> - if typeof value is 'function' - page '*', value - else if typeof fn is 'function' - route = new Route(value) - callbacks.push route.middleware(fn) - else if typeof value is 'string' - page.show(value, fn) - else - page.start(value) - return - -page.start = (options = {}) -> - unless running - running = true - addEventListener 'popstate', onpopstate - addEventListener 'click', onclick - page.replace currentPath(), null, null, true - return - -page.stop = -> - if running - running = false - removeEventListener 'click', onclick - removeEventListener 'popstate', onpopstate - return - -page.show = (path, state) -> - return if path is currentState?.path - context = new Context(path, state) - previousState = currentState - currentState = context.state - if res = page.dispatch(context) - currentState = previousState - location.assign(res) - else - context.pushState() - updateCanonicalLink() - track() - context - -page.replace = (path, state, skipDispatch, init) -> - context = new Context(path, state or currentState) - context.init = init - currentState = context.state - result = page.dispatch(context) unless skipDispatch - if result - context = new Context(result) - context.init = init - currentState = context.state - page.dispatch(context) - context.replaceState() - updateCanonicalLink() - track() unless skipDispatch - context - -page.dispatch = (context) -> - i = 0 - next = -> - res = fn(context, next) if fn = callbacks[i++] - return res - return next() - -page.canGoBack = -> - not Context.isIntialState(currentState) - -page.canGoForward = -> - not Context.isLastState(currentState) - -currentPath = -> - location.pathname + location.search + location.hash - -class Context - @initialPath: currentPath() - @sessionId: Date.now() - @stateId: 0 - - @isIntialState: (state) -> - state.id == 0 - - @isLastState: (state) -> - state.id == @stateId - 1 - - @isInitialPopState: (state) -> - state.path is @initialPath and @stateId is 1 - - @isSameSession: (state) -> - state.sessionId is @sessionId - - constructor: (@path = '/', @state = {}) -> - @pathname = @path.replace /(?:\?([^#]*))?(?:#(.*))?$/, (_, query, hash) => - @query = query - @hash = hash - '' - - @state.id ?= @constructor.stateId++ - @state.sessionId ?= @constructor.sessionId - @state.path = @path - - pushState: -> - history.pushState @state, '', @path - return - - replaceState: -> - try history.replaceState @state, '', @path # NS_ERROR_FAILURE in Firefox - return - -class Route - constructor: (@path, options = {}) -> - @keys = [] - @regexp = pathtoRegexp @path, @keys - - middleware: (fn) -> - (context, next) => - if @match context.pathname, params = [] - context.params = params - return fn(context, next) - else - return next() - - match: (path, params) -> - return unless matchData = @regexp.exec(path) - - for value, i in matchData[1..] - value = decodeURIComponent value if typeof value is 'string' - if key = @keys[i] - params[key.name] = value - else - params.push value - true - -pathtoRegexp = (path, keys) -> - return path if path instanceof RegExp - - path = "(#{path.join '|'})" if path instanceof Array +*/ + +let running = false; +let currentState = null; +const callbacks = []; + +this.page = function(value, fn) { + if (typeof value === 'function') { + page('*', value); + } else if (typeof fn === 'function') { + const route = new Route(value); + callbacks.push(route.middleware(fn)); + } else if (typeof value === 'string') { + page.show(value, fn); + } else { + page.start(value); + } +}; + +page.start = function(options) { + if (options == null) { options = {}; } + if (!running) { + running = true; + addEventListener('popstate', onpopstate); + addEventListener('click', onclick); + page.replace(currentPath(), null, null, true); + } +}; + +page.stop = function() { + if (running) { + running = false; + removeEventListener('click', onclick); + removeEventListener('popstate', onpopstate); + } +}; + +page.show = function(path, state) { + let res; + if (path === (currentState != null ? currentState.path : undefined)) { return; } + const context = new Context(path, state); + const previousState = currentState; + currentState = context.state; + if (res = page.dispatch(context)) { + currentState = previousState; + location.assign(res); + } else { + context.pushState(); + updateCanonicalLink(); + track(); + } + return context; +}; + +page.replace = function(path, state, skipDispatch, init) { + let result; + let context = new Context(path, state || currentState); + context.init = init; + currentState = context.state; + if (!skipDispatch) { result = page.dispatch(context); } + if (result) { + context = new Context(result); + context.init = init; + currentState = context.state; + page.dispatch(context); + } + context.replaceState(); + updateCanonicalLink(); + if (!skipDispatch) { track(); } + return context; +}; + +page.dispatch = function(context) { + let i = 0; + var next = function() { + let fn, res; + if (fn = callbacks[i++]) { res = fn(context, next); } + return res; + }; + return next(); +}; + +page.canGoBack = () => !Context.isIntialState(currentState); + +page.canGoForward = () => !Context.isLastState(currentState); + +var currentPath = () => location.pathname + location.search + location.hash; + +class Context { + static initClass() { + this.initialPath = currentPath(); + this.sessionId = Date.now(); + this.stateId = 0; + } + + static isIntialState(state) { + return state.id === 0; + } + + static isLastState(state) { + return state.id === (this.stateId - 1); + } + + static isInitialPopState(state) { + return (state.path === this.initialPath) && (this.stateId === 1); + } + + static isSameSession(state) { + return state.sessionId === this.sessionId; + } + + constructor(path, state) { + if (path == null) { path = '/'; } + this.path = path; + if (state == null) { state = {}; } + this.state = state; + this.pathname = this.path.replace(/(?:\?([^#]*))?(?:#(.*))?$/, (_, query, hash) => { + this.query = query; + this.hash = hash; + return ''; + }); + + if (this.state.id == null) { this.state.id = this.constructor.stateId++; } + if (this.state.sessionId == null) { this.state.sessionId = this.constructor.sessionId; } + this.state.path = this.path; + } + + pushState() { + history.pushState(this.state, '', this.path); + } + + replaceState() { + try { history.replaceState(this.state, '', this.path); } catch (error) {} // NS_ERROR_FAILURE in Firefox + } +} +Context.initClass(); + +class Route { + constructor(path, options) { + this.path = path; + if (options == null) { options = {}; } + this.keys = []; + this.regexp = pathtoRegexp(this.path, this.keys); + } + + middleware(fn) { + return (context, next) => { + let params; + if (this.match(context.pathname, (params = []))) { + context.params = params; + return fn(context, next); + } else { + return next(); + } + }; + } + + match(path, params) { + let matchData; + if (!(matchData = this.regexp.exec(path))) { return; } + + const iterable = matchData.slice(1); + for (let i = 0; i < iterable.length; i++) { + var key; + var value = iterable[i]; + if (typeof value === 'string') { value = decodeURIComponent(value); } + if ((key = this.keys[i])) { + params[key.name] = value; + } else { + params.push(value); + } + } + return true; + } +} + +var pathtoRegexp = function(path, keys) { + if (path instanceof RegExp) { return path; } + + if (path instanceof Array) { path = `(${path.join('|')})`; } path = path - .replace /\/\(/g, '(?:/' - .replace /(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, (_, slash = '', format = '', key, capture, optional) -> - keys.push name: key, optional: !!optional - str = if optional then '' else slash - str += '(?:' - str += slash if optional - str += format - str += capture or if format then '([^/.]+?)' else '([^/]+?)' - str += ')' - str += optional if optional - str - .replace /([\/.])/g, '\\$1' - .replace /\*/g, '(.*)' - - new RegExp "^#{path}$" - -onpopstate = (event) -> - return if not event.state or Context.isInitialPopState(event.state) - - if Context.isSameSession(event.state) - page.replace(event.state.path, event.state) - else - location.reload() - return - -onclick = (event) -> - try - return if event.which isnt 1 or event.metaKey or event.ctrlKey or event.shiftKey or event.defaultPrevented - catch - return - - link = $.eventTarget(event) - link = link.parentNode while link and link.tagName isnt 'A' - - if link and not link.target and isSameOrigin(link.href) - event.preventDefault() - path = link.pathname + link.search + link.hash - path = path.replace /^\/\/+/, '/' # IE11 bug - page.show(path) - return - -isSameOrigin = (url) -> - url.indexOf("#{location.protocol}//#{location.hostname}") is 0 - -updateCanonicalLink = -> - @canonicalLink ||= document.head.querySelector('link[rel="canonical"]') - @canonicalLink.setAttribute('href', "https://#{location.host}#{location.pathname}") - -trackers = [] - -page.track = (fn) -> - trackers.push(fn) - return - -track = -> - return unless app.config.env == 'production' - return if navigator.doNotTrack == '1' - return if navigator.globalPrivacyControl - - consentGiven = Cookies.get('analyticsConsent') - consentAsked = Cookies.get('analyticsConsentAsked') - - if consentGiven == '1' - tracker.call() for tracker in trackers - else if consentGiven == undefined and consentAsked == undefined - # Only ask for consent once per browser session - Cookies.set('analyticsConsentAsked', '1') - - new app.views.Notif 'AnalyticsConsent', autoHide: null - return - -@resetAnalytics = -> - for cookie in document.cookie.split(/;\s?/) - name = cookie.split('=')[0] - if name[0] == '_' && name[1] != '_' - Cookies.expire(name) - return + .replace(/\/\(/g, '(?:/') + .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional) { + if (slash == null) { slash = ''; } + if (format == null) { format = ''; } + keys.push({name: key, optional: !!optional}); + let str = optional ? '' : slash; + str += '(?:'; + if (optional) { str += slash; } + str += format; + str += capture || (format ? '([^/.]+?)' : '([^/]+?)'); + str += ')'; + if (optional) { str += optional; } + return str; + }).replace(/([\/.])/g, '\\$1') + .replace(/\*/g, '(.*)'); + + return new RegExp(`^${path}$`); +}; + +var onpopstate = function(event) { + if (!event.state || Context.isInitialPopState(event.state)) { return; } + + if (Context.isSameSession(event.state)) { + page.replace(event.state.path, event.state); + } else { + location.reload(); + } +}; + +var onclick = function(event) { + try { + if ((event.which !== 1) || event.metaKey || event.ctrlKey || event.shiftKey || event.defaultPrevented) { return; } + } catch (error) { + return; + } + + let link = $.eventTarget(event); + while (link && (link.tagName !== 'A')) { link = link.parentNode; } + + if (link && !link.target && isSameOrigin(link.href)) { + event.preventDefault(); + let path = link.pathname + link.search + link.hash; + path = path.replace(/^\/\/+/, '/'); // IE11 bug + page.show(path); + } +}; + +var isSameOrigin = url => url.indexOf(`${location.protocol}//${location.hostname}`) === 0; + +var updateCanonicalLink = function() { + if (!this.canonicalLink) { this.canonicalLink = document.head.querySelector('link[rel="canonical"]'); } + return this.canonicalLink.setAttribute('href', `https://${location.host}${location.pathname}`); +}; + +const trackers = []; + +page.track = function(fn) { + trackers.push(fn); +}; + +var track = function() { + if (app.config.env !== 'production') { return; } + if (navigator.doNotTrack === '1') { return; } + if (navigator.globalPrivacyControl) { return; } + + const consentGiven = Cookies.get('analyticsConsent'); + const consentAsked = Cookies.get('analyticsConsentAsked'); + + if (consentGiven === '1') { + for (var tracker of Array.from(trackers)) { tracker.call(); } + } else if ((consentGiven === undefined) && (consentAsked === undefined)) { + // Only ask for consent once per browser session + Cookies.set('analyticsConsentAsked', '1'); + + new app.views.Notif('AnalyticsConsent', {autoHide: null}); + } +}; + +this.resetAnalytics = function() { + for (var cookie of Array.from(document.cookie.split(/;\s?/))) { + var name = cookie.split('=')[0]; + if ((name[0] === '_') && (name[1] !== '_')) { + Cookies.expire(name); + } + } +}; diff --git a/assets/javascripts/lib/util.js b/assets/javascripts/lib/util.js index 001b13dee7..28f7369259 100644 --- a/assets/javascripts/lib/util.js +++ b/assets/javascripts/lib/util.js @@ -1,399 +1,491 @@ -# -# Traversing -# - -@$ = (selector, el = document) -> - try el.querySelector(selector) catch - -@$$ = (selector, el = document) -> - try el.querySelectorAll(selector) catch - -$.id = (id) -> - document.getElementById(id) - -$.hasChild = (parent, el) -> - return unless parent - while el - return true if el is parent - return if el is document.body - el = el.parentNode - -$.closestLink = (el, parent = document.body) -> - while el - return el if el.tagName is 'A' - return if el is parent - el = el.parentNode - -# -# Events -# - -$.on = (el, event, callback, useCapture = false) -> - if event.indexOf(' ') >= 0 - $.on el, name, callback for name in event.split(' ') - else - el.addEventListener(event, callback, useCapture) - return - -$.off = (el, event, callback, useCapture = false) -> - if event.indexOf(' ') >= 0 - $.off el, name, callback for name in event.split(' ') - else - el.removeEventListener(event, callback, useCapture) - return - -$.trigger = (el, type, canBubble = true, cancelable = true) -> - event = document.createEvent 'Event' - event.initEvent(type, canBubble, cancelable) - el.dispatchEvent(event) - return - -$.click = (el) -> - event = document.createEvent 'MouseEvent' - event.initMouseEvent 'click', true, true, window, null, 0, 0, 0, 0, false, false, false, false, 0, null - el.dispatchEvent(event) - return - -$.stopEvent = (event) -> - event.preventDefault() - event.stopPropagation() - event.stopImmediatePropagation() - return - -$.eventTarget = (event) -> - event.target.correspondingUseElement || event.target - -# -# Manipulation -# - -buildFragment = (value) -> - fragment = document.createDocumentFragment() - - if $.isCollection(value) - fragment.appendChild(child) for child in $.makeArray(value) - else - fragment.innerHTML = value - - fragment - -$.append = (el, value) -> - if typeof value is 'string' - el.insertAdjacentHTML 'beforeend', value - else - value = buildFragment(value) if $.isCollection(value) - el.appendChild(value) - return - -$.prepend = (el, value) -> - if not el.firstChild - $.append(value) - else if typeof value is 'string' - el.insertAdjacentHTML 'afterbegin', value - else - value = buildFragment(value) if $.isCollection(value) - el.insertBefore(value, el.firstChild) - return - -$.before = (el, value) -> - if typeof value is 'string' or $.isCollection(value) - value = buildFragment(value) - - el.parentNode.insertBefore(value, el) - return - -$.after = (el, value) -> - if typeof value is 'string' or $.isCollection(value) - value = buildFragment(value) - - if el.nextSibling - el.parentNode.insertBefore(value, el.nextSibling) - else - el.parentNode.appendChild(value) - return - -$.remove = (value) -> - if $.isCollection(value) - el.parentNode?.removeChild(el) for el in $.makeArray(value) - else - value.parentNode?.removeChild(value) - return - -$.empty = (el) -> - el.removeChild(el.firstChild) while el.firstChild - return - -# Calls the function while the element is off the DOM to avoid triggering -# unnecessary reflows and repaints. -$.batchUpdate = (el, fn) -> - parent = el.parentNode - sibling = el.nextSibling - parent.removeChild(el) - - fn(el) - - if (sibling) - parent.insertBefore(el, sibling) - else - parent.appendChild(el) - return - -# -# Offset -# - -$.rect = (el) -> - el.getBoundingClientRect() - -$.offset = (el, container = document.body) -> - top = 0 - left = 0 - - while el and el isnt container - top += el.offsetTop - left += el.offsetLeft - el = el.offsetParent - - top: top - left: left - -$.scrollParent = (el) -> - while (el = el.parentNode) and el.nodeType is 1 - break if el.scrollTop > 0 - break if getComputedStyle(el)?.overflowY in ['auto', 'scroll'] - el - -$.scrollTo = (el, parent, position = 'center', options = {}) -> - return unless el - - parent ?= $.scrollParent(el) - return unless parent - - parentHeight = parent.clientHeight - parentScrollHeight = parent.scrollHeight - return unless parentScrollHeight > parentHeight - - top = $.offset(el, parent).top - offsetTop = parent.firstElementChild.offsetTop - - switch position - when 'top' - parent.scrollTop = top - offsetTop - (if options.margin? then options.margin else 0) - when 'center' - parent.scrollTop = top - Math.round(parentHeight / 2 - el.offsetHeight / 2) - when 'continuous' - scrollTop = parent.scrollTop - height = el.offsetHeight - - lastElementOffset = parent.lastElementChild.offsetTop + parent.lastElementChild.offsetHeight - offsetBottom = if lastElementOffset > 0 then parentScrollHeight - lastElementOffset else 0 - - # If the target element is above the visible portion of its scrollable - # ancestor, move it near the top with a gap = options.topGap * target's height. - if top - offsetTop <= scrollTop + height * (options.topGap or 1) - parent.scrollTop = top - offsetTop - height * (options.topGap or 1) - # If the target element is below the visible portion of its scrollable - # ancestor, move it near the bottom with a gap = options.bottomGap * target's height. - else if top + offsetBottom >= scrollTop + parentHeight - height * ((options.bottomGap or 1) + 1) - parent.scrollTop = top + offsetBottom - parentHeight + height * ((options.bottomGap or 1) + 1) - return - -$.scrollToWithImageLock = (el, parent, args...) -> - parent ?= $.scrollParent(el) - return unless parent - - $.scrollTo el, parent, args... - - # Lock the scroll position on the target element for up to 3 seconds while - # nearby images are loaded and rendered. - for image in parent.getElementsByTagName('img') when not image.complete - do -> - onLoad = (event) -> - clearTimeout(timeout) - unbind(event.target) - $.scrollTo el, parent, args... - - unbind = (target) -> - $.off target, 'load', onLoad - - $.on image, 'load', onLoad - timeout = setTimeout unbind.bind(null, image), 3000 - return - -# Calls the function while locking the element's position relative to the window. -$.lockScroll = (el, fn) -> - if parent = $.scrollParent(el) - top = $.rect(el).top - top -= $.rect(parent).top unless parent in [document.body, document.documentElement] - fn() - parent.scrollTop = $.offset(el, parent).top - top - else - fn() - return - -smoothScroll = smoothStart = smoothEnd = smoothDistance = smoothDuration = null - -$.smoothScroll = (el, end) -> - unless window.requestAnimationFrame - el.scrollTop = end - return - - smoothEnd = end - - if smoothScroll - newDistance = smoothEnd - smoothStart - smoothDuration += Math.min 300, Math.abs(smoothDistance - newDistance) - smoothDistance = newDistance - return - - smoothStart = el.scrollTop - smoothDistance = smoothEnd - smoothStart - smoothDuration = Math.min 300, Math.abs(smoothDistance) - startTime = Date.now() - - smoothScroll = -> - p = Math.min 1, (Date.now() - startTime) / smoothDuration - y = Math.max 0, Math.floor(smoothStart + smoothDistance * (if p < 0.5 then 2 * p * p else p * (4 - p * 2) - 1)) - el.scrollTop = y - if p is 1 - smoothScroll = null - else - requestAnimationFrame(smoothScroll) - requestAnimationFrame(smoothScroll) - -# -# Utilities -# - -$.extend = (target, objects...) -> - for object in objects when object - for key, value of object - target[key] = value - target - -$.makeArray = (object) -> - if Array.isArray(object) - object - else - Array::slice.apply(object) - -$.arrayDelete = (array, object) -> - index = array.indexOf(object) - if index >= 0 - array.splice(index, 1) - true - else - false - -# Returns true if the object is an array or a collection of DOM elements. -$.isCollection = (object) -> - Array.isArray(object) or typeof object?.item is 'function' - -ESCAPE_HTML_MAP = - '&': '&' - '<': '<' - '>': '>' - '"': '"' - "'": ''' +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining + * DS104: Avoid inline assignments + * DS204: Change includes calls to have a more natural evaluation order + * DS207: Consider shorter variations of null checks + * DS208: Avoid top-level this + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +// +// Traversing +// + +let smoothDistance, smoothDuration, smoothEnd, smoothStart; +this.$ = function(selector, el) { + if (el == null) { el = document; } + try { return el.querySelector(selector); } catch (error) {} +}; + +this.$$ = function(selector, el) { + if (el == null) { el = document; } + try { return el.querySelectorAll(selector); } catch (error) {} +}; + +$.id = id => document.getElementById(id); + +$.hasChild = function(parent, el) { + if (!parent) { return; } + while (el) { + if (el === parent) { return true; } + if (el === document.body) { return; } + el = el.parentNode; + } +}; + +$.closestLink = function(el, parent) { + if (parent == null) { parent = document.body; } + while (el) { + if (el.tagName === 'A') { return el; } + if (el === parent) { return; } + el = el.parentNode; + } +}; + +// +// Events +// + +$.on = function(el, event, callback, useCapture) { + if (useCapture == null) { useCapture = false; } + if (event.indexOf(' ') >= 0) { + for (var name of Array.from(event.split(' '))) { $.on(el, name, callback); } + } else { + el.addEventListener(event, callback, useCapture); + } +}; + +$.off = function(el, event, callback, useCapture) { + if (useCapture == null) { useCapture = false; } + if (event.indexOf(' ') >= 0) { + for (var name of Array.from(event.split(' '))) { $.off(el, name, callback); } + } else { + el.removeEventListener(event, callback, useCapture); + } +}; + +$.trigger = function(el, type, canBubble, cancelable) { + if (canBubble == null) { canBubble = true; } + if (cancelable == null) { cancelable = true; } + const event = document.createEvent('Event'); + event.initEvent(type, canBubble, cancelable); + el.dispatchEvent(event); +}; + +$.click = function(el) { + const event = document.createEvent('MouseEvent'); + event.initMouseEvent('click', true, true, window, null, 0, 0, 0, 0, false, false, false, false, 0, null); + el.dispatchEvent(event); +}; + +$.stopEvent = function(event) { + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation(); +}; + +$.eventTarget = event => event.target.correspondingUseElement || event.target; + +// +// Manipulation +// + +const buildFragment = function(value) { + const fragment = document.createDocumentFragment(); + + if ($.isCollection(value)) { + for (var child of Array.from($.makeArray(value))) { fragment.appendChild(child); } + } else { + fragment.innerHTML = value; + } + + return fragment; +}; + +$.append = function(el, value) { + if (typeof value === 'string') { + el.insertAdjacentHTML('beforeend', value); + } else { + if ($.isCollection(value)) { value = buildFragment(value); } + el.appendChild(value); + } +}; + +$.prepend = function(el, value) { + if (!el.firstChild) { + $.append(value); + } else if (typeof value === 'string') { + el.insertAdjacentHTML('afterbegin', value); + } else { + if ($.isCollection(value)) { value = buildFragment(value); } + el.insertBefore(value, el.firstChild); + } +}; + +$.before = function(el, value) { + if ((typeof value === 'string') || $.isCollection(value)) { + value = buildFragment(value); + } + + el.parentNode.insertBefore(value, el); +}; + +$.after = function(el, value) { + if ((typeof value === 'string') || $.isCollection(value)) { + value = buildFragment(value); + } + + if (el.nextSibling) { + el.parentNode.insertBefore(value, el.nextSibling); + } else { + el.parentNode.appendChild(value); + } +}; + +$.remove = function(value) { + if ($.isCollection(value)) { + for (var el of Array.from($.makeArray(value))) { if (el.parentNode != null) { + el.parentNode.removeChild(el); + } } + } else { + if (value.parentNode != null) { + value.parentNode.removeChild(value); + } + } +}; + +$.empty = function(el) { + while (el.firstChild) { el.removeChild(el.firstChild); } +}; + +// Calls the function while the element is off the DOM to avoid triggering +// unnecessary reflows and repaints. +$.batchUpdate = function(el, fn) { + const parent = el.parentNode; + const sibling = el.nextSibling; + parent.removeChild(el); + + fn(el); + + if (sibling) { + parent.insertBefore(el, sibling); + } else { + parent.appendChild(el); + } +}; + +// +// Offset +// + +$.rect = el => el.getBoundingClientRect(); + +$.offset = function(el, container) { + if (container == null) { container = document.body; } + let top = 0; + let left = 0; + + while (el && (el !== container)) { + top += el.offsetTop; + left += el.offsetLeft; + el = el.offsetParent; + } + + return { + top, + left + }; +}; + +$.scrollParent = function(el) { + while ((el = el.parentNode) && (el.nodeType === 1)) { + var needle; + if (el.scrollTop > 0) { break; } + if ((needle = __guard__(getComputedStyle(el), x => x.overflowY), ['auto', 'scroll'].includes(needle))) { break; } + } + return el; +}; + +$.scrollTo = function(el, parent, position, options) { + if (position == null) { position = 'center'; } + if (options == null) { options = {}; } + if (!el) { return; } + + if (parent == null) { parent = $.scrollParent(el); } + if (!parent) { return; } + + const parentHeight = parent.clientHeight; + const parentScrollHeight = parent.scrollHeight; + if (!(parentScrollHeight > parentHeight)) { return; } + + const { + top + } = $.offset(el, parent); + const { + offsetTop + } = parent.firstElementChild; + + switch (position) { + case 'top': + parent.scrollTop = top - offsetTop - ((options.margin != null) ? options.margin : 0); + break; + case 'center': + parent.scrollTop = top - Math.round((parentHeight / 2) - (el.offsetHeight / 2)); + break; + case 'continuous': + var { + scrollTop + } = parent; + var height = el.offsetHeight; + + var lastElementOffset = parent.lastElementChild.offsetTop + parent.lastElementChild.offsetHeight; + var offsetBottom = lastElementOffset > 0 ? parentScrollHeight - lastElementOffset : 0; + + // If the target element is above the visible portion of its scrollable + // ancestor, move it near the top with a gap = options.topGap * target's height. + if ((top - offsetTop) <= (scrollTop + (height * (options.topGap || 1)))) { + parent.scrollTop = top - offsetTop - (height * (options.topGap || 1)); + // If the target element is below the visible portion of its scrollable + // ancestor, move it near the bottom with a gap = options.bottomGap * target's height. + } else if ((top + offsetBottom) >= ((scrollTop + parentHeight) - (height * ((options.bottomGap || 1) + 1)))) { + parent.scrollTop = ((top + offsetBottom) - parentHeight) + (height * ((options.bottomGap || 1) + 1)); + } + break; + } +}; + +$.scrollToWithImageLock = function(el, parent, ...args) { + if (parent == null) { parent = $.scrollParent(el); } + if (!parent) { return; } + + $.scrollTo(el, parent, ...Array.from(args)); + + // Lock the scroll position on the target element for up to 3 seconds while + // nearby images are loaded and rendered. + for (var image of Array.from(parent.getElementsByTagName('img'))) { + if (!image.complete) { + (function() { + let timeout; + const onLoad = function(event) { + clearTimeout(timeout); + unbind(event.target); + return $.scrollTo(el, parent, ...Array.from(args)); + }; + + var unbind = target => $.off(target, 'load', onLoad); + + $.on(image, 'load', onLoad); + return timeout = setTimeout(unbind.bind(null, image), 3000); + })(); + } + } +}; + +// Calls the function while locking the element's position relative to the window. +$.lockScroll = function(el, fn) { + let parent; + if (parent = $.scrollParent(el)) { + let { + top + } = $.rect(el); + if (![document.body, document.documentElement].includes(parent)) { top -= $.rect(parent).top; } + fn(); + parent.scrollTop = $.offset(el, parent).top - top; + } else { + fn(); + } +}; + +let smoothScroll = (smoothStart = (smoothEnd = (smoothDistance = (smoothDuration = null)))); + +$.smoothScroll = function(el, end) { + if (!window.requestAnimationFrame) { + el.scrollTop = end; + return; + } + + smoothEnd = end; + + if (smoothScroll) { + const newDistance = smoothEnd - smoothStart; + smoothDuration += Math.min(300, Math.abs(smoothDistance - newDistance)); + smoothDistance = newDistance; + return; + } + + smoothStart = el.scrollTop; + smoothDistance = smoothEnd - smoothStart; + smoothDuration = Math.min(300, Math.abs(smoothDistance)); + const startTime = Date.now(); + + smoothScroll = function() { + const p = Math.min(1, (Date.now() - startTime) / smoothDuration); + const y = Math.max(0, Math.floor(smoothStart + (smoothDistance * (p < 0.5 ? 2 * p * p : (p * (4 - (p * 2))) - 1)))); + el.scrollTop = y; + if (p === 1) { + return smoothScroll = null; + } else { + return requestAnimationFrame(smoothScroll); + } + }; + return requestAnimationFrame(smoothScroll); +}; + +// +// Utilities +// + +$.extend = function(target, ...objects) { + for (var object of Array.from(objects)) { + if (object) { + for (var key in object) { + var value = object[key]; + target[key] = value; + } + } + } + return target; +}; + +$.makeArray = function(object) { + if (Array.isArray(object)) { + return object; + } else { + return Array.prototype.slice.apply(object); + } +}; + +$.arrayDelete = function(array, object) { + const index = array.indexOf(object); + if (index >= 0) { + array.splice(index, 1); + return true; + } else { + return false; + } +}; + +// Returns true if the object is an array or a collection of DOM elements. +$.isCollection = object => Array.isArray(object) || (typeof (object != null ? object.item : undefined) === 'function'); + +const ESCAPE_HTML_MAP = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', '/': '/' - -ESCAPE_HTML_REGEXP = /[&<>"'\/]/g - -$.escape = (string) -> - string.replace ESCAPE_HTML_REGEXP, (match) -> ESCAPE_HTML_MAP[match] - -ESCAPE_REGEXP = /([.*+?^=!:${}()|\[\]\/\\])/g - -$.escapeRegexp = (string) -> - string.replace ESCAPE_REGEXP, "\\$1" - -$.urlDecode = (string) -> - decodeURIComponent string.replace(/\+/g, '%20') - -$.classify = (string) -> - string = string.split('_') - for substr, i in string - string[i] = substr[0].toUpperCase() + substr[1..] - string.join('') - -$.framify = (fn, obj) -> - if window.requestAnimationFrame - (args...) -> requestAnimationFrame(fn.bind(obj, args...)) - else - fn - -$.requestAnimationFrame = (fn) -> - if window.requestAnimationFrame - requestAnimationFrame(fn) - else - setTimeout(fn, 0) - return - -# -# Miscellaneous -# - -$.noop = -> - -$.popup = (value) -> - try - win = window.open() - win.opener = null if win.opener - win.location = value.href or value - catch - window.open value.href or value, '_blank' - return - -isMac = null -$.isMac = -> - isMac ?= navigator.userAgent?.indexOf('Mac') >= 0 - -isIE = null -$.isIE = -> - isIE ?= navigator.userAgent?.indexOf('MSIE') >= 0 || navigator.userAgent?.indexOf('rv:11.0') >= 0 - -isChromeForAndroid = null -$.isChromeForAndroid = -> - isChromeForAndroid ?= navigator.userAgent?.indexOf('Android') >= 0 && /Chrome\/([.0-9])+ Mobile/.test(navigator.userAgent) - -isAndroid = null -$.isAndroid = -> - isAndroid ?= navigator.userAgent?.indexOf('Android') >= 0 - -isIOS = null -$.isIOS = -> - isIOS ?= navigator.userAgent?.indexOf('iPhone') >= 0 || navigator.userAgent?.indexOf('iPad') >= 0 - -$.overlayScrollbarsEnabled = -> - return false unless $.isMac() - div = document.createElement('div') - div.setAttribute('style', 'width: 100px; height: 100px; overflow: scroll; position: absolute') - document.body.appendChild(div) - result = div.offsetWidth is div.clientWidth - document.body.removeChild(div) - result - -HIGHLIGHT_DEFAULTS = - className: 'highlight' +}; + +const ESCAPE_HTML_REGEXP = /[&<>"'\/]/g; + +$.escape = string => string.replace(ESCAPE_HTML_REGEXP, match => ESCAPE_HTML_MAP[match]); + +const ESCAPE_REGEXP = /([.*+?^=!:${}()|\[\]\/\\])/g; + +$.escapeRegexp = string => string.replace(ESCAPE_REGEXP, "\\$1"); + +$.urlDecode = string => decodeURIComponent(string.replace(/\+/g, '%20')); + +$.classify = function(string) { + string = string.split('_'); + for (let i = 0; i < string.length; i++) { + var substr = string[i]; + string[i] = substr[0].toUpperCase() + substr.slice(1); + } + return string.join(''); +}; + +$.framify = function(fn, obj) { + if (window.requestAnimationFrame) { + return (...args) => requestAnimationFrame(fn.bind(obj, ...Array.from(args))); + } else { + return fn; + } +}; + +$.requestAnimationFrame = function(fn) { + if (window.requestAnimationFrame) { + requestAnimationFrame(fn); + } else { + setTimeout(fn, 0); + } +}; + +// +// Miscellaneous +// + +$.noop = function() {}; + +$.popup = function(value) { + try { + const win = window.open(); + if (win.opener) { win.opener = null; } + win.location = value.href || value; + } catch (error) { + window.open(value.href || value, '_blank'); + } +}; + +let isMac = null; +$.isMac = () => isMac != null ? isMac : (isMac = (navigator.userAgent != null ? navigator.userAgent.indexOf('Mac') : undefined) >= 0); + +let isIE = null; +$.isIE = () => isIE != null ? isIE : (isIE = ((navigator.userAgent != null ? navigator.userAgent.indexOf('MSIE') : undefined) >= 0) || ((navigator.userAgent != null ? navigator.userAgent.indexOf('rv:11.0') : undefined) >= 0)); + +let isChromeForAndroid = null; +$.isChromeForAndroid = () => isChromeForAndroid != null ? isChromeForAndroid : (isChromeForAndroid = ((navigator.userAgent != null ? navigator.userAgent.indexOf('Android') : undefined) >= 0) && /Chrome\/([.0-9])+ Mobile/.test(navigator.userAgent)); + +let isAndroid = null; +$.isAndroid = () => isAndroid != null ? isAndroid : (isAndroid = (navigator.userAgent != null ? navigator.userAgent.indexOf('Android') : undefined) >= 0); + +let isIOS = null; +$.isIOS = () => isIOS != null ? isIOS : (isIOS = ((navigator.userAgent != null ? navigator.userAgent.indexOf('iPhone') : undefined) >= 0) || ((navigator.userAgent != null ? navigator.userAgent.indexOf('iPad') : undefined) >= 0)); + +$.overlayScrollbarsEnabled = function() { + if (!$.isMac()) { return false; } + const div = document.createElement('div'); + div.setAttribute('style', 'width: 100px; height: 100px; overflow: scroll; position: absolute'); + document.body.appendChild(div); + const result = div.offsetWidth === div.clientWidth; + document.body.removeChild(div); + return result; +}; + +const HIGHLIGHT_DEFAULTS = { + className: 'highlight', delay: 1000 - -$.highlight = (el, options = {}) -> - options = $.extend {}, HIGHLIGHT_DEFAULTS, options - el.classList.add(options.className) - setTimeout (-> el.classList.remove(options.className)), options.delay - return - -$.copyToClipboard = (string) -> - textarea = document.createElement('textarea') - textarea.style.position = 'fixed' - textarea.style.opacity = 0 - textarea.value = string - document.body.appendChild(textarea) - try - textarea.select() - result = !!document.execCommand('copy') - catch - result = false - finally - document.body.removeChild(textarea) - result +}; + +$.highlight = function(el, options) { + if (options == null) { options = {}; } + options = $.extend({}, HIGHLIGHT_DEFAULTS, options); + el.classList.add(options.className); + setTimeout((() => el.classList.remove(options.className)), options.delay); +}; + +$.copyToClipboard = function(string) { + let result; + const textarea = document.createElement('textarea'); + textarea.style.position = 'fixed'; + textarea.style.opacity = 0; + textarea.value = string; + document.body.appendChild(textarea); + try { + textarea.select(); + result = !!document.execCommand('copy'); + } catch (error) { + result = false; + } + finally { + document.body.removeChild(textarea); + } + return result; +}; + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/assets/javascripts/models/doc.js b/assets/javascripts/models/doc.js index c51e13fae4..8ca5b01422 100644 --- a/assets/javascripts/models/doc.js +++ b/assets/javascripts/models/doc.js @@ -1,147 +1,174 @@ -class app.models.Doc extends app.Model - # Attributes: name, slug, type, version, release, db_size, mtime, links - - constructor: -> - super - @reset @ - @slug_without_version = @slug.split('~')[0] - @fullName = "#{@name}" + if @version then " #{@version}" else '' - @icon = @slug_without_version - @short_version = @version.split(' ')[0] if @version - @text = @toEntry().text - - reset: (data) -> - @resetEntries data.entries - @resetTypes data.types - return - - resetEntries: (entries) -> - @entries = new app.collections.Entries(entries) - @entries.each (entry) => entry.doc = @ - return - - resetTypes: (types) -> - @types = new app.collections.Types(types) - @types.each (type) => type.doc = @ - return - - fullPath: (path = '') -> - path = "/#{path}" unless path[0] is '/' - "/#{@slug}#{path}" - - fileUrl: (path) -> - "#{app.config.docs_origin}#{@fullPath(path)}?#{@mtime}" - - dbUrl: -> - "#{app.config.docs_origin}/#{@slug}/#{app.config.db_filename}?#{@mtime}" - - indexUrl: -> - "#{app.indexHost()}/#{@slug}/#{app.config.index_filename}?#{@mtime}" - - toEntry: -> - return @entry if @entry - @entry = new app.models.Entry - doc: @ - name: @fullName +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +app.models.Doc = class Doc extends app.Model { + // Attributes: name, slug, type, version, release, db_size, mtime, links + + constructor() { + super(...arguments); + this.reset(this); + this.slug_without_version = this.slug.split('~')[0]; + this.fullName = `${this.name}` + (this.version ? ` ${this.version}` : ''); + this.icon = this.slug_without_version; + if (this.version) { this.short_version = this.version.split(' ')[0]; } + this.text = this.toEntry().text; + } + + reset(data) { + this.resetEntries(data.entries); + this.resetTypes(data.types); + } + + resetEntries(entries) { + this.entries = new app.collections.Entries(entries); + this.entries.each(entry => { return entry.doc = this; }); + } + + resetTypes(types) { + this.types = new app.collections.Types(types); + this.types.each(type => { return type.doc = this; }); + } + + fullPath(path) { + if (path == null) { path = ''; } + if (path[0] !== '/') { path = `/${path}`; } + return `/${this.slug}${path}`; + } + + fileUrl(path) { + return `${app.config.docs_origin}${this.fullPath(path)}?${this.mtime}`; + } + + dbUrl() { + return `${app.config.docs_origin}/${this.slug}/${app.config.db_filename}?${this.mtime}`; + } + + indexUrl() { + return `${app.indexHost()}/${this.slug}/${app.config.index_filename}?${this.mtime}`; + } + + toEntry() { + if (this.entry) { return this.entry; } + this.entry = new app.models.Entry({ + doc: this, + name: this.fullName, path: 'index' - @entry.addAlias(@name) if @version - @entry - - findEntryByPathAndHash: (path, hash) -> - if hash and entry = @entries.findBy 'path', "#{path}##{hash}" - entry - else if path is 'index' - @toEntry() - else - @entries.findBy 'path', path - - load: (onSuccess, onError, options = {}) -> - return if options.readCache and @_loadFromCache(onSuccess) - - callback = (data) => - @reset data - onSuccess() - @_setCache data if options.writeCache - return - - ajax - url: @indexUrl() - success: callback + }); + if (this.version) { this.entry.addAlias(this.name); } + return this.entry; + } + + findEntryByPathAndHash(path, hash) { + let entry; + if (hash && (entry = this.entries.findBy('path', `${path}#${hash}`))) { + return entry; + } else if (path === 'index') { + return this.toEntry(); + } else { + return this.entries.findBy('path', path); + } + } + + load(onSuccess, onError, options) { + if (options == null) { options = {}; } + if (options.readCache && this._loadFromCache(onSuccess)) { return; } + + const callback = data => { + this.reset(data); + onSuccess(); + if (options.writeCache) { this._setCache(data); } + }; + + return ajax({ + url: this.indexUrl(), + success: callback, error: onError - - clearCache: -> - app.localStorage.del @slug - return - - _loadFromCache: (onSuccess) -> - return unless data = @_getCache() - - callback = => - @reset data - onSuccess() - return - - setTimeout callback, 0 - true - - _getCache: -> - return unless data = app.localStorage.get @slug - - if data[0] is @mtime - return data[1] - else - @clearCache() - return - - _setCache: (data) -> - app.localStorage.set @slug, [@mtime, data] - return - - install: (onSuccess, onError, onProgress) -> - return if @installing - @installing = true - - error = => - @installing = null - onError() - return - - success = (data) => - @installing = null - app.db.store @, data, onSuccess, error - return - - ajax - url: @dbUrl() - success: success - error: error - progress: onProgress + }); + } + + clearCache() { + app.localStorage.del(this.slug); + } + + _loadFromCache(onSuccess) { + let data; + if (!(data = this._getCache())) { return; } + + const callback = () => { + this.reset(data); + onSuccess(); + }; + + setTimeout(callback, 0); + return true; + } + + _getCache() { + let data; + if (!(data = app.localStorage.get(this.slug))) { return; } + + if (data[0] === this.mtime) { + return data[1]; + } else { + this.clearCache(); + return; + } + } + + _setCache(data) { + app.localStorage.set(this.slug, [this.mtime, data]); + } + + install(onSuccess, onError, onProgress) { + if (this.installing) { return; } + this.installing = true; + + const error = () => { + this.installing = null; + onError(); + }; + + const success = data => { + this.installing = null; + app.db.store(this, data, onSuccess, error); + }; + + ajax({ + url: this.dbUrl(), + success, + error, + progress: onProgress, timeout: 3600 - return - - uninstall: (onSuccess, onError) -> - return if @installing - @installing = true - - success = => - @installing = null - onSuccess() - return - - error = => - @installing = null - onError() - return - - app.db.unstore @, success, error - return - - getInstallStatus: (callback) -> - app.db.version @, (value) -> - callback installed: !!value, mtime: value - return - - isOutdated: (status) -> - return false if not status - isInstalled = status.installed or app.settings.get('autoInstall') - isInstalled and @mtime isnt status.mtime + }); + } + + uninstall(onSuccess, onError) { + if (this.installing) { return; } + this.installing = true; + + const success = () => { + this.installing = null; + onSuccess(); + }; + + const error = () => { + this.installing = null; + onError(); + }; + + app.db.unstore(this, success, error); + } + + getInstallStatus(callback) { + app.db.version(this, value => callback({installed: !!value, mtime: value})); + } + + isOutdated(status) { + if (!status) { return false; } + const isInstalled = status.installed || app.settings.get('autoInstall'); + return isInstalled && (this.mtime !== status.mtime); + } +}; diff --git a/assets/javascripts/models/entry.js b/assets/javascripts/models/entry.js index 2d07c1599c..99b0b525c6 100644 --- a/assets/javascripts/models/entry.js +++ b/assets/javascripts/models/entry.js @@ -1,85 +1,116 @@ -#= require app/searcher +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +//= require app/searcher -class app.models.Entry extends app.Model - # Attributes: name, type, path +(function() { + let applyAliases = undefined; + const Cls = (app.models.Entry = class Entry extends app.Model { + static initClass() { + + let ALIASES; + applyAliases = function(string) { + if (ALIASES.hasOwnProperty(string)) { + return [string, ALIASES[string]]; + } else { + const words = string.split('.'); + for (let i = 0; i < words.length; i++) { + var word = words[i]; + if (ALIASES.hasOwnProperty(word)) { + words[i] = ALIASES[word]; + return [string, words.join('.')]; + } + } + } + return string; + }; + + this.ALIASES = (ALIASES = { + 'angular': 'ng', + 'angular.js': 'ng', + 'backbone.js': 'bb', + 'c++': 'cpp', + 'coffeescript': 'cs', + 'crystal': 'cr', + 'elixir': 'ex', + 'javascript': 'js', + 'julia': 'jl', + 'jquery': '$', + 'knockout.js': 'ko', + 'kubernetes': 'k8s', + 'less': 'ls', + 'lodash': '_', + 'löve': 'love', + 'marionette': 'mn', + 'markdown': 'md', + 'matplotlib': 'mpl', + 'modernizr': 'mdr', + 'moment.js': 'mt', + 'openjdk': 'java', + 'nginx': 'ngx', + 'numpy': 'np', + 'pandas': 'pd', + 'postgresql': 'pg', + 'python': 'py', + 'ruby.on.rails': 'ror', + 'ruby': 'rb', + 'rust': 'rs', + 'sass': 'scss', + 'tensorflow': 'tf', + 'typescript': 'ts', + 'underscore.js': '_' + }); + } + // Attributes: name, type, path - constructor: -> - super - @text = applyAliases(app.Searcher.normalizeString(@name)) + constructor() { + super(...arguments); + this.text = applyAliases(app.Searcher.normalizeString(this.name)); + } - addAlias: (name) -> - text = applyAliases(app.Searcher.normalizeString(name)) - @text = [@text] unless Array.isArray(@text) - @text.push(if Array.isArray(text) then text[1] else text) - return + addAlias(name) { + const text = applyAliases(app.Searcher.normalizeString(name)); + if (!Array.isArray(this.text)) { this.text = [this.text]; } + this.text.push(Array.isArray(text) ? text[1] : text); + } - fullPath: -> - @doc.fullPath if @isIndex() then '' else @path + fullPath() { + return this.doc.fullPath(this.isIndex() ? '' : this.path); + } - dbPath: -> - @path.replace /#.*/, '' + dbPath() { + return this.path.replace(/#.*/, ''); + } - filePath: -> - @doc.fullPath @_filePath() + filePath() { + return this.doc.fullPath(this._filePath()); + } - fileUrl: -> - @doc.fileUrl @_filePath() + fileUrl() { + return this.doc.fileUrl(this._filePath()); + } - _filePath: -> - result = @path.replace /#.*/, '' - result += '.html' unless result[-5..-1] is '.html' - result + _filePath() { + let result = this.path.replace(/#.*/, ''); + if (result.slice(-5) !== '.html') { result += '.html'; } + return result; + } - isIndex: -> - @path is 'index' + isIndex() { + return this.path === 'index'; + } - getType: -> - @doc.types.findBy 'name', @type + getType() { + return this.doc.types.findBy('name', this.type); + } - loadFile: (onSuccess, onError) -> - app.db.load(@, onSuccess, onError) - - applyAliases = (string) -> - if ALIASES.hasOwnProperty(string) - return [string, ALIASES[string]] - else - words = string.split('.') - for word, i in words when ALIASES.hasOwnProperty(word) - words[i] = ALIASES[word] - return [string, words.join('.')] - return string - - @ALIASES = ALIASES = - 'angular': 'ng' - 'angular.js': 'ng' - 'backbone.js': 'bb' - 'c++': 'cpp' - 'coffeescript': 'cs' - 'crystal': 'cr' - 'elixir': 'ex' - 'javascript': 'js' - 'julia': 'jl' - 'jquery': '$' - 'knockout.js': 'ko' - 'kubernetes': 'k8s' - 'less': 'ls' - 'lodash': '_' - 'löve': 'love' - 'marionette': 'mn' - 'markdown': 'md' - 'matplotlib': 'mpl' - 'modernizr': 'mdr' - 'moment.js': 'mt' - 'openjdk': 'java' - 'nginx': 'ngx' - 'numpy': 'np' - 'pandas': 'pd' - 'postgresql': 'pg' - 'python': 'py' - 'ruby.on.rails': 'ror' - 'ruby': 'rb' - 'rust': 'rs' - 'sass': 'scss' - 'tensorflow': 'tf' - 'typescript': 'ts' - 'underscore.js': '_' + loadFile(onSuccess, onError) { + return app.db.load(this, onSuccess, onError); + } + }); + Cls.initClass(); + return Cls; +})(); diff --git a/assets/javascripts/models/model.js b/assets/javascripts/models/model.js index 7f157f7c4f..405032b6ee 100644 --- a/assets/javascripts/models/model.js +++ b/assets/javascripts/models/model.js @@ -1,3 +1,5 @@ -class app.Model - constructor: (attributes) -> - @[key] = value for key, value of attributes +app.Model = class Model { + constructor(attributes) { + for (var key in attributes) { var value = attributes[key]; this[key] = value; } + } +}; diff --git a/assets/javascripts/models/type.js b/assets/javascripts/models/type.js index 6351ad16d7..29c30ac99f 100644 --- a/assets/javascripts/models/type.js +++ b/assets/javascripts/models/type.js @@ -1,14 +1,24 @@ -class app.models.Type extends app.Model - # Attributes: name, slug, count +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +app.models.Type = class Type extends app.Model { + // Attributes: name, slug, count - fullPath: -> - "/#{@doc.slug}-#{@slug}/" + fullPath() { + return `/${this.doc.slug}-${this.slug}/`; + } - entries: -> - @doc.entries.findAllBy 'type', @name + entries() { + return this.doc.entries.findAllBy('type', this.name); + } - toEntry: -> - new app.models.Entry - doc: @doc - name: "#{@doc.name} / #{@name}" - path: '..' + @fullPath() + toEntry() { + return new app.models.Entry({ + doc: this.doc, + name: `${this.doc.name} / ${this.name}`, + path: '..' + this.fullPath() + }); + } +}; diff --git a/assets/javascripts/templates/base.js b/assets/javascripts/templates/base.js index 841d1e0bb7..efcf7a1b96 100644 --- a/assets/javascripts/templates/base.js +++ b/assets/javascripts/templates/base.js @@ -1,11 +1,19 @@ -app.templates.render = (name, value, args...) -> - template = app.templates[name] +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +app.templates.render = function(name, value, ...args) { + const template = app.templates[name]; - if Array.isArray(value) - result = '' - result += template(val, args...) for val in value - result - else if typeof template is 'function' - template(value, args...) - else - template + if (Array.isArray(value)) { + let result = ''; + for (var val of Array.from(value)) { result += template(val, ...Array.from(args)); } + return result; + } else if (typeof template === 'function') { + return template(value, ...Array.from(args)); + } else { + return template; + } +}; diff --git a/assets/javascripts/templates/error_tmpl.js b/assets/javascripts/templates/error_tmpl.js index 9cca1f9d32..c5b8ac6225 100644 --- a/assets/javascripts/templates/error_tmpl.js +++ b/assets/javascripts/templates/error_tmpl.js @@ -1,73 +1,85 @@ -error = (title, text = '', links = '') -> - text = """ #{text}
""" if text - links = """#{links}
""" if links - """""" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const error = function(title, text, links) { + if (text == null) { text = ''; } + if (links == null) { links = ''; } + if (text) { text = `#{title}
#{text}#{links}${text}
`; } + if (links) { links = `${links}
`; } + return ``; +}; -back = 'Go back' +const back = 'Go back'; -app.templates.notFoundPage = -> - error """ Page not found. """, - """ It may be missing from the source documentation or this could be a bug. """, - back +app.templates.notFoundPage = () => error(" Page not found. ", + " It may be missing from the source documentation or this could be a bug. ", + back); -app.templates.pageLoadError = -> - error """ The page failed to load. """, - """ It may be missing from the server (try reloading the app) or you could be offline (try installing the documentation for offline usage when online again).${title}
${text}${links}
- If you're online and you keep seeing this, you're likely behind a proxy or firewall that blocks cross-domain requests. """, - """ #{back} · Reload - · Retry """ +app.templates.pageLoadError = () => error(" The page failed to load. ", + ` It may be missing from the server (try reloading the app) or you could be offline (try installing the documentation for offline usage when online again).
+If you're online and you keep seeing this, you're likely behind a proxy or firewall that blocks cross-domain requests. `, + ` ${back} · Reload +· Retry ` +); -app.templates.bootError = -> - error """ The app failed to load. """, - """ Check your Internet connection and try reloading.
- If you keep seeing this, you're likely behind a proxy or firewall that blocks cross-domain requests. """ +app.templates.bootError = () => error(" The app failed to load. ", + ` Check your Internet connection and try reloading.
+If you keep seeing this, you're likely behind a proxy or firewall that blocks cross-domain requests. ` +); -app.templates.offlineError = (reason, exception) -> - if reason is 'cookie_blocked' - return error """ Cookies must be enabled to use offline mode. """ +app.templates.offlineError = function(reason, exception) { + if (reason === 'cookie_blocked') { + return error(" Cookies must be enabled to use offline mode. "); + } - reason = switch reason - when 'not_supported' - """ DevDocs requires IndexedDB to cache documentations for offline access.
- Unfortunately your browser either doesn't support IndexedDB or doesn't make it available. """ - when 'buggy' - """ DevDocs requires IndexedDB to cache documentations for offline access.
- Unfortunately your browser's implementation of IndexedDB contains bugs that prevent DevDocs from using it. """ - when 'private_mode' - """ Your browser appears to be running in private mode.
- This prevents DevDocs from caching documentations for offline access.""" - when 'exception' - """ An error occurred when trying to open the IndexedDB database:
-#{exception.name}: #{exception.message}""" - when 'cant_open' - """ An error occurred when trying to open the IndexedDB database:
-#{exception.name}: #{exception.message}
- This could be because you're browsing in private mode or have disallowed offline storage on the domain. """ - when 'version' - """ The IndexedDB database was modified with a newer version of the app.
- Reload the page to use offline mode. """ - when 'empty' - """ The IndexedDB database appears to be corrupted. Try resetting the app. """ + reason = (() => { switch (reason) { + case 'not_supported': + return ` DevDocs requires IndexedDB to cache documentations for offline access.
+Unfortunately your browser either doesn't support IndexedDB or doesn't make it available. `; + case 'buggy': + return ` DevDocs requires IndexedDB to cache documentations for offline access.
+Unfortunately your browser's implementation of IndexedDB contains bugs that prevent DevDocs from using it. `; + case 'private_mode': + return ` Your browser appears to be running in private mode.
+This prevents DevDocs from caching documentations for offline access.`; + case 'exception': + return ` An error occurred when trying to open the IndexedDB database:
+${exception.name}: ${exception.message}`; + case 'cant_open': + return ` An error occurred when trying to open the IndexedDB database:
+${exception.name}: ${exception.message}
+This could be because you're browsing in private mode or have disallowed offline storage on the domain. `; + case 'version': + return ` The IndexedDB database was modified with a newer version of the app.
+Reload the page to use offline mode. `; + case 'empty': + return " The IndexedDB database appears to be corrupted. Try resetting the app. "; + } })(); - error 'Offline mode is unavailable.', reason + return error('Offline mode is unavailable.', reason); +}; -app.templates.unsupportedBrowser = """ ---""" +app.templates.unsupportedBrowser = `\ +Your browser is unsupported, sorry.
-DevDocs is an API documentation browser which supports the following browsers: -
-
-- Recent versions of Firefox, Chrome, or Opera -
- Safari 11.1+ -
- Edge 17+ -
- iOS 11.3+ -
- If you're unable to upgrade, we apologize. - We decided to prioritize speed and new features over support for older browsers. -
- Note: if you're already using one of the browsers above, check your settings and add-ons. - The app uses feature detection, not user agent sniffing. -
- — @DevDocs -
+\ +`; diff --git a/assets/javascripts/templates/notice_tmpl.js b/assets/javascripts/templates/notice_tmpl.js index 10cc534eb0..2d64ecd276 100644 --- a/assets/javascripts/templates/notice_tmpl.js +++ b/assets/javascripts/templates/notice_tmpl.js @@ -1,9 +1,14 @@ -notice = (text) -> """Your browser is unsupported, sorry.
+DevDocs is an API documentation browser which supports the following browsers: +
+
+- Recent versions of Firefox, Chrome, or Opera +
- Safari 11.1+ +
- Edge 17+ +
- iOS 11.3+ +
+ If you're unable to upgrade, we apologize. + We decided to prioritize speed and new features over support for older browsers. +
+ Note: if you're already using one of the browsers above, check your settings and add-ons. + The app uses feature detection, not user agent sniffing. +
+ — @DevDocs +
#{text}
""" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const notice = text => `${text}
`; -app.templates.singleDocNotice = (doc) -> - notice """ You're browsing the #{doc.fullName} documentation. To browse all docs, go to - #{app.config.production_host} (or pressesc). """ +app.templates.singleDocNotice = doc => notice(` You're browsing the ${doc.fullName} documentation. To browse all docs, go to +${app.config.production_host} (or pressesc). ` +); -app.templates.disabledDocNotice = -> - notice """ This documentation is disabled. - To enable it, go to Preferences. """ +app.templates.disabledDocNotice = () => notice(` This documentation is disabled. +To enable it, go to Preferences. ` +); diff --git a/assets/javascripts/templates/notif_tmpl.js b/assets/javascripts/templates/notif_tmpl.js index 0821036e77..f58808c6f5 100644 --- a/assets/javascripts/templates/notif_tmpl.js +++ b/assets/javascripts/templates/notif_tmpl.js @@ -1,76 +1,81 @@ -notif = (title, html) -> - html = html.replace /#{title} - #{html} - - """ +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const notif = function(title, html) { + html = html.replace(/${title} +${html} +\ +`; +}; -textNotif = (title, message) -> - notif title, """#{message}""" +const textNotif = (title, message) => notif(title, `
${message}`); -app.templates.notifUpdateReady = -> - textNotif """DevDocs has been updated.""", - """Reload the page to use the new version.""" +app.templates.notifUpdateReady = () => textNotif("DevDocs has been updated.", + "Reload the page to use the new version."); -app.templates.notifError = -> - textNotif """ Oops, an error occurred. """, - """ Try reloading, and if the problem persists, - resetting the app.
- You can also report this issue on GitHub. """ +app.templates.notifError = () => textNotif(" Oops, an error occurred. ", + ` Try reloading, and if the problem persists, +resetting the app.
+You can also report this issue on GitHub. ` +); -app.templates.notifQuotaExceeded = -> - textNotif """ The offline database has exceeded its size limitation. """, - """ Unfortunately this quota can't be detected programmatically, and the database can't be opened while over the quota, so it had to be reset. """ +app.templates.notifQuotaExceeded = () => textNotif(" The offline database has exceeded its size limitation. ", + " Unfortunately this quota can't be detected programmatically, and the database can't be opened while over the quota, so it had to be reset. "); -app.templates.notifCookieBlocked = -> - textNotif """ Please enable cookies. """, - """ DevDocs will not work properly if cookies are disabled. """ +app.templates.notifCookieBlocked = () => textNotif(" Please enable cookies. ", + " DevDocs will not work properly if cookies are disabled. "); -app.templates.notifInvalidLocation = -> - textNotif """ DevDocs must be loaded from #{app.config.production_host} """, - """ Otherwise things are likely to break. """ +app.templates.notifInvalidLocation = () => textNotif(` DevDocs must be loaded from ${app.config.production_host} `, + " Otherwise things are likely to break. "); -app.templates.notifImportInvalid = -> - textNotif """ Oops, an error occurred. """, - """ The file you selected is invalid. """ +app.templates.notifImportInvalid = () => textNotif(" Oops, an error occurred. ", + " The file you selected is invalid. "); -app.templates.notifNews = (news) -> - notif 'Changelog', """#{app.templates.newsList(news, years: false)}""" +app.templates.notifNews = news => notif('Changelog', `${app.templates.newsList(news, {years: false})}`); -app.templates.notifUpdates = (docs, disabledDocs) -> - html = '' +app.templates.notifUpdates = function(docs, disabledDocs) { + let doc; + let html = '`); +}; -app.templates.notifShare = -> - textNotif """ Hi there! """, - """ Like DevDocs? Help us reach more developers by sharing the link with your friends on - Twitter, Facebook, - Reddit, etc.'; - if docs.length > 0 - html += '" + return notif('Updates', `${html}' - html += '' + if (docs.length > 0) { + html += '' - for doc in docs - html += "
- #{doc.name}" - html += "
→#{doc.release}" if doc.release - html += ''; + html += ''; + } - if disabledDocs.length > 0 - html += ''; + for (doc of Array.from(docs)) { + html += `
- ${doc.name}`; + if (doc.release) { html += `
→${doc.release}`; } + } + html += '' + if (disabledDocs.length > 0) { + html += 'Disabled:' - html += '
' - for doc in disabledDocs - html += "
- #{doc.name}" - html += "
→#{doc.release}" if doc.release - html += """Enable""" - html += ''; + } - notif 'Updates', "#{html}Disabled:'; + html += '
'; + for (doc of Array.from(disabledDocs)) { + html += `
- ${doc.name}`; + if (doc.release) { html += `
→${doc.release}`; } + html += "Enable"; + } + html += '
Thanks :) """ +app.templates.notifShare = () => textNotif(" Hi there! ", + ` Like DevDocs? Help us reach more developers by sharing the link with your friends on +Twitter, Facebook, +Reddit, etc.
Thanks :) ` +); -app.templates.notifUpdateDocs = -> - textNotif """ Documentation updates available. """, - """ Install them as soon as possible to avoid broken pages. """ +app.templates.notifUpdateDocs = () => textNotif(" Documentation updates available. ", + " Install them as soon as possible to avoid broken pages. "); -app.templates.notifAnalyticsConsent = -> - textNotif """ Tracking cookies """, - """ We would like to gather usage data about how DevDocs is used through Google Analytics and Gauges. We only collect anonymous traffic information. - Please confirm if you accept our tracking cookies. You can always change your decision in the settings. -
Accept or Decline """ +app.templates.notifAnalyticsConsent = () => textNotif(" Tracking cookies ", + ` We would like to gather usage data about how DevDocs is used through Google Analytics and Gauges. We only collect anonymous traffic information. +Please confirm if you accept our tracking cookies. You can always change your decision in the settings. +
Accept or Decline ` +); diff --git a/assets/javascripts/templates/pages/about_tmpl.js b/assets/javascripts/templates/pages/about_tmpl.js index 5f1166c0fa..c981a3c368 100644 --- a/assets/javascripts/templates/pages/about_tmpl.js +++ b/assets/javascripts/templates/pages/about_tmpl.js @@ -1,91 +1,106 @@ -app.templates.aboutPage = -> - all_docs = app.docs.all().concat(app.disabledDocs.all()...) - # de-duplicate docs by doc.name - docs = [] - docs.push doc for doc in all_docs when not (docs.find (d) -> d.name == doc.name) - """ - - -DevDocs: API Documentation Browser
-DevDocs combines multiple developer documentations in a clean and organized web UI with instant search, offline support, mobile version, dark theme, keyboard shortcuts, and more. -
DevDocs is free and open source. It was created by Thibaut Courouble and is operated by freeCodeCamp. -
To keep up-to-date with the latest news: -
-
- Follow @DevDocs on Twitter -
- Watch the repository on GitHub -
- Join the Discord chat room +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +app.templates.aboutPage = function() { + let doc; + const all_docs = app.docs.all().concat(...Array.from(app.disabledDocs.all() || [])); + // de-duplicate docs by doc.name + const docs = []; + for (doc of Array.from(all_docs)) { if (!(docs.find(d => d.name === doc.name))) { docs.push(doc); } } + return `\ + -
Copyright and License
-- Copyright 2013–2023 Thibaut Courouble and other contributors
- This software is licensed under the terms of the Mozilla Public License v2.0.
- You may obtain a copy of the source code at github.com/freeCodeCamp/devdocs.
- For more information, see the COPYRIGHT - and LICENSE files. +DevDocs: API Documentation Browser
+DevDocs combines multiple developer documentations in a clean and organized web UI with instant search, offline support, mobile version, dark theme, keyboard shortcuts, and more. +
DevDocs is free and open source. It was created by Thibaut Courouble and is operated by freeCodeCamp. +
To keep up-to-date with the latest news: +
-Plugins and Extensions
- +Copyright and License
++ Copyright 2013–2023 Thibaut Courouble and other contributors
+ This software is licensed under the terms of the Mozilla Public License v2.0.
+ You may obtain a copy of the source code at github.com/freeCodeCamp/devdocs.
+ For more information, see the COPYRIGHT + and LICENSE files. -Questions & Answers
--
+- Where can I suggest new docs and features? -
- You can suggest and vote for new docs on the Trello board.
- If you have a specific feature request, add it to the issue tracker.
- Otherwise, come talk to us in the Discord chat room. -- Where can I report bugs? -
- In the issue tracker. Thanks! -
Plugins and Extensions
+ -Credits
+Questions & Answers
++
-- Where can I suggest new docs and features? +
- You can suggest and vote for new docs on the Trello board.
+ If you have a specific feature request, add it to the issue tracker.
+ Otherwise, come talk to us in the Discord chat room. +- Where can I report bugs? +
- In the issue tracker. Thanks! +
Special thanks to: -
-
+- Sentry and Gauges for offering a free account to DevDocs -
- MaxCDN, Shopify, JetBrains and Code School for sponsoring DevDocs in the past -
- Heroku and New Relic for providing awesome free service -
- Jeremy Kratz for the C/C++ logo -
Credits
--+-
-- Documentation - Copyright/License - Source code - #{( - " - " for doc in docs - ).join('')} -#{doc.name} -#{doc.attribution} -Source code -Special thanks to: +
+
-- Sentry and Gauges for offering a free account to DevDocs +
- MaxCDN, Shopify, JetBrains and Code School for sponsoring DevDocs in the past +
- Heroku and New Relic for providing awesome free service +
- Jeremy Kratz for the C/C++ logo +
Privacy Policy
--
-""" +- devdocs.io ("App") is operated by freeCodeCamp ("We"). -
- We do not collect personal information through the app. -
- We use Google Analytics and Gauges to collect anonymous traffic information if you have given consent to this. You can change your decision in the settings. -
- We use Sentry to collect crash data and improve the app. -
- The app uses cookies to store user preferences. -
- By using the app, you signify your acceptance of this policy. If you do not agree to this policy, please do not use the app. -
- If you have any questions regarding privacy, please email privacy@freecodecamp.org. -
++ ++
++ Documentation + Copyright/License + Source code + ${((() => { + const result = []; + + for (doc of Array.from(docs)) { result.push(` \ + `); + } + + return result; + })()).join('')} +${doc.name} \ +${doc.attribution} \ +Source code \ +Privacy Policy
++
\ +`; +}; diff --git a/assets/javascripts/templates/pages/help_tmpl.js b/assets/javascripts/templates/pages/help_tmpl.js index 53031ff196..753b3424ef 100644 --- a/assets/javascripts/templates/pages/help_tmpl.js +++ b/assets/javascripts/templates/pages/help_tmpl.js @@ -1,169 +1,193 @@ -app.templates.helpPage = -> - ctrlKey = if $.isMac() then 'cmd' else 'ctrl' - navKey = if $.isMac() then 'cmd' else 'alt' - arrowScroll = app.settings.get('arrowScroll') +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +app.templates.helpPage = function() { + let key, value; + const ctrlKey = $.isMac() ? 'cmd' : 'ctrl'; + const navKey = $.isMac() ? 'cmd' : 'alt'; + const arrowScroll = app.settings.get('arrowScroll'); - aliases_one = {} - aliases_two = {} - keys = Object.keys(app.models.Entry.ALIASES) - middle = Math.ceil(keys.length / 2) - 1 - for key, i in keys - (if i > middle then aliases_two else aliases_one)[key] = app.models.Entry.ALIASES[key] + const aliases_one = {}; + const aliases_two = {}; + const keys = Object.keys(app.models.Entry.ALIASES); + const middle = Math.ceil(keys.length / 2) - 1; + for (let i = 0; i < keys.length; i++) { + key = keys[i]; + (i > middle ? aliases_two : aliases_one)[key] = app.models.Entry.ALIASES[key]; + } - """ - + return `\ + -- devdocs.io ("App") is operated by freeCodeCamp ("We"). +
- We do not collect personal information through the app. +
- We use Google Analytics and Gauges to collect anonymous traffic information if you have given consent to this. You can change your decision in the settings. +
- We use Sentry to collect crash data and improve the app. +
- The app uses cookies to store user preferences. +
- By using the app, you signify your acceptance of this policy. If you do not agree to this policy, please do not use the app. +
- If you have any questions regarding privacy, please email privacy@freecodecamp.org. +
User Guide
+User Guide
-Managing Documentations
-- Documentations can be enabled and disabled in the Preferences. - Alternatively, you can enable a documentation by searching for it in the main search - and clicking the "Enable" link in the results. - For faster and better search, only enable the documentations you plan on actively using. -
- Once a documentation is enabled, it becomes part of the search and its content can be downloaded for offline access — and faster page loads when online — in the Offline area. +
Managing Documentations
++ Documentations can be enabled and disabled in the Preferences. + Alternatively, you can enable a documentation by searching for it in the main search + and clicking the "Enable" link in the results. + For faster and better search, only enable the documentations you plan on actively using. +
+ Once a documentation is enabled, it becomes part of the search and its content can be downloaded for offline access — and faster page loads when online — in the Offline area. -
Search
-- The search is case-insensitive and ignores whitespace. It supports fuzzy matching - (e.g.
bgcpmatchesbackground-clip) - as well as aliases (full list below). --
-- Searching a single documentation -
- - The search can be scoped to a single documentation by typing its name (or an abbreviation) - and pressing
tab(spaceon mobile). - For example, to search the JavaScript documentation, enterjavascript- orjs, thentab.
- To clear the current scope, empty the search field and hitbackspaceor -esc. -- Prefilling the search field -
- - The search can be prefilled from the URL by visiting devdocs.io/#q=keyword. - Characters after
#q=will be used as search query.
- To search a single documentation, add its name (or an abbreviation) and a space before the keyword: - devdocs.io/#q=js date. -- Searching using the address bar -
- - 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
tabwhen devdocs.io is autocompleted - in the omnibox (to set a custom keyword, click Manage search engines\u2026 in Chrome's settings). -- On Firefox, add the search from the address bar: - Click ••• in the address bar, and select Add Search Engine. Then, you can add a keyword for this search engine in the preferences. -
- Note: the above search features only work for documentations that are enabled. +
Search
++ The search is case-insensitive and ignores whitespace. It supports fuzzy matching + (e.g.
bgcpmatchesbackground-clip) + as well as aliases (full list below). ++
+- Searching a single documentation +
- + The search can be scoped to a single documentation by typing its name (or an abbreviation) + and pressing
tab(spaceon mobile). + For example, to search the JavaScript documentation, enterjavascript+ orjs, thentab.
+ To clear the current scope, empty the search field and hitbackspaceor +esc. +- Prefilling the search field +
- + The search can be prefilled from the URL by visiting devdocs.io/#q=keyword. + Characters after
#q=will be used as search query.
+ To search a single documentation, add its name (or an abbreviation) and a space before the keyword: + devdocs.io/#q=js date. +- Searching using the address bar +
- + 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
tabwhen devdocs.io is autocompleted + in the omnibox (to set a custom keyword, click Manage search engines\u2026 in Chrome's settings). +- On Firefox, add the search from the address bar: + Click ••• in the address bar, and select Add Search Engine. Then, you can add a keyword for this search engine in the preferences. +
+ Note: the above search features only work for documentations that are enabled. -
Keyboard Shortcuts
-Sidebar
--
-- - #{if arrowScroll then '
shift+ ' else ''} -↓-↑-- Move selection -
- - #{if arrowScroll then '
shift+ ' else ''} -→-←-- Show/hide sub-list -
- -
enter-- Open selection -
- -
#{ctrlKey} + enter-- Open selection in a new tab -
- -
alt + r-- Reveal current page in sidebar -
Browsing
--
+- -
#{navKey} + ←-#{navKey} + →-- Go back/forward -
- - #{if arrowScroll +
Keyboard Shortcuts
+Sidebar
++
+- + ${arrowScroll ? '
shift+ ' : ''} +↓+↑+- Move selection +
- + ${arrowScroll ? '
shift+ ' : ''} +→+←+- Show/hide sub-list +
- +
enter+- Open selection +
- +
${ctrlKey} + enter+- Open selection in a new tab +
- +
alt + r+- Reveal current page in sidebar +
Browsing
++
-- +
${navKey} + ←+${navKey} + →+- Go back/forward +
- + ${arrowScroll ? '
↓' + '↑' - else + : 'alt + ↓' + 'alt + ↑' + '
' + 'shift + ↓' + 'shift + ↑'} -- Scroll step by step
-- -
space-shift + space-- Scroll screen by screen -
- -
#{ctrlKey} + ↑-#{ctrlKey} + ↓-- Scroll to the top/bottom -
- -
alt + f-- Focus first link in the content area
(press tab to focus the other links) -App
--
-- -
ctrl + ,-- Open preferences -
- -
esc-- Clear search field / reset UI -
- -
?-- Show this page -
Miscellaneous
--
-- -
alt + c-- Copy URL of original page -
- -
alt + o-- Open original page -
- -
alt + g-- Search on Google -
- -
alt + s-- Search on Stack Overflow -
- -
alt + d-- Search on DuckDuckGo -
- 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. +- Scroll step by step
+- +
space+shift + space+- Scroll screen by screen +
- +
${ctrlKey} + ↑+${ctrlKey} + ↓+- Scroll to the top/bottom +
- +
alt + f+- Focus first link in the content area
(press tab to focus the other links) +App
++
+- +
ctrl + ,+- Open preferences +
- +
esc+- Clear search field / reset UI +
- +
?+- Show this page +
Miscellaneous
++
+- +
alt + c+- Copy URL of original page +
- +
alt + o+- Open original page +
- +
alt + g+- Search on Google +
- +
alt + s+- Search on Stack Overflow +
- +
alt + d+- Search on DuckDuckGo +
+ 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. -Search Aliases
----
-- Word - Alias - #{(" #{key} #{value}" for key, value of aliases_one).join('')} - -
-- Word - Alias - #{(" #{key} #{value}" for key, value of aliases_two).join('')} - Feel free to suggest new aliases on GitHub. -""" +
Search Aliases
++++
++ Word + Alias + ${((() => { + const result = []; + for (key in aliases_one) { + value = aliases_one[key]; + result.push(` ${key} ${value}`); + } + return result; + })()).join('')} + +
++ Word + Alias + ${((() => { + const result1 = []; + for (key in aliases_two) { + value = aliases_two[key]; + result1.push(` ${key} ${value}`); + } + return result1; + })()).join('')} + Feel free to suggest new aliases on GitHub.\ +`; +}; diff --git a/assets/javascripts/templates/pages/offline_tmpl.js b/assets/javascripts/templates/pages/offline_tmpl.js index bb9e06e803..abed046e57 100644 --- a/assets/javascripts/templates/pages/offline_tmpl.js +++ b/assets/javascripts/templates/pages/offline_tmpl.js @@ -1,80 +1,89 @@ -app.templates.offlinePage = (docs) -> """ -
Offline Documentation
+/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +app.templates.offlinePage = docs => `\ +Offline Documentation
-- -- -++ +-++---
-- - #{docs} -Documentation -Size -Status -Action -Note: your browser may delete DevDocs's offline data if your computer is running low on disk space and you haven't used the app in a while. Load this page before going offline to make sure the data is still there. -
Questions & Answers
--
-""" +- How does this work? -
- Each page is cached as a key-value pair in IndexedDB (downloaded from a single file).
- The app also uses Service Workers and localStorage to cache the assets and index files. -- Can I close the tab/browser? -
- #{canICloseTheTab()} -
- What if I don't update a documentation? -
- You'll see outdated content and some pages will be missing or broken, because the rest of the app (including data for the search and sidebar) uses a different caching mechanism that's updated automatically. -
- I found a bug, where do I report it? -
- In the issue tracker. Thanks! -
- How do I uninstall/reset the app? -
- Click here. -
- Why aren't all documentations listed above? -
- You have to enable them first. -
+++
++ + ${docs} +Documentation +Size +Status +Action +Note: your browser may delete DevDocs's offline data if your computer is running low on disk space and you haven't used the app in a while. Load this page before going offline to make sure the data is still there. +
Questions & Answers
++
\ +`; -canICloseTheTab = -> - if app.ServiceWorker.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 - reason = "aren't available in your browser (or are disabled)" +var canICloseTheTab = function() { + if (app.ServiceWorker.isEnabled()) { + return " 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 { + let reason = "aren't available in your browser (or are disabled)"; - if app.config.env != 'production' - reason = "are disabled in your development instance of DevDocs (enable them by setting the- How does this work? +
- Each page is cached as a key-value pair in IndexedDB (downloaded from a single file).
+ The app also uses Service Workers and localStorage to cache the assets and index files. +- Can I close the tab/browser? +
- ${canICloseTheTab()} +
- What if I don't update a documentation? +
- You'll see outdated content and some pages will be missing or broken, because the rest of the app (including data for the search and sidebar) uses a different caching mechanism that's updated automatically. +
- I found a bug, where do I report it? +
- In the issue tracker. Thanks! +
- How do I uninstall/reset the app? +
- Click here. +
- Why aren't all documentations listed above? +
- You have to enable them first. +
ENABLE_SERVICE_WORKERenvironment variable totrue)" + if (app.config.env !== 'production') { + reason = "are disabled in your development instance of DevDocs (enable them by setting theENABLE_SERVICE_WORKERenvironment variable totrue)"; + } - """ No. Service Workers #{reason}, so loading devdocs.io offline won't work.
- The current tab will continue to function even when you go offline (provided you installed all the documentations beforehand). """ + return ` No. Service Workers ${reason}, so loading devdocs.io offline won't work.
+The current tab will continue to function even when you go offline (provided you installed all the documentations beforehand). `; + } +}; -app.templates.offlineDoc = (doc, status) -> - outdated = doc.isOutdated(status) +app.templates.offlineDoc = function(doc, status) { + const outdated = doc.isOutdated(status); - html = """ -- #{doc.fullName} -#{Math.ceil(doc.db_size / 100000) / 10} MB - """ + let html = `\ ++ ' + return html + ''; +}; diff --git a/assets/javascripts/templates/pages/settings_tmpl.js b/assets/javascripts/templates/pages/settings_tmpl.js index 048afa1a8e..26c1eb9742 100644 --- a/assets/javascripts/templates/pages/settings_tmpl.js +++ b/assets/javascripts/templates/pages/settings_tmpl.js @@ -1,81 +1,86 @@ -themeOption = ({ label, value }, settings) -> """ - -""" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const themeOption = ({ label, value }, settings) => `\ +\ +`; -app.templates.settingsPage = (settings) -> """ -${doc.fullName} +${Math.ceil(doc.db_size / 100000) / 10} MB \ +`; - html += if !(status and status.installed) - """ -- -- """ - else if outdated - """ - Outdated -- - """ - else - """ -Up‑to‑date -- """ + html += !(status && status.installed) ? + `\ + - +\ +` + : outdated ? + `\ + Outdated +- \ +` + : + `\ +Up‑to‑date +\ +`; - html + ' Preferences
+app.templates.settingsPage = settings => `\ +Preferences
--Theme:
-- #{if settings.autoSupported - themeOption label: "Automatic Matches system setting", value: "auto", settings - else +-++Theme:
++ ${settings.autoSupported ? + themeOption({label: "Automatic Matches system setting", value: "auto"}, settings) + : ""} - #{themeOption label: "Light", value: "default", settings} - #{themeOption label: "Dark", value: "dark", settings} -+ ${themeOption({label: "Light", value: "default"}, settings)} + ${themeOption({label: "Dark", value: "dark"}, settings)}-General:
++-General:
-- - - - - - -++ + + + + ++-Scrolling:
++-Scrolling:
-- - - - - -++ + + + ++- - +
+ + -
- -""" +
+ \ +`; diff --git a/assets/javascripts/templates/pages/type_tmpl.js b/assets/javascripts/templates/pages/type_tmpl.js index c419a6a8c4..c130884286 100644 --- a/assets/javascripts/templates/pages/type_tmpl.js +++ b/assets/javascripts/templates/pages/type_tmpl.js @@ -1,6 +1,9 @@ -app.templates.typePage = (type) -> - """
#{type.doc.fullName} / #{type.name}
-#{app.templates.render 'typePageEntry', type.entries()}
""" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +app.templates.typePage = type => `${type.doc.fullName} / ${type.name}
+${app.templates.render('typePageEntry', type.entries())}
`; -app.templates.typePageEntry = (entry) -> - """- #{$.escape entry.name}
""" +app.templates.typePageEntry = entry => `- ${$.escape(entry.name)}
`; diff --git a/assets/javascripts/templates/path_tmpl.js b/assets/javascripts/templates/path_tmpl.js index f28925c9ca..d0344542d6 100644 --- a/assets/javascripts/templates/path_tmpl.js +++ b/assets/javascripts/templates/path_tmpl.js @@ -1,7 +1,13 @@ -arrow = """""" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const arrow = ""; -app.templates.path = (doc, type, entry) -> - html = """#{doc.fullName}""" - html += """#{arrow}#{type.name}""" if type - html += """#{arrow}#{$.escape entry.name}""" if entry - html +app.templates.path = function(doc, type, entry) { + let html = `${doc.fullName}`; + if (type) { html += `${arrow}${type.name}`; } + if (entry) { html += `${arrow}${$.escape(entry.name)}`; } + return html; +}; diff --git a/assets/javascripts/templates/sidebar_tmpl.js b/assets/javascripts/templates/sidebar_tmpl.js index 46797e5658..2f9c7ce3b1 100644 --- a/assets/javascripts/templates/sidebar_tmpl.js +++ b/assets/javascripts/templates/sidebar_tmpl.js @@ -1,68 +1,79 @@ -templates = app.templates +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const { + templates +} = app; -arrow = """""" +const arrow = ""; -templates.sidebarDoc = (doc, options = {}) -> - link = """""" - if options.disabled - link += """Enable""" - else - link += arrow - link += """#{doc.release}""" if doc.release - link += """#{doc.name}""" - link += " #{doc.version}" if options.fullName or options.disabled and doc.version - link + "" +templates.sidebarDoc = function(doc, options) { + if (options == null) { options = {}; } + let link = ``; + if (options.disabled) { + link += `Enable`; + } else { + link += arrow; + } + if (doc.release) { link += `${doc.release}`; } + link += `${doc.name}`; + if (options.fullName || (options.disabled && doc.version)) { link += ` ${doc.version}`; } + return link + ""; +}; -templates.sidebarType = (type) -> - """#{arrow}#{type.count}#{$.escape type.name}""" +templates.sidebarType = type => `${arrow}${type.count}${$.escape(type.name)}`; -templates.sidebarEntry = (entry) -> - """#{$.escape entry.name}""" +templates.sidebarEntry = entry => `${$.escape(entry.name)}`; -templates.sidebarResult = (entry) -> - addons = if entry.isIndex() and app.disabledDocs.contains(entry.doc) - """Enable""" - else - """""" - addons += """#{entry.doc.short_version}""" if entry.doc.version and not entry.isIndex() - """#{addons}#{$.escape entry.name}""" +templates.sidebarResult = function(entry) { + let addons = entry.isIndex() && app.disabledDocs.contains(entry.doc) ? + `Enable` + : + ""; + if (entry.doc.version && !entry.isIndex()) { addons += `${entry.doc.short_version}`; } + return `${addons}${$.escape(entry.name)}`; +}; -templates.sidebarNoResults = -> - html = """No results.""" - html += """ -Note: documentations must be enabled to appear in the search.- """ unless app.isSingleDoc() or app.disabledDocs.isEmpty() - html +templates.sidebarNoResults = function() { + let html = "No results."; + if (!app.isSingleDoc() && !app.disabledDocs.isEmpty()) { html += `\ +Note: documentations must be enabled to appear in the search.\ +`; } + return html; +}; -templates.sidebarPageLink = (count) -> - """Show more\u2026 (#{count})""" +templates.sidebarPageLink = count => `Show more\u2026 (${count})`; -templates.sidebarLabel = (doc, options = {}) -> - label = """""" +templates.sidebarLabel = function(doc, options) { + if (options == null) { options = {}; } + let label = "`; +}; -templates.sidebarVersionedDoc = (doc, versions, options = {}) -> - html = """#{arrow}#{doc.name}#{versions}""" +templates.sidebarVersionedDoc = function(doc, versions, options) { + if (options == null) { options = {}; } + let html = `${versions}`; +}; -templates.sidebarDisabled = (options) -> - """#{arrow}Disabled (#{options.count}) Customize
""" +templates.sidebarDisabled = options => `${arrow}Disabled (${options.count}) Customize
`; -templates.sidebarDisabledList = (html) -> - """#{html}""" +templates.sidebarDisabledList = html => `${html}`; -templates.sidebarDisabledVersionedDoc = (doc, versions) -> - """#{arrow}#{doc.name}#{versions}""" +templates.sidebarDisabledVersionedDoc = (doc, versions) => `${arrow}${doc.name}${versions}`; -templates.docPickerHeader = """Documentation Enable""" +templates.docPickerHeader = "Documentation Enable"; -templates.docPickerNote = """ -Tip: for faster and better search results, select only the docs you need.- Vote for new documentation - """ +templates.docPickerNote = `\ +Tip: for faster and better search results, select only the docs you need.+Vote for new documentation\ +`; diff --git a/assets/javascripts/templates/tip_tmpl.js b/assets/javascripts/templates/tip_tmpl.js index 55979fa452..665ac327a8 100644 --- a/assets/javascripts/templates/tip_tmpl.js +++ b/assets/javascripts/templates/tip_tmpl.js @@ -1,10 +1,15 @@ -app.templates.tipKeyNav = () -> """ -- ProTip - (click to dismiss) -
- Hit #{if app.settings.get('arrowScroll') then '
shift+' else ''}↓↑←→to navigate the sidebar.
- Hitspace / shift space#{if app.settings.get('arrowScroll') then ' or↓/↑' else ',alt ↓/↑orshift ↓/↑'} to scroll the page. -- See all keyboard shortcuts -""" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +app.templates.tipKeyNav = () => `\ +
+ ProTip + (click to dismiss) +
+ Hit ${app.settings.get('arrowScroll') ? '
shift+' : ''}↓↑←→to navigate the sidebar.
+ Hitspace / shift space${app.settings.get('arrowScroll') ? ' or↓/↑' : ',alt ↓/↑orshift ↓/↑'} to scroll the page. ++ See all keyboard shortcuts\ +`; diff --git a/assets/javascripts/views/content/content.js b/assets/javascripts/views/content/content.js index 4e01733ebd..7f50299576 100644 --- a/assets/javascripts/views/content/content.js +++ b/assets/javascripts/views/content/content.js @@ -1,195 +1,259 @@ -class app.views.Content extends app.View - @el: '._content' - @loadingClass: '_content-loading' - - @events: - click: 'onClick' - - @shortcuts: - altUp: 'scrollStepUp' - altDown: 'scrollStepDown' - pageUp: 'scrollPageUp' - pageDown: 'scrollPageDown' - pageTop: 'scrollToTop' - pageBottom: 'scrollToBottom' - altF: 'onAltF' - - @routes: - before: 'beforeRoute' - after: 'afterRoute' - - init: -> - @scrollEl = if app.isMobile() +/* + * decaffeinate suggestions: + * DS002: Fix invalid constructor + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining + * DS104: Avoid inline assignments + * DS204: Change includes calls to have a more natural evaluation order + * DS205: Consider reworking code to avoid use of IIFEs + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const Cls = (app.views.Content = class Content extends app.View { + constructor(...args) { + this.scrollToTop = this.scrollToTop.bind(this); + this.scrollToBottom = this.scrollToBottom.bind(this); + this.scrollStepUp = this.scrollStepUp.bind(this); + this.scrollStepDown = this.scrollStepDown.bind(this); + this.scrollPageUp = this.scrollPageUp.bind(this); + this.scrollPageDown = this.scrollPageDown.bind(this); + this.onReady = this.onReady.bind(this); + this.onBootError = this.onBootError.bind(this); + this.onEntryLoading = this.onEntryLoading.bind(this); + this.onEntryLoaded = this.onEntryLoaded.bind(this); + this.beforeRoute = this.beforeRoute.bind(this); + this.afterRoute = this.afterRoute.bind(this); + this.onClick = this.onClick.bind(this); + this.onAltF = this.onAltF.bind(this); + super(...args); + } + + static initClass() { + this.el = '._content'; + this.loadingClass = '_content-loading'; + + this.events = + {click: 'onClick'}; + + this.shortcuts = { + altUp: 'scrollStepUp', + altDown: 'scrollStepDown', + pageUp: 'scrollPageUp', + pageDown: 'scrollPageDown', + pageTop: 'scrollToTop', + pageBottom: 'scrollToBottom', + altF: 'onAltF' + }; + + this.routes = { + before: 'beforeRoute', + after: 'afterRoute' + }; + } + + init() { + this.scrollEl = app.isMobile() ? (document.scrollingElement || document.body) - else - @el - @scrollMap = {} - @scrollStack = [] - - @rootPage = new app.views.RootPage - @staticPage = new app.views.StaticPage - @settingsPage = new app.views.SettingsPage - @offlinePage = new app.views.OfflinePage - @typePage = new app.views.TypePage - @entryPage = new app.views.EntryPage - - @entryPage - .on 'loading', @onEntryLoading - .on 'loaded', @onEntryLoaded + : + this.el; + this.scrollMap = {}; + this.scrollStack = []; + + this.rootPage = new app.views.RootPage; + this.staticPage = new app.views.StaticPage; + this.settingsPage = new app.views.SettingsPage; + this.offlinePage = new app.views.OfflinePage; + this.typePage = new app.views.TypePage; + this.entryPage = new app.views.EntryPage; + + this.entryPage + .on('loading', this.onEntryLoading) + .on('loaded', this.onEntryLoaded); app - .on 'ready', @onReady - .on 'bootError', @onBootError - - return - - show: (view) -> - @hideLoading() - unless view is @view - @view?.deactivate() - @html @view = view - @view.activate() - return - - showLoading: -> - @addClass @constructor.loadingClass - return - - isLoading: -> - @el.classList.contains @constructor.loadingClass - - hideLoading: -> - @removeClass @constructor.loadingClass - return - - scrollTo: (value) -> - @scrollEl.scrollTop = value or 0 - return - - smoothScrollTo: (value) -> - if app.settings.get('fastScroll') - @scrollTo value - else - $.smoothScroll @scrollEl, value or 0 - return - - scrollBy: (n) -> - @smoothScrollTo @scrollEl.scrollTop + n - return - - scrollToTop: => - @smoothScrollTo 0 - return - - scrollToBottom: => - @smoothScrollTo @scrollEl.scrollHeight - return - - scrollStepUp: => - @scrollBy -80 - return - - scrollStepDown: => - @scrollBy 80 - return - - scrollPageUp: => - @scrollBy 40 - @scrollEl.clientHeight - return - - scrollPageDown: => - @scrollBy @scrollEl.clientHeight - 40 - return - - scrollToTarget: -> - if @routeCtx.hash and el = @findTargetByHash @routeCtx.hash - $.scrollToWithImageLock el, @scrollEl, 'top', - margin: if @scrollEl is @el then 0 else $.offset(@el).top - $.highlight el, className: '_highlight' - else - @scrollTo @scrollMap[@routeCtx.state.id] - return - - onReady: => - @hideLoading() - return - - onBootError: => - @hideLoading() - @html @tmpl('bootError') - return - - onEntryLoading: => - @showLoading() - if @scrollToTargetTimeout - clearTimeout @scrollToTargetTimeout - @scrollToTargetTimeout = null - return - - onEntryLoaded: => - @hideLoading() - if @scrollToTargetTimeout - clearTimeout @scrollToTargetTimeout - @scrollToTargetTimeout = null - @scrollToTarget() - return - - beforeRoute: (context) => - @cacheScrollPosition() - @routeCtx = context - @scrollToTargetTimeout = @delay @scrollToTarget - return - - cacheScrollPosition: -> - return if not @routeCtx or @routeCtx.hash - return if @routeCtx.path is '/' - - unless @scrollMap[@routeCtx.state.id]? - @scrollStack.push @routeCtx.state.id - while @scrollStack.length > app.config.history_cache_size - delete @scrollMap[@scrollStack.shift()] - - @scrollMap[@routeCtx.state.id] = @scrollEl.scrollTop - return - - afterRoute: (route, context) => - if route != 'entry' and route != 'type' - resetFavicon() - - switch route - when 'root' - @show @rootPage - when 'entry' - @show @entryPage - when 'type' - @show @typePage - when 'settings' - @show @settingsPage - when 'offline' - @show @offlinePage - else - @show @staticPage - - @view.onRoute(context) - app.document.setTitle @view.getTitle?() - return - - onClick: (event) => - link = $.closestLink $.eventTarget(event), @el - if link and @isExternalUrl link.getAttribute('href') - $.stopEvent(event) - $.popup(link) - return - - onAltF: (event) => - unless document.activeElement and $.hasChild @el, document.activeElement - @find('a:not(:empty)')?.focus() - $.stopEvent(event) - - findTargetByHash: (hash) -> - el = try $.id decodeURIComponent(hash) catch - el or= try $.id(hash) catch - el - - isExternalUrl: (url) -> - url?[0..5] in ['http:/', 'https:'] + .on('ready', this.onReady) + .on('bootError', this.onBootError); + + } + + show(view) { + this.hideLoading(); + if (view !== this.view) { + if (this.view != null) { + this.view.deactivate(); + } + this.html(this.view = view); + this.view.activate(); + } + } + + showLoading() { + this.addClass(this.constructor.loadingClass); + } + + isLoading() { + return this.el.classList.contains(this.constructor.loadingClass); + } + + hideLoading() { + this.removeClass(this.constructor.loadingClass); + } + + scrollTo(value) { + this.scrollEl.scrollTop = value || 0; + } + + smoothScrollTo(value) { + if (app.settings.get('fastScroll')) { + this.scrollTo(value); + } else { + $.smoothScroll(this.scrollEl, value || 0); + } + } + + scrollBy(n) { + this.smoothScrollTo(this.scrollEl.scrollTop + n); + } + + scrollToTop() { + this.smoothScrollTo(0); + } + + scrollToBottom() { + this.smoothScrollTo(this.scrollEl.scrollHeight); + } + + scrollStepUp() { + this.scrollBy(-80); + } + + scrollStepDown() { + this.scrollBy(80); + } + + scrollPageUp() { + this.scrollBy(40 - this.scrollEl.clientHeight); + } + + scrollPageDown() { + this.scrollBy(this.scrollEl.clientHeight - 40); + } + + scrollToTarget() { + let el; + if (this.routeCtx.hash && (el = this.findTargetByHash(this.routeCtx.hash))) { + $.scrollToWithImageLock(el, this.scrollEl, 'top', + {margin: this.scrollEl === this.el ? 0 : $.offset(this.el).top}); + $.highlight(el, {className: '_highlight'}); + } else { + this.scrollTo(this.scrollMap[this.routeCtx.state.id]); + } + } + + onReady() { + this.hideLoading(); + } + + onBootError() { + this.hideLoading(); + this.html(this.tmpl('bootError')); + } + + onEntryLoading() { + this.showLoading(); + if (this.scrollToTargetTimeout) { + clearTimeout(this.scrollToTargetTimeout); + this.scrollToTargetTimeout = null; + } + } + + onEntryLoaded() { + this.hideLoading(); + if (this.scrollToTargetTimeout) { + clearTimeout(this.scrollToTargetTimeout); + this.scrollToTargetTimeout = null; + } + this.scrollToTarget(); + } + + beforeRoute(context) { + this.cacheScrollPosition(); + this.routeCtx = context; + this.scrollToTargetTimeout = this.delay(this.scrollToTarget); + } + + cacheScrollPosition() { + if (!this.routeCtx || this.routeCtx.hash) { return; } + if (this.routeCtx.path === '/') { return; } + + if (this.scrollMap[this.routeCtx.state.id] == null) { + this.scrollStack.push(this.routeCtx.state.id); + while (this.scrollStack.length > app.config.history_cache_size) { + delete this.scrollMap[this.scrollStack.shift()]; + } + } + + this.scrollMap[this.routeCtx.state.id] = this.scrollEl.scrollTop; + } + + afterRoute(route, context) { + if ((route !== 'entry') && (route !== 'type')) { + resetFavicon(); + } + + switch (route) { + case 'root': + this.show(this.rootPage); + break; + case 'entry': + this.show(this.entryPage); + break; + case 'type': + this.show(this.typePage); + break; + case 'settings': + this.show(this.settingsPage); + break; + case 'offline': + this.show(this.offlinePage); + break; + default: + this.show(this.staticPage); + } + + this.view.onRoute(context); + app.document.setTitle(typeof this.view.getTitle === 'function' ? this.view.getTitle() : undefined); + } + + onClick(event) { + const link = $.closestLink($.eventTarget(event), this.el); + if (link && this.isExternalUrl(link.getAttribute('href'))) { + $.stopEvent(event); + $.popup(link); + } + } + + onAltF(event) { + if (!document.activeElement || !$.hasChild(this.el, document.activeElement)) { + __guard__(this.find('a:not(:empty)'), x => x.focus()); + return $.stopEvent(event); + } + } + + findTargetByHash(hash) { + let el = (() => { try { return $.id(decodeURIComponent(hash)); } catch (error) {} })(); + if (!el) { el = (() => { try { return $.id(hash); } catch (error1) {} })(); } + return el; + } + + isExternalUrl(url) { + let needle; + return (needle = __guard__(url, x => x.slice(0, 6)), ['http:/', 'https:'].includes(needle)); + } +}); +Cls.initClass(); + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/assets/javascripts/views/content/entry_page.js b/assets/javascripts/views/content/entry_page.js index dff008877e..bf9852d2bd 100644 --- a/assets/javascripts/views/content/entry_page.js +++ b/assets/javascripts/views/content/entry_page.js @@ -1,166 +1,225 @@ -class app.views.EntryPage extends app.View - @className: '_page' - @errorClass: '_page-error' - - @events: - click: 'onClick' - - @shortcuts: - altC: 'onAltC' - altO: 'onAltO' - - @routes: - before: 'beforeRoute' - - init: -> - @cacheMap = {} - @cacheStack = [] - return - - deactivate: -> - if super - @empty() - @entry = null - return - - loading: -> - @empty() - @trigger 'loading' - return - - render: (content = '', fromCache = false) -> - return unless @activated - @empty() - @subview = new (@subViewClass()) @el, @entry - - $.batchUpdate @el, => - @subview.render(content, fromCache) - @addCopyButtons() unless fromCache - return - - if app.disabledDocs.findBy 'slug', @entry.doc.slug - @hiddenView = new app.views.HiddenPage @el, @entry - - setFaviconForDoc(@entry.doc) - @delay @polyfillMathML - @trigger 'loaded' - return - - addCopyButtons: -> - unless @copyButton - @copyButton = document.createElement('button') - @copyButton.innerHTML = '' - @copyButton.type = 'button' - @copyButton.className = '_pre-clip' - @copyButton.title = 'Copy to clipboard' - @copyButton.setAttribute 'aria-label', 'Copy to clipboard' - el.appendChild @copyButton.cloneNode(true) for el in @findAllByTag('pre') - return - - polyfillMathML: -> - return unless window.supportsMathML is false and !@polyfilledMathML and @findByTag('math') - @polyfilledMathML = true - $.append document.head, """""" - return - - LINKS = - home: 'Homepage' - code: 'Source code' - - prepareContent: (content) -> - return content unless @entry.isIndex() and @entry.doc.links - - links = for link, url of @entry.doc.links - """#{LINKS[link]}""" - - """
#{links.join('')}
#{content}""" - - empty: -> - @subview?.deactivate() - @subview = null - - @hiddenView?.deactivate() - @hiddenView = null - - @resetClass() - super - return - - subViewClass: -> - app.views["#{$.classify(@entry.doc.type)}Page"] or app.views.BasePage - - getTitle: -> - @entry.doc.fullName + if @entry.isIndex() then ' documentation' else " / #{@entry.name}" - - beforeRoute: => - @cache() - @abort() - return - - onRoute: (context) -> - isSameFile = context.entry.filePath() is @entry?.filePath() - @entry = context.entry - @restore() or @load() unless isSameFile - return - - load: -> - @loading() - @xhr = @entry.loadFile @onSuccess, @onError - return - - abort: -> - if @xhr - @xhr.abort() - @xhr = @entry = null - return - - onSuccess: (response) => - return unless @activated - @xhr = null - @render @prepareContent(response) - return - - onError: => - @xhr = null - @render @tmpl('pageLoadError') - @resetClass() - @addClass @constructor.errorClass - app.serviceWorker?.update() - return - - cache: -> - return if @xhr or not @entry or @cacheMap[path = @entry.filePath()] - - @cacheMap[path] = @el.innerHTML - @cacheStack.push(path) - - while @cacheStack.length > app.config.history_cache_size - delete @cacheMap[@cacheStack.shift()] - return - - restore: -> - if @cacheMap[path = @entry.filePath()] - @render @cacheMap[path], true - true - - onClick: (event) => - target = $.eventTarget(event) - if target.hasAttribute 'data-retry' - $.stopEvent(event) - @load() - else if target.classList.contains '_pre-clip' - $.stopEvent(event) - target.classList.add if $.copyToClipboard(target.parentNode.textContent) then '_pre-clip-success' else '_pre-clip-error' - setTimeout (-> target.className = '_pre-clip'), 2000 - return - - onAltC: => - return unless link = @find('._attribution:last-child ._attribution-link') - console.log(link.href + location.hash) - navigator.clipboard.writeText(link.href + location.hash) - return - - onAltO: => - return unless link = @find('._attribution:last-child ._attribution-link') - @delay -> $.popup(link.href + location.hash) - return +/* + * decaffeinate suggestions: + * DS002: Fix invalid constructor + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +(function() { + let LINKS = undefined; + const Cls = (app.views.EntryPage = class EntryPage extends app.View { + constructor(...args) { + this.beforeRoute = this.beforeRoute.bind(this); + this.onSuccess = this.onSuccess.bind(this); + this.onError = this.onError.bind(this); + this.onClick = this.onClick.bind(this); + this.onAltC = this.onAltC.bind(this); + this.onAltO = this.onAltO.bind(this); + super(...args); + } + + static initClass() { + this.className = '_page'; + this.errorClass = '_page-error'; + + this.events = + {click: 'onClick'}; + + this.shortcuts = { + altC: 'onAltC', + altO: 'onAltO' + }; + + this.routes = + {before: 'beforeRoute'}; + + LINKS = { + home: 'Homepage', + code: 'Source code' + }; + } + + init() { + this.cacheMap = {}; + this.cacheStack = []; + } + + deactivate() { + if (super.deactivate(...arguments)) { + this.empty(); + this.entry = null; + } + } + + loading() { + this.empty(); + this.trigger('loading'); + } + + render(content, fromCache) { + if (content == null) { content = ''; } + if (fromCache == null) { fromCache = false; } + if (!this.activated) { return; } + this.empty(); + this.subview = new (this.subViewClass())(this.el, this.entry); + + $.batchUpdate(this.el, () => { + this.subview.render(content, fromCache); + if (!fromCache) { this.addCopyButtons(); } + }); + + if (app.disabledDocs.findBy('slug', this.entry.doc.slug)) { + this.hiddenView = new app.views.HiddenPage(this.el, this.entry); + } + + setFaviconForDoc(this.entry.doc); + this.delay(this.polyfillMathML); + this.trigger('loaded'); + } + + addCopyButtons() { + if (!this.copyButton) { + this.copyButton = document.createElement('button'); + this.copyButton.innerHTML = ''; + this.copyButton.type = 'button'; + this.copyButton.className = '_pre-clip'; + this.copyButton.title = 'Copy to clipboard'; + this.copyButton.setAttribute('aria-label', 'Copy to clipboard'); + } + for (var el of Array.from(this.findAllByTag('pre'))) { el.appendChild(this.copyButton.cloneNode(true)); } + } + + polyfillMathML() { + if ((window.supportsMathML !== false) || !!this.polyfilledMathML || !this.findByTag('math')) { return; } + this.polyfilledMathML = true; + $.append(document.head, ``); + } + + prepareContent(content) { + if (!this.entry.isIndex() || !this.entry.doc.links) { return content; } + + const links = (() => { + const result = []; + for (var link in this.entry.doc.links) { + var url = this.entry.doc.links[link]; + result.push(`${LINKS[link]}`); + } + return result; + })(); + + return `${links.join('')}
${content}`; + } + + empty() { + if (this.subview != null) { + this.subview.deactivate(); + } + this.subview = null; + + if (this.hiddenView != null) { + this.hiddenView.deactivate(); + } + this.hiddenView = null; + + this.resetClass(); + super.empty(...arguments); + } + + subViewClass() { + return app.views[`${$.classify(this.entry.doc.type)}Page`] || app.views.BasePage; + } + + getTitle() { + return this.entry.doc.fullName + (this.entry.isIndex() ? ' documentation' : ` / ${this.entry.name}`); + } + + beforeRoute() { + this.cache(); + this.abort(); + } + + onRoute(context) { + const isSameFile = context.entry.filePath() === (this.entry != null ? this.entry.filePath() : undefined); + this.entry = context.entry; + if (!isSameFile) { this.restore() || this.load(); } + } + + load() { + this.loading(); + this.xhr = this.entry.loadFile(this.onSuccess, this.onError); + } + + abort() { + if (this.xhr) { + this.xhr.abort(); + this.xhr = (this.entry = null); + } + } + + onSuccess(response) { + if (!this.activated) { return; } + this.xhr = null; + this.render(this.prepareContent(response)); + } + + onError() { + this.xhr = null; + this.render(this.tmpl('pageLoadError')); + this.resetClass(); + this.addClass(this.constructor.errorClass); + if (app.serviceWorker != null) { + app.serviceWorker.update(); + } + } + + cache() { + let path; + if (this.xhr || !this.entry || this.cacheMap[(path = this.entry.filePath())]) { return; } + + this.cacheMap[path] = this.el.innerHTML; + this.cacheStack.push(path); + + while (this.cacheStack.length > app.config.history_cache_size) { + delete this.cacheMap[this.cacheStack.shift()]; + } + } + + restore() { + let path; + if (this.cacheMap[(path = this.entry.filePath())]) { + this.render(this.cacheMap[path], true); + return true; + } + } + + onClick(event) { + const target = $.eventTarget(event); + if (target.hasAttribute('data-retry')) { + $.stopEvent(event); + this.load(); + } else if (target.classList.contains('_pre-clip')) { + $.stopEvent(event); + target.classList.add($.copyToClipboard(target.parentNode.textContent) ? '_pre-clip-success' : '_pre-clip-error'); + setTimeout((() => target.className = '_pre-clip'), 2000); + } + } + + onAltC() { + let link; + if (!(link = this.find('._attribution:last-child ._attribution-link'))) { return; } + console.log(link.href + location.hash); + navigator.clipboard.writeText(link.href + location.hash); + } + + onAltO() { + let link; + if (!(link = this.find('._attribution:last-child ._attribution-link'))) { return; } + this.delay(() => $.popup(link.href + location.hash)); + } + }); + Cls.initClass(); + return Cls; +})(); diff --git a/assets/javascripts/views/content/offline_page.js b/assets/javascripts/views/content/offline_page.js index 2f0d615f47..4cac6f2089 100644 --- a/assets/javascripts/views/content/offline_page.js +++ b/assets/javascripts/views/content/offline_page.js @@ -1,92 +1,128 @@ -class app.views.OfflinePage extends app.View - @className: '_static' +/* + * decaffeinate suggestions: + * DS002: Fix invalid constructor + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const Cls = (app.views.OfflinePage = class OfflinePage extends app.View { + constructor(...args) { + this.onClick = this.onClick.bind(this); + super(...args); + } - @events: - click: 'onClick' - change: 'onChange' + static initClass() { + this.className = '_static'; + + this.events = { + click: 'onClick', + change: 'onChange' + }; + } - deactivate: -> - if super - @empty() - return + deactivate() { + if (super.deactivate(...arguments)) { + this.empty(); + } + } - render: -> - if app.cookieBlocked - @html @tmpl('offlineError', 'cookie_blocked') - return + render() { + if (app.cookieBlocked) { + this.html(this.tmpl('offlineError', 'cookie_blocked')); + return; + } - app.docs.getInstallStatuses (statuses) => - return unless @activated - if statuses is false - @html @tmpl('offlineError', app.db.reason, app.db.error) - else - html = '' - html += @renderDoc(doc, statuses[doc.slug]) for doc in app.docs.all() - @html @tmpl('offlinePage', html) - @refreshLinks() - return - return + app.docs.getInstallStatuses(statuses => { + if (!this.activated) { return; } + if (statuses === false) { + this.html(this.tmpl('offlineError', app.db.reason, app.db.error)); + } else { + let html = ''; + for (var doc of Array.from(app.docs.all())) { html += this.renderDoc(doc, statuses[doc.slug]); } + this.html(this.tmpl('offlinePage', html)); + this.refreshLinks(); + } + }); + } - renderDoc: (doc, status) -> - app.templates.render('offlineDoc', doc, status) + renderDoc(doc, status) { + return app.templates.render('offlineDoc', doc, status); + } - getTitle: -> - 'Offline' + getTitle() { + return 'Offline'; + } - refreshLinks: -> - for action in ['install', 'update', 'uninstall'] - @find("[data-action-all='#{action}']").classList[if @find("[data-action='#{action}']") then 'add' else 'remove']('_show') - return + refreshLinks() { + for (var action of ['install', 'update', 'uninstall']) { + this.find(`[data-action-all='${action}']`).classList[this.find(`[data-action='${action}']`) ? 'add' : 'remove']('_show'); + } + } - docByEl: (el) -> - el = el.parentNode until slug = el.getAttribute('data-slug') - app.docs.findBy('slug', slug) + docByEl(el) { + let slug; + while (!(slug = el.getAttribute('data-slug'))) { el = el.parentNode; } + return app.docs.findBy('slug', slug); + } - docEl: (doc) -> - @find("[data-slug='#{doc.slug}']") + docEl(doc) { + return this.find(`[data-slug='${doc.slug}']`); + } - onRoute: (context) -> - @render() - return + onRoute(context) { + this.render(); + } - onClick: (event) => - el = $.eventTarget(event) - if action = el.getAttribute('data-action') - doc = @docByEl(el) - action = 'install' if action is 'update' - doc[action](@onInstallSuccess.bind(@, doc), @onInstallError.bind(@, doc), @onInstallProgress.bind(@, doc)) - el.parentNode.innerHTML = "#{el.textContent.replace(/e$/, '')}ing…" - else if action = el.getAttribute('data-action-all') || el.parentElement.getAttribute('data-action-all') - return unless action isnt 'uninstall' or window.confirm('Uninstall all docs?') - app.db.migrate() - $.click(el) for el in @findAll("[data-action='#{action}']") - return + onClick(event) { + let action; + let el = $.eventTarget(event); + if (action = el.getAttribute('data-action')) { + const doc = this.docByEl(el); + if (action === 'update') { action = 'install'; } + doc[action](this.onInstallSuccess.bind(this, doc), this.onInstallError.bind(this, doc), this.onInstallProgress.bind(this, doc)); + el.parentNode.innerHTML = `${el.textContent.replace(/e$/, '')}ing…`; + } else if (action = el.getAttribute('data-action-all') || el.parentElement.getAttribute('data-action-all')) { + if ((action === 'uninstall') && !window.confirm('Uninstall all docs?')) { return; } + app.db.migrate(); + for (el of Array.from(this.findAll(`[data-action='${action}']`))) { $.click(el); } + } + } - onInstallSuccess: (doc) -> - return unless @activated - doc.getInstallStatus (status) => - return unless @activated - if el = @docEl(doc) - el.outerHTML = @renderDoc(doc, status) - $.highlight el, className: '_highlight' - @refreshLinks() - return - return + onInstallSuccess(doc) { + if (!this.activated) { return; } + doc.getInstallStatus(status => { + let el; + if (!this.activated) { return; } + if (el = this.docEl(doc)) { + el.outerHTML = this.renderDoc(doc, status); + $.highlight(el, {className: '_highlight'}); + this.refreshLinks(); + } + }); + } - onInstallError: (doc) -> - return unless @activated - if el = @docEl(doc) - el.lastElementChild.textContent = 'Error' - return + onInstallError(doc) { + let el; + if (!this.activated) { return; } + if (el = this.docEl(doc)) { + el.lastElementChild.textContent = 'Error'; + } + } - onInstallProgress: (doc, event) -> - return unless @activated and event.lengthComputable - if el = @docEl(doc) - percentage = Math.round event.loaded * 100 / event.total - el.lastElementChild.textContent = el.lastElementChild.textContent.replace(/(\s.+)?$/, " (#{percentage}%)") - return + onInstallProgress(doc, event) { + let el; + if (!this.activated || !event.lengthComputable) { return; } + if (el = this.docEl(doc)) { + const percentage = Math.round((event.loaded * 100) / event.total); + el.lastElementChild.textContent = el.lastElementChild.textContent.replace(/(\s.+)?$/, ` (${percentage}%)`); + } + } - onChange: (event) -> - if event.target.name is 'autoUpdate' - app.settings.set 'manualUpdate', !event.target.checked - return + onChange(event) { + if (event.target.name === 'autoUpdate') { + app.settings.set('manualUpdate', !event.target.checked); + } + } +}); +Cls.initClass(); diff --git a/assets/javascripts/views/content/root_page.js b/assets/javascripts/views/content/root_page.js index b48a1df3ba..3693412340 100644 --- a/assets/javascripts/views/content/root_page.js +++ b/assets/javascripts/views/content/root_page.js @@ -1,43 +1,61 @@ -class app.views.RootPage extends app.View - @events: - click: 'onClick' - - init: -> - @setHidden false unless @isHidden() # reserve space in local storage - @render() - return - - render: -> - @empty() - - tmpl = if app.isAndroidWebview() +/* + * decaffeinate suggestions: + * DS002: Fix invalid constructor + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const Cls = (app.views.RootPage = class RootPage extends app.View { + constructor(...args) { + this.onClick = this.onClick.bind(this); + super(...args); + } + + static initClass() { + this.events = + {click: 'onClick'}; + } + + init() { + if (!this.isHidden()) { this.setHidden(false); } // reserve space in local storage + this.render(); + } + + render() { + this.empty(); + + const tmpl = app.isAndroidWebview() ? 'androidWarning' - else if @isHidden() + : this.isHidden() ? 'splash' - else if app.isMobile() + : app.isMobile() ? 'mobileIntro' - else - 'intro' - - @append @tmpl(tmpl) - return - - hideIntro: -> - @setHidden true - @render() - return - - setHidden: (value) -> - app.settings.set 'hideIntro', value - return - - isHidden: -> - app.isSingleDoc() or app.settings.get 'hideIntro' - - onRoute: -> - - onClick: (event) => - if $.eventTarget(event).hasAttribute 'data-hide-intro' - $.stopEvent(event) - @hideIntro() - return + : + 'intro'; + + this.append(this.tmpl(tmpl)); + } + + hideIntro() { + this.setHidden(true); + this.render(); + } + + setHidden(value) { + app.settings.set('hideIntro', value); + } + + isHidden() { + return app.isSingleDoc() || app.settings.get('hideIntro'); + } + + onRoute() {} + + onClick(event) { + if ($.eventTarget(event).hasAttribute('data-hide-intro')) { + $.stopEvent(event); + this.hideIntro(); + } + } +}); +Cls.initClass(); diff --git a/assets/javascripts/views/content/settings_page.js b/assets/javascripts/views/content/settings_page.js index 7a7b624696..5174368bd6 100644 --- a/assets/javascripts/views/content/settings_page.js +++ b/assets/javascripts/views/content/settings_page.js @@ -1,116 +1,151 @@ -class app.views.SettingsPage extends app.View - @className: '_static' - - @events: - click: 'onClick' - change: 'onChange' - - render: -> - @html @tmpl('settingsPage', @currentSettings()) - return - - currentSettings: -> - settings = {} - settings.theme = app.settings.get('theme') - settings.smoothScroll = !app.settings.get('fastScroll') - settings.arrowScroll = app.settings.get('arrowScroll') - settings.noAutofocus = app.settings.get('noAutofocus') - settings.autoInstall = app.settings.get('autoInstall') - settings.analyticsConsent = app.settings.get('analyticsConsent') - settings.spaceScroll = app.settings.get('spaceScroll') - settings.spaceTimeout = app.settings.get('spaceTimeout') - settings.autoSupported = app.settings.autoSupported - settings[layout] = app.settings.hasLayout(layout) for layout in app.settings.LAYOUTS - settings - - getTitle: -> - 'Preferences' - - setTheme: (value) -> - app.settings.set('theme', value) - return - - toggleLayout: (layout, enable) -> - app.settings.setLayout(layout, enable) - return - - toggleSmoothScroll: (enable) -> - app.settings.set('fastScroll', !enable) - return - - toggleAnalyticsConsent: (enable) -> - app.settings.set('analyticsConsent', if enable then '1' else '0') - resetAnalytics() unless enable - return - - toggleSpaceScroll: (enable) -> - app.settings.set('spaceScroll', if enable then 1 else 0) - return - - setScrollTimeout: (value) -> - app.settings.set('spaceTimeout', value) - - toggle: (name, enable) -> - app.settings.set(name, enable) - return - - export: -> - data = new Blob([JSON.stringify(app.settings.export())], type: 'application/json') - link = document.createElement('a') - link.href = URL.createObjectURL(data) - link.download = 'devdocs.json' - link.style.display = 'none' - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - return - - import: (file, input) -> - unless file and file.type is 'application/json' - new app.views.Notif 'ImportInvalid', autoHide: false - return - - reader = new FileReader() - reader.onloadend = -> - data = try JSON.parse(reader.result) - unless data and data.constructor is Object - new app.views.Notif 'ImportInvalid', autoHide: false - return - app.settings.import(data) - $.trigger input.form, 'import' - return - reader.readAsText(file) - return - - onChange: (event) => - input = event.target - switch input.name - when 'theme' - @setTheme input.value - when 'layout' - @toggleLayout input.value, input.checked - when 'smoothScroll' - @toggleSmoothScroll input.checked - when 'import' - @import input.files[0], input - when 'analyticsConsent' - @toggleAnalyticsConsent input.checked - when 'spaceScroll' - @toggleSpaceScroll input.checked - when 'spaceTimeout' - @setScrollTimeout input.value - else - @toggle input.name, input.checked - return - - onClick: (event) => - target = $.eventTarget(event) - switch target.getAttribute('data-action') - when 'export' - $.stopEvent(event) - @export() - return - - onRoute: (context) -> - @render() - return +/* + * decaffeinate suggestions: + * DS002: Fix invalid constructor + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const Cls = (app.views.SettingsPage = class SettingsPage extends app.View { + constructor(...args) { + this.onChange = this.onChange.bind(this); + this.onClick = this.onClick.bind(this); + super(...args); + } + + static initClass() { + this.className = '_static'; + + this.events = { + click: 'onClick', + change: 'onChange' + }; + } + + render() { + this.html(this.tmpl('settingsPage', this.currentSettings())); + } + + currentSettings() { + const settings = {}; + settings.theme = app.settings.get('theme'); + settings.smoothScroll = !app.settings.get('fastScroll'); + settings.arrowScroll = app.settings.get('arrowScroll'); + settings.noAutofocus = app.settings.get('noAutofocus'); + settings.autoInstall = app.settings.get('autoInstall'); + settings.analyticsConsent = app.settings.get('analyticsConsent'); + settings.spaceScroll = app.settings.get('spaceScroll'); + settings.spaceTimeout = app.settings.get('spaceTimeout'); + settings.autoSupported = app.settings.autoSupported; + for (var layout of Array.from(app.settings.LAYOUTS)) { settings[layout] = app.settings.hasLayout(layout); } + return settings; + } + + getTitle() { + return 'Preferences'; + } + + setTheme(value) { + app.settings.set('theme', value); + } + + toggleLayout(layout, enable) { + app.settings.setLayout(layout, enable); + } + + toggleSmoothScroll(enable) { + app.settings.set('fastScroll', !enable); + } + + toggleAnalyticsConsent(enable) { + app.settings.set('analyticsConsent', enable ? '1' : '0'); + if (!enable) { resetAnalytics(); } + } + + toggleSpaceScroll(enable) { + app.settings.set('spaceScroll', enable ? 1 : 0); + } + + setScrollTimeout(value) { + return app.settings.set('spaceTimeout', value); + } + + toggle(name, enable) { + app.settings.set(name, enable); + } + + export() { + const data = new Blob([JSON.stringify(app.settings.export())], {type: 'application/json'}); + const link = document.createElement('a'); + link.href = URL.createObjectURL(data); + link.download = 'devdocs.json'; + link.style.display = 'none'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + + import(file, input) { + if (!file || (file.type !== 'application/json')) { + new app.views.Notif('ImportInvalid', {autoHide: false}); + return; + } + + const reader = new FileReader(); + reader.onloadend = function() { + const data = (() => { try { return JSON.parse(reader.result); } catch (error) {} })(); + if (!data || (data.constructor !== Object)) { + new app.views.Notif('ImportInvalid', {autoHide: false}); + return; + } + app.settings.import(data); + $.trigger(input.form, 'import'); + }; + reader.readAsText(file); + } + + onChange(event) { + const input = event.target; + switch (input.name) { + case 'theme': + this.setTheme(input.value); + break; + case 'layout': + this.toggleLayout(input.value, input.checked); + break; + case 'smoothScroll': + this.toggleSmoothScroll(input.checked); + break; + case 'import': + this.import(input.files[0], input); + break; + case 'analyticsConsent': + this.toggleAnalyticsConsent(input.checked); + break; + case 'spaceScroll': + this.toggleSpaceScroll(input.checked); + break; + case 'spaceTimeout': + this.setScrollTimeout(input.value); + break; + default: + this.toggle(input.name, input.checked); + } + } + + onClick(event) { + const target = $.eventTarget(event); + switch (target.getAttribute('data-action')) { + case 'export': + $.stopEvent(event); + this.export(); + break; + } + } + + onRoute(context) { + this.render(); + } +}); +Cls.initClass(); diff --git a/assets/javascripts/views/content/static_page.js b/assets/javascripts/views/content/static_page.js index d7bee725b6..a998dbf758 100644 --- a/assets/javascripts/views/content/static_page.js +++ b/assets/javascripts/views/content/static_page.js @@ -1,26 +1,39 @@ -class app.views.StaticPage extends app.View - @className: '_static' +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const Cls = (app.views.StaticPage = class StaticPage extends app.View { + static initClass() { + this.className = '_static'; + + this.titles = { + about: 'About', + news: 'News', + help: 'User Guide', + notFound: '404' + }; + } - @titles: - about: 'About' - news: 'News' - help: 'User Guide' - notFound: '404' + deactivate() { + if (super.deactivate(...arguments)) { + this.empty(); + this.page = null; + } + } - deactivate: -> - if super - @empty() - @page = null - return + render(page) { + this.page = page; + this.html(this.tmpl(`${this.page}Page`)); + } - render: (page) -> - @page = page - @html @tmpl("#{@page}Page") - return + getTitle() { + return this.constructor.titles[this.page]; + } - getTitle: -> - @constructor.titles[@page] - - onRoute: (context) -> - @render context.page or 'notFound' - return + onRoute(context) { + this.render(context.page || 'notFound'); + } +}); +Cls.initClass(); diff --git a/assets/javascripts/views/content/type_page.js b/assets/javascripts/views/content/type_page.js index ef360c14c4..f54ee25a53 100644 --- a/assets/javascripts/views/content/type_page.js +++ b/assets/javascripts/views/content/type_page.js @@ -1,20 +1,33 @@ -class app.views.TypePage extends app.View - @className: '_page' +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const Cls = (app.views.TypePage = class TypePage extends app.View { + static initClass() { + this.className = '_page'; + } - deactivate: -> - if super - @empty() - @type = null - return + deactivate() { + if (super.deactivate(...arguments)) { + this.empty(); + this.type = null; + } + } - render: (@type) -> - @html @tmpl('typePage', @type) - setFaviconForDoc(@type.doc) - return + render(type) { + this.type = type; + this.html(this.tmpl('typePage', this.type)); + setFaviconForDoc(this.type.doc); + } - getTitle: -> - "#{@type.doc.fullName} / #{@type.name}" + getTitle() { + return `${this.type.doc.fullName} / ${this.type.name}`; + } - onRoute: (context) -> - @render context.type - return + onRoute(context) { + this.render(context.type); + } +}); +Cls.initClass(); diff --git a/assets/javascripts/views/layout/document.js b/assets/javascripts/views/layout/document.js index a10d0b3ce2..6b0dd57d42 100644 --- a/assets/javascripts/views/layout/document.js +++ b/assets/javascripts/views/layout/document.js @@ -1,85 +1,111 @@ -class app.views.Document extends app.View - @el: document - - @events: - visibilitychange: 'onVisibilityChange' - - @shortcuts: - help: 'onHelp' - preferences: 'onPreferences' - escape: 'onEscape' - superLeft: 'onBack' - superRight: 'onForward' - - @routes: - after: 'afterRoute' - - init: -> - @addSubview @menu = new app.views.Menu, - @addSubview @sidebar = new app.views.Sidebar - @addSubview @resizer = new app.views.Resizer if app.views.Resizer.isSupported() - @addSubview @content = new app.views.Content - @addSubview @path = new app.views.Path unless app.isSingleDoc() or app.isMobile() - @settings = new app.views.Settings unless app.isSingleDoc() - - $.on document.body, 'click', @onClick - - @activate() - return - - setTitle: (title) -> - @el.title = if title then "#{title} — DevDocs" else 'DevDocs API Documentation' - - afterRoute: (route) => - if route is 'settings' - @settings?.activate() - else - @settings?.deactivate() - return - - onVisibilityChange: => - return unless @el.visibilityState is 'visible' - @delay -> - location.reload() if app.isMobile() isnt app.views.Mobile.detect() - return - , 300 - return - - 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() +/* + * decaffeinate suggestions: + * DS002: Fix invalid constructor + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const Cls = (app.views.Document = class Document extends app.View { + constructor(...args) { + this.afterRoute = this.afterRoute.bind(this); + this.onVisibilityChange = this.onVisibilityChange.bind(this); + super(...args); + } + + static initClass() { + this.el = document; + + this.events = + {visibilitychange: 'onVisibilityChange'}; + + this.shortcuts = { + help: 'onHelp', + preferences: 'onPreferences', + escape: 'onEscape', + superLeft: 'onBack', + superRight: 'onForward' + }; + + this.routes = + {after: 'afterRoute'}; + } + + init() { + this.addSubview((this.menu = new app.views.Menu), + this.addSubview(this.sidebar = new app.views.Sidebar)); + if (app.views.Resizer.isSupported()) { this.addSubview(this.resizer = new app.views.Resizer); } + this.addSubview(this.content = new app.views.Content); + if (!app.isSingleDoc() && !app.isMobile()) { this.addSubview(this.path = new app.views.Path); } + if (!app.isSingleDoc()) { this.settings = new app.views.Settings; } + + $.on(document.body, 'click', this.onClick); + + this.activate(); + } + + setTitle(title) { + return this.el.title = title ? `${title} — DevDocs` : 'DevDocs API Documentation'; + } + + afterRoute(route) { + if (route === 'settings') { + if (this.settings != null) { + this.settings.activate(); + } + } else { + if (this.settings != null) { + this.settings.deactivate(); + } + } + } + + onVisibilityChange() { + if (this.el.visibilityState !== 'visible') { return; } + this.delay(function() { + if (app.isMobile() !== app.views.Mobile.detect()) { location.reload(); } + } + , 300); + } + + onHelp() { + app.router.show('/help#shortcuts'); + } + + onPreferences() { + app.router.show('/settings'); + } + + onEscape() { + const path = !app.isSingleDoc() || (location.pathname === app.doc.fullPath()) ? '/' - else - app.doc.fullPath() - - app.router.show(path) - return - - onBack: -> - history.back() - return - - onForward: -> - history.forward() - return - - onClick: (event) -> - target = $.eventTarget(event) - return unless target.hasAttribute('data-behavior') - $.stopEvent(event) - switch target.getAttribute('data-behavior') - when 'back' then history.back() - when 'reload' then window.location.reload() - when 'reboot' then app.reboot() - when 'hard-reload' then app.reload() - when 'reset' then app.reset() if confirm('Are you sure you want to reset DevDocs?') - when 'accept-analytics' then Cookies.set('analyticsConsent', '1', expires: 1e8) && app.reboot() - when 'decline-analytics' then Cookies.set('analyticsConsent', '0', expires: 1e8) && app.reboot() - return + : + app.doc.fullPath(); + + app.router.show(path); + } + + onBack() { + history.back(); + } + + onForward() { + history.forward(); + } + + onClick(event) { + const target = $.eventTarget(event); + if (!target.hasAttribute('data-behavior')) { return; } + $.stopEvent(event); + switch (target.getAttribute('data-behavior')) { + case 'back': history.back(); break; + case 'reload': window.location.reload(); break; + case 'reboot': app.reboot(); break; + case 'hard-reload': app.reload(); break; + case 'reset': if (confirm('Are you sure you want to reset DevDocs?')) { app.reset(); } break; + case 'accept-analytics': Cookies.set('analyticsConsent', '1', {expires: 1e8}) && app.reboot(); break; + case 'decline-analytics': Cookies.set('analyticsConsent', '0', {expires: 1e8}) && app.reboot(); break; + } + } +}); +Cls.initClass(); diff --git a/assets/javascripts/views/layout/menu.js b/assets/javascripts/views/layout/menu.js index e2282176ec..4254d1151a 100644 --- a/assets/javascripts/views/layout/menu.js +++ b/assets/javascripts/views/layout/menu.js @@ -1,23 +1,39 @@ -class app.views.Menu extends app.View - @el: '._menu' - @activeClass: 'active' +/* + * decaffeinate suggestions: + * DS002: Fix invalid constructor + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const Cls = (app.views.Menu = class Menu extends app.View { + constructor(...args) { + this.onGlobalClick = this.onGlobalClick.bind(this); + super(...args); + } - @events: - click: 'onClick' + static initClass() { + this.el = '._menu'; + this.activeClass = 'active'; + + this.events = + {click: 'onClick'}; + } - init: -> - $.on document.body, 'click', @onGlobalClick - return + init() { + $.on(document.body, 'click', this.onGlobalClick); + } - onClick: (event) -> - target = $.eventTarget(event) - target.blur() if target.tagName is 'A' - return + onClick(event) { + const target = $.eventTarget(event); + if (target.tagName === 'A') { target.blur(); } + } - onGlobalClick: (event) => - return if event.which isnt 1 - if event.target.hasAttribute?('data-toggle-menu') - @toggleClass @constructor.activeClass - else if @hasClass @constructor.activeClass - @removeClass @constructor.activeClass - return + onGlobalClick(event) { + if (event.which !== 1) { return; } + if (typeof event.target.hasAttribute === 'function' ? event.target.hasAttribute('data-toggle-menu') : undefined) { + this.toggleClass(this.constructor.activeClass); + } else if (this.hasClass(this.constructor.activeClass)) { + this.removeClass(this.constructor.activeClass); + } + } +}); +Cls.initClass(); diff --git a/assets/javascripts/views/layout/mobile.js b/assets/javascripts/views/layout/mobile.js index 1fd5e28577..bc336f59e0 100644 --- a/assets/javascripts/views/layout/mobile.js +++ b/assets/javascripts/views/layout/mobile.js @@ -1,155 +1,195 @@ -class app.views.Mobile extends app.View - @className: '_mobile' - - @elements: - body: 'body' - content: '._container' - sidebar: '._sidebar' - docPicker: '._settings ._sidebar' - - @shortcuts: - escape: 'onEscape' - - @routes: - after: 'afterRoute' - - @detect: -> - if Cookies.get('override-mobile-detect')? - return JSON.parse Cookies.get('override-mobile-detect') - try - (window.matchMedia('(max-width: 480px)').matches) or - (window.matchMedia('(max-width: 767px)').matches) or - (window.matchMedia('(max-height: 767px) and (max-width: 1024px)').matches) or - # Need to sniff the user agent because some Android and Windows Phone devices don't take - # resolution (dpi) into account when reporting device width/height. - (navigator.userAgent.indexOf('Android') isnt -1 and navigator.userAgent.indexOf('Mobile') isnt -1) or - (navigator.userAgent.indexOf('IEMobile') isnt -1) - catch - false - - @detectAndroidWebview: -> - try - /(Android).*( Version\/.\.. ).*(Chrome)/.test(navigator.userAgent) - catch - false - - constructor: -> - @el = document.documentElement - super - - init: -> - $.on $('._search'), 'touchend', @onTapSearch - - @toggleSidebar = $('button[data-toggle-sidebar]') - @toggleSidebar.removeAttribute('hidden') - $.on @toggleSidebar, 'click', @onClickToggleSidebar - - @back = $('button[data-back]') - @back.removeAttribute('hidden') - $.on @back, 'click', @onClickBack - - @forward = $('button[data-forward]') - @forward.removeAttribute('hidden') - $.on @forward, 'click', @onClickForward - - @docPickerTab = $('button[data-tab="doc-picker"]') - @docPickerTab.removeAttribute('hidden') - $.on @docPickerTab, 'click', @onClickDocPickerTab - - @settingsTab = $('button[data-tab="settings"]') - @settingsTab.removeAttribute('hidden') - $.on @settingsTab, 'click', @onClickSettingsTab +/* + * decaffeinate suggestions: + * DS002: Fix invalid constructor + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const Cls = (app.views.Mobile = class Mobile extends app.View { + static initClass() { + this.className = '_mobile'; + + this.elements = { + body: 'body', + content: '._container', + sidebar: '._sidebar', + docPicker: '._settings ._sidebar' + }; + + this.shortcuts = + {escape: 'onEscape'}; + + this.routes = + {after: 'afterRoute'}; + } + + static detect() { + if (Cookies.get('override-mobile-detect') != null) { + return JSON.parse(Cookies.get('override-mobile-detect')); + } + try { + return (window.matchMedia('(max-width: 480px)').matches) || + (window.matchMedia('(max-width: 767px)').matches) || + (window.matchMedia('(max-height: 767px) and (max-width: 1024px)').matches) || + // Need to sniff the user agent because some Android and Windows Phone devices don't take + // resolution (dpi) into account when reporting device width/height. + ((navigator.userAgent.indexOf('Android') !== -1) && (navigator.userAgent.indexOf('Mobile') !== -1)) || + (navigator.userAgent.indexOf('IEMobile') !== -1); + } catch (error) { + return false; + } + } + + static detectAndroidWebview() { + try { + return /(Android).*( Version\/.\.. ).*(Chrome)/.test(navigator.userAgent); + } catch (error) { + return false; + } + } + + constructor() { + this.showSidebar = this.showSidebar.bind(this); + this.hideSidebar = this.hideSidebar.bind(this); + this.onClickBack = this.onClickBack.bind(this); + this.onClickForward = this.onClickForward.bind(this); + this.onClickToggleSidebar = this.onClickToggleSidebar.bind(this); + this.onClickDocPickerTab = this.onClickDocPickerTab.bind(this); + this.onClickSettingsTab = this.onClickSettingsTab.bind(this); + this.onTapSearch = this.onTapSearch.bind(this); + this.onEscape = this.onEscape.bind(this); + this.afterRoute = this.afterRoute.bind(this); + this.el = document.documentElement; + super(...arguments); + } + + init() { + $.on($('._search'), 'touchend', this.onTapSearch); + + this.toggleSidebar = $('button[data-toggle-sidebar]'); + this.toggleSidebar.removeAttribute('hidden'); + $.on(this.toggleSidebar, 'click', this.onClickToggleSidebar); + + this.back = $('button[data-back]'); + this.back.removeAttribute('hidden'); + $.on(this.back, 'click', this.onClickBack); + + this.forward = $('button[data-forward]'); + this.forward.removeAttribute('hidden'); + $.on(this.forward, 'click', this.onClickForward); + + this.docPickerTab = $('button[data-tab="doc-picker"]'); + this.docPickerTab.removeAttribute('hidden'); + $.on(this.docPickerTab, 'click', this.onClickDocPickerTab); + + this.settingsTab = $('button[data-tab="settings"]'); + this.settingsTab.removeAttribute('hidden'); + $.on(this.settingsTab, 'click', this.onClickSettingsTab); app.document.sidebar.search - .on 'searching', @showSidebar - - @activate() - return - - showSidebar: => - if @isSidebarShown() - window.scrollTo 0, 0 - return - - @contentTop = window.scrollY - @content.style.display = 'none' - @sidebar.style.display = 'block' - - if selection = @findByClass app.views.ListSelect.activeClass - scrollContainer = if window.scrollY is @body.scrollTop then @body else document.documentElement - $.scrollTo selection, scrollContainer, 'center' - else - window.scrollTo 0, @findByClass(app.views.ListFold.activeClass) and @sidebarTop or 0 - return - - hideSidebar: => - return unless @isSidebarShown() - @sidebarTop = window.scrollY - @sidebar.style.display = 'none' - @content.style.display = 'block' - window.scrollTo 0, @contentTop or 0 - return - - isSidebarShown: -> - @sidebar.style.display isnt 'none' - - onClickBack: => - history.back() - - onClickForward: => - history.forward() - - onClickToggleSidebar: => - if @isSidebarShown() then @hideSidebar() else @showSidebar() - return - - onClickDocPickerTab: (event) => - $.stopEvent(event) - @showDocPicker() - return - - onClickSettingsTab: (event) => - $.stopEvent(event) - @showSettings() - return - - showDocPicker: -> - window.scrollTo 0, 0 - @docPickerTab.classList.add 'active' - @settingsTab.classList.remove 'active' - @docPicker.style.display = 'block' - @content.style.display = 'none' - return - - showSettings: -> - window.scrollTo 0, 0 - @docPickerTab.classList.remove 'active' - @settingsTab.classList.add 'active' - @docPicker.style.display = 'none' - @content.style.display = 'block' - return - - onTapSearch: => - window.scrollTo 0, 0 - - onEscape: => - @hideSidebar() - - afterRoute: (route) => - @hideSidebar() - - if route is 'settings' - @showDocPicker() - else - @content.style.display = 'block' - - if page.canGoBack() - @back.removeAttribute('disabled') - else - @back.setAttribute('disabled', 'disabled') - - if page.canGoForward() - @forward.removeAttribute('disabled') - else - @forward.setAttribute('disabled', 'disabled') - return + .on('searching', this.showSidebar); + + this.activate(); + } + + showSidebar() { + let selection; + if (this.isSidebarShown()) { + window.scrollTo(0, 0); + return; + } + + this.contentTop = window.scrollY; + this.content.style.display = 'none'; + this.sidebar.style.display = 'block'; + + if (selection = this.findByClass(app.views.ListSelect.activeClass)) { + const scrollContainer = window.scrollY === this.body.scrollTop ? this.body : document.documentElement; + $.scrollTo(selection, scrollContainer, 'center'); + } else { + window.scrollTo(0, (this.findByClass(app.views.ListFold.activeClass) && this.sidebarTop) || 0); + } + } + + hideSidebar() { + if (!this.isSidebarShown()) { return; } + this.sidebarTop = window.scrollY; + this.sidebar.style.display = 'none'; + this.content.style.display = 'block'; + window.scrollTo(0, this.contentTop || 0); + } + + isSidebarShown() { + return this.sidebar.style.display !== 'none'; + } + + onClickBack() { + return history.back(); + } + + onClickForward() { + return history.forward(); + } + + onClickToggleSidebar() { + if (this.isSidebarShown()) { this.hideSidebar(); } else { this.showSidebar(); } + } + + onClickDocPickerTab(event) { + $.stopEvent(event); + this.showDocPicker(); + } + + onClickSettingsTab(event) { + $.stopEvent(event); + this.showSettings(); + } + + showDocPicker() { + window.scrollTo(0, 0); + this.docPickerTab.classList.add('active'); + this.settingsTab.classList.remove('active'); + this.docPicker.style.display = 'block'; + this.content.style.display = 'none'; + } + + showSettings() { + window.scrollTo(0, 0); + this.docPickerTab.classList.remove('active'); + this.settingsTab.classList.add('active'); + this.docPicker.style.display = 'none'; + this.content.style.display = 'block'; + } + + onTapSearch() { + return window.scrollTo(0, 0); + } + + onEscape() { + return this.hideSidebar(); + } + + afterRoute(route) { + this.hideSidebar(); + + if (route === 'settings') { + this.showDocPicker(); + } else { + this.content.style.display = 'block'; + } + + if (page.canGoBack()) { + this.back.removeAttribute('disabled'); + } else { + this.back.setAttribute('disabled', 'disabled'); + } + + if (page.canGoForward()) { + this.forward.removeAttribute('disabled'); + } else { + this.forward.setAttribute('disabled', 'disabled'); + } + } +}); +Cls.initClass(); diff --git a/assets/javascripts/views/layout/path.js b/assets/javascripts/views/layout/path.js index fb34afda61..fa9eb92e61 100644 --- a/assets/javascripts/views/layout/path.js +++ b/assets/javascripts/views/layout/path.js @@ -1,43 +1,64 @@ -class app.views.Path extends app.View - @className: '_path' - @attributes: - role: 'complementary' +/* + * decaffeinate suggestions: + * DS002: Fix invalid constructor + * DS101: Remove unnecessary use of Array.from + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const Cls = (app.views.Path = class Path extends app.View { + constructor(...args) { + this.onClick = this.onClick.bind(this); + this.afterRoute = this.afterRoute.bind(this); + super(...args); + } - @events: - click: 'onClick' + static initClass() { + this.className = '_path'; + this.attributes = + {role: 'complementary'}; + + this.events = + {click: 'onClick'}; + + this.routes = + {after: 'afterRoute'}; + } - @routes: - after: 'afterRoute' + render(...args) { + this.html(this.tmpl('path', ...Array.from(args))); + this.show(); + } - render: (args...) -> - @html @tmpl 'path', args... - @show() - return + show() { + if (!this.el.parentNode) { this.prependTo(app.el); } + } - show: -> - @prependTo app.el unless @el.parentNode - return + hide() { + if (this.el.parentNode) { $.remove(this.el); } + } - hide: -> - $.remove @el if @el.parentNode - return + onClick(event) { + let link; + if (link = $.closestLink(event.target, this.el)) { this.clicked = true; } + } - onClick: (event) => - @clicked = true if link = $.closestLink event.target, @el - return + afterRoute(route, context) { + if (context.type) { + this.render(context.doc, context.type); + } else if (context.entry) { + if (context.entry.isIndex()) { + this.render(context.doc); + } else { + this.render(context.doc, context.entry.getType(), context.entry); + } + } else { + this.hide(); + } - afterRoute: (route, context) => - if context.type - @render context.doc, context.type - else if context.entry - if context.entry.isIndex() - @render context.doc - else - @render context.doc, context.entry.getType(), context.entry - else - @hide() - - if @clicked - @clicked = null - app.document.sidebar.reset() - return + if (this.clicked) { + this.clicked = null; + app.document.sidebar.reset(); + } + } +}); +Cls.initClass(); diff --git a/assets/javascripts/views/layout/resizer.js b/assets/javascripts/views/layout/resizer.js index 5584bfbe50..44f3bdb153 100644 --- a/assets/javascripts/views/layout/resizer.js +++ b/assets/javascripts/views/layout/resizer.js @@ -1,49 +1,75 @@ -class app.views.Resizer extends app.View - @className: '_resizer' +/* + * decaffeinate suggestions: + * DS002: Fix invalid constructor + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +(function() { + let MIN = undefined; + let MAX = undefined; + const Cls = (app.views.Resizer = class Resizer extends app.View { + constructor(...args) { + this.onDragStart = this.onDragStart.bind(this); + this.onDrag = this.onDrag.bind(this); + this.onDragEnd = this.onDragEnd.bind(this); + super(...args); + } - @events: - dragstart: 'onDragStart' - dragend: 'onDragEnd' + static initClass() { + this.className = '_resizer'; + + this.events = { + dragstart: 'onDragStart', + dragend: 'onDragEnd' + }; + + MIN = 260; + MAX = 600; + } - @isSupported: -> - 'ondragstart' of document.createElement('div') and !app.isMobile() + static isSupported() { + return 'ondragstart' in document.createElement('div') && !app.isMobile(); + } - init: -> - @el.setAttribute('draggable', 'true') - @appendTo $('._app') - return + init() { + this.el.setAttribute('draggable', 'true'); + this.appendTo($('._app')); + } - MIN = 260 - MAX = 600 + resize(value, save) { + value -= app.el.offsetLeft; + if (!(value > 0)) { return; } + value = Math.min(Math.max(Math.round(value), MIN), MAX); + const newSize = `${value}px`; + document.documentElement.style.setProperty('--sidebarWidth', newSize); + if (save) { app.settings.setSize(value); } + } - resize: (value, save) -> - value -= app.el.offsetLeft - return unless value > 0 - value = Math.min(Math.max(Math.round(value), MIN), MAX) - newSize = "#{value}px" - document.documentElement.style.setProperty('--sidebarWidth', newSize) - app.settings.setSize(value) if save - return + onDragStart(event) { + event.dataTransfer.effectAllowed = 'link'; + event.dataTransfer.setData('Text', ''); + $.on(window, 'dragover', this.onDrag); + } - onDragStart: (event) => - event.dataTransfer.effectAllowed = 'link' - event.dataTransfer.setData('Text', '') - $.on(window, 'dragover', @onDrag) - return + onDrag(event) { + const value = event.pageX; + if (!(value > 0)) { return; } + this.lastDragValue = value; + if (this.lastDrag && (this.lastDrag > (Date.now() - 50))) { return; } + this.lastDrag = Date.now(); + this.resize(value, false); + } - onDrag: (event) => - value = event.pageX - return unless value > 0 - @lastDragValue = value - return if @lastDrag and @lastDrag > Date.now() - 50 - @lastDrag = Date.now() - @resize(value, false) - return - - onDragEnd: (event) => - $.off(window, 'dragover', @onDrag) - value = event.pageX or (event.screenX - window.screenX) - if @lastDragValue and not (@lastDragValue - 5 < value < @lastDragValue + 5) # https://github.com/freeCodeCamp/devdocs/issues/265 - value = @lastDragValue - @resize(value, true) - return + onDragEnd(event) { + $.off(window, 'dragover', this.onDrag); + let value = event.pageX || (event.screenX - window.screenX); + if (this.lastDragValue && !(this.lastDragValue - 5 < value && value < this.lastDragValue + 5)) { // https://github.com/freeCodeCamp/devdocs/issues/265 + value = this.lastDragValue; + } + this.resize(value, true); + } + }); + Cls.initClass(); + return Cls; +})(); diff --git a/assets/javascripts/views/layout/settings.js b/assets/javascripts/views/layout/settings.js index 6941b9cd25..57d0b0611b 100644 --- a/assets/javascripts/views/layout/settings.js +++ b/assets/javascripts/views/layout/settings.js @@ -1,83 +1,127 @@ -class app.views.Settings extends app.View - SIDEBAR_HIDDEN_LAYOUT = '_sidebar-hidden' - - @el: '._settings' - - @elements: - sidebar: '._sidebar' - saveBtn: 'button[type="submit"]' - backBtn: 'button[data-back]' - - @events: - import: 'onImport' - change: 'onChange' - submit: 'onSubmit' - click: 'onClick' - - @shortcuts: - enter: 'onEnter' - - init: -> - @addSubview @docPicker = new app.views.DocPicker - return - - activate: -> - if super - @render() - document.body.classList.remove(SIDEBAR_HIDDEN_LAYOUT) - return - - deactivate: -> - if super - @resetClass() - @docPicker.detach() - document.body.classList.add(SIDEBAR_HIDDEN_LAYOUT) if app.settings.hasLayout(SIDEBAR_HIDDEN_LAYOUT) - return - - render: -> - @docPicker.appendTo @sidebar - @refreshElements() - @addClass '_in' - return - - save: (options = {}) -> - unless @saving - @saving = true - - if options.import - docs = app.settings.getDocs() - else - docs = @docPicker.getSelectedDocs() - app.settings.setDocs(docs) - - @saveBtn.textContent = 'Saving\u2026' - disabledDocs = new app.collections.Docs(doc for doc in app.docs.all() when docs.indexOf(doc.slug) is -1) - disabledDocs.uninstall -> - app.db.migrate() - app.reload() - return - - onChange: => - @addClass('_dirty') - return - - onEnter: => - @save() - return - - onSubmit: (event) => - event.preventDefault() - @save() - return - - onImport: => - @addClass('_dirty') - @save(import: true) - return - - onClick: (event) => - return if event.which isnt 1 - if event.target is @backBtn - $.stopEvent(event) - app.router.show '/' - return +/* + * decaffeinate suggestions: + * DS002: Fix invalid constructor + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +(function() { + let SIDEBAR_HIDDEN_LAYOUT = undefined; + const Cls = (app.views.Settings = class Settings extends app.View { + constructor(...args) { + this.onChange = this.onChange.bind(this); + this.onEnter = this.onEnter.bind(this); + this.onSubmit = this.onSubmit.bind(this); + this.onImport = this.onImport.bind(this); + this.onClick = this.onClick.bind(this); + super(...args); + } + + static initClass() { + SIDEBAR_HIDDEN_LAYOUT = '_sidebar-hidden'; + + this.el = '._settings'; + + this.elements = { + sidebar: '._sidebar', + saveBtn: 'button[type="submit"]', + backBtn: 'button[data-back]' + }; + + this.events = { + import: 'onImport', + change: 'onChange', + submit: 'onSubmit', + click: 'onClick' + }; + + this.shortcuts = + {enter: 'onEnter'}; + } + + init() { + this.addSubview(this.docPicker = new app.views.DocPicker); + } + + activate() { + if (super.activate(...arguments)) { + this.render(); + document.body.classList.remove(SIDEBAR_HIDDEN_LAYOUT); + } + } + + deactivate() { + if (super.deactivate(...arguments)) { + this.resetClass(); + this.docPicker.detach(); + if (app.settings.hasLayout(SIDEBAR_HIDDEN_LAYOUT)) { document.body.classList.add(SIDEBAR_HIDDEN_LAYOUT); } + } + } + + render() { + this.docPicker.appendTo(this.sidebar); + this.refreshElements(); + this.addClass('_in'); + } + + save(options) { + if (options == null) { options = {}; } + if (!this.saving) { + let docs; + this.saving = true; + + if (options.import) { + docs = app.settings.getDocs(); + } else { + docs = this.docPicker.getSelectedDocs(); + app.settings.setDocs(docs); + } + + this.saveBtn.textContent = 'Saving\u2026'; + const disabledDocs = new app.collections.Docs((() => { + const result = []; + for (var doc of Array.from(app.docs.all())) { if (docs.indexOf(doc.slug) === -1) { + result.push(doc); + } + } + return result; + })()); + disabledDocs.uninstall(function() { + app.db.migrate(); + return app.reload(); + }); + } + } + + onChange() { + this.addClass('_dirty'); + } + + onEnter() { + this.save(); + } + + onSubmit(event) { + event.preventDefault(); + this.save(); + } + + onImport() { + this.addClass('_dirty'); + this.save({import: true}); + } + + onClick(event) { + if (event.which !== 1) { return; } + if (event.target === this.backBtn) { + $.stopEvent(event); + app.router.show('/'); + } + } + }); + Cls.initClass(); + return Cls; +})(); diff --git a/assets/javascripts/views/list/list_focus.js b/assets/javascripts/views/list/list_focus.js index 808810196d..72e251fb3b 100644 --- a/assets/javascripts/views/list/list_focus.js +++ b/assets/javascripts/views/list/list_focus.js @@ -1,124 +1,177 @@ -class app.views.ListFocus extends app.View - @activeClass: 'focus' - - @events: - click: 'onClick' - - @shortcuts: - up: 'onUp' - down: 'onDown' - left: 'onLeft' - enter: 'onEnter' - superEnter: 'onSuperEnter' - escape: 'blur' - - constructor: (@el) -> - super - @focusOnNextFrame = $.framify(@focus, @) - - focus: (el, options = {}) -> - if el and not el.classList.contains @constructor.activeClass - @blur() - el.classList.add @constructor.activeClass - $.trigger el, 'focus' unless options.silent is true - return - - blur: => - if cursor = @getCursor() - cursor.classList.remove @constructor.activeClass - $.trigger cursor, 'blur' - return - - getCursor: -> - @findByClass(@constructor.activeClass) or @findByClass(app.views.ListSelect.activeClass) - - findNext: (cursor) -> - if next = cursor.nextSibling - if next.tagName is 'A' - next - else if next.tagName is 'SPAN' # pagination link - $.click(next) - @findNext cursor - else if next.tagName is 'DIV' # sub-list - if cursor.className.indexOf(' open') >= 0 - @findFirst(next) or @findNext(next) - else - @findNext(next) - else if next.tagName is 'H6' # title - @findNext(next) - else if cursor.parentNode isnt @el - @findNext cursor.parentNode - - findFirst: (cursor) -> - return unless first = cursor.firstChild - - if first.tagName is 'A' - first - else if first.tagName is 'SPAN' # pagination link - $.click(first) - @findFirst cursor - - findPrev: (cursor) -> - if prev = cursor.previousSibling - if prev.tagName is 'A' - prev - else if prev.tagName is 'SPAN' # pagination link - $.click(prev) - @findPrev cursor - else if prev.tagName is 'DIV' # sub-list - if prev.previousSibling.className.indexOf('open') >= 0 - @findLast(prev) or @findPrev(prev) - else - @findPrev(prev) - else if prev.tagName is 'H6' # title - @findPrev(prev) - else if cursor.parentNode isnt @el - @findPrev cursor.parentNode - - findLast: (cursor) -> - return unless last = cursor.lastChild - - if last.tagName is 'A' - last - else if last.tagName is 'SPAN' or last.tagName is 'H6' # pagination link or title - @findPrev last - else if last.tagName is 'DIV' # sub-list - @findLast last - - onDown: => - if cursor = @getCursor() - @focusOnNextFrame @findNext(cursor) - else - @focusOnNextFrame @findByTag('a') - return - - onUp: => - if cursor = @getCursor() - @focusOnNextFrame @findPrev(cursor) - else - @focusOnNextFrame @findLastByTag('a') - return - - onLeft: => - cursor = @getCursor() - if cursor and not cursor.classList.contains(app.views.ListFold.activeClass) and cursor.parentNode isnt @el - prev = cursor.parentNode.previousSibling - @focusOnNextFrame cursor.parentNode.previousSibling if prev and prev.classList.contains(app.views.ListFold.targetClass) - return - - onEnter: => - if cursor = @getCursor() - $.click(cursor) - return - - onSuperEnter: => - if cursor = @getCursor() - $.popup(cursor) - return - - onClick: (event) => - return if event.which isnt 1 or event.metaKey or event.ctrlKey - target = $.eventTarget(event) - if target.tagName is 'A' - @focus target, silent: true - return +/* + * decaffeinate suggestions: + * DS002: Fix invalid constructor + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const Cls = (app.views.ListFocus = class ListFocus extends app.View { + static initClass() { + this.activeClass = 'focus'; + + this.events = + {click: 'onClick'}; + + this.shortcuts = { + up: 'onUp', + down: 'onDown', + left: 'onLeft', + enter: 'onEnter', + superEnter: 'onSuperEnter', + escape: 'blur' + }; + } + + constructor(el) { + this.blur = this.blur.bind(this); + this.onDown = this.onDown.bind(this); + this.onUp = this.onUp.bind(this); + this.onLeft = this.onLeft.bind(this); + this.onEnter = this.onEnter.bind(this); + this.onSuperEnter = this.onSuperEnter.bind(this); + this.onClick = this.onClick.bind(this); + this.el = el; + super(...arguments); + this.focusOnNextFrame = $.framify(this.focus, this); + } + + focus(el, options) { + if (options == null) { options = {}; } + if (el && !el.classList.contains(this.constructor.activeClass)) { + this.blur(); + el.classList.add(this.constructor.activeClass); + if (options.silent !== true) { $.trigger(el, 'focus'); } + } + } + + blur() { + let cursor; + if (cursor = this.getCursor()) { + cursor.classList.remove(this.constructor.activeClass); + $.trigger(cursor, 'blur'); + } + } + + getCursor() { + return this.findByClass(this.constructor.activeClass) || this.findByClass(app.views.ListSelect.activeClass); + } + + findNext(cursor) { + let next; + if (next = cursor.nextSibling) { + if (next.tagName === 'A') { + return next; + } else if (next.tagName === 'SPAN') { // pagination link + $.click(next); + return this.findNext(cursor); + } else if (next.tagName === 'DIV') { // sub-list + if (cursor.className.indexOf(' open') >= 0) { + return this.findFirst(next) || this.findNext(next); + } else { + return this.findNext(next); + } + } else if (next.tagName === 'H6') { // title + return this.findNext(next); + } + } else if (cursor.parentNode !== this.el) { + return this.findNext(cursor.parentNode); + } + } + + findFirst(cursor) { + let first; + if (!(first = cursor.firstChild)) { return; } + + if (first.tagName === 'A') { + return first; + } else if (first.tagName === 'SPAN') { // pagination link + $.click(first); + return this.findFirst(cursor); + } + } + + findPrev(cursor) { + let prev; + if (prev = cursor.previousSibling) { + if (prev.tagName === 'A') { + return prev; + } else if (prev.tagName === 'SPAN') { // pagination link + $.click(prev); + return this.findPrev(cursor); + } else if (prev.tagName === 'DIV') { // sub-list + if (prev.previousSibling.className.indexOf('open') >= 0) { + return this.findLast(prev) || this.findPrev(prev); + } else { + return this.findPrev(prev); + } + } else if (prev.tagName === 'H6') { // title + return this.findPrev(prev); + } + } else if (cursor.parentNode !== this.el) { + return this.findPrev(cursor.parentNode); + } + } + + findLast(cursor) { + let last; + if (!(last = cursor.lastChild)) { return; } + + if (last.tagName === 'A') { + return last; + } else if ((last.tagName === 'SPAN') || (last.tagName === 'H6')) { // pagination link or title + return this.findPrev(last); + } else if (last.tagName === 'DIV') { // sub-list + return this.findLast(last); + } + } + + onDown() { + let cursor; + if ((cursor = this.getCursor())) { + this.focusOnNextFrame(this.findNext(cursor)); + } else { + this.focusOnNextFrame(this.findByTag('a')); + } + } + + onUp() { + let cursor; + if ((cursor = this.getCursor())) { + this.focusOnNextFrame(this.findPrev(cursor)); + } else { + this.focusOnNextFrame(this.findLastByTag('a')); + } + } + + onLeft() { + const cursor = this.getCursor(); + if (cursor && !cursor.classList.contains(app.views.ListFold.activeClass) && (cursor.parentNode !== this.el)) { + const prev = cursor.parentNode.previousSibling; + if (prev && prev.classList.contains(app.views.ListFold.targetClass)) { this.focusOnNextFrame(cursor.parentNode.previousSibling); } + } + } + + onEnter() { + let cursor; + if (cursor = this.getCursor()) { + $.click(cursor); + } + } + + onSuperEnter() { + let cursor; + if (cursor = this.getCursor()) { + $.popup(cursor); + } + } + + onClick(event) { + if ((event.which !== 1) || event.metaKey || event.ctrlKey) { return; } + const target = $.eventTarget(event); + if (target.tagName === 'A') { + this.focus(target, {silent: true}); + } + } +}); +Cls.initClass(); diff --git a/assets/javascripts/views/list/list_fold.js b/assets/javascripts/views/list/list_fold.js index da6f1d5e0e..828f90fd59 100644 --- a/assets/javascripts/views/list/list_fold.js +++ b/assets/javascripts/views/list/list_fold.js @@ -1,71 +1,95 @@ -class app.views.ListFold extends app.View - @targetClass: '_list-dir' - @handleClass: '_list-arrow' - @activeClass: 'open' +/* + * decaffeinate suggestions: + * DS002: Fix invalid constructor + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const Cls = (app.views.ListFold = class ListFold extends app.View { + static initClass() { + this.targetClass = '_list-dir'; + this.handleClass = '_list-arrow'; + this.activeClass = 'open'; + + this.events = + {click: 'onClick'}; + + this.shortcuts = { + left: 'onLeft', + right: 'onRight' + }; + } - @events: - click: 'onClick' + constructor(el) { this.onLeft = this.onLeft.bind(this); this.onRight = this.onRight.bind(this); this.onClick = this.onClick.bind(this); this.el = el; super(...arguments); } - @shortcuts: - left: 'onLeft' - right: 'onRight' + open(el) { + if (el && !el.classList.contains(this.constructor.activeClass)) { + el.classList.add(this.constructor.activeClass); + $.trigger(el, 'open'); + } + } - constructor: (@el) -> super + close(el) { + if (el && el.classList.contains(this.constructor.activeClass)) { + el.classList.remove(this.constructor.activeClass); + $.trigger(el, 'close'); + } + } - open: (el) -> - if el and not el.classList.contains @constructor.activeClass - el.classList.add @constructor.activeClass - $.trigger el, 'open' - return + toggle(el) { + if (el.classList.contains(this.constructor.activeClass)) { + this.close(el); + } else { + this.open(el); + } + } - close: (el) -> - if el and el.classList.contains @constructor.activeClass - el.classList.remove @constructor.activeClass - $.trigger el, 'close' - return + reset() { + let el; + while ((el = this.findByClass(this.constructor.activeClass))) { + this.close(el); + } + } - toggle: (el) -> - if el.classList.contains @constructor.activeClass - @close el - else - @open el - return + getCursor() { + return this.findByClass(app.views.ListFocus.activeClass) || this.findByClass(app.views.ListSelect.activeClass); + } - reset: -> - while el = @findByClass @constructor.activeClass - @close el - return + onLeft() { + const cursor = this.getCursor(); + if (cursor != null ? cursor.classList.contains(this.constructor.activeClass) : undefined) { + this.close(cursor); + } + } - getCursor: -> - @findByClass(app.views.ListFocus.activeClass) or @findByClass(app.views.ListSelect.activeClass) + onRight() { + const cursor = this.getCursor(); + if (cursor != null ? cursor.classList.contains(this.constructor.targetClass) : undefined) { + this.open(cursor); + } + } - onLeft: => - cursor = @getCursor() - if cursor?.classList.contains @constructor.activeClass - @close cursor - return + onClick(event) { + if ((event.which !== 1) || event.metaKey || event.ctrlKey) { return; } + if (!event.pageY) { return; } // ignore fabricated clicks + let el = $.eventTarget(event); + if (el.parentNode.tagName.toUpperCase() === 'SVG') { el = el.parentNode; } - onRight: => - cursor = @getCursor() - if cursor?.classList.contains @constructor.targetClass - @open cursor - return - - onClick: (event) => - return if event.which isnt 1 or event.metaKey or event.ctrlKey - return unless event.pageY # ignore fabricated clicks - el = $.eventTarget(event) - el = el.parentNode if el.parentNode.tagName.toUpperCase() is 'SVG' - - if el.classList.contains @constructor.handleClass - $.stopEvent(event) - @toggle el.parentNode - else if el.classList.contains @constructor.targetClass - if el.hasAttribute('href') - if el.classList.contains(@constructor.activeClass) - @close(el) if el.classList.contains(app.views.ListSelect.activeClass) - else - @open(el) - else - @toggle(el) - return + if (el.classList.contains(this.constructor.handleClass)) { + $.stopEvent(event); + this.toggle(el.parentNode); + } else if (el.classList.contains(this.constructor.targetClass)) { + if (el.hasAttribute('href')) { + if (el.classList.contains(this.constructor.activeClass)) { + if (el.classList.contains(app.views.ListSelect.activeClass)) { this.close(el); } + } else { + this.open(el); + } + } else { + this.toggle(el); + } + } + } +}); +Cls.initClass(); diff --git a/assets/javascripts/views/list/list_select.js b/assets/javascripts/views/list/list_select.js index fe06b70b67..6721726ef1 100644 --- a/assets/javascripts/views/list/list_select.js +++ b/assets/javascripts/views/list/list_select.js @@ -1,43 +1,65 @@ -class app.views.ListSelect extends app.View - @activeClass: 'active' - - @events: - click: 'onClick' - - constructor: (@el) -> super - - deactivate: -> - @deselect() if super - return - - select: (el) -> - @deselect() - if el - el.classList.add @constructor.activeClass - $.trigger el, 'select' - return - - deselect: -> - if selection = @getSelection() - selection.classList.remove @constructor.activeClass - $.trigger selection, 'deselect' - return - - selectByHref: (href) -> - unless @getSelection()?.getAttribute('href') is href - @select @find("a[href='#{href}']") - return - - selectCurrent: -> - @selectByHref location.pathname + location.hash - return - - getSelection: -> - @findByClass @constructor.activeClass - - onClick: (event) => - return if event.which isnt 1 or event.metaKey or event.ctrlKey - target = $.eventTarget(event) - if target.tagName is 'A' - @select target - return +/* + * decaffeinate suggestions: + * DS002: Fix invalid constructor + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const Cls = (app.views.ListSelect = class ListSelect extends app.View { + static initClass() { + this.activeClass = 'active'; + + this.events = + {click: 'onClick'}; + } + + constructor(el) { this.onClick = this.onClick.bind(this); this.el = el; super(...arguments); } + + deactivate() { + if (super.deactivate(...arguments)) { this.deselect(); } + } + + select(el) { + this.deselect(); + if (el) { + el.classList.add(this.constructor.activeClass); + $.trigger(el, 'select'); + } + } + + deselect() { + let selection; + if (selection = this.getSelection()) { + selection.classList.remove(this.constructor.activeClass); + $.trigger(selection, 'deselect'); + } + } + + selectByHref(href) { + if (__guard__(this.getSelection(), x => x.getAttribute('href')) !== href) { + this.select(this.find(`a[href='${href}']`)); + } + } + + selectCurrent() { + this.selectByHref(location.pathname + location.hash); + } + + getSelection() { + return this.findByClass(this.constructor.activeClass); + } + + onClick(event) { + if ((event.which !== 1) || event.metaKey || event.ctrlKey) { return; } + const target = $.eventTarget(event); + if (target.tagName === 'A') { + this.select(target); + } + } +}); +Cls.initClass(); + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/assets/javascripts/views/list/paginated_list.js b/assets/javascripts/views/list/paginated_list.js index 0c6c438565..10e2ead7d3 100644 --- a/assets/javascripts/views/list/paginated_list.js +++ b/assets/javascripts/views/list/paginated_list.js @@ -1,90 +1,121 @@ -class app.views.PaginatedList extends app.View - PER_PAGE = app.config.max_results - - constructor: (@data) -> - (@constructor.events or= {}).click ?= 'onClick' - super - - renderPaginated: -> - @page = 0 - - if @totalPages() > 1 - @paginateNext() - else - @html @renderAll() - return - - # render: (dataSlice) -> implemented by subclass - - renderAll: -> - @render @data - - renderPage: (page) -> - @render @data[((page - 1) * PER_PAGE)...(page * PER_PAGE)] - - renderPageLink: (count) -> - @tmpl 'sidebarPageLink', count - - renderPrevLink: (page) -> - @renderPageLink (page - 1) * PER_PAGE - - renderNextLink: (page) -> - @renderPageLink @data.length - page * PER_PAGE - - totalPages: -> - Math.ceil @data.length / PER_PAGE - - paginate: (link) -> - $.lockScroll link.nextSibling or link.previousSibling, => - $.batchUpdate @el, => - if link.nextSibling then @paginatePrev link else @paginateNext link - return - return - return - - paginateNext: -> - @remove @el.lastChild if @el.lastChild # remove link - @hideTopPage() if @page >= 2 # keep previous page into view - @page++ - @append @renderPage(@page) - @append @renderNextLink(@page) if @page < @totalPages() - return - - paginatePrev: -> - @remove @el.firstChild # remove link - @hideBottomPage() - @page-- - @prepend @renderPage(@page - 1) # previous page is offset by one - @prepend @renderPrevLink(@page - 1) if @page >= 3 - return - - paginateTo: (object) -> - index = @data.indexOf(object) - if index >= PER_PAGE - @paginateNext() for [0...(index // PER_PAGE)] - return - - hideTopPage: -> - n = if @page <= 2 - PER_PAGE - else - PER_PAGE + 1 # remove link - @remove @el.firstChild for [0...n] - @prepend @renderPrevLink(@page) - return - - hideBottomPage: -> - n = if @page is @totalPages() - @data.length % PER_PAGE or PER_PAGE - else - PER_PAGE + 1 # remove link - @remove @el.lastChild for [0...n] - @append @renderNextLink(@page - 1) - return - - onClick: (event) => - target = $.eventTarget(event) - if target.tagName is 'SPAN' # link - $.stopEvent(event) - @paginate target - return +/* + * decaffeinate suggestions: + * DS002: Fix invalid constructor + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS202: Simplify dynamic range loops + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +(function() { + let PER_PAGE = undefined; + const Cls = (app.views.PaginatedList = class PaginatedList extends app.View { + static initClass() { + PER_PAGE = app.config.max_results; + } + + constructor(data) { + let base; + this.onClick = this.onClick.bind(this); + this.data = data; + if (((base = this.constructor.events || (this.constructor.events = {}))).click == null) { base.click = 'onClick'; } + super(...arguments); + } + + renderPaginated() { + this.page = 0; + + if (this.totalPages() > 1) { + this.paginateNext(); + } else { + this.html(this.renderAll()); + } + } + + // render: (dataSlice) -> implemented by subclass + + renderAll() { + return this.render(this.data); + } + + renderPage(page) { + return this.render(this.data.slice(((page - 1) * PER_PAGE), (page * PER_PAGE))); + } + + renderPageLink(count) { + return this.tmpl('sidebarPageLink', count); + } + + renderPrevLink(page) { + return this.renderPageLink((page - 1) * PER_PAGE); + } + + renderNextLink(page) { + return this.renderPageLink(this.data.length - (page * PER_PAGE)); + } + + totalPages() { + return Math.ceil(this.data.length / PER_PAGE); + } + + paginate(link) { + $.lockScroll(link.nextSibling || link.previousSibling, () => { + $.batchUpdate(this.el, () => { + if (link.nextSibling) { this.paginatePrev(link); } else { this.paginateNext(link); } + }); + }); + } + + paginateNext() { + if (this.el.lastChild) { this.remove(this.el.lastChild); } // remove link + if (this.page >= 2) { this.hideTopPage(); } // keep previous page into view + this.page++; + this.append(this.renderPage(this.page)); + if (this.page < this.totalPages()) { this.append(this.renderNextLink(this.page)); } + } + + paginatePrev() { + this.remove(this.el.firstChild); // remove link + this.hideBottomPage(); + this.page--; + this.prepend(this.renderPage(this.page - 1)); // previous page is offset by one + if (this.page >= 3) { this.prepend(this.renderPrevLink(this.page - 1)); } + } + + paginateTo(object) { + const index = this.data.indexOf(object); + if (index >= PER_PAGE) { + for (let i = 0, end = Math.floor(index / PER_PAGE), asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) { this.paginateNext(); } + } + } + + hideTopPage() { + const n = this.page <= 2 ? + PER_PAGE + : + PER_PAGE + 1; // remove link + for (let i = 0, end = n, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) { this.remove(this.el.firstChild); } + this.prepend(this.renderPrevLink(this.page)); + } + + hideBottomPage() { + const n = this.page === this.totalPages() ? + (this.data.length % PER_PAGE) || PER_PAGE + : + PER_PAGE + 1; // remove link + for (let i = 0, end = n, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) { this.remove(this.el.lastChild); } + this.append(this.renderNextLink(this.page - 1)); + } + + onClick(event) { + const target = $.eventTarget(event); + if (target.tagName === 'SPAN') { // link + $.stopEvent(event); + this.paginate(target); + } + } + }); + Cls.initClass(); + return Cls; +})(); diff --git a/assets/javascripts/views/misc/news.js b/assets/javascripts/views/misc/news.js index a39fbb15cb..2aec85537a 100644 --- a/assets/javascripts/views/misc/news.js +++ b/assets/javascripts/views/misc/news.js @@ -1,34 +1,55 @@ -#= require views/misc/notif - -class app.views.News extends app.views.Notif - @className += ' _notif-news' - - @defautOptions: - autoHide: 30000 - - init: -> - @unreadNews = @getUnreadNews() - @show() if @unreadNews.length - @markAllAsRead() - return - - render: -> - @html app.templates.notifNews(@unreadNews) - return - - getUnreadNews: -> - return [] unless time = @getLastReadTime() - - for news in app.news - break if new Date(news[0]).getTime() <= time - news - - getLastNewsTime: -> - new Date(app.news[0][0]).getTime() - - getLastReadTime: -> - app.settings.get 'news' - - markAllAsRead: -> - app.settings.set 'news', @getLastNewsTime() - return +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +//= require views/misc/notif + +const Cls = (app.views.News = class News extends app.views.Notif { + static initClass() { + this.className += ' _notif-news'; + + this.defautOptions = + {autoHide: 30000}; + } + + init() { + this.unreadNews = this.getUnreadNews(); + if (this.unreadNews.length) { this.show(); } + this.markAllAsRead(); + } + + render() { + this.html(app.templates.notifNews(this.unreadNews)); + } + + getUnreadNews() { + let time; + if (!(time = this.getLastReadTime())) { return []; } + + return (() => { + const result = []; + for (var news of Array.from(app.news)) { + if (new Date(news[0]).getTime() <= time) { break; } + result.push(news); + } + return result; + })(); + } + + getLastNewsTime() { + return new Date(app.news[0][0]).getTime(); + } + + getLastReadTime() { + return app.settings.get('news'); + } + + markAllAsRead() { + app.settings.set('news', this.getLastNewsTime()); + } +}); +Cls.initClass(); diff --git a/assets/javascripts/views/misc/notice.js b/assets/javascripts/views/misc/notice.js index 2007930ed4..e3b66a607b 100644 --- a/assets/javascripts/views/misc/notice.js +++ b/assets/javascripts/views/misc/notice.js @@ -1,27 +1,38 @@ -class app.views.Notice extends app.View - @className: '_notice' - @attributes: - role: 'alert' +/* + * decaffeinate suggestions: + * DS002: Fix invalid constructor + * DS101: Remove unnecessary use of Array.from + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const Cls = (app.views.Notice = class Notice extends app.View { + static initClass() { + this.className = '_notice'; + this.attributes = + {role: 'alert'}; + } - constructor: (@type, @args...) -> super + constructor(type, ...rest) { this.type = type; [...this.args] = Array.from(rest); super(...arguments); } - init: -> - @activate() - return + init() { + this.activate(); + } - activate: -> - @show() if super - return + activate() { + if (super.activate(...arguments)) { this.show(); } + } - deactivate: -> - @hide() if super - return + deactivate() { + if (super.deactivate(...arguments)) { this.hide(); } + } - show: -> - @html @tmpl("#{@type}Notice", @args...) - @prependTo app.el - return + show() { + this.html(this.tmpl(`${this.type}Notice`, ...Array.from(this.args))); + this.prependTo(app.el); + } - hide: -> - $.remove @el - return + hide() { + $.remove(this.el); + } +}); +Cls.initClass(); diff --git a/assets/javascripts/views/misc/notif.js b/assets/javascripts/views/misc/notif.js index dcf2a051da..48b46a943b 100644 --- a/assets/javascripts/views/misc/notif.js +++ b/assets/javascripts/views/misc/notif.js @@ -1,59 +1,78 @@ -class app.views.Notif extends app.View - @className: '_notif' - @activeClass: '_in' - @attributes: - role: 'alert' +/* + * decaffeinate suggestions: + * DS002: Fix invalid constructor + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const Cls = (app.views.Notif = class Notif extends app.View { + static initClass() { + this.className = '_notif'; + this.activeClass = '_in'; + this.attributes = + {role: 'alert'}; + + this.defautOptions = + {autoHide: 15000}; + + this.events = + {click: 'onClick'}; + } - @defautOptions: - autoHide: 15000 + constructor(type, options) { + this.onClick = this.onClick.bind(this); + this.type = type; + if (options == null) { options = {}; } + this.options = options; + this.options = $.extend({}, this.constructor.defautOptions, this.options); + super(...arguments); + } - @events: - click: 'onClick' + init() { + this.show(); + } - constructor: (@type, @options = {}) -> - @options = $.extend {}, @constructor.defautOptions, @options - super + show() { + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = this.delay(this.hide, this.options.autoHide); + } else { + this.render(); + this.position(); + this.activate(); + this.appendTo(document.body); + this.el.offsetWidth; // force reflow + this.addClass(this.constructor.activeClass); + if (this.options.autoHide) { this.timeout = this.delay(this.hide, this.options.autoHide); } + } + } - init: -> - @show() - return + hide() { + clearTimeout(this.timeout); + this.timeout = null; + this.detach(); + } - show: -> - if @timeout - clearTimeout @timeout - @timeout = @delay @hide, @options.autoHide - else - @render() - @position() - @activate() - @appendTo document.body - @el.offsetWidth # force reflow - @addClass @constructor.activeClass - @timeout = @delay @hide, @options.autoHide if @options.autoHide - return + render() { + this.html(this.tmpl(`notif${this.type}`)); + } - hide: -> - clearTimeout @timeout - @timeout = null - @detach() - return + position() { + const notifications = $$(`.${app.views.Notif.className}`); + if (notifications.length) { + const lastNotif = notifications[notifications.length - 1]; + this.el.style.top = lastNotif.offsetTop + lastNotif.offsetHeight + 16 + 'px'; + } + } - render: -> - @html @tmpl("notif#{@type}") - return - - position: -> - notifications = $$ ".#{app.views.Notif.className}" - if notifications.length - lastNotif = notifications[notifications.length - 1] - @el.style.top = lastNotif.offsetTop + lastNotif.offsetHeight + 16 + 'px' - return - - onClick: (event) => - return if event.which isnt 1 - target = $.eventTarget(event) - return if target.hasAttribute('data-behavior') - if target.tagName isnt 'A' or target.classList.contains('_notif-close') - $.stopEvent(event) - @hide() - return + onClick(event) { + if (event.which !== 1) { return; } + const target = $.eventTarget(event); + if (target.hasAttribute('data-behavior')) { return; } + if ((target.tagName !== 'A') || target.classList.contains('_notif-close')) { + $.stopEvent(event); + this.hide(); + } + } +}); +Cls.initClass(); diff --git a/assets/javascripts/views/misc/tip.js b/assets/javascripts/views/misc/tip.js index 6fec52a2f4..9c2dd0b4d2 100644 --- a/assets/javascripts/views/misc/tip.js +++ b/assets/javascripts/views/misc/tip.js @@ -1,11 +1,20 @@ -#= require views/misc/notif +/* + * decaffeinate suggestions: + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +//= require views/misc/notif -class app.views.Tip extends app.views.Notif - @className: '_notif _notif-tip' +const Cls = (app.views.Tip = class Tip extends app.views.Notif { + static initClass() { + this.className = '_notif _notif-tip'; + + this.defautOptions = + {autoHide: false}; + } - @defautOptions: - autoHide: false - - render: -> - @html @tmpl("tip#{@type}") - return + render() { + this.html(this.tmpl(`tip${this.type}`)); + } +}); +Cls.initClass(); diff --git a/assets/javascripts/views/misc/updates.js b/assets/javascripts/views/misc/updates.js index 8b91ccfd98..c41f88dac5 100644 --- a/assets/javascripts/views/misc/updates.js +++ b/assets/javascripts/views/misc/updates.js @@ -1,34 +1,56 @@ -#= require views/misc/notif - -class app.views.Updates extends app.views.Notif - @className += ' _notif-news' - - @defautOptions: - autoHide: 30000 - - init: -> - @lastUpdateTime = @getLastUpdateTime() - @updatedDocs = @getUpdatedDocs() - @updatedDisabledDocs = @getUpdatedDisabledDocs() - @show() if @updatedDocs.length > 0 or @updatedDisabledDocs.length > 0 - @markAllAsRead() - return - - render: -> - @html app.templates.notifUpdates(@updatedDocs, @updatedDisabledDocs) - return - - getUpdatedDocs: -> - return [] unless @lastUpdateTime - doc for doc in app.docs.all() when doc.mtime > @lastUpdateTime - - getUpdatedDisabledDocs: -> - return [] unless @lastUpdateTime - doc for doc in app.disabledDocs.all() when doc.mtime > @lastUpdateTime and app.docs.findBy('slug_without_version', doc.slug_without_version) - - getLastUpdateTime: -> - app.settings.get 'version' - - markAllAsRead: -> - app.settings.set 'version', if app.config.env is 'production' then app.config.version else Math.floor(Date.now() / 1000) - return +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +//= require views/misc/notif + +const Cls = (app.views.Updates = class Updates extends app.views.Notif { + static initClass() { + this.className += ' _notif-news'; + + this.defautOptions = + {autoHide: 30000}; + } + + init() { + this.lastUpdateTime = this.getLastUpdateTime(); + this.updatedDocs = this.getUpdatedDocs(); + this.updatedDisabledDocs = this.getUpdatedDisabledDocs(); + if ((this.updatedDocs.length > 0) || (this.updatedDisabledDocs.length > 0)) { this.show(); } + this.markAllAsRead(); + } + + render() { + this.html(app.templates.notifUpdates(this.updatedDocs, this.updatedDisabledDocs)); + } + + getUpdatedDocs() { + if (!this.lastUpdateTime) { return []; } + return Array.from(app.docs.all()).filter((doc) => doc.mtime > this.lastUpdateTime); + } + + getUpdatedDisabledDocs() { + if (!this.lastUpdateTime) { return []; } + return (() => { + const result = []; + for (var doc of Array.from(app.disabledDocs.all())) { if ((doc.mtime > this.lastUpdateTime) && app.docs.findBy('slug_without_version', doc.slug_without_version)) { + result.push(doc); + } + } + return result; + })(); + } + + getLastUpdateTime() { + return app.settings.get('version'); + } + + markAllAsRead() { + app.settings.set('version', app.config.env === 'production' ? app.config.version : Math.floor(Date.now() / 1000)); + } +}); +Cls.initClass(); diff --git a/assets/javascripts/views/pages/base.js b/assets/javascripts/views/pages/base.js index e1c0b6a49e..a36a9d3a4e 100644 --- a/assets/javascripts/views/pages/base.js +++ b/assets/javascripts/views/pages/base.js @@ -1,43 +1,61 @@ -class app.views.BasePage extends app.View - constructor: (@el, @entry) -> super +/* + * decaffeinate suggestions: + * DS002: Fix invalid constructor + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +app.views.BasePage = class BasePage extends app.View { + constructor(el, entry) { this.paintCode = this.paintCode.bind(this); this.el = el; this.entry = entry; super(...arguments); } - deactivate: -> - if super - @highlightNodes = [] + deactivate() { + if (super.deactivate(...arguments)) { + return this.highlightNodes = []; + } + } - render: (content, fromCache = false) -> - @highlightNodes = [] - @previousTiming = null - @addClass "_#{@entry.doc.type}" unless @constructor.className - @html content - @highlightCode() unless fromCache - @activate() - @delay @afterRender if @afterRender - if @highlightNodes.length > 0 - $.requestAnimationFrame => $.requestAnimationFrame(@paintCode) - return + render(content, fromCache) { + if (fromCache == null) { fromCache = false; } + this.highlightNodes = []; + this.previousTiming = null; + if (!this.constructor.className) { this.addClass(`_${this.entry.doc.type}`); } + this.html(content); + if (!fromCache) { this.highlightCode(); } + this.activate(); + if (this.afterRender) { this.delay(this.afterRender); } + if (this.highlightNodes.length > 0) { + $.requestAnimationFrame(() => $.requestAnimationFrame(this.paintCode)); + } + } - highlightCode: -> - for el in @findAll('pre[data-language]') - language = el.getAttribute('data-language') - el.classList.add("language-#{language}") - @highlightNodes.push(el) - return + highlightCode() { + for (var el of Array.from(this.findAll('pre[data-language]'))) { + var language = el.getAttribute('data-language'); + el.classList.add(`language-${language}`); + this.highlightNodes.push(el); + } + } - paintCode: (timing) => - if @previousTiming - if Math.round(1000 / (timing - @previousTiming)) > 50 # fps - @nodesPerFrame = Math.round(Math.min(@nodesPerFrame * 1.25, 50)) - else - @nodesPerFrame = Math.round(Math.max(@nodesPerFrame * .8, 10)) - else - @nodesPerFrame = 10 + paintCode(timing) { + if (this.previousTiming) { + if (Math.round(1000 / (timing - this.previousTiming)) > 50) { // fps + this.nodesPerFrame = Math.round(Math.min(this.nodesPerFrame * 1.25, 50)); + } else { + this.nodesPerFrame = Math.round(Math.max(this.nodesPerFrame * .8, 10)); + } + } else { + this.nodesPerFrame = 10; + } - for el in @highlightNodes.splice(0, @nodesPerFrame) - $.remove(clipEl) if clipEl = el.lastElementChild - Prism.highlightElement(el) - $.append(el, clipEl) if clipEl + for (var el of Array.from(this.highlightNodes.splice(0, this.nodesPerFrame))) { + var clipEl; + if (clipEl = el.lastElementChild) { $.remove(clipEl); } + Prism.highlightElement(el); + if (clipEl) { $.append(el, clipEl); } + } - $.requestAnimationFrame(@paintCode) if @highlightNodes.length > 0 - @previousTiming = timing - return + if (this.highlightNodes.length > 0) { $.requestAnimationFrame(this.paintCode); } + this.previousTiming = timing; + } +}; diff --git a/assets/javascripts/views/pages/hidden.js b/assets/javascripts/views/pages/hidden.js index f17080a06b..d6e236c75f 100644 --- a/assets/javascripts/views/pages/hidden.js +++ b/assets/javascripts/views/pages/hidden.js @@ -1,16 +1,28 @@ -class app.views.HiddenPage extends app.View - @events: - click: 'onClick' +/* + * decaffeinate suggestions: + * DS002: Fix invalid constructor + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const Cls = (app.views.HiddenPage = class HiddenPage extends app.View { + static initClass() { + this.events = + {click: 'onClick'}; + } - constructor: (@el, @entry) -> super + constructor(el, entry) { this.onClick = this.onClick.bind(this); this.el = el; this.entry = entry; super(...arguments); } - init: -> - @addSubview @notice = new app.views.Notice 'disabledDoc' - @activate() - return + init() { + this.addSubview(this.notice = new app.views.Notice('disabledDoc')); + this.activate(); + } - onClick: (event) => - if link = $.closestLink(event.target, @el) - $.stopEvent(event) - $.popup(link) - return + onClick(event) { + let link; + if (link = $.closestLink(event.target, this.el)) { + $.stopEvent(event); + $.popup(link); + } + } +}); +Cls.initClass(); diff --git a/assets/javascripts/views/pages/jquery.js b/assets/javascripts/views/pages/jquery.js index 47f021959f..d97ba502b3 100644 --- a/assets/javascripts/views/pages/jquery.js +++ b/assets/javascripts/views/pages/jquery.js @@ -1,57 +1,81 @@ -#= require views/pages/base - -class app.views.JqueryPage extends app.views.BasePage - @demoClassName: '_jquery-demo' - - afterRender: -> - # Prevent jQuery Mobile's demo iframes from scrolling the page - for iframe in @findAllByTag 'iframe' - iframe.style.display = 'none' - $.on iframe, 'load', @onIframeLoaded - - @runExamples() - - onIframeLoaded: (event) => - event.target.style.display = '' - $.off event.target, 'load', @onIframeLoaded - return - - runExamples: -> - for el in @findAllByClass 'entry-example' - try @runExample el catch - return - - runExample: (el) -> - source = el.getElementsByClassName('syntaxhighlighter')[0] - return unless source and source.innerHTML.indexOf('!doctype') isnt -1 - - unless iframe = el.getElementsByClassName(@constructor.demoClassName)[0] - iframe = document.createElement 'iframe' - iframe.className = @constructor.demoClassName - iframe.width = '100%' - iframe.height = 200 - el.appendChild(iframe) - - doc = iframe.contentDocument - doc.write @fixIframeSource(source.textContent) - doc.close() - return - - fixIframeSource: (source) -> - source = source.replace '"/resources/', '"https://api.jquery.com/resources/' # attr(), keydown() - source = source.replace '', """ - - - - """ - source.replace / +\ +` + ); + return source.replace(/ \ -` +`, ); return source.replace(/