@uniweb/core 0.8.5 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/route-match.js +33 -3
- package/src/website.js +5 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/core",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.6",
|
|
4
4
|
"description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"vitest": "^4.1.7"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@uniweb/
|
|
39
|
-
"@uniweb/
|
|
38
|
+
"@uniweb/semantic-parser": "^1.2.2",
|
|
39
|
+
"@uniweb/theming": "^0.1.15"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"test": "vitest run"
|
package/src/route-match.js
CHANGED
|
@@ -100,6 +100,35 @@ export function routePatternToRegex(pattern) {
|
|
|
100
100
|
return { regex: new RegExp(`^${source}$`), paramNames }
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Decode a value that arrived from a URL, falling back to the raw input.
|
|
105
|
+
*
|
|
106
|
+
* Guarded rather than bare, for two independent reasons:
|
|
107
|
+
*
|
|
108
|
+
* A `%` that is not an escape is legitimate content — `/100%-Guide` authored by
|
|
109
|
+
* hand, or a value that has already been decoded once — and `decodeURIComponent`
|
|
110
|
+
* throws `URIError` on those. Falling back to the input keeps such a route
|
|
111
|
+
* matching exactly as well as it did before.
|
|
112
|
+
*
|
|
113
|
+
* And the input is attacker-controlled: `/blog/%zz` is a URL anyone can paste or
|
|
114
|
+
* link. This module is called by hosts that resolve a path to a page *per
|
|
115
|
+
* request*, where a throw out of the matcher is a visitor-triggerable 500 rather
|
|
116
|
+
* than a client-side error. A malformed escape is not a reason to lose an
|
|
117
|
+
* otherwise-good match, so the fallback is the raw capture rather than a miss —
|
|
118
|
+
* a route miss would turn a typo'd escape into a 404 on a page that exists.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} value
|
|
121
|
+
* @returns {string}
|
|
122
|
+
*/
|
|
123
|
+
export function decodeRouteValue(value) {
|
|
124
|
+
if (typeof value !== 'string' || !value.includes('%')) return value
|
|
125
|
+
try {
|
|
126
|
+
return decodeURIComponent(value)
|
|
127
|
+
} catch {
|
|
128
|
+
return value
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
103
132
|
/**
|
|
104
133
|
* Match a concrete path against a route pattern.
|
|
105
134
|
*
|
|
@@ -109,8 +138,9 @@ export function routePatternToRegex(pattern) {
|
|
|
109
138
|
* matchDynamicRoute('/blog/:slug', '/blog/') // → null (a param is non-empty)
|
|
110
139
|
* ```
|
|
111
140
|
*
|
|
112
|
-
* Captured values are
|
|
113
|
-
*
|
|
141
|
+
* Captured values are decoded, so a path carries percent encoding and the param
|
|
142
|
+
* does not. A malformed escape falls back to the raw capture rather than
|
|
143
|
+
* throwing — see `decodeRouteValue`. This function does not throw.
|
|
114
144
|
*
|
|
115
145
|
* @param {string} pattern - Route pattern with `:param` placeholders
|
|
116
146
|
* @param {string} path - Concrete path to match
|
|
@@ -123,7 +153,7 @@ export function matchDynamicRoute(pattern, path) {
|
|
|
123
153
|
|
|
124
154
|
const params = {}
|
|
125
155
|
paramNames.forEach((name, i) => {
|
|
126
|
-
params[name] =
|
|
156
|
+
params[name] = decodeRouteValue(match[i + 1])
|
|
127
157
|
})
|
|
128
158
|
return { params }
|
|
129
159
|
}
|
package/src/website.js
CHANGED
|
@@ -11,28 +11,7 @@ import FetcherDispatcher from './fetcher-dispatcher.js'
|
|
|
11
11
|
import ObservableState from './observable-state.js'
|
|
12
12
|
import { normalizeSeo } from './seo.js'
|
|
13
13
|
import { resolveDefaultLocale } from './locale-config.js'
|
|
14
|
-
import { matchDynamicRoute } from './route-match.js'
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Decode a route that arrived from a URL.
|
|
18
|
-
*
|
|
19
|
-
* `location.pathname` is percent-encoded per RFC 3986, while page routes and
|
|
20
|
-
* `i18n.routeTranslations` are authored as plain text — so the two are only
|
|
21
|
-
* comparable once the incoming side is decoded.
|
|
22
|
-
*
|
|
23
|
-
* Guarded rather than bare: a route may legitimately contain a `%` that is not
|
|
24
|
-
* an escape (`/100%-Guide` authored by hand, or a value already decoded once),
|
|
25
|
-
* and `decodeURIComponent` throws `URIError` on those. Falling back to the input
|
|
26
|
-
* keeps a malformed route matching exactly as well as it did before.
|
|
27
|
-
*/
|
|
28
|
-
function decodeRoute(route) {
|
|
29
|
-
if (typeof route !== 'string' || !route.includes('%')) return route
|
|
30
|
-
try {
|
|
31
|
-
return decodeURIComponent(route)
|
|
32
|
-
} catch {
|
|
33
|
-
return route
|
|
34
|
-
}
|
|
35
|
-
}
|
|
14
|
+
import { matchDynamicRoute, decodeRouteValue } from './route-match.js'
|
|
36
15
|
|
|
37
16
|
/**
|
|
38
17
|
* Website — orchestration root for a single site instance.
|
|
@@ -354,9 +333,9 @@ export default class Website {
|
|
|
354
333
|
// Without it translateRoute() emits a URL this method cannot read back —
|
|
355
334
|
// every translated route carrying a non-ASCII character or an apostrophe
|
|
356
335
|
// resolved to nothing and rendered the 404 page, while the SAME route with
|
|
357
|
-
// an all-ASCII slug worked.
|
|
358
|
-
//
|
|
359
|
-
const route =
|
|
336
|
+
// an all-ASCII slug worked. The helper is shared with the captured-param
|
|
337
|
+
// decode in `route-match.js`, which needs the identical guard.
|
|
338
|
+
const route = decodeRouteValue(displayRoute)
|
|
360
339
|
|
|
361
340
|
// Exact match
|
|
362
341
|
const canonical = entry.reverse.get(route)
|
|
@@ -452,7 +431,7 @@ export default class Website {
|
|
|
452
431
|
// Decode before ANY comparison: a published payload can hold translated
|
|
453
432
|
// display routes verbatim, so the direct match below needs the same plain
|
|
454
433
|
// text form the reverse-translate path does.
|
|
455
|
-
let stripped =
|
|
434
|
+
let stripped = decodeRouteValue(route)
|
|
456
435
|
if (this.activeLocale && this.activeLocale !== this.defaultLocale) {
|
|
457
436
|
const prefix = `/${this.activeLocale}`
|
|
458
437
|
if (stripped === prefix || stripped === `${prefix}/`) {
|