@docusaurus/utils 3.3.0 → 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/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
- function normalizeFrontMatterTag(
35
- tagsPath: string,
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
- ): Tag {
38
- function toTagObject(tagString: string): Tag {
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(tag.permalink),
89
+ permalink: normalizeTagPermalink({
90
+ permalink: tag.permalink,
91
+ tagsBaseRoutePath,
92
+ }),
93
+ description: tag.description,
61
94
  };
62
95
  }
63
96
 
64
- /**
65
- * Takes tag objects as they are defined in front matter, and normalizes each
66
- * into a standard tag object. The permalink is created by appending the
67
- * sluggified label to `tagsPath`. Front matter tags already containing
68
- * permalinks would still have `tagsPath` prepended.
69
- *
70
- * The result will always be unique by permalinks. The behavior with colliding
71
- * permalinks is undetermined.
72
- */
73
- export function normalizeFrontMatterTags(
74
- /** Base path to append the tag permalinks to. */
75
- tagsPath: string,
76
- /** Can be `undefined`, so that we can directly pipe in `frontMatter.tags`. */
77
- frontMatterTags: FrontMatterTag[] | undefined = [],
78
- ): Tag[] {
79
- const tags = frontMatterTags.map((tag) =>
80
- normalizeFrontMatterTag(tagsPath, tag),
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
- return _.uniqBy(tags, (tag) => tag.permalink);
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: 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 Tag[],
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>\?|&|#[^!])/g, '$1');
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 type URLPath = {pathname: string; search?: string; hash?: string};
168
-
169
- // Let's name the concept of (pathname + search + hash) as URLPath
170
- // See also https://twitter.com/kettanaito/status/1741768992866308120
171
- // Note: this function also resolves relative pathnames while parsing!
172
- export function parseURLPath(urlPath: string, fromPath?: string): URLPath {
173
- function parseURL(url: string, base?: string | URL): URL {
174
- try {
175
- // A possible alternative? https://github.com/unjs/ufo#url
176
- return new URL(url, base ?? 'https://example.com');
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
- const base = fromPath ? parseURL(fromPath) : undefined;
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
- : urlPath.includes('?')
191
+ : url.href.includes('?')
197
192
  ? ''
198
193
  : undefined;
199
194
 
200
195
  // Fixes annoying url.hash behavior
201
196
  // "" => undefined
202
197
  // "#" => ""
203
- // "?param => "param"
198
+ // "#param => "param"
204
199
  const hash = url.hash
205
200
  ? url.hash.slice(1)
206
- : urlPath.includes('#')
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}`;