@docusaurus/mdx-loader 3.0.0-beta.0 → 3.0.0-rc.1

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 (62) hide show
  1. package/lib/loader.d.ts.map +1 -1
  2. package/lib/loader.js +20 -11
  3. package/lib/loader.js.map +1 -1
  4. package/lib/processor.d.ts +13 -2
  5. package/lib/processor.d.ts.map +1 -1
  6. package/lib/processor.js +27 -16
  7. package/lib/processor.js.map +1 -1
  8. package/lib/remark/admonitions/index.d.ts.map +1 -1
  9. package/lib/remark/admonitions/index.js +5 -10
  10. package/lib/remark/admonitions/index.js.map +1 -1
  11. package/lib/remark/contentTitle/index.d.ts +15 -0
  12. package/lib/remark/contentTitle/index.d.ts.map +1 -0
  13. package/lib/remark/contentTitle/index.js +38 -0
  14. package/lib/remark/contentTitle/index.js.map +1 -0
  15. package/lib/remark/details/index.d.ts.map +1 -1
  16. package/lib/remark/details/index.js +3 -4
  17. package/lib/remark/details/index.js.map +1 -1
  18. package/lib/remark/head/index.d.ts.map +1 -1
  19. package/lib/remark/head/index.js +3 -4
  20. package/lib/remark/head/index.js.map +1 -1
  21. package/lib/remark/headings/index.d.ts.map +1 -1
  22. package/lib/remark/headings/index.js +2 -3
  23. package/lib/remark/headings/index.js.map +1 -1
  24. package/lib/remark/mdx1Compat/codeCompatPlugin.d.ts.map +1 -1
  25. package/lib/remark/mdx1Compat/codeCompatPlugin.js +4 -9
  26. package/lib/remark/mdx1Compat/codeCompatPlugin.js.map +1 -1
  27. package/lib/remark/mermaid/index.d.ts.map +1 -1
  28. package/lib/remark/mermaid/index.js +5 -5
  29. package/lib/remark/mermaid/index.js.map +1 -1
  30. package/lib/remark/toc/index.d.ts.map +1 -1
  31. package/lib/remark/toc/index.js +2 -2
  32. package/lib/remark/toc/index.js.map +1 -1
  33. package/lib/remark/transformImage/index.d.ts.map +1 -1
  34. package/lib/remark/transformImage/index.js +8 -7
  35. package/lib/remark/transformImage/index.js.map +1 -1
  36. package/lib/remark/transformLinks/index.d.ts.map +1 -1
  37. package/lib/remark/transformLinks/index.js +8 -7
  38. package/lib/remark/transformLinks/index.js.map +1 -1
  39. package/lib/remark/unusedDirectives/index.d.ts +4 -0
  40. package/lib/remark/unusedDirectives/index.d.ts.map +1 -0
  41. package/lib/remark/unusedDirectives/index.js +117 -0
  42. package/lib/remark/unusedDirectives/index.js.map +1 -0
  43. package/lib/remark/utils/index.d.ts +10 -3
  44. package/lib/remark/utils/index.d.ts.map +1 -1
  45. package/lib/remark/utils/index.js +19 -1
  46. package/lib/remark/utils/index.js.map +1 -1
  47. package/package.json +26 -25
  48. package/src/loader.ts +29 -17
  49. package/src/processor.ts +36 -18
  50. package/src/remark/admonitions/index.ts +3 -1
  51. package/src/remark/contentTitle/index.ts +55 -0
  52. package/src/remark/details/index.ts +2 -2
  53. package/src/remark/head/index.ts +2 -2
  54. package/src/remark/headings/index.ts +1 -1
  55. package/src/remark/mdx1Compat/codeCompatPlugin.ts +6 -8
  56. package/src/remark/mermaid/index.ts +7 -4
  57. package/src/remark/toc/index.ts +11 -5
  58. package/src/remark/transformImage/index.ts +9 -10
  59. package/src/remark/transformLinks/index.ts +9 -10
  60. package/src/remark/unusedDirectives/index.ts +167 -0
  61. package/src/remark/utils/index.ts +23 -2
  62. package/src/types.d.mts +34 -0
