@docusaurus/utils 2.0.0-beta.16 → 2.0.0-beta.19

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.
Files changed (86) hide show
  1. package/lib/constants.d.ts +51 -1
  2. package/lib/constants.d.ts.map +1 -1
  3. package/lib/constants.js +57 -10
  4. package/lib/constants.js.map +1 -1
  5. package/lib/dataFileUtils.d.ts +38 -2
  6. package/lib/dataFileUtils.d.ts.map +1 -1
  7. package/lib/dataFileUtils.js +39 -13
  8. package/lib/dataFileUtils.js.map +1 -1
  9. package/lib/emitUtils.d.ts +32 -0
  10. package/lib/emitUtils.d.ts.map +1 -0
  11. package/lib/emitUtils.js +80 -0
  12. package/lib/emitUtils.js.map +1 -0
  13. package/lib/gitUtils.d.ts +54 -5
  14. package/lib/gitUtils.d.ts.map +1 -1
  15. package/lib/gitUtils.js +17 -14
  16. package/lib/gitUtils.js.map +1 -1
  17. package/lib/globUtils.d.ts +28 -0
  18. package/lib/globUtils.d.ts.map +1 -1
  19. package/lib/globUtils.js +36 -13
  20. package/lib/globUtils.js.map +1 -1
  21. package/lib/hashUtils.d.ts +5 -4
  22. package/lib/hashUtils.d.ts.map +1 -1
  23. package/lib/hashUtils.js +7 -6
  24. package/lib/hashUtils.js.map +1 -1
  25. package/lib/i18nUtils.d.ts +51 -0
  26. package/lib/i18nUtils.d.ts.map +1 -0
  27. package/lib/i18nUtils.js +69 -0
  28. package/lib/i18nUtils.js.map +1 -0
  29. package/lib/index.d.ts +10 -54
  30. package/lib/index.d.ts.map +1 -1
  31. package/lib/index.js +56 -256
  32. package/lib/index.js.map +1 -1
  33. package/lib/jsUtils.d.ts +45 -0
  34. package/lib/jsUtils.d.ts.map +1 -0
  35. package/lib/jsUtils.js +94 -0
  36. package/lib/jsUtils.js.map +1 -0
  37. package/lib/markdownLinks.d.ts +48 -5
  38. package/lib/markdownLinks.d.ts.map +1 -1
  39. package/lib/markdownLinks.js +29 -13
  40. package/lib/markdownLinks.js.map +1 -1
  41. package/lib/markdownUtils.d.ts +112 -0
  42. package/lib/markdownUtils.d.ts.map +1 -0
  43. package/lib/markdownUtils.js +271 -0
  44. package/lib/markdownUtils.js.map +1 -0
  45. package/lib/pathUtils.d.ts +2 -1
  46. package/lib/pathUtils.d.ts.map +1 -1
  47. package/lib/pathUtils.js +16 -7
  48. package/lib/pathUtils.js.map +1 -1
  49. package/lib/slugger.d.ts +10 -0
  50. package/lib/slugger.d.ts.map +1 -1
  51. package/lib/slugger.js +6 -2
  52. package/lib/slugger.js.map +1 -1
  53. package/lib/tags.d.ts +42 -10
  54. package/lib/tags.d.ts.map +1 -1
  55. package/lib/tags.js +40 -26
  56. package/lib/tags.js.map +1 -1
  57. package/lib/urlUtils.d.ts +57 -0
  58. package/lib/urlUtils.d.ts.map +1 -1
  59. package/lib/urlUtils.js +132 -6
  60. package/lib/urlUtils.js.map +1 -1
  61. package/lib/webpackUtils.d.ts +5 -0
  62. package/lib/webpackUtils.d.ts.map +1 -1
  63. package/lib/webpackUtils.js +8 -5
  64. package/lib/webpackUtils.js.map +1 -1
  65. package/package.json +11 -11
  66. package/src/constants.ts +65 -9
  67. package/src/dataFileUtils.ts +44 -12
  68. package/src/emitUtils.ts +99 -0
  69. package/src/gitUtils.ts +77 -17
  70. package/src/globUtils.ts +34 -13
  71. package/src/hashUtils.ts +6 -5
  72. package/src/i18nUtils.ts +115 -0
  73. package/src/index.ts +43 -307
  74. package/src/jsUtils.ts +102 -0
  75. package/src/markdownLinks.ts +71 -28
  76. package/src/markdownUtils.ts +354 -0
  77. package/src/pathUtils.ts +15 -7
  78. package/src/slugger.ts +13 -1
  79. package/src/tags.ts +53 -28
  80. package/src/urlUtils.ts +145 -7
  81. package/src/webpackUtils.ts +11 -4
  82. package/lib/markdownParser.d.ts +0 -32
  83. package/lib/markdownParser.d.ts.map +0 -1
  84. package/lib/markdownParser.js +0 -161
  85. package/lib/markdownParser.js.map +0 -1
  86. package/src/markdownParser.ts +0 -201
