@astrojs/starlight 0.5.6 → 0.6.0
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/404.astro +17 -17
- package/CHANGELOG.md +40 -2
- package/components/CallToAction.astro +32 -35
- package/components/ContentPanel.astro +18 -18
- package/components/EditLink.astro +23 -23
- package/components/FallbackContentNotice.astro +16 -18
- package/components/Footer.astro +28 -34
- package/components/HeadSEO.astro +69 -75
- package/components/Header.astro +47 -51
- package/components/Hero.astro +108 -121
- package/components/Icons.ts +73 -71
- package/components/LanguageSelect.astro +31 -31
- package/components/LastUpdated.astro +16 -18
- package/components/MarkdownContent.astro +105 -117
- package/components/MobileMenuToggle.astro +80 -80
- package/components/PrevNextLinks.astro +60 -60
- package/components/RightSidebar.astro +17 -17
- package/components/RightSidebarPanel.astro +41 -41
- package/components/Search.astro +293 -306
- package/components/Select.astro +64 -65
- package/components/Sidebar.astro +23 -23
- package/components/SidebarSublist.astro +96 -95
- package/components/SiteTitle.astro +65 -63
- package/components/SkipLink.astro +17 -17
- package/components/SocialIcons.astro +36 -34
- package/components/TableOfContents/MobileTableOfContents.astro +124 -124
- package/components/TableOfContents/TableOfContentsList.astro +64 -69
- package/components/TableOfContents/generateToC.ts +41 -43
- package/components/TableOfContents/starlight-toc.ts +84 -90
- package/components/TableOfContents.astro +8 -8
- package/components/ThemeProvider.astro +28 -32
- package/components/ThemeSelect.astro +66 -71
- package/global.d.ts +3 -3
- package/index.astro +1 -1
- package/index.ts +52 -59
- package/integrations/asides.ts +109 -111
- package/integrations/sitemap.ts +10 -13
- package/integrations/virtual-user-config.ts +37 -37
- package/layout/Page.astro +84 -72
- package/layout/PageFrame.astro +67 -69
- package/layout/TwoColumnContent.astro +40 -42
- package/package.json +2 -2
- package/schema.ts +126 -113
- package/schemas/favicon.ts +40 -0
- package/schemas/head.ts +12 -23
- package/schemas/i18n.ts +146 -160
- package/schemas/logo.ts +22 -22
- package/schemas/prevNextLink.ts +14 -14
- package/schemas/tableOfContents.ts +14 -18
- package/style/asides.css +27 -27
- package/style/props.css +168 -172
- package/style/reset.css +13 -13
- package/style/shiki.css +11 -11
- package/style/util.css +30 -30
- package/translations/fr.json +21 -21
- package/translations/index.ts +4 -4
- package/translations/it.json +20 -20
- package/translations/tr.json +1 -1
- package/translations/zh.json +0 -1
- package/user-components/Card.astro +50 -54
- package/user-components/CardGrid.astro +21 -21
- package/user-components/Icon.astro +19 -21
- package/user-components/TabItem.astro +3 -3
- package/user-components/Tabs.astro +113 -117
- package/user-components/rehype-tabs.ts +72 -72
- package/utils/base.ts +6 -6
- package/utils/git.ts +55 -55
- package/utils/head.ts +56 -54
- package/utils/i18n.ts +3 -3
- package/utils/localizedUrl.ts +25 -25
- package/utils/navigation.ts +225 -203
- package/utils/routing.ts +74 -85
- package/utils/slugs.ts +41 -51
- package/utils/translations.ts +26 -32
- package/utils/user-config.ts +278 -278
- package/virtual.d.ts +8 -8
|
@@ -3,43 +3,43 @@ import { rehype } from 'rehype';
|
|
|
3
3
|
import { CONTINUE, SKIP, visit } from 'unist-util-visit';
|
|
4
4
|
|
|
5
5
|
interface Panel {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
panelId: string;
|
|
7
|
+
tabId: string;
|
|
8
|
+
label: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
declare module 'vfile' {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
interface DataMap {
|
|
13
|
+
panels: Panel[];
|
|
14
|
+
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export const TabItemTagname = 'starlight-tab-item';
|
|
18
18
|
|
|
19
19
|
// https://github.com/adobe/react-spectrum/blob/99ca82e87ba2d7fdd54f5b49326fd242320b4b51/packages/%40react-aria/focus/src/FocusScope.tsx#L256-L275
|
|
20
20
|
const focusableElementSelectors = [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
21
|
+
'input:not([disabled]):not([type=hidden])',
|
|
22
|
+
'select:not([disabled])',
|
|
23
|
+
'textarea:not([disabled])',
|
|
24
|
+
'button:not([disabled])',
|
|
25
|
+
'a[href]',
|
|
26
|
+
'area[href]',
|
|
27
|
+
'summary',
|
|
28
|
+
'iframe',
|
|
29
|
+
'object',
|
|
30
|
+
'embed',
|
|
31
|
+
'audio[controls]',
|
|
32
|
+
'video[controls]',
|
|
33
|
+
'[contenteditable]',
|
|
34
|
+
'[tabindex]:not([disabled])',
|
|
35
35
|
]
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
.map((selector) => `${selector}:not([hidden]):not([tabindex="-1"])`)
|
|
37
|
+
.join(',');
|
|
38
38
|
|
|
39
39
|
let count = 0;
|
|
40
40
|
const getIDs = () => {
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const id = count++;
|
|
42
|
+
return { panelId: 'tab-panel-' + id, tabId: 'tab-' + id };
|
|
43
43
|
};
|
|
44
44
|
|
|
45
45
|
/**
|
|
@@ -48,51 +48,51 @@ const getIDs = () => {
|
|
|
48
48
|
* attributes.
|
|
49
49
|
*/
|
|
50
50
|
const tabsProcessor = rehype()
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
51
|
+
.data('settings', { fragment: true })
|
|
52
|
+
.use(function tabs() {
|
|
53
|
+
return (tree, file) => {
|
|
54
|
+
file.data.panels = [];
|
|
55
|
+
let isFirst = true;
|
|
56
|
+
visit(tree, 'element', (node) => {
|
|
57
|
+
if (node.tagName !== TabItemTagname || !node.properties) {
|
|
58
|
+
return CONTINUE;
|
|
59
|
+
}
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
const { dataLabel } = node.properties;
|
|
62
|
+
const ids = getIDs();
|
|
63
|
+
file.data.panels?.push({
|
|
64
|
+
...ids,
|
|
65
|
+
label: String(dataLabel),
|
|
66
|
+
});
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
// Remove `<TabItem>` props
|
|
69
|
+
delete node.properties.dataLabel;
|
|
70
|
+
// Turn into `<section>` with required attributes
|
|
71
|
+
node.tagName = 'section';
|
|
72
|
+
node.properties.id = ids.panelId;
|
|
73
|
+
node.properties['aria-labelledby'] = ids.tabId;
|
|
74
|
+
node.properties.role = 'tabpanel';
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// Hide all panels except the first
|
|
84
|
-
// TODO: make initially visible tab configurable
|
|
85
|
-
if (isFirst) {
|
|
86
|
-
isFirst = false;
|
|
87
|
-
} else {
|
|
88
|
-
node.properties.hidden = true;
|
|
89
|
-
}
|
|
76
|
+
const focusableChild = select(focusableElementSelectors, node);
|
|
77
|
+
// If the panel does not contain any focusable elements, include it in
|
|
78
|
+
// the tab sequence of the page.
|
|
79
|
+
if (!focusableChild) {
|
|
80
|
+
node.properties.tabindex = 0;
|
|
81
|
+
}
|
|
90
82
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
83
|
+
// Hide all panels except the first
|
|
84
|
+
// TODO: make initially visible tab configurable
|
|
85
|
+
if (isFirst) {
|
|
86
|
+
isFirst = false;
|
|
87
|
+
} else {
|
|
88
|
+
node.properties.hidden = true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Skip over the tab panel’s children.
|
|
92
|
+
return SKIP;
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
98
|
* Process tab panel items to extract data for the tab links and format
|
|
@@ -100,11 +100,11 @@ const tabsProcessor = rehype()
|
|
|
100
100
|
* @param html Inner HTML passed to the `<Tabs>` component.
|
|
101
101
|
*/
|
|
102
102
|
export const processPanels = (html: string) => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
const file = tabsProcessor.processSync({ value: html });
|
|
104
|
+
return {
|
|
105
|
+
/** Data for each tab panel. */
|
|
106
|
+
panels: file.data.panels,
|
|
107
|
+
/** Processed HTML for the tab panels. */
|
|
108
|
+
html: file.toString(),
|
|
109
|
+
};
|
|
110
110
|
};
|
package/utils/base.ts
CHANGED
|
@@ -2,19 +2,19 @@ const base = stripTrailingSlash(import.meta.env.BASE_URL);
|
|
|
2
2
|
|
|
3
3
|
/** Get the a root-relative URL path with the site’s `base` prefixed. */
|
|
4
4
|
export function pathWithBase(path: string) {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
path = stripLeadingSlash(stripTrailingSlash(path));
|
|
6
|
+
return path ? base + '/' + path + '/' : base + '/';
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
/** Get the a root-relative file URL path with the site’s `base` prefixed. */
|
|
10
10
|
export function fileWithBase(path: string) {
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
path = stripLeadingSlash(stripTrailingSlash(path));
|
|
12
|
+
return path ? base + '/' + path : base;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function stripLeadingSlash(path: string) {
|
|
16
|
-
|
|
16
|
+
return path.replace(/^\//, '');
|
|
17
17
|
}
|
|
18
18
|
function stripTrailingSlash(path: string) {
|
|
19
|
-
|
|
19
|
+
return path.replace(/\/$/, '');
|
|
20
20
|
}
|
package/utils/git.ts
CHANGED
|
@@ -28,69 +28,69 @@ class FileNotTrackedError extends Error {}
|
|
|
28
28
|
* unexpected text.
|
|
29
29
|
*/
|
|
30
30
|
export function getFileCommitDate(
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
file: string,
|
|
32
|
+
age: 'oldest' | 'newest' = 'oldest'
|
|
33
33
|
): {
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
date: Date;
|
|
35
|
+
timestamp: number;
|
|
36
36
|
} {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
try {
|
|
38
|
+
const { stdout } = execaSync('which', ['git']);
|
|
39
|
+
if (!stdout) {
|
|
40
|
+
throw new GitNotFoundError(
|
|
41
|
+
`Failed to retrieve git history for "${file}" because git is not installed.`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
} catch {}
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
try {
|
|
47
|
+
const { stdout } = execaSync('test', ['-f', file]);
|
|
48
|
+
if (!stdout) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Failed to retrieve git history for "${file}" because the file does not exist.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
} catch {}
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
55
|
+
const result = execaSync(
|
|
56
|
+
'git',
|
|
57
|
+
[
|
|
58
|
+
'log',
|
|
59
|
+
`--format=%ct`,
|
|
60
|
+
'--max-count=1',
|
|
61
|
+
...(age === 'oldest' ? ['--follow', '--diff-filter=A'] : []),
|
|
62
|
+
'--',
|
|
63
|
+
path.basename(file),
|
|
64
|
+
],
|
|
65
|
+
{
|
|
66
|
+
cwd: path.dirname(file),
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
if (result.exitCode !== 0) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Failed to retrieve the git history for file "${file}" with exit code ${result.exitCode}: ${result.stderr}`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
let regex = /^(?<timestamp>\d+)$/;
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
const output = result.stdout.trim();
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
if (!output) {
|
|
79
|
+
throw new FileNotTrackedError(
|
|
80
|
+
`Failed to retrieve the git history for file "${file}" because the file is not tracked by git.`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
const match = output.match(regex);
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
if (!match) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Failed to retrieve the git history for file "${file}" with unexpected output: ${output}`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
const timestamp = Number(match.groups!.timestamp);
|
|
93
|
+
const date = new Date(timestamp * 1000);
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
return { date, timestamp };
|
|
96
96
|
}
|
package/utils/head.ts
CHANGED
|
@@ -4,11 +4,11 @@ const HeadSchema = HeadConfigSchema();
|
|
|
4
4
|
|
|
5
5
|
/** Create a fully parsed, merged, and sorted head entry array from multiple sources. */
|
|
6
6
|
export function createHead(defaults: HeadUserConfig, ...heads: HeadConfig[]) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
let head = HeadSchema.parse(defaults);
|
|
8
|
+
for (const next of heads) {
|
|
9
|
+
head = mergeHead(head, next);
|
|
10
|
+
}
|
|
11
|
+
return sortHead(head);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -20,76 +20,78 @@ export function createHead(defaults: HeadUserConfig, ...heads: HeadConfig[]) {
|
|
|
20
20
|
* `property`, and `http-equiv` attributes for `<meta>` tags.
|
|
21
21
|
*/
|
|
22
22
|
function hasTag(head: HeadConfig, entry: HeadConfig[number]): boolean {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
switch (entry.tag) {
|
|
24
|
+
case 'title':
|
|
25
|
+
return head.some(({ tag }) => tag === 'title');
|
|
26
|
+
case 'meta':
|
|
27
|
+
return hasOneOf(head, entry, ['name', 'property', 'http-equiv']);
|
|
28
|
+
default:
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Test if a head config object contains a tag of the same type
|
|
35
35
|
* as `entry` and a matching attribute for one of the passed `keys`.
|
|
36
36
|
*/
|
|
37
|
-
function hasOneOf(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
)
|
|
42
|
-
const attr = getAttr(keys, entry);
|
|
43
|
-
if (!attr) return false;
|
|
44
|
-
const [key, val] = attr;
|
|
45
|
-
return head.some(({ tag, attrs }) => tag === entry.tag && attrs[key] === val);
|
|
37
|
+
function hasOneOf(head: HeadConfig, entry: HeadConfig[number], keys: string[]): boolean {
|
|
38
|
+
const attr = getAttr(keys, entry);
|
|
39
|
+
if (!attr) return false;
|
|
40
|
+
const [key, val] = attr;
|
|
41
|
+
return head.some(({ tag, attrs }) => tag === entry.tag && attrs[key] === val);
|
|
46
42
|
}
|
|
47
43
|
|
|
48
44
|
/** Find the first matching key–value pair in a head entry’s attributes. */
|
|
49
45
|
function getAttr(
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
keys: string[],
|
|
47
|
+
entry: HeadConfig[number]
|
|
52
48
|
): [key: string, value: string | boolean] | undefined {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
49
|
+
let attr: [string, string | boolean] | undefined;
|
|
50
|
+
for (const key of keys) {
|
|
51
|
+
const val = entry.attrs[key];
|
|
52
|
+
if (val) {
|
|
53
|
+
attr = [key, val];
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return attr;
|
|
62
58
|
}
|
|
63
59
|
|
|
64
60
|
/** Merge two heads, overwriting entries in the first head that exist in the second. */
|
|
65
61
|
function mergeHead(oldHead: HeadConfig, newHead: HeadConfig) {
|
|
66
|
-
|
|
62
|
+
return [...oldHead.filter((tag) => !hasTag(newHead, tag)), ...newHead];
|
|
67
63
|
}
|
|
68
64
|
|
|
69
65
|
/** Sort head tags to place important tags first and relegate “SEO” meta tags. */
|
|
70
66
|
function sortHead(head: HeadConfig) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
67
|
+
return head.sort((a, b) => {
|
|
68
|
+
const aImportance = getImportance(a);
|
|
69
|
+
const bImportance = getImportance(b);
|
|
70
|
+
return aImportance > bImportance ? -1 : bImportance > aImportance ? 1 : 0;
|
|
71
|
+
});
|
|
76
72
|
}
|
|
77
73
|
|
|
78
74
|
/** Get the relative importance of a specific head tag. */
|
|
79
75
|
function getImportance(entry: HeadConfig[number]) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
76
|
+
// 1. Important meta tags.
|
|
77
|
+
if (
|
|
78
|
+
entry.tag === 'meta' &&
|
|
79
|
+
('charset' in entry.attrs || 'http-equiv' in entry.attrs || entry.attrs.name === 'viewport')
|
|
80
|
+
) {
|
|
81
|
+
return 100;
|
|
82
|
+
}
|
|
83
|
+
// 2. Page title
|
|
84
|
+
if (entry.tag === 'title') return 90;
|
|
85
|
+
// 3. Anything that isn’t an SEO meta tag.
|
|
86
|
+
if (entry.tag !== 'meta') {
|
|
87
|
+
// The default favicon should be below any extra icons that the user may have set
|
|
88
|
+
// because if several icons are equally appropriate, the last one is used and we
|
|
89
|
+
// want to use the SVG icon when supported.
|
|
90
|
+
if (entry.tag === 'link' && 'rel' in entry.attrs && entry.attrs.rel === 'shortcut icon') {
|
|
91
|
+
return 70;
|
|
92
|
+
}
|
|
93
|
+
return 80;
|
|
94
|
+
}
|
|
95
|
+
// 4. SEO meta tags.
|
|
96
|
+
return 0;
|
|
95
97
|
}
|
package/utils/i18n.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* pickLang({ en: 'Hello', fr: 'Bonjour' }, 'en'); // => 'Hello'
|
|
10
10
|
*/
|
|
11
11
|
export function pickLang<T extends Record<string, string>>(
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
dictionary: T,
|
|
13
|
+
lang: keyof T
|
|
14
14
|
): string | undefined {
|
|
15
|
-
|
|
15
|
+
return dictionary[lang];
|
|
16
16
|
}
|
package/utils/localizedUrl.ts
CHANGED
|
@@ -4,29 +4,29 @@ import config from 'virtual:starlight/user-config';
|
|
|
4
4
|
* Get the equivalent of the passed URL for the passed locale.
|
|
5
5
|
*/
|
|
6
6
|
export function localizedUrl(url: URL, locale: string | undefined): URL {
|
|
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
|
-
|
|
7
|
+
// Create a new URL object to void mutating the global.
|
|
8
|
+
url = new URL(url);
|
|
9
|
+
if (!config.locales) {
|
|
10
|
+
// i18n is not configured on this site, no localization required.
|
|
11
|
+
return url;
|
|
12
|
+
}
|
|
13
|
+
if (locale === 'root') locale = '';
|
|
14
|
+
/** Base URL with trailing `/` stripped. */
|
|
15
|
+
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
|
16
|
+
const hasBase = url.pathname.startsWith(base);
|
|
17
|
+
// Temporarily remove base to simplify
|
|
18
|
+
if (hasBase) url.pathname = url.pathname.replace(base, '');
|
|
19
|
+
const [_leadingSlash, baseSegment] = url.pathname.split('/');
|
|
20
|
+
if (baseSegment && baseSegment in config.locales) {
|
|
21
|
+
// We’re in a localized route, substitute the new locale (or strip for root lang).
|
|
22
|
+
url.pathname = locale
|
|
23
|
+
? url.pathname.replace(baseSegment, locale)
|
|
24
|
+
: url.pathname.replace('/' + baseSegment, '');
|
|
25
|
+
} else if (locale) {
|
|
26
|
+
// We’re in the root language. Inject the new locale if we have one.
|
|
27
|
+
url.pathname = '/' + locale + url.pathname;
|
|
28
|
+
}
|
|
29
|
+
// Restore base
|
|
30
|
+
if (hasBase) url.pathname = base + url.pathname;
|
|
31
|
+
return url;
|
|
32
32
|
}
|