package/src/processor.ts CHANGED
@@ -5,8 +5,8 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
- import emoji from 'remark-emoji';
9
8
  import headings from './remark/headings';
9
+ import contentTitle from './remark/contentTitle';
10
10
  import toc from './remark/toc';
11
11
  import transformImage from './remark/transformImage';
12
12
  import transformLinks from './remark/transformLinks';
@@ -14,8 +14,10 @@ import details from './remark/details';
14
14
  import head from './remark/head';
15
15
  import mermaid from './remark/mermaid';
16
16
  import transformAdmonitions from './remark/admonitions';
17
+ import unusedDirectivesWarning from './remark/unusedDirectives';
17
18
  import codeCompatPlugin from './remark/mdx1Compat/codeCompatPlugin';
18
19
  import {getFormat} from './format';
20
+ import type {WebpackCompilerName} from '@docusaurus/utils';
19
21
  import type {MDXFrontMatter} from './frontMatter';
20
22
  import type {Options} from './loader';
21
23
  import type {AdmonitionOptions} from './remark/admonitions';
@@ -28,24 +30,27 @@ import type {ProcessorOptions} from '@mdx-js/mdx';
28
30
  // See https://github.com/microsoft/TypeScript/issues/49721#issuecomment-1517839391
29
31
  type Pluggable = any; // TODO fix this asap
30
32
 
33
+ type SimpleProcessorResult = {content: string; data: {[key: string]: unknown}};
34
+
31
35
  // TODO alt interface because impossible to import type Processor (ESM + TS :/)
32
36
  type SimpleProcessor = {
33
37
  process: ({
34
38
  content,
35
39
  filePath,
40
+ frontMatter,
41
+ compilerName,
36
42
  }: {
37
43
  content: string;
38
44
  filePath: string;
39
- }) => Promise<string>;
45
+ frontMatter: {[key: string]: unknown};
46
+ compilerName: WebpackCompilerName;
47
+ }) => Promise<SimpleProcessorResult>;
40
48
  };
41
49
 
42
- const DEFAULT_OPTIONS: MDXOptions = {
43
- admonitions: true,
44
- rehypePlugins: [],
45
- remarkPlugins: [headings, emoji, toc],
46
- beforeDefaultRemarkPlugins: [],
47
- beforeDefaultRehypePlugins: [],
48
- };
50
+ async function getDefaultRemarkPlugins(): Promise<MDXPlugin[]> {
51
+ const {default: emoji} = await import('remark-emoji');
52
+ return [headings, emoji, toc];
53
+ }
49
54
 
50
55
  export type MDXPlugin = Pluggable;
51
56
 
