diff --git a/.github/workflows/build.yml b/.github/workflows/deploy-heroku.yml similarity index 57% rename from .github/workflows/build.yml rename to .github/workflows/deploy-heroku.yml index e83dfeb3df..bf0ac69568 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/deploy-heroku.yml @@ -12,8 +12,10 @@ jobs: if: github.repository == 'freeCodeCamp/devdocs' steps: - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0 + with: + fetch-depth: 0 # Heroku rejects shallow pushes - name: Set up Ruby - uses: ruby/setup-ruby@8aeb6ff8030dd539317f8e1769a044873b56ea71 # v1.268.0 + uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1.310.0 with: bundler-cache: true # runs 'bundle install' and caches installed gems automatically - name: Run tests @@ -22,10 +24,11 @@ jobs: run: | curl https://cli-assets.heroku.com/install.sh | sh - name: Deploy to Heroku - uses: akhileshns/heroku-deploy@e3eb99d45a8e2ec5dca08735e089607befa4bf28 # v3.14.15 - with: - heroku_api_key: ${{secrets.HEROKU_API_KEY}} - heroku_app_name: "devdocs" - heroku_email: "team@freecodecamp.com" - dontuseforce: true # --force should never be necessary - dontautocreate: true # The app exists, it should not be created + env: + HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} + run: | + heroku --version + heroku stack:set heroku-26 --app devdocs + heroku git:remote --app devdocs + # no --force; --force should never be necessary + git push heroku HEAD:main diff --git a/.github/workflows/schedule-doc-report.yml b/.github/workflows/schedule-doc-report.yml index f5d1eb1213..528c4d66ca 100644 --- a/.github/workflows/schedule-doc-report.yml +++ b/.github/workflows/schedule-doc-report.yml @@ -11,7 +11,7 @@ jobs: steps: - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0 - name: Set up Ruby - uses: ruby/setup-ruby@8aeb6ff8030dd539317f8e1769a044873b56ea71 # v1.268.0 + uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1.310.0 with: bundler-cache: true # runs 'bundle install' and caches installed gems automatically - name: Generate report diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 953d3e0256..dd8b0c94b1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Set up Ruby - uses: ruby/setup-ruby@8aeb6ff8030dd539317f8e1769a044873b56ea71 # v1.268.0 + uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1.310.0 with: bundler-cache: true # runs 'bundle install' and caches installed gems automatically - name: Run tests diff --git a/.gitignore b/.gitignore index bbf749a452..aac9f85ba5 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,10 @@ public/docs/**/* docs/**/* !docs/*.md /vendor +/coverage *.tar *.tar.bz2 *.tar.gz *.zip +assets/stylesheets/components/_environment.scss +assets/stylesheets/global/_icons.scss diff --git a/.ruby-version b/.ruby-version index 2aa5131992..7636e75650 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.4.7 +4.0.5 diff --git a/.tool-versions b/.tool-versions index 3f03c7a73d..aac7389b82 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -ruby 3.4.7 +ruby 4.0.5 diff --git a/COPYRIGHT b/COPYRIGHT index 374054bdbf..ad89869379 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -1,4 +1,4 @@ -Copyright 2013-2025 Thibaut Courouble and other contributors +Copyright 2013-2026 Thibaut Courouble and other contributors This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/Dockerfile b/Dockerfile index 97e72162f6..d4ea7534fc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:3.4.7 +FROM ruby:4.0.5 ENV LANG=C.UTF-8 ENV ENABLE_SERVICE_WORKER=true @@ -11,7 +11,8 @@ RUN apt-get update && \ COPY Gemfile Gemfile.lock Rakefile /devdocs/ -RUN bundle install --system && \ +RUN bundle config set path.system true && \ + bundle install && \ rm -rf ~/.gem /root/.bundle/cache /usr/local/bundle/cache COPY . /devdocs diff --git a/Dockerfile-alpine b/Dockerfile-alpine index 5f72bad5c9..c09cb3a249 100644 --- a/Dockerfile-alpine +++ b/Dockerfile-alpine @@ -1,4 +1,4 @@ -FROM ruby:3.4.7-alpine +FROM ruby:4.0.5-alpine ENV LANG=C.UTF-8 ENV ENABLE_SERVICE_WORKER=true @@ -9,7 +9,9 @@ COPY . /devdocs RUN apk --update add nodejs build-base libstdc++ gzip git zlib-dev libcurl && \ gem install bundler && \ - bundle install --system --without test && \ + bundle config set path.system true && \ + bundle config set without 'test' && \ + bundle install && \ thor docs:download --all && \ thor assets:compile && \ apk del gzip build-base git zlib-dev && \ diff --git a/Gemfile b/Gemfile index 9893bb5774..c4d7aaa761 100644 --- a/Gemfile +++ b/Gemfile @@ -1,8 +1,9 @@ source 'https://rubygems.org' -ruby '3.4.7' +ruby '4.0.5' gem 'activesupport', require: false -gem 'html-pipeline' +gem 'html-pipeline', '~> 2.14' +gem 'ostruct' gem 'nokogiri' gem 'pry-byebug' gem 'rake' @@ -15,16 +16,15 @@ group :app do gem 'browser' gem 'chunky_png' gem 'erubi' + gem 'dartsass-sprockets' gem 'image_optim_pack', platforms: :ruby gem 'image_optim' gem 'rack-ssl-enforcer' gem 'rack' gem 'rss' - gem 'sass' gem 'sinatra-contrib' gem 'sinatra' gem 'sprockets-helpers' - gem 'sprockets-sass' gem 'sprockets' gem 'thin' end @@ -49,9 +49,12 @@ group :test do gem 'minitest' gem 'rack-test', require: false gem 'rr', require: false + gem 'simplecov', require: false end if ENV['SELENIUM'] == '1' gem 'capybara' gem 'selenium-webdriver' end + +gem "webrick", "~> 1.9" diff --git a/Gemfile.lock b/Gemfile.lock index 066795a2e8..4274379693 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,171 +1,345 @@ GEM remote: https://rubygems.org/ specs: - activesupport (7.2.3) + actionpack (8.1.3) + actionview (= 8.1.3) + activesupport (= 8.1.3) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actionview (8.1.3) + activesupport (= 8.1.3) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activesupport (8.1.3) base64 - benchmark (>= 0.3) bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + json logger (>= 1.4.2) minitest (>= 5.1) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) - base64 (0.2.0) - benchmark (0.3.0) + uri (>= 0.13.1) + base64 (0.3.0) better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) - bigdecimal (3.1.9) - browser (5.3.1) - byebug (12.0.0) + bigdecimal (4.1.2) + browser (6.2.0) + builder (3.3.0) + byebug (13.0.0) + reline (>= 0.6.0) chunky_png (1.4.0) coderay (1.1.3) - concurrent-ruby (1.3.5) - connection_pool (2.4.1) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) + crass (1.0.6) daemons (1.4.1) + dartsass-sprockets (3.2.1) + railties (>= 4.0.0) + sassc-embedded (~> 1.80.1) + sprockets (> 3.0) + sprockets-rails + tilt + date (3.5.1) + docile (1.4.1) drb (2.2.3) + erb (6.0.4) erubi (1.13.1) - ethon (0.17.0) + ethon (0.18.0) ffi (>= 1.15.0) + logger eventmachine (1.2.7) - execjs (2.9.1) - exifr (1.4.0) - ffi (1.17.2) + execjs (2.10.1) + exifr (1.5.1) + ffi (1.17.4-aarch64-linux-gnu) + ffi (1.17.4-aarch64-linux-musl) + ffi (1.17.4-arm-linux-gnu) + ffi (1.17.4-arm-linux-musl) + ffi (1.17.4-arm64-darwin) + ffi (1.17.4-x86_64-darwin) + ffi (1.17.4-x86_64-linux-gnu) + ffi (1.17.4-x86_64-linux-musl) fspath (3.1.2) + google-protobuf (4.34.1) + bigdecimal + rake (~> 13.3) + google-protobuf (4.34.1-aarch64-linux-gnu) + bigdecimal + rake (~> 13.3) + google-protobuf (4.34.1-aarch64-linux-musl) + bigdecimal + rake (~> 13.3) + google-protobuf (4.34.1-arm64-darwin) + bigdecimal + rake (~> 13.3) + google-protobuf (4.34.1-x86_64-darwin) + bigdecimal + rake (~> 13.3) + google-protobuf (4.34.1-x86_64-linux-gnu) + bigdecimal + rake (~> 13.3) + google-protobuf (4.34.1-x86_64-linux-musl) + bigdecimal + rake (~> 13.3) highline (3.1.2) reline html-pipeline (2.14.3) activesupport (>= 2) nokogiri (>= 1.4) - i18n (1.14.7) + i18n (1.14.8) concurrent-ruby (~> 1.0) - image_optim (0.31.3) + image_optim (0.31.4) exifr (~> 1.2, >= 1.2.2) fspath (~> 3.0) image_size (>= 1.5, < 4) in_threads (~> 1.3) progress (~> 3.0, >= 3.0.1) - image_optim_pack (0.10.1) + image_optim_pack (0.13.1.20260418) + fspath (>= 2.1, < 4) + image_optim (~> 0.19) + image_optim_pack (0.13.1.20260418-aarch64-linux) + fspath (>= 2.1, < 4) + image_optim (~> 0.19) + image_optim_pack (0.13.1.20260418-aarch64-linux-gnu) + fspath (>= 2.1, < 4) + image_optim (~> 0.19) + image_optim_pack (0.13.1.20260418-aarch64-linux-musl) + fspath (>= 2.1, < 4) + image_optim (~> 0.19) + image_optim_pack (0.13.1.20260418-arm64-darwin) fspath (>= 2.1, < 4) image_optim (~> 0.19) - image_size (3.3.0) + image_optim_pack (0.13.1.20260418-x86_64-darwin) + fspath (>= 2.1, < 4) + image_optim (~> 0.19) + image_optim_pack (0.13.1.20260418-x86_64-linux-gnu) + fspath (>= 2.1, < 4) + image_optim (~> 0.19) + image_optim_pack (0.13.1.20260418-x86_64-linux-musl) + fspath (>= 2.1, < 4) + image_optim (~> 0.19) + image_size (3.4.0) in_threads (1.6.0) - io-console (0.8.0) - logger (1.6.6) + io-console (0.8.2) + irb (1.18.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + json (2.19.4) + logger (1.7.0) + loofah (2.25.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) method_source (1.1.0) - mini_portile2 (2.8.9) - minitest (5.26.2) - multi_json (1.15.0) - mustermann (3.0.3) - ruby2_keywords (~> 0.0.1) - newrelic_rpm (8.16.0) - nokogiri (1.18.10) - mini_portile2 (~> 2.8.2) + minitest (6.0.5) + drb (~> 2.0) + prism (~> 1.5) + multi_json (1.20.1) + mustermann (3.1.1) + newrelic_rpm (10.4.0) + logger + nokogiri (1.19.2-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.2-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.19.2-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.2-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.19.2-arm64-darwin) + racc (~> 1.4) + nokogiri (1.19.2-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.19.2-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.2-x86_64-linux-musl) racc (~> 1.4) options (2.3.2) + ostruct (0.6.3) + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) progress (3.6.0) progress_bar (1.3.4) highline (>= 1.6) options (~> 2.3.0) - pry (0.15.2) + pry (0.16.0) coderay (~> 1.1) method_source (~> 1.0) - pry-byebug (3.11.0) - byebug (~> 12.0) - pry (>= 0.13, < 0.16) + reline (>= 0.6.0) + pry-byebug (3.12.0) + byebug (~> 13.0) + pry (>= 0.13, < 0.17) + psych (5.3.1) + date + stringio racc (1.8.1) - rack (2.2.21) - rack-protection (3.2.0) + rack (3.2.6) + rack-protection (4.2.1) base64 (>= 0.1.0) - rack (~> 2.2, >= 2.2.4) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) + rack-session (2.1.2) + base64 (>= 0.1.0) + rack (>= 3.0.0) rack-ssl-enforcer (0.2.9) rack-test (2.2.0) rack (>= 1.3) - rake (13.3.1) - rb-fsevent (0.11.2) - rb-inotify (0.10.1) - ffi (~> 1.0) + rackup (2.3.1) + rack (>= 3) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.7.0) + loofah (~> 2.25) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + rake (13.4.2) + rdoc (7.2.0) + erb + psych (>= 4.0.0) + tsort redcarpet (3.6.1) - reline (0.6.0) + reline (0.6.3) io-console (~> 0.5) - rexml (3.3.9) - rouge (1.11.1) + rexml (3.4.4) + rouge (4.7.0) rr (3.1.2) - rss (0.3.1) + rss (0.3.2) rexml - ruby2_keywords (0.0.5) - sass (3.7.4) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - securerandom (0.3.2) - sinatra (3.2.0) + sass-embedded (1.99.0-aarch64-linux-gnu) + google-protobuf (~> 4.31) + sass-embedded (1.99.0-aarch64-linux-musl) + google-protobuf (~> 4.31) + sass-embedded (1.99.0-arm-linux-gnueabihf) + google-protobuf (~> 4.31) + sass-embedded (1.99.0-arm-linux-musleabihf) + google-protobuf (~> 4.31) + sass-embedded (1.99.0-arm64-darwin) + google-protobuf (~> 4.31) + sass-embedded (1.99.0-x86_64-darwin) + google-protobuf (~> 4.31) + sass-embedded (1.99.0-x86_64-linux-gnu) + google-protobuf (~> 4.31) + sass-embedded (1.99.0-x86_64-linux-musl) + google-protobuf (~> 4.31) + sassc-embedded (1.80.8) + sass-embedded (~> 1.80) + securerandom (0.4.1) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.13.2) + simplecov_json_formatter (0.1.4) + sinatra (4.2.1) + logger (>= 1.6.0) mustermann (~> 3.0) - rack (~> 2.2, >= 2.2.4) - rack-protection (= 3.2.0) + rack (>= 3.0.0, < 4) + rack-protection (= 4.2.1) + rack-session (>= 2.0.0, < 3) tilt (~> 2.0) - sinatra-contrib (3.2.0) + sinatra-contrib (4.2.1) multi_json (>= 0.0.2) mustermann (~> 3.0) - rack-protection (= 3.2.0) - sinatra (= 3.2.0) + rack-protection (= 4.2.1) + sinatra (= 4.2.1) tilt (~> 2.0) - sprockets (3.7.5) - base64 + sprockets (4.2.2) concurrent-ruby (~> 1.0) - rack (> 1, < 3) + logger + rack (>= 2.2.4, < 4) sprockets-helpers (1.4.0) sprockets (>= 2.2) - sprockets-sass (2.0.0.beta2) - sprockets (>= 2.0, < 4.0) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) + sprockets (>= 3.0.0) + stringio (3.2.0) strings (0.2.1) strings-ansi (~> 0.2) unicode-display_width (>= 1.5, < 3.0) unicode_utils (~> 1.4) strings-ansi (0.2.0) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) - terser (1.2.6) + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) + terser (1.2.7) execjs (>= 0.3.0, < 3) - thin (1.8.2) + thin (2.0.1) daemons (~> 1.0, >= 1.0.9) eventmachine (~> 1.0, >= 1.0.4) - rack (>= 1, < 3) - thor (1.4.0) - tilt (2.6.0) + logger + rack (>= 1, < 4) + thor (1.5.0) + tilt (2.7.0) + tsort (0.2.0) tty-pager (0.14.0) strings (~> 0.2.0) tty-screen (~> 0.8) - tty-screen (0.8.1) - typhoeus (1.4.1) - ethon (>= 0.9.0) + tty-screen (0.8.2) + typhoeus (1.6.0) + ethon (>= 0.18.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (2.3.0) + unicode-display_width (2.6.0) unicode_utils (1.4.0) unix_utils (0.0.15) + uri (1.1.1) + useragent (0.16.11) + webrick (1.9.2) yajl-ruby (1.4.3) + zeitwerk (2.7.5) PLATFORMS - ruby + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-gnueabihf + arm-linux-musl + arm-linux-musleabihf + arm64-darwin + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl DEPENDENCIES activesupport better_errors browser chunky_png + dartsass-sprockets erubi - html-pipeline + html-pipeline (~> 2.14) image_optim image_optim_pack minitest newrelic_rpm nokogiri + ostruct progress_bar pry-byebug rack @@ -175,12 +349,11 @@ DEPENDENCIES redcarpet rr rss - sass + simplecov sinatra sinatra-contrib sprockets sprockets-helpers - sprockets-sass terminal-table terser thin @@ -188,10 +361,153 @@ DEPENDENCIES tty-pager typhoeus unix_utils + webrick (~> 1.9) yajl-ruby +CHECKSUMS + actionpack (8.1.3) sha256=af998cae4d47c5d581a2cc363b5c77eb718b7c4b45748d81b1887b25621c29a3 + actionview (8.1.3) sha256=1347c88c7f3edb38100c5ce0e9fb5e62d7755f3edc1b61cce2eb0b2c6ea2fd5d + activesupport (8.1.3) sha256=21a5e0dfbd4c3ddd9e1317ec6a4d782fa226e7867dc70b0743acda81a1dca20e + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + better_errors (2.10.1) sha256=f798f1bac93f3e775925b7fcb24cffbcf0bb62ee2210f5350f161a6b75fc0a73 + bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd + browser (6.2.0) sha256=281d5295788825c9396427c292c2d2be0a5c91875c93c390fde6e5d61a5ace2d + builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f + byebug (13.0.0) sha256=d2263efe751941ca520fa29744b71972d39cbc41839496706f5d9b22e92ae05d + chunky_png (1.4.0) sha256=89d5b31b55c0cf4da3cf89a2b4ebc3178d8abe8cbaf116a1dba95668502fdcfe + coderay (1.1.3) sha256=dc530018a4684512f8f38143cd2a096c9f02a1fc2459edcfe534787a7fc77d4b + concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab + connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a + crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d + daemons (1.4.1) sha256=8fc76d76faec669feb5e455d72f35bd4c46dc6735e28c420afb822fac1fa9a1d + dartsass-sprockets (3.2.1) sha256=5163c59d155bfb834e2554bec00e2ff9ab97ad3ea82545cbacf650cf0469f3c3 + date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 + docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e + drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 + erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9 + erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 + ethon (0.18.0) sha256=b598afc9f30448cb068b850714b7d6948e941476095d04f90a4ac65b8d6efcb2 + eventmachine (1.2.7) sha256=994016e42aa041477ba9cff45cbe50de2047f25dd418eba003e84f0d16560972 + execjs (2.10.1) sha256=abe0ae028467eb8e30c10814eb934d07876a691aae7e803d813b7ce5a75e73f1 + exifr (1.5.1) sha256=ad87a5dbc92946fbf1d8ccd178d557d95d9168e8b3c5c5f381ea2bffcc312521 + ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df + ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39 + ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564 + ffi (1.17.4-arm-linux-musl) sha256=9d4838ded0465bef6e2426935f6bcc93134b6616785a84ffd2a3d82bc3cf6f95 + ffi (1.17.4-arm64-darwin) sha256=19071aaf1419251b0a46852abf960e77330a3b334d13a4ab51d58b31a937001b + ffi (1.17.4-x86_64-darwin) sha256=aa70390523cf3235096cf64962b709b4cfbd5c082a2cb2ae714eb0fe2ccda496 + ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d + ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e + fspath (3.1.2) sha256=b5ac9bafb97e2c8f8f9e303cd98ebd484be76fe9aa588bc4d01c6d99e78c9d75 + google-protobuf (4.34.1) sha256=347181542b8d659c60f028fa3791c9cccce651a91ad27782dbc5c5e374796cdc + google-protobuf (4.34.1-aarch64-linux-gnu) sha256=f9c07607dc139c895f2792a7740fcd01cd94d4d7b0e0a939045b50d7999f0b1d + google-protobuf (4.34.1-aarch64-linux-musl) sha256=db58e5a4a492b43c6614486aea31b7fb86955b175d1d48f28ebf388f058d78a9 + google-protobuf (4.34.1-arm64-darwin) sha256=2745061f973119e6e7f3c81a0c77025d291a3caa6585a2cd24a25bbc7bedb267 + google-protobuf (4.34.1-x86_64-darwin) sha256=4dc498376e218871613589c4d872400d42ad9ae0c700bdb2606fe1c77a593075 + google-protobuf (4.34.1-x86_64-linux-gnu) sha256=87088c9fd8e47b5b40ca498fc1195add6149e941ff7e81c532a5b0b8876d4cc9 + google-protobuf (4.34.1-x86_64-linux-musl) sha256=8c0e91436fbe504ffc64f0bd621f2e69adbcce8ed2c58439d7a21117069cfdd7 + highline (3.1.2) sha256=67cbd34d19f6ef11a7ee1d82ffab5d36dfd5b3be861f450fc1716c7125f4bb4a + html-pipeline (2.14.3) sha256=8a1d4d7128b2141913387cac0f8ba898bb6812557001acc0c2b46910f59413a0 + i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5 + image_optim (0.31.4) sha256=5bffd0891268e8d189d46ee4b8599918581bba1032d244e6ace4779a434776c0 + image_optim_pack (0.13.1.20260418) sha256=ee0f6a34f3d37b8553c6289d0a1beb7110c0c2e665068585c0e070c637755db7 + image_optim_pack (0.13.1.20260418-aarch64-linux) sha256=126251ce06869799163cb454b37fbbdd2f8336120dc7c6d6c55821d4e1e8956f + image_optim_pack (0.13.1.20260418-aarch64-linux-gnu) sha256=cd453d28bd2d6b02aded04c04ba046e4b409a12e930ec8f5a87b289b0cce259f + image_optim_pack (0.13.1.20260418-aarch64-linux-musl) sha256=832011b9e266b65ddef5b6a9ce3f4625775f62c8cc6bb3e05703d18a4df8a25b + image_optim_pack (0.13.1.20260418-arm64-darwin) sha256=f491b719c6940d8b89c4fed33d434afae516f6b75803d73b33d32fc482107ece + image_optim_pack (0.13.1.20260418-x86_64-darwin) sha256=dc869667175e05c5d5a242ebe50f06437da3ab0ff4a89045cce46045bd673f95 + image_optim_pack (0.13.1.20260418-x86_64-linux-gnu) sha256=21892f6ec63f386a7a63c920a440a13034d23620b74cd89badd22acff1e4aab5 + image_optim_pack (0.13.1.20260418-x86_64-linux-musl) sha256=a68e1953cf4edcdd3d7590fec1b14bd49e3177e3cb16c344a7c7f5af7b8223c2 + image_size (3.4.0) sha256=c6a580513fe74947e25e5d3f0aea1e33add6c20f7d0007efa65504317b7f029a + in_threads (1.6.0) sha256=91a7e6138d279dc632f59b8a9a409e47148948e297c0f69c92f9a2479a182149 + io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc + irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3 + json (2.19.4) sha256=670a7d333fb3b18ca5b29cb255eb7bef099e40d88c02c80bd42a3f30fe5239ac + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + loofah (2.25.1) sha256=d436c73dbd0c1147b16c4a41db097942d217303e1f7728704b37e4df9f6d2e04 + method_source (1.1.0) sha256=181301c9c45b731b4769bc81e8860e72f9161ad7d66dd99103c9ab84f560f5c5 + minitest (6.0.5) sha256=f007d7246bf4feea549502842cd7c6aba8851cdc9c90ba06de9c476c0d01155c + multi_json (1.20.1) sha256=2f3934e805cc45ef91b551a1f89d0e9191abd06a5e04a2ef09a6a036c452ca6d + mustermann (3.1.1) sha256=4c6170c7234d5499c345562ba7c7dfe73e1754286dcc1abb053064d66a127198 + newrelic_rpm (10.4.0) sha256=36e3bc31adc89c3fe2056d1014a4d93b38bb63050050a1a442b35c4559531291 + nokogiri (1.19.2-aarch64-linux-gnu) sha256=c34d5c8208025587554608e98fd88ab125b29c80f9352b821964e9a5d5cfbd19 + nokogiri (1.19.2-aarch64-linux-musl) sha256=7f6b4b0202d507326841a4f790294bf75098aef50c7173443812e3ac5cb06515 + nokogiri (1.19.2-arm-linux-gnu) sha256=b7fa1139016f3dc850bda1260988f0d749934a939d04ef2da13bec060d7d5081 + nokogiri (1.19.2-arm-linux-musl) sha256=61114d44f6742ff72194a1b3020967201e2eb982814778d130f6471c11f9828c + nokogiri (1.19.2-arm64-darwin) sha256=58d8ea2e31a967b843b70487a44c14c8ba1866daa1b9da9be9dbdf1b43dee205 + nokogiri (1.19.2-x86_64-darwin) sha256=7d9af11fda72dfaa2961d8c4d5380ca0b51bc389dc5f8d4b859b9644f195e7a4 + nokogiri (1.19.2-x86_64-linux-gnu) sha256=fa8feca882b73e871a9845f3817a72e9734c8e974bdc4fbad6e4bc6e8076b94f + nokogiri (1.19.2-x86_64-linux-musl) sha256=93128448e61a9383a30baef041bf1f5817e22f297a1d400521e90294445069a8 + options (2.3.2) sha256=32413a4b9e363234eed2eecfb2a1a9deb32810f72c54820a37a62f65b905c5e8 + ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 + pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6 + prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 + prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 + progress (3.6.0) sha256=360ed306dfa43d6174e847d563c70736dca249e2333cfec4b0387306c86cd573 + progress_bar (1.3.4) sha256=adb10e040275e08eadfbe405749584e4b01fd15e8e692fdcb4b1969e9c071c8c + pry (0.16.0) sha256=d76c69065698ed1f85e717bd33d7942c38a50868f6b0673c636192b3d1b6054e + pry-byebug (3.12.0) sha256=594e094ae8a8390a7ad4c7b36ae36e13304ed02664c67417d108dc5f7213d1b7 + psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974 + racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f + rack (3.2.6) sha256=5ed78e1f73b2e25679bec7d45ee2d4483cc4146eb1be0264fc4d94cb5ef212c2 + rack-protection (4.2.1) sha256=cf6e2842df8c55f5e4d1a4be015e603e19e9bc3a7178bae58949ccbb58558bac + rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8 + rack-ssl-enforcer (0.2.9) sha256=99ef4051d1d1e8ca46bbeecb824506798db03e51db3028cb0acf9c18971b1108 + rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463 + rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868 + rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d + rails-html-sanitizer (1.7.0) sha256=28b145cceaf9cc214a9874feaa183c3acba036c9592b19886e0e45efc62b1e89 + railties (8.1.3) sha256=913eb0e0cb520aac687ffd74916bd726d48fa21f47833c6292576ef6a286de22 + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192 + redcarpet (3.6.1) sha256=d444910e6aa55480c6bcdc0cdb057626e8a32c054c29e793fa642ba2f155f445 + reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835 + rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 + rouge (4.7.0) sha256=dba5896715c0325c362e895460a6d350803dbf6427454f49a47500f3193ea739 + rr (3.1.2) sha256=cc4a5cc91f012327d317dc661154419f9d36d8f9c05031ac46accd89ceb0ff1b + rss (0.3.2) sha256=3bd0446d32d832cda00ba07f4b179401f903b52ea1fdaac0f1f08de61a501efa + sass-embedded (1.99.0-aarch64-linux-gnu) sha256=a46615b0295ca7bd979b9ce79f6b9f1d26881736400188bd6fd5c4b7c9b46473 + sass-embedded (1.99.0-aarch64-linux-musl) sha256=eaa6d56968909d1d54073c46e21c13fd5bbcb5af609d7e4fbe6b196426ca8e49 + sass-embedded (1.99.0-arm-linux-gnueabihf) sha256=f5f0748934660cda6948917af6dc974caf4d36ea6121e450982153b7d9c49b55 + sass-embedded (1.99.0-arm-linux-musleabihf) sha256=29a4056e76bc136025ba5d03e82d86ecdabfb6c07a473a4fdedcd72fb28dbbc4 + sass-embedded (1.99.0-arm64-darwin) sha256=20773f2fb0e8f4d0194eb1874dab4c0ef60262ff6dafe29361e217beb2208629 + sass-embedded (1.99.0-x86_64-darwin) sha256=371774c83b6dce8a2cb7b5ce5a0f918ff2735bf200b00e3bc7067366654fff13 + sass-embedded (1.99.0-x86_64-linux-gnu) sha256=a4e2ae5e9951815cb8b3ab408cee8b800852491988a57de735f18d259c70d7c4 + sass-embedded (1.99.0-x86_64-linux-musl) sha256=94be72a6f856c610e67b12303dc76a58412e64b24ce97bdf4912199b8145c8dd + sassc-embedded (1.80.8) sha256=ea62825e4031cc8267e6befd492bcf8e29f11ccc18500f4e8ee9adb7feb6d563 + securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 + simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5 + simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246 + simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428 + sinatra (4.2.1) sha256=b7aeb9b11d046b552972ade834f1f9be98b185fa8444480688e3627625377080 + sinatra-contrib (4.2.1) sha256=10d091c944d268aa910c618ea40a3c3ebe0533e6e32990d84af92235a3d26b4a + sprockets (4.2.2) sha256=761e5a49f1c288704763f73139763564c845a8f856d52fba013458f8af1b59b1 + sprockets-helpers (1.4.0) sha256=320a1d210a3e990b682c87dcca9881e78461c430636938b5a6053c5386aa2314 + sprockets-rails (3.5.2) sha256=a9e88e6ce9f8c912d349aa5401509165ec42326baf9e942a85de4b76dbc4119e + stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 + strings (0.2.1) sha256=933293b3c95cf85b81eb44b3cf673e3087661ba739bbadfeadf442083158d6fb + strings-ansi (0.2.0) sha256=90262d760ea4a94cc2ae8d58205277a343409c288cbe7c29416b1826bd511c88 + terminal-table (4.0.0) sha256=f504793203f8251b2ea7c7068333053f0beeea26093ec9962e62ea79f94301d2 + terser (1.2.7) sha256=1b12eb49769dadac44caac3485b38928ff4ab435f1bbbacfe8048cff84c6aa1b + thin (2.0.1) sha256=5bbde5648377f5c3864b5da7cd89a23b5c2d8d8bb9435719f6db49644bcdade9 + thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 + tilt (2.7.0) sha256=0d5b9ba69f6a36490c64b0eee9f6e9aad517e20dcc848800a06eb116f08c6ab3 + tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f + tty-pager (0.14.0) sha256=9f90e63d45433e37f3096c666820f1b6634bcc876924c8479b3dfd2f5df6e033 + tty-screen (0.8.2) sha256=c090652115beae764336c28802d633f204fb84da93c6a968aa5d8e319e819b50 + typhoeus (1.6.0) sha256=bacc41c23e379547e29801dc235cd1699b70b955a1ba3d32b2b877aa844c331d + tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b + unicode-display_width (2.6.0) sha256=12279874bba6d5e4d2728cef814b19197dbb10d7a7837a869bab65da943b7f5a + unicode_utils (1.4.0) sha256=b922d0cf2313b6b7136ada6645ce7154ffc86418ca07d53b058efe9eb72f2a40 + unix_utils (0.0.15) sha256=e1c89eda822928288e8aada00ccc1b77fe1f95aa8f227e393da79f170588918c + uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 + useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844 + webrick (1.9.2) sha256=beb4a15fc474defed24a3bda4ffd88a490d517c9e4e6118c3edce59e45864131 + yajl-ruby (1.4.3) sha256=8c974d9c11ae07b0a3b6d26efea8407269b02e4138118fbe3ef0d2ec9724d1d2 + zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd + RUBY VERSION - ruby 3.4.7p58 + ruby 4.0.5 BUNDLED WITH - 2.4.6 + 4.0.12 diff --git a/README.md b/README.md index 8bbea4327e..b10787b794 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ Keep track of development news: * Join the `#contributors` chat room on [Discord](https://discord.gg/PRyKn3Vbay) * Watch the repository on [GitHub](https://github.com/freeCodeCamp/devdocs/subscription) -* Follow [@DevDocs](https://twitter.com/DevDocs) on Twitter **Table of Contents:** [Quick Start](#quick-start) · [Vision](#vision) · [App](#app) · [Scraper](#scraper) · [Commands](#available-commands) · [Contributing](#contributing) · [Documentation](#documentation) · [Related Projects](#related-projects) · [License](#copyright--license) · [Questions?](#questions) @@ -46,7 +45,9 @@ docker run --name devdocs -d -p 9292:9292 devdocs DevDocs is made of two pieces: a Ruby scraper that generates the documentation and metadata, and a JavaScript app powered by a small Sinatra app. -DevDocs requires Ruby 3.4.1 (defined in [`Gemfile`](./Gemfile)), libcurl, and a JavaScript runtime supported by [ExecJS](https://github.com/rails/execjs#readme) (included in OS X and Windows; [Node.js](https://nodejs.org/en/) on Linux). Once you have these installed, run the following commands: +DevDocs requires Ruby 4.0.5 (defined in [`Gemfile`](./Gemfile)), libcurl, and a JavaScript runtime supported by [ExecJS](https://github.com/rails/execjs#readme) (included in OS X and Windows; [Node.js](https://nodejs.org/en/) on Linux). On Arch Linux run `pacman -S ruby ruby-bundler ruby-erb ruby-irb`. + +Once you have these installed, run the following commands: ```sh git clone https://github.com/freeCodeCamp/devdocs.git && cd devdocs @@ -144,6 +145,7 @@ thor console:docs # Start a REPL in the "Docs" module thor test:all # Run all tests thor test:docs # Run "Docs" tests thor test:app # Run "App" tests +thor test:coverage # Run coverage report on "App" tests # Assets thor assets:compile # Compile assets (not required in development mode) @@ -163,6 +165,24 @@ Contributions are welcome. Please read the [contributing guidelines](./.github/C * [Filter Reference](./docs/filter-reference.md) * [Maintainers’ Guide](./docs/maintainers.md) +## DevDocs Quick Usage Cheatsheet + +Below are some helpful shortcuts and usage tips that are not immediately obvious to new users: + +- Press / or Ctrl + K to instantly focus the search bar. +- Press ? to open DevDocs’ built-in help overlay. +- Press or to navigate search results without touching the mouse. +- Press Enter to open the highlighted search result. +- Press Backspace to go back to the previously viewed page. +- Press Shift + S to toggle the sidebar visibility. +- Press A to open the list of all installed documentation sets. +- Press Esc to close popups, overlays, and search. +- Use the **⚡ Offline Mode toggle** to download docs for offline use. +- You can pin specific documentation sets to the sidebar for quicker access. + +These shortcuts make DevDocs faster to navigate and more efficient for daily use. + + ## Related Projects Made something cool? Feel free to open a PR to add a new row to this table! You might want to discover new projects via https://github.com/topics/devdocs. @@ -195,7 +215,7 @@ Made something cool? Feel free to open a PR to add a new row to this table! You ## Copyright / License -Copyright 2013–2025 Thibaut Courouble and [other contributors](https://github.com/freeCodeCamp/devdocs/graphs/contributors) +Copyright 2013–2026 Thibaut Courouble and [other contributors](https://github.com/freeCodeCamp/devdocs/graphs/contributors) This software is licensed under the terms of the Mozilla Public License v2.0. See the [COPYRIGHT](./COPYRIGHT) and [LICENSE](./LICENSE) files. diff --git a/assets/javascripts/app/app.js b/assets/javascripts/app/app.js index 06d5327e60..3ced2c403c 100644 --- a/assets/javascripts/app/app.js +++ b/assets/javascripts/app/app.js @@ -282,16 +282,6 @@ class App extends Events { 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(); diff --git a/assets/javascripts/app/config.js.erb b/assets/javascripts/app/config.js.erb index 04bc8cbaf4..f1362e4de2 100644 --- a/assets/javascripts/app/config.js.erb +++ b/assets/javascripts/app/config.js.erb @@ -6,7 +6,6 @@ app.config = { env: '<%= App.environment %>', history_cache_size: 10, index_filename: 'index.json', - index_path: '/<%= App.docs_prefix %>', max_results: 50, production_host: 'devdocs.io', search_param: 'q', diff --git a/assets/javascripts/app/router.js b/assets/javascripts/app/router.js index 3c243341fe..4772e49737 100644 --- a/assets/javascripts/app/router.js +++ b/assets/javascripts/app/router.js @@ -179,8 +179,6 @@ app.Router = class Router extends Events { 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); } } } @@ -191,14 +189,6 @@ app.Router = class Router extends Events { } catch (error) {} } - getInitialPathFromCookie() { - const path = Cookies.get("initial_path"); - if (path) { - Cookies.expire("initial_path"); - return path; - } - } - replaceHash(hash) { page.replace( location.pathname + location.search + (hash || ""), diff --git a/assets/javascripts/application.js b/assets/javascripts/application.js index 0bfa45687b..dd5aaec99b 100644 --- a/assets/javascripts/application.js +++ b/assets/javascripts/application.js @@ -18,6 +18,8 @@ //= require_tree ./templates +//= link_tree ../images/sprites + //= require tracking var init = function () { diff --git a/assets/javascripts/lib/license.js b/assets/javascripts/lib/license.js index 15b42c98f4..e4c3c0103a 100644 --- a/assets/javascripts/lib/license.js +++ b/assets/javascripts/lib/license.js @@ -1,5 +1,5 @@ /* - * Copyright 2013-2025 Thibaut Courouble and other contributors + * Copyright 2013-2026 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: diff --git a/assets/javascripts/lib/page.js b/assets/javascripts/lib/page.js index 0043e76612..2feb29c7e1 100644 --- a/assets/javascripts/lib/page.js +++ b/assets/javascripts/lib/page.js @@ -98,26 +98,35 @@ page.canGoForward = () => !Context.isLastState(currentState); const currentPath = () => location.pathname + location.search + location.hash; class Context { + /** + * A counter tracking the largest state ID used. + */ + static stateId = 0; + + /** + * The session ID to apply across all contexts. + */ + static sessionId = Date.now(); + static isIntialState(state) { return state.id === 0; } static isLastState(state) { - return state.id === this.stateId - 1; + return state.id === Context.stateId - 1; } static isInitialPopState(state) { - return state.path === this.initialPath && this.stateId === 1; + return state.path === this.initialPath && Context.stateId === 1; } static isSameSession(state) { - return state.sessionId === this.sessionId; + return state.sessionId === Context.sessionId; } constructor(path, state) { this.initialPath = currentPath(); - this.sessionId = Date.now(); - this.stateId = 0; + if (path == null) { path = "/"; } @@ -136,10 +145,11 @@ class Context { ); if (this.state.id == null) { - this.state.id = this.constructor.stateId++; + Context.stateId++; + this.state.id = Context.stateId; } if (this.state.sessionId == null) { - this.state.sessionId = this.constructor.sessionId; + this.state.sessionId = Context.sessionId; } this.state.path = this.path; } diff --git a/assets/javascripts/lib/util.js b/assets/javascripts/lib/util.js index ca977da113..555c129e33 100644 --- a/assets/javascripts/lib/util.js +++ b/assets/javascripts/lib/util.js @@ -450,6 +450,9 @@ $.escapeRegexp = (string) => string.replace(ESCAPE_REGEXP, "\\$1"); $.urlDecode = (string) => decodeURIComponent(string.replace(/\+/g, "%20")); +// Hash fragments are not form-encoded, so literal plus signs must stay plus signs. +$.urlDecodeFragment = (string) => decodeURIComponent(string); + $.classify = function (string) { string = string.split("_"); for (let i = 0; i < string.length; i++) { diff --git a/assets/javascripts/models/doc.js b/assets/javascripts/models/doc.js index a900c61ea1..4e726fcde3 100644 --- a/assets/javascripts/models/doc.js +++ b/assets/javascripts/models/doc.js @@ -51,9 +51,9 @@ app.models.Doc = class Doc extends app.Model { } indexUrl() { - return `${app.indexHost()}/${this.slug}/${app.config.index_filename}?${ - this.mtime - }`; + return `${app.config.docs_origin}/${this.slug}/${ + app.config.index_filename + }?${this.mtime}`; } toEntry() { diff --git a/assets/javascripts/news.json b/assets/javascripts/news.json index 1f821abae4..ab349bc8a5 100644 --- a/assets/javascripts/news.json +++ b/assets/javascripts/news.json @@ -1,4 +1,32 @@ [ + [ + "2026-07-27", + "New documentation: Tokio" + ], + [ + "2026-07-09", + "New documentation: MapLibre GL JS" + ], + [ + "2026-07-08", + "New documentation: Odin" + ], + [ + "2026-07-03", + "New documentations: Celery, RabbitMQ, ColdFusion" + ], + [ + "2026-06-02", + "New documentation: PowerShell" + ], + [ + "2026-05-26", + "New documentations: OpenTofu, Cycle.js, Polars" + ], + [ + "2026-02-14", + "New documentation: CouchDB" + ], [ "2025-10-19", "New documentations: Lit, Graphviz, Bun" @@ -242,7 +270,7 @@ "New documentations: Bluebird, ESLint and Homebrew" ], [ "2017-11-18", - "Added print & PDF stylesheet.\nFeedback welcome on Twitter and GitHub." + "Added print & PDF stylesheet.\nFeedback welcome on GitHub." ], [ "2017-09-10", "Preferences can now be exported and imported." @@ -395,7 +423,7 @@ "New Python 2 documentation" ], [ "2014-11-09", - "New design\nFeedback welcome on Twitter and GitHub." + "New design\nFeedback welcome on GitHub." ], [ "2014-10-19", "New SVG, Marionette.js, and Mongoose documentations" diff --git a/assets/javascripts/templates/error_tmpl.js b/assets/javascripts/templates/error_tmpl.js index c047d46e5e..7f96247382 100644 --- a/assets/javascripts/templates/error_tmpl.js +++ b/assets/javascripts/templates/error_tmpl.js @@ -90,6 +90,6 @@ app.templates.unsupportedBrowser = `\ 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 + — DevDocs \ `; diff --git a/assets/javascripts/templates/pages/about_tmpl.js b/assets/javascripts/templates/pages/about_tmpl.js index e3142da5fc..600169a5ac 100644 --- a/assets/javascripts/templates/pages/about_tmpl.js +++ b/assets/javascripts/templates/pages/about_tmpl.js @@ -25,14 +25,13 @@ app.templates.aboutPage = function () {

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:

- Copyright 2013–2025 Thibaut Courouble and other contributors
+ Copyright 2013–2026 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 diff --git a/assets/javascripts/templates/pages/news_tmpl.js.erb b/assets/javascripts/templates/pages/news_tmpl.js.erb index 72f4b1dd07..3bcfec32f4 100644 --- a/assets/javascripts/templates/pages/news_tmpl.js.erb +++ b/assets/javascripts/templates/pages/news_tmpl.js.erb @@ -2,7 +2,6 @@ app.templates.newsPage = () => `

Changelog

-For the latest news, follow @DevDocs.
For development updates, follow the project on GitHub.

${app.templates.newsList(app.news)}
`; diff --git a/assets/javascripts/templates/pages/root_tmpl.js.erb b/assets/javascripts/templates/pages/root_tmpl.js.erb index b82e9a9e6a..176906696c 100644 --- a/assets/javascripts/templates/pages/root_tmpl.js.erb +++ b/assets/javascripts/templates/pages/root_tmpl.js.erb @@ -37,7 +37,6 @@ app.templates.intro = `\
  • To search a specific documentation, type its name (or an abbr.), then Tab.
  • You can search using your browser's address bar — learn how.
  • DevDocs works offline, on mobile, and can be installed as web app. -
  • For the latest news, follow @DevDocs.
  • DevDocs is free and open source.
  • And if you're new to coding, check out freeCodeCamp's open source curriculum. @@ -56,7 +55,6 @@ app.templates.mobileIntro = `\
  • Pick your docs in the Preferences.
  • The search supports fuzzy matching.
  • To search a specific documentation, type its name (or an abbr.), then Space. -
  • For the latest news, follow @DevDocs.
  • DevDocs is open source.

    Happy coding! diff --git a/assets/javascripts/vendor/prism.js b/assets/javascripts/vendor/prism.js index 96519c4a8d..d52319c69e 100644 --- a/assets/javascripts/vendor/prism.js +++ b/assets/javascripts/vendor/prism.js @@ -1,5 +1,5 @@ /* PrismJS 1.30.0 -https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+bash+c+cpp+cmake+coffeescript+crystal+d+dart+diff+django+dot+elixir+erlang+go+groovy+java+json+julia+kotlin+latex+lua+markdown+markup-templating+matlab+nginx+nim+nix+ocaml+perl+php+python+qml+r+jsx+ruby+rust+scss+scala+shell-session+sql+tcl+typescript+yaml+zig */ +https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+bash+c+csharp+cpp+cmake+coffeescript+crystal+d+dart+diff+django+dot+elixir+erlang+gdscript+go+groovy+java+json+julia+kotlin+latex+lua+markdown+markup-templating+matlab+nginx+nim+nix+ocaml+perl+php+python+qml+r+jsx+ruby+rust+scss+scala+shell-session+sql+tcl+typescript+yaml+zig */ /// var _self = (typeof window !== 'undefined') @@ -2038,6 +2038,373 @@ Prism.languages.insertBefore('c', 'function', { delete Prism.languages.c['boolean']; +(function (Prism) { + + /** + * Replaces all placeholders "<>" of given pattern with the n-th replacement (zero based). + * + * Note: This is a simple text based replacement. Be careful when using backreferences! + * + * @param {string} pattern the given pattern. + * @param {string[]} replacements a list of replacement which can be inserted into the given pattern. + * @returns {string} the pattern with all placeholders replaced with their corresponding replacements. + * @example replace(/a<<0>>a/.source, [/b+/.source]) === /a(?:b+)a/.source + */ + function replace(pattern, replacements) { + return pattern.replace(/<<(\d+)>>/g, function (m, index) { + return '(?:' + replacements[+index] + ')'; + }); + } + /** + * @param {string} pattern + * @param {string[]} replacements + * @param {string} [flags] + * @returns {RegExp} + */ + function re(pattern, replacements, flags) { + return RegExp(replace(pattern, replacements), flags || ''); + } + + /** + * Creates a nested pattern where all occurrences of the string `<>` are replaced with the pattern itself. + * + * @param {string} pattern + * @param {number} depthLog2 + * @returns {string} + */ + function nested(pattern, depthLog2) { + for (var i = 0; i < depthLog2; i++) { + pattern = pattern.replace(/<>/g, function () { return '(?:' + pattern + ')'; }); + } + return pattern.replace(/<>/g, '[^\\s\\S]'); + } + + // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/ + var keywordKinds = { + // keywords which represent a return or variable type + type: 'bool byte char decimal double dynamic float int long object sbyte short string uint ulong ushort var void', + // keywords which are used to declare a type + typeDeclaration: 'class enum interface record struct', + // contextual keywords + // ("var" and "dynamic" are missing because they are used like types) + contextual: 'add alias and ascending async await by descending from(?=\\s*(?:\\w|$)) get global group into init(?=\\s*;) join let nameof not notnull on or orderby partial remove select set unmanaged value when where with(?=\\s*{)', + // all other keywords + other: 'abstract as base break case catch checked const continue default delegate do else event explicit extern finally fixed for foreach goto if implicit in internal is lock namespace new null operator out override params private protected public readonly ref return sealed sizeof stackalloc static switch this throw try typeof unchecked unsafe using virtual volatile while yield' + }; + + // keywords + function keywordsToPattern(words) { + return '\\b(?:' + words.trim().replace(/ /g, '|') + ')\\b'; + } + var typeDeclarationKeywords = keywordsToPattern(keywordKinds.typeDeclaration); + var keywords = RegExp(keywordsToPattern(keywordKinds.type + ' ' + keywordKinds.typeDeclaration + ' ' + keywordKinds.contextual + ' ' + keywordKinds.other)); + var nonTypeKeywords = keywordsToPattern(keywordKinds.typeDeclaration + ' ' + keywordKinds.contextual + ' ' + keywordKinds.other); + var nonContextualKeywords = keywordsToPattern(keywordKinds.type + ' ' + keywordKinds.typeDeclaration + ' ' + keywordKinds.other); + + // types + var generic = nested(/<(?:[^<>;=+\-*/%&|^]|<>)*>/.source, 2); // the idea behind the other forbidden characters is to prevent false positives. Same for tupleElement. + var nestedRound = nested(/\((?:[^()]|<>)*\)/.source, 2); + var name = /@?\b[A-Za-z_]\w*\b/.source; + var genericName = replace(/<<0>>(?:\s*<<1>>)?/.source, [name, generic]); + var identifier = replace(/(?!<<0>>)<<1>>(?:\s*\.\s*<<1>>)*/.source, [nonTypeKeywords, genericName]); + var array = /\[\s*(?:,\s*)*\]/.source; + var typeExpressionWithoutTuple = replace(/<<0>>(?:\s*(?:\?\s*)?<<1>>)*(?:\s*\?)?/.source, [identifier, array]); + var tupleElement = replace(/[^,()<>[\];=+\-*/%&|^]|<<0>>|<<1>>|<<2>>/.source, [generic, nestedRound, array]); + var tuple = replace(/\(<<0>>+(?:,<<0>>+)+\)/.source, [tupleElement]); + var typeExpression = replace(/(?:<<0>>|<<1>>)(?:\s*(?:\?\s*)?<<2>>)*(?:\s*\?)?/.source, [tuple, identifier, array]); + + var typeInside = { + 'keyword': keywords, + 'punctuation': /[<>()?,.:[\]]/ + }; + + // strings & characters + // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/lexical-structure#character-literals + // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/lexical-structure#string-literals + var character = /'(?:[^\r\n'\\]|\\.|\\[Uux][\da-fA-F]{1,8})'/.source; // simplified pattern + var regularString = /"(?:\\.|[^\\"\r\n])*"/.source; + var verbatimString = /@"(?:""|\\[\s\S]|[^\\"])*"(?!")/.source; + + + Prism.languages.csharp = Prism.languages.extend('clike', { + 'string': [ + { + pattern: re(/(^|[^$\\])<<0>>/.source, [verbatimString]), + lookbehind: true, + greedy: true + }, + { + pattern: re(/(^|[^@$\\])<<0>>/.source, [regularString]), + lookbehind: true, + greedy: true + } + ], + 'class-name': [ + { + // Using static + // using static System.Math; + pattern: re(/(\busing\s+static\s+)<<0>>(?=\s*;)/.source, [identifier]), + lookbehind: true, + inside: typeInside + }, + { + // Using alias (type) + // using Project = PC.MyCompany.Project; + pattern: re(/(\busing\s+<<0>>\s*=\s*)<<1>>(?=\s*;)/.source, [name, typeExpression]), + lookbehind: true, + inside: typeInside + }, + { + // Using alias (alias) + // using Project = PC.MyCompany.Project; + pattern: re(/(\busing\s+)<<0>>(?=\s*=)/.source, [name]), + lookbehind: true + }, + { + // Type declarations + // class Foo + // interface Foo + pattern: re(/(\b<<0>>\s+)<<1>>/.source, [typeDeclarationKeywords, genericName]), + lookbehind: true, + inside: typeInside + }, + { + // Single catch exception declaration + // catch(Foo) + // (things like catch(Foo e) is covered by variable declaration) + pattern: re(/(\bcatch\s*\(\s*)<<0>>/.source, [identifier]), + lookbehind: true, + inside: typeInside + }, + { + // Name of the type parameter of generic constraints + // where Foo : class + pattern: re(/(\bwhere\s+)<<0>>/.source, [name]), + lookbehind: true + }, + { + // Casts and checks via as and is. + // as Foo, is Bar + // (things like if(a is Foo b) is covered by variable declaration) + pattern: re(/(\b(?:is(?:\s+not)?|as)\s+)<<0>>/.source, [typeExpressionWithoutTuple]), + lookbehind: true, + inside: typeInside + }, + { + // Variable, field and parameter declaration + // (Foo bar, Bar baz, Foo[,,] bay, Foo> bax) + pattern: re(/\b<<0>>(?=\s+(?!<<1>>|with\s*\{)<<2>>(?:\s*[=,;:{)\]]|\s+(?:in|when)\b))/.source, [typeExpression, nonContextualKeywords, name]), + inside: typeInside + } + ], + 'keyword': keywords, + // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/lexical-structure#literals + 'number': /(?:\b0(?:x[\da-f_]*[\da-f]|b[01_]*[01])|(?:\B\.\d+(?:_+\d+)*|\b\d+(?:_+\d+)*(?:\.\d+(?:_+\d+)*)?)(?:e[-+]?\d+(?:_+\d+)*)?)(?:[dflmu]|lu|ul)?\b/i, + 'operator': />>=?|<<=?|[-=]>|([-+&|])\1|~|\?\?=?|[-+*/%&|^!=<>]=?/, + 'punctuation': /\?\.?|::|[{}[\];(),.:]/ + }); + + Prism.languages.insertBefore('csharp', 'number', { + 'range': { + pattern: /\.\./, + alias: 'operator' + } + }); + + Prism.languages.insertBefore('csharp', 'punctuation', { + 'named-parameter': { + pattern: re(/([(,]\s*)<<0>>(?=\s*:)/.source, [name]), + lookbehind: true, + alias: 'punctuation' + } + }); + + Prism.languages.insertBefore('csharp', 'class-name', { + 'namespace': { + // namespace Foo.Bar {} + // using Foo.Bar; + pattern: re(/(\b(?:namespace|using)\s+)<<0>>(?:\s*\.\s*<<0>>)*(?=\s*[;{])/.source, [name]), + lookbehind: true, + inside: { + 'punctuation': /\./ + } + }, + 'type-expression': { + // default(Foo), typeof(Foo), sizeof(int) + pattern: re(/(\b(?:default|sizeof|typeof)\s*\(\s*(?!\s))(?:[^()\s]|\s(?!\s)|<<0>>)*(?=\s*\))/.source, [nestedRound]), + lookbehind: true, + alias: 'class-name', + inside: typeInside + }, + 'return-type': { + // Foo ForBar(); Foo IFoo.Bar() => 0 + // int this[int index] => 0; T IReadOnlyList.this[int index] => this[index]; + // int Foo => 0; int Foo { get; set } = 0; + pattern: re(/<<0>>(?=\s+(?:<<1>>\s*(?:=>|[({]|\.\s*this\s*\[)|this\s*\[))/.source, [typeExpression, identifier]), + inside: typeInside, + alias: 'class-name' + }, + 'constructor-invocation': { + // new List> { } + pattern: re(/(\bnew\s+)<<0>>(?=\s*[[({])/.source, [typeExpression]), + lookbehind: true, + inside: typeInside, + alias: 'class-name' + }, + /*'explicit-implementation': { + // int IFoo.Bar => 0; void IFoo>.Foo(); + pattern: replace(/\b<<0>>(?=\.<<1>>)/, className, methodOrPropertyDeclaration), + inside: classNameInside, + alias: 'class-name' + },*/ + 'generic-method': { + // foo() + pattern: re(/<<0>>\s*<<1>>(?=\s*\()/.source, [name, generic]), + inside: { + 'function': re(/^<<0>>/.source, [name]), + 'generic': { + pattern: RegExp(generic), + alias: 'class-name', + inside: typeInside + } + } + }, + 'type-list': { + // The list of types inherited or of generic constraints + // class Foo : Bar, IList + // where F : Bar, IList + pattern: re( + /\b((?:<<0>>\s+<<1>>|record\s+<<1>>\s*<<5>>|where\s+<<2>>)\s*:\s*)(?:<<3>>|<<4>>|<<1>>\s*<<5>>|<<6>>)(?:\s*,\s*(?:<<3>>|<<4>>|<<6>>))*(?=\s*(?:where|[{;]|=>|$))/.source, + [typeDeclarationKeywords, genericName, name, typeExpression, keywords.source, nestedRound, /\bnew\s*\(\s*\)/.source] + ), + lookbehind: true, + inside: { + 'record-arguments': { + pattern: re(/(^(?!new\s*\()<<0>>\s*)<<1>>/.source, [genericName, nestedRound]), + lookbehind: true, + greedy: true, + inside: Prism.languages.csharp + }, + 'keyword': keywords, + 'class-name': { + pattern: RegExp(typeExpression), + greedy: true, + inside: typeInside + }, + 'punctuation': /[,()]/ + } + }, + 'preprocessor': { + pattern: /(^[\t ]*)#.*/m, + lookbehind: true, + alias: 'property', + inside: { + // highlight preprocessor directives as keywords + 'directive': { + pattern: /(#)\b(?:define|elif|else|endif|endregion|error|if|line|nullable|pragma|region|undef|warning)\b/, + lookbehind: true, + alias: 'keyword' + } + } + } + }); + + // attributes + var regularStringOrCharacter = regularString + '|' + character; + var regularStringCharacterOrComment = replace(/\/(?![*/])|\/\/[^\r\n]*[\r\n]|\/\*(?:[^*]|\*(?!\/))*\*\/|<<0>>/.source, [regularStringOrCharacter]); + var roundExpression = nested(replace(/[^"'/()]|<<0>>|\(<>*\)/.source, [regularStringCharacterOrComment]), 2); + + // https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/attributes/#attribute-targets + var attrTarget = /\b(?:assembly|event|field|method|module|param|property|return|type)\b/.source; + var attr = replace(/<<0>>(?:\s*\(<<1>>*\))?/.source, [identifier, roundExpression]); + + Prism.languages.insertBefore('csharp', 'class-name', { + 'attribute': { + // Attributes + // [Foo], [Foo(1), Bar(2, Prop = "foo")], [return: Foo(1), Bar(2)], [assembly: Foo(Bar)] + pattern: re(/((?:^|[^\s\w>)?])\s*\[\s*)(?:<<0>>\s*:\s*)?<<1>>(?:\s*,\s*<<1>>)*(?=\s*\])/.source, [attrTarget, attr]), + lookbehind: true, + greedy: true, + inside: { + 'target': { + pattern: re(/^<<0>>(?=\s*:)/.source, [attrTarget]), + alias: 'keyword' + }, + 'attribute-arguments': { + pattern: re(/\(<<0>>*\)/.source, [roundExpression]), + inside: Prism.languages.csharp + }, + 'class-name': { + pattern: RegExp(identifier), + inside: { + 'punctuation': /\./ + } + }, + 'punctuation': /[:,]/ + } + } + }); + + + // string interpolation + var formatString = /:[^}\r\n]+/.source; + // multi line + var mInterpolationRound = nested(replace(/[^"'/()]|<<0>>|\(<>*\)/.source, [regularStringCharacterOrComment]), 2); + var mInterpolation = replace(/\{(?!\{)(?:(?![}:])<<0>>)*<<1>>?\}/.source, [mInterpolationRound, formatString]); + // single line + var sInterpolationRound = nested(replace(/[^"'/()]|\/(?!\*)|\/\*(?:[^*]|\*(?!\/))*\*\/|<<0>>|\(<>*\)/.source, [regularStringOrCharacter]), 2); + var sInterpolation = replace(/\{(?!\{)(?:(?![}:])<<0>>)*<<1>>?\}/.source, [sInterpolationRound, formatString]); + + function createInterpolationInside(interpolation, interpolationRound) { + return { + 'interpolation': { + pattern: re(/((?:^|[^{])(?:\{\{)*)<<0>>/.source, [interpolation]), + lookbehind: true, + inside: { + 'format-string': { + pattern: re(/(^\{(?:(?![}:])<<0>>)*)<<1>>(?=\}$)/.source, [interpolationRound, formatString]), + lookbehind: true, + inside: { + 'punctuation': /^:/ + } + }, + 'punctuation': /^\{|\}$/, + 'expression': { + pattern: /[\s\S]+/, + alias: 'language-csharp', + inside: Prism.languages.csharp + } + } + }, + 'string': /[\s\S]+/ + }; + } + + Prism.languages.insertBefore('csharp', 'string', { + 'interpolation-string': [ + { + pattern: re(/(^|[^\\])(?:\$@|@\$)"(?:""|\\[\s\S]|\{\{|<<0>>|[^\\{"])*"/.source, [mInterpolation]), + lookbehind: true, + greedy: true, + inside: createInterpolationInside(mInterpolation, mInterpolationRound), + }, + { + pattern: re(/(^|[^@\\])\$"(?:\\.|\{\{|<<0>>|[^\\"{])*"/.source, [sInterpolation]), + lookbehind: true, + greedy: true, + inside: createInterpolationInside(sInterpolation, sInterpolationRound), + } + ], + 'char': { + pattern: RegExp(character), + greedy: true + } + }); + + Prism.languages.dotnet = Prism.languages.cs = Prism.languages.csharp; + +}(Prism)); + (function (Prism) { var keyword = /\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\b/; @@ -3150,6 +3517,34 @@ Prism.languages.erlang = { }; +Prism.languages.gdscript = { + 'comment': /#.*/, + 'string': { + pattern: /@?(?:("|')(?:(?!\1)[^\n\\]|\\[\s\S])*\1(?!"|')|"""(?:[^\\]|\\[\s\S])*?""")/, + greedy: true + }, + 'class-name': { + // class_name Foo, extends Bar, class InnerClass + // export(int) var baz, export(int, 0) var i + // as Node + // const FOO: int = 9, var bar: bool = true + // func add(reference: Item, amount: int) -> Item: + pattern: /(^(?:class|class_name|extends)[ \t]+|^export\([ \t]*|\bas[ \t]+|(?:\b(?:const|var)[ \t]|[,(])[ \t]*\w+[ \t]*:[ \t]*|->[ \t]*)[a-zA-Z_]\w*/m, + lookbehind: true + }, + 'keyword': /\b(?:and|as|assert|break|breakpoint|class|class_name|const|continue|elif|else|enum|export|extends|for|func|if|in|is|master|mastersync|match|not|null|onready|or|pass|preload|puppet|puppetsync|remote|remotesync|return|self|setget|signal|static|tool|var|while|yield)\b/, + 'function': /\b[a-z_]\w*(?=[ \t]*\()/i, + 'variable': /\$\w+/, + 'number': [ + /\b0b[01_]+\b|\b0x[\da-fA-F_]+\b|(?:\b\d[\d_]*(?:\.[\d_]*)?|\B\.[\d_]+)(?:e[+-]?[\d_]+)?\b/, + /\b(?:INF|NAN|PI|TAU)\b/ + ], + 'constant': /\b[A-Z][A-Z_\d]*\b/, + 'boolean': /\b(?:false|true)\b/, + 'operator': /->|:=|&&|\|\||<<|>>|[-+*/%&|!<>=]=?|[~^]/, + 'punctuation': /[.:,;()[\]{}]/ +}; + Prism.languages.go = Prism.languages.extend('clike', { 'string': { pattern: /(^|[^\\])"(?:\\.|[^"\\\r\n])*"|`[^`]*`/, diff --git a/assets/javascripts/views/content/content.js b/assets/javascripts/views/content/content.js index 875f96b4c6..e654d33ee3 100644 --- a/assets/javascripts/views/content/content.js +++ b/assets/javascripts/views/content/content.js @@ -149,6 +149,16 @@ app.views.Content = class Content extends app.View { beforeRoute(context) { this.cacheScrollPosition(); + + /* + * If scroll position wasn't cached from an earlier visit: + * - let the anchor (if there is one) set position, or + * - scroll to top. + */ + if (!this.scrollMap[context.state.id] && !context.hash) { + this.scrollToTop(); + } + this.routeCtx = context; this.scrollToTargetTimeout = this.delay(this.scrollToTarget); } diff --git a/assets/javascripts/views/layout/resizer.js b/assets/javascripts/views/layout/resizer.js index 93d7331624..4b20efb088 100644 --- a/assets/javascripts/views/layout/resizer.js +++ b/assets/javascripts/views/layout/resizer.js @@ -44,14 +44,20 @@ app.views.Resizer = class Resizer extends app.View { return; } this.lastDragValue = value; - if (this.lastDrag && this.lastDrag > Date.now() - 50) { + if (this.rafPending) { return; } - this.lastDrag = Date.now(); - this.resize(value, false); + this.rafPending = requestAnimationFrame(() => { + this.rafPending = null; + this.resize(this.lastDragValue, false); + }); } onDragEnd(event) { + if (this.rafPending) { + cancelAnimationFrame(this.rafPending); + this.rafPending = null; + } $.off(window, "dragover", this.onDrag); let value = event.pageX || event.screenX - window.screenX; if ( diff --git a/assets/javascripts/views/search/search.js b/assets/javascripts/views/search/search.js index 163d96e1f7..94105618f0 100644 --- a/assets/javascripts/views/search/search.js +++ b/assets/javascripts/views/search/search.js @@ -216,7 +216,7 @@ app.views.Search = class Search extends app.View { getHashValue() { try { - return Search.HASH_RGX.exec($.urlDecode(location.hash))?.[1]; + return Search.HASH_RGX.exec($.urlDecodeFragment(location.hash))?.[1]; } catch (error) {} } }; diff --git a/assets/javascripts/views/search/search_scope.js b/assets/javascripts/views/search/search_scope.js index c06d54e0ae..45984bc176 100644 --- a/assets/javascripts/views/search/search_scope.js +++ b/assets/javascripts/views/search/search_scope.js @@ -157,7 +157,7 @@ app.views.SearchScope = class SearchScope extends app.View { extractHashValue() { const value = this.getHashValue(); if (value) { - const newHash = $.urlDecode(location.hash).replace( + const newHash = $.urlDecodeFragment(location.hash).replace( `#${SearchScope.SEARCH_PARAM}=${value} `, `#${SearchScope.SEARCH_PARAM}=` ); @@ -168,7 +168,7 @@ app.views.SearchScope = class SearchScope extends app.View { getHashValue() { try { - return SearchScope.HASH_RGX.exec($.urlDecode(location.hash))?.[1]; + return SearchScope.HASH_RGX.exec($.urlDecodeFragment(location.hash))?.[1]; } catch (error) {} } diff --git a/assets/stylesheets/application.css.scss b/assets/stylesheets/application.css.scss index 12247d4405..13630eea4b 100644 --- a/assets/stylesheets/application.css.scss +++ b/assets/stylesheets/application.css.scss @@ -1,142 +1,136 @@ -//= depend_on sprites/docs.png -//= depend_on sprites/docs@2x.png -//= depend_on sprites/docs.json - /*! - * Copyright 2013-2025 Thibaut Courouble and other contributors + * Copyright 2013-2026 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/ */ -@import 'global/variables-light', - 'global/variables-dark', - 'global/variables', - 'global/mixins', - 'global/icons', - 'global/classes', - 'global/base'; + // SCSS partials that were formerly imported here are now invoked on an as-needed, per-file basis. -@import 'components/app', - 'components/header', - 'components/notif', - 'components/sidebar', - 'components/settings', - 'components/content', - 'components/page', - 'components/fail', - 'components/path', - 'components/notice', - 'components/prism', - 'components/mobile', - 'components/environment'; +@use 'components/app'; +@use 'components/header'; +@use 'components/notif'; +@use 'components/sidebar'; +@use 'components/settings'; +@use 'components/content'; +@use 'components/page'; +@use 'components/fail'; +@use 'components/path'; +@use 'components/notice'; +@use 'components/prism'; +@use 'components/mobile'; +@use 'components/environment'; -@import 'pages/simple', - 'pages/angular', - 'pages/angularjs', - 'pages/apache', - 'pages/async', - 'pages/bash', - 'pages/bootstrap', - 'pages/cppref', - 'pages/cakephp', - 'pages/clojure', - 'pages/codeception', - 'pages/coffeescript', - 'pages/cordova', - 'pages/crystal', - 'pages/cypress', - 'pages/d', - 'pages/d3', - 'pages/dart', - 'pages/dojo', - 'pages/drupal', - 'pages/eigen3', - 'pages/elixir', - 'pages/elisp', - 'pages/ember', - 'pages/erlang', - 'pages/express', - 'pages/fastapi', - 'pages/fluture', - 'pages/git', - 'pages/github', - 'pages/gnuplot', - 'pages/go', - 'pages/graphite', - 'pages/groovy', - 'pages/gtk', - 'pages/hapi', - 'pages/haproxy', - 'pages/haskell', - 'pages/jasmine', - 'pages/jekyll', - 'pages/joi', - 'pages/jq', - 'pages/jquery', - 'pages/julia', - 'pages/knockout', - 'pages/kotlin', - 'pages/kubectl', - 'pages/kubernetes', - 'pages/laravel', - 'pages/liquid', - 'pages/lit', - 'pages/love', - 'pages/lua', - 'pages/gnu_make', - 'pages/mariadb', - 'pages/mdn', - 'pages/meteor', - 'pages/mkdocs', - 'pages/modernizr', - 'pages/moment', - 'pages/nginx', - 'pages/node', - 'pages/npm', - 'pages/nushell', - 'pages/octave', - 'pages/openjdk', - 'pages/openlayers', - 'pages/perl', - 'pages/phalcon', - 'pages/phaser', - 'pages/php', - 'pages/phpunit', - 'pages/postgres', - 'pages/pug', - 'pages/pygame', - 'pages/python', - 'pages/qt', - 'pages/ramda', - 'pages/rdoc', - 'pages/react', - 'pages/react_native', - 'pages/reactivex', - 'pages/redis', - 'pages/rethinkdb', - 'pages/rfc', - 'pages/rubydoc', - 'pages/rust', - 'pages/rxjs', - 'pages/sanctuary', - 'pages/sanctuary_def', - 'pages/sanctuary_type_classes', - 'pages/scala', - 'pages/sinon', - 'pages/sphinx', - 'pages/sphinx_simple', - 'pages/sqlite', - 'pages/support_tables', - 'pages/tailwindcss', - 'pages/tcl_tk', - 'pages/tensorflow', - 'pages/terraform', - 'pages/typescript', - 'pages/underscore', - 'pages/vue', - 'pages/webpack', - 'pages/wordpress', - 'pages/yard', - 'pages/yii'; +@use 'pages/angular'; +@use 'pages/angularjs'; +@use 'pages/apache'; +@use 'pages/async'; +@use 'pages/bash'; +@use 'pages/bootstrap'; +@use 'pages/cppref'; +@use 'pages/cakephp'; +@use 'pages/celery'; +@use 'pages/clojure'; +@use 'pages/codeception'; +@use 'pages/coffeescript'; +@use 'pages/cordova'; +@use 'pages/crystal'; +@use 'pages/cypress'; +@use 'pages/d'; +@use 'pages/d3'; +@use 'pages/dart'; +@use 'pages/dojo'; +@use 'pages/drupal'; +@use 'pages/eigen3'; +@use 'pages/elixir'; +@use 'pages/elisp'; +@use 'pages/ember'; +@use 'pages/erlang'; +@use 'pages/express'; +@use 'pages/fastapi'; +@use 'pages/fluture'; +@use 'pages/git'; +@use 'pages/github'; +@use 'pages/gnuplot'; +@use 'pages/go'; +@use 'pages/graphite'; +@use 'pages/groovy'; +@use 'pages/gtk'; +@use 'pages/hapi'; +@use 'pages/haproxy'; +@use 'pages/haskell'; +@use 'pages/jasmine'; +@use 'pages/jekyll'; +@use 'pages/joi'; +@use 'pages/jq'; +@use 'pages/jquery'; +@use 'pages/julia'; +@use 'pages/knockout'; +@use 'pages/kotlin'; +@use 'pages/kubectl'; +@use 'pages/kubernetes'; +@use 'pages/laravel'; +@use 'pages/liquid'; +@use 'pages/lit'; +@use 'pages/love'; +@use 'pages/lua'; +@use 'pages/gnu_make'; +@use 'pages/maplibre_gl'; +@use 'pages/mariadb'; +@use 'pages/mdn'; +@use 'pages/meteor'; +@use 'pages/mkdocs'; +@use 'pages/modernizr'; +@use 'pages/moment'; +@use 'pages/nginx'; +@use 'pages/node'; +@use 'pages/npm'; +@use 'pages/nushell'; +@use 'pages/octave'; +@use 'pages/openjdk'; +@use 'pages/openlayers'; +@use 'pages/opentofu'; +@use 'pages/perl'; +@use 'pages/phalcon'; +@use 'pages/phaser'; +@use 'pages/php'; +@use 'pages/phpunit'; +@use 'pages/postgres'; +@use 'pages/pug'; +@use 'pages/pygame'; +@use 'pages/python'; +@use 'pages/qt'; +@use 'pages/rabbit_mq'; +@use 'pages/ramda'; +@use 'pages/rdoc'; +@use 'pages/react'; +@use 'pages/react_native'; +@use 'pages/reactivex'; +@use 'pages/redis'; +@use 'pages/rethinkdb'; +@use 'pages/rfc'; +@use 'pages/rubydoc'; +@use 'pages/rust'; +@use 'pages/rxjs'; +@use 'pages/sanctuary'; +@use 'pages/sanctuary_def'; +@use 'pages/sanctuary_type_classes'; +@use 'pages/scala'; +@use 'pages/sinon'; +@use 'pages/sphinx'; +@use 'pages/sphinx_simple'; +@use 'pages/sqlite'; +@use 'pages/support_tables'; +@use 'pages/tailwindcss'; +@use 'pages/tcl_tk'; +@use 'pages/tensorflow'; +@use 'pages/terraform'; +@use 'pages/tokio'; +@use 'pages/typescript'; +@use 'pages/underscore'; +@use 'pages/vue'; +@use 'pages/webpack'; +@use 'pages/wordpress'; +@use 'pages/yard'; +@use 'pages/yii'; diff --git a/assets/stylesheets/components/_app.scss b/assets/stylesheets/components/_app.scss index 96e09938ff..1edfd6d5ab 100644 --- a/assets/stylesheets/components/_app.scss +++ b/assets/stylesheets/components/_app.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + html._booting { background: var(--contentBackground); } body._max-width { background: none; } html._booting body._max-width { background: var(--documentBackground); } diff --git a/assets/stylesheets/components/_content.scss b/assets/stylesheets/components/_content.scss index 5522371e22..b25956e6d2 100644 --- a/assets/stylesheets/components/_content.scss +++ b/assets/stylesheets/components/_content.scss @@ -2,6 +2,11 @@ // Content // +@use 'global/base'; +@use 'global/icons'; +@use 'global/mixins' as m; +@use 'global/classes'; + ._container { position: relative; z-index: var(--contentZ); @@ -10,7 +15,7 @@ pointer-events: none; @extend %border-box; - @include mobile { margin-left: var(--sidebarMediumWidth); } + @include m.mobile { margin-left: var(--sidebarMediumWidth); } ._sidebar-hidden & { margin-left: 0; } body:not(._native-scrollbars) & { -webkit-margin-end: -1px; } diff --git a/assets/stylesheets/components/_fail.scss b/assets/stylesheets/components/_fail.scss index c520977e51..cc6a75e3b4 100644 --- a/assets/stylesheets/components/_fail.scss +++ b/assets/stylesheets/components/_fail.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._fail { // Don't use CSS variables, in case the browser doesn't support them. display: block; position: relative; diff --git a/assets/stylesheets/components/_header.scss b/assets/stylesheets/components/_header.scss index 03c302a81e..bc33d39c62 100644 --- a/assets/stylesheets/components/_header.scss +++ b/assets/stylesheets/components/_header.scss @@ -2,6 +2,10 @@ // Header // +@use 'global/icons'; +@use 'global/mixins' as m; +@use 'global/classes'; + ._header { position: absolute; z-index: var(--headerZ); @@ -17,7 +21,7 @@ @extend %border-box; @extend %user-select-none; - @include mobile { width: var(--sidebarMediumWidth); } + @include m.mobile { width: var(--sidebarMediumWidth); } } ._header-left { diff --git a/assets/stylesheets/components/_notice.scss b/assets/stylesheets/components/_notice.scss index 1e8cd381c8..497a8989b4 100644 --- a/assets/stylesheets/components/_notice.scss +++ b/assets/stylesheets/components/_notice.scss @@ -1,3 +1,5 @@ +@use 'global/mixins' as m; + ._notice { position: absolute; z-index: var(--noticeZ); @@ -9,7 +11,7 @@ background: var(--noticeBackground); box-shadow: inset 0 1px var(--noticeBorder); - @include mobile { left: var(--sidebarMediumWidth); } + @include m.mobile { left: var(--sidebarMediumWidth); } ._sidebar-hidden & { left: 0; } diff --git a/assets/stylesheets/components/_notif.scss b/assets/stylesheets/components/_notif.scss index f0880fdd88..fbca6180d0 100644 --- a/assets/stylesheets/components/_notif.scss +++ b/assets/stylesheets/components/_notif.scss @@ -1,3 +1,6 @@ +@use 'global/classes'; +@use 'global/icons'; + ._notif, %notif { position: absolute; z-index: 2; diff --git a/assets/stylesheets/components/_page.scss b/assets/stylesheets/components/_page.scss index 900ed6050e..7f4778fc51 100644 --- a/assets/stylesheets/components/_page.scss +++ b/assets/stylesheets/components/_page.scss @@ -1,3 +1,6 @@ +@use 'global/classes'; +@use 'components/content'; + // // Page // diff --git a/assets/stylesheets/components/_path.scss b/assets/stylesheets/components/_path.scss index 5853fa04fa..bd60e7f2b5 100644 --- a/assets/stylesheets/components/_path.scss +++ b/assets/stylesheets/components/_path.scss @@ -1,3 +1,6 @@ +@use 'global/mixins' as m; +@use 'global/icons'; + ._path { position: absolute; z-index: var(--headerZ); @@ -11,7 +14,7 @@ background: var(--pathBackground); box-shadow: inset 0 1px var(--pathBorder); - @include mobile { left: var(--sidebarMediumWidth); } + @include m.mobile { left: var(--sidebarMediumWidth); } ._sidebar-hidden & { left: 0; } diff --git a/assets/stylesheets/components/_settings.scss b/assets/stylesheets/components/_settings.scss index 807f550063..01cf535596 100644 --- a/assets/stylesheets/components/_settings.scss +++ b/assets/stylesheets/components/_settings.scss @@ -2,6 +2,9 @@ // Settings // +@use 'global/classes'; +@use 'global/icons'; + ._settings { display: none; position: absolute; diff --git a/assets/stylesheets/components/_sidebar.scss b/assets/stylesheets/components/_sidebar.scss index 8813f92dac..e94fc0161d 100644 --- a/assets/stylesheets/components/_sidebar.scss +++ b/assets/stylesheets/components/_sidebar.scss @@ -2,6 +2,10 @@ // Sidebar // +@use 'global/classes'; +@use 'global/icons'; +@use 'global/mixins' as m; + ._sidebar { position: absolute; z-index: var(--sidebarZ); @@ -76,7 +80,7 @@ box-shadow: inset -1px 0 var(--sidebarBorder); @extend %border-box; - @include mobile { width: var(--sidebarMediumWidth); } + @include m.mobile { width: var(--sidebarMediumWidth); } ._sidebar > & { min-height: 100%; } @@ -311,7 +315,7 @@ transform: translate3d(0, 0, 0); @extend %border-box; - @include mobile { min-width: var(--sidebarMediumWidth); } + @include m.mobile { min-width: var(--sidebarMediumWidth); } > ._list-text { display: inline; } diff --git a/assets/stylesheets/global/_base.scss b/assets/stylesheets/global/_base.scss index 6cbc41a0fd..640ca929a5 100644 --- a/assets/stylesheets/global/_base.scss +++ b/assets/stylesheets/global/_base.scss @@ -1,13 +1,17 @@ +@use 'global/classes'; +@use 'global/mixins' as m; +@use 'global/variables' as *; +@use 'global/print'; + html { height: 100%; font-size: 100%; background: #fff; // fallback to show the error message to browsers that don't support CSS variables. background: var(--documentBackground); - @include mobile { font-size: 93.75%; } + @include m.mobile { font-size: 93.75%; } - @include print { background: none; } - @import 'global/print'; + @include m.print { background: none; } } html._theme-default { diff --git a/assets/stylesheets/global/_print.scss b/assets/stylesheets/global/_print.scss index 08c14fd1f8..119bda52cb 100644 --- a/assets/stylesheets/global/_print.scss +++ b/assets/stylesheets/global/_print.scss @@ -1,4 +1,6 @@ -@include print { +@use 'global/mixins' as m; + +@include m.print { ._header, ._sidebar, ._path, ._notif, ._toc, ._pre-clip, ._notice, ._links { display: none !important; } diff --git a/assets/stylesheets/global/_variables-dark.scss b/assets/stylesheets/global/_variables-dark.scss index b9c88491a6..23a4b0a110 100644 --- a/assets/stylesheets/global/_variables-dark.scss +++ b/assets/stylesheets/global/_variables-dark.scss @@ -1,3 +1,5 @@ +@use 'sass:color'; + html._theme-dark { --absolute: white; @@ -29,8 +31,8 @@ html._theme-dark { --headerBackground: #1c1c1c; --headerBorder: #000; - --searchTagBackground: #{darken(#1c1c1c, 5%)}; - --searchBorder: #{darken(#000, 2%)}; + --searchTagBackground: #{color.adjust(#1c1c1c, $lightness: -5%)}; + --searchBorder: #{color.adjust(#000, $lightness: -2%)}; --sidebarBackground: #24282a; --transparentSidebarBackground: #{rgba(#24282a, 0)}; diff --git a/assets/stylesheets/global/_variables-light.scss b/assets/stylesheets/global/_variables-light.scss index f2dcb80721..e56e7e0839 100644 --- a/assets/stylesheets/global/_variables-light.scss +++ b/assets/stylesheets/global/_variables-light.scss @@ -1,3 +1,5 @@ +@use 'sass:color'; + html._theme-default { --absolute: black; @@ -29,8 +31,8 @@ html._theme-default { --headerBackground: #eee; --headerBorder: #d7d7d7; - --searchTagBackground: #{darken(#eee, 5%)}; - --searchBorder: #{darken(#d7d7d7, 2%)}; + --searchTagBackground: #{color.adjust(#eee, $lightness: -5%)}; + --searchBorder: #{color.adjust(#d7d7d7, $lightness: -2%)}; --sidebarBackground: #f9f9f9; --transparentSidebarBackground: #{rgba(#f9f9f9, 0)}; diff --git a/assets/stylesheets/global/_variables.scss b/assets/stylesheets/global/_variables.scss index 944db8aff0..7a86b805a9 100644 --- a/assets/stylesheets/global/_variables.scss +++ b/assets/stylesheets/global/_variables.scss @@ -1,3 +1,6 @@ +@use 'global/variables-light'; +@use 'global/variables-dark'; + // Variables needed to style the error message for browsers that don't support CSS variables. $baseFont: -apple-system, BlinkMacSystemFont, 'San Francisco', 'Segoe UI', Roboto, Ubuntu, 'Helvetica Neue', Arial, sans-serif; $textColor: #333; diff --git a/assets/stylesheets/pages/_angular.scss b/assets/stylesheets/pages/_angular.scss index 18b0d93c03..92bcd93cc8 100644 --- a/assets/stylesheets/pages/_angular.scss +++ b/assets/stylesheets/pages/_angular.scss @@ -1,3 +1,6 @@ +@use 'global/base'; +@use 'pages/simple'; + ._angular { @extend %simple; diff --git a/assets/stylesheets/pages/_angularjs.scss b/assets/stylesheets/pages/_angularjs.scss index 13f8579633..9c4da3d606 100644 --- a/assets/stylesheets/pages/_angularjs.scss +++ b/assets/stylesheets/pages/_angularjs.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._angularjs { h2 { @extend %block-heading; } diff --git a/assets/stylesheets/pages/_apache.scss b/assets/stylesheets/pages/_apache.scss index ff494bb52e..c1a353d455 100644 --- a/assets/stylesheets/pages/_apache.scss +++ b/assets/stylesheets/pages/_apache.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._apache { @extend %simple; diff --git a/assets/stylesheets/pages/_async.scss b/assets/stylesheets/pages/_async.scss index c8961dfdad..a7c4d9b292 100644 --- a/assets/stylesheets/pages/_async.scss +++ b/assets/stylesheets/pages/_async.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._async { @extend %simple; diff --git a/assets/stylesheets/pages/_bash.scss b/assets/stylesheets/pages/_bash.scss index 8d3f6dd7f0..25d84c2551 100644 --- a/assets/stylesheets/pages/_bash.scss +++ b/assets/stylesheets/pages/_bash.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._bash { dl > dt > code, dl > dt > kbd { diff --git a/assets/stylesheets/pages/_bootstrap.scss b/assets/stylesheets/pages/_bootstrap.scss index 42289ccb5a..1d52961e6a 100644 --- a/assets/stylesheets/pages/_bootstrap.scss +++ b/assets/stylesheets/pages/_bootstrap.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._bootstrap { @extend %simple; diff --git a/assets/stylesheets/pages/_cakephp.scss b/assets/stylesheets/pages/_cakephp.scss index 11825c4d8f..5b79ed3828 100644 --- a/assets/stylesheets/pages/_cakephp.scss +++ b/assets/stylesheets/pages/_cakephp.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._cakephp { @extend %simple; diff --git a/assets/stylesheets/pages/_celery.scss b/assets/stylesheets/pages/_celery.scss new file mode 100644 index 0000000000..d9735c7341 --- /dev/null +++ b/assets/stylesheets/pages/_celery.scss @@ -0,0 +1,14 @@ +@use 'pages/simple'; + +._celery { + @extend %simple; + + .note, .warning, .seealso { + @extend %note; + .admonition-title { font-weight: bold; } + } + + .note { @extend %note-gray; } + .seealso { @extend %note-green; } + .warning { @extend %note-red; } +} diff --git a/assets/stylesheets/pages/_chef.scss b/assets/stylesheets/pages/_chef.scss index 1314c4037f..7583e9ac3d 100644 --- a/assets/stylesheets/pages/_chef.scss +++ b/assets/stylesheets/pages/_chef.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._chef { @extend %simple; diff --git a/assets/stylesheets/pages/_clojure.scss b/assets/stylesheets/pages/_clojure.scss index c1b2647cc5..d5db5e14f9 100644 --- a/assets/stylesheets/pages/_clojure.scss +++ b/assets/stylesheets/pages/_clojure.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._clojure { h2:not([id]) { @extend %block-heading; } h2[id], h3 { @extend %block-label, %label-blue; } diff --git a/assets/stylesheets/pages/_codeception.scss b/assets/stylesheets/pages/_codeception.scss index 73576a4752..09e5a68a0b 100644 --- a/assets/stylesheets/pages/_codeception.scss +++ b/assets/stylesheets/pages/_codeception.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._codeception { @extend %simple; diff --git a/assets/stylesheets/pages/_coffeescript.scss b/assets/stylesheets/pages/_coffeescript.scss index af4125fd76..756fb86ed3 100644 --- a/assets/stylesheets/pages/_coffeescript.scss +++ b/assets/stylesheets/pages/_coffeescript.scss @@ -1,3 +1,6 @@ +@use 'pages/simple'; + + ._coffeescript { @extend %simple; diff --git a/assets/stylesheets/pages/_cordova.scss b/assets/stylesheets/pages/_cordova.scss index 01231ea63e..4b20cc9698 100644 --- a/assets/stylesheets/pages/_cordova.scss +++ b/assets/stylesheets/pages/_cordova.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._cordova { @extend %simple; diff --git a/assets/stylesheets/pages/_cppref.scss b/assets/stylesheets/pages/_cppref.scss index f50b297ddc..1b679ae518 100644 --- a/assets/stylesheets/pages/_cppref.scss +++ b/assets/stylesheets/pages/_cppref.scss @@ -1,3 +1,5 @@ +@use 'global/base'; + ._cppref { > h2, > h3 { @extend %block-heading; } > h4 { @extend %block-label, %label-blue; } diff --git a/assets/stylesheets/pages/_crystal.scss b/assets/stylesheets/pages/_crystal.scss index 9e8904a573..9ab5736f25 100644 --- a/assets/stylesheets/pages/_crystal.scss +++ b/assets/stylesheets/pages/_crystal.scss @@ -1,3 +1,6 @@ +@use 'global/base'; +@use 'pages/simple'; + ._crystal { @extend %simple; diff --git a/assets/stylesheets/pages/_cypress.scss b/assets/stylesheets/pages/_cypress.scss index aa1108d2ba..178d4c0994 100644 --- a/assets/stylesheets/pages/_cypress.scss +++ b/assets/stylesheets/pages/_cypress.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._cypress { @extend %simple; diff --git a/assets/stylesheets/pages/_d.scss b/assets/stylesheets/pages/_d.scss index 7e6721453a..d6c52a7d3f 100644 --- a/assets/stylesheets/pages/_d.scss +++ b/assets/stylesheets/pages/_d.scss @@ -1,3 +1,5 @@ +@use 'global/base'; + ._d { h2 { @extend %block-heading; } h3, .d_decl { @extend %block-label, %label-blue; } diff --git a/assets/stylesheets/pages/_d3.scss b/assets/stylesheets/pages/_d3.scss index 8357c721bf..d3ba4401de 100644 --- a/assets/stylesheets/pages/_d3.scss +++ b/assets/stylesheets/pages/_d3.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._d3 { > h2 { @extend %block-heading; } > h3 { @extend %block-label; } diff --git a/assets/stylesheets/pages/_dart.scss b/assets/stylesheets/pages/_dart.scss index fedd9883dd..b96febe084 100644 --- a/assets/stylesheets/pages/_dart.scss +++ b/assets/stylesheets/pages/_dart.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._dart { @extend %simple; diff --git a/assets/stylesheets/pages/_dojo.scss b/assets/stylesheets/pages/_dojo.scss index d22b1f8147..3398514380 100644 --- a/assets/stylesheets/pages/_dojo.scss +++ b/assets/stylesheets/pages/_dojo.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._dojo { @extend %simple; diff --git a/assets/stylesheets/pages/_drupal.scss b/assets/stylesheets/pages/_drupal.scss index 405aa83cfc..7c8eae0954 100644 --- a/assets/stylesheets/pages/_drupal.scss +++ b/assets/stylesheets/pages/_drupal.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._drupal { h3 { @extend %block-heading; } .signature { @extend %note, %note-blue; } diff --git a/assets/stylesheets/pages/_elisp.scss b/assets/stylesheets/pages/_elisp.scss index f8fbe58092..3d40a23570 100644 --- a/assets/stylesheets/pages/_elisp.scss +++ b/assets/stylesheets/pages/_elisp.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._elisp { dl > dt { @extend %block-label, %label-blue; diff --git a/assets/stylesheets/pages/_elixir.scss b/assets/stylesheets/pages/_elixir.scss index 240bbd83d9..2e2b55609a 100644 --- a/assets/stylesheets/pages/_elixir.scss +++ b/assets/stylesheets/pages/_elixir.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._elixir { @extend %simple; diff --git a/assets/stylesheets/pages/_ember.scss b/assets/stylesheets/pages/_ember.scss index 9b3f4689a7..d46775b6bc 100644 --- a/assets/stylesheets/pages/_ember.scss +++ b/assets/stylesheets/pages/_ember.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._ember { @extend %simple; diff --git a/assets/stylesheets/pages/_erlang.scss b/assets/stylesheets/pages/_erlang.scss index d723c1c13d..9ccad8565f 100644 --- a/assets/stylesheets/pages/_erlang.scss +++ b/assets/stylesheets/pages/_erlang.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._erlang { @extend %simple; diff --git a/assets/stylesheets/pages/_express.scss b/assets/stylesheets/pages/_express.scss index de1bf21ec5..7672b5bac5 100644 --- a/assets/stylesheets/pages/_express.scss +++ b/assets/stylesheets/pages/_express.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._express { @extend %simple; diff --git a/assets/stylesheets/pages/_fastapi.scss b/assets/stylesheets/pages/_fastapi.scss index 5f56c1ce61..b0fbb3b468 100644 --- a/assets/stylesheets/pages/_fastapi.scss +++ b/assets/stylesheets/pages/_fastapi.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._fastapi { > h2 { @extend %block-heading; } > h3 { @extend %block-label, %label-blue; } diff --git a/assets/stylesheets/pages/_fluture.scss b/assets/stylesheets/pages/_fluture.scss index e3072c6d23..806e8c64cd 100644 --- a/assets/stylesheets/pages/_fluture.scss +++ b/assets/stylesheets/pages/_fluture.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._fluture { @extend %simple; diff --git a/assets/stylesheets/pages/_git.scss b/assets/stylesheets/pages/_git.scss index 1d5cad3ce0..122764ee07 100644 --- a/assets/stylesheets/pages/_git.scss +++ b/assets/stylesheets/pages/_git.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._git { padding-left: 1rem; diff --git a/assets/stylesheets/pages/_github.scss b/assets/stylesheets/pages/_github.scss index bda57d4cc6..f785df9a14 100644 --- a/assets/stylesheets/pages/_github.scss +++ b/assets/stylesheets/pages/_github.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._github { @extend %simple; diff --git a/assets/stylesheets/pages/_gnu_make.scss b/assets/stylesheets/pages/_gnu_make.scss index 338b1668d0..79c6854d7d 100644 --- a/assets/stylesheets/pages/_gnu_make.scss +++ b/assets/stylesheets/pages/_gnu_make.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._gnu_make { dl dt { @extend %block-label, %label-blue; diff --git a/assets/stylesheets/pages/_gnuplot.scss b/assets/stylesheets/pages/_gnuplot.scss index b7d584e787..8fd9cbfe6d 100644 --- a/assets/stylesheets/pages/_gnuplot.scss +++ b/assets/stylesheets/pages/_gnuplot.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._gnuplot { .CENTER { text-align: center; diff --git a/assets/stylesheets/pages/_go.scss b/assets/stylesheets/pages/_go.scss index 6fd7a15e47..a82a707484 100644 --- a/assets/stylesheets/pages/_go.scss +++ b/assets/stylesheets/pages/_go.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._go { @extend %simple; diff --git a/assets/stylesheets/pages/_graphite.scss b/assets/stylesheets/pages/_graphite.scss index 341d581ce0..7bf392fdab 100644 --- a/assets/stylesheets/pages/_graphite.scss +++ b/assets/stylesheets/pages/_graphite.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._graphite { @extend %simple; diff --git a/assets/stylesheets/pages/_groovy.scss b/assets/stylesheets/pages/_groovy.scss index d8c150d165..9e71dbd4f8 100644 --- a/assets/stylesheets/pages/_groovy.scss +++ b/assets/stylesheets/pages/_groovy.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._groovy { padding-left: 1rem; diff --git a/assets/stylesheets/pages/_gtk.scss b/assets/stylesheets/pages/_gtk.scss index 9df0cd3e2a..4144dda97f 100644 --- a/assets/stylesheets/pages/_gtk.scss +++ b/assets/stylesheets/pages/_gtk.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._gtk { padding-left: 1rem; diff --git a/assets/stylesheets/pages/_hapi.scss b/assets/stylesheets/pages/_hapi.scss index 44ea884d23..2ac2b85bac 100644 --- a/assets/stylesheets/pages/_hapi.scss +++ b/assets/stylesheets/pages/_hapi.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._hapi { @extend %simple; diff --git a/assets/stylesheets/pages/_haproxy.scss b/assets/stylesheets/pages/_haproxy.scss index 24c49b4750..34db1c96ed 100644 --- a/assets/stylesheets/pages/_haproxy.scss +++ b/assets/stylesheets/pages/_haproxy.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._haproxy { padding-left: 1rem; diff --git a/assets/stylesheets/pages/_haskell.scss b/assets/stylesheets/pages/_haskell.scss index 4c98f778ab..057d3e0496 100644 --- a/assets/stylesheets/pages/_haskell.scss +++ b/assets/stylesheets/pages/_haskell.scss @@ -1,3 +1,5 @@ +@use 'global/base'; + ._haskell-api { > h2 { @extend %block-heading; } > h3 { @extend %block-label; } diff --git a/assets/stylesheets/pages/_jasmine.scss b/assets/stylesheets/pages/_jasmine.scss index 4a4b8d464f..4ff6ec9774 100644 --- a/assets/stylesheets/pages/_jasmine.scss +++ b/assets/stylesheets/pages/_jasmine.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._jasmine { .subsection-title, h2 { @extend %block-heading; } h4 { @extend %block-label, %label-blue; } diff --git a/assets/stylesheets/pages/_jekyll.scss b/assets/stylesheets/pages/_jekyll.scss index 00a86795e6..8320358ea1 100644 --- a/assets/stylesheets/pages/_jekyll.scss +++ b/assets/stylesheets/pages/_jekyll.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._jekyll { @extend %simple; diff --git a/assets/stylesheets/pages/_joi.scss b/assets/stylesheets/pages/_joi.scss index 53cd740bac..14fbac6475 100644 --- a/assets/stylesheets/pages/_joi.scss +++ b/assets/stylesheets/pages/_joi.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._joi { @extend %simple; diff --git a/assets/stylesheets/pages/_jq.scss b/assets/stylesheets/pages/_jq.scss index 652522238b..da236b9b7e 100644 --- a/assets/stylesheets/pages/_jq.scss +++ b/assets/stylesheets/pages/_jq.scss @@ -1,3 +1,6 @@ +@use 'global/base'; +@use 'pages/simple'; + ._jq { @extend %simple; diff --git a/assets/stylesheets/pages/_jquery.scss b/assets/stylesheets/pages/_jquery.scss index d1a62a1cb0..ad814d5b66 100644 --- a/assets/stylesheets/pages/_jquery.scss +++ b/assets/stylesheets/pages/_jquery.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._jquery { // // Index page diff --git a/assets/stylesheets/pages/_julia.scss b/assets/stylesheets/pages/_julia.scss index 4887da64c6..26b18b232d 100644 --- a/assets/stylesheets/pages/_julia.scss +++ b/assets/stylesheets/pages/_julia.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._julia { @extend %simple; diff --git a/assets/stylesheets/pages/_knockout.scss b/assets/stylesheets/pages/_knockout.scss index eb21ad10ff..868f1e9efe 100644 --- a/assets/stylesheets/pages/_knockout.scss +++ b/assets/stylesheets/pages/_knockout.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._knockout { > h2 { @extend %block-heading; } > h3 { @extend %block-label, %label-blue; } diff --git a/assets/stylesheets/pages/_kotlin.scss b/assets/stylesheets/pages/_kotlin.scss index 9857a6a368..cf503a01ec 100644 --- a/assets/stylesheets/pages/_kotlin.scss +++ b/assets/stylesheets/pages/_kotlin.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._kotlin { h2 { @extend %block-heading; } h3 { @extend %block-label, %label-blue; } diff --git a/assets/stylesheets/pages/_kubectl.scss b/assets/stylesheets/pages/_kubectl.scss index afd3090d7a..e79ff5045f 100644 --- a/assets/stylesheets/pages/_kubectl.scss +++ b/assets/stylesheets/pages/_kubectl.scss @@ -1,4 +1,5 @@ +@use 'pages/simple'; + ._kubectl { @extend %simple; - } diff --git a/assets/stylesheets/pages/_kubernetes.scss b/assets/stylesheets/pages/_kubernetes.scss index 52791ba3e1..1bf3624eb6 100644 --- a/assets/stylesheets/pages/_kubernetes.scss +++ b/assets/stylesheets/pages/_kubernetes.scss @@ -1,4 +1,5 @@ +@use 'pages/simple'; + ._kubernetes { @extend %simple; - } diff --git a/assets/stylesheets/pages/_laravel.scss b/assets/stylesheets/pages/_laravel.scss index cf9e11eb08..3b11a9014a 100644 --- a/assets/stylesheets/pages/_laravel.scss +++ b/assets/stylesheets/pages/_laravel.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._laravel { h2 { @extend %block-heading; } h3 { @extend %block-label, %label-blue; } diff --git a/assets/stylesheets/pages/_liquid.scss b/assets/stylesheets/pages/_liquid.scss index 9072acc050..7fda2ed757 100644 --- a/assets/stylesheets/pages/_liquid.scss +++ b/assets/stylesheets/pages/_liquid.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._liquid { @extend %simple; diff --git a/assets/stylesheets/pages/_lit.scss b/assets/stylesheets/pages/_lit.scss index f0066d80a1..def8d40647 100644 --- a/assets/stylesheets/pages/_lit.scss +++ b/assets/stylesheets/pages/_lit.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._lit { @extend %simple; diff --git a/assets/stylesheets/pages/_love.scss b/assets/stylesheets/pages/_love.scss index a43826070b..79579f7842 100644 --- a/assets/stylesheets/pages/_love.scss +++ b/assets/stylesheets/pages/_love.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._love { @extend %simple; diff --git a/assets/stylesheets/pages/_lua.scss b/assets/stylesheets/pages/_lua.scss index 68cca61e54..0d4ca9bb33 100644 --- a/assets/stylesheets/pages/_lua.scss +++ b/assets/stylesheets/pages/_lua.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._lua { @extend %simple; diff --git a/assets/stylesheets/pages/_maplibre_gl.scss b/assets/stylesheets/pages/_maplibre_gl.scss new file mode 100644 index 0000000000..513943e9f1 --- /dev/null +++ b/assets/stylesheets/pages/_maplibre_gl.scss @@ -0,0 +1,17 @@ +@use 'global/classes'; + +._maplibre_gl { + > h1 { @extend %block-heading; } + > h2 { @extend %block-heading; } + > h3 { @extend %block-label, %label-blue; } + + code { @extend %label; } + + .admonition { @extend %note; } + .admonition.tip { @extend %note-green; } + .admonition.note, .admonition.info { @extend %note-blue; } + .admonition.warning { @extend %note-orange; } + .admonition-title { + font-weight: var(--bolderFontWeight); + } +} diff --git a/assets/stylesheets/pages/_mariadb.scss b/assets/stylesheets/pages/_mariadb.scss index 19d8d6398b..8f30614dc4 100644 --- a/assets/stylesheets/pages/_mariadb.scss +++ b/assets/stylesheets/pages/_mariadb.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._mariadb { @extend %simple; diff --git a/assets/stylesheets/pages/_mdn.scss b/assets/stylesheets/pages/_mdn.scss index 1bc6b8d831..1508491a84 100644 --- a/assets/stylesheets/pages/_mdn.scss +++ b/assets/stylesheets/pages/_mdn.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._mdn { container-type: inline-size; @@ -126,6 +128,7 @@ } .bc-supports-yes, .bc-supports-yes + dd, .bc-supports-yes + dd + dd { background: var(--noteGreenBackground); } + .bc-supports-preview, .bc-supports-preview + dd, .bc-supports-preview + dd + dd { background: var(--noteBlueBackground); } .bc-supports-unknown, .bc-supports-unknown + dd, .bc-supports-unknown + dd + dd { background: var(--noteBackground); } .bc-supports-partial, .bc-supports-partial + dd, .bc-supports-partial + dd + dd { background: var(--noteOrangeBackground); } .bc-supports-no, .bc-supports-no + dd, .bc-supports-no + dd + dd { background: var(--noteRedBackground); } diff --git a/assets/stylesheets/pages/_meteor.scss b/assets/stylesheets/pages/_meteor.scss index 5497239f78..b67d410595 100644 --- a/assets/stylesheets/pages/_meteor.scss +++ b/assets/stylesheets/pages/_meteor.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._meteor { @extend %simple; diff --git a/assets/stylesheets/pages/_mkdocs.scss b/assets/stylesheets/pages/_mkdocs.scss index e374474c84..e19e3c94a3 100644 --- a/assets/stylesheets/pages/_mkdocs.scss +++ b/assets/stylesheets/pages/_mkdocs.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._mkdocs { h2 { @extend %block-heading; } h3 { @extend %block-label, %label-blue; } diff --git a/assets/stylesheets/pages/_modernizr.scss b/assets/stylesheets/pages/_modernizr.scss index eb34fe3739..297ebd4de8 100644 --- a/assets/stylesheets/pages/_modernizr.scss +++ b/assets/stylesheets/pages/_modernizr.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._modernizr { h2 { @extend %block-heading; } h3 { @extend %block-label, %label-blue; } diff --git a/assets/stylesheets/pages/_moment.scss b/assets/stylesheets/pages/_moment.scss index eb5eb39928..2e09ac1bb1 100644 --- a/assets/stylesheets/pages/_moment.scss +++ b/assets/stylesheets/pages/_moment.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._moment { > h2 { @extend %block-heading; } > h3 { @extend %block-label, %label-blue; } diff --git a/assets/stylesheets/pages/_nginx.scss b/assets/stylesheets/pages/_nginx.scss index 30d09449bf..63947fa8c4 100644 --- a/assets/stylesheets/pages/_nginx.scss +++ b/assets/stylesheets/pages/_nginx.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._nginx { h4 { @extend %block-heading; } .note { @extend %note; } diff --git a/assets/stylesheets/pages/_node.scss b/assets/stylesheets/pages/_node.scss index 2853a59637..ac64d8351f 100644 --- a/assets/stylesheets/pages/_node.scss +++ b/assets/stylesheets/pages/_node.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._node { // https://nodejs.org/api/documentation.html#stability-index .api_stability { clear: both; } diff --git a/assets/stylesheets/pages/_npm.scss b/assets/stylesheets/pages/_npm.scss index bdb4482d61..4951ad92a7 100644 --- a/assets/stylesheets/pages/_npm.scss +++ b/assets/stylesheets/pages/_npm.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._npm { @extend %simple; diff --git a/assets/stylesheets/pages/_nushell.scss b/assets/stylesheets/pages/_nushell.scss index 6aa4eb2bbf..2c9c2e76e4 100644 --- a/assets/stylesheets/pages/_nushell.scss +++ b/assets/stylesheets/pages/_nushell.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._nushell { @extend %simple; diff --git a/assets/stylesheets/pages/_octave.scss b/assets/stylesheets/pages/_octave.scss index 1dfc3f8ca2..ab78576da4 100644 --- a/assets/stylesheets/pages/_octave.scss +++ b/assets/stylesheets/pages/_octave.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._octave { @extend %simple; diff --git a/assets/stylesheets/pages/_openjdk.scss b/assets/stylesheets/pages/_openjdk.scss index 7330951a56..b3728bd4d9 100644 --- a/assets/stylesheets/pages/_openjdk.scss +++ b/assets/stylesheets/pages/_openjdk.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._openjdk { @extend %simple; > .inheritance { @extend %note; } diff --git a/assets/stylesheets/pages/_openlayers.scss b/assets/stylesheets/pages/_openlayers.scss index 50628b2ff3..f5d4d6773e 100644 --- a/assets/stylesheets/pages/_openlayers.scss +++ b/assets/stylesheets/pages/_openlayers.scss @@ -1,3 +1,6 @@ +@use 'global/base'; +@use 'pages/simple'; + ._openlayers { @extend %simple; .nameContainer { diff --git a/assets/stylesheets/pages/_opentofu.scss b/assets/stylesheets/pages/_opentofu.scss new file mode 100644 index 0000000000..c8c9106c0f --- /dev/null +++ b/assets/stylesheets/pages/_opentofu.scss @@ -0,0 +1,22 @@ +@use 'pages/simple'; + +._opentofu { + .theme-code-block figure { + margin-inline-start: 0; + } + + // "sr" means screen reader + .sr-only { + clip: rect(0,0,0,0); + border-width: 0; + height: 1px; + margin: -1px; + padding: 0; + position: absolute; + width: 1px + } + + .font-bold { + font-weight: var(--boldFontWeight); + } +} diff --git a/assets/stylesheets/pages/_perl.scss b/assets/stylesheets/pages/_perl.scss index 777eac8eec..a35b006c7c 100644 --- a/assets/stylesheets/pages/_perl.scss +++ b/assets/stylesheets/pages/_perl.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._perl { @extend %simple; diff --git a/assets/stylesheets/pages/_phalcon.scss b/assets/stylesheets/pages/_phalcon.scss index 60a32758a6..ac2d6c045c 100644 --- a/assets/stylesheets/pages/_phalcon.scss +++ b/assets/stylesheets/pages/_phalcon.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._phalcon { @extend %simple; diff --git a/assets/stylesheets/pages/_phaser.scss b/assets/stylesheets/pages/_phaser.scss index 2effc2aee0..0c3cf5aa30 100644 --- a/assets/stylesheets/pages/_phaser.scss +++ b/assets/stylesheets/pages/_phaser.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._phaser { @extend %simple; diff --git a/assets/stylesheets/pages/_php.scss b/assets/stylesheets/pages/_php.scss index c6fd4d6bc6..e381dad828 100644 --- a/assets/stylesheets/pages/_php.scss +++ b/assets/stylesheets/pages/_php.scss @@ -1,3 +1,6 @@ +@use 'global/classes'; +@use 'components/content'; + ._php { h1 { margin-top: 0; diff --git a/assets/stylesheets/pages/_phpunit.scss b/assets/stylesheets/pages/_phpunit.scss index 2178441330..8d3ee3e4af 100644 --- a/assets/stylesheets/pages/_phpunit.scss +++ b/assets/stylesheets/pages/_phpunit.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._phpunit { @extend %simple; diff --git a/assets/stylesheets/pages/_postgres.scss b/assets/stylesheets/pages/_postgres.scss index 7c42a00139..b4dae591ca 100644 --- a/assets/stylesheets/pages/_postgres.scss +++ b/assets/stylesheets/pages/_postgres.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._postgres { padding-left: 1rem; diff --git a/assets/stylesheets/pages/_pug.scss b/assets/stylesheets/pages/_pug.scss index 820c92118f..489bc01624 100644 --- a/assets/stylesheets/pages/_pug.scss +++ b/assets/stylesheets/pages/_pug.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._pug { @extend %simple; diff --git a/assets/stylesheets/pages/_pygame.scss b/assets/stylesheets/pages/_pygame.scss index 380af31f52..56bffcb150 100644 --- a/assets/stylesheets/pages/_pygame.scss +++ b/assets/stylesheets/pages/_pygame.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._pygame { @extend %simple; diff --git a/assets/stylesheets/pages/_python.scss b/assets/stylesheets/pages/_python.scss index 78f2d4dbe3..a557cc5c07 100644 --- a/assets/stylesheets/pages/_python.scss +++ b/assets/stylesheets/pages/_python.scss @@ -1,3 +1,5 @@ +@use 'pages/sphinx'; + ._python { @extend %sphinx; diff --git a/assets/stylesheets/pages/_qt.scss b/assets/stylesheets/pages/_qt.scss index 360776c060..81a5756bdb 100644 --- a/assets/stylesheets/pages/_qt.scss +++ b/assets/stylesheets/pages/_qt.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._qt { @extend %simple; diff --git a/assets/stylesheets/pages/_rabbit_mq.scss b/assets/stylesheets/pages/_rabbit_mq.scss new file mode 100644 index 0000000000..fe29e8a41e --- /dev/null +++ b/assets/stylesheets/pages/_rabbit_mq.scss @@ -0,0 +1,17 @@ +@use 'pages/simple'; + +._rabbit_mq { + @extend %simple; + + .alert--info, .alert--warning, .alert--success, .alert--secondary { + @extend %note; + .admonitionHeading_Gvgb { + font-weight: 700; + text-transform: uppercase; + } + } + .alert--success { @extend %note, %note-green; } + .alert--info { @extend %note, %note-blue; } + .alert--warning { @extend %note, %note-orange; } + .alert--secondary { @extend %note, %note-gray; } +} diff --git a/assets/stylesheets/pages/_ramda.scss b/assets/stylesheets/pages/_ramda.scss index a94530575c..913292aa88 100644 --- a/assets/stylesheets/pages/_ramda.scss +++ b/assets/stylesheets/pages/_ramda.scss @@ -1,3 +1,6 @@ +@use 'global/base'; +@use 'pages/simple'; + ._ramda { @extend %simple; diff --git a/assets/stylesheets/pages/_rdoc.scss b/assets/stylesheets/pages/_rdoc.scss index 6622e68e39..2648aa4a71 100644 --- a/assets/stylesheets/pages/_rdoc.scss +++ b/assets/stylesheets/pages/_rdoc.scss @@ -1,3 +1,6 @@ +@use 'global/classes'; +@use 'global/mixins' as m; + ._rdoc { > .description, > .documentation-section { padding-left: 1rem; } > .description > h2, header > h3, > h2 { @extend %block-heading; } @@ -28,7 +31,7 @@ &:hover { text-decoration: underline; } - @include print { + @include m.print { display: none; } } diff --git a/assets/stylesheets/pages/_react.scss b/assets/stylesheets/pages/_react.scss index 6bb32bdfe2..a34d3c194a 100644 --- a/assets/stylesheets/pages/_react.scss +++ b/assets/stylesheets/pages/_react.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._react { @extend %simple; diff --git a/assets/stylesheets/pages/_react_native.scss b/assets/stylesheets/pages/_react_native.scss index 824c9d19da..3bf0dfd005 100644 --- a/assets/stylesheets/pages/_react_native.scss +++ b/assets/stylesheets/pages/_react_native.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._react_native { @extend %simple; diff --git a/assets/stylesheets/pages/_reactivex.scss b/assets/stylesheets/pages/_reactivex.scss index dd67b850a0..146267bd7c 100644 --- a/assets/stylesheets/pages/_reactivex.scss +++ b/assets/stylesheets/pages/_reactivex.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._reactivex { @extend %simple; diff --git a/assets/stylesheets/pages/_redis.scss b/assets/stylesheets/pages/_redis.scss index e4541f8b5a..a6237b540f 100644 --- a/assets/stylesheets/pages/_redis.scss +++ b/assets/stylesheets/pages/_redis.scss @@ -1,3 +1,6 @@ +@use 'global/base'; +@use 'global/classes'; + ._redis { padding-left: 1rem; diff --git a/assets/stylesheets/pages/_rethinkdb.scss b/assets/stylesheets/pages/_rethinkdb.scss index 0a64abd41f..390ef9cdf3 100644 --- a/assets/stylesheets/pages/_rethinkdb.scss +++ b/assets/stylesheets/pages/_rethinkdb.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._rethinkdb { @extend %simple; diff --git a/assets/stylesheets/pages/_rfc.scss b/assets/stylesheets/pages/_rfc.scss index eab55beb59..6cbc59765c 100644 --- a/assets/stylesheets/pages/_rfc.scss +++ b/assets/stylesheets/pages/_rfc.scss @@ -1,3 +1,5 @@ +@use 'global/base'; + ._rfc-pre { font-size: .8125rem; min-width: 38rem; diff --git a/assets/stylesheets/pages/_rubydoc.scss b/assets/stylesheets/pages/_rubydoc.scss index c28d0815ba..dcdbc83c9f 100644 --- a/assets/stylesheets/pages/_rubydoc.scss +++ b/assets/stylesheets/pages/_rubydoc.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._rubydoc { @extend %simple; diff --git a/assets/stylesheets/pages/_rust.scss b/assets/stylesheets/pages/_rust.scss index 47cc09ca2d..f73af8ba50 100644 --- a/assets/stylesheets/pages/_rust.scss +++ b/assets/stylesheets/pages/_rust.scss @@ -1,3 +1,6 @@ +@use 'global/base'; +@use 'pages/simple'; + ._rust { @extend %simple; diff --git a/assets/stylesheets/pages/_rxjs.scss b/assets/stylesheets/pages/_rxjs.scss index 15e1252b6a..4c3e51620e 100644 --- a/assets/stylesheets/pages/_rxjs.scss +++ b/assets/stylesheets/pages/_rxjs.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._rxjs { @extend %simple; diff --git a/assets/stylesheets/pages/_sanctuary.scss b/assets/stylesheets/pages/_sanctuary.scss index 45f5a8398f..639f5551c4 100644 --- a/assets/stylesheets/pages/_sanctuary.scss +++ b/assets/stylesheets/pages/_sanctuary.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._sanctuary { @extend %simple; diff --git a/assets/stylesheets/pages/_sanctuary_def.scss b/assets/stylesheets/pages/_sanctuary_def.scss index 738c551c6e..ba21063379 100644 --- a/assets/stylesheets/pages/_sanctuary_def.scss +++ b/assets/stylesheets/pages/_sanctuary_def.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._sanctuary_def { @extend %simple; diff --git a/assets/stylesheets/pages/_sanctuary_type_classes.scss b/assets/stylesheets/pages/_sanctuary_type_classes.scss index eb20d10a8d..6bb1fd56b7 100644 --- a/assets/stylesheets/pages/_sanctuary_type_classes.scss +++ b/assets/stylesheets/pages/_sanctuary_type_classes.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._sanctuary_type_classes { @extend %simple; diff --git a/assets/stylesheets/pages/_scala.scss b/assets/stylesheets/pages/_scala.scss index 94b8f7b451..343c6f2e70 100644 --- a/assets/stylesheets/pages/_scala.scss +++ b/assets/stylesheets/pages/_scala.scss @@ -1,3 +1,7 @@ +@use 'global/base'; +@use 'global/mixins' as m; +@use 'pages/simple'; + ._scala { @extend %simple; @@ -22,7 +26,7 @@ a { padding: .4em } - @include print { + @include m.print { display: none; } } @@ -36,7 +40,7 @@ &:hover { text-decoration: underline; } - @include print { + @include m.print { display: none; } } diff --git a/assets/stylesheets/pages/_simple.scss b/assets/stylesheets/pages/_simple.scss index 64157a8625..755f96c156 100644 --- a/assets/stylesheets/pages/_simple.scss +++ b/assets/stylesheets/pages/_simple.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + %simple { padding-left: 1rem; diff --git a/assets/stylesheets/pages/_sinon.scss b/assets/stylesheets/pages/_sinon.scss index 5df93ddd92..79b24e4276 100644 --- a/assets/stylesheets/pages/_sinon.scss +++ b/assets/stylesheets/pages/_sinon.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._sinon { @extend %simple; diff --git a/assets/stylesheets/pages/_sphinx.scss b/assets/stylesheets/pages/_sphinx.scss index cb23ef6922..cbe8b53630 100644 --- a/assets/stylesheets/pages/_sphinx.scss +++ b/assets/stylesheets/pages/_sphinx.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + %sphinx { h2 { @extend %block-heading; } h3 { @extend %block-label; } diff --git a/assets/stylesheets/pages/_sphinx_simple.scss b/assets/stylesheets/pages/_sphinx_simple.scss index cbee1ea19d..49327a95a8 100644 --- a/assets/stylesheets/pages/_sphinx_simple.scss +++ b/assets/stylesheets/pages/_sphinx_simple.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._sphinx_simple { @extend %simple; diff --git a/assets/stylesheets/pages/_sqlite.scss b/assets/stylesheets/pages/_sqlite.scss index b84047a54d..75ed75044b 100644 --- a/assets/stylesheets/pages/_sqlite.scss +++ b/assets/stylesheets/pages/_sqlite.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._sqlite { @extend %simple; diff --git a/assets/stylesheets/pages/_support_tables.scss b/assets/stylesheets/pages/_support_tables.scss index 1fc45b752f..3e90d257aa 100644 --- a/assets/stylesheets/pages/_support_tables.scss +++ b/assets/stylesheets/pages/_support_tables.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._support_tables { h2 { @extend %block-heading; } code { @extend %label; } diff --git a/assets/stylesheets/pages/_tcl_tk.scss b/assets/stylesheets/pages/_tcl_tk.scss index 3731e59d52..acf87807bb 100644 --- a/assets/stylesheets/pages/_tcl_tk.scss +++ b/assets/stylesheets/pages/_tcl_tk.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._tcl_tk { @extend %simple; diff --git a/assets/stylesheets/pages/_tensorflow.scss b/assets/stylesheets/pages/_tensorflow.scss index 3967e9c36c..4e7a2fd418 100644 --- a/assets/stylesheets/pages/_tensorflow.scss +++ b/assets/stylesheets/pages/_tensorflow.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._tensorflow { @extend %simple; diff --git a/assets/stylesheets/pages/_terraform.scss b/assets/stylesheets/pages/_terraform.scss index 6b3123aad9..aacf4ef8b7 100644 --- a/assets/stylesheets/pages/_terraform.scss +++ b/assets/stylesheets/pages/_terraform.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._terraform { @extend %simple; .note, .alert { @extend %note; } diff --git a/assets/stylesheets/pages/_tokio.scss b/assets/stylesheets/pages/_tokio.scss new file mode 100644 index 0000000000..6c9f50dba4 --- /dev/null +++ b/assets/stylesheets/pages/_tokio.scss @@ -0,0 +1,10 @@ +@use 'pages/simple'; + +._tokio { + @extend %simple; + + .stab { + @extend %note; + @extend %note-orange; + } +} diff --git a/assets/stylesheets/pages/_typescript.scss b/assets/stylesheets/pages/_typescript.scss index ffe7282a12..1c49e600d6 100644 --- a/assets/stylesheets/pages/_typescript.scss +++ b/assets/stylesheets/pages/_typescript.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._typescript { @extend %simple; .deprecated { @extend %label-red; } diff --git a/assets/stylesheets/pages/_underscore.scss b/assets/stylesheets/pages/_underscore.scss index f592e390bd..38e85bb13e 100644 --- a/assets/stylesheets/pages/_underscore.scss +++ b/assets/stylesheets/pages/_underscore.scss @@ -1,3 +1,5 @@ +@use 'global/classes'; + ._underscore { padding-left: 1rem; diff --git a/assets/stylesheets/pages/_vue.scss b/assets/stylesheets/pages/_vue.scss index 24b77e66c6..0897a2e697 100644 --- a/assets/stylesheets/pages/_vue.scss +++ b/assets/stylesheets/pages/_vue.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._vue { @extend %simple; diff --git a/assets/stylesheets/pages/_webpack.scss b/assets/stylesheets/pages/_webpack.scss index 487e02693d..d2a4bba59d 100644 --- a/assets/stylesheets/pages/_webpack.scss +++ b/assets/stylesheets/pages/_webpack.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._webpack { @extend %simple; diff --git a/assets/stylesheets/pages/_wordpress.scss b/assets/stylesheets/pages/_wordpress.scss index 1da15abd23..b995541486 100644 --- a/assets/stylesheets/pages/_wordpress.scss +++ b/assets/stylesheets/pages/_wordpress.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._wordpress { @extend %simple; @@ -12,4 +14,4 @@ .callout-alert { @extend %note, %note-orange; } -} \ No newline at end of file +} diff --git a/assets/stylesheets/pages/_yard.scss b/assets/stylesheets/pages/_yard.scss index 195059f768..ace2ff4ab3 100644 --- a/assets/stylesheets/pages/_yard.scss +++ b/assets/stylesheets/pages/_yard.scss @@ -1,3 +1,6 @@ +@use 'global/classes'; +@use 'pages/simple'; + ._yard { @extend %simple; diff --git a/assets/stylesheets/pages/_yii.scss b/assets/stylesheets/pages/_yii.scss index 6cb99978dc..a53e5c3068 100644 --- a/assets/stylesheets/pages/_yii.scss +++ b/assets/stylesheets/pages/_yii.scss @@ -1,3 +1,5 @@ +@use 'pages/simple'; + ._yii { @extend %simple; diff --git a/docs/file-scrapers.md b/docs/file-scrapers.md index 076510fcd9..058fbec644 100644 --- a/docs/file-scrapers.md +++ b/docs/file-scrapers.md @@ -62,7 +62,7 @@ bsdtar --extract --file - --directory=docs/erlang\~$VERSION/ ## es-toolkit ```sh -git clone https://github.com/toss/es-toolkit docs/es_toolkit +git clone --branch v$RELEASE https://github.com/toss/es-toolkit docs/es_toolkit ``` ## Gnu @@ -197,7 +197,7 @@ mv ./usr/share/doc/openjdk-16-jre-headless/api/ docs/openjdk~$VERSION From the home directory; `devdocs`, execute below: ```sh -curl https://pandas.pydata.org/docs/pandas.zip -o tmp.zip && unzip tmp.zip -d docs/pandas~2 && rm tmp.zip +curl https://pandas.pydata.org/docs/pandas.zip -o tmp.zip && unzip tmp.zip -d docs/pandas~3 && rm tmp.zip ``` @@ -262,23 +262,22 @@ done ### Ruby / Minitest ```sh -git clone https://github.com/seattlerb/minitest +git clone https://github.com/minitest/minitest cd minitest/ +echo -e "source 'https://rubygems.org'\n\ngem 'hoe'\ngem 'rdoc', '< 7'" > Gemfile bundle install -bundle add rdoc hoe -bundle exec rak docs -cd .. -cp -r minitest/docs $DEVDOCS/docs/minitest +bundle exec rake docs +cp -r docs $DEVDOCS/docs/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) +* Run `git clone --branch v$RELEASE --depth 7 https://github.com/rails/rails.git && cd rails` * 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 && cd ..` * Run `cp -r guides/output html/guides` -* Run `cp -r html $DEVDOCS/docs/rails~[version]` +* Run `cp -r html $DEVDOCS/docs/rails~$VERSION` ### Ruby Download the tarball of Ruby from https://www.ruby-lang.org/en/downloads/, extract it, run @@ -321,3 +320,9 @@ mv three.js-r${VERSION}/docs/* docs/threejs~${VERSION}/ rm -rf three.js-r${VERSION}/ rm threejs.tar.gz ``` + +## PowerShell + +```sh +curl -L 'https://github.com/MicrosoftDocs/PowerShell-Docs/archive/refs/heads/main.zip' | bsdtar --extract --file - --directory=docs/powershell/ --strip-components=2 +``` diff --git a/lib/app.rb b/lib/app.rb index 675936fbac..3b59b526e1 100644 --- a/lib/app.rb +++ b/lib/app.rb @@ -181,7 +181,7 @@ def user_has_docs?(slug) def doc_index_urls docs.each_with_object [] do |slug, result| if doc = settings.docs[slug] - result << File.join('', settings.docs_prefix, slug, 'index.json') + "?#{doc['mtime']}" + result << "#{settings.docs_origin}/#{slug}/index.json?#{doc['mtime']}" end end end @@ -232,29 +232,6 @@ def service_worker_cache_name end end - def redirect_via_js(path) - response.set_cookie :initial_path, value: path, expires: Time.now + 15, path: '/' - redirect '/', 302 - end - - def supports_js_redirection? - modern_browser?(browser) && !memoized_cookies.empty? - end - - # https://github.com/fnando/browser#detecting-modern-browsers - # https://github.com/fnando/browser/blob/v2.6.1/lib/browser/browser.rb - # This restores the old browser gem `#modern?` functionality as it was in 2.6.1 - # It's possible this isn't even really needed any longer, these versions are quite old now - def modern_browser?(browser) - [ - browser.webkit?, - browser.firefox? && browser.version.to_i >= 17, - browser.ie? && browser.version.to_i >= 9 && !browser.compatibility_view?, - browser.edge? && !browser.compatibility_view?, - browser.opera? && browser.version.to_i >= 12, - browser.firefox? && browser.device.tablet? && browser.platform.android? && b.version.to_i >= 14 - ].any? - end end before do @@ -285,11 +262,8 @@ def modern_browser?(browser) %w(settings offline about news help).each do |page| get "/#{page}" do - if supports_js_redirection? - redirect_via_js "/#{page}" - else - redirect "/#/#{page}", 302 - end + response.headers['Content-Security-Policy'] = settings.csp if settings.csp + erb :index end end @@ -424,8 +398,6 @@ def modern_browser?(browser) redirect "/#{doc}#{type}/#{query_string_for_redirection}" elsif rest.length > 1 && rest.end_with?('/') redirect "/#{doc}#{type}#{rest[0...-1]}#{query_string_for_redirection}" - elsif user_has_docs?(doc) && supports_js_redirection? - redirect_via_js(request.path) else response.headers['Content-Security-Policy'] = settings.csp if settings.csp erb :other diff --git a/lib/docs/filters/ansible/clean_html.rb b/lib/docs/filters/ansible/clean_html.rb index 27054785e5..b1dc0a8baf 100644 --- a/lib/docs/filters/ansible/clean_html.rb +++ b/lib/docs/filters/ansible/clean_html.rb @@ -14,6 +14,10 @@ def call end end + css('pre[data-language="yaml+jinja"]').each do |node| + node['data-language'] = 'yaml' + end + doc end diff --git a/lib/docs/filters/ansible/entries.rb b/lib/docs/filters/ansible/entries.rb index a859754ee8..e63a0e22d6 100644 --- a/lib/docs/filters/ansible/entries.rb +++ b/lib/docs/filters/ansible/entries.rb @@ -11,7 +11,7 @@ def get_name if version >= "2.10" || version == "" if slug =~ /\Acollections\// and slug !~ /index$/ - name = name.split('.')[2] + name = name.split(' – ').first end end diff --git a/lib/docs/filters/axios/clean_html.rb b/lib/docs/filters/axios/clean_html.rb index ab71d1ad4c..e772344e3a 100644 --- a/lib/docs/filters/axios/clean_html.rb +++ b/lib/docs/filters/axios/clean_html.rb @@ -7,6 +7,7 @@ def call end @doc = at_css('main > .body') css('.links').remove + css('.sponsors_container').remove css('pre').each do |node| node.content = node.content node['data-language'] = node['class'][/lang-(\w+)/, 1] diff --git a/lib/docs/filters/bash/clean_html.rb b/lib/docs/filters/bash/clean_html.rb index 1baf1629c1..a6e2f2a1c5 100644 --- a/lib/docs/filters/bash/clean_html.rb +++ b/lib/docs/filters/bash/clean_html.rb @@ -4,16 +4,16 @@ class CleanHtmlFilter < Filter def call @doc = at_css('> div[id]') if at_css('> div[id]') # Remove the navigation header and footer and the lines underneath and above it - at_css('.header + hr').remove + at_css('.nav-panel + hr').try(:remove) line_above = at_xpath('//div[@class="header"]/preceding::hr[1]') line_above.remove unless line_above.nil? - css('.header').remove + css('.nav-panel').remove - css('.copiable-anchor').remove + css('.copiable-anchor', '.copiable-link').remove # Remove chapter and section numbers from title title_node = at_css('h1, h2, h3, h4, h5, h6') - title_node.content = title_node.content.gsub(/(\d+\.?)+/, '').strip + title_node.content = title_node.content.gsub(/(\d+\.?)+/, '').gsub('¶', '').strip title_node.name = 'h1' # Remove the "D. " from names like "D. Concept Index" and "D. Function Index" @@ -23,39 +23,32 @@ def call # In the original reference they are used to add width between two columns xpath('//td[text()=" " and not(descendant::*)]').remove - # Add id's to additional entry nodes - css('dl > dt > code').each do |node| - # Only take the direct text (i.e. "

    Hello World
    " becomes "Hello") - node['id'] = node.xpath('text()').to_s.strip - end - - # Fix hashes of index entries so they link to the correct hash on the linked page - css('table[class^=index-] td[valign=top] > a').each do |node| - path = node['href'].split('#')[0] - hash = node.content - - # Fix the index entries linking to the Special Parameters page - # There are multiple index entries that should link to the same paragraph on that page - # Example: the documentation for "$!" is equal to the documentation for "!" - if path.downcase.include?('special-parameters') - if hash.size > 1 && hash[0] == '$' - hash = hash[1..-1] - end + # The manual marks index targets with empty anchors (), which the + # clean_text filter strips. Move their ids onto the enclosing node so the index entries + # keep linking to the right paragraph. + css('a[id]').each do |node| + next unless node.content.strip.empty? + parent = node.parent + if parent && parent['id'].blank? + parent['id'] = node['id'] + else + # Several anchors can share a parent; keep the extra ones alive by giving + # them content the clean_text filter doesn't consider empty + node.content = "​" end - - node['href'] = path + '#' + hash - end - - # Fix index table letter hashes (the "Jump to" hashes) - css('table[class^=index-] th > a').each do |node| - node['id'] = node['name'] end # Remove the rows with a horizontal line in them from the index tables - css('td[colspan="4"]').remove + css('td[colspan="3"]').remove + + # Remove the empty spacer cells of the index tables + css('td.printindex-index-entry').each do |node| + previous = node.previous_element + previous.remove if previous && previous.name == 'td' && previous.content.strip.empty? + end # Remove additional text from menu entry and index entry cells - css('td[valign=top]').each do |node| + css('td.printindex-index-entry, td.printindex-index-section').each do |node| link = node.at_css('a') node.children = link unless link.nil? end diff --git a/lib/docs/filters/bash/entries.rb b/lib/docs/filters/bash/entries.rb index c22316a2c2..c146d226f8 100644 --- a/lib/docs/filters/bash/entries.rb +++ b/lib/docs/filters/bash/entries.rb @@ -15,13 +15,14 @@ def get_name name.gsub!(/[[:digit:]]/, '') end + name.remove! '¶' name.strip end def get_type return 'Manual: Appendices' if name.start_with?('Appendix') - return 'Manual: Indexes' if at_css('a[rel=up]').content.include?("Index") + return 'Manual: Indexes' if at_css('a[rel=up]').try(:content).to_s.include?("Index") "Manual" end @@ -38,7 +39,7 @@ def additional_entries entries = [] - css('table[class^=index-] td[valign=top] > a').each_slice(2) do |entry_node, section_node| + css('td.printindex-index-entry > a').each do |entry_node| entry_name = entry_node.content entry_path = entry_node['href'] entries << [entry_name, entry_path, entry_type] diff --git a/lib/docs/filters/bun/clean_html.rb b/lib/docs/filters/bun/clean_html.rb index 90dca441d8..43a425c2e3 100644 --- a/lib/docs/filters/bun/clean_html.rb +++ b/lib/docs/filters/bun/clean_html.rb @@ -2,7 +2,8 @@ module Docs class Bun class CleanHtmlFilter < Filter def call - @doc = at_css('section > .w-full') + @doc = at_css('#content-area') + doc.children = css('#header, #content') header = at_css('header:has(h1)') if header @@ -10,14 +11,15 @@ def call header.name = 'h1' end - css('.CodeBlockTab').remove - css('.CopyIcon').remove + css('*[aria-label="Navigate to header"]', '*[aria-label="Copy the contents from the code block"]').each do |node| + node.parent.remove + end + css('img').remove css('svg').remove - css('a:contains("Edit on GitHub")').remove - css('a:contains("Previous")').remove - css('a:contains("Next")').remove - - css('pre').each do |node| + + css('.code-block *[data-component-part="code-block-header"]').remove + css('.code-block', '.code-group').each do |node| + node.name = 'pre' node.content = node.content node['data-language'] = 'typescript' node.remove_attribute('style') @@ -25,6 +27,7 @@ def call css('.font-mono').each do |node| node.name = 'code' + node.content = node.content end css('.font-mono.text-blue-600').each do |node| diff --git a/lib/docs/filters/bun/entries.rb b/lib/docs/filters/bun/entries.rb index 7ae0e1b4ae..6703c649dc 100644 --- a/lib/docs/filters/bun/entries.rb +++ b/lib/docs/filters/bun/entries.rb @@ -11,12 +11,12 @@ def get_type end def additional_entries - if slug.start_with?('cli') + if slug.start_with?('pm/cli') css('h2[id]').each_with_object [] do |node, entries| name = get_name + " " + node.content.strip entries << [name, node['id']] end - elsif slug.start_with?('api') + elsif slug.start_with?('runtime') css('h2[id]').each_with_object [] do |node, entries| name = get_name + ": " + node.content.strip entries << [name, node['id']] diff --git a/lib/docs/filters/celery/clean_html.rb b/lib/docs/filters/celery/clean_html.rb new file mode 100644 index 0000000000..53fc811a4b --- /dev/null +++ b/lib/docs/filters/celery/clean_html.rb @@ -0,0 +1,35 @@ +module Docs + class Celery + class CleanHtmlFilter < Filter + def call + # 'This document describes the current stable version of Celery...' + css('div.deck').remove + + css('.reference.external.image-reference').remove + + # Table of Contents. + css('nav.contents.local', 'table.docutils').remove + + # ¶ anchor links, [source] external links. + css('a.headerlink', 'a.toc-backref', '.viewcode-link', 'hr').remove + + css('h1', 'h2', 'h3', 'h4', 'h5', 'h6').each do |node| + node.content = node.inner_text + end + + css('.highlight-python pre', '.highlight-default pre', '.highlight-pycon pre').each do |node| + node.content = node.content + node['data-language'] = 'python' + node.parent.parent.replace(node) + end + + # Lists are wrapped in blockquotes for some reason. + css('blockquote ol', 'blockquote ul').each do |node| + node.parent.parent.replace(node) + end + + doc + end + end + end +end diff --git a/lib/docs/filters/celery/entries.rb b/lib/docs/filters/celery/entries.rb new file mode 100644 index 0000000000..c18a975236 --- /dev/null +++ b/lib/docs/filters/celery/entries.rb @@ -0,0 +1,27 @@ +module Docs + class Celery + class EntriesFilter < Docs::EntriesFilter + def get_name + at_css('h1').content.gsub(/\P{ASCII}/, '') + end + + def get_type + if subpath.include?('internals/') + 'Internal Module Reference' + elsif subpath.include?('reference/') + 'API Reference' + elsif subpath.include?('userguide/') + 'User Guide' + else + name + end + end + + def additional_entries + css('dl.py dt[id]').each_with_object [] do |node, entries| + entries << [node['id'], node['id'], get_type] + end + end + end + end +end diff --git a/lib/docs/filters/coldfusion/clean_html.rb b/lib/docs/filters/coldfusion/clean_html.rb new file mode 100644 index 0000000000..99d92a208d --- /dev/null +++ b/lib/docs/filters/coldfusion/clean_html.rb @@ -0,0 +1,91 @@ +module Docs + class Coldfusion + class CleanHtmlFilter < Filter + def call + # Listing/category pages (Tags, Functions, a category, or a guide index) + # use a different layout; keep their main container as-is after cleanup. + @doc = build_root + + # Remove site chrome and interactive widgets. + css('nav', 'footer', 'script', 'noscript', '#cfbreak', '.newsletter').remove + css('.modal', '.add-example-modal-lg', '.example-modal').remove + css('.example-btn', '.copy-btn', '.issuebutton', '.issuecount').remove + css('button').remove + css('br').remove + css('#search2').remove + + # Drop the "Add An Example" / edit / fork affordances. + css('a[href*="github.com"]', '#forkme', '#foundeo').remove + css('a.label.label-danger').remove # Edit links + + # Clean up the breadcrumb: keep the engine-version labels (they convey + # ColdFusion/Lucee/BoxLang availability) but drop navigation links and + # the issue tracker widget. + if (crumb = at_css('.breadcrumb')) + crumb.css('.label-warning').remove + crumb.css('.divider').remove + crumb.css('a[rel="nofollow"]').remove + # Remove navigation breadcrumb items (CFDocs > Functions > cf45 > …) + # that are not engine-availability labels. + crumb.css('li:not(.pull-right)').each do |li| + li.remove unless li.at_css('.label-acf, .label-lucee, .label-boxlang, .label-railo') + end + end + + # Code blocks: tag them so DevDocs applies CFML syntax highlighting. + css('pre.prettyprint', 'pre').each do |node| + node.remove_attribute('class') + node['data-language'] = 'coldfusion' + end + + # Inline code: nothing special needed, but strip prettyprint hints. + css('code').each { |node| node.remove_attribute('class') } + + # Unwrap Bootstrap `.container` layout wrappers; DevDocs supplies its own + # page width, so these only add centering/padding we don't want. + css('.container').each { |node| node.before(node.children).remove } + + # Remove now-empty wrappers left behind by the source template's many + # conditional blank lines. + css('div', 'p', 'span', 'ul', 'ol').each do |node| + node.remove if node.inner_html.strip.empty? && node.element_children.empty? + end + + doc + end + + # cfdocs splits an entry's content across the `.jumbotron` header (name, + # description, syntax), the `.breadcrumb`, and the main `.container` + # (arguments, compatibility, links, examples). Merge them into one root. + # + # NOTE: between filters the document is re-parsed as an HTML *fragment* + # (there is no ), so selectors must not depend on `body >`. + def build_root + # First .jumbotron is the page header; #cfbreak is the trailing + # newsletter jumbotron, which we ignore. + header = css('.jumbotron').reject { |n| n['id'] == 'cfbreak' } + .map { |n| n.at_css('.container') || n } + .first + breadcrumb = at_css('.breadcrumb') + + # The main content container holds the reference sections. It is a + # `.container` that is not the breadcrumb and not inside a jumbotron or + # nav. Identify it by the section headings it contains. + main = css('.container').find do |node| + next false if node.matches?('.breadcrumb') + next false if node.ancestors('.jumbotron').any? || node.ancestors('nav').any? + node.at_css('h2, .param, .panel') || node.at_css('#examples') + end + + root = Nokogiri::HTML.fragment('
    ').at_css('div') + root << header.dup if header + root << breadcrumb.dup if breadcrumb + root << main.dup if main + + # Fall back to the full document/fragment if the expected structure is + # missing (e.g. some guide pages). + root.element_children.any? ? root : (at_css('body') || doc) + end + end + end +end diff --git a/lib/docs/filters/coldfusion/entries.rb b/lib/docs/filters/coldfusion/entries.rb new file mode 100644 index 0000000000..7cdcb4445b --- /dev/null +++ b/lib/docs/filters/coldfusion/entries.rb @@ -0,0 +1,66 @@ +module Docs + class Coldfusion + class EntriesFilter < Docs::EntriesFilter + # Category/listing slugs that aggregate other entries and must not appear + # as entries themselves. Categories generally end in "-functions" or + # "-tags", but list the fixed index pages explicitly. + INDEX_SLUGS = %w(index tags functions all).freeze + + def include_default_entry? + entry_page? + end + + def get_name + if (h1 = at_css('#docname')) + h1.content.strip + elsif (h1 = at_css('h1')) + # Guide pages: use the heading text without anchor noise. + h1.content.strip + else + super + end + end + + def get_type + return 'Guides' if guide_page? + + # Use the second breadcrumb link (Tags / Functions) as the category. + crumb = css('.breadcrumb a').map { |a| a.content.strip } + if crumb.include?('Tags') + 'Tags' + elsif crumb.include?('Functions') + 'Functions' + else + 'Guides' + end + end + + private + + # A real reference entry: a tag or function page. These have a `data-doc` + # whose value matches the slug (no spaces) and usually a `#syntax` block. + def entry_page? + return false if index_slug? + return true if guide_page? + + doc_name = at_css('[data-doc]').try(:[], 'data-doc') + return false if doc_name.nil? + # Category pages have human titles with spaces (e.g. "String Functions"). + !doc_name.include?(' ') + end + + def guide_page? + # Guides have no breadcrumb but do have content; they are neither tags + # nor functions nor category indexes. + return false if index_slug? + at_css('.breadcrumb').nil? && at_css('h1') + end + + def index_slug? + s = slug.to_s.downcase + return true if INDEX_SLUGS.include?(s) + s.end_with?('-functions', '-tags') + end + end + end +end diff --git a/lib/docs/filters/core/clean_text.rb b/lib/docs/filters/core/clean_text.rb index 1deeba01ff..1446868181 100644 --- a/lib/docs/filters/core/clean_text.rb +++ b/lib/docs/filters/core/clean_text.rb @@ -6,9 +6,12 @@ class CleanTextFilter < Filter def call return html if context[:clean_text] == false - html.strip! - while html.gsub!(EMPTY_NODES_RGX, ''); end - html + + # Clone frozen literal. + result = html.dup + result.strip! + while result.gsub!(EMPTY_NODES_RGX, ''); end + result end end end diff --git a/lib/docs/filters/core/normalize_paths.rb b/lib/docs/filters/core/normalize_paths.rb index 431c781a06..4daac68bf7 100644 --- a/lib/docs/filters/core/normalize_paths.rb +++ b/lib/docs/filters/core/normalize_paths.rb @@ -51,6 +51,8 @@ def normalize_path(path) "#{path}index" elsif path.end_with? '.html' path[0..-6] + elsif path.end_with? '.md' + path[0..-4] else path end diff --git a/lib/docs/filters/couchdb/clean_html.rb b/lib/docs/filters/couchdb/clean_html.rb new file mode 100644 index 0000000000..7c30aea4fa --- /dev/null +++ b/lib/docs/filters/couchdb/clean_html.rb @@ -0,0 +1,29 @@ +module Docs + class Couchdb + class CleanHtmlFilter < Filter + def call + css('.section-number').remove + css('.headerlink').remove + + css('.sig-name').each do |node| + node.name = 'code' + end + + css('pre').each do |node| + node.content = node.content.strip + + classes = node.parent.parent.classes + if classes.include? 'highlight-bash' + node['data-language'] = 'bash' + else + node['data-language'] = 'javascript' + end + + node.parent.parent.replace(node) + end + + doc + end + end + end +end diff --git a/lib/docs/filters/couchdb/entries.rb b/lib/docs/filters/couchdb/entries.rb new file mode 100644 index 0000000000..3d03a9616e --- /dev/null +++ b/lib/docs/filters/couchdb/entries.rb @@ -0,0 +1,53 @@ +module Docs + class Couchdb + class EntriesFilter < Docs::EntriesFilter + SLUG_MAP = { + 'api' => 'API', + 'json' => 'JSON Structures', + 'cluster' => 'Cluster Management', + 'replication' => 'Replication', + 'maintenance' => 'Maintenance', + 'partitioned' => 'Partitioned Databases' + } + + def get_name + at_css('h1').content.gsub(/\P{ASCII}/, '').split('.').last + end + + def get_type + if slug.start_with?('ddocs/views') + 'Views' + elsif slug.start_with?('ddocs') + 'Design Documents' + else + SLUG_MAP[slug[/^(.+?)[-\/]/, 1]] || name + end + end + + def additional_entries + needs_breakup = [ + 'JSON Structure Reference', + 'Design Documents', + 'Partitioned Databases' + ] + + if needs_breakup.include?(name) + entries = [] + + css('section > section').each do |node| + h2 = node.at_css('h2') + + if h2.present? + name = node.at_css('h2').content.split('.').last + entries << [name, node['id']] + end + end + + entries + else + [] + end + end + end + end +end diff --git a/lib/docs/filters/cyclejs/clean_html.rb b/lib/docs/filters/cyclejs/clean_html.rb new file mode 100644 index 0000000000..cfafee84b9 --- /dev/null +++ b/lib/docs/filters/cyclejs/clean_html.rb @@ -0,0 +1,23 @@ +module Docs + class Cyclejs + class CleanHtmlFilter < Filter + def call + return "

    Cycle.js

    A functional and reactive JavaScript framework for predictable code

    " if root_page? + css('br').remove + + css('pre > code').each do |node| + parent = node.parent + parent['data-language'] = 'javascript' + parent.content = node.content.strip + end + + css('table[style]', 'tr[style]', 'td[style]', 'th[style]').remove_attr('style') + css('img').each do |node| + node['alt'] = node['alt'].presence || '' + end + + doc + end + end + end +end diff --git a/lib/docs/filters/cyclejs/entries.rb b/lib/docs/filters/cyclejs/entries.rb new file mode 100644 index 0000000000..b7149201c3 --- /dev/null +++ b/lib/docs/filters/cyclejs/entries.rb @@ -0,0 +1,27 @@ +module Docs + class Cyclejs + class EntriesFilter < Docs::EntriesFilter + def get_name + title = at_css('h1') + name = title ? title.content.strip : subpath.sub(/\.html\z/, '').titleize + name = 'Cycle.js' if root_page? + name = 'API Reference' if slug == 'api/index' + name.delete_suffix! ' - source' + name + end + + def get_type + slug.start_with?('api/') ? 'API' : 'Guide' + end + + def additional_entries + css('h2[id], h3[id]').map do |node| + name = node.content.strip + name.sub!(/\A#\s*/, '') + name.sub!(/\s+#\z/, '') + [get_name + ': ' + name, node['id']] + end + end + end + end +end diff --git a/lib/docs/filters/elixir/clean_html.rb b/lib/docs/filters/elixir/clean_html.rb index 2d656ff65d..e3607ddfab 100644 --- a/lib/docs/filters/elixir/clean_html.rb +++ b/lib/docs/filters/elixir/clean_html.rb @@ -47,7 +47,7 @@ def api end end - css('h1 a.icon-action[title="View Source"]').each do |node| + css('.top-heading a.icon-action[title="View Source"]').each do |node| node['class'] = 'source' node.content = "Source" end diff --git a/lib/docs/filters/elixir/entries.rb b/lib/docs/filters/elixir/entries.rb index 640f65e19a..df8db120e1 100644 --- a/lib/docs/filters/elixir/entries.rb +++ b/lib/docs/filters/elixir/entries.rb @@ -7,19 +7,31 @@ def get_name end def get_type - section = at_css('h1 a.source').attr('href').match('elixir/pages/([^/]+)/')&.captures&.first + # Use section titles for Elixir docs + section = at_css('.top-heading a.source')&.attr('href')&.match('elixir/pages/([^/]+)/')&.captures&.first if section == "mix-and-otp" return "Mix & OTP" elsif section - return section.gsub("-", " ").capitalize + return section.gsub(/[-_]/, " ").capitalize end - name = at_css('h1 span').text + # Use section titles for guides + guide = at_css('.top-heading a.source')&.attr('href')&.match('guides/(?:([^/]+)/)?') + if guide + section = guide.captures.first || "Guides" + return section.gsub(/[-_]/, " ").capitalize + end + + # Sometimes the heading includes additional text in a tag, + # in which case the main text is wrapped in a + # e.g. https://elixir.hexdocs.pm/Exception.html + heading = at_css('.top-heading h1 span') || at_css('.top-heading h1') + name = heading.text case name.split(' ').first when 'mix' then 'Mix Tasks' when 'Changelog' then 'References' else - case at_css('h1 small').try(:content) + case at_css('.top-heading h1 small').try(:content) when 'exception' 'Exceptions' when 'protocol' diff --git a/lib/docs/filters/eslint/clean_html.rb b/lib/docs/filters/eslint/clean_html.rb index f347cef969..de7a45e85d 100644 --- a/lib/docs/filters/eslint/clean_html.rb +++ b/lib/docs/filters/eslint/clean_html.rb @@ -24,8 +24,10 @@ def call end css('code', 'p').remove_attr('class') - - css('.resource__image', '.resource__domain').remove + css('.copy-btn').remove + + css('svg').remove + css('.resource__icon', '.resource__img').remove doc end diff --git a/lib/docs/filters/express/clean_html.rb b/lib/docs/filters/express/clean_html.rb index eeb98f1d50..ee5bb93ab9 100644 --- a/lib/docs/filters/express/clean_html.rb +++ b/lib/docs/filters/express/clean_html.rb @@ -39,6 +39,7 @@ def call css('pre > code').each do |node| node.parent['data-language'] = node['class'][/language-(\w+)/, 1] if node['class'] + node.parent['data-language'] ||= 'javascript' node.parent.content = node.parent.content end diff --git a/lib/docs/filters/godot/clean_html.rb b/lib/docs/filters/godot/clean_html.rb index 54ace4df87..f5e0578c0a 100644 --- a/lib/docs/filters/godot/clean_html.rb +++ b/lib/docs/filters/godot/clean_html.rb @@ -37,6 +37,9 @@ def call # remove the remotely hosted "percent-translated" badge css('a[href^="https://hosted.weblate"]').remove if root_page? + # remove the "User-contributed notes" section, which is loaded via JS + css('div#godot-giscus').remove + doc end end diff --git a/lib/docs/filters/godot/entries.rb b/lib/docs/filters/godot/entries.rb index 9cda5afa27..fa143d116d 100644 --- a/lib/docs/filters/godot/entries.rb +++ b/lib/docs/filters/godot/entries.rb @@ -4,6 +4,7 @@ class EntriesFilter < Docs::EntriesFilter def get_name name = at_css('h1').content name.remove! "\u{00B6}" # Remove the pilcrow + name.remove! "\u{F0C1}" # Remove the anchor icon name end diff --git a/lib/docs/filters/homebrew/entries.rb b/lib/docs/filters/homebrew/entries.rb index b3a3ba1826..2c355d7d07 100644 --- a/lib/docs/filters/homebrew/entries.rb +++ b/lib/docs/filters/homebrew/entries.rb @@ -3,8 +3,20 @@ class Homebrew class EntriesFilter < Docs::EntriesFilter def get_name header = at_css('h1') - name = header.nil? ? current_url.normalized_path[1..-1].gsub(/-/, ' ') : header.content.strip - name.remove! %r{\(.*} + + if current_url.path.start_with?('/rubydoc') + # Titles in rubydoc content follow a different format, with a bit of cleanup needed. + name = header + .content + .gsub('Class: ', '') + .gsub('Module: ', '') + .gsub('Exception: ', '') + .gsub(' Private', '') + else + name = header.nil? ? current_url.normalized_path[1..-1].gsub(/-/, ' ') : header.content.strip + name.remove! %r{\(.*} + end + name end @@ -26,13 +38,21 @@ def get_name Building-Against-Non-Homebrew-Dependencies How-to-Create-and-Maintain-a-Tap Brew-Test-Bot - Prose-Style-Guidelines Typechecking Reproducible-Builds ) def get_type - if CONTRIBUTOR_SLUGS.include?(slug) + if current_url.path.start_with?('/rubydoc') + header = at_css('h1').content + if header.start_with?('Module') + 'API Modules' + elsif header.start_with?('Class') + 'API Classes' + else + 'API Exceptions' + end + elsif CONTRIBUTOR_SLUGS.include?(slug) 'Contributors' else 'Users' diff --git a/lib/docs/filters/htmx/clean_html.rb b/lib/docs/filters/htmx/clean_html.rb index 4c14c6aad3..95a3107327 100644 --- a/lib/docs/filters/htmx/clean_html.rb +++ b/lib/docs/filters/htmx/clean_html.rb @@ -2,6 +2,7 @@ module Docs class Htmx class CleanHtmlFilter < Filter def call + css('.ad').remove css('.zola-anchor').remove doc.prepend_child("

    htmx

    ") if root_page? css('div:contains("NEWS:")').remove diff --git a/lib/docs/filters/http/clean_html.rb b/lib/docs/filters/http/clean_html.rb index 9c5abe3e35..c63e7743c5 100644 --- a/lib/docs/filters/http/clean_html.rb +++ b/lib/docs/filters/http/clean_html.rb @@ -25,8 +25,18 @@ def mdn end def ietf - raise "#{slug} is obsolete!" if at_css('.meta-info *:contains("Obsoleted by")') - @doc = at_css('.draftcontent') + # datatracker serves two layouts: a legacy plain-text rendering wrapped + # in .rfcmarkup (older RFCs) and a modern structured HTML rendering + # wrapped in .rfchtml (RFCs with xml2rfc v3 sources, e.g. RFC 9110+). + doc['class'].to_s.include?('rfchtml') ? ietf_html : ietf_text + end + + def ietf_html + css('.pilcrow', '.noprint').remove + end + + def ietf_text + @doc = at_css('.rfcmarkup') doc.child.remove while doc.child.name != 'pre' css('span.grey', '.invisible', '.noprint', 'a[href^="#page-"]').remove diff --git a/lib/docs/filters/http/entries.rb b/lib/docs/filters/http/entries.rb index 0bc64538c4..0686c17276 100644 --- a/lib/docs/filters/http/entries.rb +++ b/lib/docs/filters/http/entries.rb @@ -3,7 +3,9 @@ class Http class EntriesFilter < Docs::EntriesFilter def get_name if current_url.host == 'datatracker.ietf.org' - name = at_css('h1').content + # The modern (.rfchtml) layout has two h1s: the RFC number and the + # title. The legacy (.rfcmarkup) layout has a single title h1. + name = (at_css('h1#title') || at_css('h1')).content name.remove! %r{\A.+\:} name.remove! %r{\A.+\-\-} name = 'WebDAV' if name.include?('WebDAV') @@ -24,13 +26,13 @@ def get_name def get_type return name if current_url.host == 'datatracker.ietf.org' - if slug.start_with?('Headers/Content-Security-Policy') + if slug.start_with?('Reference/Headers/Content-Security-Policy') 'CSP' - elsif slug.start_with?('Headers') + elsif slug.start_with?('Reference/Headers') 'Headers' - elsif slug.start_with?('Methods') + elsif slug.start_with?('Reference/Methods') 'Methods' - elsif slug.start_with?('Status') + elsif slug.start_with?('Reference/Status') 'Status' elsif slug.start_with?('Basics_of_HTTP') 'Guides: Basics' @@ -86,15 +88,31 @@ def rfc LEVEL_3 = /\A(\d+)\.\d+\.\d+\z/ LEVEL_4 = /\A(\d+)\.\d+\.\d+\.\d+\z/ + # Returns ordered [section-id, heading-text] pairs for each numbered + # section, handling both datatracker layouts: the legacy text rendering + # puts the section id on the heading element itself, while the modern HTML + # rendering wraps each section in
    with the heading + # as a child. + def section_headings + sections = css('section[id^="section-"]') + if sections.any? + sections.filter_map do |section| + heading = section.element_children.find { |node| node.name =~ /\Ah[1-6]\z/ } + [section['id'], heading.content.strip.gsub(/\s+/, ' ')] if heading + end + else + css('*[id^="section-"]').map { |node| [node['id'], node.content.strip] } + end + end + def additional_entries return [] unless current_url.host == 'datatracker.ietf.org' type = nil - css('*[id^="section-"]').each_with_object([]) do |node, entries| - id = node['id'] + section_headings.each_with_object([]) do |(id, content), entries| break entries if entries.any? { |e| e[1] == id } - content = node.content.strip + content = content.dup content.remove! %r{\s*\.+\d*\z} content.remove! %r{\A[\.\s]+} diff --git a/lib/docs/filters/javascript/clean_html.rb b/lib/docs/filters/javascript/clean_html.rb index eeb23ba8a8..1b450470fc 100644 --- a/lib/docs/filters/javascript/clean_html.rb +++ b/lib/docs/filters/javascript/clean_html.rb @@ -19,6 +19,12 @@ def other css('div > .overheadIndicator:first-child:last-child', 'div > .blockIndicator:first-child:last-child').each do |node| node.parent.replace(node) end + + css('.baseline-indicator').each do |node| + if node.next.text == '> ' + node.next.remove + end + end end end end diff --git a/lib/docs/filters/maplibre_gl/clean_html.rb b/lib/docs/filters/maplibre_gl/clean_html.rb new file mode 100644 index 0000000000..126a1f475d --- /dev/null +++ b/lib/docs/filters/maplibre_gl/clean_html.rb @@ -0,0 +1,32 @@ +module Docs + class MaplibreGl + class CleanHtmlFilter < Filter + def call + # Anchor icons next to headings. + css('.headerlink').remove + + # The TypeDoc pages carry a "Defined in: " paragraph on + # every member; it points at a pinned commit and adds noise. + css('p').each do |node| + node.remove if node.content.start_with?('Defined in:') + end + + # MkDocs Material renders highlighted code as + #
    …lots of spans…
    + # with per-line anchors. Flatten each block to its plain text and tag + # the language so DevDocs can re-highlight it. + css('div.highlight').each do |node| + language = node['class'][/language-(\w+)/, 1] + pre = node.at_css('pre') + next unless pre + + pre['data-language'] = language if language + pre.content = pre.at_css('code').content + node.replace(pre) + end + + doc + end + end + end +end diff --git a/lib/docs/filters/maplibre_gl/entries.rb b/lib/docs/filters/maplibre_gl/entries.rb new file mode 100644 index 0000000000..61b36da220 --- /dev/null +++ b/lib/docs/filters/maplibre_gl/entries.rb @@ -0,0 +1,52 @@ +module Docs + class MaplibreGl + class EntriesFilter < Docs::EntriesFilter + # Sections of a TypeDoc class page whose members are worth indexing + # individually. + MEMBER_SECTIONS = %w(Constructors Properties Methods Events Accessors).freeze + + def get_name + at_css('h1').content.strip + end + + def get_type + # DevDocs lower-cases paths, so match against the lower-cased segments. + case path + when %r{api/classes/} then 'Classes' + when %r{api/functions/} then 'Functions' + when %r{api/type-aliases/} then 'Type Aliases' + when %r{api/interfaces/} then 'Interfaces' + when %r{api/enumerations/} then 'Enumerations' + when %r{api/variables/} then 'Variables' + when %r{\Aguides/} then 'Guides' + else 'Miscellaneous' + end + end + + def additional_entries + return [] unless class_page? + + entries = [] + section = nil + + css('h2, h3').each do |node| + if node.name == 'h2' + section = node.content.strip + elsif MEMBER_SECTIONS.include?(section) + member = node.content.strip.sub(/\(\)\z/, '').sub(/\?\z/, '') + next if member.empty? + entries << ["#{get_name}.#{member}", "#{path}##{node['id']}"] + end + end + + entries + end + + private + + def class_page? + path.include?('api/classes/') + end + end + end +end diff --git a/lib/docs/filters/mdn/compat_tables.rb b/lib/docs/filters/mdn/compat_tables.rb index 162fd3b76c..5b1db94422 100644 --- a/lib/docs/filters/mdn/compat_tables.rb +++ b/lib/docs/filters/mdn/compat_tables.rb @@ -138,77 +138,52 @@ def add_data_to_entry(json, entry) version_removed = [] notes = [] - if json - if json.is_a?(Array) - json.each do |element| - - if element['version_added'] - version_added.push(element['version_added']) - else - version_added.push(false) - end - - if element['version_removed'] - version_removed.push(element['version_removed']) - else - version_removed.push(false) - end - - if element['notes'] - notes.push(element['notes']) - else - notes.push(false) - end - - end - else - version_added.push(json['version_added']) - version_removed.push(json['version_removed']) - notes.push(json['notes']) - end + if !json + format_string = "
    ?
    " + entry.add_child(format_string) + return + end - version_added.map! do |version| - if version == true - 'Yes' - elsif version == false - 'No' - elsif version.is_a?(String) - version - else - '?' - end + format_string = "" + (json.is_a?(Array) ? json : [json]).each do |element| + version = element['version_added'] + if version.is_a?(String) and element['release_date'] + format_string = "" + format_string = "" if element['release_date'] > Time.now.iso8601 + version_added.push("#{version}") + elsif version == 'preview' + format_string = "" + version_added.push(version) + elsif version.is_a?(String) + format_string = "" + version_added.push(version) + elsif version == true + format_string = "" + version_added.push('Yes') + else + format_string = "" + version_added.push('No') end + version_removed.push(element['version_removed'] || false) + notes.push(element['notes'] || false) + end - if version_removed[0] - format_string = "" - elsif version_added[0] == 'No' - format_string = "" - elsif version_added[0] == '?' - format_string = "" + for value in (0..version_added.length-1) do + if version_removed[value] + version_string = "#{version_added[value]}–#{version_removed[value]}" else - format_string = "" + version_string = version_added[value] end - for value in (0..version_added.length-1) do - if version_removed[value] - version_string = "#{version_added[value]}–#{version_removed[value]}" - else - version_string = version_added[value] - end - - if notes[value] - format_string += "
    #{version_string}#{notes[value]}
    " - else - format_string += "
    #{version_string}
    " - end + if notes[value] + format_string += "
    #{version_string}#{notes[value]}
    " + else + format_string += "
    #{version_string}
    " end - - format_string += "" - - else - format_string = "
    ?
    " end + format_string += "" + entry.add_child(format_string) end diff --git a/lib/docs/filters/nim/clean_html.rb b/lib/docs/filters/nim/clean_html.rb index 89b638fe6d..66dfe270d7 100644 --- a/lib/docs/filters/nim/clean_html.rb +++ b/lib/docs/filters/nim/clean_html.rb @@ -58,6 +58,11 @@ def call css('a', 'dl', 'table', 'code').remove_attr('class') css('table').remove_attr('border') + # Remove Source/Edit links. + css('dd > a').each do |node| + node.remove if node.inner_html == 'Source' || node.inner_html == 'Edit' + end + doc end end diff --git a/lib/docs/filters/node/clean_html.rb b/lib/docs/filters/node/clean_html.rb index 94ca39f666..3827fe0339 100644 --- a/lib/docs/filters/node/clean_html.rb +++ b/lib/docs/filters/node/clean_html.rb @@ -2,6 +2,10 @@ module Docs class Node class CleanHtmlFilter < Filter def call + unless at_css('h1') + at_css('h2').name = 'h1' + end + css('hr').remove css('pre').each do |node| diff --git a/lib/docs/filters/node/entries.rb b/lib/docs/filters/node/entries.rb index e5d508f7fc..45d581aae3 100644 --- a/lib/docs/filters/node/entries.rb +++ b/lib/docs/filters/node/entries.rb @@ -7,7 +7,7 @@ def get_name end def get_type - at_css('h2').content.strip + at_css('h1, h2').content.strip end def additional_entries diff --git a/lib/docs/filters/nushell/clean_html.rb b/lib/docs/filters/nushell/clean_html.rb index d7e866954d..aaaa46eb67 100644 --- a/lib/docs/filters/nushell/clean_html.rb +++ b/lib/docs/filters/nushell/clean_html.rb @@ -3,9 +3,19 @@ module Docs class Nushell class CleanHtmlFilter < Filter def call - @doc = at_css('.theme-default-content > div:only-child', '.theme-default-content') css('footer').remove - css('h1 a, h2 a').remove + + css('span.lang').remove + + css('h1 > a').each do |node| + node.before(node.children).remove + end + + css('pre > code:first-child').each do |node| + node.parent['data-language'] = 'sh' + node.parent.content = node.css('.line').map(&:content).join("\n") + end + doc end end diff --git a/lib/docs/filters/nushell/entries.rb b/lib/docs/filters/nushell/entries.rb index 6e92241938..a5d386db94 100644 --- a/lib/docs/filters/nushell/entries.rb +++ b/lib/docs/filters/nushell/entries.rb @@ -23,6 +23,11 @@ def include_default_entry? false end + def get_name + name = at_css('h1').content + name + end + def additional_entries entries = [] type = "" @@ -38,6 +43,7 @@ def additional_entries css("h1").each do |node| name = node.at_css("code") ? node.at_css("code").text : node.text + node = node.at_css('> span') if node.at_css('> span') type = node.children.length >= 3 ? node.children[2].text.sub(" for ", "").capitalize : node.text diff --git a/lib/docs/filters/ocaml/clean_html.rb b/lib/docs/filters/ocaml/clean_html.rb index 83bb86a0b1..b333094bc0 100644 --- a/lib/docs/filters/ocaml/clean_html.rb +++ b/lib/docs/filters/ocaml/clean_html.rb @@ -2,6 +2,7 @@ module Docs class Ocaml class CleanHtmlFilter < Filter def call + @doc = at_css('.api') || doc css('#sidebar').remove diff --git a/lib/docs/filters/octave/clean_html.rb b/lib/docs/filters/octave/clean_html.rb index 835a9406cb..bbf31c3ef4 100644 --- a/lib/docs/filters/octave/clean_html.rb +++ b/lib/docs/filters/octave/clean_html.rb @@ -12,6 +12,8 @@ def root def other css('.header', 'hr').remove + css('.nav-panel').remove + css('.copiable-link').remove css('.footnote > h3').each do |node| node.name = 'h5' diff --git a/lib/docs/filters/octave/entries.rb b/lib/docs/filters/octave/entries.rb index ea4c37f4b9..4d4e57dc0a 100644 --- a/lib/docs/filters/octave/entries.rb +++ b/lib/docs/filters/octave/entries.rb @@ -2,7 +2,7 @@ module Docs class Octave class EntriesFilter < Docs::EntriesFilter def get_name - at_css('h1').content.sub(/(A?[0-9.]+ )/, '') + at_css('h1').content.sub(/(A?[0-9.]+¶ )/, '') end def get_type diff --git a/lib/docs/filters/odin/clean_html.rb b/lib/docs/filters/odin/clean_html.rb new file mode 100644 index 0000000000..19c2db6079 --- /dev/null +++ b/lib/docs/filters/odin/clean_html.rb @@ -0,0 +1,23 @@ +module Docs + class Odin + class CleanHtmlFilter < Filter + def call + @doc = at_css('#pkg') || doc + + css('nav').remove + css('.pkg-breadcrumb').remove + css('.a-hidden').remove + css('.doc-source').remove + css('.odin-search-wrapper').remove + css('#pkg-sidebar').remove + css('#odin-search-info').remove + css('#odin-search-results').remove + css('.pkg-index').remove + css('h2 .text-decoration-none').remove + css('h3 .text-decoration-none').remove + + doc + end + end + end +end diff --git a/lib/docs/filters/odin/entries.rb b/lib/docs/filters/odin/entries.rb new file mode 100644 index 0000000000..070104bc89 --- /dev/null +++ b/lib/docs/filters/odin/entries.rb @@ -0,0 +1,47 @@ +module Docs + class Odin + class EntriesFilter < Docs::EntriesFilter + def get_name + if base_url.host == 'odin-lang.org' + title = context[:html_title].gsub(/ \| Odin Programming Language/, "") + title + else + breadcrumbs = css(".pkg-breadcrumb > ol > li") + if breadcrumbs + if breadcrumbs[1] + breadcrumbs[1].content + elsif breadcrumbs[0] + breadcrumbs[0].content + end + end + + title = context[:html_title].gsub(/ - pkg.odin-lang.org/, "") + title = title.gsub(/^package /, "") + + # For the package index page, use the angle bracket + # to propel the entry to the top + if title and title.end_with?("library") + "- package index" + else + title + end + end + end + + def get_type + if base_url.host == 'odin-lang.org' + "Documentation" + elsif base_url.host == 'pkg.odin-lang.org' + base_package_name = subpath.gsub(/\/.*/, "") + "Packages / " + base_package_name + end + end + + def additional_entries + entries = [] + entries + end + end + end +end + diff --git a/lib/docs/filters/opentofu/clean_html.rb b/lib/docs/filters/opentofu/clean_html.rb new file mode 100644 index 0000000000..dd85d23579 --- /dev/null +++ b/lib/docs/filters/opentofu/clean_html.rb @@ -0,0 +1,30 @@ +module Docs + class Opentofu + class CleanHtmlFilter < Filter + def fix_syntax_highlight + css('pre').each do |node| + node['data-language'] = node['class'][/language-(\w+)/, 1] if node['class'] + node.content = node.css('.token-line').map(&:content).join("\n") + node.remove_attribute('class') + node.remove_attribute('style') + end + + css('[class*="buttonGroup_"]').remove + end + + # Some SVG icons are just too big and not needed. + def remove_svg_icons + css('[role="alert"] svg').remove + end + + def call + @doc = at_css("main article > .prose") + + remove_svg_icons + fix_syntax_highlight + + doc + end + end + end +end diff --git a/lib/docs/filters/opentofu/entries.rb b/lib/docs/filters/opentofu/entries.rb new file mode 100644 index 0000000000..88f2c1146a --- /dev/null +++ b/lib/docs/filters/opentofu/entries.rb @@ -0,0 +1,21 @@ +module Docs + class Opentofu + class EntriesFilter < Docs::EntriesFilter + def get_name + at_css('main article h1').content + end + + def get_type + segments = slug.split('/') + if segments[0..1] == ['language', 'functions'] + # We have too many functions (120+ out of ~300 pages) + "Function" + elsif segments.first == 'cli' + "CLI" + else + segments.first.titlecase + end + end + end + end +end diff --git a/lib/docs/filters/php/clean_html.rb b/lib/docs/filters/php/clean_html.rb index 910746404c..272117e1d8 100644 --- a/lib/docs/filters/php/clean_html.rb +++ b/lib/docs/filters/php/clean_html.rb @@ -4,6 +4,7 @@ class Php class CleanHtmlFilter < Filter def call + @doc = at_css('#layout-content') root_page? ? root : other doc end diff --git a/lib/docs/filters/phpunit/clean_html.rb b/lib/docs/filters/phpunit/clean_html.rb index dacb15874b..8bbf619a87 100644 --- a/lib/docs/filters/phpunit/clean_html.rb +++ b/lib/docs/filters/phpunit/clean_html.rb @@ -9,14 +9,16 @@ def call node['data-language'] = 'php' end + # When extracting strings, filter out non-ASCII chars that mysteriously get added. + if slug.match(/assertion|annotations|configuration/) css('h2').each do |node| - node['id'] = node.content + node['id'] = node.content.gsub(/\P{ASCII}/, '') end end - css('h1').each do |node| - node.content = node.content.gsub(/\d*\./, '').strip + css('h1', 'h2', 'h3').each do |node| + node.content = node.content.gsub(/\d*\. |\P{ASCII}/, '') end doc diff --git a/lib/docs/filters/phpunit/entries.rb b/lib/docs/filters/phpunit/entries.rb index fd426be6c7..793b815010 100644 --- a/lib/docs/filters/phpunit/entries.rb +++ b/lib/docs/filters/phpunit/entries.rb @@ -7,8 +7,9 @@ def get_name end def get_type + name.gsub!(/\P{ASCII}/, '') if name.in? ['Assertions', 'Annotations', 'The XML Configuration File'] - name + name.gsub('The ', '') else 'Guides' end @@ -17,11 +18,10 @@ def get_type def additional_entries return [] if type == 'Guides' - css('h2').map do |node| - [node.content, node['id']] + css('h3').map do |node| + [node.content.gsub('The ', ''), node['id'] || node.ancestors('section[id]').first['id']] end end - end end end diff --git a/lib/docs/filters/playwright/clean_html.rb b/lib/docs/filters/playwright/clean_html.rb index cc97294d3d..911a002bfc 100644 --- a/lib/docs/filters/playwright/clean_html.rb +++ b/lib/docs/filters/playwright/clean_html.rb @@ -11,6 +11,7 @@ def call css('.alert').each do |node| node.name = 'blockquote' + node.css('svg').remove # warning icons end css('pre').each do |node| @@ -21,6 +22,7 @@ def call end css('*[class]').remove_attribute('class') + css('*[style]').remove_attribute('style') doc end diff --git a/lib/docs/filters/polars/clean_html.rb b/lib/docs/filters/polars/clean_html.rb new file mode 100644 index 0000000000..e270c8d985 --- /dev/null +++ b/lib/docs/filters/polars/clean_html.rb @@ -0,0 +1,32 @@ +module Docs + class Polars + class CleanHtmlFilter < Filter + def call + # Remove pydata-sphinx-theme chrome that survives the container extraction + # or sits inside the article (sidebars, in-page TOC, prev/next nav, footer). + css( + '.bd-sidebar-primary', + '.bd-sidebar-secondary', + '.bd-toc', + '.bd-header-article', + '.prev-next-area', + '.prev-next-footer', + '.bd-footer', + '.headerlink', + 'form' + ).remove + + # Drop banner/logo imagery on the landing page. + css('img').remove if root_page? + + # Make sure every code block is tagged so Prism highlights it as Python. + css('.highlight pre').each do |node| + node.content = node.content + node['data-language'] = 'python' + end + + doc + end + end + end +end diff --git a/lib/docs/filters/polars/entries.rb b/lib/docs/filters/polars/entries.rb new file mode 100644 index 0000000000..ab881f40ce --- /dev/null +++ b/lib/docs/filters/polars/entries.rb @@ -0,0 +1,64 @@ +module Docs + class Polars + class EntriesFilter < Docs::EntriesFilter + # Map the leading path segment of a reference page to a human readable + # type. The Polars reference is laid out as
    /... under the base + # url (e.g. dataframe/api/polars.DataFrame.count.html). Top-level members + # (plain functions, datatypes, IO, config, ...) instead live flat under + # api/ and are classified by name in #classify_api. + SECTION_TYPES = { + 'dataframe' => 'DataFrame', + 'lazyframe' => 'LazyFrame', + 'series' => 'Series', + 'expressions' => 'Expressions', + 'functions' => 'Functions', + 'selectors' => 'Selectors', + 'datatypes' => 'Data Types', + 'datatype_expr' => 'Data Types', + 'config' => 'Config', + 'io' => 'Input/output', + 'sql' => 'SQL', + 'exceptions' => 'Exceptions', + 'testing' => 'Testing', + 'catalog' => 'Catalog', + 'metadata' => 'Metadata', + 'schema' => 'Schema', + 'plugins' => 'Plugins' + }.freeze + + def get_name + name = at_css('h1').content.strip + # This runs before clean_html removes the headerlink, so strip its + # anchor character off the heading. + name.sub! %r{\s*[#\u{00B6}]+\s*\z}, '' + name + end + + def get_type + return 'Manual' if root_page? + segment = slug.split('/').first + return classify_api(get_name) if segment == 'api' + SECTION_TYPES[segment] || 'Manual' + end + + private + + # Members stored flat under api/ (top-level polars.* objects). + def classify_api(name) + case name + when %r{\Apolars\.datatypes\.} then 'Data Types' + when %r{\Apolars\.Config\b} then 'Config' + when %r{\Apolars\.exceptions\.} then 'Exceptions' + when %r{\Apolars\.testing\.} then 'Testing' + when %r{\Apolars\.(api|plugins)\.} then 'Plugins' + when %r{\Apolars\.io\.} then 'Input/output' + when %r{\Apolars\.DataFrame\.} then 'DataFrame' + when %r{\Apolars\.LazyFrame\.} then 'LazyFrame' + when %r{\Apolars\.(read_|scan_|write_|from_)}, %r{\Apolars\.json_normalize\b} + 'Input/output' + else 'Functions' + end + end + end + end +end diff --git a/lib/docs/filters/powershell/attribution.rb b/lib/docs/filters/powershell/attribution.rb new file mode 100644 index 0000000000..956c87b57f --- /dev/null +++ b/lib/docs/filters/powershell/attribution.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Docs + class Powershell + class AttributionFilter < Docs::AttributionFilter + def attribution_link + url = current_url.to_s.downcase + base = self.version == 'Scripting' ? 'scripting' : 'module' + url.sub! 'http://localhost/', "https://learn.microsoft.com/en-us/powershell/#{base}/" + url.remove! %r{\.md$} + url << "?view=powershell-" + self.version + %(#{url}) + end + end + end +end diff --git a/lib/docs/filters/powershell/clean_html.rb b/lib/docs/filters/powershell/clean_html.rb new file mode 100644 index 0000000000..14d24323a0 --- /dev/null +++ b/lib/docs/filters/powershell/clean_html.rb @@ -0,0 +1,23 @@ +module Docs + class Powershell + class CleanHtmlFilter < Filter + def call + # web + css('header').remove + css('#ms--content-header').remove + css('#article-header').remove + css('.left-container').remove + css('.layout-body-aside').remove + css('#site-user-feedback-footer').remove + css('footer').remove + # markdown-folder-to-html + css('#menuLink').remove + css('#menu').remove + css('script').remove + css('style').remove + css('.heading-anchor-permalink').remove + doc + end + end + end +end diff --git a/lib/docs/filters/powershell/entries.rb b/lib/docs/filters/powershell/entries.rb new file mode 100644 index 0000000000..dafc3fe4f3 --- /dev/null +++ b/lib/docs/filters/powershell/entries.rb @@ -0,0 +1,14 @@ +module Docs + class Powershell + class EntriesFilter < Docs::EntriesFilter + def get_name + at_css('h1').content.strip + end + + def get_type + parts = slug.split('/') + parts.length > 1 ? parts.first : 'Overview' + end + end + end +end diff --git a/lib/docs/filters/pytorch/clean_html.rb b/lib/docs/filters/pytorch/clean_html.rb index dd19c3e0f7..455b939ee4 100644 --- a/lib/docs/filters/pytorch/clean_html.rb +++ b/lib/docs/filters/pytorch/clean_html.rb @@ -2,9 +2,11 @@ module Docs class Pytorch class CleanHtmlFilter < Filter def call - @doc = at_css('.pytorch-article') - # Show katex-mathml nodes and remove katex-html nodes - css('.katex-html').remove + if root = at_css('#pytorch-article') + @doc = root + # Show katex-mathml nodes and remove katex-html nodes + css('.katex-html').remove + end doc end end diff --git a/lib/docs/filters/pytorch/entries.rb b/lib/docs/filters/pytorch/entries.rb index 37e26e3129..85d2f06418 100644 --- a/lib/docs/filters/pytorch/entries.rb +++ b/lib/docs/filters/pytorch/entries.rb @@ -2,9 +2,23 @@ module Docs class Pytorch class EntriesFilter < Docs::EntriesFilter def get_breadcrumbs - css('.pytorch-breadcrumbs > li').map { - |node| node.content.delete_suffix(' >').strip - }.reject { |item| item.nil? || item.empty? } + breadcrumbs = if at_css('.pytorch-breadcrumbs') + css('.pytorch-breadcrumbs > li').map { |node| + node.content.delete_suffix(' >').strip + } + else + css('.bd-breadcrumbs > li').map { |node| + text = node.content.strip + text.empty? && node.at_css('.fa-home') ? 'Docs' : text + } + end.reject { |item| item.nil? || item.empty? } + + if breadcrumbs.last&.end_with?('.') + resolved_name = at_css('h1').content.delete_suffix('#').strip + breadcrumbs[-1] = resolved_name + end + + breadcrumbs end def get_name @@ -12,7 +26,15 @@ def get_name end def get_type - get_breadcrumbs[1] + if at_css('.pytorch-breadcrumbs') + get_breadcrumbs[1] + else + get_breadcrumbs.size > 2 ? get_breadcrumbs[2] : get_breadcrumbs[1] + end + end + + def include_default_entry? + !get_breadcrumbs.nil? && get_breadcrumbs.size >= 2 end def additional_entries diff --git a/lib/docs/filters/rabbit_mq/clean_html.rb b/lib/docs/filters/rabbit_mq/clean_html.rb new file mode 100644 index 0000000000..1513f2395a --- /dev/null +++ b/lib/docs/filters/rabbit_mq/clean_html.rb @@ -0,0 +1,24 @@ +module Docs + class RabbitMq + class CleanHtmlFilter < Filter + def call + @doc = at_css('.theme-doc-markdown.markdown') + + css('.theme-admonition svg').remove + + css('pre').each do |node| + node.content = node.css('.token-line').map(&:content).join("\n") + + node['data-language'] = + if node['class'].include?('language-bash') + 'bash' + elsif node['class'].include?('language-java') + 'java' + end + end + + doc + end + end + end +end diff --git a/lib/docs/filters/rabbit_mq/entries.rb b/lib/docs/filters/rabbit_mq/entries.rb new file mode 100644 index 0000000000..eb4a73c6e5 --- /dev/null +++ b/lib/docs/filters/rabbit_mq/entries.rb @@ -0,0 +1,19 @@ +module Docs + class RabbitMq + class EntriesFilter < Docs::EntriesFilter + def get_name + at_css('h1').content + end + + def get_type + breadcrumb = css('.breadcrumbs__item') + + if breadcrumb.length > 1 + breadcrumb[1].inner_text + else + 'Other' + end + end + end + end +end diff --git a/lib/docs/filters/rails/clean_html_guides.rb b/lib/docs/filters/rails/clean_html_guides.rb index bd8ad439c8..86ffc07210 100644 --- a/lib/docs/filters/rails/clean_html_guides.rb +++ b/lib/docs/filters/rails/clean_html_guides.rb @@ -4,8 +4,9 @@ class CleanHtmlGuidesFilter < Filter def call return doc unless slug.start_with?('guides') - at_css('#mainCol').prepend_child at_css('#feature .wrapper').children - @doc = at_css('#mainCol') + main_col = at_css('#mainCol') || at_css('#column-main') + main_col.prepend_child at_css('#feature .wrapper').children + @doc = main_col container = Nokogiri::XML::Node.new 'div', doc.document container['class'] = '_simple' diff --git a/lib/docs/filters/rails/entries.rb b/lib/docs/filters/rails/entries.rb index 343f85952f..a263125285 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('#mainCol h2').content.strip + name = (at_css('#mainCol h2') || at_css('#column-main h2')).content.strip name.remove! %r{\s\(.+\)\z} return name end diff --git a/lib/docs/filters/rdoc/clean_html.rb b/lib/docs/filters/rdoc/clean_html.rb index ff6d76604d..b4d9669f97 100644 --- a/lib/docs/filters/rdoc/clean_html.rb +++ b/lib/docs/filters/rdoc/clean_html.rb @@ -42,7 +42,9 @@ def other link_node.content = 'Show source' link_node['class'] = 'method-click-advice' - node.parent.parent.at_css('.method-heading').add_child(link_node) + # Only add "Show source" if source is present + method_root = node.ancestors('.method-detail').first + method_root.at_css('.method-heading').add_child(link_node) if method_root.at_css('.method-source-code') end # (RDoc for Ruby 3.4+) Remove the additional "Source" toggle from the page @@ -50,7 +52,8 @@ def other # Add class to differentiate Ruby code from C code css('.method-source-code').each do |node| - node.parent.prepend_child(node) + header = node.ancestors('.method-detail').first.at_css('.method-header') + header.add_next_sibling(node) pre = node.at_css('pre') pre['class'] = pre.at_css('.ruby-keyword') ? 'ruby' : 'c' end @@ -61,6 +64,9 @@ def other node['data-language'] = 'c' if node['class'] == 'c' node['data-language'] = 'ruby' if node['class'] && node['class'].include?('ruby') end + + # Remove navigation breadcrumbs + css('ol[role="navigation"]').remove end end end diff --git a/lib/docs/filters/rust/clean_html.rb b/lib/docs/filters/rust/clean_html.rb index 748d3ffbfd..9ce04be520 100644 --- a/lib/docs/filters/rust/clean_html.rb +++ b/lib/docs/filters/rust/clean_html.rb @@ -5,7 +5,7 @@ class Rust class CleanHtmlFilter < Filter def call if slug.start_with?('book') || slug.start_with?('reference') || slug.start_with?('error_codes') - @doc = at_css('#content main') + @doc = at_css('#mdbook-content main') elsif slug.start_with?('error_codes') css('.error-undescribed').remove diff --git a/lib/docs/filters/rust/entries.rb b/lib/docs/filters/rust/entries.rb index 4effefd668..7ab86d6444 100644 --- a/lib/docs/filters/rust/entries.rb +++ b/lib/docs/filters/rust/entries.rb @@ -3,25 +3,22 @@ class Rust class EntriesFilter < Docs::EntriesFilter def get_name - if slug.start_with?('book') - name = at_css('main h1', 'main h2') - - if slug.start_with?('book/appendix') - return name ? name.content : 'Appendix' - end - + name = at_css('main h1', 'main h2', '.main-heading h1') + if slug.start_with?('book/appendix') + name ? name.content : 'Appendix' + elsif slug.start_with?('book') ch1 = slug[/ch(\d+)-(\d+)/, 1] || '00' ch2 = slug[/ch(\d+)-(\d+)/, 2] || '00' name ? "#{ch1}.#{ch2}. #{name.content}" : 'Introduction' elsif slug.start_with?('reference') - at_css('main h1').content + name.content elsif slug == 'error_codes/error-index' 'Compiler Errors' elsif slug.start_with?('error_codes') slug.split('/').last.upcase else - at_css('main h1').at_css('button')&.remove - name = at_css('main h1').content.remove(/\A.+\s/).remove('⎘') + name.at_css('button')&.remove + name = name.content.strip.remove(/\A.+\s/) path = slug.split('/') if path.length == 2 # Anything in the standard library but not in a `std::*` module is diff --git a/lib/docs/filters/tokio/clean_html.rb b/lib/docs/filters/tokio/clean_html.rb new file mode 100644 index 0000000000..d7a88fde0f --- /dev/null +++ b/lib/docs/filters/tokio/clean_html.rb @@ -0,0 +1,32 @@ +module Docs + class Tokio + class CleanHtmlFilter < Filter + def call + css('.main-heading h1').each do |node| + node.parent.replace(node) + end + + doc.css( + 'a.doc-anchor', + 'a.anchor', + 'a.src', + 'button#copy-path', + '.rustdoc-breadcrumbs', + '.sub-heading' + ).remove + + doc.css('pre.rust').each do |code| + code['data-language'] = 'rust' + end + + # Get rid of toggles hiding useful info. + doc.css('details').each do |node| + node.name = 'div' + end + doc.css('summary.hideme').remove + + doc + end + end + end +end diff --git a/lib/docs/filters/tokio/entries.rb b/lib/docs/filters/tokio/entries.rb new file mode 100644 index 0000000000..3d7814f7b7 --- /dev/null +++ b/lib/docs/filters/tokio/entries.rb @@ -0,0 +1,35 @@ +module Docs + class Tokio + class EntriesFilter < Docs::EntriesFilter + def get_name + namespace = at_css('.rustdoc-breadcrumbs').inner_text + header = at_css('h1 > span').inner_text + "#{namespace}::#{header}" + end + + def get_type + header = at_css('h1').inner_text + + if header.include?('Attribute Macro') + 'Attribute Macros' + elsif header.include?('Macro') + 'Macros' + elsif header.include?('Module') + 'Modules' + elsif header.include?('Struct') + 'Structs' + elsif header.include?('Function') + 'Functions' + elsif header.include?('Enum') + 'Enums' + elsif header.include?('Trait') + 'Traits' + elsif header.include?('Type') + 'Types' + else + 'Other' + end + end + end + end +end diff --git a/lib/docs/filters/typescript/clean_html.rb b/lib/docs/filters/typescript/clean_html.rb index 87e2296ea5..3be9535182 100644 --- a/lib/docs/filters/typescript/clean_html.rb +++ b/lib/docs/filters/typescript/clean_html.rb @@ -36,7 +36,12 @@ def other else # tsconfig page end + css('> section').each do |node| + node.before(node.children).remove + end + css('.anchor', 'a:contains("Try")', 'h2 a', 'h3 a', 'svg', '#full-option-list').remove + css('#sidebar', '.page-helpful-popup', '.handbook-toc', '.bottom-section-content').remove # tsconfig page css('.markdown', '.compiler-option', '.compiler-option-md', '.compiler-content').each do |node| diff --git a/lib/docs/filters/vite/clean_html.rb b/lib/docs/filters/vite/clean_html.rb index c19745ca3e..5b3efa9431 100644 --- a/lib/docs/filters/vite/clean_html.rb +++ b/lib/docs/filters/vite/clean_html.rb @@ -24,6 +24,8 @@ def call node.remove end + css('.vp-code-group > .tabs').remove + css('.lang').remove css('.line-numbers-wrapper').remove css('pre').each do |node| diff --git a/lib/docs/filters/vue_router/clean_html.rb b/lib/docs/filters/vue_router/clean_html.rb index d7fa396863..64e55c294a 100644 --- a/lib/docs/filters/vue_router/clean_html.rb +++ b/lib/docs/filters/vue_router/clean_html.rb @@ -2,10 +2,15 @@ module Docs class VueRouter class CleanHtmlFilter < Filter def call - @doc = at_css('main') + @doc = at_css('main > div:only-child > div:only-child', 'main', '.main') + css('p + h1').each do |node| + # breadcrumbs + node.previous_element.remove + end # Remove unneeded elements - css('.bit-sponsor, .header-anchor').remove + css('.bit-sponsor, .header-anchor', '.rulekit', 'div[hidden]', '.sponsors_outer').remove + css('.vp-code-group > .tabs').remove css('.custom-block').each do |node| node.name = 'blockquote' @@ -14,6 +19,12 @@ def call title.name = 'strong' unless title.nil? end + css('span.lang').remove + css('pre > code:first-child').each do |node| + node.parent['data-language'] = 'js' + node.parent.content = node.css('.line').map(&:content).join("\n") + end + # Remove data-v-* attributes css('*').each do |node| node.attributes.each_key do |attribute| diff --git a/lib/docs/filters/vue_router/entries.rb b/lib/docs/filters/vue_router/entries.rb index 11c5e8a068..1f1ae8f40e 100644 --- a/lib/docs/filters/vue_router/entries.rb +++ b/lib/docs/filters/vue_router/entries.rb @@ -3,7 +3,8 @@ class VueRouter class EntriesFilter < Docs::EntriesFilter def get_name name = at_css('h1').content - name.sub! %r{#\s*}, '' + name.sub! %r{#\s*|\s*\u200B\s*}, '' + name.strip! name end @@ -18,6 +19,7 @@ def include_default_entry? end def additional_entries + return [] return [] unless subpath.start_with?('api') entries = [ @@ -29,14 +31,18 @@ def additional_entries css('h3').each do |node| entry_name = node.content.strip + entry_name.sub! %r{#\s*|\s*\u200B\s*}, '' # Get the previous h2 title title = node - title = title.previous_element until title.name == 'h2' - title = title.content.strip - title.sub! %r{#\s*}, '' - - entry_name.sub! %r{#\s*}, '' + begin + title = title.previous_element until title.name == 'h2' + title = title.content.strip + title.sub! %r{#\s*}, '' + rescue + title = '' + entry_name = "#{name}.#{entry_name}" + end case title when 'Router Construction Options' diff --git a/lib/docs/scrapers/angular.rb b/lib/docs/scrapers/angular.rb index 4725c3fd00..6de0386f02 100644 --- a/lib/docs/scrapers/angular.rb +++ b/lib/docs/scrapers/angular.rb @@ -100,7 +100,7 @@ def self.handle_redirects(version) end version do - self.release = '20.3.4' + self.release = '22.0.0' self.base_url = 'https://angular.dev/' self.root_path = 'overview' @@ -113,6 +113,34 @@ def self.handle_redirects(version) include Docs::Angular::Since18 end + version '21' do + self.release = '21.2.16' + self.base_url = 'https://v21.angular.dev/' + self.root_path = 'overview' + + options[:follow_links] = true + options[:container] = '.docs-app-main-content' + options[:fix_urls] = Since18.handle_redirects(self.version) + + html_filters.push 'angular/entries', 'angular/clean_html_v18' + + include Docs::Angular::Since18 + end + + version '20' do + self.release = '20.3.15' + self.base_url = 'https://v20.angular.dev/' + self.root_path = 'overview' + + options[:follow_links] = true + options[:container] = '.docs-app-main-content' + options[:fix_urls] = Since18.handle_redirects(self.version) + + html_filters.push 'angular/entries', 'angular/clean_html_v18' + + include Docs::Angular::Since18 + end + version '19' do self.release = '19.2.15' self.base_url = 'https://v19.angular.dev/' diff --git a/lib/docs/scrapers/ansible.rb b/lib/docs/scrapers/ansible.rb index 79a5dd8ae0..868a6d81f3 100644 --- a/lib/docs/scrapers/ansible.rb +++ b/lib/docs/scrapers/ansible.rb @@ -33,8 +33,8 @@ class Ansible < UrlScraper ] version do - # 2025-08-14 - self.base_url = "https://docs.ansible.com/ansible/latest/" + # 2026-05-26 + self.base_url = "https://docs.ansible.com/projects/ansible/latest/" end version '2.11' do diff --git a/lib/docs/scrapers/astro.rb b/lib/docs/scrapers/astro.rb index 40c496e15e..0eea6dfdc0 100644 --- a/lib/docs/scrapers/astro.rb +++ b/lib/docs/scrapers/astro.rb @@ -16,7 +16,7 @@ class Astro < UrlScraper options[:skip_patterns] = [/tutorial/, /getting-started/] - self.release = '5.10.2' + self.release = '5.16.5' self.base_url = 'https://docs.astro.build/en/' self.initial_paths = %w(install-and-setup/) diff --git a/lib/docs/scrapers/axios.rb b/lib/docs/scrapers/axios.rb index c29b41ba6c..60da406836 100644 --- 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.9.0' + self.release = '1.15.0' self.base_url = "https://axios-http.com/docs/" self.initial_paths = %w(index intro) options[:skip] = %w(sponsor) diff --git a/lib/docs/scrapers/bash.rb b/lib/docs/scrapers/bash.rb index 3c91a05608..2c0295625e 100644 --- a/lib/docs/scrapers/bash.rb +++ b/lib/docs/scrapers/bash.rb @@ -1,7 +1,7 @@ module Docs class Bash < FileScraper self.type = 'bash' - self.release = '5.2' + self.release = '5.3' self.base_url = 'https://www.gnu.org/software/bash/manual/html_node' self.root_path = 'index.html' self.links = { diff --git a/lib/docs/scrapers/bun.rb b/lib/docs/scrapers/bun.rb index e161cf3d4f..44c5d41c84 100644 --- a/lib/docs/scrapers/bun.rb +++ b/lib/docs/scrapers/bun.rb @@ -7,7 +7,7 @@ class Bun < UrlScraper home: 'https://leafletjs.com/', code: 'https://github.com/oven-sh/bun' } - self.release = '1.3.0' + self.release = '1.3.12' self.base_url = "https://bun.com/docs/" self.root_path = 'installation' @@ -19,14 +19,15 @@ class Bun < UrlScraper Licensed under the MIT License. HTML - options[:skip_patterns] = [/^project/] + options[:download_images] = false + options[:skip_patterns] = [/^project/, /^feedback/] options[:fix_urls] = ->(url) do url.sub! %r{.md$}, '' url end def get_latest_version(opts) - tags = get_github_tags('oven-sh', 'bun', opts) + get_latest_github_release('oven-sh', 'bun', opts)[5..] end end end diff --git a/lib/docs/scrapers/celery.rb b/lib/docs/scrapers/celery.rb new file mode 100644 index 0000000000..270e594a91 --- /dev/null +++ b/lib/docs/scrapers/celery.rb @@ -0,0 +1,28 @@ +module Docs + class Celery < UrlScraper + self.name = 'Celery' + self.type = 'celery' + self.release = '5.6.3' + self.base_url = 'https://docs.celeryq.dev/en/stable/' + self.links = { + home: 'https://docs.celeryq.dev', + code: 'https://github.com/celery/celery' + } + + html_filters.push 'celery/entries', 'celery/clean_html' + + options[:container] = 'div.body' + options[:rate_limit] = 100 + options[:only_patterns] = [/userguide\//, /reference\//] + + options[:attribution] = <<-HTML + Copyright © 2017-2026 Asif Saif Uddin, core team & contributors. All rights reserved.
    + Celery is licensed under The BSD License (3 Clause, also known as the new BSD license). The license is an OSI approved Open Source license and is GPL-compatible. + HTML + + def get_latest_version(opts) + tags = get_github_tags('celery', 'celery', opts) + tags[0]['name'][1..-1] + end + end +end diff --git a/lib/docs/scrapers/coldfusion.rb b/lib/docs/scrapers/coldfusion.rb new file mode 100644 index 0000000000..ce827d9eda --- /dev/null +++ b/lib/docs/scrapers/coldfusion.rb @@ -0,0 +1,58 @@ +module Docs + class Coldfusion < UrlScraper + self.name = 'ColdFusion' + self.slug = 'coldfusion' + self.type = 'simple' + self.base_url = 'https://cfdocs.org/' + self.root_path = 'index.cfm' + self.links = { + home: 'https://cfdocs.org/', + code: 'https://github.com/foundeo/cfdocs' + } + self.release = '2026-04-30' + + html_filters.push 'coldfusion/entries', 'coldfusion/clean_html' + + options[:root_title] = 'ColdFusion' + + # cfdocs links categories with an encoded dash (e.g. /array%2Dfunctions); + # decode and clean those so entry paths look like /array-functions. + options[:decode_and_clean_paths] = true + + # cfdocs.org renders a page for every tag/function/guide at the site root, + # e.g. /hash or /cfhtmltopdf. Category "listing" pages (such as /tags, + # /functions and /array-functions) are crawled to discover entries, but the + # Entries filter excludes them from the index. + # + # Skip site chrome, utilities, reports and other non-reference pages. + options[:skip] = %w( + 404.cfm contributors.cfm trycf.cfm ucase.cfm llms.cfm + how-to-contribute opensearch.xml robots.txt) + + options[:skip_patterns] = [ + /\Aassets\b/, + /\Areports\b/, + /\Autilities\b/, + /\Aslack\b/, + /openimage/, + /\.json\z/, + /\.png\z/, + /\.ico\z/, + /\.xml\z/, + /\.css\z/, + /\.js\z/ + ] + + options[:attribution] = <<-HTML + © 2012–present Foundeo, Inc. and the CFDocs contributors.
    + Licensed under the MIT License.
    + ColdFusion is a trademark of Adobe Systems Incorporated. + HTML + + def get_latest_version(opts) + # CFDocs is continuously updated and has no formal version number; use the + # date of the latest commit as a proxy version. + get_latest_github_commit_date('foundeo', 'cfdocs', opts) + end + end +end diff --git a/lib/docs/scrapers/couchdb.rb b/lib/docs/scrapers/couchdb.rb new file mode 100644 index 0000000000..a077e195a1 --- /dev/null +++ b/lib/docs/scrapers/couchdb.rb @@ -0,0 +1,39 @@ +module Docs + class Couchdb < UrlScraper + self.name = 'CouchDB' + self.type = 'couchdb' + self.root_path = 'index.html' + + self.links = { + home: 'https://couchdb.apache.org/', + code: 'https://github.com/apache/couchdb' + } + + html_filters.push 'couchdb/clean_html', 'couchdb/entries' + + options[:container] = 'div[itemprop=articleBody]' + options[:only_patterns] = [ + /api\//, + /cluster\//, + /ddocs\//, + /replication\//, + /maintenance\//, + /partitioned-dbs\//, + /json\-structure*/ + ] + options[:rate_limit] = 50 # Docs are subject to Cloudflare limiters. + options[:attribution] = <<-HTML + Copyright © 2025 The Apache Software Foundation — Licensed under the Apache License 2.0 + HTML + + version '3.5' do + self.release = '3.5.1' + self.base_url = "https://docs.couchdb.org/en/#{self.release}" + end + + def get_latest_version(opts) + doc = fetch_doc('https://couchdb.apache.org/', opts) + doc.at_css('.download-pane > h2').content.split(' ').last + end + end +end diff --git a/lib/docs/scrapers/crystal.rb b/lib/docs/scrapers/crystal.rb index 09e62a4558..0534c8525d 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.18.0' + self.release = '1.20.0' self.base_urls = [ "https://crystal-lang.org/api/#{release}/", "https://crystal-lang.org/reference/#{release[0..2]}/", @@ -37,7 +37,7 @@ def initial_urls HTML else <<-HTML - © 2012–2025 Manas Technology Solutions.
    + © 2012–2026 Manas Technology Solutions.
    Licensed under the Apache License, Version 2.0. HTML end diff --git a/lib/docs/scrapers/cyclejs.rb b/lib/docs/scrapers/cyclejs.rb new file mode 100644 index 0000000000..abaa3c9a7d --- /dev/null +++ b/lib/docs/scrapers/cyclejs.rb @@ -0,0 +1,84 @@ +module Docs + class Cyclejs < UrlScraper + self.name = 'Cycle.js' + self.slug = 'cyclejs' + self.type = 'cyclejs' + self.release = '23.1.0' + self.base_url = 'https://cycle.js.org/' + self.root_path = 'index.html' + self.initial_paths = %w( + getting-started.html + model-view-intent.html + streams.html + drivers.html + components.html + basic-examples.html + dialogue.html + releases.html + api/index.html + api/run.html + api/rxjs-run.html + api/most-run.html + api/dom.html + api/html.html + api/http.html + api/history.html + api/isolate.html + api/state.html + ) + + self.links = { + home: 'https://cycle.js.org/', + code: 'https://github.com/cyclejs/cyclejs' + } + + html_filters.push 'cyclejs/clean_html', 'cyclejs/entries' + + options[:only_patterns] = [ + /\Aindex\.html\z/, + /\Agetting-started\.html\z/, + /\Amodel-view-intent\.html\z/, + /\Astreams\.html\z/, + /\Adrivers\.html\z/, + /\Acomponents\.html\z/, + /\Abasic-examples\.html\z/, + /\Adialogue\.html\z/, + /\Areleases\.html\z/, + /\Aapi\// + ] + + options[:download_images] = false + options[:attribution] = <<-HTML + © 2014–present Cycle.js contributors.
    + Licensed under the MIT License. + HTML + + def get_latest_version(opts) + get_npm_version('@cycle/dom', opts) + end + + private + + def parse(response) + document = Parser.new(response.body).html + markdown = document.at_css('script#markdown') + + return super unless markdown + + html = markdown_renderer.render(markdown.content.strip) + title = document.at_css('title').try(:content).try(:strip) + [Parser.new("#{title}#{html}").html, title] + end + + def markdown_renderer + require 'redcarpet' + @markdown_renderer ||= Redcarpet::Markdown.new( + Redcarpet::Render::HTML.new(with_toc_data: true), + autolink: true, + fenced_code_blocks: true, + no_intra_emphasis: true, + tables: true + ) + end + end +end diff --git a/lib/docs/scrapers/django.rb b/lib/docs/scrapers/django.rb index 4a5848c4f5..63c8d25e2e 100644 --- a/lib/docs/scrapers/django.rb +++ b/lib/docs/scrapers/django.rb @@ -34,6 +34,11 @@ class Django < FileScraper Licensed under the BSD License. HTML + version '6.1' do + self.release = '6.1' + self.base_url = "https://docs.djangoproject.com/en/#{self.version}/" + end + version '6.0' do self.release = '6.0' self.base_url = "https://docs.djangoproject.com/en/#{self.version}/" diff --git a/lib/docs/scrapers/electron.rb b/lib/docs/scrapers/electron.rb index 76d5a1703c..f81f7eedd7 100644 --- a/lib/docs/scrapers/electron.rb +++ b/lib/docs/scrapers/electron.rb @@ -26,8 +26,7 @@ class Electron < UrlScraper HTML def get_latest_version(opts) - doc = fetch_doc('https://www.electronjs.org/releases/stable', opts) - doc.at_css('.release-card__metadata>a')['href'].gsub!(/[a-zA-Z\/:]/, '')[1..-1] + get_latest_github_release('electron', 'electron', opts) end end end diff --git a/lib/docs/scrapers/elixir.rb b/lib/docs/scrapers/elixir.rb index 14b3c1645c..8f545342d2 100644 --- a/lib/docs/scrapers/elixir.rb +++ b/lib/docs/scrapers/elixir.rb @@ -17,52 +17,64 @@ class Elixir < UrlScraper options[:root_title] = 'Elixir' options[:attribution] = <<-HTML - © 2012-2024 The Elixir Team
    + © 2012-2026 The Elixir Team
    Licensed under the Apache License, Version 2.0. HTML def initial_urls - [ "https://hexdocs.pm/elixir/#{self.class.release}/introduction.html", - "https://hexdocs.pm/eex/#{self.class.release}/EEx.html", - "https://hexdocs.pm/ex_unit/#{self.class.release}/ExUnit.html", - "https://hexdocs.pm/iex/#{self.class.release}/IEx.html", - "https://hexdocs.pm/logger/#{self.class.release}/Logger.html", - "https://hexdocs.pm/mix/#{self.class.release}/Mix.html" ] + [ "https://elixir.hexdocs.pm/#{self.class.release}/introduction.html", + "https://eex.hexdocs.pm/#{self.class.release}/EEx.html", + "https://ex-unit.hexdocs.pm/#{self.class.release}/ExUnit.html", + "https://iex.hexdocs.pm/#{self.class.release}/IEx.html", + "https://logger.hexdocs.pm/#{self.class.release}/Logger.html", + "https://mix.hexdocs.pm/#{self.class.release}/Mix.html" ] + end + + version '1.20' do + self.release = '1.20.1' + self.base_urls = [ + "https://elixir.hexdocs.pm/#{release}/", + "https://eex.hexdocs.pm/#{release}/", + "https://ex-unit.hexdocs.pm/#{release}/", + "https://iex.hexdocs.pm/#{release}/", + "https://logger.hexdocs.pm/#{release}/", + "https://mix.hexdocs.pm/#{release}/" + ] end version '1.18' do self.release = '1.18.1' self.base_urls = [ - "https://hexdocs.pm/elixir/#{release}/", - "https://hexdocs.pm/eex/#{release}/", - "https://hexdocs.pm/ex_unit/#{release}/", - "https://hexdocs.pm/iex/#{release}/", - "https://hexdocs.pm/logger/#{release}/", - "https://hexdocs.pm/mix/#{release}/" + "https://elixir.hexdocs.pm/#{release}/", + "https://eex.hexdocs.pm/#{release}/", + "https://ex-unit.hexdocs.pm/#{release}/", + "https://iex.hexdocs.pm/#{release}/", + "https://logger.hexdocs.pm/#{release}/", + "https://mix.hexdocs.pm/#{release}/" ] end version '1.17' do self.release = '1.17.2' self.base_urls = [ - "https://hexdocs.pm/elixir/#{release}/", - "https://hexdocs.pm/eex/#{release}/", - "https://hexdocs.pm/ex_unit/#{release}/", - "https://hexdocs.pm/iex/#{release}/", - "https://hexdocs.pm/logger/#{release}/", - "https://hexdocs.pm/mix/#{release}/" + "https://elixir.hexdocs.pm/#{release}/", + "https://eex.hexdocs.pm/#{release}/", + "https://ex-unit.hexdocs.pm/#{release}/", + "https://iex.hexdocs.pm/#{release}/", + "https://logger.hexdocs.pm/#{release}/", + "https://mix.hexdocs.pm/#{release}/" ] end version '1.16' do self.release = '1.16.3' self.base_urls = [ - "https://hexdocs.pm/elixir/#{release}/", - "https://hexdocs.pm/eex/#{release}/", - "https://hexdocs.pm/ex_unit/#{release}/", - "https://hexdocs.pm/iex/#{release}/", - "https://hexdocs.pm/logger/#{release}/", - "https://hexdocs.pm/mix/#{release}/" + "https://elixir.hexdocs.pm/#{release}/", + "https://eex.hexdocs.pm/#{release}/", + "https://ex-unit.hexdocs.pm/#{release}/", + "https://iex.hexdocs.pm/#{release}/", + "https://logger.hexdocs.pm/#{release}/", + "https://mix.hexdocs.pm/#{release}/" ] end @@ -71,12 +83,12 @@ def initial_urls version '1.15' do self.release = '1.15.4' self.base_urls = [ - "https://hexdocs.pm/elixir/#{release}/", - "https://hexdocs.pm/eex/#{release}/", - "https://hexdocs.pm/ex_unit/#{release}/", - "https://hexdocs.pm/iex/#{release}/", - "https://hexdocs.pm/logger/#{release}/", - "https://hexdocs.pm/mix/#{release}/", + "https://elixir.hexdocs.pm/#{release}/", + "https://eex.hexdocs.pm/#{release}/", + "https://ex-unit.hexdocs.pm/#{release}/", + "https://iex.hexdocs.pm/#{release}/", + "https://logger.hexdocs.pm/#{release}/", + "https://mix.hexdocs.pm/#{release}/", 'https://elixir-lang.org/getting-started/' ] end @@ -84,12 +96,12 @@ def initial_urls version '1.14' do self.release = '1.14.1' self.base_urls = [ - "https://hexdocs.pm/elixir/#{release}/", - "https://hexdocs.pm/eex/#{release}/", - "https://hexdocs.pm/ex_unit/#{release}/", - "https://hexdocs.pm/iex/#{release}/", - "https://hexdocs.pm/logger/#{release}/", - "https://hexdocs.pm/mix/#{release}/", + "https://elixir.hexdocs.pm/#{release}/", + "https://eex.hexdocs.pm/#{release}/", + "https://ex-unit.hexdocs.pm/#{release}/", + "https://iex.hexdocs.pm/#{release}/", + "https://logger.hexdocs.pm/#{release}/", + "https://mix.hexdocs.pm/#{release}/", 'https://elixir-lang.org/getting-started/' ] end @@ -97,12 +109,12 @@ def initial_urls version '1.13' do self.release = '1.13.4' self.base_urls = [ - "https://hexdocs.pm/elixir/#{release}/", - "https://hexdocs.pm/eex/#{release}/", - "https://hexdocs.pm/ex_unit/#{release}/", - "https://hexdocs.pm/iex/#{release}/", - "https://hexdocs.pm/logger/#{release}/", - "https://hexdocs.pm/mix/#{release}/", + "https://elixir.hexdocs.pm/#{release}/", + "https://eex.hexdocs.pm/#{release}/", + "https://ex-unit.hexdocs.pm/#{release}/", + "https://iex.hexdocs.pm/#{release}/", + "https://logger.hexdocs.pm/#{release}/", + "https://mix.hexdocs.pm/#{release}/", 'https://elixir-lang.org/getting-started/' ] end @@ -110,12 +122,12 @@ def initial_urls version '1.12' do self.release = '1.12.0' self.base_urls = [ - "https://hexdocs.pm/elixir/#{release}/", - "https://hexdocs.pm/eex/#{release}/", - "https://hexdocs.pm/ex_unit/#{release}/", - "https://hexdocs.pm/iex/#{release}/", - "https://hexdocs.pm/logger/#{release}/", - "https://hexdocs.pm/mix/#{release}/", + "https://elixir.hexdocs.pm/#{release}/", + "https://eex.hexdocs.pm/#{release}/", + "https://ex-unit.hexdocs.pm/#{release}/", + "https://iex.hexdocs.pm/#{release}/", + "https://logger.hexdocs.pm/#{release}/", + "https://mix.hexdocs.pm/#{release}/", 'https://elixir-lang.org/getting-started/' ] end @@ -123,12 +135,12 @@ def initial_urls version '1.11' do self.release = '1.11.2' self.base_urls = [ - "https://hexdocs.pm/elixir/#{release}/", - "https://hexdocs.pm/eex/#{release}/", - "https://hexdocs.pm/ex_unit/#{release}/", - "https://hexdocs.pm/iex/#{release}/", - "https://hexdocs.pm/logger/#{release}/", - "https://hexdocs.pm/mix/#{release}/", + "https://elixir.hexdocs.pm/#{release}/", + "https://eex.hexdocs.pm/#{release}/", + "https://ex-unit.hexdocs.pm/#{release}/", + "https://iex.hexdocs.pm/#{release}/", + "https://logger.hexdocs.pm/#{release}/", + "https://mix.hexdocs.pm/#{release}/", 'https://elixir-lang.org/getting-started/' ] end @@ -136,12 +148,12 @@ def initial_urls version '1.10' do self.release = '1.10.4' self.base_urls = [ - "https://hexdocs.pm/elixir/#{release}/", - "https://hexdocs.pm/eex/#{release}/", - "https://hexdocs.pm/ex_unit/#{release}/", - "https://hexdocs.pm/iex/#{release}/", - "https://hexdocs.pm/logger/#{release}/", - "https://hexdocs.pm/mix/#{release}/", + "https://elixir.hexdocs.pm/#{release}/", + "https://eex.hexdocs.pm/#{release}/", + "https://ex-unit.hexdocs.pm/#{release}/", + "https://iex.hexdocs.pm/#{release}/", + "https://logger.hexdocs.pm/#{release}/", + "https://mix.hexdocs.pm/#{release}/", 'https://elixir-lang.org/getting-started/' ] end @@ -149,12 +161,12 @@ def initial_urls version '1.9' do self.release = '1.9.4' self.base_urls = [ - "https://hexdocs.pm/elixir/#{release}/", - "https://hexdocs.pm/eex/#{release}/", - "https://hexdocs.pm/ex_unit/#{release}/", - "https://hexdocs.pm/iex/#{release}/", - "https://hexdocs.pm/logger/#{release}/", - "https://hexdocs.pm/mix/#{release}/", + "https://elixir.hexdocs.pm/#{release}/", + "https://eex.hexdocs.pm/#{release}/", + "https://ex-unit.hexdocs.pm/#{release}/", + "https://iex.hexdocs.pm/#{release}/", + "https://logger.hexdocs.pm/#{release}/", + "https://mix.hexdocs.pm/#{release}/", 'https://elixir-lang.org/getting-started/' ] end @@ -162,12 +174,12 @@ def initial_urls version '1.8' do self.release = '1.8.2' self.base_urls = [ - "https://hexdocs.pm/elixir/#{release}/", - "https://hexdocs.pm/eex/#{release}/", - "https://hexdocs.pm/ex_unit/#{release}/", - "https://hexdocs.pm/iex/#{release}/", - "https://hexdocs.pm/logger/#{release}/", - "https://hexdocs.pm/mix/#{release}/", + "https://elixir.hexdocs.pm/#{release}/", + "https://eex.hexdocs.pm/#{release}/", + "https://ex-unit.hexdocs.pm/#{release}/", + "https://iex.hexdocs.pm/#{release}/", + "https://logger.hexdocs.pm/#{release}/", + "https://mix.hexdocs.pm/#{release}/", 'https://elixir-lang.org/getting-started/' ] end @@ -175,12 +187,12 @@ def initial_urls version '1.7' do self.release = '1.7.4' self.base_urls = [ - "https://hexdocs.pm/elixir/#{release}/", - "https://hexdocs.pm/eex/#{release}/", - "https://hexdocs.pm/ex_unit/#{release}/", - "https://hexdocs.pm/iex/#{release}/", - "https://hexdocs.pm/logger/#{release}/", - "https://hexdocs.pm/mix/#{release}/", + "https://elixir.hexdocs.pm/#{release}/", + "https://eex.hexdocs.pm/#{release}/", + "https://ex-unit.hexdocs.pm/#{release}/", + "https://iex.hexdocs.pm/#{release}/", + "https://logger.hexdocs.pm/#{release}/", + "https://mix.hexdocs.pm/#{release}/", 'https://elixir-lang.org/getting-started/' ] end @@ -188,12 +200,12 @@ def initial_urls version '1.6' do self.release = '1.6.6' self.base_urls = [ - "https://hexdocs.pm/elixir/#{release}/", - "https://hexdocs.pm/eex/#{release}/", - "https://hexdocs.pm/ex_unit/#{release}/", - "https://hexdocs.pm/iex/#{release}/", - "https://hexdocs.pm/logger/#{release}/", - "https://hexdocs.pm/mix/#{release}/", + "https://elixir.hexdocs.pm/#{release}/", + "https://eex.hexdocs.pm/#{release}/", + "https://ex-unit.hexdocs.pm/#{release}/", + "https://iex.hexdocs.pm/#{release}/", + "https://logger.hexdocs.pm/#{release}/", + "https://mix.hexdocs.pm/#{release}/", 'https://elixir-lang.org/getting-started/' ] end @@ -201,12 +213,12 @@ def initial_urls version '1.5' do self.release = '1.5.3' self.base_urls = [ - "https://hexdocs.pm/elixir/#{release}/", - "https://hexdocs.pm/eex/#{release}/", - "https://hexdocs.pm/ex_unit/#{release}/", - "https://hexdocs.pm/iex/#{release}/", - "https://hexdocs.pm/logger/#{release}/", - "https://hexdocs.pm/mix/#{release}/", + "https://elixir.hexdocs.pm/#{release}/", + "https://eex.hexdocs.pm/#{release}/", + "https://ex-unit.hexdocs.pm/#{release}/", + "https://iex.hexdocs.pm/#{release}/", + "https://logger.hexdocs.pm/#{release}/", + "https://mix.hexdocs.pm/#{release}/", 'https://elixir-lang.org/getting-started/' ] end @@ -214,12 +226,12 @@ def initial_urls version '1.4' do self.release = '1.4.5' self.base_urls = [ - "https://hexdocs.pm/elixir/#{release}/", - "https://hexdocs.pm/eex/#{release}/", - "https://hexdocs.pm/ex_unit/#{release}/", - "https://hexdocs.pm/iex/#{release}/", - "https://hexdocs.pm/logger/#{release}/", - "https://hexdocs.pm/mix/#{release}/", + "https://elixir.hexdocs.pm/#{release}/", + "https://eex.hexdocs.pm/#{release}/", + "https://ex-unit.hexdocs.pm/#{release}/", + "https://iex.hexdocs.pm/#{release}/", + "https://logger.hexdocs.pm/#{release}/", + "https://mix.hexdocs.pm/#{release}/", 'https://elixir-lang.org/getting-started/' ] end @@ -227,18 +239,18 @@ def initial_urls version '1.3' do self.release = '1.3.4' self.base_urls = [ - "https://hexdocs.pm/elixir/#{release}/", - "https://hexdocs.pm/eex/#{release}/", - "https://hexdocs.pm/ex_unit/#{release}/", - "https://hexdocs.pm/iex/#{release}/", - "https://hexdocs.pm/logger/#{release}/", - "https://hexdocs.pm/mix/#{release}/", + "https://elixir.hexdocs.pm/#{release}/", + "https://eex.hexdocs.pm/#{release}/", + "https://ex-unit.hexdocs.pm/#{release}/", + "https://iex.hexdocs.pm/#{release}/", + "https://logger.hexdocs.pm/#{release}/", + "https://mix.hexdocs.pm/#{release}/", 'https://elixir-lang.org/getting-started/' ] end def get_latest_version(opts) - doc = fetch_doc('https://hexdocs.pm/elixir/api-reference.html', opts) + doc = fetch_doc('https://elixir.hexdocs.pm/api-reference.html', opts) doc.at_css('.sidebar-projectVersion').content.strip[1..-1] end end diff --git a/lib/docs/scrapers/es_toolkit.rb b/lib/docs/scrapers/es_toolkit.rb index 85079723a0..0b5f26bba2 100644 --- a/lib/docs/scrapers/es_toolkit.rb +++ b/lib/docs/scrapers/es_toolkit.rb @@ -7,14 +7,15 @@ class EsToolkit < FileScraper code: "https://github.com/toss/es-toolkit", home: "https://es-toolkit.slash.page", } + self.release = '1.45.1' options[:attribution] = <<-HTML - © 2024-2025, Viva Republica
    + © 2024-2026, Viva Republica
    Licensed under the MIT License. HTML def get_latest_version(opts) - get_github_tags("toss", "es-toolkit", opts).first["name"] + get_github_tags("toss", "es-toolkit", opts).first["name"][1..] end def build_pages(&block) diff --git a/lib/docs/scrapers/eslint.rb b/lib/docs/scrapers/eslint.rb index af71cf4a8d..6cd6c90196 100644 --- a/lib/docs/scrapers/eslint.rb +++ b/lib/docs/scrapers/eslint.rb @@ -2,7 +2,7 @@ module Docs class Eslint < UrlScraper self.name = 'ESLint' self.type = 'simple' - self.release = '9.25.0' + self.release = '10.4.0' self.base_url = 'https://eslint.org/docs/latest/' self.root_path = '/' self.links = { diff --git a/lib/docs/scrapers/express.rb b/lib/docs/scrapers/express.rb index ff6514b5fb..887787e8f1 100644 --- a/lib/docs/scrapers/express.rb +++ b/lib/docs/scrapers/express.rb @@ -28,7 +28,7 @@ class Express < UrlScraper HTML version do - self.release = '5.1.0' + self.release = '5.2.1' self.root_path = '5x/api.html' end diff --git a/lib/docs/scrapers/fastapi.rb b/lib/docs/scrapers/fastapi.rb index d5b3f4e5d5..7fd8a787f8 100644 --- a/lib/docs/scrapers/fastapi.rb +++ b/lib/docs/scrapers/fastapi.rb @@ -2,7 +2,7 @@ module Docs class Fastapi < UrlScraper self.name = 'FastAPI' self.type = 'fastapi' - self.release = '0.115.6' + self.release = '0.136.3' self.base_url = 'https://fastapi.tiangolo.com/' self.root_path = '/' self.links = { diff --git a/lib/docs/scrapers/fish.rb b/lib/docs/scrapers/fish.rb index d74ed975ab..fb3aa669d3 100644 --- a/lib/docs/scrapers/fish.rb +++ b/lib/docs/scrapers/fish.rb @@ -12,12 +12,60 @@ class Fish < UrlScraper # https://fishshell.com/docs/current/license.html options[:attribution] = <<-HTML - © 2005-2009 Axel Liljencrantz, 2009-2025 fish-shell contributors
    + © 2005-2009 Axel Liljencrantz, 2009-2026 fish-shell contributors
    Licensed under the GNU General Public License, version 2. HTML + version '4.7' do + self.release = '4.7.1' + 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 '4.6' do + self.release = '4.6.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 '4.5' do + self.release = '4.5.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 '4.4' do + self.release = '4.4.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 '4.3' do + self.release = '4.3.3' + 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 '4.2' do + self.release = '4.2.1' + 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 '4.1' do - self.release = '4.1.0' + self.release = '4.1.2' self.base_url = "https://fishshell.com/docs/#{version}/" options[:skip].concat %w(genindex.html relnotes.html) diff --git a/lib/docs/scrapers/git.rb b/lib/docs/scrapers/git.rb index 2bda8d96dd..fcbcf4ffd4 100644 --- a/lib/docs/scrapers/git.rb +++ b/lib/docs/scrapers/git.rb @@ -1,7 +1,7 @@ module Docs class Git < UrlScraper self.type = 'git' - self.release = '2.51.0' + self.release = '2.54.0' self.base_url = 'https://git-scm.com/docs' self.initial_paths = %w( /git.html @@ -41,12 +41,12 @@ class Git < UrlScraper options[:container] = '#content' options[:only_patterns] = [/\A\/[^\/]+\z/] - options[:skip] = %w(/howto-index.html) + options[:skip] = %w(/api-index /howto-index) # https://github.com/git/git?tab=License-1-ov-file#readme # NOT https://github.com/git/git-scm.com/blob/gh-pages/MIT-LICENSE.txt options[:attribution] = <<-HTML - © 2005–2025 Linus Torvalds and others
    + © 2005–2026 Linus Torvalds and others
    Licensed under the GNU General Public License version 2. HTML diff --git a/lib/docs/scrapers/github.rb b/lib/docs/scrapers/github.rb index dcadcec865..144dbd7802 100644 --- a/lib/docs/scrapers/github.rb +++ b/lib/docs/scrapers/github.rb @@ -23,7 +23,7 @@ def parse(response) &.first parsed = JSON.parse(embedded_json) - [parsed['payload']['blob']['richText'], parsed['title']] + [parsed['payload']['codeViewBlobRoute']['richText'], parsed['title']] end end end diff --git a/lib/docs/scrapers/go.rb b/lib/docs/scrapers/go.rb index 4f12e3cc98..717aff483f 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.25.1' + self.release = '1.26.0' self.base_url = 'https://golang.org/pkg/' self.links = { home: 'https://golang.org/', @@ -10,7 +10,7 @@ class Go < UrlScraper # Run godoc locally, since https://golang.org/pkg/ redirects to https://pkg.go.dev/std with rate limiting / scraping protection. - # podman run --net host --rm -it docker.io/golang:1.25.1 + # podman run --net host --rm -it docker.io/golang:1.26.0 #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 @@ -18,8 +18,8 @@ class Go < UrlScraper # or using alpine # podman run --net host --rm -it alpine:latest #podman# apk add curl - #podman# curl -LO https://go.dev/dl/go1.25.1.linux-amd64.tar.gz - #podman# tar xf go1.25.1.linux-amd64.tar.gz + #podman# curl -LO https://go.dev/dl/go1.26.0.linux-amd64.tar.gz + #podman# tar xf go1.26.0.linux-amd64.tar.gz #podman# go/bin/go install golang.org/x/tools/cmd/godoc@latest #podman# /root/go/bin/godoc -http 0.0.0.0:6060 -v diff --git a/lib/docs/scrapers/godot.rb b/lib/docs/scrapers/godot.rb index 3c7995479a..970d53f8a5 100644 --- a/lib/docs/scrapers/godot.rb +++ b/lib/docs/scrapers/godot.rb @@ -21,6 +21,12 @@ class Godot < UrlScraper Licensed under the Creative Commons Attribution Unported License v3.0. HTML + version '4.7' do + self.release = '4.7.0' + self.base_url = "https://docs.godotengine.org/en/#{self.version}/" + html_filters.push 'godot/entries', 'godot/clean_html', 'sphinx/clean_html' + end + version '4.2' do self.release = '4.2.2' self.base_url = "https://docs.godotengine.org/en/#{self.version}/" diff --git a/lib/docs/scrapers/haproxy.rb b/lib/docs/scrapers/haproxy.rb index 5f87168732..6e48add6b0 100644 --- a/lib/docs/scrapers/haproxy.rb +++ b/lib/docs/scrapers/haproxy.rb @@ -16,12 +16,27 @@ class Haproxy < UrlScraper options[:follow_links] = false options[:attribution] = <<-HTML - © 2024 Willy Tarreau, HAProxy contributors
    + © 2025 Willy Tarreau, HAProxy contributors
    Licensed under the GNU General Public License version 2. HTML + version '3.3' do + self.release = '3.3.0' + self.base_url = "https://docs.haproxy.org/#{self.version}/" + end + + version '3.2' do + self.release = '3.2.9' + self.base_url = "https://docs.haproxy.org/#{self.version}/" + end + + version '3.1' do + self.release = '3.1.10' + self.base_url = "https://docs.haproxy.org/#{self.version}/" + end + version '3.0' do - self.release = '3.0.0' + self.release = '3.0.12' self.base_url = "https://docs.haproxy.org/#{self.version}/" end diff --git a/lib/docs/scrapers/haskell.rb b/lib/docs/scrapers/haskell.rb index 8d822ece0f..d0aeda1456 100644 --- a/lib/docs/scrapers/haskell.rb +++ b/lib/docs/scrapers/haskell.rb @@ -78,10 +78,9 @@ class Haskell < UrlScraper end def get_latest_version(opts) - doc = fetch_doc('https://www.haskell.org/ghc/download.html', opts) - links = doc.css('a').to_a - versions = links.map {|link| link.content.scan(/\A([0-9.]+)\Z/)} - versions.find {|version| !version.empty?}[0][0] + tags = get_github_tags('ghc', 'ghc', opts) + tag = tags.find {|t| t['name'].ends_with?('-release') }['name'] + tag[/ghc-(.*)-release/, 1] end end diff --git a/lib/docs/scrapers/homebrew.rb b/lib/docs/scrapers/homebrew.rb index 0faeaa5785..353ec01d2b 100644 --- a/lib/docs/scrapers/homebrew.rb +++ b/lib/docs/scrapers/homebrew.rb @@ -1,8 +1,10 @@ module Docs class Homebrew < UrlScraper + # include MultipleBaseUrls + self.name = 'Homebrew' self.type = 'simple' - self.release = '4.6.15' + self.release = '6.0.3' self.base_url = 'https://docs.brew.sh/' self.links = { home: 'https://brew.sh', @@ -11,9 +13,29 @@ class Homebrew < UrlScraper html_filters.push 'homebrew/entries', 'homebrew/clean_html' - options[:container] = ->(filter) { filter.root_page? ? '#home' : '#page' } + options[:container] = ->(filter) do + if filter.current_url.path.include?('rubydoc') + '#content' + else + '#default' + end + end - options[:skip_patterns] = [/maintainer/i, /core\-contributor/i, /kickstarter/i, /governance/i] + # Mostly handbook articles with no code. + options[:skip_patterns] = [ + /maintainer/i, + /core\-contributor/i, + /kickstarter/i, + /governance/i, + /responsible/i, + /leadership/i, + /prose/i, + /expense/i, + /creating-a-homebrew-issue/i, + /homebrew-and-java/i, + /checksum_deprecation/i, + /Working-with/i + ] options[:attribution] = <<-HTML © 2009–present Homebrew contributors
    diff --git a/lib/docs/scrapers/htmx.rb b/lib/docs/scrapers/htmx.rb index 719277b2b9..ec1f51e07d 100644 --- a/lib/docs/scrapers/htmx.rb +++ b/lib/docs/scrapers/htmx.rb @@ -7,27 +7,35 @@ class Htmx < UrlScraper home: 'https://htmx.org/', code: 'https://github.com/bigskysoftware/htmx' } - self.release = '1.9.10' - self.base_url = "https://htmx.org/" self.initial_paths = %w(reference/) html_filters.push 'htmx/entries', 'htmx/clean_html' options[:trailing_slash] = true - options[:container] = '.content' + options[:container] = '.content' options[:download_images] = false options[:skip_patterns] = [ - /\Aessays/, - /\Aexamples/, - /\Amigration-guide/, - /\Aposts/, - ] + /\Aessays/, + /\Aexamples/, + /\Amigration-guide/, + /\Aposts/, + ] - # https://github.com/bigskysoftware/htmx/blob/master/LICENSE + # https://github.com/bigskysoftware/htmx/blob/master/LICENSE options[:attribution] = <<-HTML - Licensed under the Zero-Clause BSD License. + Licensed under the Zero-Clause BSD License. HTML + version do + self.release = '2.0.7' + self.base_url = "https://htmx.org/" + end + + version '1' do + self.release = '1.9.12' + self.base_url = "https://v1.htmx.org/" + end + def get_latest_version(opts) get_npm_version('htmx.org', opts) end diff --git a/lib/docs/scrapers/http.rb b/lib/docs/scrapers/http.rb index a2f4a7f9b0..535dc9ec2f 100644 --- a/lib/docs/scrapers/http.rb +++ b/lib/docs/scrapers/http.rb @@ -2,7 +2,7 @@ module Docs class Http < Mdn include MultipleBaseUrls - # release = '2022-11-17' + # release = '2026-06-21' self.name = 'HTTP' self.base_urls = [ 'https://developer.mozilla.org/en-US/docs/Web/HTTP', @@ -18,14 +18,18 @@ class Http < Mdn Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later. HTML - html_filters.push 'http/clean_html', 'http/entries', 'title' + # 'http/clean_html' must run before 'mdn/clean_html', which unwraps all + #

    /

    child anchors and would otherwise discard the datatracker + # section-id anchors before they can be promoted to their heading. + html_filters.insert_before 'mdn/clean_html', 'http/clean_html' + html_filters.push 'http/entries', 'title' options[:root_title] = 'HTTP' options[:title] = ->(filter) do filter.current_url.host == 'datatracker.ietf.org' ? false : filter.default_title end options[:container] = ->(filter) do - filter.current_url.host == 'datatracker.ietf.org' ? '.content' : Docs::Mdn.options[:container] + filter.current_url.host == 'datatracker.ietf.org' ? '.rfcmarkup, .rfchtml' : Docs::Mdn.options[:container] end options[:skip_links] = ->(filter) do filter.current_url.host == 'datatracker.ietf.org' @@ -47,11 +51,14 @@ class Http < Mdn def initial_urls %w(https://developer.mozilla.org/en-US/docs/Web/HTTP https://datatracker.ietf.org/doc/html/rfc4918 + https://datatracker.ietf.org/doc/html/rfc6265 https://datatracker.ietf.org/doc/html/rfc9110 https://datatracker.ietf.org/doc/html/rfc9111 https://datatracker.ietf.org/doc/html/rfc9112 https://datatracker.ietf.org/doc/html/rfc9113 https://datatracker.ietf.org/doc/html/rfc9114 + https://datatracker.ietf.org/doc/html/rfc9651 + https://datatracker.ietf.org/doc/html/rfc9931 https://datatracker.ietf.org/doc/html/rfc5023) end end diff --git a/lib/docs/scrapers/i3.rb b/lib/docs/scrapers/i3.rb index 899a718acf..2f80785c74 100644 --- a/lib/docs/scrapers/i3.rb +++ b/lib/docs/scrapers/i3.rb @@ -3,7 +3,7 @@ class I3 < UrlScraper self.name = 'i3' self.type = 'simple' self.slug = 'i3' - self.release = '4.24' + self.release = '4.25.1' self.base_url = 'https://i3wm.org/docs/userguide.html' self.links = { home: 'https://i3wm.org/', diff --git a/lib/docs/scrapers/jq.rb b/lib/docs/scrapers/jq.rb index 38e29ffecd..882e150ad2 100644 --- a/lib/docs/scrapers/jq.rb +++ b/lib/docs/scrapers/jq.rb @@ -3,12 +3,12 @@ class Jq < UrlScraper self.name = 'jq' self.slug = 'jq' self.type = 'jq' - self.release = '1.7' + self.release = '1.8' self.links = { - home: 'https://jqlang.github.io/jq/' + home: 'https://jqlang.org/' } - self.base_url = "https://jqlang.github.io/jq/manual/v#{self.release}/index.html" + self.base_url = "https://jqlang.org/manual/v#{self.release}/" html_filters.push 'jq/entries', 'jq/clean_html' diff --git a/lib/docs/scrapers/jquery/jquery_core.rb b/lib/docs/scrapers/jquery/jquery_core.rb index 3b761e792e..9af2f32f54 100644 --- a/lib/docs/scrapers/jquery/jquery_core.rb +++ b/lib/docs/scrapers/jquery/jquery_core.rb @@ -1,7 +1,7 @@ module Docs class JqueryCore < Jquery self.name = 'jQuery' - self.release = '3.7.1' + self.release = '4.0.0' self.base_url = 'https://api.jquery.com/' self.initial_paths = %w(/index/index) self.links = { diff --git a/lib/docs/scrapers/julia.rb b/lib/docs/scrapers/julia.rb index dc8d78a984..50f0337a97 100644 --- a/lib/docs/scrapers/julia.rb +++ b/lib/docs/scrapers/julia.rb @@ -7,10 +7,21 @@ class Julia < UrlScraper options[:attribution] = <<-HTML - © 2009–2024 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
    + © 2009–2026 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
    Licensed under the MIT License. HTML + version '1.12' do + self.release = '1.12.6' + 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.11' do self.release = '1.11.2' self.base_url = "https://docs.julialang.org/en/v#{version}/" diff --git a/lib/docs/scrapers/lodash.rb b/lib/docs/scrapers/lodash.rb index bce625e615..d8c9f1f143 100644 --- a/lib/docs/scrapers/lodash.rb +++ b/lib/docs/scrapers/lodash.rb @@ -19,7 +19,7 @@ class Lodash < UrlScraper HTML version '4' do - self.release = '4.17.10' + self.release = '4.18.1' self.base_url = "https://lodash.com/docs/#{release}" end diff --git a/lib/docs/scrapers/love.rb b/lib/docs/scrapers/love.rb index f702da8be9..1979db059a 100644 --- a/lib/docs/scrapers/love.rb +++ b/lib/docs/scrapers/love.rb @@ -43,8 +43,7 @@ class Love < UrlScraper HTML def get_latest_version(opts) - doc = fetch_doc('https://love2d.org/wiki/Version_History', opts) - doc.at_css('#mw-content-text table a').content + get_github_tags('love2d', 'love', opts).first['name'] end end end diff --git a/lib/docs/scrapers/lua.rb b/lib/docs/scrapers/lua.rb index b7989fda83..abfdab98c5 100644 --- a/lib/docs/scrapers/lua.rb +++ b/lib/docs/scrapers/lua.rb @@ -11,10 +11,15 @@ class Lua < UrlScraper options[:skip_links] = true options[:attribution] = <<-HTML - © 1994–2020 Lua.org, PUC-Rio.
    + © 1994–2025 Lua.org, PUC-Rio.
    Licensed under the MIT License. HTML + version '5.5' do + self.release = '5.5.0' + self.base_url = 'https://www.lua.org/manual/5.5/' + end + version '5.4' do self.release = '5.4.1' self.base_url = 'https://www.lua.org/manual/5.4/' diff --git a/lib/docs/scrapers/maplibre_gl.rb b/lib/docs/scrapers/maplibre_gl.rb new file mode 100644 index 0000000000..a36ca6d3d5 --- /dev/null +++ b/lib/docs/scrapers/maplibre_gl.rb @@ -0,0 +1,47 @@ +module Docs + class MaplibreGl < UrlScraper + self.name = 'MapLibre GL JS' + self.slug = 'maplibre_gl' + self.type = 'maplibre_gl' + self.release = '5.24.0' + self.base_url = 'https://maplibre.org/maplibre-gl-js/docs/' + self.root_path = '/' + self.links = { + home: 'https://maplibre.org/maplibre-gl-js/docs/', + code: 'https://github.com/maplibre/maplibre-gl-js' + } + + html_filters.push 'maplibre_gl/clean_html', 'maplibre_gl/entries' + + options[:container] = '.md-content__inner' + + # Only scrape the TypeDoc-generated API reference and the guides. + # The examples (200+ interactive demos) and the embedded style + # specification are excluded as they aren't API reference material. + options[:only_patterns] = [ + %r{\AAPI/classes/}, + %r{\AAPI/functions/}, + %r{\AAPI/type-aliases/}, + %r{\AAPI/interfaces/}, + %r{\AAPI/enumerations/}, + %r{\AAPI/variables/}, + %r{\Aguides/} + ] + + # The site's navigation links point at the `www.` host, which then + # redirects (301) to the bare domain used by `base_url`. Rewrite them so + # they are recognised as internal URLs and get followed. + options[:fix_urls] = ->(url) do + url.sub(%r{\Ahttps://www\.maplibre\.org/}, 'https://maplibre.org/') + end + + options[:attribution] = <<-HTML + © MapLibre contributors
    + Licensed under the 3-Clause BSD License. + HTML + + def get_latest_version(opts) + get_npm_version('maplibre-gl', opts) + end + end +end diff --git a/lib/docs/scrapers/matplotlib.rb b/lib/docs/scrapers/matplotlib.rb index 5e30998c2b..5e2447a31e 100644 --- a/lib/docs/scrapers/matplotlib.rb +++ b/lib/docs/scrapers/matplotlib.rb @@ -16,12 +16,12 @@ class Matplotlib < UrlScraper options[:skip] = %w(api_changes.html tutorial.html faq.html) options[:attribution] = <<-HTML - © 2012–2023 Matplotlib Development Team. All rights reserved.
    + © 2012–2026 Matplotlib Development Team. All rights reserved.
    Licensed under the Matplotlib License Agreement. HTML version do - self.release = '3.9.2' + self.release = '3.10.8' self.base_urls = [ "https://matplotlib.org/stable/api/", "https://matplotlib.org/stable/mpl_toolkits/mplot3d/", @@ -29,6 +29,15 @@ class Matplotlib < UrlScraper ] end + version '3.9' do + self.release = '3.9.3' + self.base_urls = [ + "https://matplotlib.org/#{release}/api/", + "https://matplotlib.org/#{release}/mpl_toolkits/mplot3d/", + "https://matplotlib.org/#{release}/mpl_toolkits/axes_grid/api/" + ] + end + version '3.8' do self.release = '3.8.4' self.base_urls = [ diff --git a/lib/docs/scrapers/mdn/html.rb b/lib/docs/scrapers/mdn/html.rb index 9a1f3ab4e6..319b73cbf6 100644 --- a/lib/docs/scrapers/mdn/html.rb +++ b/lib/docs/scrapers/mdn/html.rb @@ -2,7 +2,7 @@ module Docs class Html < Mdn prepend FixInternalUrlsBehavior - # release = '2025-09-15' + # release = '2026-05-26' self.name = 'HTML' self.base_url = 'https://developer.mozilla.org/en-US/docs/Web/HTML' self.links = { diff --git a/lib/docs/scrapers/mdn/javascript.rb b/lib/docs/scrapers/mdn/javascript.rb index b8d9391151..913da9a418 100644 --- a/lib/docs/scrapers/mdn/javascript.rb +++ b/lib/docs/scrapers/mdn/javascript.rb @@ -3,7 +3,7 @@ class Javascript < Mdn prepend FixInternalUrlsBehavior prepend FixRedirectionsBehavior - # release = '2025-09-15' + # release = '2026-07-08' self.name = 'JavaScript' self.base_url = 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference' self.links = { diff --git a/lib/docs/scrapers/mongoose.rb b/lib/docs/scrapers/mongoose.rb index bcd9673329..1818c78c59 100644 --- a/lib/docs/scrapers/mongoose.rb +++ b/lib/docs/scrapers/mongoose.rb @@ -29,9 +29,7 @@ class Mongoose < UrlScraper HTML def get_latest_version(opts) - doc = fetch_doc('https://mongoosejs.com/docs/', opts) - label = doc.at_css('.pure-menu-link').content.strip - label.sub(/Version /, '') + get_github_tags('Automattic', 'mongoose', opts).find {|t| t['name'].starts_with?(/\d/)}['name'] end end end diff --git a/lib/docs/scrapers/nginx.rb b/lib/docs/scrapers/nginx.rb index e7af592414..d7bdf11131 100644 --- a/lib/docs/scrapers/nginx.rb +++ b/lib/docs/scrapers/nginx.rb @@ -2,7 +2,7 @@ module Docs class Nginx < UrlScraper self.name = 'nginx' self.type = 'nginx' - self.release = '1.29.0' + self.release = '1.31.1' self.base_url = 'https://nginx.org/en/docs/' self.links = { home: 'https://nginx.org/', @@ -23,7 +23,7 @@ class Nginx < UrlScraper # http://nginx.org/LICENSE options[:attribution] = <<-HTML © 2002-2021 Igor Sysoev
    - © 2011-2025 Nginx, Inc.
    + © 2011-2026 Nginx, Inc.
    Licensed under the BSD License. HTML diff --git a/lib/docs/scrapers/nix.rb b/lib/docs/scrapers/nix.rb index b2ee5c8798..31deb9141b 100644 --- a/lib/docs/scrapers/nix.rb +++ b/lib/docs/scrapers/nix.rb @@ -23,7 +23,7 @@ class Nix < UrlScraper def get_latest_version(opts) doc = fetch_doc('https://nixos.org/manual/nix/stable/', opts) - doc.at_css('a.active')['href'].scan(/([0-9.]+)/)[0][0] + doc.at_css('h1.menu-title').content.scan(/([0-9.]+)/).first.first end end end diff --git a/lib/docs/scrapers/node.rb b/lib/docs/scrapers/node.rb index 7331de7d59..82eed214c3 100644 --- a/lib/docs/scrapers/node.rb +++ b/lib/docs/scrapers/node.rb @@ -24,12 +24,12 @@ class Node < UrlScraper HTML version do - self.release = '25.0.0' + self.release = '26.3.1' self.base_url = 'https://nodejs.org/api/' end version '24 LTS' do - self.release = '24.10.0' + self.release = '24.14.0' self.base_url = 'https://nodejs.org/dist/latest-v24.x/docs/api/' end diff --git a/lib/docs/scrapers/numpy.rb b/lib/docs/scrapers/numpy.rb index a679caaae2..61c07cb66b 100644 --- a/lib/docs/scrapers/numpy.rb +++ b/lib/docs/scrapers/numpy.rb @@ -1,7 +1,7 @@ module Docs # Requires downloading the documents to local disk first. # Go to https://numpy.org/doc/, click "HTML+zip" to download - # (example url: https://numpy.org/doc/2.2/numpy-html.zip), + # (example url: https://numpy.org/doc/2.4/numpy-html.zip), # then extract into "docs/numpy~#{version}/" class Numpy < FileScraper self.name = 'NumPy' @@ -30,6 +30,12 @@ class Numpy < FileScraper Licensed under the 3-clause BSD License. HTML + version '2.4' do + self.release = '2.4' + self.base_url = "https://numpy.org/doc/#{self.version}/" + options[:container] = nil + end + version '2.2' do self.release = '2.2' self.base_url = "https://numpy.org/doc/#{self.version}/" diff --git a/lib/docs/scrapers/nushell.rb b/lib/docs/scrapers/nushell.rb index c3e11e2075..6e7ef8adfb 100644 --- a/lib/docs/scrapers/nushell.rb +++ b/lib/docs/scrapers/nushell.rb @@ -6,7 +6,7 @@ class Nushell < UrlScraper self.name = "Nushell" self.slug = "nushell" self.type = "nushell" - self.release = "0.85.0" + self.release = "0.113.1" self.links = { home: "https://www.nushell.sh/", code: "https://github.com/nushell/nushell", @@ -14,11 +14,11 @@ class Nushell < UrlScraper html_filters.push "nushell/clean_html", "nushell/entries", "nushell/fix_links" - options[:container] = '.theme-container' + options[:container] = 'main > [vp-content]' options[:follow_links] = true options[:title] = "Nushell" options[:attribution] = <<-HTML - Copyright © 2019–2023 The Nushell Project Developers + Copyright © 2019–2026 The Nushell Project Developers Licensed under the MIT License. HTML diff --git a/lib/docs/scrapers/ocaml.rb b/lib/docs/scrapers/ocaml.rb index 97a25a9413..10ba166c23 100644 --- a/lib/docs/scrapers/ocaml.rb +++ b/lib/docs/scrapers/ocaml.rb @@ -21,11 +21,11 @@ class Ocaml < UrlScraper ] options[:attribution] = <<-HTML - © 1995-2024 INRIA. + © 1995-2025 INRIA. HTML version '' do - self.release = '5.3' + self.release = '5.4' self.base_url = "https://ocaml.org/manual/#{self.release}/" end diff --git a/lib/docs/scrapers/octave.rb b/lib/docs/scrapers/octave.rb index ee798d58dc..83a9bad8c2 100644 --- a/lib/docs/scrapers/octave.rb +++ b/lib/docs/scrapers/octave.rb @@ -10,6 +10,8 @@ class Octave < UrlScraper html_filters.push 'octave/clean_html', 'octave/entries', 'title' + options[:container] = 'body > div[id], body > .contents' + options[:skip] = %w( Copying.html Preface.html @@ -23,19 +25,24 @@ class Octave < UrlScraper options[:root_title] = 'GNU Octave' options[:attribution] = <<-HTML - © 1996–2025 The Octave Project Developers
    + © 1996–2026 The Octave Project Developers
    Permission is granted to make and distribute verbatim copies of this manual provided the copyright notice and this permission notice are preserved on all copies.
    Permission is granted to copy and distribute modified versions of this manual under the conditions for verbatim copying, provided that the entire resulting derived work is distributed under the terms of a permission notice identical to this one.
    Permission is granted to copy and distribute translations of this manual into another language, under the above conditions for modified versions. HTML + version '11' do + self.release = '11.1.0' + self.base_url = "https://docs.octave.org/v#{self.release}/" + end + version '10' do - self.release = '10.1.0' + self.release = '10.3.0' self.base_url = "https://docs.octave.org/v#{self.release}/" end version '9' do - self.release = '9.2.0' + self.release = '9.4.0' self.base_url = "https://docs.octave.org/v#{self.release}/" end diff --git a/lib/docs/scrapers/odin.rb b/lib/docs/scrapers/odin.rb new file mode 100644 index 0000000000..e54a43d490 --- /dev/null +++ b/lib/docs/scrapers/odin.rb @@ -0,0 +1,62 @@ +module Docs + class Odin < UrlScraper + include MultipleBaseUrls + self.name = 'Odin' + self.slug = 'odin' + self.type = 'odin' + self.release = '2026-07' + self.base_urls = [ + 'https://odin-lang.org/', + 'https://pkg.odin-lang.org/' + ] + + def initial_urls + [ + 'https://odin-lang.org/docs', + 'https://odin-lang.org/spec', + 'https://pkg.odin-lang.org/base', + 'https://pkg.odin-lang.org/core', + 'https://pkg.odin-lang.org/vendor' + ] + end + + self.root_path = 'https://odin-lang.org/' + self.initial_paths = %w(base core vendor) + + self.links = { + home: 'https://odin-lang.org/', + code: 'https://github.com/odin-lang/Odin' + } + + html_filters.push 'odin/entries', 'odin/clean_html' + options[:download_images] = false + + options[:container] = '.odin-main' + + options[:only_patterns] = [ + /docs/, + /spec/, + /base/, + /core/, + /vendor/ + ] + options[:trailing_slash] = false + options[:skip] = %w( + docs/examples + docs/nightly + docs/odin-book + docs/spec + docs/packages + ) + + options[:attribution] = <<-HTML + © 2016-#{Date.today.year} Ginger Bill
    + Licensed under the zlib License. + HTML + + def get_latest_version(opts) + get_latest_github_release('odin-lang', 'Odin', opts)[4..] + end + + end +end diff --git a/lib/docs/scrapers/openlayers.rb b/lib/docs/scrapers/openlayers.rb index b9bf9d347a..3d08fae679 100644 --- a/lib/docs/scrapers/openlayers.rb +++ b/lib/docs/scrapers/openlayers.rb @@ -3,7 +3,7 @@ class Openlayers < UrlScraper self.name = 'OpenLayers' self.type = 'openlayers' self.slug = 'openlayers' - self.release = '10.6.1' + self.release = '10.8.0' self.base_url = "https://openlayers.org/en/latest/apidoc/" self.links = { home: 'https://openlayers.org/', diff --git a/lib/docs/scrapers/opentofu.rb b/lib/docs/scrapers/opentofu.rb new file mode 100644 index 0000000000..52b2757157 --- /dev/null +++ b/lib/docs/scrapers/opentofu.rb @@ -0,0 +1,47 @@ +module Docs + class Opentofu < UrlScraper + self.name = 'OpenTofu' + self.type = 'opentofu' + self.links = { + home: 'https://opentofu.org/', + } + + html_filters.push 'opentofu/entries', 'opentofu/clean_html' + + # Empty spans are used by Prism for code highlighting. + # Don't clean them + options[:clean_text] = false + options[:trailing_slash] = true + # https://github.com/opentofu/opentofu/blob/main/LICENSE + options[:attribution] = <<-HTML + Copyright (c) The OpenTofu Authors
    + Copyright (c) 2014 HashiCorp, Inc.
    + Mozilla Public License, version 2.0 + HTML + + def get_latest_version(opts) + contents = get_latest_github_release('opentofu', 'opentofu', opts) + contents.sub("v", "") + end + + version '1.12' do + self.release = '1.12.0' + self.base_url = "https://opentofu.org/docs/v#{self.version}/" + end + + version '1.11' do + self.release = '1.11.5' + self.base_url = "https://opentofu.org/docs/v#{self.version}/" + end + + version '1.10' do + self.release = '1.10.9' + self.base_url = "https://opentofu.org/docs/v#{self.version}/" + end + + version '1.9' do + self.release = '1.9.4' + self.base_url = "https://opentofu.org/docs/v#{self.version}/" + end + end +end diff --git a/lib/docs/scrapers/pandas.rb b/lib/docs/scrapers/pandas.rb index 872930d31c..a9c521bf97 100644 --- a/lib/docs/scrapers/pandas.rb +++ b/lib/docs/scrapers/pandas.rb @@ -18,6 +18,29 @@ class Pandas < FileScraper Licensed under the 3-clause BSD License. HTML + version '3' do + self.release = '3.0.8' + self.base_url = "https://pandas.pydata.org/pandas-docs/version/#{self.release}/" + + html_filters.push 'pandas/clean_html', 'pandas/entries' + + options[:container] = 'main section' + + options[:skip_patterns] = [ + /development/, + /getting_started/, + /whatsnew/ + ] + + options[:skip] = [ + 'panel.html', + 'pandas.pdf', + 'pandas.zip', + 'ecosystem.html' + ] + + end + version '2' do self.release = '2.3.0' self.base_url = "https://pandas.pydata.org/pandas-docs/version/#{self.release}/" diff --git a/lib/docs/scrapers/phoenix.rb b/lib/docs/scrapers/phoenix.rb index d115ef053e..7e7018c4c6 100644 --- a/lib/docs/scrapers/phoenix.rb +++ b/lib/docs/scrapers/phoenix.rb @@ -1,34 +1,44 @@ module Docs class Phoenix < UrlScraper + include MultipleBaseUrls + self.type = 'elixir' - self.release = '1.6.11' - self.base_url = 'https://hexdocs.pm/' - self.root_path = 'phoenix/Phoenix.html' - self.initial_paths = %w( - phoenix/api-reference.html - ecto/api-reference.html - phoenix_html/api-reference.html - phoenix_live_view/api-reference.html - phoenix_pubsub/api-reference.html - plug/api-reference.html) + self.release = '1.8.8' + self.root_path = 'overview.html' self.links = { home: 'http://www.phoenixframework.org', code: 'https://github.com/phoenixframework/phoenix' } + self.base_urls = %w( + https://phoenix.hexdocs.pm/ + https://ecto.hexdocs.pm/ + https://phoenix-html.hexdocs.pm/ + https://phoenix-live-view.hexdocs.pm/ + https://phoenix-pubsub.hexdocs.pm/ + https://plug.hexdocs.pm/ + ) + + def initial_urls + %w( + https://phoenix.hexdocs.pm/overview.html + https://ecto.hexdocs.pm/Ecto.html + https://phoenix-html.hexdocs.pm/Phoenix.HTML.html + https://phoenix-live-view.hexdocs.pm/welcome.html + https://phoenix-pubsub.hexdocs.pm/Phoenix.PubSub.html + https://plug.hexdocs.pm/readme.html + ) + end + html_filters.push 'elixir/clean_html', 'elixir/entries' options[:container] = '#content' - options[:skip_patterns] = [/extra-api-reference/, /js/, /\d+\.\d+\.\d+/] - options[:only_patterns] = [ - /\Aphoenix\//, - /\Aecto\//, - /\Aphoenix_pubsub\//, - /\Aphoenix_html\//, - /\Aphoenix_live_view\//, - /\Aplug\// - ] + # Filter docs for JS libraries, these use a different HTML layout. + # e.g. https://phoenix.hexdocs.pm/js/ and https://phoenix-live-view.hexdocs.pm/1.2.0/js/ + # + # Only match on `js` directories so we still catch normal pages like https://phoenix-live-view.hexdocs.pm/js-interop.html + options[:skip_patterns] = [%r{(\A|/)js/}] options[:attribution] = -> (filter) { if filter.slug.start_with?('ecto') @@ -56,7 +66,7 @@ class Phoenix < UrlScraper } def get_latest_version(opts) - doc = fetch_doc('https://hexdocs.pm/phoenix/Phoenix.html', opts) + doc = fetch_doc('https://phoenix.hexdocs.pm/api-reference.html', opts) doc.at_css('.sidebar-projectVersion').content.strip[1..-1] end end diff --git a/lib/docs/scrapers/php.rb b/lib/docs/scrapers/php.rb index 5a2f42e24c..898110da2e 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.4' + self.release = '8.5' 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–2024 The PHP Documentation Group
    + © 1997–2025 The PHP Documentation Group
    Licensed under the Creative Commons Attribution License v3.0 or later. HTML diff --git a/lib/docs/scrapers/phpunit.rb b/lib/docs/scrapers/phpunit.rb index 12efbbfcba..ba1dafc5bd 100644 --- a/lib/docs/scrapers/phpunit.rb +++ b/lib/docs/scrapers/phpunit.rb @@ -17,14 +17,23 @@ class Phpunit < UrlScraper options[:title] = false options[:attribution] = <<-HTML - © 2005–2025 Sebastian Bergmann
    + © 2005–2026 Sebastian Bergmann
    Licensed under the Creative Commons Attribution 3.0 Unported License. HTML FILTERS = %w(phpunit/clean_html phpunit/entries title) version do - self.release = '12.0' + self.release = '13.2' + self.base_url = "https://docs.phpunit.de/en/#{release}/" + + html_filters.push FILTERS + + options[:container] = '.document' + end + + version '12' do + self.release = '12.5' self.base_url = "https://docs.phpunit.de/en/#{release}/" html_filters.push FILTERS diff --git a/lib/docs/scrapers/playwright.rb b/lib/docs/scrapers/playwright.rb index 479d7476e4..0694722374 100644 --- a/lib/docs/scrapers/playwright.rb +++ b/lib/docs/scrapers/playwright.rb @@ -2,7 +2,7 @@ module Docs class Playwright < UrlScraper self.name = 'Playwright' self.type = 'simple' - self.release = '1.55.0' + self.release = '1.61.0' self.base_url = 'https://playwright.dev/docs/' self.root_path = 'intro' self.links = { @@ -16,7 +16,7 @@ class Playwright < UrlScraper # https://github.com/microsoft/playwright/blob/main/LICENSE options[:attribution] = <<-HTML - © 2025 Microsoft
    + © 2026 Microsoft
    Licensed under the Apache License, Version 2.0. HTML diff --git a/lib/docs/scrapers/polars.rb b/lib/docs/scrapers/polars.rb new file mode 100644 index 0000000000..3ebcd2557a --- /dev/null +++ b/lib/docs/scrapers/polars.rb @@ -0,0 +1,58 @@ +module Docs + class Polars < UrlScraper + self.name = 'Polars' + self.type = 'sphinx' + self.release = '1.41.0' + self.base_url = 'https://docs.pola.rs/api/python/stable/reference/' + self.root_path = 'index.html' + self.links = { + home: 'https://pola.rs/', + code: 'https://github.com/pola-rs/polars' + } + + html_filters.push 'polars/entries', 'sphinx/clean_html', 'polars/clean_html' + + # pydata-sphinx-theme keeps the page content in the article body. + options[:container] = 'article.bd-article' + + options[:skip_patterns] = [/_changelog/, /whatsnew/] + + # https://github.com/pola-rs/polars/blob/main/LICENSE + options[:attribution] = <<-HTML + © 2020 Ritchie Vink
    + © 2022 Polars contributors
    + Licensed under the MIT License. + HTML + + # Polars tags both Rust (rs-*) and Python (py-*) releases in the same repo. + # The tags API only lists recent Rust ones, but the latest GitHub release is + # always the Python one, so use that and drop the py- prefix. + def get_latest_version(opts) + get_latest_github_release('pola-rs', 'polars', opts).sub(/\Apy-/, '') + end + + private + + def parse(response) + if response.body.include?('class="sig') + doc = Nokogiri::HTML5(response.body) + doc.css('.sig').each do |node| + node.css('.headerlink').remove + node.css('.reference.external').each { |a| a.remove if a.text.strip == '[source]' } + sig = node.text.gsub(/\s+/, ' ').strip + if (m = sig.match(/\A(.+?\()\s*(.+?)\s*(\).*)\z/m)) + head, params, tail = m[1], m[2], m[3] + split_params = params.split(/,\s+/).map { |p| p.sub(/,\z/, '') }.reject(&:empty?) + sig = "#{head}\n #{split_params.join(",\n ")},\n#{tail}" unless split_params.empty? + end + pre = Nokogiri::XML::Node.new('pre', doc) + pre['data-language'] = 'python' + pre.content = sig + node.replace(pre) + end + response.body.replace(doc.to_html) + end + super + end + end +end diff --git a/lib/docs/scrapers/postgresql.rb b/lib/docs/scrapers/postgresql.rb index 38ac56a06e..3d56492cd9 100644 --- a/lib/docs/scrapers/postgresql.rb +++ b/lib/docs/scrapers/postgresql.rb @@ -52,12 +52,12 @@ class Postgresql < UrlScraper /\Aunsupported-features/ ] options[:attribution] = <<-HTML - © 1996–2025 The PostgreSQL Global Development Group
    + © 1996–2026 The PostgreSQL Global Development Group
    Licensed under the PostgreSQL License. HTML version '18' do - self.release = '18.0' + self.release = '18.3' self.base_url = "https://www.postgresql.org/docs/#{version}/" end @@ -70,7 +70,7 @@ class Postgresql < UrlScraper self.release = '16.1' self.base_url = "https://www.postgresql.org/docs/#{version}/" end - + version '15' do self.release = '15.4' self.base_url = "https://www.postgresql.org/docs/#{version}/" diff --git a/lib/docs/scrapers/powershell.rb b/lib/docs/scrapers/powershell.rb new file mode 100644 index 0000000000..046ad405da --- /dev/null +++ b/lib/docs/scrapers/powershell.rb @@ -0,0 +1,86 @@ +module Docs + class Powershell < FileScraper + self.name = 'PowerShell' + self.type = 'simple' + self.root_path = 'Microsoft.PowerShell.Core/Get-Help.html' + self.links = { + home: 'https://learn.microsoft.com/powershell', + code: 'https://github.com/MicrosoftDocs/PowerShell-Docs' + } + html_filters.push 'powershell/clean_html', 'powershell/entries' + text_filters.replace 'attribution', 'powershell/attribution' + + # https://github.com/MicrosoftDocs/PowerShell-Docs/blob/main/LICENSE-CODE.md + # https://github.com/MicrosoftDocs/PowerShell-Docs/blob/main/LICENSE.md + options[:attribution] = <<-HTML + © Microsoft Corporation + Code licensed under an MIT-style License. Documentation licensed under CC BY 4.0. + HTML + + version '7.7' do + self.release = '7.7' + end + + version '7.6' do + self.release = '7.6' + end + + version '7.5' do + self.release = '7.5' + end + + version '7.4' do + self.release = '7.4' + end + + version '5.1' do + self.release = '5.1' + end + + version 'Scripting' do + self.root_path = 'discover-powershell.html' + + def source_directory + @source_directory ||= File.join(Docs::FileScraper::SOURCE_DIRECTORY, 'powershell', 'docs-conceptual') + end + end + + def source_directory + @source_directory ||= File.join(Docs::FileScraper::SOURCE_DIRECTORY, 'powershell', self.class.version) + end + + def get_latest_version(opts) + # Stable PowerShell releases are tagged like "v7.5.2"; the docs are + # versioned by major.minor (e.g. "7.5"), so drop the patch component. + get_latest_github_release('PowerShell', 'PowerShell', opts).split('.')[0..1].join('.') + end + + # No index page, enumerate all HTML files + def request_all(urls) + assert_source_directory_exists + Dir.glob(File.join(source_directory, '**', '*.md')).sort.each do |path| + url = File.join(base_url.to_s, path.sub("#{source_directory}/", '')) + yield request_one(url) + end + end + + private + + def parse(response) + body = response.body.sub(/\A---\s*\n.*?\n---\s*\n/m, '') + html = markdown_renderer.render(body) + [Parser.new("#{html}").html, ""] + end + + def markdown_renderer + require 'redcarpet' + @markdown_renderer ||= Redcarpet::Markdown.new( + Redcarpet::Render::HTML.new(with_toc_data: true), + autolink: true, + fenced_code_blocks: true, + no_intra_emphasis: true, + tables: true + ) + end + end +end diff --git a/lib/docs/scrapers/prettier.rb b/lib/docs/scrapers/prettier.rb index 888a042582..80bf39022c 100644 --- a/lib/docs/scrapers/prettier.rb +++ b/lib/docs/scrapers/prettier.rb @@ -2,7 +2,7 @@ module Docs class Prettier < UrlScraper self.name = 'Prettier' self.type = 'simple' - self.release = '3.6.2' + self.release = '3.8.2' self.base_url = 'https://prettier.io/docs/' self.links = { home: 'https://prettier.io/', diff --git a/lib/docs/scrapers/python.rb b/lib/docs/scrapers/python.rb index e9edbec572..a0bd1ee480 100644 --- a/lib/docs/scrapers/python.rb +++ b/lib/docs/scrapers/python.rb @@ -15,6 +15,7 @@ class Python < UrlScraper options[:skip_patterns] = [/whatsnew/] options[:skip] = %w( + archives/ library/2to3.html library/formatter.html library/intro.html @@ -23,12 +24,12 @@ class Python < UrlScraper library/sunau.html) options[:attribution] = <<-HTML - © 2001–2025 Python Software Foundation
    + © 2001 Python Software Foundation
    Licensed under the PSF License. HTML version '3.14' do - self.release = '3.14.0' + self.release = '3.14.6' self.base_url = "https://docs.python.org/#{self.version}/" html_filters.push 'python/entries_v3', 'sphinx/clean_html', 'python/clean_html' diff --git a/lib/docs/scrapers/pytorch.rb b/lib/docs/scrapers/pytorch.rb index cfa1d51010..e45ac3b096 100644 --- a/lib/docs/scrapers/pytorch.rb +++ b/lib/docs/scrapers/pytorch.rb @@ -19,6 +19,16 @@ class Pytorch < UrlScraper PyTorch has a BSD-style license, as found in the LICENSE file. HTML + version '2.9' do + self.release = '2.9' + self.base_url = "https://docs.pytorch.org/docs/#{release}/" + end + + version '2.8' do + self.release = '2.8' + self.base_url = "https://docs.pytorch.org/docs/#{release}/" + end + version '2.7' do self.release = '2.7' self.base_url = "https://docs.pytorch.org/docs/#{release}/" diff --git a/lib/docs/scrapers/qunit.rb b/lib/docs/scrapers/qunit.rb index 125605e373..f1eed67582 100644 --- a/lib/docs/scrapers/qunit.rb +++ b/lib/docs/scrapers/qunit.rb @@ -4,7 +4,7 @@ module Docs class Qunit < UrlScraper self.name = 'QUnit' self.type = 'qunit' - self.release = '2.24.1' + self.release = '2.25.0' self.base_url = 'https://qunitjs.com/api/' self.root_path = '/' self.links = { diff --git a/lib/docs/scrapers/rabbit_mq.rb b/lib/docs/scrapers/rabbit_mq.rb new file mode 100644 index 0000000000..9ce23eca3b --- /dev/null +++ b/lib/docs/scrapers/rabbit_mq.rb @@ -0,0 +1,29 @@ +module Docs + class RabbitMq < UrlScraper + self.name = 'RabbitMQ' + self.type = 'rabbit_mq' + self.links = { + home: 'https://www.rabbitmq.com/', + code: 'https://github.com/rabbitmq/rabbitmq-server' + } + + html_filters.push 'rabbit_mq/entries', 'rabbit_mq/clean_html' + + options[:root_path] = '/docs' + options[:skip_patterns] = [/\/next|\/\d\.\d*/, /rabbitmq\//] + options[:attribution] = <<-HTML + Copyright © 2005-2026 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.
    + Licensed under the Apache License, Version 2.0. + HTML + + version do + self.base_url = 'https://www.rabbitmq.com/docs' + self.release = '4.3.2' + end + + def get_latest_version(opts) + tags = get_github_tags("rabbitmq", "rabbitmq-server", opts) + tags[0]['name'][1..-1] + end + end +end diff --git a/lib/docs/scrapers/rdoc/minitest.rb b/lib/docs/scrapers/rdoc/minitest.rb index a502343590..7385fb4006 100644 --- a/lib/docs/scrapers/rdoc/minitest.rb +++ b/lib/docs/scrapers/rdoc/minitest.rb @@ -3,11 +3,11 @@ class Minitest < Rdoc self.name = 'Ruby / Minitest' self.slug = 'minitest' - self.release = '5.25.4' + self.release = '6.0.1' self.links = { + home: 'https://minite.st', code: 'https://github.com/minitest/minitest' } - self.base_url = 'https://docs.seattlerb.org/minitest/' html_filters.replace 'rdoc/entries', 'minitest/entries' diff --git a/lib/docs/scrapers/rdoc/rails.rb b/lib/docs/scrapers/rdoc/rails.rb index 03aad4a356..a78cbd5042 100644 --- a/lib/docs/scrapers/rdoc/rails.rb +++ b/lib/docs/scrapers/rdoc/rails.rb @@ -75,6 +75,10 @@ class Rails < Rdoc end end + version '8.1' do + self.release = '8.1.2' + end + version '8.0' do self.release = '8.0.1' end diff --git a/lib/docs/scrapers/rdoc/ruby.rb b/lib/docs/scrapers/rdoc/ruby.rb index baba8359a1..7f7f9ac612 100644 --- a/lib/docs/scrapers/rdoc/ruby.rb +++ b/lib/docs/scrapers/rdoc/ruby.rb @@ -27,7 +27,6 @@ class Ruby < Rdoc syntax_rdoc.html extension_rdoc.html extension_ja_rdoc.html - Data.html English.html Fcntl.html Kconv.html @@ -69,6 +68,11 @@ class Ruby < Rdoc Licensed under their own licenses. HTML + version '4.0' do + self.release = '4.0.1' + self.root_path = 'index.html' + end + version '3.4' do self.release = '3.4.7' end diff --git a/lib/docs/scrapers/redis.rb b/lib/docs/scrapers/redis.rb index 9c8e03b19b..9c7cc12333 100644 --- a/lib/docs/scrapers/redis.rb +++ b/lib/docs/scrapers/redis.rb @@ -21,8 +21,7 @@ class Redis < UrlScraper HTML def get_latest_version(opts) - body = fetch('http://download.redis.io/redis-stable/00-RELEASENOTES', opts) - body.scan(/Redis Community Edition ([0-9.]+)/)[0][0] + get_latest_github_release('redis', 'redis', opts) end private diff --git a/lib/docs/scrapers/requests.rb b/lib/docs/scrapers/requests.rb index d031e3752a..41107d3ddf 100644 --- a/lib/docs/scrapers/requests.rb +++ b/lib/docs/scrapers/requests.rb @@ -6,7 +6,7 @@ class Requests < UrlScraper home: 'https://requests.readthedocs.io/', code: 'https://github.com/psf/requests' } - self.release = '2.28.1' + self.release = '2.32.5' self.base_url = "https://requests.readthedocs.io/en/latest/api/" html_filters.push 'requests/entries', 'sphinx/clean_html' @@ -14,7 +14,7 @@ class Requests < UrlScraper options[:container] = '.body > section' options[:attribution] = <<-HTML - © 2011-2022 Kenneth Reitz and other contributors
    + © 2011-2025 Kenneth Reitz and other contributors
    Licensed under the Apache license. HTML diff --git a/lib/docs/scrapers/rust.rb b/lib/docs/scrapers/rust.rb index 2ad145d372..3258a7a2de 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.90.0' + self.release = '1.97.0' self.base_url = 'https://doc.rust-lang.org/' self.root_path = 'book/index.html' self.initial_paths = %w( diff --git a/lib/docs/scrapers/sqlite.rb b/lib/docs/scrapers/sqlite.rb index c7a4cd36df..07d3daf33f 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.50.0' + self.release = '3.53.0' self.base_url = 'https://sqlite.org/' self.root_path = 'docs.html' self.initial_paths = %w(keyword_index.html) diff --git a/lib/docs/scrapers/support_tables.rb b/lib/docs/scrapers/support_tables.rb index 09e7eaf9e4..fc18c11cce 100644 --- a/lib/docs/scrapers/support_tables.rb +++ b/lib/docs/scrapers/support_tables.rb @@ -7,9 +7,12 @@ class SupportTables < Scraper self.name = 'Support Tables' self.slug = 'browser_support_tables' self.type = 'support_tables' - self.release = '1.0.30001751' + self.release = '1.0.30001805' self.base_url = 'https://github.com/Fyrd/caniuse/raw/main/' + # Clone of https://github.com/Fyrd/caniuse into docs/caniuse + CANIUSE_DIRECTORY = 'docs/caniuse' + # https://github.com/Fyrd/caniuse/blob/main/LICENSE options[:attribution] = <<-HTML © 2020 Alexis Deveria
    @@ -17,13 +20,16 @@ class SupportTables < Scraper HTML def build_pages - url = 'https://github.com/Fyrd/caniuse/raw/main/data.json' - instrument 'running.scraper', urls: [url] + if Dir.exist?(CANIUSE_DIRECTORY) + system('git', '-C', CANIUSE_DIRECTORY, 'pull') + else + system('git', 'clone', '--depth', '32', 'https://github.com/Fyrd/caniuse', CANIUSE_DIRECTORY) + end - response = Request.run(url) - instrument 'process_response.scraper', response: response + path = File.join(CANIUSE_DIRECTORY, 'data.json') + instrument 'running.scraper', urls: [path] - data = JSON.parse(response.body) + data = JSON.parse(File.read(path)) instrument 'queued.scraper', urls: data['data'].keys data['agents']['and_chr']['browser'] = 'Android Chrome' @@ -43,12 +49,9 @@ def build_pages yield index_page data['data'].each do |feature_id, feature| - url = "https://github.com/Fyrd/caniuse/raw/main/features-json/#{feature_id}.json" - - response = Request.run(url) - instrument 'process_response.scraper', response: response + path = File.join(CANIUSE_DIRECTORY, 'features-json', "#{feature_id}.json") - feature = JSON.parse(response.body) + feature = JSON.parse(File.read(path)) name = feature['title'] type = feature['categories'].find { |category| name.include?(category) } || feature['categories'].first diff --git a/lib/docs/scrapers/tensorflow/tensorflow.rb b/lib/docs/scrapers/tensorflow/tensorflow.rb index 770d35b87c..a54e2ac014 100644 --- a/lib/docs/scrapers/tensorflow/tensorflow.rb +++ b/lib/docs/scrapers/tensorflow/tensorflow.rb @@ -45,8 +45,8 @@ class Tensorflow < UrlScraper end def get_latest_version(opts) - doc = fetch_doc(self.base_url, opts) - doc.title[/TensorFlow v([.\d]+)/, 1] + tag = get_github_tags('tensorflow', 'tensorflow', opts).find { |t| !t['name'].include?('-rc') } + tag['name'][1..-1] end private diff --git a/lib/docs/scrapers/threejs.rb b/lib/docs/scrapers/threejs.rb index c3ef9bc25d..7fd3147359 100644 --- a/lib/docs/scrapers/threejs.rb +++ b/lib/docs/scrapers/threejs.rb @@ -44,17 +44,17 @@ class Threejs < UrlScraper self.base_url = "https://threejs.org/docs" def get_latest_version(opts) - get_latest_github_release('mrdoob', 'three.js', opts)[1..] + get_github_tags('mrdoob', 'three.js', opts).first['name'][1..] end def initial_paths paths = [] - url = 'https://threejs.org/docs/list.json' + url = 'https://threejs.org/docs/search.json' response = Request.run(url) json_data = JSON.parse(response.body) # Process both API and manual sections - process_documentation(json_data['en'], paths) + process_documentation(json_data, paths) paths end @@ -78,4 +78,4 @@ def process_documentation(data, paths, prefix = '') end end end -end \ No newline at end of file +end diff --git a/lib/docs/scrapers/tokio.rb b/lib/docs/scrapers/tokio.rb new file mode 100644 index 0000000000..6cf56d019e --- /dev/null +++ b/lib/docs/scrapers/tokio.rb @@ -0,0 +1,31 @@ +module Docs + class Tokio < UrlScraper + self.name = 'Tokio' + self.type = 'tokio' + self.root_path = 'index.html' + self.links = { + home: 'https://tokio.rs/', + code: 'https://github.com/tokio-rs/tokio' + } + + html_filters.push 'tokio/entries', 'tokio/clean_html' + + options[:rate_limit] = 50 + options[:container] = 'section.content' + options[:skip_patterns] = [/\/next|\/\d\.\d*/, /rabbitmq\//] + options[:attribution] = <<-HTML + MIT License
    + Copyright © Tokio Contributors + HTML + + version do + self.release = '1.53.1' + self.base_url = "https://docs.rs/tokio/#{self.release}/tokio/" + end + + def get_latest_version(opts) + doc = fetch_doc('https://docs.rs/tokio/latest/tokio/index.html', opts) + doc.at_css('h2 > .version').content.strip + end + end +end diff --git a/lib/docs/scrapers/typescript.rb b/lib/docs/scrapers/typescript.rb index 246d77c9d3..06acbe95c2 100644 --- a/lib/docs/scrapers/typescript.rb +++ b/lib/docs/scrapers/typescript.rb @@ -29,12 +29,12 @@ class Typescript < UrlScraper ] options[:attribution] = <<-HTML - © 2012-2025 Microsoft
    + © 2012-2026 Microsoft
    Licensed under the Apache License, Version 2.0. HTML version do - self.release = '5.9.2' + self.release = '6.0.3' self.base_url = 'https://www.typescriptlang.org/' end diff --git a/lib/docs/scrapers/vite.rb b/lib/docs/scrapers/vite.rb index 1a55691376..70b322f549 100644 --- a/lib/docs/scrapers/vite.rb +++ b/lib/docs/scrapers/vite.rb @@ -11,7 +11,7 @@ class Vite < UrlScraper options[:root_title] = 'Vite' options[:attribution] = <<-HTML - © 2019–present, Yuxi (Evan) You and Vite contributors
    + © 2019-present, VoidZero Inc. and Vite contributors
    Licensed under the MIT License. HTML @@ -22,10 +22,15 @@ class Vite < UrlScraper html_filters.push 'vite/entries', 'vite/clean_html' version do - self.release = '7.1.2' + self.release = '8.0.0' self.base_url = 'https://vite.dev/' end + version '7' do + self.release = '7.3.1' + self.base_url = 'https://v7.vite.dev/' + end + version '6' do self.release = '6.3.5' self.base_url = 'https://v6.vite.dev/' diff --git a/lib/docs/scrapers/vitest.rb b/lib/docs/scrapers/vitest.rb index ab7a3d1bb3..5855de21fb 100644 --- a/lib/docs/scrapers/vitest.rb +++ b/lib/docs/scrapers/vitest.rb @@ -13,19 +13,23 @@ class Vitest < UrlScraper options[:skip] = %w(blog) options[:attribution] = <<-HTML - © 2021-Present Anthony Fu
    - © 2021-Present Matias Capeletto
    + © 2021-Present VoidZero Inc. and Vitest contributors
    Licensed under the MIT License. HTML self.initial_paths = %w(guide/) html_filters.push 'vitest/entries', 'vite/clean_html' - + version do - self.release = '3.0.8' + self.release = '4.1.0' self.base_url = 'https://vitest.dev/' end - + + version '3' do + self.release = '3.2.4' + self.base_url = 'https://vitest.dev/' + end + version '2' do self.release = '2.1.9' self.base_url = 'https://v2.vitest.dev/' diff --git a/lib/docs/scrapers/vue.rb b/lib/docs/scrapers/vue.rb index 158a546b19..79515edcdf 100644 --- a/lib/docs/scrapers/vue.rb +++ b/lib/docs/scrapers/vue.rb @@ -14,12 +14,12 @@ class Vue < UrlScraper options[:replace_paths] = { 'guide/' => 'guide/index.html' } options[:attribution] = <<-HTML - © 2013–present Yuxi Evan You
    + © 2018-present, Yuxi (Evan) You and Vue contributors
    Licensed under the MIT License. HTML version '3' do - self.release = '3.5.18' + self.release = '3.5.38' self.base_url = 'https://vuejs.org/' self.initial_paths = %w(guide/introduction.html) html_filters.push 'vue/entries_v3', 'vue/clean_html' diff --git a/lib/docs/scrapers/vue_router.rb b/lib/docs/scrapers/vue_router.rb index 3456fd59db..7e4ee6ed1e 100644 --- a/lib/docs/scrapers/vue_router.rb +++ b/lib/docs/scrapers/vue_router.rb @@ -16,10 +16,15 @@ class VueRouter < UrlScraper ] options[:attribution] = <<-HTML - © 2013–present Evan You
    + © 2014-present Evan You, Eduardo San Martin Morote
    Licensed under the MIT License. HTML + version '5' do + self.release = '5.0.2' + self.base_url = 'https://router.vuejs.org/' + end + version '4' do self.release = '4.0.12' self.base_url = 'https://next.router.vuejs.org/' diff --git a/lib/docs/scrapers/vueuse.rb b/lib/docs/scrapers/vueuse.rb index 31e1a8fb75..7ba0e10c5b 100644 --- a/lib/docs/scrapers/vueuse.rb +++ b/lib/docs/scrapers/vueuse.rb @@ -22,7 +22,7 @@ class Vueuse < UrlScraper Licensed under the MIT License. HTML - self.release = '13.6.0' + self.release = '14.2.1' self.base_url = 'https://vueuse.org/' self.initial_paths = %w(functions.html) html_filters.push 'vueuse/entries', 'vite/clean_html', 'vueuse/clean_html' diff --git a/lib/docs/scrapers/yarn.rb b/lib/docs/scrapers/yarn.rb index 6539c7d5ee..5eec6a7049 100644 --- a/lib/docs/scrapers/yarn.rb +++ b/lib/docs/scrapers/yarn.rb @@ -13,7 +13,7 @@ class Yarn < UrlScraper HTML version do - self.release = '4.5.1' + self.release = '4.12.0' self.base_url = 'https://yarnpkg.com/' self.links = { home: 'https://yarnpkg.com/', diff --git a/lib/docs/scrapers/zig.rb b/lib/docs/scrapers/zig.rb index 2f31658e4e..4a36509ccd 100644 --- a/lib/docs/scrapers/zig.rb +++ b/lib/docs/scrapers/zig.rb @@ -2,7 +2,7 @@ module Docs class Zig < UrlScraper self.name = 'Zig' self.type = 'simple' - self.release = '0.15.2' + self.release = '0.16.0' self.base_url = "https://ziglang.org/documentation/#{self.release}/" self.links = { home: 'https://ziglang.org/', @@ -13,7 +13,7 @@ class Zig < UrlScraper options[:follow_links] = false options[:attribution] = <<-HTML - © 2015–2025, Zig contributors
    + © 2015–2026, Zig contributors
    Licensed under the MIT License. HTML diff --git a/lib/tasks/docs.thor b/lib/tasks/docs.thor index 8dc32d85a9..bf668a3aa0 100644 --- a/lib/tasks/docs.thor +++ b/lib/tasks/docs.thor @@ -122,6 +122,7 @@ class DocsCLI < Thor option :default, type: :boolean option :installed, type: :boolean option :all, type: :boolean + option :rclone, type: :boolean def download(*names) require 'unix_utils' docs = if options[:default] @@ -223,7 +224,7 @@ class DocsCLI < Thor option :amend, type: :boolean def commit(name) doc = Docs.find(name, false) - message = options[:message] || "Update #{doc.name} documentation (#{doc.versions.first.release})" + message = options[:message] || "Update #{doc.name} documentation (#{doc.versions.first.release})".delete_suffix(" ()") amend = " --amend" if options[:amend] system("git add assets/ *#{name}*") && system("git commit -m '#{message}'#{amend}") rescue Docs::DocNotFound => error @@ -235,6 +236,7 @@ class DocsCLI < Thor puts 'Docs -- BEGIN' require 'open-uri' + require 'net/http' require 'thread' docs = Docs.all_versions @@ -247,18 +249,36 @@ class DocsCLI < Thor dir = File.join(Docs.store_path, doc.path) FileUtils.mkpath(dir) - ['index.json', 'meta.json'].each do |filename| + # Only meta.json is needed to build the manifest; clients (and the + # service worker precache) load index.json directly from the CDN, + # which serves it with CORS headers. + ['meta.json'].each do |filename| json = "https://documents.devdocs.io/#{doc.path}/#{filename}?#{time}" begin - URI.open(json, "Accept-Encoding" => "identity") do |file| - mutex.synchronize do - path = File.join(dir, filename) - File.write(path, file.read) + attempts = 0 + + begin + attempts += 1 + + URI.open(json, "Accept-Encoding" => "identity") do |file| + mutex.synchronize do + path = File.join(dir, filename) + File.write(path, file.read) + end end + rescue Net::OpenTimeout, Net::ReadTimeout => e + if attempts <= 3 + wait_seconds = 2**(attempts - 1) + puts "Docs -- Retrying #{json} in #{wait_seconds}s (#{e.class}: #{e.message})" + sleep(wait_seconds) + retry + end + + raise end rescue => e - puts "Docs -- Failed to download #{json}!" - throw e + puts "Docs -- Failed to download #{json} after #{attempts} attempts!" + raise end end @@ -347,18 +367,33 @@ class DocsCLI < Thor def download_doc(doc) target_path = File.join(Docs.store_path, doc.path) - URI.open "https://downloads.devdocs.io/#{doc.path}.tar.gz" do |file| - FileUtils.mkpath(target_path) - file.close - tar = UnixUtils.gunzip(file.path) - dir = UnixUtils.untar(tar) - FileUtils.rm(tar) - FileUtils.rm_rf(target_path) - FileUtils.mv(dir, target_path) - FileUtils.rm(file.path) + + if options[:rclone] + require 'tmpdir' + Dir.mktmpdir do |dir| + system("rclone copy devdocs:devdocs-downloads/#{doc.path}.tar.gz #{dir}") + tar_gz_path = File.join(dir, "#{File.basename(doc.path)}.tar.gz") + raise "rclone did not download #{doc.path}.tar.gz (not found on remote?)" unless File.exist?(tar_gz_path) + extract_doc(tar_gz_path, target_path) + end + else + URI.open "https://downloads.devdocs.io/#{doc.path}.tar.gz" do |file| + file.close + extract_doc(file.path, target_path) + FileUtils.rm(file.path) + end end end + def extract_doc(tar_gz_path, target_path) + FileUtils.mkpath(target_path) + tar = UnixUtils.gunzip(tar_gz_path) + dir = UnixUtils.untar(tar) + FileUtils.rm(tar) + FileUtils.rm_rf(target_path) + FileUtils.mv(dir, target_path) + end + def package_doc(doc) path = File.join Docs.store_path, doc.path diff --git a/lib/tasks/sprites.thor b/lib/tasks/sprites.thor index fb1b39aa5d..54df982f07 100644 --- a/lib/tasks/sprites.thor +++ b/lib/tasks/sprites.thor @@ -56,6 +56,9 @@ class SpritesCLI < Thor save_manifest(items, icons_per_row, 'assets/images/sprites/docs.json') + # SCSS wrapped in ERB templates must be compiled manually. + compile_scss_erb + if options[:remove_public_icons] logger.info('Removing public/icons') FileUtils.rm_rf('public/icons') @@ -213,6 +216,18 @@ class SpritesCLI < Thor "assets/images/sprites/docs#{size == 32 ? '@2x' : ''}.png" end + def compile_scss_erb + scss_erb_files = Dir['assets/stylesheets/**/*.scss.erb'] + + scss_erb_files.each do |erb_path| + scss_path = erb_path.gsub('.erb', '') + File.open(scss_path, 'w') do |f| + f.write(ERB.new(File.open(erb_path).read).result) + logger.info("Compiling #{erb_path} to #{scss_path}") + end + end + end + def image_optim @image_optim ||= ImageOptim.new( :config_paths => [], diff --git a/lib/tasks/test.thor b/lib/tasks/test.thor index e68fec8f26..f001fe7b8c 100644 --- a/lib/tasks/test.thor +++ b/lib/tasks/test.thor @@ -26,4 +26,11 @@ class TestCLI < Thor def app require 'app_test' end + + desc 'coverage', 'Coverage report for core tests' + def coverage + ENV['COVERAGE'] = 'true' + require_relative '../../test/test_helper' + self.all + end end diff --git a/public/icons/docs/celery/16.png b/public/icons/docs/celery/16.png new file mode 100644 index 0000000000..3d582e88bd Binary files /dev/null and b/public/icons/docs/celery/16.png differ diff --git a/public/icons/docs/celery/16@2x.png b/public/icons/docs/celery/16@2x.png new file mode 100644 index 0000000000..fda9845140 Binary files /dev/null and b/public/icons/docs/celery/16@2x.png differ diff --git a/public/icons/docs/celery/SOURCE b/public/icons/docs/celery/SOURCE new file mode 100644 index 0000000000..2f071f4820 --- /dev/null +++ b/public/icons/docs/celery/SOURCE @@ -0,0 +1 @@ +https://docs.celeryq.dev/en/stable/_static/favicon.ico diff --git a/public/icons/docs/coldfusion/16.png b/public/icons/docs/coldfusion/16.png new file mode 100644 index 0000000000..00ccc8f85e Binary files /dev/null and b/public/icons/docs/coldfusion/16.png differ diff --git a/public/icons/docs/coldfusion/16@2x.png b/public/icons/docs/coldfusion/16@2x.png new file mode 100644 index 0000000000..888db9e321 Binary files /dev/null and b/public/icons/docs/coldfusion/16@2x.png differ diff --git a/public/icons/docs/coldfusion/SOURCE b/public/icons/docs/coldfusion/SOURCE new file mode 100644 index 0000000000..32de636dcb --- /dev/null +++ b/public/icons/docs/coldfusion/SOURCE @@ -0,0 +1 @@ +https://cfdocs.org/apple-touch-icon.png diff --git a/public/icons/docs/couchdb/16.png b/public/icons/docs/couchdb/16.png new file mode 100644 index 0000000000..7cae0c8b51 Binary files /dev/null and b/public/icons/docs/couchdb/16.png differ diff --git a/public/icons/docs/couchdb/16@2x.png b/public/icons/docs/couchdb/16@2x.png new file mode 100644 index 0000000000..81dc55b202 Binary files /dev/null and b/public/icons/docs/couchdb/16@2x.png differ diff --git a/public/icons/docs/couchdb/SOURCE b/public/icons/docs/couchdb/SOURCE new file mode 100644 index 0000000000..86560405ed --- /dev/null +++ b/public/icons/docs/couchdb/SOURCE @@ -0,0 +1 @@ +https://docs.couchdb.org/en/stable/_static/favicon.ico diff --git a/public/icons/docs/cyclejs/16.png b/public/icons/docs/cyclejs/16.png new file mode 100644 index 0000000000..b870a7a678 Binary files /dev/null and b/public/icons/docs/cyclejs/16.png differ diff --git a/public/icons/docs/cyclejs/16@2x.png b/public/icons/docs/cyclejs/16@2x.png new file mode 100644 index 0000000000..7e8be668a4 Binary files /dev/null and b/public/icons/docs/cyclejs/16@2x.png differ diff --git a/public/icons/docs/cyclejs/SOURCE b/public/icons/docs/cyclejs/SOURCE new file mode 100644 index 0000000000..eb89497c99 --- /dev/null +++ b/public/icons/docs/cyclejs/SOURCE @@ -0,0 +1 @@ +https://cycle.js.org/img/cyclejs_logo.svg diff --git a/public/icons/docs/maplibre_gl/16.png b/public/icons/docs/maplibre_gl/16.png new file mode 100644 index 0000000000..00c1db8283 Binary files /dev/null and b/public/icons/docs/maplibre_gl/16.png differ diff --git a/public/icons/docs/maplibre_gl/16@2x.png b/public/icons/docs/maplibre_gl/16@2x.png new file mode 100644 index 0000000000..49f4b9e988 Binary files /dev/null and b/public/icons/docs/maplibre_gl/16@2x.png differ diff --git a/public/icons/docs/maplibre_gl/SOURCE b/public/icons/docs/maplibre_gl/SOURCE new file mode 100644 index 0000000000..ddd48e6326 --- /dev/null +++ b/public/icons/docs/maplibre_gl/SOURCE @@ -0,0 +1 @@ +SOURCE: https://raw.githubusercontent.com/maplibre/maplibre-gl-js/main/docs/assets/logo.svg diff --git a/public/icons/docs/minitest/16.png b/public/icons/docs/minitest/16.png new file mode 100644 index 0000000000..dd04a16cc9 Binary files /dev/null and b/public/icons/docs/minitest/16.png differ diff --git a/public/icons/docs/minitest/16@2x.png b/public/icons/docs/minitest/16@2x.png new file mode 100644 index 0000000000..2992e604c1 Binary files /dev/null and b/public/icons/docs/minitest/16@2x.png differ diff --git a/public/icons/docs/minitest/SOURCE b/public/icons/docs/minitest/SOURCE new file mode 100644 index 0000000000..6c36bfe062 --- /dev/null +++ b/public/icons/docs/minitest/SOURCE @@ -0,0 +1,2 @@ +https://minite.st/favicon-16x16.png +https://minite.st/favicon-32x32.png diff --git a/public/icons/docs/odin/16.png b/public/icons/docs/odin/16.png new file mode 100644 index 0000000000..038d7d801b Binary files /dev/null and b/public/icons/docs/odin/16.png differ diff --git a/public/icons/docs/odin/16@2x.png b/public/icons/docs/odin/16@2x.png new file mode 100644 index 0000000000..d4a3e83320 Binary files /dev/null and b/public/icons/docs/odin/16@2x.png differ diff --git a/public/icons/docs/odin/SOURCE b/public/icons/docs/odin/SOURCE new file mode 100644 index 0000000000..167c103d26 --- /dev/null +++ b/public/icons/docs/odin/SOURCE @@ -0,0 +1 @@ +https://github.com/odin-lang/odin-lang.org/blob/master/static/favicon.svg \ No newline at end of file diff --git a/public/icons/docs/odin_packages/16.png b/public/icons/docs/odin_packages/16.png new file mode 100644 index 0000000000..038d7d801b Binary files /dev/null and b/public/icons/docs/odin_packages/16.png differ diff --git a/public/icons/docs/odin_packages/16@2x.png b/public/icons/docs/odin_packages/16@2x.png new file mode 100644 index 0000000000..d4a3e83320 Binary files /dev/null and b/public/icons/docs/odin_packages/16@2x.png differ diff --git a/public/icons/docs/odin_packages/SOURCE b/public/icons/docs/odin_packages/SOURCE new file mode 100644 index 0000000000..167c103d26 --- /dev/null +++ b/public/icons/docs/odin_packages/SOURCE @@ -0,0 +1 @@ +https://github.com/odin-lang/odin-lang.org/blob/master/static/favicon.svg \ No newline at end of file diff --git a/public/icons/docs/opentofu/16.png b/public/icons/docs/opentofu/16.png new file mode 100644 index 0000000000..2c18bbedef Binary files /dev/null and b/public/icons/docs/opentofu/16.png differ diff --git a/public/icons/docs/opentofu/16@2x.png b/public/icons/docs/opentofu/16@2x.png new file mode 100644 index 0000000000..073cc5053a Binary files /dev/null and b/public/icons/docs/opentofu/16@2x.png differ diff --git a/public/icons/docs/opentofu/SOURCE b/public/icons/docs/opentofu/SOURCE new file mode 100644 index 0000000000..893b013fe2 --- /dev/null +++ b/public/icons/docs/opentofu/SOURCE @@ -0,0 +1,2 @@ +https://github.com/opentofu/opentofu.org/blob/main/static/favicons/favicon-16x16.png +https://github.com/opentofu/opentofu.org/blob/main/static/favicons/favicon-32x32.png diff --git a/public/icons/docs/polars/16.png b/public/icons/docs/polars/16.png new file mode 100644 index 0000000000..c005950aa8 Binary files /dev/null and b/public/icons/docs/polars/16.png differ diff --git a/public/icons/docs/polars/16@2x.png b/public/icons/docs/polars/16@2x.png new file mode 100644 index 0000000000..940e5427f8 Binary files /dev/null and b/public/icons/docs/polars/16@2x.png differ diff --git a/public/icons/docs/polars/SOURCE b/public/icons/docs/polars/SOURCE new file mode 100644 index 0000000000..c63c050877 --- /dev/null +++ b/public/icons/docs/polars/SOURCE @@ -0,0 +1 @@ +https://raw.githubusercontent.com/pola-rs/polars-static/master/icons/favicon-32x32.png diff --git a/public/icons/docs/powershell/16.png b/public/icons/docs/powershell/16.png new file mode 100644 index 0000000000..730b58a302 Binary files /dev/null and b/public/icons/docs/powershell/16.png differ diff --git a/public/icons/docs/powershell/16@2x.png b/public/icons/docs/powershell/16@2x.png new file mode 100644 index 0000000000..5840c2dafd Binary files /dev/null and b/public/icons/docs/powershell/16@2x.png differ diff --git a/public/icons/docs/powershell/SOURCE b/public/icons/docs/powershell/SOURCE new file mode 100644 index 0000000000..3604afa785 --- /dev/null +++ b/public/icons/docs/powershell/SOURCE @@ -0,0 +1 @@ +https://learn.microsoft.com/en-us/powershell/media/index/ps_black_128.svg diff --git a/public/icons/docs/rabbitmq/16.png b/public/icons/docs/rabbitmq/16.png new file mode 100644 index 0000000000..0f99414aab Binary files /dev/null and b/public/icons/docs/rabbitmq/16.png differ diff --git a/public/icons/docs/rabbitmq/16@2x.png b/public/icons/docs/rabbitmq/16@2x.png new file mode 100644 index 0000000000..e0b7027dfa Binary files /dev/null and b/public/icons/docs/rabbitmq/16@2x.png differ diff --git a/public/icons/docs/rabbitmq/SOURCE b/public/icons/docs/rabbitmq/SOURCE new file mode 100644 index 0000000000..98720bdbd4 --- /dev/null +++ b/public/icons/docs/rabbitmq/SOURCE @@ -0,0 +1 @@ +https://www.rabbitmq.com/assets/files/rabbitmq-logo-e91cacd38fcef5219149bc5cfa10b384.svg diff --git a/public/icons/docs/tokio/16.png b/public/icons/docs/tokio/16.png new file mode 100644 index 0000000000..830b4021d5 Binary files /dev/null and b/public/icons/docs/tokio/16.png differ diff --git a/public/icons/docs/tokio/16@2x.png b/public/icons/docs/tokio/16@2x.png new file mode 100644 index 0000000000..f593b8573d Binary files /dev/null and b/public/icons/docs/tokio/16@2x.png differ diff --git a/public/icons/docs/tokio/SOURCE b/public/icons/docs/tokio/SOURCE new file mode 100644 index 0000000000..e09093716f --- /dev/null +++ b/public/icons/docs/tokio/SOURCE @@ -0,0 +1 @@ +https://upload.wikimedia.org/wikipedia/commons/6/60/Tokio_logo.svg diff --git a/public/icons/docs/vite/16.png b/public/icons/docs/vite/16.png index 90ffa78658..a081c7b8c1 100644 Binary files a/public/icons/docs/vite/16.png and b/public/icons/docs/vite/16.png differ diff --git a/public/icons/docs/vite/16@2x.png b/public/icons/docs/vite/16@2x.png index 375fb680ed..e8d199489b 100644 Binary files a/public/icons/docs/vite/16@2x.png and b/public/icons/docs/vite/16@2x.png differ diff --git a/public/icons/docs/vite/SOURCE b/public/icons/docs/vite/SOURCE index f1fe46eefe..f7b304f1f2 100644 --- a/public/icons/docs/vite/SOURCE +++ b/public/icons/docs/vite/SOURCE @@ -1 +1 @@ -https://vitejs.dev/logo.svg +https://vite.dev/logo-without-border.svg diff --git a/public/icons/docs/vitest/16.png b/public/icons/docs/vitest/16.png index fd44a6bb81..7fbe9bee38 100644 Binary files a/public/icons/docs/vitest/16.png and b/public/icons/docs/vitest/16.png differ diff --git a/public/icons/docs/vitest/16@2x.png b/public/icons/docs/vitest/16@2x.png index 051339ca94..6b72ed130d 100644 Binary files a/public/icons/docs/vitest/16@2x.png and b/public/icons/docs/vitest/16@2x.png differ diff --git a/public/icons/docs/vitest/SOURCE b/public/icons/docs/vitest/SOURCE index 911a2163a4..0cf0aaabab 100644 --- a/public/icons/docs/vitest/SOURCE +++ b/public/icons/docs/vitest/SOURCE @@ -1 +1,2 @@ https://vitest.dev/logo.svg +https://vitest.dev/favicon.ico diff --git a/test/app_test.rb b/test/app_test.rb index 44fca64aab..4b5e4c3297 100644 --- a/test/app_test.rb +++ b/test/app_test.rb @@ -46,21 +46,11 @@ def app end describe "/[static-page]" do - it "redirects to /#/[static-page] by default" do - %w(offline about news help).each do |page| + it "renders the app shell directly" do + %w(settings offline about news help).each do |page| get "/#{page}", {}, 'HTTP_USER_AGENT' => MODERN_BROWSER - assert last_response.redirect? - assert_equal "https://example.org/#/#{page}", last_response['Location'] - end - end - - it "redirects via JS cookie when a cookie exists" do - %w(offline about news help).each do |page| - set_cookie('foo=bar') - get "/#{page}", {}, 'HTTP_USER_AGENT' => MODERN_BROWSER - assert last_response.redirect? - assert_equal 'https://example.org/', last_response['Location'] - assert last_response['Set-Cookie'].start_with?("initial_path=%2F#{page}; path=/; expires=") + assert last_response.ok? + assert_nil last_response['Set-Cookie'] end end end @@ -90,12 +80,11 @@ def app assert last_response.ok? end - it "redirects via JS cookie when the doc exists and is enabled" do + it "renders when the doc exists and is enabled" do set_cookie('docs=html~5') get '/html~5/', {}, 'HTTP_USER_AGENT' => MODERN_BROWSER - assert last_response.redirect? - assert_equal 'https://example.org/', last_response['Location'] - assert last_response['Set-Cookie'].start_with?("initial_path=%2Fhtml%7E5%2F; path=/; expires=") + assert last_response.ok? + assert_nil last_response['Set-Cookie'] end it "renders when the doc exists, has no version in the path, and isn't enabled" do @@ -103,12 +92,11 @@ def app assert last_response.ok? end - it "redirects via JS cookie when the doc exists, has no version in the path, and a version is enabled" do + it "renders when the doc exists, has no version in the path, and a version is enabled" do set_cookie('docs=html~5') get '/html/', {}, 'HTTP_USER_AGENT' => MODERN_BROWSER - assert last_response.redirect? - assert_equal 'https://example.org/', last_response['Location'] - assert last_response['Set-Cookie'].start_with?("initial_path=%2Fhtml%2F; path=/; expires=") + assert last_response.ok? + assert_nil last_response['Set-Cookie'] end it "renders when the doc exists and is enabled, and the request is from Googlebot" do diff --git a/test/assets/search_hash_test.js b/test/assets/search_hash_test.js new file mode 100644 index 0000000000..276f3df387 --- /dev/null +++ b/test/assets/search_hash_test.js @@ -0,0 +1,50 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const test = require("node:test"); +const vm = require("node:vm"); + +const context = { + app: { + config: { search_param: "q" }, + router: { replaceHash: () => {} }, + views: {}, + View: class {}, + }, + location: { hash: "" }, + $: { + urlDecodeFragment: decodeURIComponent, + }, +}; + +vm.createContext(context); + +for (const file of [ + "assets/javascripts/views/search/search_scope.js", + "assets/javascripts/views/search/search.js", +]) { + vm.runInContext(fs.readFileSync(file, "utf8"), context, { + filename: file, + }); +} + +test("URL search hash preserves plus signs in a scoped C++ query", () => { + context.location.hash = "#q=c++%20std::min"; + + const scope = new context.app.views.SearchScope(); + let replacedHash; + context.app.router.replaceHash = (hash) => { + replacedHash = hash; + }; + + assert.equal(scope.getHashValue(), "c++"); + assert.equal(scope.extractHashValue(), "c++"); + assert.equal(replacedHash, "#q=std::min"); +}); + +test("URL search hash preserves encoded literal plus signs in the query", () => { + context.location.hash = "#q=operator%2B"; + + const search = new context.app.views.Search(); + + assert.equal(search.getHashValue(), "operator+"); +}); diff --git a/test/lib/docs/core/doc_test.rb b/test/lib/docs/core/doc_test.rb index 6b41cdd9c8..b6c08c5f4f 100644 --- a/test/lib/docs/core/doc_test.rb +++ b/test/lib/docs/core/doc_test.rb @@ -1,5 +1,6 @@ require_relative '../../../test_helper' require_relative '../../../../lib/docs' +require 'base64' class DocsDocTest < Minitest::Spec let :doc do @@ -237,6 +238,18 @@ class DocsDocTest < Minitest::Spec doc.store_page(store, 'id') end end + + context "when the store can't be opened" do + before do + stub(store).open { raise Docs::SetupError, "oh no" } + end + + it "logs an error" do + output = capture_io { doc.store_page(store, 'id') }.first + + assert_match "ERROR: oh no", output + end + end end describe ".store_pages" do @@ -353,6 +366,18 @@ class DocsDocTest < Minitest::Spec doc.store_pages(store) end end + + context "when the store can't be opened" do + before do + stub(store).replace { raise Docs::SetupError, "oh no" } + end + + it "logs an error" do + output = capture_io { doc.store_pages(store) }.first + + assert_match "ERROR: oh no", output + end + end end describe ".versions" do @@ -406,4 +431,372 @@ class DocsDocTest < Minitest::Spec assert_equal "Outdated minor version", instance.outdated_state('2006-01-02', '2006-02-03') end end + + # PRIVATE CLASS METHODS + + describe ".default_slug" do + it "returns name.downcase when name is alphanumeric" do + doc.name = 'FooBar' + assert_equal 'foobar', doc.send(:default_slug) + end + + it "returns name.downcase when name contains underscores" do + doc.name = 'Foo_Bar' + assert_equal 'foo_bar', doc.send(:default_slug) + end + + it "returns nil when name contains a hyphen" do + doc.name = 'Foo-Bar' + assert_nil doc.send(:default_slug) + end + + it "returns nil when name contains a space" do + doc.name = 'Foo Bar' + assert_nil doc.send(:default_slug) + end + + it "returns nil when name contains a dot" do + doc.name = 'Foo.Bar' + assert_nil doc.send(:default_slug) + end + end + + describe ".store_page?" do + it "returns true when page has non-empty :entries" do + assert doc.send(:store_page?, page) + end + + it "returns false when page has empty :entries" do + refute doc.send(:store_page?, page.merge(entries: [])) + end + + it "returns false when page has nil :entries" do + refute doc.send(:store_page?, page.merge(entries: nil)) + end + end + + describe ".store_index" do + let :index do + obj = Object.new + stub(obj).to_json { '{"entries":[]}' } + obj + end + + context "when read_write is true (default)" do + it "reads old JSON from store" do + mock(store).read('index.json') { '{}' } + stub(store).write + doc.send(:store_index, store, 'index.json', index) + end + + it "writes new JSON to store" do + stub(store).read { '{}' } + mock(store).write('index.json', '{"entries":[]}') + doc.send(:store_index, store, 'index.json', index) + end + end + + context "when read_write is false" do + it "doesn't read from store" do + dont_allow(store).read + doc.send(:store_index, store, 'index.json', index, false) + end + + it "doesn't write to store" do + dont_allow(store).write + doc.send(:store_index, store, 'index.json', index, false) + end + end + end + + describe ".store_meta" do + it "writes to META_FILENAME" do + stub(store).size { 0 } + mock(store).write(Docs::Doc::META_FILENAME, anything) + doc.send(:store_meta, store) + end + + it "includes :mtime as an integer in the written JSON" do + stub(store).size { 0 } + stub(store).write(Docs::Doc::META_FILENAME, anything) do |_, json| + @json = JSON.parse(json) + end + doc.send(:store_meta, store) + assert @json.key?('mtime') + assert_instance_of Integer, @json['mtime'] + end + + it "includes :db_size from the store" do + stub(store).size(Docs::Doc::DB_FILENAME) { 42 } + stub(store).write(Docs::Doc::META_FILENAME, anything) do |_, json| + @json = JSON.parse(json) + end + doc.send(:store_meta, store) + assert_equal 42, @json['db_size'] + end + end + + # PRIVATE INSTANCE METHODS + + describe "#fetch" do + let :instance do + doc.new + end + + let :logger do + l = Object.new + stub(l).debug + stub(l).error + l + end + + let :opts do + { logger: logger } + end + + let :response do + r = Object.new + stub(r).success? { true } + stub(r).body { 'body' } + r + end + + before do + stub(Docs::Request).run { response } + end + + it "returns the response body on success" do + assert_equal 'body', instance.send(:fetch, 'http://example.com/', opts) + end + + it "calls Request.run with the given url" do + mock(Docs::Request).run('http://example.com/', anything) { response } + instance.send(:fetch, 'http://example.com/', opts) + end + + it "adds Authorization header when opts has :github_token and url is GitHub API" do + opts[:github_token] = 'mytoken' + mock(Docs::Request).run(anything, anything) do |_, request_opts| + assert_equal 'token mytoken', request_opts[:headers]['Authorization'] + response + end + instance.send(:fetch, 'https://api.github.com/repos', opts) + end + + it "doesn't add Authorization header for non-GitHub API urls even with :github_token" do + opts[:github_token] = 'mytoken' + mock(Docs::Request).run(anything, anything) do |_, request_opts| + refute request_opts[:headers].key?('Authorization') + response + end + instance.send(:fetch, 'http://example.com/', opts) + end + + context "when response is unsuccessful" do + before do + stub(response).success? { false } + stub(response).timed_out? { false } + stub(response).code { 404 } + end + + it "raises an error" do + assert_raises(RuntimeError) do + instance.send(:fetch, 'http://example.com/', opts) + end + end + + it "includes the response code in the error message" do + err = assert_raises(RuntimeError) { instance.send(:fetch, 'http://example.com/', opts) } + assert_match '404', err.message + end + end + + context "when response times out" do + before do + stub(response).success? { false } + stub(response).timed_out? { true } + end + + it "raises an error with a timeout message" do + err = assert_raises(RuntimeError) { instance.send(:fetch, 'http://example.com/', opts) } + assert_match 'Timed out', err.message + end + end + end + + describe "#fetch_doc" do + let :instance do + doc.new + end + + let :opts do + { logger: nil } + end + + it "returns a Nokogiri::HTML::Document" do + stub(instance).fetch { '' } + assert_instance_of Nokogiri::HTML::Document, instance.send(:fetch_doc, 'http://example.com/', opts) + end + + it "fetches the given url" do + mock(instance).fetch('http://example.com/', opts) { '' } + instance.send(:fetch_doc, 'http://example.com/', opts) + end + end + + describe "#fetch_json" do + let :instance do + doc.new + end + + let :opts do + { logger: nil } + end + + it "returns parsed JSON" do + stub(instance).fetch { '{"key":"value"}' } + assert_equal({ 'key' => 'value' }, instance.send(:fetch_json, 'http://example.com/', opts)) + end + + it "fetches the given url" do + mock(instance).fetch('http://example.com/', opts) { '{}' } + instance.send(:fetch_json, 'http://example.com/', opts) + end + end + + describe "#get_npm_version" do + let :instance do + doc.new + end + + let :opts do + { logger: nil } + end + + it "returns the 'latest' dist-tag by default" do + stub(instance).fetch_json { { 'dist-tags' => { 'latest' => '1.2.3' } } } + assert_equal '1.2.3', instance.send(:get_npm_version, 'some-package', opts) + end + + it "returns a specific dist-tag when given" do + stub(instance).fetch_json { { 'dist-tags' => { 'next' => '2.0.0-beta' } } } + assert_equal '2.0.0-beta', instance.send(:get_npm_version, 'some-package', opts, 'next') + end + + it "fetches from the npm registry URL" do + mock(instance).fetch_json('https://registry.npmjs.com/some-package', opts) { { 'dist-tags' => { 'latest' => '1.0.0' } } } + instance.send(:get_npm_version, 'some-package', opts) + end + end + + describe "#get_latest_github_release" do + let :instance do + doc.new + end + + let :opts do + { logger: nil } + end + + it "strips a leading 'v' from the tag name" do + stub(instance).fetch_json { { 'tag_name' => 'v1.2.3' } } + assert_equal '1.2.3', instance.send(:get_latest_github_release, 'owner', 'repo', opts) + end + + it "returns the tag name unchanged when it doesn't start with 'v'" do + stub(instance).fetch_json { { 'tag_name' => '1.2.3' } } + assert_equal '1.2.3', instance.send(:get_latest_github_release, 'owner', 'repo', opts) + end + + it "fetches from the correct GitHub releases URL" do + mock(instance).fetch_json('https://api.github.com/repos/owner/repo/releases/latest', opts) { { 'tag_name' => '1.0.0' } } + instance.send(:get_latest_github_release, 'owner', 'repo', opts) + end + end + + describe "#get_github_tags" do + let :instance do + doc.new + end + + let :opts do + { logger: nil } + end + + it "fetches from the correct GitHub tags URL" do + mock(instance).fetch_json('https://api.github.com/repos/owner/repo/tags', opts) { [] } + instance.send(:get_github_tags, 'owner', 'repo', opts) + end + + it "returns the tags list" do + tags = [{ 'name' => 'v1.0' }] + stub(instance).fetch_json { tags } + assert_equal tags, instance.send(:get_github_tags, 'owner', 'repo', opts) + end + end + + describe "#get_github_file_contents" do + let :instance do + doc.new + end + + let :opts do + { logger: nil } + end + + it "returns Base64-decoded file contents" do + stub(instance).fetch_json { { 'content' => ::Base64.encode64('hello world') } } + assert_equal 'hello world', instance.send(:get_github_file_contents, 'owner', 'repo', 'README.md', opts) + end + + it "fetches from the correct GitHub contents URL" do + mock(instance).fetch_json('https://api.github.com/repos/owner/repo/contents/README.md', opts) { { 'content' => ::Base64.encode64('') } } + instance.send(:get_github_file_contents, 'owner', 'repo', 'README.md', opts) + end + end + + describe "#get_latest_github_commit_date" do + let :instance do + doc.new + end + + let :opts do + { logger: nil } + end + + it "returns the epoch timestamp of the latest commit" do + date_str = '2023-06-15T12:00:00Z' + commits = [{ 'commit' => { 'author' => { 'date' => date_str } } }] + stub(instance).fetch_json { commits } + assert_equal Date.iso8601(date_str).to_time.to_i, instance.send(:get_latest_github_commit_date, 'owner', 'repo', opts) + end + + it "fetches from the correct GitHub commits URL" do + date_str = '2023-06-15T12:00:00Z' + commits = [{ 'commit' => { 'author' => { 'date' => date_str } } }] + mock(instance).fetch_json('https://api.github.com/repos/owner/repo/commits', opts) { commits } + instance.send(:get_latest_github_commit_date, 'owner', 'repo', opts) + end + end + + describe "#get_gitlab_tags" do + let :instance do + doc.new + end + + let :opts do + { logger: nil } + end + + it "fetches from the correct GitLab tags URL" do + mock(instance).fetch_json('https://gitlab.example.com/api/v4/projects/group%2Fproject/repository/tags', opts) { [] } + instance.send(:get_gitlab_tags, 'gitlab.example.com', 'group', 'project', opts) + end + + it "returns the tags list" do + tags = [{ 'name' => 'v1.0' }] + stub(instance).fetch_json { tags } + assert_equal tags, instance.send(:get_gitlab_tags, 'gitlab.example.com', 'group', 'project', opts) + end + end end diff --git a/test/lib/docs/core/entry_index_test.rb b/test/lib/docs/core/entry_index_test.rb index 95495eb90a..6222d4e163 100644 --- a/test/lib/docs/core/entry_index_test.rb +++ b/test/lib/docs/core/entry_index_test.rb @@ -154,6 +154,72 @@ class DocsEntryIndexTest < Minitest::Spec end end + describe "#sort_fn" do + def sort(a, b) + index.send(:sort_fn, a, b) + end + + context "when neither string starts with a digit 1-9" do + it "uses case-insensitive string comparison" do + assert_operator sort('apple', 'banana'), :<, 0 + assert_operator sort('banana', 'apple'), :>, 0 + end + + it "returns 0 for case-insensitive equal strings" do + assert_equal 0, sort('Apple', 'apple') + end + + it "treats strings starting with '0' as non-numeric" do + assert_equal '0apple'.casecmp('banana'), sort('0apple', 'banana') + end + end + + context "when at least one string starts with 1-9 but neither contains version dots" do + it "uses case-insensitive string comparison" do + # lexicographic: '1' < '2', so '10 Test' < '2 Test' + assert_operator sort('10 Test', '2 Test'), :<, 0 + end + + it "returns 0 for case-insensitive equal strings" do + assert_equal 0, sort('2 Test', '2 test') + end + end + + context "when one string has version dots and the other does not" do + it "sorts the dotted string before the undotted one" do + assert_operator sort('1.2. Test', '2 Test'), :<, 0 + end + + it "sorts the undotted string after the dotted one" do + assert_operator sort('2 Test', '1.2. Test'), :>, 0 + end + + it "sorts the dotted string before a non-numeric string" do + assert_operator sort('1.2. Test', 'abc'), :<, 0 + end + end + + context "when both strings have version dots" do + it "sorts numerically rather than lexicographically" do + assert_operator sort('1.9. Test', '1.10. Test'), :<, 0 + assert_operator sort('4.20. Test', '4.3. Test'), :>, 0 + end + + it "pads shorter version depths with zeros before comparing" do + assert_operator sort('4. Test', '4.2. Test'), :<, 0 + assert_operator sort('4.2. Test', '4. Test'), :>, 0 + end + + it "compares multi-level versions against shorter ones correctly" do + assert_operator sort('4.2.2. Test', '4.3. Test'), :<, 0 + end + + it "returns 0 for identical version strings" do + assert_equal 0, sort('1.2. Test', '1.2. Test') + end + end + end + describe "#to_json" do it "returns the JSON string for #as_json" do stub(index).as_json { { entries: [1], types: [2] } } diff --git a/test/lib/docs/core/filter_test.rb b/test/lib/docs/core/filter_test.rb index 10872c7beb..205c6bced1 100644 --- a/test/lib/docs/core/filter_test.rb +++ b/test/lib/docs/core/filter_test.rb @@ -8,6 +8,12 @@ class DocsFilterTest < Minitest::Spec before do context[:base_url] = 'http://example.com/path' context[:url] = 'http://example.com/path/file' + context[:version] = '6.1' + context[:release] = '6.1.1' + context[:links] = { + home: 'homunculus', + code: 'codified' + } end describe "#subpath" do @@ -55,6 +61,24 @@ def slug(subpath) end end + describe "#version" do + it "returns the version" do + assert_equal filter.version, "6.1" + end + end + + describe "#release" do + it "returns the release" do + assert_equal filter.release, "6.1.1" + end + end + + describe "#links" do + it "returns the links hash" do + assert_equal filter.links, { home: 'homunculus', code: 'codified' } + end + end + describe "#root_page?" do it "returns true when #subpath is blank" do stub(filter).subpath { '' } @@ -176,4 +200,18 @@ def slug(subpath) refute filter.absolute_url_string?('http') end end + + describe "#clean_path" do + it "replaces some special characters with dashes" do + bad_path = "/horrible!url:design;huh".freeze + good_path = "/horrible-url-design-huh".freeze + assert_equal filter.clean_path(bad_path), good_path + end + + it "replaces + with the word 'plus'" do + bad_path = "/c++".freeze + good_path = "/c_plus__plus_".freeze + assert_equal filter.clean_path(bad_path), good_path + end + end end diff --git a/test/lib/docs/core/scraper_test.rb b/test/lib/docs/core/scraper_test.rb index 7b5522b3cb..e2efebf1af 100644 --- a/test/lib/docs/core/scraper_test.rb +++ b/test/lib/docs/core/scraper_test.rb @@ -510,4 +510,166 @@ class Scraper < Docs::Scraper assert_equal Docs, pipeline.instrumentation_service end end + + # PRIVATE INSTANCE METHODS + + describe "#url_for" do + it "returns the root url when path is empty" do + assert_equal scraper.root_url.to_s, scraper.send(:url_for, '') + end + + it "returns the root url when path is '/'" do + assert_equal scraper.root_url.to_s, scraper.send(:url_for, '/') + end + + it "joins base_url and path for a non-root path" do + stub(Scraper).base_url { 'http://example.com/' } + assert_equal 'http://example.com/page', scraper.send(:url_for, '/page') + end + end + + describe "#parse" do + it "returns a two-element array" do + response.body = 'T' + assert_equal 2, scraper.send(:parse, response).length + end + + it "extracts the title from an HTML document" do + response.body = 'My Title' + _, title = scraper.send(:parse, response) + assert_equal 'My Title', title + end + + it "returns nil title for an HTML fragment" do + response.body = '
    just a fragment
    ' + _, title = scraper.send(:parse, response) + assert_nil title + end + end + + describe "#pipeline_context" do + it "returns options merged with the response url" do + stub(scraper).options { { base_url: 'http://example.com/' } } + response.url = 'http://example.com/page' + context = scraper.send(:pipeline_context, response) + assert_equal 'http://example.com/page', context[:url] + assert_equal 'http://example.com/', context[:base_url] + end + + it "does not mutate the options hash" do + opts = { base_url: 'http://example.com/' } + stub(scraper).options { opts } + response.url = 'url' + scraper.send(:pipeline_context, response) + refute opts.key?(:url) + end + end + + describe "#process_response" do + before do + stub(scraper).parse(response) { ['
    ', 'My Title'] } + response.url = 'http://example.com/' + end + + it "returns a hash" do + assert_instance_of Hash, scraper.send(:process_response, response) + end + + it "passes :html_title from parse into the pipeline context" do + mock(scraper.pipeline).call(anything, anything, anything) do |_, context, _| + assert_equal 'My Title', context[:html_title] + end + scraper.send(:process_response, response) + end + end + + describe "#with_filters" do + it "uses only the given filters inside the block" do + scraper.send(:with_filters) do + @filters = scraper.pipeline.filters + end + assert_equal [], @filters + end + + it "resets the pipeline after the block" do + pipeline_before = scraper.pipeline + scraper.send(:with_filters) {} + refute_same pipeline_before, scraper.pipeline + end + end + + describe "#additional_options" do + it "returns an empty hash" do + assert_equal({}, scraper.send(:additional_options)) + end + end + + class FixedScraper < Scraper + include Docs::Scraper::FixInternalUrlsBehavior + end + + describe "FixInternalUrlsBehavior" do + let :fixed_scraper do + FixedScraper.new.tap { |s| s.extend FakeInstrumentation } + end + + describe ".with_internal_urls (private)" do + it "sets .internal_urls to the result of fetch_internal_urls during the block" do + any_instance_of(FixedScraper) do |instance| + stub(instance).fetch_internal_urls { ['url1', 'url2'] } + end + FixedScraper.send(:with_internal_urls) do + @urls = FixedScraper.internal_urls + end + assert_equal ['url1', 'url2'], @urls + end + + it "clears .internal_urls after the block" do + any_instance_of(FixedScraper) do |instance| + stub(instance).fetch_internal_urls { [] } + end + FixedScraper.send(:with_internal_urls) {} + assert_nil FixedScraper.internal_urls + end + end + + describe "#additional_options (private)" do + it "returns an empty hash when .internal_urls is nil" do + stub(FixedScraper).internal_urls { nil } + assert_equal({}, fixed_scraper.send(:additional_options)) + end + + it "returns fixed_internal_urls: true when .internal_urls is set" do + stub(FixedScraper).internal_urls { ['url'] } + assert fixed_scraper.send(:additional_options)[:fixed_internal_urls] + end + + it "sets :only to a Set of .internal_urls" do + stub(FixedScraper).internal_urls { ['url1', 'url2'] } + assert_equal Set.new(['url1', 'url2']), fixed_scraper.send(:additional_options)[:only] + end + + it "sets :only_patterns to nil" do + stub(FixedScraper).internal_urls { ['url'] } + assert_nil fixed_scraper.send(:additional_options)[:only_patterns] + end + + it "sets :skip to nil" do + stub(FixedScraper).internal_urls { ['url'] } + assert_nil fixed_scraper.send(:additional_options)[:skip] + end + end + + describe "#process_response (private)" do + before do + stub(fixed_scraper).parse(response) { ['
    ', nil] } + response.url = 'http://example.com/page' + end + + it "merges :response_url from the response into the result" do + result = fixed_scraper.send(:process_response, response) + assert_equal 'http://example.com/page', result[:response_url] + end + end + end end diff --git a/test/lib/docs/core/scrapers/url_scraper_test.rb b/test/lib/docs/core/scrapers/url_scraper_test.rb index 77fc235d57..7469ce4070 100644 --- a/test/lib/docs/core/scrapers/url_scraper_test.rb +++ b/test/lib/docs/core/scrapers/url_scraper_test.rb @@ -85,6 +85,38 @@ class Scraper < Docs::UrlScraper stub(Docs::Requester).run { 'response' } assert_equal 'response', result end + + context "when options[:rate_limit] is set" do + before do + @saved_limiter = Docs::UrlScraper.class_variable_get(:@@rate_limiter) + Docs::UrlScraper.class_variable_set(:@@rate_limiter, nil) + stub(Scraper).options { { rate_limit: 5 } } + stub(Docs::Requester).run + end + + after do + Docs::UrlScraper.class_variable_set(:@@rate_limiter, @saved_limiter) + end + + it "creates a RateLimiter with the given limit" do + stub(Typhoeus).before + scraper.send :request_all, 'urls' + assert_equal 5, Docs::UrlScraper.class_variable_get(:@@rate_limiter).limit + end + + it "registers the RateLimiter with Typhoeus.before" do + mock(Typhoeus).before + scraper.send :request_all, 'urls' + end + + it "updates the existing RateLimiter's limit instead of creating a new one" do + existing = Docs::UrlScraper::RateLimiter.new(3) + Docs::UrlScraper.class_variable_set(:@@rate_limiter, existing) + dont_allow(Typhoeus).before + scraper.send :request_all, 'urls' + assert_equal 5, existing.limit + end + end end describe "#process_response?" do @@ -120,4 +152,34 @@ class Scraper < Docs::UrlScraper assert result end end + + describe "RateLimiter" do + let :limiter do + Docs::UrlScraper::RateLimiter.new(3) + end + + describe "#initialize" do + it "sets the limit" do + assert_equal 3, limiter.limit + end + end + + describe "#limit=" do + it "updates the limit" do + limiter.limit = 10 + assert_equal 10, limiter.limit + end + end + + describe "#to_proc" do + it "returns a Proc" do + assert_instance_of Proc, limiter.to_proc + end + + it "returns a proc that calls #call" do + mock(limiter).call + limiter.to_proc.call + end + end + end end diff --git a/test/lib/docs/core/subscriber_test.rb b/test/lib/docs/core/subscriber_test.rb new file mode 100644 index 0000000000..8f56cb9853 --- /dev/null +++ b/test/lib/docs/core/subscriber_test.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require_relative '../../../test_helper' +require_relative '../../../../lib/docs' + +class DocsSubscriberTest < Minitest::Spec + let(:subscriber) { Docs::Subscriber.new } + + describe "#format_url" do + it "removes http:// scheme" do + assert_equal 'devdocs.test.co/path', subscriber.send(:format_url, 'http://devdocs.test.co/path') + end + + it "removes https:// scheme" do + assert_equal 'devdocs.test.co/path', subscriber.send(:format_url, 'https://devdocs.test.co/path') + end + + it "returns non-http urls unchanged" do + assert_equal 'devdocs.test.co/path', subscriber.send(:format_url, 'devdocs.test.co/path') + end + end + + describe "#format_path" do + it "removes the current working directory prefix" do + full_path = File.join(Dir.pwd, 'some/file.rb') + assert_equal 'some/file.rb', subscriber.send(:format_path, full_path) + end + + it "returns paths outside the cwd unchanged" do + assert_equal '/other/path', subscriber.send(:format_path, '/other/path') + end + + it "calls to_s on the argument" do + path = Object.new + def path.to_s; '/other/path'; end + assert_equal '/other/path', subscriber.send(:format_path, path) + end + end + + describe "#justify" do + describe "when terminal_width is nil" do + before { subscriber.instance_variable_set(:@terminal_width, nil) } + + it "returns the string unchanged" do + assert_equal 'hello', subscriber.send(:justify, 'hello') + end + end + + describe "when terminal_width is set" do + before { subscriber.instance_variable_set(:@terminal_width, 20) } + + it "pads a short string to terminal width" do + result = subscriber.send(:justify, 'hi') + assert_equal 20, result.length + assert result.start_with?('hi') + end + + it "truncates a long string to terminal width" do + result = subscriber.send(:justify, 'a' * 30) + assert_equal 20, result.length + end + + it "reserves space for a trailing tag and pads the rest" do + result = subscriber.send(:justify, 'hi [tag]') + assert_equal 20, result.length + assert result.end_with?('[tag]') + end + + it "truncates the non-tag part when the string is too long" do + result = subscriber.send(:justify, ('a' * 30) + ' [tag]') + assert_equal 20, result.length + assert result.end_with?('[tag]') + end + end + end + + describe "#terminal_width" do + it "returns nil when stdout is not a tty" do + old_stdout = $stdout + $stdout = StringIO.new + sub = Docs::Subscriber.new + assert_nil sub.send(:terminal_width) + ensure + $stdout = old_stdout + end + + it "reads COLUMNS env var when stdout is a tty" do + sub = Docs::Subscriber.new + # Override tty? on the instance to avoid needing a real tty + def sub.tty?; true; end + old_columns = ENV.delete('COLUMNS') + ENV['COLUMNS'] = '666' + assert_equal 666, sub.send(:terminal_width) + ensure + ENV['COLUMNS'] = old_columns + end + end +end diff --git a/test/lib/docs/filters/core/attribution_test.rb b/test/lib/docs/filters/core/attribution_test.rb new file mode 100644 index 0000000000..73ed53e912 --- /dev/null +++ b/test/lib/docs/filters/core/attribution_test.rb @@ -0,0 +1,39 @@ +require_relative '../../../../test_helper' +require_relative '../../../../../lib/docs' + +class AttributionFilterTest < Minitest::Spec + include FilterTestHelper + self.filter_class = Docs::AttributionFilter + self.filter_type = 'html' + + before do + @body = "
    Hello!
    " + end + + context "when attribution is a string" do + before do + context[:attribution] = "Copyright 2033 Mark Twain" + context[:base_url] = "q" + end + + it "adds it to the document" do + assert_equal filter_output.at_css('#mtwain').inner_html, "Copyright 2033 Mark Twain" + refute_nil filter_output.at_css('p._attribution-p'), "Attribution node should exist" + refute_nil filter_output.at_css('.doc'), "Existing doc should still be present" + refute_nil filter_output.at_css('a._attribution-link'), "Attribution link should exist" + end + end + + context "when attribution is a Proc" do + before do + context[:attribution] = ->(filter) { + "Copyright 2034 Mark Twain" + } + context[:base_url] = "q" + end + + it "adds its output to the document" do + assert_equal filter_output.at_css('#mtwain').inner_html, "Copyright 2034 Mark Twain" + end + end +end diff --git a/test/lib/docs/filters/core/clean_local_urls_test.rb b/test/lib/docs/filters/core/clean_local_urls_test.rb new file mode 100644 index 0000000000..e2a2e8eb67 --- /dev/null +++ b/test/lib/docs/filters/core/clean_local_urls_test.rb @@ -0,0 +1,22 @@ +require_relative '../../../../test_helper' +require_relative '../../../../../lib/docs' + +class CleanLocalUrlsFilterTest < Minitest::Spec + include FilterTestHelper + self.filter_class = Docs::CleanLocalUrlsFilter + + it "removes images/iframes referencing localhost" do + context[:base_url] = 'http://localhost' + @body = '' + assert_empty filter_output.css('img, iframe') + end + + it "turns links referencing localhost into plain text" do + context[:base_url] = 'http://localhost' + @body = 'Link' + node = filter_output.at_css('span') + assert node, 'expected a node' + refute node['href'] + assert_equal 'Link', node.text + end +end diff --git a/test/lib/docs/filters/core/images_test.rb b/test/lib/docs/filters/core/images_test.rb new file mode 100644 index 0000000000..2763b18cfe --- /dev/null +++ b/test/lib/docs/filters/core/images_test.rb @@ -0,0 +1,134 @@ +require_relative '../../../../test_helper' +require_relative '../../../../../lib/docs' +require 'ostruct' + +class ImagesFilterTest < Minitest::Spec + include FilterTestHelper + self.filter_class = Docs::ImagesFilter + + IMG_SRC = 'http://example.com/img.png' + IMG_BODY = '' + + before do + Docs::ImagesFilter.cache.clear + context[:optimize_images] = false + end + + def make_response(success: true, mime_type: 'image/png', content_length: 100, body: 'imgdata', code: 200) + OpenStruct.new(success?: success, mime_type: mime_type, content_length: content_length, body: body, code: code) + end + + def stub_request(response) + stub(Docs::Request).run { |_url, &block| block.call(response) } + end + + context "when download_images is false" do + it "returns doc unchanged and makes no requests" do + context[:download_images] = false + @body = IMG_BODY + dont_allow(Docs::Request).run + assert_equal IMG_SRC, filter_output.at_css('img')['src'] + end + end + + context "with a data: URL" do + it "skips the image and makes no request" do + @body = '' + dont_allow(Docs::Request).run + assert_equal 'data:image/png;base64,aaaaaa2320345bbb234', filter_output.at_css('img')['src'] + end + end + + context "with a non-http(s) URL" do + it "skips the image and makes no request" do + @body = '' + dont_allow(Docs::Request).run + filter_output + end + end + + context "with a successful image response" do + it "replaces src with a base64 data URI" do + @body = IMG_BODY + stub_request make_response(body: 'imgdata', mime_type: 'image/png', content_length: 7) + expected = "data:image/png;base64,#{Base64.strict_encode64('imgdata')}" + assert_equal expected, filter_output.at_css('img')['src'] + end + + it "caches the data URI for subsequent calls" do + @body = IMG_BODY + stub_request make_response(body: 'imgdata', mime_type: 'image/png', content_length: 7) + filter_output + assert Docs::ImagesFilter.cache.key?(IMG_SRC) + assert Docs::ImagesFilter.cache[IMG_SRC].start_with?('data:') + end + end + + context "when response is not successful" do + it "instruments broken.image" do + @body = IMG_BODY + stub_request make_response(success: false, code: 404) + @called = false + filter.subscribe('broken.image') { @called = true } + filter_output + assert @called + end + + it "does not alter the src" do + @body = IMG_BODY + stub_request make_response(success: false, code: 404) + assert_equal IMG_SRC, filter_output.at_css('img')['src'] + end + end + + context "when response mime type is not image/" do + it "instruments invalid.image" do + @body = IMG_BODY + stub_request make_response(mime_type: 'text/html') + @called = false + filter.subscribe('invalid.image') { @called = true } + filter_output + assert @called + end + end + + context "when an exception is raised during the request" do + it "instruments error.image" do + @body = IMG_BODY + stub(Docs::Request).run { raise 'connection error' } + @called = false + filter.subscribe('error.image') { @called = true } + filter_output + assert @called + end + end + + context "with a cached src" do + it "uses the cached data URI without making a request" do + cached = "data:image/png;base64,#{Base64.strict_encode64('cached')}" + Docs::ImagesFilter.cache[IMG_SRC] = cached + @body = IMG_BODY + dont_allow(Docs::Request).run + assert_equal cached, filter_output.at_css('img')['src'] + end + + it "leaves src unchanged when cache marks the URL as failed" do + Docs::ImagesFilter.cache[IMG_SRC] = false + @body = IMG_BODY + dont_allow(Docs::Request).run + assert_equal IMG_SRC, filter_output.at_css('img')['src'] + end + end + + context "when optimize_images is not disabled" do + it "passes image data through optimize_image_data" do + @body = IMG_BODY + optimized = 'optimized_imgdata' + stub_request make_response(body: 'imgdata', mime_type: 'image/png', content_length: 7) + stub(Docs::ImagesFilter).optimize_image_data('imgdata') { optimized } + context.delete(:optimize_images) + expected = "data:image/png;base64,#{Base64.strict_encode64(optimized)}" + assert_equal expected, filter_output.at_css('img')['src'] + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 8ae02364c6..602d158bdd 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,3 +1,13 @@ +# Flag set by Thor coverage task. +if ENV['COVERAGE'] == 'true' + require 'simplecov' + + # Focus only on core app files. + SimpleCov.add_filter {|src| !(src.filename =~ /\/core\//)} + + SimpleCov.start 'rails' +end + ENV['RACK_ENV'] = 'test' require 'bundler/setup' diff --git a/views/unsupported.erb b/views/unsupported.erb index 6c1ded7ef1..3540adddb7 100644 --- a/views/unsupported.erb +++ b/views/unsupported.erb @@ -20,7 +20,7 @@ We decided to prioritize speed and new features over support for older browsers.

    - — @DevDocs + — DevDocs