@docusaurus/utils 0.0.0-4756 → 0.0.0-4760
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 +49 -1
- package/lib/constants.d.ts.map +1 -1
- package/lib/constants.js +53 -8
- 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 +34 -7
- package/lib/dataFileUtils.js.map +1 -1
- package/lib/emitUtils.d.ts +12 -0
- package/lib/emitUtils.d.ts.map +1 -1
- package/lib/emitUtils.js +22 -3
- package/lib/emitUtils.js.map +1 -1
- package/lib/gitUtils.d.ts +42 -2
- package/lib/gitUtils.d.ts.map +1 -1
- package/lib/gitUtils.js +2 -2
- package/lib/gitUtils.js.map +1 -1
- package/lib/globUtils.d.ts +27 -0
- package/lib/globUtils.d.ts.map +1 -1
- package/lib/globUtils.js +28 -10
- 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 +6 -5
- package/lib/hashUtils.js.map +1 -1
- package/lib/i18nUtils.d.ts +11 -0
- package/lib/i18nUtils.d.ts.map +1 -1
- package/lib/i18nUtils.js +12 -3
- package/lib/i18nUtils.js.map +1 -1
- package/lib/index.d.ts +4 -4
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -4
- package/lib/index.js.map +1 -1
- package/lib/jsUtils.d.ts +32 -4
- package/lib/jsUtils.d.ts.map +1 -1
- package/lib/jsUtils.js +35 -13
- package/lib/jsUtils.js.map +1 -1
- package/lib/markdownLinks.d.ts +45 -4
- package/lib/markdownLinks.d.ts.map +1 -1
- package/lib/markdownLinks.js +17 -8
- package/lib/markdownLinks.js.map +1 -1
- package/lib/markdownUtils.d.ts +80 -9
- package/lib/markdownUtils.d.ts.map +1 -1
- package/lib/markdownUtils.js +61 -10
- package/lib/markdownUtils.js.map +1 -1
- package/lib/pathUtils.d.ts +1 -1
- package/lib/pathUtils.d.ts.map +1 -1
- package/lib/pathUtils.js +2 -2
- 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 +4 -0
- package/lib/slugger.js.map +1 -1
- package/lib/tags.d.ts +31 -10
- package/lib/tags.d.ts.map +1 -1
- package/lib/tags.js +38 -24
- package/lib/tags.js.map +1 -1
- package/lib/urlUtils.d.ts +45 -2
- package/lib/urlUtils.d.ts.map +1 -1
- package/lib/urlUtils.js +48 -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 +6 -2
- package/lib/webpackUtils.js.map +1 -1
- package/package.json +4 -4
- package/src/constants.ts +59 -7
- package/src/dataFileUtils.ts +43 -10
- package/src/emitUtils.ts +24 -5
- package/src/gitUtils.ts +46 -4
- package/src/globUtils.ts +29 -13
- package/src/hashUtils.ts +6 -5
- package/src/i18nUtils.ts +13 -4
- package/src/index.ts +3 -6
- package/src/jsUtils.ts +34 -20
- package/src/markdownLinks.ts +58 -22
- package/src/markdownUtils.ts +101 -23
- package/src/pathUtils.ts +2 -2
- package/src/slugger.ts +13 -1
- package/src/tags.ts +37 -25
- package/src/urlUtils.ts +49 -6
- package/src/webpackUtils.ts +10 -2
package/src/globUtils.ts
CHANGED
|
@@ -10,24 +10,31 @@
|
|
|
10
10
|
import Micromatch from 'micromatch'; // Note: Micromatch is used by Globby
|
|
11
11
|
import path from 'path';
|
|
12
12
|
|
|
13
|
+
/** A re-export of the globby instance. */
|
|
13
14
|
export {default as Globby} from 'globby';
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The default glob patterns we ignore when sourcing content.
|
|
18
|
+
* - Ignore files and folders starting with `_` recursively
|
|
19
|
+
* - Ignore tests
|
|
20
|
+
*/
|
|
17
21
|
export const GlobExcludeDefault = [
|
|
18
|
-
// Ignore files starting with _
|
|
19
22
|
'**/_*.{js,jsx,ts,tsx,md,mdx}',
|
|
20
|
-
|
|
21
|
-
// Ignore folders starting with _ (including folder content)
|
|
22
23
|
'**/_*/**',
|
|
23
|
-
|
|
24
|
-
// Ignore tests
|
|
25
24
|
'**/*.test.{js,jsx,ts,tsx}',
|
|
26
25
|
'**/__tests__/**',
|
|
27
26
|
];
|
|
28
27
|
|
|
29
28
|
type Matcher = (str: string) => boolean;
|
|
30
29
|
|
|
30
|
+
/**
|
|
31
|
+
* A very thin wrapper around `Micromatch.makeRe`.
|
|
32
|
+
*
|
|
33
|
+
* @see {@link createAbsoluteFilePathMatcher}
|
|
34
|
+
* @param patterns A list of glob patterns.
|
|
35
|
+
* @returns A matcher handle that tells if a file path is matched by any of the
|
|
36
|
+
* patterns.
|
|
37
|
+
*/
|
|
31
38
|
export function createMatcher(patterns: string[]): Matcher {
|
|
32
39
|
const regexp = new RegExp(
|
|
33
40
|
patterns.map((pattern) => Micromatch.makeRe(pattern).source).join('|'),
|
|
@@ -35,10 +42,19 @@ export function createMatcher(patterns: string[]): Matcher {
|
|
|
35
42
|
return (str) => regexp.test(str);
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
/**
|
|
46
|
+
* We use match patterns like `"** /_* /**"` (ignore the spaces), where `"_*"`
|
|
47
|
+
* should only be matched within a subfolder. This function would:
|
|
48
|
+
* - Match `/user/sebastien/website/docs/_partials/xyz.md`
|
|
49
|
+
* - Ignore `/user/_sebastien/website/docs/partials/xyz.md`
|
|
50
|
+
*
|
|
51
|
+
* @param patterns A list of glob patterns.
|
|
52
|
+
* @param rootFolders A list of root folders to resolve the glob from.
|
|
53
|
+
* @returns A matcher handle that tells if a file path is matched by any of the
|
|
54
|
+
* patterns, resolved from the first root folder that contains the path.
|
|
55
|
+
* @throws Throws when the returned matcher receives a path that doesn't belong
|
|
56
|
+
* to any of the `rootFolders`.
|
|
57
|
+
*/
|
|
42
58
|
export function createAbsoluteFilePathMatcher(
|
|
43
59
|
patterns: string[],
|
|
44
60
|
rootFolders: string[],
|
|
@@ -51,8 +67,8 @@ export function createAbsoluteFilePathMatcher(
|
|
|
51
67
|
);
|
|
52
68
|
if (!rootFolder) {
|
|
53
69
|
throw new Error(
|
|
54
|
-
`createAbsoluteFilePathMatcher unexpected error, absoluteFilePath=${absoluteFilePath} was not contained in any of the root folders ${
|
|
55
|
-
|
|
70
|
+
`createAbsoluteFilePathMatcher unexpected error, absoluteFilePath=${absoluteFilePath} was not contained in any of the root folders: ${rootFolders.join(
|
|
71
|
+
', ',
|
|
56
72
|
)}`,
|
|
57
73
|
);
|
|
58
74
|
}
|
package/src/hashUtils.ts
CHANGED
|
@@ -9,20 +9,21 @@ import {createHash} from 'crypto';
|
|
|
9
9
|
import _ from 'lodash';
|
|
10
10
|
import {shortName, isNameTooLong} from './pathUtils';
|
|
11
11
|
|
|
12
|
+
/** Thin wrapper around `crypto.createHash("md5")`. */
|
|
12
13
|
export function md5Hash(str: string): string {
|
|
13
14
|
return createHash('md5').update(str).digest('hex');
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
/** Creates an MD5 hash and truncates it to the given length. */
|
|
16
18
|
export function simpleHash(str: string, length: number): string {
|
|
17
|
-
return md5Hash(str).
|
|
19
|
+
return md5Hash(str).substring(0, length);
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
// Based on https://github.com/gatsbyjs/gatsby/pull/21518/files
|
|
21
23
|
/**
|
|
22
|
-
* Given an input string, convert to kebab-case and append a hash
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* filename per OS. Avoids ERRNAMETOOLONG error.
|
|
24
|
+
* Given an input string, convert to kebab-case and append a hash, avoiding name
|
|
25
|
+
* collision. Also removes part of the string if its larger than the allowed
|
|
26
|
+
* filename per OS, avoiding `ERRNAMETOOLONG` error.
|
|
26
27
|
*/
|
|
27
28
|
export function docuHash(str: string): string {
|
|
28
29
|
if (str === '/') {
|
package/src/i18nUtils.ts
CHANGED
|
@@ -8,16 +8,21 @@
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import _ from 'lodash';
|
|
10
10
|
import type {TranslationFileContent, TranslationFile} from '@docusaurus/types';
|
|
11
|
-
import {DEFAULT_PLUGIN_ID} from './constants';
|
|
11
|
+
import {DEFAULT_PLUGIN_ID, I18N_DIR_NAME} from './constants';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Takes a list of translation file contents, and shallow-merges them into one.
|
|
15
|
+
*/
|
|
13
16
|
export function mergeTranslations(
|
|
14
17
|
contents: TranslationFileContent[],
|
|
15
18
|
): TranslationFileContent {
|
|
16
19
|
return contents.reduce((acc, content) => ({...acc, ...content}), {});
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Useful to update all the messages of a translation file. Used in tests to
|
|
24
|
+
* simulate translations.
|
|
25
|
+
*/
|
|
21
26
|
export function updateTranslationFileMessages(
|
|
22
27
|
translationFile: TranslationFile,
|
|
23
28
|
updateMessage: (message: string) => string,
|
|
@@ -31,6 +36,10 @@ export function updateTranslationFileMessages(
|
|
|
31
36
|
};
|
|
32
37
|
}
|
|
33
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Takes everything needed and constructs a plugin i18n path. Plugins should
|
|
41
|
+
* expect everything it needs for translations to be found under this path.
|
|
42
|
+
*/
|
|
34
43
|
export function getPluginI18nPath({
|
|
35
44
|
siteDir,
|
|
36
45
|
locale,
|
|
@@ -46,7 +55,7 @@ export function getPluginI18nPath({
|
|
|
46
55
|
}): string {
|
|
47
56
|
return path.join(
|
|
48
57
|
siteDir,
|
|
49
|
-
|
|
58
|
+
I18N_DIR_NAME,
|
|
50
59
|
// namespace first by locale: convenient to work in a single folder for a
|
|
51
60
|
// translator
|
|
52
61
|
locale,
|
package/src/index.ts
CHANGED
|
@@ -13,9 +13,11 @@ export {
|
|
|
13
13
|
BABEL_CONFIG_FILE_NAME,
|
|
14
14
|
GENERATED_FILES_DIR_NAME,
|
|
15
15
|
SRC_DIR_NAME,
|
|
16
|
-
|
|
16
|
+
DEFAULT_STATIC_DIR_NAME,
|
|
17
17
|
OUTPUT_STATIC_ASSETS_DIR_NAME,
|
|
18
18
|
THEME_PATH,
|
|
19
|
+
I18N_DIR_NAME,
|
|
20
|
+
CODE_TRANSLATIONS_FILE_NAME,
|
|
19
21
|
DEFAULT_PORT,
|
|
20
22
|
DEFAULT_PLUGIN_ID,
|
|
21
23
|
WEBPACK_URL_LOADER_LIMIT,
|
|
@@ -34,7 +36,6 @@ export {
|
|
|
34
36
|
export {
|
|
35
37
|
removeSuffix,
|
|
36
38
|
removePrefix,
|
|
37
|
-
getElementsAround,
|
|
38
39
|
mapAsyncSequential,
|
|
39
40
|
findAsyncSequential,
|
|
40
41
|
reportMessage,
|
|
@@ -56,8 +57,6 @@ export {
|
|
|
56
57
|
export {
|
|
57
58
|
type Tag,
|
|
58
59
|
type FrontMatterTag,
|
|
59
|
-
type TaggedItemGroup,
|
|
60
|
-
normalizeFrontMatterTag,
|
|
61
60
|
normalizeFrontMatterTags,
|
|
62
61
|
groupTaggedItems,
|
|
63
62
|
} from './tags';
|
|
@@ -73,8 +72,6 @@ export {
|
|
|
73
72
|
export {
|
|
74
73
|
type ContentPaths,
|
|
75
74
|
type BrokenMarkdownLink,
|
|
76
|
-
type ReplaceMarkdownLinksParams,
|
|
77
|
-
type ReplaceMarkdownLinksReturn,
|
|
78
75
|
replaceMarkdownLinks,
|
|
79
76
|
} from './markdownLinks';
|
|
80
77
|
export {type SluggerOptions, type Slugger, createSlugger} from './slugger';
|
package/src/jsUtils.ts
CHANGED
|
@@ -8,36 +8,27 @@
|
|
|
8
8
|
import type {ReportingSeverity} from '@docusaurus/types';
|
|
9
9
|
import logger from '@docusaurus/logger';
|
|
10
10
|
|
|
11
|
+
/** Removes a given string suffix from `str`. */
|
|
11
12
|
export function removeSuffix(str: string, suffix: string): string {
|
|
12
13
|
if (suffix === '') {
|
|
13
|
-
|
|
14
|
+
// str.slice(0, 0) is ""
|
|
15
|
+
return str;
|
|
14
16
|
}
|
|
15
17
|
return str.endsWith(suffix) ? str.slice(0, -suffix.length) : str;
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
/** Removes a given string prefix from `str`. */
|
|
18
21
|
export function removePrefix(str: string, prefix: string): string {
|
|
19
22
|
return str.startsWith(prefix) ? str.slice(prefix.length) : str;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const min = 0;
|
|
30
|
-
const max = array.length - 1;
|
|
31
|
-
if (aroundIndex < min || aroundIndex > max) {
|
|
32
|
-
throw new Error(
|
|
33
|
-
`Valid "aroundIndex" for array (of size ${array.length}) are between ${min} and ${max}, but you provided ${aroundIndex}.`,
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
const previous = aroundIndex === min ? undefined : array[aroundIndex - 1];
|
|
37
|
-
const next = aroundIndex === max ? undefined : array[aroundIndex + 1];
|
|
38
|
-
return {previous, next};
|
|
39
|
-
}
|
|
40
|
-
|
|
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
|
+
*/
|
|
41
32
|
export async function mapAsyncSequential<T, R>(
|
|
42
33
|
array: T[],
|
|
43
34
|
action: (t: T) => Promise<R>,
|
|
@@ -50,6 +41,14 @@ export async function mapAsyncSequential<T, R>(
|
|
|
50
41
|
return results;
|
|
51
42
|
}
|
|
52
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
|
+
*/
|
|
53
52
|
export async function findAsyncSequential<T>(
|
|
54
53
|
array: T[],
|
|
55
54
|
predicate: (t: T) => Promise<boolean>,
|
|
@@ -62,6 +61,21 @@ export async function findAsyncSequential<T>(
|
|
|
62
61
|
return undefined;
|
|
63
62
|
}
|
|
64
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
|
+
*/
|
|
65
79
|
export function reportMessage(
|
|
66
80
|
message: string,
|
|
67
81
|
reportingSeverity: ReportingSeverity,
|
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
|
-
|
|
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
|
+
*/
|
|
54
|
+
export function replaceMarkdownLinks<T extends ContentPaths>({
|
|
55
|
+
siteDir,
|
|
56
|
+
fileString,
|
|
57
|
+
filePath,
|
|
58
|
+
contentPaths,
|
|
59
|
+
sourceToPermalink,
|
|
60
|
+
}: {
|
|
61
|
+
/** Absolute path to the site directory, used to resolve aliased paths. */
|
|
23
62
|
siteDir: string;
|
|
63
|
+
/** The Markdown file content to be processed. */
|
|
24
64
|
fileString: string;
|
|
65
|
+
/** Absolute path to the current file containing `fileString`. */
|
|
25
66
|
filePath: string;
|
|
67
|
+
/** The content paths which the file reference may live in. */
|
|
26
68
|
contentPaths: T;
|
|
69
|
+
/**
|
|
70
|
+
* A map from source paths to their URLs. Source paths are `@site` aliased.
|
|
71
|
+
*/
|
|
27
72
|
sourceToPermalink: Record<string, string>;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
73
|
+
}): {
|
|
74
|
+
/**
|
|
75
|
+
* The content with all Markdown file references replaced with their URLs.
|
|
76
|
+
* Unresolved links are left as-is.
|
|
77
|
+
*/
|
|
31
78
|
newContent: string;
|
|
79
|
+
/** The list of broken links, */
|
|
32
80
|
brokenMarkdownLinks: BrokenMarkdownLink<T>[];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function replaceMarkdownLinks<T extends ContentPaths>({
|
|
36
|
-
siteDir,
|
|
37
|
-
fileString,
|
|
38
|
-
filePath,
|
|
39
|
-
contentPaths,
|
|
40
|
-
sourceToPermalink,
|
|
41
|
-
}: ReplaceMarkdownLinksParams<T>): ReplaceMarkdownLinksReturn<T> {
|
|
42
|
-
const {contentPath, contentPathLocalized} = contentPaths;
|
|
43
|
-
|
|
81
|
+
} {
|
|
44
82
|
const brokenMarkdownLinks: BrokenMarkdownLink<T>[] = [];
|
|
45
83
|
|
|
46
84
|
// Replace internal markdown linking (except in fenced blocks).
|
|
@@ -64,9 +102,8 @@ export function replaceMarkdownLinks<T extends ContentPaths>({
|
|
|
64
102
|
|
|
65
103
|
let modifiedLine = line;
|
|
66
104
|
// Replace inline-style links or reference-style links e.g:
|
|
67
|
-
// This is [Document 1](doc1.md)
|
|
68
|
-
//
|
|
69
|
-
// [doc1]: doc1.md -> we replace this doc1.md with correct link
|
|
105
|
+
// This is [Document 1](doc1.md)
|
|
106
|
+
// [doc1]: doc1.md
|
|
70
107
|
const mdRegex =
|
|
71
108
|
/(?:\]\(|\]:\s*)(?!https?:\/\/|@site\/)(?<filename>[^'")\]\s>]+\.mdx?)/g;
|
|
72
109
|
let mdMatch = mdRegex.exec(modifiedLine);
|
|
@@ -75,10 +112,9 @@ export function replaceMarkdownLinks<T extends ContentPaths>({
|
|
|
75
112
|
const mdLink = mdMatch.groups!.filename!;
|
|
76
113
|
|
|
77
114
|
const sourcesToTry = [
|
|
78
|
-
path.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
];
|
|
115
|
+
path.dirname(filePath),
|
|
116
|
+
...getContentPathList(contentPaths),
|
|
117
|
+
].map((p) => path.join(p, decodeURIComponent(mdLink)));
|
|
82
118
|
|
|
83
119
|
const aliasedSourceMatch = sourcesToTry
|
|
84
120
|
.map((source) => aliasedSitePath(source, siteDir))
|
package/src/markdownUtils.ts
CHANGED
|
@@ -7,12 +7,25 @@
|
|
|
7
7
|
|
|
8
8
|
import logger from '@docusaurus/logger';
|
|
9
9
|
import matter from 'gray-matter';
|
|
10
|
-
import {createSlugger, type Slugger} from './slugger';
|
|
10
|
+
import {createSlugger, type Slugger, type SluggerOptions} from './slugger';
|
|
11
11
|
|
|
12
|
-
//
|
|
13
|
-
//
|
|
12
|
+
// Some utilities for parsing Markdown content. These things are only used on
|
|
13
|
+
// server-side when we infer metadata like `title` and `description` from the
|
|
14
|
+
// content. Most parsing is still done in MDX through the mdx-loader.
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parses custom ID from a heading. The ID must be composed of letters,
|
|
18
|
+
* underscores, and dashes only.
|
|
19
|
+
*
|
|
20
|
+
* @param heading e.g. `## Some heading {#some-heading}` where the last
|
|
21
|
+
* character must be `}` for the ID to be recognized
|
|
22
|
+
*/
|
|
14
23
|
export function parseMarkdownHeadingId(heading: string): {
|
|
24
|
+
/**
|
|
25
|
+
* The heading content sans the ID part, right-trimmed. e.g. `## Some heading`
|
|
26
|
+
*/
|
|
15
27
|
text: string;
|
|
28
|
+
/** The heading ID. e.g. `some-heading` */
|
|
16
29
|
id?: string;
|
|
17
30
|
} {
|
|
18
31
|
const customHeadingIdRegex = /\s*\{#(?<id>[\w-]+)\}$/;
|
|
@@ -26,26 +39,40 @@ export function parseMarkdownHeadingId(heading: string): {
|
|
|
26
39
|
return {text: heading, id: undefined};
|
|
27
40
|
}
|
|
28
41
|
|
|
29
|
-
// Hacky way of stripping out import statements from the excerpt
|
|
30
42
|
// TODO: Find a better way to do so, possibly by compiling the Markdown content,
|
|
31
43
|
// stripping out HTML tags and obtaining the first line.
|
|
44
|
+
/**
|
|
45
|
+
* Creates an excerpt of a Markdown file. This function will:
|
|
46
|
+
*
|
|
47
|
+
* - Ignore h1 headings (setext or atx)
|
|
48
|
+
* - Ignore import/export
|
|
49
|
+
* - Ignore code blocks
|
|
50
|
+
*
|
|
51
|
+
* And for the first contentful line, it will strip away most Markdown
|
|
52
|
+
* syntax, including HTML tags, emphasis, links (keeping the text), etc.
|
|
53
|
+
*/
|
|
32
54
|
export function createExcerpt(fileString: string): string | undefined {
|
|
33
55
|
const fileLines = fileString
|
|
34
|
-
.
|
|
56
|
+
.trimStart()
|
|
35
57
|
// Remove Markdown alternate title
|
|
36
58
|
.replace(/^[^\n]*\n[=]+/g, '')
|
|
37
59
|
.split('\n');
|
|
38
60
|
let inCode = false;
|
|
61
|
+
let inImport = false;
|
|
39
62
|
let lastCodeFence = '';
|
|
40
63
|
|
|
41
64
|
for (const fileLine of fileLines) {
|
|
65
|
+
if (fileLine === '' && inImport) {
|
|
66
|
+
inImport = false;
|
|
67
|
+
}
|
|
42
68
|
// Skip empty line.
|
|
43
69
|
if (!fileLine.trim()) {
|
|
44
70
|
continue;
|
|
45
71
|
}
|
|
46
72
|
|
|
47
73
|
// Skip import/export declaration.
|
|
48
|
-
if (/^(?:import|export)\s.*/.test(fileLine)) {
|
|
74
|
+
if ((/^(?:import|export)\s.*/.test(fileLine) || inImport) && !inCode) {
|
|
75
|
+
inImport = true;
|
|
49
76
|
continue;
|
|
50
77
|
}
|
|
51
78
|
|
|
@@ -102,8 +129,22 @@ export function createExcerpt(fileString: string): string | undefined {
|
|
|
102
129
|
return undefined;
|
|
103
130
|
}
|
|
104
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Takes a raw Markdown file content, and parses the front matter using
|
|
134
|
+
* gray-matter. Worth noting that gray-matter accepts TOML and other markup
|
|
135
|
+
* languages as well.
|
|
136
|
+
*
|
|
137
|
+
* @throws Throws when gray-matter throws. e.g.:
|
|
138
|
+
* ```md
|
|
139
|
+
* ---
|
|
140
|
+
* foo: : bar
|
|
141
|
+
* ---
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
105
144
|
export function parseFrontMatter(markdownFileContent: string): {
|
|
145
|
+
/** Front matter as parsed by gray-matter. */
|
|
106
146
|
frontMatter: Record<string, unknown>;
|
|
147
|
+
/** The remaining content, trimmed. */
|
|
107
148
|
content: string;
|
|
108
149
|
} {
|
|
109
150
|
const {data, content} = matter(markdownFileContent);
|
|
@@ -113,11 +154,6 @@ export function parseFrontMatter(markdownFileContent: string): {
|
|
|
113
154
|
};
|
|
114
155
|
}
|
|
115
156
|
|
|
116
|
-
/**
|
|
117
|
-
* Try to convert markdown heading to text. Does not need to be perfect, it is
|
|
118
|
-
* only used as a fallback when frontMatter.title is not provided. For now, we
|
|
119
|
-
* just unwrap possible inline code blocks (# `config.js`)
|
|
120
|
-
*/
|
|
121
157
|
function toTextContentTitle(contentTitle: string): string {
|
|
122
158
|
if (contentTitle.startsWith('`') && contentTitle.endsWith('`')) {
|
|
123
159
|
return contentTitle.substring(1, contentTitle.length - 1);
|
|
@@ -125,10 +161,36 @@ function toTextContentTitle(contentTitle: string): string {
|
|
|
125
161
|
return contentTitle;
|
|
126
162
|
}
|
|
127
163
|
|
|
164
|
+
type ParseMarkdownContentTitleOptions = {
|
|
165
|
+
/**
|
|
166
|
+
* If `true`, the matching title will be removed from the returned content.
|
|
167
|
+
* We can promise that at least one empty line will be left between the
|
|
168
|
+
* content before and after, but you shouldn't make too much assumption
|
|
169
|
+
* about what's left.
|
|
170
|
+
*/
|
|
171
|
+
removeContentTitle?: boolean;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Takes the raw Markdown content, without front matter, and tries to find an h1
|
|
176
|
+
* title (setext or atx) to be used as metadata.
|
|
177
|
+
*
|
|
178
|
+
* It only searches until the first contentful paragraph, ignoring import/export
|
|
179
|
+
* declarations.
|
|
180
|
+
*
|
|
181
|
+
* It will try to convert markdown to reasonable text, but won't be best effort,
|
|
182
|
+
* since it's only used as a fallback when `frontMatter.title` is not provided.
|
|
183
|
+
* For now, we just unwrap inline code (``# `config.js` `` => `config.js`).
|
|
184
|
+
*/
|
|
128
185
|
export function parseMarkdownContentTitle(
|
|
129
186
|
contentUntrimmed: string,
|
|
130
|
-
options?:
|
|
131
|
-
): {
|
|
187
|
+
options?: ParseMarkdownContentTitleOptions,
|
|
188
|
+
): {
|
|
189
|
+
/** The content, optionally without the content title. */
|
|
190
|
+
content: string;
|
|
191
|
+
/** The title, trimmed and without the `#`. */
|
|
192
|
+
contentTitle: string | undefined;
|
|
193
|
+
} {
|
|
132
194
|
const removeContentTitleOption = options?.removeContentTitle ?? false;
|
|
133
195
|
|
|
134
196
|
const content = contentUntrimmed.trim();
|
|
@@ -171,17 +233,28 @@ export function parseMarkdownContentTitle(
|
|
|
171
233
|
};
|
|
172
234
|
}
|
|
173
235
|
|
|
174
|
-
|
|
236
|
+
/**
|
|
237
|
+
* Makes a full-round parse.
|
|
238
|
+
*
|
|
239
|
+
* @throws Throws when `parseFrontMatter` throws, usually because of invalid
|
|
240
|
+
* syntax.
|
|
241
|
+
*/
|
|
242
|
+
export function parseMarkdownString(
|
|
243
|
+
markdownFileContent: string,
|
|
244
|
+
options?: ParseMarkdownContentTitleOptions,
|
|
245
|
+
): {
|
|
246
|
+
/** @see {@link parseFrontMatter} */
|
|
175
247
|
frontMatter: Record<string, unknown>;
|
|
176
|
-
|
|
248
|
+
/** @see {@link parseMarkdownContentTitle} */
|
|
177
249
|
contentTitle: string | undefined;
|
|
250
|
+
/** @see {@link createExcerpt} */
|
|
178
251
|
excerpt: string | undefined;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
252
|
+
/**
|
|
253
|
+
* Content without front matter and (optionally) without title, depending on
|
|
254
|
+
* the `removeContentTitle` option.
|
|
255
|
+
*/
|
|
256
|
+
content: string;
|
|
257
|
+
} {
|
|
185
258
|
try {
|
|
186
259
|
const {frontMatter, content: contentWithoutFrontMatter} =
|
|
187
260
|
parseFrontMatter(markdownFileContent);
|
|
@@ -229,11 +302,16 @@ function addHeadingId(
|
|
|
229
302
|
return `${headingHashes}${headingText} {#${slug}}`;
|
|
230
303
|
}
|
|
231
304
|
|
|
232
|
-
export type WriteHeadingIDOptions = {
|
|
233
|
-
|
|
305
|
+
export type WriteHeadingIDOptions = SluggerOptions & {
|
|
306
|
+
/** Overwrite existing heading IDs. */
|
|
234
307
|
overwrite?: boolean;
|
|
235
308
|
};
|
|
236
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Takes Markdown content, returns new content with heading IDs written.
|
|
312
|
+
* Respects existing IDs (unless `overwrite=true`) and never generates colliding
|
|
313
|
+
* IDs (through the slugger).
|
|
314
|
+
*/
|
|
237
315
|
export function writeMarkdownHeadingId(
|
|
238
316
|
content: string,
|
|
239
317
|
options: WriteHeadingIDOptions = {maintainCase: false, overwrite: false},
|
package/src/pathUtils.ts
CHANGED
|
@@ -24,7 +24,7 @@ export const isNameTooLong = (str: string): boolean =>
|
|
|
24
24
|
? str.length + SPACE_FOR_APPENDING > MAX_PATH_SEGMENT_CHARS // MacOS (APFS) and Windows (NTFS) filename length limit (255 chars)
|
|
25
25
|
: Buffer.from(str).length + SPACE_FOR_APPENDING > MAX_PATH_SEGMENT_BYTES; // Other (255 bytes)
|
|
26
26
|
|
|
27
|
-
export
|
|
27
|
+
export function shortName(str: string): string {
|
|
28
28
|
if (isMacOs() || isWindows()) {
|
|
29
29
|
const overflowingChars = str.length - MAX_PATH_SEGMENT_CHARS;
|
|
30
30
|
return str.slice(
|
|
@@ -41,7 +41,7 @@ export const shortName = (str: string): string => {
|
|
|
41
41
|
Buffer.byteLength(strBuffer) - overflowingBytes - SPACE_FOR_APPENDING - 1,
|
|
42
42
|
)
|
|
43
43
|
.toString();
|
|
44
|
-
}
|
|
44
|
+
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Convert Windows backslash paths to posix style paths.
|
package/src/slugger.ts
CHANGED
|
@@ -10,12 +10,24 @@ import GithubSlugger from 'github-slugger';
|
|
|
10
10
|
// We create our own abstraction on top of the lib:
|
|
11
11
|
// - unify usage everywhere in the codebase
|
|
12
12
|
// - ability to add extra options
|
|
13
|
-
export type SluggerOptions = {
|
|
13
|
+
export type SluggerOptions = {
|
|
14
|
+
/** Keep the headings' casing, otherwise make all lowercase. */
|
|
15
|
+
maintainCase?: boolean;
|
|
16
|
+
};
|
|
14
17
|
|
|
15
18
|
export type Slugger = {
|
|
19
|
+
/**
|
|
20
|
+
* Takes a Markdown heading like "Josh Cena" and sluggifies it according to
|
|
21
|
+
* GitHub semantics (in this case `josh-cena`). Stateful, because if you try
|
|
22
|
+
* to sluggify "Josh Cena" again it would return `josh-cena-1`.
|
|
23
|
+
*/
|
|
16
24
|
slug: (value: string, options?: SluggerOptions) => string;
|
|
17
25
|
};
|
|
18
26
|
|
|
27
|
+
/**
|
|
28
|
+
* A thin wrapper around github-slugger. This is a factory function that returns
|
|
29
|
+
* a stateful Slugger object.
|
|
30
|
+
*/
|
|
19
31
|
export function createSlugger(): Slugger {
|
|
20
32
|
const githubSlugger = new GithubSlugger();
|
|
21
33
|
return {
|