Skip to content

Commit aeb9492

Browse files
authored
Fixed stay no page functionality when using mike's canonical versioning (#7559)
1 parent 50a15be commit aeb9492

File tree

3 files changed

+150
-13
lines changed

3 files changed

+150
-13
lines changed

docs/setup/setting-up-versioning.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ MkDocs implements this behavior by default, but there are a few caveats:
6464
- the [`site_url`][mkdocs.site_url] must be set correctly in `mkdocs.yml`.
6565
See the ["Publishing a new version"](#publishing-a-new-version) section for
6666
an example.
67-
- you must be viewing the site at that URL (and not locally, for example).
6867
- the redirect happens via JavaScript and there is no way to know which page you
6968
will be redirected to ahead of time.
7069

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { Sitemap } from "../../sitemap"
2+
3+
/** See docstring for `selectedVersionCorrespondingURL` for the meaning of these fields. */
4+
type CorrespondingURLParams = {
5+
selectedVersionSitemap: Sitemap
6+
selectedVersionBaseURL: URL
7+
currentLocation: URL
8+
currentBaseURL: string
9+
}
10+
11+
/**
12+
* Choose a URL to navigate to when the user chooses a version in the version
13+
* selector.
14+
*
15+
* The parameters in `params` are named as follows, in order to make it clearer
16+
* which parameter means what when invoking the function:
17+
*
18+
* - selectedVersionSitemap: Sitemap - as obtained by fetchSitemap from `${selectedVersionBaseURL}/sitemap.xml`
19+
*
20+
* - selectedVersionBaseURL: URL - usually `${currentBaseURL}/../selectedVersion`
21+
*
22+
* - currentLocation: URL - current web browser location
23+
*
24+
* - currentBaseURL: string - as obtained from `config.base`
25+
*
26+
* @param params - arguments with the meanings explained above.
27+
* @returns the URL to navigate to or null if we can't be sure that the
28+
* corresponding page to the current page exists in the selected version
29+
*/
30+
export function selectedVersionCorrespondingURL(
31+
params: CorrespondingURLParams
32+
): URL | undefined {
33+
const {selectedVersionSitemap,
34+
selectedVersionBaseURL,
35+
currentLocation,
36+
currentBaseURL} = params
37+
const current_path = safeURLParse(currentBaseURL)?.pathname
38+
if (current_path === undefined) {
39+
return
40+
}
41+
const currentRelativePath = stripPrefix(currentLocation.pathname, current_path)
42+
if (currentRelativePath === undefined) {
43+
return
44+
}
45+
const sitemapCommonPrefix = shortestCommonPrefix(selectedVersionSitemap.keys())
46+
if (!selectedVersionSitemap.has(sitemapCommonPrefix)) {
47+
// We could also check that `commonSitemapPrefix` ends in the canonical version,
48+
// similarly to https://github.com/squidfunk/mkdocs-material/pull/7227. However,
49+
// I don't believe that Mike/MkDocs ever generate sitemaps where it would matter
50+
return
51+
}
52+
53+
const potentialSitemapURL = safeURLParse(currentRelativePath, sitemapCommonPrefix)
54+
if (!potentialSitemapURL || !selectedVersionSitemap.has(potentialSitemapURL.href)) {
55+
return
56+
}
57+
58+
const result = safeURLParse(currentRelativePath, selectedVersionBaseURL)
59+
if (!result) {
60+
return
61+
}
62+
result.hash = currentLocation.hash
63+
result.search = currentLocation.search
64+
return result
65+
}
66+
67+
/**
68+
* A version of `new URL` that never throws. A polyfill for URL.parse() which is
69+
* not yet ubuquitous.
70+
*
71+
* @param url - passed to `new URL` constructor
72+
* @param base - passed to `new URL` constructor
73+
*
74+
* @returns `new URL(url, base)` or undefined if the URL is invalid.
75+
*/
76+
function safeURLParse(url: string|URL, base?: string|URL): URL | undefined {
77+
try {
78+
return new URL(url, base)
79+
} catch {
80+
return
81+
}
82+
}
83+
84+
// Basic string manipulation
85+
86+
/** Strip a given prefix from a function
87+
*
88+
* @param s - string
89+
* @param prefix - prefix to strip
90+
*
91+
* @returns either the string with the prefix stripped or undefined if the
92+
* string did not begin with the prefix.
93+
*/
94+
export function stripPrefix(s: string, prefix: string): string | undefined {
95+
if (s.startsWith(prefix)) {
96+
return s.slice(prefix.length)
97+
}
98+
return undefined
99+
}
100+
101+
/** Find the length of the longest common prefix of two strings
102+
*
103+
* @param s1 - first string
104+
* @param s2 - second string
105+
*
106+
* @returns - the length of the longest common prefix of the two strings.
107+
*/
108+
function commonPrefixLen(s1: string, s2: string): number {
109+
const max = Math.min(s1.length, s2.length)
110+
let result
111+
for (result = 0; result < max; ++result) {
112+
if (s1[result] !== s2[result]) {
113+
break
114+
}
115+
}
116+
return result
117+
}
118+
119+
/** Find the longest common prefix of any number of strings
120+
*
121+
* @param strs - an iterable of strings
122+
*
123+
* @returns the longest common prefix of all the strings
124+
*/
125+
export function shortestCommonPrefix(strs: Iterable<string>): string {
126+
let result // Undefined if no iterations happened
127+
for (const s of strs) {
128+
if (result === undefined) {
129+
result = s
130+
} else {
131+
result = result.slice(0, commonPrefixLen(result, s))
132+
}
133+
}
134+
return result ?? ""
135+
}

src/templates/assets/javascripts/integrations/version/index.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ import {
4848

4949
import { fetchSitemap } from "../sitemap"
5050

51+
import { selectedVersionCorrespondingURL } from "./findurl"
52+
5153
/* ----------------------------------------------------------------------------
5254
* Helper types
5355
* ------------------------------------------------------------------------- */
@@ -122,22 +124,23 @@ export function setupVersionSelector(
122124
return EMPTY
123125
}
124126
ev.preventDefault()
125-
return of(url)
127+
return of(new URL(url))
126128
}
127129
}
128130
return EMPTY
129131
}),
130-
switchMap(url => {
131-
return fetchSitemap(new URL(url))
132-
.pipe(
133-
map(sitemap => {
134-
const location = getLocation()
135-
const path = location.href.replace(config.base, url)
136-
return sitemap.has(path.split("#")[0])
137-
? new URL(path)
138-
: new URL(url)
139-
})
140-
)
132+
switchMap(selectedVersionBaseURL => {
133+
return fetchSitemap(selectedVersionBaseURL).pipe(
134+
map(
135+
sitemap =>
136+
selectedVersionCorrespondingURL({
137+
selectedVersionSitemap: sitemap,
138+
selectedVersionBaseURL,
139+
currentLocation: getLocation(),
140+
currentBaseURL: config.base
141+
}) ?? selectedVersionBaseURL,
142+
),
143+
)
141144
})
142145
)
143146
)

0 commit comments

Comments
 (0)