diff --git a/assets/javascripts/app/app.js b/assets/javascripts/app/app.js
index 3ced2c403c..f05b963dd5 100644
--- a/assets/javascripts/app/app.js
+++ b/assets/javascripts/app/app.js
@@ -117,17 +117,18 @@ class App extends Events {
delete this.DOC;
}
- bootAll() {
+ async bootAll() {
const docs = this.settings.getDocs();
for (var doc of this.DOCS) {
(docs.includes(doc.slug) ? this.docs : this.disabledDocs).add(doc);
}
+ delete this.DOCS;
this.migrateDocs();
+ await this.migrateToLatestVersions();
this.docs.load(this.start.bind(this), this.onBootError.bind(this), {
readCache: true,
writeCache: true,
});
- delete this.DOCS;
}
start() {
@@ -190,6 +191,86 @@ class App extends Events {
}
}
+ // With the "latest version" preference enabled, replace the enabled docs for
+ // which a newer version is available with that version.
+ async migrateToLatestVersions() {
+ if (!this.settings.get("autoLatestVersion")) {
+ return;
+ }
+
+ const allDocs = this.docs.all().concat(this.disabledDocs.all());
+ // The same version can supersede several enabled docs, so it's only loaded
+ // once, e.g. when both CMake 3.9 and CMake 3.10 are enabled.
+ const migrations = new Map();
+
+ for (const outdated of this.docs.all()) {
+ const latest = outdated.findLatestVersion(allDocs);
+ if (latest === outdated) {
+ continue;
+ }
+ if (!migrations.has(latest)) {
+ migrations.set(latest, []);
+ }
+ migrations.get(latest).push(outdated);
+ }
+
+ const loaded = await this.loadLatestVersions([...migrations.keys()]);
+ let needsSaving;
+
+ for (const [latest, outdatedDocs] of migrations) {
+ if (!loaded.has(latest)) {
+ continue;
+ }
+ for (const outdated of outdatedDocs) {
+ this.docs.remove(outdated);
+ this.disabledDocs.add(outdated);
+ }
+ if (!this.docs.contains(latest)) {
+ this.disabledDocs.remove(latest);
+ this.docs.add(latest);
+ }
+ needsSaving = true;
+ }
+
+ if (needsSaving) {
+ this.docs.sort();
+ this.saveDocs();
+ }
+ }
+
+ // Saving drops the offline data of the docs that are disabled, so the index
+ // of their latest version has to load before they are replaced. Loads no
+ // more docs at once than Docs#load does.
+ async loadLatestVersions(docs) {
+ const loaded = new Set();
+ let i = 0;
+
+ const next = async () => {
+ while (i < docs.length) {
+ const doc = docs[i++];
+ const success = await new Promise((resolve) =>
+ doc.load(
+ () => resolve(true),
+ () => resolve(false),
+ { readCache: true, writeCache: true },
+ ),
+ );
+ if (success) {
+ loaded.add(doc);
+ }
+ }
+ };
+
+ await Promise.all(
+ Array.from(
+ { length: Math.min(docs.length, app.collections.Docs.CONCURRENCY) },
+ next,
+ ),
+ );
+
+ return loaded;
+ }
+
enableDoc(doc, _onSuccess, onError) {
if (this.docs.contains(doc)) {
return;
diff --git a/assets/javascripts/app/settings.js b/assets/javascripts/app/settings.js
index 617830ecc9..113005a65d 100644
--- a/assets/javascripts/app/settings.js
+++ b/assets/javascripts/app/settings.js
@@ -14,6 +14,7 @@ app.Settings = class Settings {
"tips",
"noAutofocus",
"autoInstall",
+ "autoLatestVersion",
"spaceScroll",
"spaceTimeout",
"noDocSpecificIcon",
@@ -40,6 +41,7 @@ app.Settings = class Settings {
spaceScroll: 1,
spaceTimeout: 0.5,
noDocSpecificIcon: false,
+ autoLatestVersion: false,
};
constructor() {
diff --git a/assets/javascripts/models/doc.js b/assets/javascripts/models/doc.js
index 0a5c815a85..990c4046df 100644
--- a/assets/javascripts/models/doc.js
+++ b/assets/javascripts/models/doc.js
@@ -1,6 +1,8 @@
app.models.Doc = class Doc extends app.Model {
// Attributes: name, slug, type, version, release, db_size, mtime, links
+ static NUMBERED_VERSION_RGX = /^\d+(\.\d+)*$/;
+
constructor() {
super(...arguments);
this.reset(this);
@@ -192,6 +194,53 @@ app.models.Doc = class Doc extends app.Model {
);
}
+ // Whether the doc holds a numbered version of its documentation (e.g. "3.9"),
+ // as opposed to a variant (e.g. "10 LTS" or "Python"), which can't be
+ // ordered. An empty version means the doc holds the latest version
+ // (e.g. `angular`), whereas docs without a version aren't versioned at all.
+ hasNumberedVersion() {
+ return (
+ this.version === "" || Doc.NUMBERED_VERSION_RGX.test(this.version || "")
+ );
+ }
+
+ // Compares numbered versions (e.g. "3.9" is older than "3.12").
+ // An empty version means the latest version and is newer than any other.
+ isNewerVersionThan(other) {
+ if (this.version === "" || other.version === "") {
+ return this.version === "" && other.version !== "";
+ }
+ const version = this.version.split(".");
+ const otherVersion = other.version.split(".");
+ for (let i = 0; i < Math.max(version.length, otherVersion.length); i++) {
+ const diff =
+ (parseInt(version[i], 10) || 0) - (parseInt(otherVersion[i], 10) || 0);
+ if (diff !== 0) {
+ return diff > 0;
+ }
+ }
+ return false;
+ }
+
+ // Returns the doc holding the latest version of the same documentation among
+ // `docs`, or the doc itself when there is none.
+ findLatestVersion(docs) {
+ let latest = this;
+ if (!this.hasNumberedVersion()) {
+ return latest;
+ }
+ for (var doc of docs) {
+ if (
+ doc.name === this.name &&
+ doc.hasNumberedVersion() &&
+ doc.isNewerVersionThan(latest)
+ ) {
+ latest = doc;
+ }
+ }
+ return latest;
+ }
+
isOutdated(status) {
if (!status) {
return false;
diff --git a/assets/javascripts/templates/pages/settings_tmpl.js b/assets/javascripts/templates/pages/settings_tmpl.js
index cfd30de1b2..f0f84bfa19 100644
--- a/assets/javascripts/templates/pages/settings_tmpl.js
+++ b/assets/javascripts/templates/pages/settings_tmpl.js
@@ -60,6 +60,12 @@ app.templates.settingsPage = (settings) => `\
}>Automatically download documentation for offline use
Only enable this when bandwidth isn't a concern to you.
+