@docusaurus/utils 3.3.2 → 3.4.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/lib/hashUtils.d.ts +4 -1
- package/lib/hashUtils.d.ts.map +1 -1
- package/lib/hashUtils.js +9 -3
- package/lib/hashUtils.js.map +1 -1
- package/lib/index.d.ts +4 -3
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +9 -4
- package/lib/index.js.map +1 -1
- package/lib/markdownLinks.d.ts +7 -34
- package/lib/markdownLinks.d.ts.map +1 -1
- package/lib/markdownLinks.js +22 -104
- package/lib/markdownLinks.js.map +1 -1
- package/lib/tags.d.ts +35 -18
- package/lib/tags.d.ts.map +1 -1
- package/lib/tags.js +57 -28
- package/lib/tags.js.map +1 -1
- package/lib/urlUtils.d.ts +14 -0
- package/lib/urlUtils.d.ts.map +1 -1
- package/lib/urlUtils.js +66 -19
- package/lib/urlUtils.js.map +1 -1
- package/package.json +6 -5
- package/src/hashUtils.ts +20 -3
- package/src/index.ts +11 -3
- package/src/markdownLinks.ts +32 -151
- package/src/tags.ts +121 -36
- package/src/urlUtils.ts +76 -22
package/src/tags.ts
CHANGED
|
@@ -6,13 +6,34 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import _ from 'lodash';
|
|
9
|
+
import logger from '@docusaurus/logger';
|
|
9
10
|
import {normalizeUrl} from './urlUtils';
|
|
11
|
+
import type {Optional} from 'utility-types';
|
|
10
12
|
|
|
11
|
-
/** What the user configures. */
|
|
12
13
|
export type Tag = {
|
|
14
|
+
/** The display label of a tag */
|
|
13
15
|
label: string;
|
|
14
16
|
/** Permalink to this tag's page, without the `/tags/` base path. */
|
|
15
17
|
permalink: string;
|
|
18
|
+
/** An optional description of the tag */
|
|
19
|
+
description: string | undefined;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type TagsFileInput = Record<string, Partial<Tag> | null>;
|
|
23
|
+
|
|
24
|
+
export type TagsFile = Record<string, Tag>;
|
|
25
|
+
|
|
26
|
+
// Tags plugins options shared between docs/blog
|
|
27
|
+
export type TagsPluginOptions = {
|
|
28
|
+
// TODO allow option tags later? | TagsFile;
|
|
29
|
+
/** Path to the tags file. */
|
|
30
|
+
tags: string | false | null | undefined;
|
|
31
|
+
/** The behavior of Docusaurus when it found inline tags. */
|
|
32
|
+
onInlineTags: 'ignore' | 'log' | 'warn' | 'throw';
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type TagMetadata = Tag & {
|
|
36
|
+
inline: boolean;
|
|
16
37
|
};
|
|
17
38
|
|
|
18
39
|
/** What the tags list page should know about each tag. */
|
|
@@ -29,62 +50,126 @@ export type TagModule = TagsListItem & {
|
|
|
29
50
|
unlisted: boolean;
|
|
30
51
|
};
|
|
31
52
|
|
|
32
|
-
export type FrontMatterTag = string | Tag
|
|
53
|
+
export type FrontMatterTag = string | Optional<Tag, 'description'>;
|
|
33
54
|
|
|
34
|
-
|
|
35
|
-
|
|
55
|
+
// We always apply tagsBaseRoutePath on purpose. For versioned docs, v1/doc.md
|
|
56
|
+
// and v2/doc.md tags with custom permalinks don't lead to the same created
|
|
57
|
+
// page. tagsBaseRoutePath is different for each doc version
|
|
58
|
+
function normalizeTagPermalink({
|
|
59
|
+
tagsBaseRoutePath,
|
|
60
|
+
permalink,
|
|
61
|
+
}: {
|
|
62
|
+
tagsBaseRoutePath: string;
|
|
63
|
+
permalink: string;
|
|
64
|
+
}): string {
|
|
65
|
+
return normalizeUrl([tagsBaseRoutePath, permalink]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeInlineTag(
|
|
69
|
+
tagsBaseRoutePath: string,
|
|
36
70
|
frontMatterTag: FrontMatterTag,
|
|
37
|
-
):
|
|
38
|
-
function toTagObject(tagString: string):
|
|
71
|
+
): TagMetadata {
|
|
72
|
+
function toTagObject(tagString: string): TagMetadata {
|
|
39
73
|
return {
|
|
74
|
+
inline: true,
|
|
40
75
|
label: tagString,
|
|
41
76
|
permalink: _.kebabCase(tagString),
|
|
77
|
+
description: undefined,
|
|
42
78
|
};
|
|
43
79
|
}
|
|
44
80
|
|
|
45
|
-
// TODO maybe make ensure the permalink is valid url path?
|
|
46
|
-
function normalizeTagPermalink(permalink: string): string {
|
|
47
|
-
// Note: we always apply tagsPath on purpose. For versioned docs, v1/doc.md
|
|
48
|
-
// and v2/doc.md tags with custom permalinks don't lead to the same created
|
|
49
|
-
// page. tagsPath is different for each doc version
|
|
50
|
-
return normalizeUrl([tagsPath, permalink]);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
81
|
const tag: Tag =
|
|
54
82
|
typeof frontMatterTag === 'string'
|
|
55
83
|
? toTagObject(frontMatterTag)
|
|
56
|
-
: frontMatterTag;
|
|
84
|
+
: {...frontMatterTag, description: frontMatterTag.description};
|
|
57
85
|
|
|
58
86
|
return {
|
|
87
|
+
inline: true,
|
|
59
88
|
label: tag.label,
|
|
60
|
-
permalink: normalizeTagPermalink(
|
|
89
|
+
permalink: normalizeTagPermalink({
|
|
90
|
+
permalink: tag.permalink,
|
|
91
|
+
tagsBaseRoutePath,
|
|
92
|
+
}),
|
|
93
|
+
description: tag.description,
|
|
61
94
|
};
|
|
62
95
|
}
|
|
63
96
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
97
|
+
export function normalizeTag({
|
|
98
|
+
tag,
|
|
99
|
+
tagsFile,
|
|
100
|
+
tagsBaseRoutePath,
|
|
101
|
+
}: {
|
|
102
|
+
tag: FrontMatterTag;
|
|
103
|
+
tagsBaseRoutePath: string;
|
|
104
|
+
tagsFile: TagsFile | null;
|
|
105
|
+
}): TagMetadata {
|
|
106
|
+
if (typeof tag === 'string') {
|
|
107
|
+
const tagDescription = tagsFile?.[tag];
|
|
108
|
+
if (tagDescription) {
|
|
109
|
+
// pre-defined tag from tags.yml
|
|
110
|
+
return {
|
|
111
|
+
inline: false,
|
|
112
|
+
label: tagDescription.label,
|
|
113
|
+
permalink: normalizeTagPermalink({
|
|
114
|
+
permalink: tagDescription.permalink,
|
|
115
|
+
tagsBaseRoutePath,
|
|
116
|
+
}),
|
|
117
|
+
description: tagDescription.description,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// legacy inline tag object, always inline, unknown because isn't a string
|
|
122
|
+
return normalizeInlineTag(tagsBaseRoutePath, tag);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function normalizeTags({
|
|
126
|
+
options,
|
|
127
|
+
source,
|
|
128
|
+
frontMatterTags,
|
|
129
|
+
tagsBaseRoutePath,
|
|
130
|
+
tagsFile,
|
|
131
|
+
}: {
|
|
132
|
+
options: TagsPluginOptions;
|
|
133
|
+
source: string;
|
|
134
|
+
frontMatterTags: FrontMatterTag[] | undefined;
|
|
135
|
+
tagsBaseRoutePath: string;
|
|
136
|
+
tagsFile: TagsFile | null;
|
|
137
|
+
}): TagMetadata[] {
|
|
138
|
+
const tags = (frontMatterTags ?? []).map((tag) =>
|
|
139
|
+
normalizeTag({tag, tagsBaseRoutePath, tagsFile}),
|
|
81
140
|
);
|
|
141
|
+
if (tagsFile !== null) {
|
|
142
|
+
reportInlineTags({tags, source, options});
|
|
143
|
+
}
|
|
144
|
+
return tags;
|
|
145
|
+
}
|
|
82
146
|
|
|
83
|
-
|
|
147
|
+
export function reportInlineTags({
|
|
148
|
+
tags,
|
|
149
|
+
source,
|
|
150
|
+
options,
|
|
151
|
+
}: {
|
|
152
|
+
tags: TagMetadata[];
|
|
153
|
+
source: string;
|
|
154
|
+
options: TagsPluginOptions;
|
|
155
|
+
}): void {
|
|
156
|
+
if (options.onInlineTags === 'ignore') {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const inlineTags = tags.filter((tag) => tag.inline);
|
|
160
|
+
if (inlineTags.length > 0) {
|
|
161
|
+
const uniqueUnknownTags = [...new Set(inlineTags.map((tag) => tag.label))];
|
|
162
|
+
const tagListString = uniqueUnknownTags.join(', ');
|
|
163
|
+
logger.report(options.onInlineTags)(
|
|
164
|
+
`Tags [${tagListString}] used in ${source} are not defined in ${
|
|
165
|
+
options.tags ?? 'tags.yml'
|
|
166
|
+
}`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
84
169
|
}
|
|
85
170
|
|
|
86
171
|
type TaggedItemGroup<Item> = {
|
|
87
|
-
tag:
|
|
172
|
+
tag: TagMetadata;
|
|
88
173
|
items: Item[];
|
|
89
174
|
};
|
|
90
175
|
|
|
@@ -102,7 +187,7 @@ export function groupTaggedItems<Item>(
|
|
|
102
187
|
* A callback telling me how to get the tags list of the current item. Usually
|
|
103
188
|
* simply getting it from some metadata of the current item.
|
|
104
189
|
*/
|
|
105
|
-
getItemTags: (item: Item) => readonly
|
|
190
|
+
getItemTags: (item: Item) => readonly TagMetadata[],
|
|
106
191
|
): {[permalink: string]: TaggedItemGroup<Item>} {
|
|
107
192
|
const result: {[permalink: string]: TaggedItemGroup<Item>} = {};
|
|
108
193
|
|
package/src/urlUtils.ts
CHANGED
|
@@ -90,7 +90,7 @@ export function normalizeUrl(rawUrls: string[]): string {
|
|
|
90
90
|
// first plain protocol part.
|
|
91
91
|
|
|
92
92
|
// Remove trailing slash before parameters or hash.
|
|
93
|
-
str = str.replace(/\/(?<search>\?|&|#[
|
|
93
|
+
str = str.replace(/\/(?<search>\?|&|#[^!/])/g, '$1');
|
|
94
94
|
|
|
95
95
|
// Replace ? in parameters with &.
|
|
96
96
|
const parts = str.split('?');
|
|
@@ -164,27 +164,22 @@ export function isValidPathname(str: string): boolean {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
export
|
|
168
|
-
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
} catch (e) {
|
|
178
|
-
throw new Error(
|
|
179
|
-
`Can't parse URL ${url}${base ? ` with base ${base}` : ''}`,
|
|
180
|
-
{cause: e},
|
|
181
|
-
);
|
|
182
|
-
}
|
|
167
|
+
export function parseURLOrPath(url: string, base?: string | URL): URL {
|
|
168
|
+
try {
|
|
169
|
+
// TODO when Node supports it, use URL.parse could be faster?
|
|
170
|
+
// see https://kilianvalkhof.com/2024/javascript/the-problem-with-new-url-and-how-url-parse-fixes-that/
|
|
171
|
+
return new URL(url, base ?? 'https://example.com');
|
|
172
|
+
} catch (e) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`Can't parse URL ${url}${base ? ` with base ${base}` : ''}`,
|
|
175
|
+
{cause: e},
|
|
176
|
+
);
|
|
183
177
|
}
|
|
178
|
+
}
|
|
184
179
|
|
|
185
|
-
|
|
186
|
-
const url = parseURL(urlPath, base);
|
|
180
|
+
export type URLPath = {pathname: string; search?: string; hash?: string};
|
|
187
181
|
|
|
182
|
+
export function toURLPath(url: URL): URLPath {
|
|
188
183
|
const {pathname} = url;
|
|
189
184
|
|
|
190
185
|
// Fixes annoying url.search behavior
|
|
@@ -193,17 +188,17 @@ export function parseURLPath(urlPath: string, fromPath?: string): URLPath {
|
|
|
193
188
|
// "?param => "param"
|
|
194
189
|
const search = url.search
|
|
195
190
|
? url.search.slice(1)
|
|
196
|
-
:
|
|
191
|
+
: url.href.includes('?')
|
|
197
192
|
? ''
|
|
198
193
|
: undefined;
|
|
199
194
|
|
|
200
195
|
// Fixes annoying url.hash behavior
|
|
201
196
|
// "" => undefined
|
|
202
197
|
// "#" => ""
|
|
203
|
-
// "
|
|
198
|
+
// "#param => "param"
|
|
204
199
|
const hash = url.hash
|
|
205
200
|
? url.hash.slice(1)
|
|
206
|
-
:
|
|
201
|
+
: url.href.includes('#')
|
|
207
202
|
? ''
|
|
208
203
|
: undefined;
|
|
209
204
|
|
|
@@ -214,6 +209,65 @@ export function parseURLPath(urlPath: string, fromPath?: string): URLPath {
|
|
|
214
209
|
};
|
|
215
210
|
}
|
|
216
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Let's name the concept of (pathname + search + hash) as URLPath
|
|
214
|
+
* See also https://twitter.com/kettanaito/status/1741768992866308120
|
|
215
|
+
* Note: this function also resolves relative pathnames while parsing!
|
|
216
|
+
*/
|
|
217
|
+
export function parseURLPath(urlPath: string, fromPath?: string): URLPath {
|
|
218
|
+
const base = fromPath ? parseURLOrPath(fromPath) : undefined;
|
|
219
|
+
const url = parseURLOrPath(urlPath, base);
|
|
220
|
+
return toURLPath(url);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* This returns results for strings like "foo", "../foo", "./foo.mdx?qs#hash"
|
|
225
|
+
* Unlike "parseURLPath()" above, this will not resolve the pathnames
|
|
226
|
+
* Te returned pathname of "../../foo.mdx" will be "../../foo.mdx", not "/foo"
|
|
227
|
+
* This returns null if the url is not "local" (contains domain/protocol etc)
|
|
228
|
+
*/
|
|
229
|
+
export function parseLocalURLPath(urlPath: string): URLPath | null {
|
|
230
|
+
// Workaround because URL("") requires a protocol
|
|
231
|
+
const unspecifiedProtocol = 'unspecified:';
|
|
232
|
+
|
|
233
|
+
const url = parseURLOrPath(urlPath, `${unspecifiedProtocol}//`);
|
|
234
|
+
// Ignore links with specified protocol / host
|
|
235
|
+
// (usually fully qualified links starting with https://)
|
|
236
|
+
if (
|
|
237
|
+
url.protocol !== unspecifiedProtocol ||
|
|
238
|
+
url.host !== '' ||
|
|
239
|
+
url.username !== '' ||
|
|
240
|
+
url.password !== ''
|
|
241
|
+
) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// We can't use "new URL()" result because it always tries to resolve urls
|
|
246
|
+
// IE it will remove any "./" or "../" in the pathname, which we don't want
|
|
247
|
+
// We have to parse it manually...
|
|
248
|
+
let localUrlPath = urlPath;
|
|
249
|
+
|
|
250
|
+
// Extract and remove the #hash part
|
|
251
|
+
const hashIndex = localUrlPath.indexOf('#');
|
|
252
|
+
const hash =
|
|
253
|
+
hashIndex !== -1 ? localUrlPath.substring(hashIndex + 1) : undefined;
|
|
254
|
+
localUrlPath =
|
|
255
|
+
hashIndex !== -1 ? localUrlPath.substring(0, hashIndex) : localUrlPath;
|
|
256
|
+
|
|
257
|
+
// Extract and remove ?search part
|
|
258
|
+
const searchIndex = localUrlPath.indexOf('?');
|
|
259
|
+
const search =
|
|
260
|
+
searchIndex !== -1 ? localUrlPath.substring(searchIndex + 1) : undefined;
|
|
261
|
+
localUrlPath =
|
|
262
|
+
searchIndex !== -1 ? localUrlPath.substring(0, searchIndex) : localUrlPath;
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
pathname: localUrlPath,
|
|
266
|
+
search,
|
|
267
|
+
hash,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
217
271
|
export function serializeURLPath(urlPath: URLPath): string {
|
|
218
272
|
const search = urlPath.search === undefined ? '' : `?${urlPath.search}`;
|
|
219
273
|
const hash = urlPath.hash === undefined ? '' : `#${urlPath.hash}`;
|