@@ -74,11 +79,15 @@ function getAdmonitionsPlugins(
74
79
  // Need to be async due to ESM dynamic imports...
75
80
  async function createProcessorFactory() {
76
81
  const {createProcessor: createMdxProcessor} = await import('@mdx-js/mdx');
82
+ const {default: frontmatter} = await import('remark-frontmatter');
77
83
  const {default: rehypeRaw} = await import('rehype-raw');
78
84
  const {default: gfm} = await import('remark-gfm');
79
85
  // TODO using fork until PR merged: https://github.com/leebyron/remark-comment/pull/3
80
86
  const {default: comment} = await import('@slorber/remark-comment');
81
87
  const {default: directive} = await import('remark-directive');
88
+ const {VFile} = await import('vfile');
89
+
90
+ const defaultRemarkPlugins = await getDefaultRemarkPlugins();
82
91
 
83
92
  // /!\ this method is synchronous on purpose
84
93
  // Using async code here can create cache entry race conditions!
@@ -91,9 +100,11 @@ async function createProcessorFactory() {
91
100
  }): SimpleProcessor {
92
101
  const remarkPlugins: MDXPlugin[] = [
93
102
  ...(options.beforeDefaultRemarkPlugins ?? []),
103
+ frontmatter,
94
104
  directive,
105
+ [contentTitle, {removeContentTitle: options.removeContentTitle}],
95
106
  ...getAdmonitionsPlugins(options.admonitions ?? false),
96
- ...DEFAULT_OPTIONS.remarkPlugins,
107
+ ...defaultRemarkPlugins,
97
108
  details,
98
109
  head,
99
110
  ...(options.markdownConfig.mermaid ? [mermaid] : []),
@@ -114,6 +125,7 @@ async function createProcessorFactory() {
114
125
  gfm,
115
126
  options.markdownConfig.mdx1Compat.comments ? comment : null,
116
127
  ...(options.remarkPlugins ?? []),
128
+ unusedDirectivesWarning,
117
129
  ].filter((plugin): plugin is MDXPlugin => Boolean(plugin));
118
130
 
119
131
  // codeCompatPlugin needs to be applied last after user-provided plugins
@@ -122,7 +134,6 @@ async function createProcessorFactory() {
122
134
 
123
135
  const rehypePlugins: MDXPlugin[] = [
124
136
  ...(options.beforeDefaultRehypePlugins ?? []),
125
- ...DEFAULT_OPTIONS.rehypePlugins,
126
137
  ...(options.rehypePlugins ?? []),
127
138
  ];
128
139
 
@@ -158,13 +169,20 @@ async function createProcessorFactory() {
158
169
  });
159
170
 
160
171
  return {
161
- process: async ({content, filePath}) =>
162
- mdxProcessor
163
- .process({
164
- value: content,
165
- path: filePath,
166
- })
167
- .then((res) => res.toString()),
172
+ process: async ({content, filePath, frontMatter, compilerName}) => {
173
+ const vfile = new VFile({
174
+ value: content,
175
+ path: filePath,
176
+ data: {
177
+ frontMatter,
178
+ compilerName,
179
+ },
180
+ });
181
+ return mdxProcessor.process(vfile).then((result) => ({
182
+ content: result.toString(),
183
+ data: result.data,
184
+ }));
185
+ },
168
186
  };
169
187
  }
170
188
 
@@ -4,7 +4,6 @@
4
4
  * This source code is licensed under the MIT license found in the
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
- import visit from 'unist-util-visit';
8
7
  // @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
9
8
  import type {Transformer, Processor} from 'unified';
10
9
 
@@ -67,6 +66,7 @@ function parseDirective(directive: ContainerDirective): {
67
66
  contentNodes: DirectiveContent;
68
67
  } {
69
68
  const hasDirectiveLabel =
69
+ // @ts-expect-error: fine
70
70
  directive.children?.[0]?.data?.directiveLabel === true;
71
71
  if (hasDirectiveLabel) {
72
72
  const [directiveLabel, ...contentNodes] = directive.children;
@@ -92,6 +92,8 @@ const plugin: Plugin = function plugin(
92
92
  const {keywords} = normalizeAdmonitionOptions(optionsInput);
93
93
 
94
94
  return async (root) => {
95
+ const {visit} = await import('unist-util-visit');
96
+
95
97
  visit(root, (node) => {
96
98
  if (node.type === 'containerDirective') {
97
99
  const directive = node as ContainerDirective;
@@ -0,0 +1,55 @@
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
+ // @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
9
+ import type {Transformer} from 'unified';
10
+ import type {Heading} from 'mdast';
11
+
12
+ // TODO as of April 2023, no way to import/re-export this ESM type easily :/
13
+ // TODO upgrade to TS 5.3
14
+ // See https://github.com/microsoft/TypeScript/issues/49721#issuecomment-1517839391
15
+ // import type {Plugin} from 'unified';
16
+ type Plugin = any; // TODO fix this asap
17
+
18
+ interface PluginOptions {
19
+ removeContentTitle?: boolean;
20
+ }
21
+
22
+ /**
23
+ * A remark plugin to extract the h1 heading found in Markdown files
24
+ * This is exposed as "data.contentTitle" to the processed vfile
25
+ * Also gives the ability to strip that content title (used for the blog plugin)
26
+ */
27
+ const plugin: Plugin = function plugin(
28
+ options: PluginOptions = {},
29
+ ): Transformer {
30
+ // content title is
31
+ const removeContentTitle = options.removeContentTitle ?? false;
32
+
33
+ return async (root, vfile) => {
34
+ const {toString} = await import('mdast-util-to-string');
35
+ const {visit, EXIT} = await import('unist-util-visit');
36
+
37
+ visit(root, 'heading', (headingNode: Heading, index, parent) => {
38
+ if (headingNode.depth === 1) {
39
+ vfile.data.contentTitle = toString(headingNode);
40
+ if (removeContentTitle) {
41
+ // @ts-expect-error: TODO how to fix?
42
+ parent!.children.splice(index, 1);
43
+ }
44
+ return EXIT; // We only handle the very first heading
45
+ }
46
+ // We only handle contentTitle if it's the very first heading found
47
+ if (headingNode.depth >= 1) {
48
+ return EXIT;
49
+ }
50
+ return undefined;
51
+ });
52
+ };
53
+ };
54
+
55
+ export default plugin;
@@ -5,7 +5,6 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
- import visit from 'unist-util-visit';
9
8
  // @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
10
9
  import type {Transformer} from 'unified';
11
10
 
@@ -15,7 +14,8 @@ import type {MdxJsxFlowElement} from 'mdast-util-mdx';
15
14
  // Transform <details> to <Details>
16
15
  // MDX 2 doesn't allow to substitute html elements with the provider anymore
17
16
  export default function plugin(): Transformer {
18
- return (root) => {
17
+ return async (root) => {
18
+ const {visit} = await import('unist-util-visit');
19
19
  visit(root, 'mdxJsxFlowElement', (node: MdxJsxFlowElement) => {
20
20
  if (node.name === 'details') {
21
21
  node.name = 'Details';
@@ -5,7 +5,6 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
- import visit from 'unist-util-visit';
9
8
  // @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
10
9
  import type {Transformer} from 'unified';
11
10
 
@@ -15,7 +14,8 @@ import type {MdxJsxFlowElement} from 'mdast-util-mdx';
15
14
  // Transform <head> to <Head>
16
15
  // MDX 2 doesn't allow to substitute html elements with the provider anymore
17
16
  export default function plugin(): Transformer {
18
- return (root) => {
17
+ return async (root) => {
18
+ const {visit} = await import('unist-util-visit');
19
19
  visit(root, 'mdxJsxFlowElement', (node: MdxJsxFlowElement) => {
20
20
  if (node.name === 'head') {
21
21
  node.name = 'Head';
@@ -8,7 +8,6 @@
8
8
  /* Based on remark-slug (https://github.com/remarkjs/remark-slug) and gatsby-remark-autolink-headers (https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-remark-autolink-headers) */
9
9
 
10
10
  import {parseMarkdownHeadingId, createSlugger} from '@docusaurus/utils';
11
- import visit from 'unist-util-visit';
12
11
  // @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
13
12
  import type {Transformer} from 'unified';
14
13
  import type {Heading, Text} from 'mdast';
@@ -16,6 +15,7 @@ import type {Heading, Text} from 'mdast';
16
15
  export default function plugin(): Transformer {
17
16
  return async (root) => {
18
17
  const {toString} = await import('mdast-util-to-string');
18
+ const {visit} = await import('unist-util-visit');
19
19
 
20
20
  const slugs = createSlugger();
21
21
  visit(root, 'heading', (headingNode: Heading) => {
@@ -5,7 +5,6 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
- import visit from 'unist-util-visit';
9
8
  // @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
10
9
  import type {Transformer, Processor} from 'unified';
11
10
  import type {Code} from 'mdast';
@@ -18,19 +17,18 @@ import type {Code} from 'mdast';
18
17
  // To make theme-classic/src/theme/MDXComponents/Pre work
19
18
  // we need to fill two properties that mdx v2 doesn't provide anymore
20
19
  export default function codeCompatPlugin(this: Processor): Transformer {
21
- return (root) => {
20
+ return async (root) => {
21
+ const {visit} = await import('unist-util-visit');
22
+
22
23
  visit(root, 'code', (node: Code) => {
23
24
  node.data = node.data || {};
25
+
24
26
  node.data.hProperties = node.data.hProperties || {};
25
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
- (node.data.hProperties as any).metastring = node.meta;
27
+ node.data.hProperties.metastring = node.meta;
27
28
 
28
29
  // Retrocompatible support for live codeblock metastring
29
30
  // Not really the appropriate place to handle that :s
30
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
- (node.data.hProperties as any).live = node.meta
32
- ?.split(' ')
33
- .includes('live');
31
+ node.data.hProperties.live = node.meta?.split(' ').includes('live');
34
32
  });
35
33
  };
36
34
  }
@@ -5,7 +5,8 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
- import visit from 'unist-util-visit';
8
+ import {transformNode} from '../utils';
9
+
9
10
  // @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
10
11
  import type {Transformer} from 'unified';
11
12
  import type {Code} from 'mdast';
@@ -15,11 +16,13 @@ import type {Code} from 'mdast';
15
16
  // https://github.com/facebook/docusaurus/issues/6370), this should be provided
16
17
  // by theme-mermaid itself
17
18
  export default function plugin(): Transformer {
18
- return (root) => {
19
- visit(root, 'code', (node: Code, index, parent) => {
19
+ return async (root) => {
20
+ const {visit} = await import('unist-util-visit');
21
+
22
+ visit(root, 'code', (node: Code) => {
20
23
  if (node.lang === 'mermaid') {
21
24
  // TODO migrate to mdxJsxFlowElement? cf admonitions
22
- parent!.children.splice(index, 1, {
25
+ transformNode(node, {
23
26
  type: 'mermaidCodeBlock',
24
27
  data: {
25
28
  hName: 'mermaid',
@@ -8,16 +8,19 @@
8
8
  import {parse, type ParserOptions} from '@babel/parser';
9
9
  import traverse from '@babel/traverse';
10
10
  import stringifyObject from 'stringify-object';
11
- import visit from 'unist-util-visit';
12
11
  import {toValue} from '../utils';
13
12
  import type {Identifier} from '@babel/types';
14
13
  import type {Node, Parent} from 'unist';
15
14
  import type {Heading, Literal} from 'mdast';
16
15
  // @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
17
16
  import type {Transformer} from 'unified';
17
+ import type {
18
+ MdxjsEsm,
19
+ // @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
20
+ } from 'mdast-util-mdx';
18
21
 
19
22
  // TODO as of April 2023, no way to import/re-export this ESM type easily :/
20
- // This might change soon, likely after TS 5.2
23
+ // TODO upgrade to TS 5.3
21
24
  // See https://github.com/microsoft/TypeScript/issues/49721#issuecomment-1517839391
22
25
  // import type {Plugin} from 'unified';
23
26
  type Plugin = any; // TODO fix this asap
@@ -88,6 +91,8 @@ const plugin: Plugin = function plugin(
88
91
 
89
92
  return async (root) => {
90
93
  const {toString} = await import('mdast-util-to-string');
94
+ const {visit} = await import('unist-util-visit');
95
+
91
96
  const headings: TOCItem[] = [];
92
97
 
93
98
  visit(root, 'heading', (child: Heading) => {
@@ -100,11 +105,12 @@ const plugin: Plugin = function plugin(
100
105
 
101
106
  headings.push({
102
107
  value: toValue(child, toString),
103
- id: child.data!.id as string,
108
+ id: child.data!.id!,
104
109
  level: child.depth,
105
110
  });
106
111
  });
107
- const {children} = root as Parent<Literal>;
112
+
113
+ const {children} = root as Parent;
108
114
  const targetIndex = await getOrCreateExistingTargetIndex(children, name);
109
115
 
110
116
  if (headings?.length) {
@@ -115,7 +121,7 @@ const plugin: Plugin = function plugin(
115
121
 
116
122
  export default plugin;
117
123
 
118
- async function createExportNode(name: string, object: any) {
124
+ async function createExportNode(name: string, object: any): Promise<MdxjsEsm> {
119
125
  const {valueToEstree} = await import('estree-util-value-to-estree');
120
126
 
121
127
  return {
@@ -16,11 +16,10 @@ import {
16
16
  getFileLoaderUtils,
17
17
  findAsyncSequential,
18
18
  } from '@docusaurus/utils';
19
- import visit from 'unist-util-visit';
20
19
  import escapeHtml from 'escape-html';
21
20
  import sizeOf from 'image-size';
22
21
  import logger from '@docusaurus/logger';
23
- import {assetRequireAttributeValue} from '../utils';
22
+ import {assetRequireAttributeValue, transformNode} from '../utils';
24
23
  // @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
25
24
  import type {Transformer} from 'unified';
26
25
  // @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
@@ -110,14 +109,12 @@ ${(err as Error).message}`;
110
109
  }
111
110
  }
112
111
 
113
- Object.keys(jsxNode).forEach(
114
- (key) => delete jsxNode[key as keyof typeof jsxNode],
115
- );
116
-
117
- jsxNode.type = 'mdxJsxTextElement';
118
- jsxNode.name = 'img';
119
- jsxNode.attributes = attributes;
120
- jsxNode.children = [];
112
+ transformNode(jsxNode, {
113
+ type: 'mdxJsxTextElement',
114
+ name: 'img',
115
+ attributes,
116
+ children: [],
117
+ });
121
118
  }
122
119
 
123
120
  async function ensureImageFileExist(imagePath: string, sourceFilePath: string) {
@@ -194,6 +191,8 @@ async function processImageNode(target: Target, context: Context) {
194
191
 
195
192
  export default function plugin(options: PluginOptions): Transformer {
196
193
  return async (root, vfile) => {
194
+ const {visit} = await import('unist-util-visit');
195
+
197
196
  const promises: Promise<void>[] = [];
198
197
  visit(root, 'image', (node: Image, index, parent) => {
199
198
  promises.push(
@@ -15,9 +15,8 @@ import {
15
15
  getFileLoaderUtils,
16
16
  findAsyncSequential,
17
17
  } from '@docusaurus/utils';
18
- import visit from 'unist-util-visit';
19
18
  import escapeHtml from 'escape-html';
20
- import {assetRequireAttributeValue} from '../utils';
19
+ import {assetRequireAttributeValue, transformNode} from '../utils';
21
20
  // @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
22
21
  import type {Transformer} from 'unified';
23
22
  // @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
@@ -90,14 +89,12 @@ async function toAssetRequireNode(
90
89
 
91
90
  const {children} = node;
92
91
 
93
- Object.keys(jsxNode).forEach(
94
- (key) => delete jsxNode[key as keyof typeof jsxNode],
95
- );
96
-
97
- jsxNode.type = 'mdxJsxTextElement';
98
- jsxNode.name = 'a';
99
- jsxNode.attributes = attributes;
100
- jsxNode.children = children;
92
+ transformNode(jsxNode, {
93
+ type: 'mdxJsxTextElement',
94
+ name: 'a',
95
+ attributes,
96
+ children,
97
+ });
101
98
  }
102
99
 
103
100
  async function ensureAssetFileExist(assetPath: string, sourceFilePath: string) {
@@ -177,6 +174,8 @@ async function processLinkNode(target: Target, context: Context) {
177
174
 
178
175
  export default function plugin(options: PluginOptions): Transformer {
179
176
  return async (root, vfile) => {
177
+ const {visit} = await import('unist-util-visit');
178
+
180
179
  const promises: Promise<void>[] = [];
181
180
  visit(root, 'link', (node: Link, index, parent) => {
182
181
  promises.push(
@@ -0,0 +1,167 @@
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
+ import path from 'path';
8
+ import process from 'process';
9
+ import logger from '@docusaurus/logger';
10
+ import {posixPath} from '@docusaurus/utils';
11
+ import {transformNode} from '../utils';
12
+
13
+ // @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
14
+ import type {Transformer, Processor, Parent} from 'unified';
15
+ import type {
16
+ Directives,
17
+ TextDirective,
18
+ // @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
19
+ } from 'mdast-util-directive';
20
+
21
+ // TODO as of April 2023, no way to import/re-export this ESM type easily :/
22
+ // This might change soon, likely after TS 5.2
23
+ // See https://github.com/microsoft/TypeScript/issues/49721#issuecomment-1517839391
24
+ // import type {Plugin} from 'unified';
25
+ type Plugin = any; // TODO fix this asap
26
+
27
+ type DirectiveType = Directives['type'];
28
+
29
+ const directiveTypes: DirectiveType[] = [
30
+ 'containerDirective',
31
+ 'leafDirective',
32
+ 'textDirective',
33
+ ];
34
+
35
+ const directivePrefixMap: {[key in DirectiveType]: string} = {
36
+ textDirective: ':',
37
+ leafDirective: '::',
38
+ containerDirective: ':::',
39
+ };
40
+
41
+ function formatDirectiveName(directive: Directives) {
42
+ const prefix = directivePrefixMap[directive.type];
43
+ if (!prefix) {
44
+ throw new Error(
45
+ `unexpected, no prefix found for directive of type ${directive.type}`,
46
+ );
47
+ }
48
+ // To simplify we don't display the eventual label/props of directives
49
+ return `${prefix}${directive.name}`;
50
+ }
51
+
52
+ function formatDirectivePosition(directive: Directives): string | undefined {
53
+ return directive.position?.start
54
+ ? logger.interpolate`number=${directive.position.start.line}:number=${directive.position.start.column}`
55
+ : undefined;
56
+ }
57
+
58
+ function formatUnusedDirectiveMessage(directive: Directives) {
59
+ const name = formatDirectiveName(directive);
60
+ const position = formatDirectivePosition(directive);
61
+
62
+ return `- ${name} ${position ? `(${position})` : ''}`;
63
+ }
64
+
65
+ function formatUnusedDirectivesMessage({
66
+ directives,
67
+ filePath,
68
+ }: {
69
+ directives: Directives[];
70
+ filePath: string;
71
+ }): string {
72
+ const supportUrl = 'https://github.com/facebook/docusaurus/pull/9394';
73
+ const customPath = posixPath(path.relative(process.cwd(), filePath));
74
+ const warningTitle = logger.interpolate`Docusaurus found ${directives.length} unused Markdown directives in file path=${customPath}`;
75
+ const customSupportUrl = logger.interpolate`url=${supportUrl}`;
76
+ const warningMessages = directives
77
+ .map(formatUnusedDirectiveMessage)
78
+ .join('\n');
79
+
80
+ return `${warningTitle}
81
+ ${warningMessages}
82
+ Your content might render in an unexpected way. Visit ${customSupportUrl} to find out why and how to fix it.`;
83
+ }
84
+
85
+ function logUnusedDirectivesWarning({
86
+ directives,
87
+ filePath,
88
+ }: {
89
+ directives: Directives[];
90
+ filePath: string;
91
+ }) {
92
+ if (directives.length > 0) {
93
+ const message = formatUnusedDirectivesMessage({
94
+ directives,
95
+ filePath,
96
+ });
97
+ logger.warn(message);
98
+ }
99
+ }
100
+
101
+ function isTextDirective(directive: Directives): directive is TextDirective {
102
+ return directive.type === 'textDirective';
103
+ }
104
+
105
+ // A simple text directive is one without any label/props
106
+ function isSimpleTextDirective(
107
+ directive: Directives,
108
+ ): directive is TextDirective {
109
+ if (isTextDirective(directive)) {
110
+ // Attributes in MDAST = Directive props
111
+ const hasAttributes =
112
+ directive.attributes && Object.keys(directive.attributes).length > 0;
113
+ // Children in MDAST = Directive label
114
+ const hasChildren = directive.children.length > 0;
115
+ return !hasAttributes && !hasChildren;
116
+ }
117
+ return false;
118
+ }
119
+
120
+ function transformSimpleTextDirectiveToString(textDirective: Directives) {
121
+ transformNode(textDirective, {
122
+ type: 'text',
123
+ value: `:${textDirective.name}`, // We ignore label/props on purpose here
124
+ });
125
+ }
126
+
127
+ function isUnusedDirective(directive: Directives) {
128
+ // If directive data is set (notably hName/hProperties set by admonitions)
129
+ // this usually means the directive has been handled by another plugin
130
+ return !directive.data;
131
+ }
132
+
133
+ const plugin: Plugin = function plugin(this: Processor): Transformer {
134
+ return async (tree, file) => {
135
+ const {visit} = await import('unist-util-visit');
136
+
137
+ const unusedDirectives: Directives[] = [];
138
+
139
+ visit<Parent, DirectiveType[]>(
140
+ tree,
141
+ directiveTypes,
142
+ (directive: Directives) => {
143
+ // If directive data is set (hName/hProperties set by admonitions)
144
+ // this usually means the directive has been handled by another plugin
145
+ if (isUnusedDirective(directive)) {
146
+ if (isSimpleTextDirective(directive)) {
147
+ transformSimpleTextDirectiveToString(directive);
148
+ } else {
149
+ unusedDirectives.push(directive);
150
+ }
151
+ }
152
+ },
153
+ );
154
+
155
+ // We only enable these warnings for the client compiler
156
+ // This avoids emitting duplicate warnings in prod mode
157
+ // Note: the client compiler is used in both dev/prod modes
158
+ if (file.data.compilerName === 'client') {
159
+ logUnusedDirectivesWarning({
160
+ directives: unusedDirectives,
161
+ filePath: file.path,
162
+ });
163
+ }
164
+ };
165
+ };
166
+
167
+ export default plugin;
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import escapeHtml from 'escape-html';
9
- import type {Parent} from 'unist';
9
+ import type {Parent, Node} from 'unist';
10
10
  import type {PhrasingContent, Heading} from 'mdast';
11
11
  import type {
12
12
  MdxJsxAttribute,
@@ -15,6 +15,27 @@ import type {
15
15
  // @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
16
16
  } from 'mdast-util-mdx';
17
17
 
18
+ /**
19
+ * Util to transform one node type to another node type
20
+ * The input node is mutated in place
21
+ * @param node the node to mutate
22
+ * @param newNode what the original node should become become
23
+ */
24
+ export function transformNode<NewNode extends Node>(
25
+ node: Node,
26
+ newNode: NewNode,
27
+ ): NewNode {
28
+ Object.keys(node).forEach((key) => {
29
+ // @ts-expect-error: unsafe but ok
30
+ delete node[key];
31
+ });
32
+ Object.keys(newNode).forEach((key) => {
33
+ // @ts-expect-error: unsafe but ok
34
+ node[key] = newNode[key];
35
+ });
36
+ return node as NewNode;
37
+ }
38
+
18
39
  export function stringifyContent(
19
40
  node: Parent,
20
41
  toString: (param: unknown) => string, // TODO weird but works
@@ -53,7 +74,7 @@ function mdxJsxTextElementToHtml(
53
74
  }
54
75
 
55
76
  export function toValue(
56
- node: PhrasingContent | Heading,
77
+ node: PhrasingContent | Heading | MdxJsxTextElement,
57
78
  toString: (param: unknown) => string, // TODO weird but works
58
79
  ): string {
59
80
  switch (node.type) {