package/src/index.ts CHANGED
@@ -5,45 +5,62 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
- import logger from '@docusaurus/logger';
9
- import path from 'path';
10
- import {createHash} from 'crypto';
11
- import _ from 'lodash';
12
- import fs from 'fs-extra';
13
- import {URL} from 'url';
14
- import type {
15
- ReportingSeverity,
16
- TranslationFileContent,
17
- TranslationFile,
18
- } from '@docusaurus/types';
19
-
20
- import resolvePathnameUnsafe from 'resolve-pathname';
21
-
22
- import {simpleHash, docuHash} from './hashUtils';
23
- import {DEFAULT_PLUGIN_ID} from './constants';
24
-
25
8
  export {
26
9
  NODE_MAJOR_VERSION,
27
10
  NODE_MINOR_VERSION,
11
+ DOCUSAURUS_VERSION,
28
12
  DEFAULT_BUILD_DIR_NAME,
29
13
  DEFAULT_CONFIG_FILE_NAME,
30
14
  BABEL_CONFIG_FILE_NAME,
31
15
  GENERATED_FILES_DIR_NAME,
32
16
  SRC_DIR_NAME,
33
- STATIC_DIR_NAME,
17
+ DEFAULT_STATIC_DIR_NAME,
34
18
  OUTPUT_STATIC_ASSETS_DIR_NAME,
35
19
  THEME_PATH,
20
+ I18N_DIR_NAME,
21
+ CODE_TRANSLATIONS_FILE_NAME,
36
22
  DEFAULT_PORT,
37
23
  DEFAULT_PLUGIN_ID,
38
24
  WEBPACK_URL_LOADER_LIMIT,
39
25
  } from './constants';
40
- export {getFileCommitDate, GitNotFoundError} from './gitUtils';
41
- export {normalizeUrl, getEditUrl} from './urlUtils';
26
+ export {generate, readOutputHTMLFile} from './emitUtils';
27
+ export {
28
+ getFileCommitDate,
29
+ FileNotTrackedError,
30
+ GitNotFoundError,
31
+ } from './gitUtils';
32
+ export {
33
+ mergeTranslations,
34
+ updateTranslationFileMessages,
35
+ getPluginI18nPath,
36
+ localizePath,
37
+ } from './i18nUtils';
38
+ export {
39
+ removeSuffix,
40
+ removePrefix,
41
+ mapAsyncSequential,
42
+ findAsyncSequential,
43
+ reportMessage,
44
+ } from './jsUtils';
45
+ export {
46
+ normalizeUrl,
47
+ getEditUrl,
48
+ fileToPath,
49
+ encodePath,
50
+ isValidPathname,
51
+ resolvePathname,
52
+ addLeadingSlash,
53
+ addTrailingSlash,
54
+ removeTrailingSlash,
55
+ hasSSHProtocol,
56
+ buildHttpsUrl,
57
+ buildSshUrl,
58
+ } from './urlUtils';
42
59
  export {
43
60
  type Tag,
61
+ type TagsListItem,
62
+ type TagModule,
44
63
  type FrontMatterTag,
45
- type TaggedItemGroup,
46
- normalizeFrontMatterTag,
47
64
  normalizeFrontMatterTags,
48
65
  groupTaggedItems,
49
66
  } from './tags';
@@ -53,12 +70,12 @@ export {
53
70
  parseFrontMatter,
54
71
  parseMarkdownContentTitle,
55
72
  parseMarkdownString,
56
- } from './markdownParser';
73
+ writeMarkdownHeadingId,
74
+ type WriteHeadingIDOptions,
75
+ } from './markdownUtils';
57
76
  export {
58
77
  type ContentPaths,
59
78
  type BrokenMarkdownLink,
60
- type ReplaceMarkdownLinksParams,
61
- type ReplaceMarkdownLinksReturn,
62
79
  replaceMarkdownLinks,
63
80
  } from './markdownLinks';
