@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.
Files changed (81) hide show
  1. package/lib/constants.d.ts +49 -1
  2. package/lib/constants.d.ts.map +1 -1
  3. package/lib/constants.js +53 -8
  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 +34 -7
  8. package/lib/dataFileUtils.js.map +1 -1
  9. package/lib/emitUtils.d.ts +12 -0
  10. package/lib/emitUtils.d.ts.map +1 -1
  11. package/lib/emitUtils.js +22 -3
  12. package/lib/emitUtils.js.map +1 -1
  13. package/lib/gitUtils.d.ts +42 -2
  14. package/lib/gitUtils.d.ts.map +1 -1
  15. package/lib/gitUtils.js +2 -2
  16. package/lib/gitUtils.js.map +1 -1
  17. package/lib/globUtils.d.ts +27 -0
  18. package/lib/globUtils.d.ts.map +1 -1
  19. package/lib/globUtils.js +28 -10
  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 +6 -5
  24. package/lib/hashUtils.js.map +1 -1
  25. package/lib/i18nUtils.d.ts +11 -0
  26. package/lib/i18nUtils.d.ts.map +1 -1
  27. package/lib/i18nUtils.js +12 -3
  28. package/lib/i18nUtils.js.map +1 -1
  29. package/lib/index.d.ts +4 -4
  30. package/lib/index.d.ts.map +1 -1
  31. package/lib/index.js +4 -4
  32. package/lib/index.js.map +1 -1
  33. package/lib/jsUtils.d.ts +32 -4
  34. package/lib/jsUtils.d.ts.map +1 -1
  35. package/lib/jsUtils.js +35 -13
  36. package/lib/jsUtils.js.map +1 -1
  37. package/lib/markdownLinks.d.ts +45 -4
  38. package/lib/markdownLinks.d.ts.map +1 -1
  39. package/lib/markdownLinks.js +17 -8
  40. package/lib/markdownLinks.js.map +1 -1
  41. package/lib/markdownUtils.d.ts +80 -9
  42. package/lib/markdownUtils.d.ts.map +1 -1
  43. package/lib/markdownUtils.js +61 -10
  44. package/lib/markdownUtils.js.map +1 -1
  45. package/lib/pathUtils.d.ts +1 -1
  46. package/lib/pathUtils.d.ts.map +1 -1
  47. package/lib/pathUtils.js +2 -2
  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 +4 -0
  52. package/lib/slugger.js.map +1 -1
  53. package/lib/tags.d.ts +31 -10
  54. package/lib/tags.d.ts.map +1 -1
  55. package/lib/tags.js +38 -24
  56. package/lib/tags.js.map +1 -1
  57. package/lib/urlUtils.d.ts +45 -2
  58. package/lib/urlUtils.d.ts.map +1 -1
  59. package/lib/urlUtils.js +48 -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 +6 -2
  64. package/lib/webpackUtils.js.map +1 -1
  65. package/package.json +4 -4
  66. package/src/constants.ts +59 -7
  67. package/src/dataFileUtils.ts +43 -10
  68. package/src/emitUtils.ts +24 -5
  69. package/src/gitUtils.ts +46 -4
  70. package/src/globUtils.ts +29 -13
  71. package/src/hashUtils.ts +6 -5
  72. package/src/i18nUtils.ts +13 -4
  73. package/src/index.ts +3 -6
  74. package/src/jsUtils.ts +34 -20
  75. package/src/markdownLinks.ts +58 -22
  76. package/src/markdownUtils.ts +101 -23
  77. package/src/pathUtils.ts +2 -2
  78. package/src/slugger.ts +13 -1
  79. package/src/tags.ts +37 -25
  80. package/src/urlUtils.ts +49 -6
  81. 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
- // The default patterns we ignore when globbing
16
- // using _ prefix for exclusion by convention
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
- // We use match patterns like '**/_*/**',
39
- // This function permits to help to:
40
- // Match /user/sebastien/website/docs/_partials/xyz.md
41
- // Ignore /user/_sebastien/website/docs/partials/xyz.md
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 ${JSON.stringify(
55
- rootFolders,
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).substr(0, length);
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
- * Avoid str collision.
24
- * Also removes part of the string if its larger than the allowed
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
- // Useful to update all the messages of a translation file
20
- // Used in tests to simulate translations
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
- 'i18n',
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
- STATIC_DIR_NAME,
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
- return str; // always returns "" otherwise!
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
- export function getElementsAround<T>(
23
- array: T[],
24
- aroundIndex: number,
25
- ): {
26
- next: T | undefined;
27
- previous: T | undefined;
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,
@@ -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> = {
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
- export type ReplaceMarkdownLinksReturn<T extends ContentPaths> = {
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) -> we replace this doc1.md with correct
68
- // ink
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.resolve(path.dirname(filePath), decodeURIComponent(mdLink)),
79
- `${contentPathLocalized}/${decodeURIComponent(mdLink)}`,
80
- `${contentPath}/${decodeURIComponent(mdLink)}`,
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))
@@ -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
- // Input: ## Some heading {#some-heading}
13
- // Output: {text: "## Some heading", id: "some-heading"}
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
- .trimLeft()
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?: {removeContentTitle?: boolean},
131
- ): {content: string; contentTitle: string | undefined} {
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
- type ParsedMarkdown = {
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
- content: string;
248
+ /** @see {@link parseMarkdownContentTitle} */
177
249
  contentTitle: string | undefined;
250
+ /** @see {@link createExcerpt} */
178
251
  excerpt: string | undefined;
179
- };
180
-
181
- export function parseMarkdownString(
182
- markdownFileContent: string,
183
- options?: {removeContentTitle?: boolean},
184
- ): ParsedMarkdown {
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
- maintainCase?: boolean;
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 const shortName = (str: string): string => {
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 = {maintainCase?: boolean};
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 {