1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
use *;
/// Reduces a requested route to its canonical form.
///
/// The normalisation is deliberately a *shape* fix, never a *lookup* fix:
/// it only removes the ways a URL can spell a route that `build.rs`
/// already generated, and never guesses at a different page. Every
/// rewrite is one a reader could reasonably have typed by hand:
///
/// - repeated separators collapse (`/zh//a` → `/zh/a`), so a link
/// assembled from a prefix plus a path that already starts with a
/// separator still resolves;
/// - a run of trailing separators collapses to the single one a
/// directory route ends with (`/zh/guide//` → `/zh/guide/`), so a
/// directory route survives a link helper that appends a slash while
/// `/zh/guide` — which names no page at all — stays distinguishable;
/// - the root stays the root, because trimming its only separator would
/// leave a string that matches nothing;
/// - everything else is preserved verbatim, including case and the
/// `.html` suffix — [`RouteSite::find_page`] owns the decision of which
/// of those a page can be addressed by, so this function only
/// guarantees the separator shape matches the generated routes.
///
/// A request with no leading separator (`zh/guide/`) gains one, because
/// every generated route is absolute.
///
/// # Arguments
///
/// - `&str` - The route as it arrived from the hash, after
/// [`parse_route`] removed any in-page anchor.
///
/// # Returns
///
/// - `String` - The canonical form of the route.
/// Returns the route with the `.html` leaf suffix removed, or the route
/// unchanged when it does not carry the suffix.
///
/// The locale prefix is never touched: `route_stem("/zh/a/b.html")` is
/// `/zh/a/b`, so the caller can re-append the suffix when it looks the
/// page back up.
///
/// # Arguments
///
/// - `&str` - The route to strip.
///
/// # Returns
///
/// - `String` - The route without its `.html` suffix.
/// Returns the candidate spellings a request may stand for, in
/// descending priority.
///
/// This is the single source of truth for route spelling:
/// [`RouteSite::find_page`] walks it, so the ambiguity rule — prefer the
/// exact route `build.rs` emitted, never merge two distinct pages — is
/// expressed once instead of at every call site.
///
/// Every candidate is the same *path* written with the suffix question
/// answered differently: bare (`/zh/a/b`), as a directory
/// (`/zh/a/b/`), and as a file (`/zh/a/b.html`). That is the whole
/// vocabulary `build.rs` emits routes from — a markdown leaf becomes one
/// form, a `README.md` becomes another — so the three spellings cover
/// every way a link can name an existing page and nothing else.
///
/// It never merges two pages that both exist. The exact route is
/// consulted before any candidate is tried, so a site carrying both
/// `/a.html` and `/a/` resolves each to itself, and a request for a page
/// that does not exist at any spelling still returns nothing.
///
/// # Arguments
///
/// - `&str` - The normalised request path.
///
/// # Returns
///
/// - `Vec<String>` - The spellings to try, most specific first, each
/// present at most once.
/// Enumerates the misspellings of a served route that a link author is
/// likely to produce, so the route audit can check each one against the
/// site.
///
/// The list is exhaustive over *shape* mistakes rather than over content,
/// and every entry is derived mechanically from `route`, so running it
/// over every generated page stays cheap and misses no shape:
///
/// - the route itself (the control);
/// - the `.html` suffix dropped (`/zh/a/b.html` → `/zh/a/b`);
/// - the suffix dropped and a separator kept (`/zh/a/b.html` →
/// `/zh/a/b/`);
/// - a separator appended to a leaf route (`/zh/a/b.html` →
/// `/zh/a/b.html/`), the trailing slash a link helper adds;
/// - the `.html` suffix doubled onto a leaf route (`/zh/a/b.html` →
/// `/zh/a/b.html.html`), the same mistake one level down;
/// - every separator doubled (`/zh/a/b.html` → `//zh//a//b.html`);
/// - a separator prepended (`//zh/a/b.html`).
///
/// Every one of these names the same page as the control, so
/// [`RouteSite::find_page`] is expected to resolve all of them. The
/// locale prefix is deliberately not varied here: dropping it is a
/// question about *which page exists*, not about spelling, and
/// [`route_without_locale_prefix`] is the probe for it.
///
/// # Arguments
///
/// - `&str` - A route the site serves.
///
/// # Returns
///
/// - `Vec<String>` - The variants to feed through
/// [`RouteSite::find_page`], in a stable order, each present once.
/// Returns the route with its locale prefix removed.
///
/// Locale prefixes always end in a separator (`/zh/`), so matching one
/// is inherently a segment-boundary match: `/zh/guide/a` yields
/// `/guide/a` while `/zh-legacy/guide/a` is returned unchanged. That is
/// the same boundary rule [`RouteSite::locale_of`] relies on when
/// deciding which locale owns a route, kept in one place so the two
/// cannot disagree.
///
/// Whether the result resolves is a question about the site's contents,
/// not about spelling: the root locale may well serve the page, and when
/// it does not, the link is wrong rather than the router.
///
/// # Arguments
///
/// - `&str` - A route the site serves.
/// - `&str` - The prefix of the locale that owns it.
///
/// # Returns
///
/// - `String` - The route as it would be written without the prefix, or
/// `route` unchanged when it does not carry `locale_prefix`.
/// Returns the root route every request collapses onto when it carries
/// no path at all.
///
/// Exposed as a function because a `pub const` is never public in this
/// project, and a test comparing routes needs the root spelled the same
/// way the normaliser spells it.
///
/// # Returns
///
/// - `&'static str` - The root route.
/// Returns how many spelling variants the audit enumerates per route.
///
/// # Returns
///
/// - `usize` - The variant count, or zero when the route has no
/// distinct spelling to try.