64
81
  export {type SluggerOptions, type Slugger, createSlugger} from './slugger';
@@ -69,6 +86,7 @@ export {
69
86
  toMessageRelativeFilePath,
70
87
  aliasedSitePath,
71
88
  escapePath,
89
+ addTrailingPathSeparator,
72
90
  } from './pathUtils';
73
91
  export {md5Hash, simpleHash, docuHash} from './hashUtils';
74
92
  export {
@@ -85,285 +103,3 @@ export {
85
103
  findFolderContainingFile,
86
104
  getFolderContainingFile,
87
105
  } from './dataFileUtils';
88
-
89
- const fileHash = new Map<string, string>();
90
- export async function generate(
91
- generatedFilesDir: string,
92
- file: string,
93
- content: string,
94
- skipCache: boolean = process.env.NODE_ENV === 'production',
95
- ): Promise<void> {
96
- const filepath = path.join(generatedFilesDir, file);
97
-
98
- if (skipCache) {
99
- await fs.ensureDir(path.dirname(filepath));
100
- await fs.writeFile(filepath, content);
101
- return;
102
- }
103
-
104
- let lastHash = fileHash.get(filepath);
105
-
106
- // If file already exists but its not in runtime cache yet,
107
- // we try to calculate the content hash and then compare
108
- // This is to avoid unnecessary overwriting and we can reuse old file.
109
- if (!lastHash && (await fs.pathExists(filepath))) {
110
- const lastContent = await fs.readFile(filepath, 'utf8');
111
- lastHash = createHash('md5').update(lastContent).digest('hex');
112
- fileHash.set(filepath, lastHash);
113
- }
114
-
115
- const currentHash = createHash('md5').update(content).digest('hex');
116
-
117
- if (lastHash !== currentHash) {
118
- await fs.ensureDir(path.dirname(filepath));
119
- await fs.writeFile(filepath, content);
120
- fileHash.set(filepath, currentHash);
121
- }
122
- }
123
-
124
- const indexRE = /(?<dirname>^|.*\/)index\.(?:mdx?|jsx?|tsx?)$/i;
125
- const extRE = /\.(?:mdx?|jsx?|tsx?)$/;
126
-
127
- /**
128
- * Convert filepath to url path.
129
- * Example: 'index.md' -> '/', 'foo/bar.js' -> '/foo/bar',
130
- */
131
- export function fileToPath(file: string): string {
132
- if (indexRE.test(file)) {
133
- return file.replace(indexRE, '/$1');
134
- }
135
- return `/${file.replace(extRE, '').replace(/\\/g, '/')}`;
136
- }
137
-
138
- export function encodePath(userPath: string): string {
139
- return userPath
140
- .split('/')
141
- .map((item) => encodeURIComponent(item))
142
- .join('/');
143
- }
144
-
145
- const chunkNameCache = new Map();
146
- /**
147
- * Generate unique chunk name given a module path.
148
- */
149
- export function genChunkName(
150
- modulePath: string,
151
- prefix?: string,
152
- preferredName?: string,
153
- shortId: boolean = process.env.NODE_ENV === 'production',
154
- ): string {
155
- let chunkName: string | undefined = chunkNameCache.get(modulePath);
156
- if (!chunkName) {
157
- if (shortId) {
158
- chunkName = simpleHash(modulePath, 8);
159
- } else {
160
- let str = modulePath;
161
- if (preferredName) {
162
- const shortHash = simpleHash(modulePath, 3);
163
- str = `${preferredName}${shortHash}`;
164
- }
165
- const name = str === '/' ? 'index' : docuHash(str);
166
- chunkName = prefix ? `${prefix}---${name}` : name;
167
- }
168
- chunkNameCache.set(modulePath, chunkName);
169
- }
170
- return chunkName;
171
- }
172
-
173
- export function isValidPathname(str: string): boolean {
174
- if (!str.startsWith('/')) {
175
- return false;
176
- }
177
- try {
178
- // weird, but is there a better way?
179
- const parsedPathname = new URL(str, 'https://domain.com').pathname;
180
- return parsedPathname === str || parsedPathname === encodeURI(str);
181
- } catch {
182
- return false;
183
- }
184
- }
185
-
186
- // resolve pathname and fail fast if resolution fails
187
- export function resolvePathname(to: string, from?: string): string {
188
- return resolvePathnameUnsafe(to, from);
189
- }
190
- export function addLeadingSlash(str: string): string {
191
- return str.startsWith('/') ? str : `/${str}`;
192
- }
193
-
194
- export function addTrailingPathSeparator(str: string): string {
195
- return str.endsWith(path.sep)
196
- ? str
197
- : // If this is Windows, we need to change the forward slash to backward
198
- `${str.replace(/\/$/, '')}${path.sep}`;
199
- }
200
-
201
- // TODO deduplicate: also present in @docusaurus/utils-common
202
- export function addTrailingSlash(str: string): string {
203
- return str.endsWith('/') ? str : `${str}/`;
204
- }
205
- export function removeTrailingSlash(str: string): string {
206
- return removeSuffix(str, '/');
207
- }
208
-
209
- export function removeSuffix(str: string, suffix: string): string {
210
- if (suffix === '') {
211
- return str; // always returns "" otherwise!
212
- }
213
- return str.endsWith(suffix) ? str.slice(0, -suffix.length) : str;
214
- }
215
-
216
- export function removePrefix(str: string, prefix: string): string {
217
- return str.startsWith(prefix) ? str.slice(prefix.length) : str;
218
- }
219
-
220
- export function getElementsAround<T>(
221
- array: T[],
222
- aroundIndex: number,
223
- ): {
224
- next: T | undefined;
225
- previous: T | undefined;
226
- } {
227
- const min = 0;
228
- const max = array.length - 1;
229
- if (aroundIndex < min || aroundIndex > max) {
230
- throw new Error(
231
- `Valid "aroundIndex" for array (of size ${array.length}) are between ${min} and ${max}, but you provided ${aroundIndex}.`,
232
- );
233
- }
234
- const previous = aroundIndex === min ? undefined : array[aroundIndex - 1];
235
- const next = aroundIndex === max ? undefined : array[aroundIndex + 1];
236
- return {previous, next};
237
- }
238
-
239
- export function getPluginI18nPath({
240
- siteDir,
241
- locale,
242
- pluginName,
243
- pluginId = DEFAULT_PLUGIN_ID,
244
- subPaths = [],
245
- }: {
246
- siteDir: string;
247
- locale: string;
248
- pluginName: string;
249
- pluginId?: string | undefined;
250
- subPaths?: string[];
251
- }): string {
252
- return path.join(
253
- siteDir,
254
- 'i18n',
255
- // namespace first by locale: convenient to work in a single folder for a
256
- // translator
257
- locale,
258
- // Make it convenient to use for single-instance
259
- // ie: return "docs", not "docs-default" nor "docs/default"
260
- `${pluginName}${pluginId === DEFAULT_PLUGIN_ID ? '' : `-${pluginId}`}`,
261
- ...subPaths,
262
- );
263
- }
264
-
265
- /**
266
- * @param permalink The URL that the HTML file corresponds to, without base URL
267
- * @param outDir Full path to the output directory
268
- * @param trailingSlash The site config option. If provided, only one path will
269
- * be read.
270
- * @returns This returns a buffer, which you have to decode string yourself if
271
- * needed. (Not always necessary since the output isn't for human consumption
272
- * anyways, and most HTML manipulation libs accept buffers)
273
- */
274
- export async function readOutputHTMLFile(
275
- permalink: string,
276
- outDir: string,
277
- trailingSlash: boolean | undefined,
278
- ): Promise<Buffer> {
279
- const withTrailingSlashPath = path.join(outDir, permalink, 'index.html');
280
- const withoutTrailingSlashPath = path.join(
281
- outDir,
282
- `${permalink.replace(/\/$/, '')}.html`,
283
- );
284
- if (trailingSlash) {
285
- return fs.readFile(withTrailingSlashPath);
286
- } else if (trailingSlash === false) {
287
- return fs.readFile(withoutTrailingSlashPath);
288
- }
289
- const HTMLPath = await findAsyncSequential(
290
- [withTrailingSlashPath, withoutTrailingSlashPath],
291
- fs.pathExists,
292
- );
293
- if (!HTMLPath) {
294
- throw new Error(
295
- `Expected output HTML file to be found at ${withTrailingSlashPath}`,
296
- );
297
- }
298
- return fs.readFile(HTMLPath);
299
- }
300
-
301
- export async function mapAsyncSequential<T, R>(
302
- array: T[],
303
- action: (t: T) => Promise<R>,
304
- ): Promise<R[]> {
305
- const results: R[] = [];
306
- for (const t of array) {
307
- const result = await action(t);
308
- results.push(result);
309
- }
310
- return results;
311
- }
312
-
313
- export async function findAsyncSequential<T>(
314
- array: T[],
315
- predicate: (t: T) => Promise<boolean>,
316
- ): Promise<T | undefined> {
317
- for (const t of array) {
318
- if (await predicate(t)) {
319
- return t;
320
- }
321
- }
322
- return undefined;
323
- }
324
-
325
- export function reportMessage(
326
- message: string,
327
- reportingSeverity: ReportingSeverity,
328
- ): void {
329
- switch (reportingSeverity) {
330
- case 'ignore':
331
- break;
332
- case 'log':
333
- logger.info(message);
334
- break;
335
- case 'warn':
336
- logger.warn(message);
337
- break;
338
- case 'error':
339
- logger.error(message);
340
- break;
341
- case 'throw':
342
- throw new Error(message);
343
- default:
344
- throw new Error(
345
- `Unexpected "reportingSeverity" value: ${reportingSeverity}.`,
346
- );
347
- }
348
- }
349
-
350
- export function mergeTranslations(
351
- contents: TranslationFileContent[],
352
- ): TranslationFileContent {
353
- return contents.reduce((acc, content) => ({...acc, ...content}), {});
354
- }
355
-
356
- // Useful to update all the messages of a translation file
357
- // Used in tests to simulate translations
358
- export function updateTranslationFileMessages(
359
- translationFile: TranslationFile,
360
- updateMessage: (message: string) => string,
361
- ): TranslationFile {
362
- return {
363
- ...translationFile,
364
- content: _.mapValues(translationFile.content, (translation) => ({
365
- ...translation,
366
- message: updateMessage(translation.message),
367
- })),
368
- };
369
- }
package/src/jsUtils.ts ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import type {ReportingSeverity} from '@docusaurus/types';
9
+ import logger from '@docusaurus/logger';
10
+
11
+ /** Removes a given string suffix from `str`. */
12
+ export function removeSuffix(str: string, suffix: string): string {
13
+ if (suffix === '') {
14
+ // str.slice(0, 0) is ""
15
+ return str;
16
+ }
17
+ return str.endsWith(suffix) ? str.slice(0, -suffix.length) : str;
18
+ }
19
+
20
+ /** Removes a given string prefix from `str`. */
21
+ export function removePrefix(str: string, prefix: string): string {
22
+ return str.startsWith(prefix) ? str.slice(prefix.length) : str;
23
+ }
24
+
25
+ /**
26
+ * `Array#map` for async operations where order matters.
27
+ * @param array The array to traverse.
28
+ * @param action An async action to be performed on every array item. Will be
29
+ * awaited before working on the next.
30
+ * @returns The list of results returned from every `action(item)`
31
+ */
32
+ export async function mapAsyncSequential<T, R>(
33
+ array: T[],
34
+ action: (t: T) => Promise<R>,
35
+ ): Promise<R[]> {
36
+ const results: R[] = [];
37
+ for (const t of array) {
38
+ const result = await action(t);
39
+ results.push(result);
40
+ }
41
+ return results;
42
+ }
43
+
44
+ /**
45
+ * `Array#find` for async operations where order matters.
46
+ * @param array The array to traverse.
47
+ * @param predicate An async predicate to be called on every array item. Should
48
+ * return a boolean indicating whether the currently element should be returned.
49
+ * @returns The function immediately returns the first item on which `predicate`
50
+ * returns `true`, or `undefined` if none matches the predicate.
51
+ */
52
+ export async function findAsyncSequential<T>(
53
+ array: T[],
54
+ predicate: (t: T) => Promise<boolean>,
55
+ ): Promise<T | undefined> {
56
+ for (const t of array) {
57
+ if (await predicate(t)) {
58
+ return t;
59
+ }
60
+ }
61
+ return undefined;
62
+ }
63
+
64
+ /**
65
+ * Takes a message and reports it according to the severity that the user wants.
66
+ *
67
+ * - `ignore`: completely no-op
68
+ * - `log`: uses the `INFO` log level
69
+ * - `warn`: uses the `WARN` log level
70
+ * - `error`: uses the `ERROR` log level
71
+ * - `throw`: aborts the process, throws the error.
72
+ *
73
+ * Since the logger doesn't have logging level filters yet, these severities
74
+ * mostly just differ by their colors.
75
+ *
76
+ * @throws In addition to throwing when `reportingSeverity === "throw"`, this
77
+ * function also throws if `reportingSeverity` is not one of the above.
78
+ */
79
+ export function reportMessage(
80
+ message: string,
81
+ reportingSeverity: ReportingSeverity,
82
+ ): void {
83
+ switch (reportingSeverity) {
84
+ case 'ignore':
85
+ break;
86
+ case 'log':
87
+ logger.info(message);
88
+ break;
89
+ case 'warn':
90
+ logger.warn(message);
91
+ break;
92
+ case 'error':
93
+ logger.error(message);
94
+ break;
95
+ case 'throw':
96
+ throw new Error(message);
97
+ default:
98
+ throw new Error(
99
+ `Unexpected "reportingSeverity" value: ${reportingSeverity}.`,
100
+ );
101
+ }
102
+ }
@@ -6,41 +6,79 @@
6
6
  */
7
7
 
8
8
  import path from 'path';
9
+ import {getContentPathList} from './dataFileUtils';
9
10
  import {aliasedSitePath} from './pathUtils';
10
11
 
12
+ /**
13
+ * Content plugins have a base path and a localized path to source content from.
14
+ * We will look into the localized path in priority.
15
+ */
11
16
  export type ContentPaths = {
17
+ /**
18
+ * The absolute path to the base content directory, like `"<siteDir>/docs"`.
19
+ */
12
20
  contentPath: string;
21
+ /**
22
+ * The absolute path to the localized content directory, like
23
+ * `"<siteDir>/i18n/zh-Hans/plugin-content-docs"`.
24
+ */
13
25
  contentPathLocalized: string;
14
26
  };
15
27
 
28
+ /** Data structure representing each broken Markdown link to be reported. */
16
29
  export type BrokenMarkdownLink<T extends ContentPaths> = {
30
+ /** Absolute path to the file containing this link. */
17
31
  filePath: string;
32
+ /**
33
+ * This is generic because it may contain extra metadata like version name,
34
+ * which the reporter can provide for context.
35
+ */
18
36
  contentPaths: T;
37
+ /**
38
+ * The content of the link, like `"./brokenFile.md"`
39
+ */
19
40
  link: string;
20
41
  };
21
42
 
22
- export type ReplaceMarkdownLinksParams<T extends ContentPaths> = {
23
- siteDir: string;
24
- fileString: string;
25
- filePath: string;
26
- contentPaths: T;
27
- sourceToPermalink: Record<string, string>;
28
- };
29
-
30
- export type ReplaceMarkdownLinksReturn<T extends ContentPaths> = {
31
- newContent: string;
32
- brokenMarkdownLinks: BrokenMarkdownLink<T>[];
33
- };
34
-
43
+ /**
44
+ * Takes a Markdown file and replaces relative file references with their URL
45
+ * counterparts, e.g. `[link](./intro.md)` => `[link](/docs/intro)`, preserving
46
+ * everything else.
47
+ *
48
+ * This method uses best effort to find a matching file. The file reference can
49
+ * be relative to the directory of the current file (most likely) or any of the
50
+ * content paths (so `/tutorials/intro.md` can be resolved as
51
+ * `<siteDir>/docs/tutorials/intro.md`). Links that contain the `http(s):` or
52
+ * `@site/` prefix will always be ignored.
53
+ */
35
54
  export function replaceMarkdownLinks<T extends ContentPaths>({
36
55
  siteDir,
37
56
  fileString,
38
57
  filePath,
39
58
  contentPaths,
40
59
  sourceToPermalink,
41
- }: ReplaceMarkdownLinksParams<T>): ReplaceMarkdownLinksReturn<T> {
42
- const {contentPath, contentPathLocalized} = contentPaths;
43
-
60
+ }: {
61
+ /** Absolute path to the site directory, used to resolve aliased paths. */
62
+ siteDir: string;
63
+ /** The Markdown file content to be processed. */
64
+ fileString: string;
65
+ /** Absolute path to the current file containing `fileString`. */
66
+ filePath: string;
67
+ /** The content paths which the file reference may live in. */
68
+ contentPaths: T;
69
+ /**
70
+ * A map from source paths to their URLs. Source paths are `@site` aliased.
71
+ */
72
+ sourceToPermalink: {[aliasedPath: string]: string};
73
+ }): {
74
+ /**
75
+ * The content with all Markdown file references replaced with their URLs.
76
+ * Unresolved links are left as-is.
77
+ */
78
+ newContent: string;
79
+ /** The list of broken links, */
80
+ brokenMarkdownLinks: BrokenMarkdownLink<T>[];
81
+ } {
44
82
  const brokenMarkdownLinks: BrokenMarkdownLink<T>[] = [];
45
83
 
46
84
  // Replace internal markdown linking (except in fenced blocks).
@@ -48,12 +86,13 @@ export function replaceMarkdownLinks<T extends ContentPaths>({
48
86
  let lastCodeFence = '';
49
87
  const lines = fileString.split('\n').map((line) => {
50
88
  if (line.trim().startsWith('```')) {
89
+ const codeFence = line.trim().match(/^`+/)![0]!;
51
90
  if (!fencedBlock) {
52
91
  fencedBlock = true;
53
- [lastCodeFence] = line.trim().match(/^`+/)!;
92
+ lastCodeFence = codeFence;
54
93
  // If we are in a ````-fenced block, all ``` would be plain text instead
55
94
  // of fences
56
- } else if (line.trim().match(/^`+/)![0].length >= lastCodeFence.length) {
95
+ } else if (codeFence.length >= lastCodeFence.length) {
57
96
  fencedBlock = false;
58
97
  }
59
98
  }
@@ -63,23 +102,27 @@ export function replaceMarkdownLinks<T extends ContentPaths>({
63
102
 
64
103
  let modifiedLine = line;
65
104
  // Replace inline-style links or reference-style links e.g:
66
- // This is [Document 1](doc1.md) -> we replace this doc1.md with correct
67
- // ink
68
- // [doc1]: doc1.md -> we replace this doc1.md with correct link
105
+ // This is [Document 1](doc1.md)
106
+ // [doc1]: doc1.md
69
107
  const mdRegex =
70
- /(?:(?:\]\()|(?:\]:\s*))(?!https?:\/\/|@site\/)(?<filename>[^'")\]\s>]+\.mdx?)/g;
108
+ /(?:\]\(|\]:\s*)(?!https?:\/\/|@site\/)(?<filename>[^'")\]\s>]+\.mdx?)/g;
71
109
  let mdMatch = mdRegex.exec(modifiedLine);
72
110
  while (mdMatch !== null) {
73
111
  // Replace it to correct html link.
74
- const mdLink = mdMatch.groups!.filename;
112
+ const mdLink = mdMatch.groups!.filename!;
75
113
 
76
- const sourcesToTry = [
77
- path.resolve(path.dirname(filePath), decodeURIComponent(mdLink)),
78
- `${contentPathLocalized}/${decodeURIComponent(mdLink)}`,
79
- `${contentPath}/${decodeURIComponent(mdLink)}`,
80
- ];
114
+ const sourcesToTry: string[] = [];
115
+ // ./file.md and ../file.md are always relative to the current file
116
+ if (!mdLink.startsWith('./') && !mdLink.startsWith('../')) {
117
+ sourcesToTry.push(...getContentPathList(contentPaths), siteDir);
118
+ }
119
+ // /file.md is always relative to the content path
120
+ if (!mdLink.startsWith('/')) {
121
+ sourcesToTry.push(path.dirname(filePath));
122
+ }
81
123
 
82
124
  const aliasedSourceMatch = sourcesToTry
125
+ .map((p) => path.join(p, decodeURIComponent(mdLink)))
83
126
  .map((source) => aliasedSitePath(source, siteDir))
84
127
  .find((source) => sourceToPermalink[source]);
85
128