@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.
- package/lib/constants.d.ts +51 -1
- package/lib/constants.d.ts.map +1 -1
- package/lib/constants.js +57 -10
- package/lib/constants.js.map +1 -1
- package/lib/dataFileUtils.d.ts +38 -2
- package/lib/dataFileUtils.d.ts.map +1 -1
- package/lib/dataFileUtils.js +39 -13
- package/lib/dataFileUtils.js.map +1 -1
- package/lib/emitUtils.d.ts +32 -0
- package/lib/emitUtils.d.ts.map +1 -0
- package/lib/emitUtils.js +80 -0
- package/lib/emitUtils.js.map +1 -0
- package/lib/gitUtils.d.ts +54 -5
- package/lib/gitUtils.d.ts.map +1 -1
- package/lib/gitUtils.js +17 -14
- package/lib/gitUtils.js.map +1 -1
- package/lib/globUtils.d.ts +28 -0
- package/lib/globUtils.d.ts.map +1 -1
- package/lib/globUtils.js +36 -13
- package/lib/globUtils.js.map +1 -1
- package/lib/hashUtils.d.ts +5 -4
- package/lib/hashUtils.d.ts.map +1 -1
- package/lib/hashUtils.js +7 -6
- package/lib/hashUtils.js.map +1 -1
- package/lib/i18nUtils.d.ts +51 -0
- package/lib/i18nUtils.d.ts.map +1 -0
- package/lib/i18nUtils.js +69 -0
- package/lib/i18nUtils.js.map +1 -0
- package/lib/index.d.ts +10 -54
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +56 -256
- package/lib/index.js.map +1 -1
- package/lib/jsUtils.d.ts +45 -0
- package/lib/jsUtils.d.ts.map +1 -0
- package/lib/jsUtils.js +94 -0
- package/lib/jsUtils.js.map +1 -0
- package/lib/markdownLinks.d.ts +48 -5
- package/lib/markdownLinks.d.ts.map +1 -1
- package/lib/markdownLinks.js +29 -13
- package/lib/markdownLinks.js.map +1 -1
- package/lib/markdownUtils.d.ts +112 -0
- package/lib/markdownUtils.d.ts.map +1 -0
- package/lib/markdownUtils.js +271 -0
- package/lib/markdownUtils.js.map +1 -0
- package/lib/pathUtils.d.ts +2 -1
- package/lib/pathUtils.d.ts.map +1 -1
- package/lib/pathUtils.js +16 -7
- package/lib/pathUtils.js.map +1 -1
- package/lib/slugger.d.ts +10 -0
- package/lib/slugger.d.ts.map +1 -1
- package/lib/slugger.js +6 -2
- package/lib/slugger.js.map +1 -1
- package/lib/tags.d.ts +42 -10
- package/lib/tags.d.ts.map +1 -1
- package/lib/tags.js +40 -26
- package/lib/tags.js.map +1 -1
- package/lib/urlUtils.d.ts +57 -0
- package/lib/urlUtils.d.ts.map +1 -1
- package/lib/urlUtils.js +132 -6
- package/lib/urlUtils.js.map +1 -1
- package/lib/webpackUtils.d.ts +5 -0
- package/lib/webpackUtils.d.ts.map +1 -1
- package/lib/webpackUtils.js +8 -5
- package/lib/webpackUtils.js.map +1 -1
- package/package.json +11 -11
- package/src/constants.ts +65 -9
- package/src/dataFileUtils.ts +44 -12
- package/src/emitUtils.ts +99 -0
- package/src/gitUtils.ts +77 -17
- package/src/globUtils.ts +34 -13
- package/src/hashUtils.ts +6 -5
- package/src/i18nUtils.ts +115 -0
- package/src/index.ts +43 -307
- package/src/jsUtils.ts +102 -0
- package/src/markdownLinks.ts +71 -28
- package/src/markdownUtils.ts +354 -0
- package/src/pathUtils.ts +15 -7
- package/src/slugger.ts +13 -1
- package/src/tags.ts +53 -28
- package/src/urlUtils.ts +145 -7
- package/src/webpackUtils.ts +11 -4
- package/lib/markdownParser.d.ts +0 -32
- package/lib/markdownParser.d.ts.map +0 -1
- package/lib/markdownParser.js +0 -161
- package/lib/markdownParser.js.map +0 -1
- 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
|
-
|
|
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 {
|
|
41
|
-
export {
|
|
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
|
-
|
|
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
|
+
}
|
package/src/markdownLinks.ts
CHANGED
|
@@ -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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
}:
|
|
42
|
-
|
|
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
|
-
|
|
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 (
|
|
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)
|
|
67
|
-
//
|
|
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
|
-
/(
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|