Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ end
group :development do
gem 'better_errors'
gem 'pry-byebug'
gem 'fiddle', platforms: :windows # reline's Windows console needs it; not bundled since Ruby 4.0
gem 'sinatra-contrib'
end

Expand Down
16 changes: 16 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,11 @@ GEM
ffi (1.17.4-arm-linux-gnu)
ffi (1.17.4-arm-linux-musl)
ffi (1.17.4-arm64-darwin)
ffi (1.17.4-x64-mingw-ucrt)
ffi (1.17.4-x86_64-darwin)
ffi (1.17.4-x86_64-linux-gnu)
ffi (1.17.4-x86_64-linux-musl)
fiddle (1.1.8)
fspath (3.1.2)
google-protobuf (4.36.2)
bigdecimal
Expand All @@ -80,6 +82,9 @@ GEM
google-protobuf (4.36.2-arm64-darwin)
bigdecimal
rake (~> 13.3)
google-protobuf (4.36.2-x64-mingw-ucrt)
bigdecimal
rake (~> 13.3)
google-protobuf (4.36.2-x86_64-darwin)
bigdecimal
rake (~> 13.3)
Expand Down Expand Up @@ -162,6 +167,8 @@ GEM
racc (~> 1.4)
nokogiri (1.19.4-arm64-darwin)
racc (~> 1.4)
nokogiri (1.19.4-x64-mingw-ucrt)
racc (~> 1.4)
nokogiri (1.19.4-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.19.4-x86_64-linux-gnu)
Expand Down Expand Up @@ -241,6 +248,8 @@ GEM
google-protobuf (~> 4.31)
sass-embedded (1.106.1-arm64-darwin)
google-protobuf (~> 4.31)
sass-embedded (1.106.1-x64-mingw-ucrt)
google-protobuf (~> 4.31)
sass-embedded (1.106.1-x86_64-darwin)
google-protobuf (~> 4.31)
sass-embedded (1.106.1-x86_64-linux-gnu)
Expand Down Expand Up @@ -303,6 +312,7 @@ PLATFORMS
arm-linux-musl
arm-linux-musleabihf
arm64-darwin
x64-mingw-ucrt
x86_64-darwin
x86_64-linux-gnu
x86_64-linux-musl
Expand All @@ -313,6 +323,7 @@ DEPENDENCIES
chunky_png
dartsass-sprockets
erubi
fiddle
html-pipeline (~> 2.14)
image_optim
image_optim_pack
Expand Down Expand Up @@ -369,14 +380,17 @@ CHECKSUMS
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-x64-mingw-ucrt) sha256=f6ff9618cfccc494138bddade27aa06c74c6c7bc367a1ea1103d80c2fcb9ed35
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
fiddle (1.1.8) sha256=7fa8ee3627271497f3add5503acdbc3f40b32f610fc1cf49634f083ef3f32eee
fspath (3.1.2) sha256=b5ac9bafb97e2c8f8f9e303cd98ebd484be76fe9aa588bc4d01c6d99e78c9d75
google-protobuf (4.36.2) sha256=2f8daff50ae14aad36ab0ccdcf284065fa71d726c444efe114e6db82ffe62494
google-protobuf (4.36.2-aarch64-linux-gnu) sha256=c3279ec6eaa59f6ed6276eda2a33cffb4306471183ab79a332e698b8564f5169
google-protobuf (4.36.2-aarch64-linux-musl) sha256=0772c9dde1e6dce9a6b44d1d35c28c29adee99d9bd6aff5e21840daeb22161ce
google-protobuf (4.36.2-arm64-darwin) sha256=f70d810e6bbab9d8ad9c1d5fd685a21de4c10ad3ad32efb9455e17e5b1286917
google-protobuf (4.36.2-x64-mingw-ucrt) sha256=02b55d56c1c56a58a5e1cc3a7dda87e97343574ea5aa4f0e503379ba2647bbd1
google-protobuf (4.36.2-x86_64-darwin) sha256=8d1bd2a344f7f589703b202ad2ee4cca8ef731b4ced45782ef92d385967384ba
google-protobuf (4.36.2-x86_64-linux-gnu) sha256=d25a820873e423f1d209ae8c672a4a28b019e2429debd0901d26670a0b4e14db
google-protobuf (4.36.2-x86_64-linux-musl) sha256=0fa17686c896da96a60a705e56ddc7d4ff56413f52787c655bd4c9fb33ffe14f
Expand Down Expand Up @@ -412,6 +426,7 @@ CHECKSUMS
nokogiri (1.19.4-arm-linux-gnu) sha256=a301313e38bb065d68239e79734bcd6f56fb6efaacebde29e9abf2a4735340ca
nokogiri (1.19.4-arm-linux-musl) sha256=588923c101bcfa78869734d247d25b598674323e7f22474fc468f6e5647311eb
nokogiri (1.19.4-arm64-darwin) sha256=a46db9853286e6597b36ebc6953817d15acf3a299583eb3f89fdc6f91dd63527
nokogiri (1.19.4-x64-mingw-ucrt) sha256=051da97b8eccfdb5444fed40246a35e10d7298b9efe759b4cd25455ea04c587e
nokogiri (1.19.4-x86_64-darwin) sha256=7fd17057d3e1f00e9954a74b3cd76595d3d4a5ef233b7ed9599047c204f70551
nokogiri (1.19.4-x86_64-linux-gnu) sha256=379fae440b28915e3f19d752ce2dcf8465ed2b2fbefd2a7ca0dd497bc981a06a
nokogiri (1.19.4-x86_64-linux-musl) sha256=17dfb7c1fa194ae02fbf7c51a7afc8d278045ab3fdacfd86f91d02d7b274470b
Expand Down Expand Up @@ -446,6 +461,7 @@ CHECKSUMS
sass-embedded (1.106.1-arm-linux-gnueabihf) sha256=b1a68f22e0a04c897210435ea2153dfac9abc4cc57ded094054e985152b5944f
sass-embedded (1.106.1-arm-linux-musleabihf) sha256=a77ad844ed78ef8b1eea427238110be7ad0312b0c93a6b8afc5fd1db2664877c
sass-embedded (1.106.1-arm64-darwin) sha256=16a8481233712d3050741aae7e46cd35f2ae65493a197967af0a9efef7324f9f
sass-embedded (1.106.1-x64-mingw-ucrt) sha256=9426034480fc7153d8476936da84bb3e0bc830cce294d2db4ee71a9f03368af9
sass-embedded (1.106.1-x86_64-darwin) sha256=e993d6803ae98c80e6fa71e2ceea8a5699517ee6dd6b1162cff456e5a0ccb4a6
sass-embedded (1.106.1-x86_64-linux-gnu) sha256=2d0fb805f990a8328aa590cb201efbe21e26c98364107632e0ea5fc6b4a988c6
sass-embedded (1.106.1-x86_64-linux-musl) sha256=28e5a7a82abd5d68fe56df7d44e6706c66f4e27d05123dd36966dc441a7dee50
Expand Down
57 changes: 55 additions & 2 deletions assets/javascripts/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import { config } from "./config.js";
import { DB } from "./db.js";
import { DocPacks } from "./doc_packs.js";
import { Resume } from "./resume.js";
import { Router } from "./router.js";
import { AppServiceWorker } from "./serviceworker.js";
import { Settings } from "./settings.js";
Expand All @@ -13,6 +15,7 @@ import { SettingsStore } from "../lib/settings_store.js";
import { Events } from "../lib/events.js";
import { LocalStorageStore } from "../lib/local_storage_store.js";
import { $ } from "../lib/util.js";
/** @import { Context } from "../lib/page.js" */
import { Doc } from "../models/doc.js";
import { unsupportedBrowser } from "../templates/error_tmpl.js";
import { AppDocument } from "../views/layout/document.js";
Expand Down Expand Up @@ -81,6 +84,9 @@ export class App extends Events {
this.serviceWorker = new AppServiceWorker();
}
this.settings = new Settings();
this.docPacks = new DocPacks(this.localStorage);
this.docPacks.showIcons();
this.resume = new Resume(this.localStorage);
this.db = new DB();

this.settings.initLayout();
Expand Down Expand Up @@ -181,10 +187,10 @@ export class App extends Events {
delete this.DOC;
}

/** Boots with every doc in the manifest, enabled or not. */
/** Boots with every doc in the manifest and the added packs, enabled or not. */
async bootAll() {
const docs = this.settings.getDocs();
for (var doc of this.DOCS) {
for (var doc of this.DOCS.concat(this.docPacks.docs())) {
(docs.includes(/** @type {string} */ (doc.slug)) ? this.docs : this.disabledDocs).add(doc);
}
delete this.DOCS;
Expand All @@ -209,6 +215,7 @@ export class App extends Events {
this.initDoc(doc);
}
this.trigger("ready");
this.resumeReading();
this.router.start();
this.hideLoadingScreen();
setTimeout(() => {
Expand All @@ -219,6 +226,51 @@ export class App extends Events {
}, 50);
}

/**
* Reopens the page read last time when DevDocs starts on its home page, at
* the same scroll position, and from then on remembers the page being read.
* Runs before the router dispatches the first page.
*/
resumeReading() {
if (this.isSingleDoc()) {
return;
}
const content = this.document.content;

const last = this.settings.get("noResume")
? null
: this.resume.target(location, (slug) => !!this.docs.findBySlug(slug));
if (last) {
this.router.replacePath(last.path);
content.restoreScroll(last.scrollTop);
}

let reading = false;
this.router.on("after", (name, /** @type {Context} */ context) => {
reading = name === "entry" || name === "type";
if (reading) {
this.resume.remember(context.pathname + (context.hash ? `#${context.hash}` : ""));
}
});

// On phones the whole document scrolls, and its scroll events go to window.
const scroller =
content.scrollEl === document.scrollingElement || content.scrollEl === document.body
? window
: content.scrollEl;
let timer;
$.on(scroller, "scroll", () => {
if (!reading) {
return;
}
clearTimeout(timer);
timer = setTimeout(
() => this.resume.rememberScroll(content.scrollEl.scrollTop),
Resume.SCROLL_DELAY,
);
});
}

/**
* Adds a doc's types and entries to the search index.
*
Expand Down Expand Up @@ -400,6 +452,7 @@ export class App extends Events {
}
new News();
new Updates();
this.docPacks.refresh();
return (this.updateChecker = new UpdateChecker());
}

Expand Down
140 changes: 140 additions & 0 deletions assets/javascripts/app/categories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// @ts-check

/**
* The categories the docs are grouped in: in the sidebar, in the preferences'
* doc list, and as a search scope (type a category's name, then Tab).
*
* The built-in docs are sorted here, by slug without its version. A doc from
* a doc pack names its own category in its manifest. Anything else is "Other".
*/

/**
* @typedef {object} Category
* @property {string} id What a pack's manifest gives as `category`.
* @property {string} name
* @property {string[]} aliases Other names that scope the search to it.
*/

/** @type {Category[]} In the order they're shown. */
export const CATEGORIES = [
{ id: "languages", name: "Languages", aliases: ["language", "lang", "langs"] },
{ id: "web", name: "Web platform", aliases: ["web"] },
{ id: "frameworks", name: "Frameworks & platforms", aliases: ["framework", "platform", "platforms"] },
{ id: "libraries", name: "Libraries", aliases: ["library", "lib", "libs"] },
{ id: "testing", name: "Testing", aliases: ["test", "tests"] },
{ id: "tools", name: "Tools", aliases: ["tool"] },
{ id: "databases", name: "Databases", aliases: ["database", "db", "dbs"] },
{ id: "ai", name: "AI & data", aliases: ["ml", "data"] },
{ id: "services", name: "Servers & services", aliases: ["service", "server", "servers"] },
{ id: "other", name: "Other", aliases: [] },
];

export const DEFAULT_CATEGORY = "other";

/** The built-in docs of each category, by slug without the version. */
const BUILT_IN = {
languages:
"c clojure coffeescript coldfusion cpp crystal d dart elisp elixir erlang gnu_cobol gnu_fortran go " +
"groovy haskell haxe javascript julia kotlin latex lua markdown nim ocaml octave odin openjdk perl " +
"php pony python r ruby rust scala tcl_tk tcllib typescript typst zig",
web: "browser_support_tables css dom html http less sass svg web_extensions xslt_xpath",
frameworks:
"angular angularjs astro backbone bootstrap bottle bun cakephp codeigniter cordova cyclejs deno " +
"django django_rest_framework dojo drupal electron ember express falcon fastapi flask godot gtk hapi " +
"jquerymobile koa laravel love marionette meteor nextjs node padrino phalcon phoenix qt rails " +
"react react_native spring_boot svelte symfony tailwindcss vertx vue wagtail wordpress yii",
libraries:
"async axios bluebird click d3 date_fns eigen3 es_toolkit fluture handlebars htmx immutable jinja " +
"joi jquery jqueryui knockout leaflet liquid lit lodash maplibre_gl modernizr moment moment_timezone " +
"nokogiri opengl openlayers phaser point_cloud_library pug pygame q rack ramda reactivex " +
"react_bootstrap react_router redux relay requests requirejs rxjs sanctuary sanctuary_def " +
"sanctuary_type_classes socketio tanstack_query tanstack_router threejs tokio trio twig underscore " +
"valibot vue_router vueuse vuex vulkan werkzeug celery",
testing:
"chai codeception codeceptjs cypress enzyme jasmine jest minitest mocha phpunit playwright " +
"puppeteer pytest qunit sinon vitest",
tools:
"ansible babel bash bazel bower chef cmake composer docker esbuild eslint fish flow gcc git " +
"gnu_make gnuplot graphviz grunt hammerspoon homebrew i3 jekyll jq jsdoc kubectl man nix npm " +
"nushell opentofu powershell prettier saltstack terraform vagrant vite webpack yarn zsh",
databases:
"couchdb duckdb graphite influxdata mariadb mongoose opentsdb postgresql redis rethinkdb sequelize sqlite",
ai: "apache_pig matplotlib numpy pandas polars pytorch scikit_image scikit_learn statsmodels tensorflow tensorflow_cpp",
services: "apache_http_server haproxy kubernetes nginx nginx_lua_module rabbitmq varnish",
};

/** @type {Map<string, string>} Slug without version to category id. */
const CATEGORY_OF_SLUG = new Map(
Object.entries(BUILT_IN).flatMap(([id, slugs]) =>
slugs.split(" ").map((slug) => [slug, id]),
),
);

/** @type {Map<string, Category>} */
const BY_ID = new Map(CATEGORIES.map((category) => [category.id, category]));

/**
* @param {unknown} id
* @returns {id is string} Whether it names a category.
*/
export function isCategoryId(id) {
return typeof id === "string" && BY_ID.has(id);
}

/**
* @param {string} id
* @returns {Category}
*/
export function getCategory(id) {
return BY_ID.get(id) || /** @type {Category} */ (BY_ID.get(DEFAULT_CATEGORY));
}

/**
* The category of a doc: its own for a pack doc, otherwise the built-in one.
*
* @param {{ slug: string, category?: unknown, pack?: unknown }} doc
* @returns {string} A category id.
*/
export function categoryOf(doc) {
if (isCategoryId(doc.category)) {
return doc.category;
}
if (doc.pack) {
return DEFAULT_CATEGORY;
}
return CATEGORY_OF_SLUG.get(doc.slug.split("~")[0]) || DEFAULT_CATEGORY;
}

/**
* The category someone means by what they typed, e.g. "db" or "Databases".
*
* @param {string} query
* @returns {Category | undefined}
*/
export function findCategory(query) {
const text = query.trim().toLowerCase();
if (!text) {
return undefined;
}
return CATEGORIES.find(
(category) =>
category.id === text ||
category.name.toLowerCase() === text ||
category.aliases.includes(text),
);
}

/**
* Groups docs by category, in the categories' order, keeping the docs' order
* within each and leaving out empty categories.
*
* @template {{ category: string }} D
* @param {D[]} docs
* @returns {{ category: Category, docs: D[] }[]}
*/
export function groupByCategory(docs) {
return CATEGORIES.map((category) => ({
category,
docs: docs.filter((doc) => doc.category === category.id),
})).filter((group) => group.docs.length > 0);
}
Loading