@doc-kit/core 1.2.1 → 1.3.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 (47) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +1 -1
  3. package/package.json +8 -11
  4. package/src/generators/ast/generate.mjs +11 -9
  5. package/src/generators/ast/index.mjs +9 -0
  6. package/src/generators/ast-js/generate.mjs +4 -4
  7. package/src/generators/index.mjs +1 -0
  8. package/src/generators/json/README.md +1 -1
  9. package/src/generators/json/generated/schema.d.ts +1 -1
  10. package/src/generators/json/index.mjs +9 -0
  11. package/src/generators/json/utils/markdown.mjs +2 -2
  12. package/src/generators/json-all/README.md +1 -1
  13. package/src/generators/json-simple/README.md +1 -1
  14. package/src/generators/json-simple/generate.mjs +13 -5
  15. package/src/generators/loader.mjs +14 -0
  16. package/src/generators/metadata/generate.mjs +4 -2
  17. package/src/generators/metadata/index.mjs +9 -0
  18. package/src/generators/metadata/maps/mdn.json +2 -0
  19. package/src/generators/metadata/types.d.ts +1 -1
  20. package/src/generators/metadata/utils/parse.mjs +2 -4
  21. package/src/generators/metadata/utils/visitors.mjs +2 -2
  22. package/src/generators/types.d.ts +8 -0
  23. package/src/generators.mjs +9 -3
  24. package/src/logger/constants.mjs +35 -0
  25. package/src/logger/transports/github.mjs +48 -15
  26. package/src/plugins/shiki/highlighter.mjs +373 -0
  27. package/src/plugins/shiki/rehype.mjs +38 -0
  28. package/src/{utils/type-annotations/highlighted.mjs → plugins/type-annotations/highlighter.mjs} +14 -19
  29. package/src/{utils → plugins}/type-annotations/remark.mjs +6 -2
  30. package/src/threading/chunk-worker.mjs +4 -0
  31. package/src/threading/constants.mjs +24 -0
  32. package/src/threading/index.mjs +10 -1
  33. package/src/threading/parallel.mjs +28 -31
  34. package/src/utils/configuration/constants.mjs +4 -6
  35. package/src/utils/configuration/index.mjs +89 -21
  36. package/src/utils/configuration/types.d.ts +27 -0
  37. package/src/utils/glob.mjs +42 -0
  38. package/src/utils/loaders.mjs +23 -1
  39. package/src/utils/markdown/constants.mjs +7 -0
  40. package/src/utils/markdown/plugins.mjs +232 -0
  41. package/src/utils/markdown/processor.mjs +60 -0
  42. package/src/utils/highlighter.mjs +0 -198
  43. package/src/utils/remark-shiki.mjs +0 -29
  44. package/src/utils/remark.mjs +0 -90
  45. /package/src/{utils → plugins}/type-annotations/hast.mjs +0 -0
  46. /package/src/{utils → plugins}/type-annotations/mdast.mjs +0 -0
  47. /package/src/{utils → plugins}/type-annotations/syntax.mjs +0 -0
@@ -1,8 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
- import { createRequire } from 'node:module';
3
2
  import { cpus } from 'node:os';
4
- import { dirname, isAbsolute, resolve } from 'node:path';
5
- import { pathToFileURL } from 'node:url';
3
+ import { fileURLToPath } from 'node:url';
6
4
  import { isMainThread } from 'node:worker_threads';
7
5
 
8
6
  import { cosmiconfig } from 'cosmiconfig';
@@ -16,9 +14,16 @@ import logger from '#logger/index.mjs';
16
14
  import { parseChangelog, parseIndex } from '#parsers/markdown.mjs';
17
15
  import { enforceArray } from '#utils/array.mjs';
18
16
  import { leftHandAssign } from '#utils/generators.mjs';
17
+ import { resolveSpecifier } from '#utils/loaders.mjs';
18
+ import {
19
+ CONFIGURED_PLUGINS,
20
+ PLUGIN_LISTS,
21
+ } from '#utils/markdown/constants.mjs';
22
+ import { resolveMarkdown } from '#utils/markdown/plugins.mjs';
19
23
  import { deepMerge } from '#utils/misc.mjs';
20
24
 
21
- import { DEFAULT_CHUNK_SIZE, DEFAULT_MAX_THREADS } from './constants.mjs';
25
+ import { DEFAULT_MAX_THREADS } from '../../threading/constants.mjs';
26
+ import { DEFAULT_CHUNK_SIZE, DEFAULT_OUTPUT_DIRECTORY } from './constants.mjs';
22
27
 
23
28
  const configExplorer = cosmiconfig('doc-kit');
24
29
 
@@ -58,12 +63,14 @@ export const getDefaultConfig = (generators, config) =>
58
63
  global: {
59
64
  project: detectProject() ?? 'API Docs',
60
65
  version: process.version,
61
- minify: true,
66
+ minify: false,
67
+ output: DEFAULT_OUTPUT_DIRECTORY,
62
68
  ref: 'HEAD',
63
69
  // Without release history there is nothing to build a version picker
64
70
  // from, so generators render single-version output.
65
71
  changelog: [],
66
72
  pathsToCopy: ['assets', 'public', 'static'],
73
+ markdown: { remarkPlugins: [], rehypePlugins: [], recmaPlugins: [] },
67
74
  },
68
75
 
69
76
  // The number of wasm memory instances is severely limited on
@@ -79,21 +86,26 @@ export const getDefaultConfig = (generators, config) =>
79
86
  );
80
87
 
81
88
  /**
82
- * Resolves an `extends` entry of a configuration file into an importable
83
- * URL: relative paths resolve against the configuration file, anything else
84
- * resolves as a package import specifier (e.g. `@node-core/doc-kit/config`).
89
+ * Resolves the Markdown plugins of a configuration (in `global`, and each
90
+ * generator's section) from the file declaring them.
85
91
  *
86
- * @param {string} specifier - The `extends` entry
87
- * @param {string} configFilePath - The configuration file it appears in
88
- * @returns {string} A `file:` URL to import
92
+ * @param {Partial<import('./types').Configuration>} config - The configuration
93
+ * @param {string} filePath - The file declaring it
94
+ * @returns {Partial<import('./types').Configuration>}
89
95
  */
90
- const resolveConfigExtends = (specifier, configFilePath) => {
91
- if (specifier.startsWith('.') || isAbsolute(specifier)) {
92
- return pathToFileURL(resolve(dirname(configFilePath), specifier)).href;
93
- }
96
+ const resolveMarkdownPlugins = (config, filePath) =>
97
+ Object.fromEntries(
98
+ Object.entries(config).map(([name, value]) => {
99
+ if (value?.markdown) {
100
+ const label = `${name}.markdown`;
101
+ const markdown = resolveMarkdown(value.markdown, label, filePath);
94
102
 
95
- return pathToFileURL(createRequire(configFilePath).resolve(specifier)).href;
96
- };
103
+ return [name, { ...value, markdown }];
104
+ }
105
+
106
+ return [name, value];
107
+ })
108
+ );
97
109
 
98
110
  /**
99
111
  * Loads an explicit configuration file or searches for one using cosmiconfig.
@@ -112,15 +124,63 @@ export const loadConfigFile = async filePath => {
112
124
 
113
125
  let { extends: presets, ...config } = result.config ?? {};
114
126
 
127
+ config = resolveMarkdownPlugins(config, result.filepath);
128
+
115
129
  for (const preset of enforceArray(presets ?? []).toReversed()) {
116
- const module = await import(resolveConfigExtends(preset, result.filepath));
130
+ const url = resolveSpecifier(preset, result.filepath);
131
+ const module = await import(url);
117
132
 
118
- config = deepMerge(module.default ?? module, config);
133
+ // A preset's Markdown plugins resolve from the preset
134
+ config = deepMerge(
135
+ resolveMarkdownPlugins(module.default ?? module, fileURLToPath(url)),
136
+ config
137
+ );
119
138
  }
120
139
 
121
140
  return config;
122
141
  };
123
142
 
143
+ /**
144
+ * Returns the Markdown plugins a generator takes: for each list of its
145
+ * pipeline with a `'...'`, the global ones, then its own. Generators rendering
146
+ * Markdown (with rehype or recma plugins) skip the global remark plugins,
147
+ * which already ran in `ast`. Its own plugins it doesn't take are ignored with
148
+ * a warning.
149
+ *
150
+ * @param {GeneratorMetadata} generator - The generator
151
+ * @param {Partial<import('./types').MarkdownConfiguration>} [markdown] - Its own plugins
152
+ * @param {import('./types').GlobalConfiguration} global - The global configuration
153
+ * @returns {Partial<import('./types').MarkdownConfiguration> | undefined}
154
+ */
155
+ const configureMarkdown = (generator, markdown = {}, global) => {
156
+ const pipeline = generator.markdown ?? {};
157
+ const renders = Boolean(pipeline.rehypePlugins || pipeline.recmaPlugins);
158
+ const configured = {};
159
+
160
+ for (const list of PLUGIN_LISTS) {
161
+ if (!pipeline[list]?.includes(CONFIGURED_PLUGINS)) {
162
+ if (markdown[list]?.length > 0) {
163
+ logger.warn(
164
+ `Ignoring \`${generator.name}.markdown.${list}\`: ` +
165
+ `\`${generator.name}\` does not take them.`
166
+ );
167
+ }
168
+
169
+ continue;
170
+ }
171
+
172
+ const plugins = markdown[list] ?? [];
173
+
174
+ if (list === 'remarkPlugins' && renders) {
175
+ configured[list] = plugins;
176
+ } else {
177
+ configured[list] = [...global.markdown[list], ...plugins];
178
+ }
179
+ }
180
+
181
+ return generator.markdown && configured;
182
+ };
183
+
124
184
  /**
125
185
  * Transforms configuration values that need async processing or coercion.
126
186
  * Only processes values that haven't been transformed yet (strings for changelog/index, non-coerced versions).
@@ -232,12 +292,20 @@ export const createRunConfiguration = async options => {
232
292
 
233
293
  // Now assign to each generator config (they inherit from global)
234
294
  await Promise.all(
235
- [...generators.values()].map(async ({ name }) => {
236
- const value = merged[name];
295
+ [...generators.values()].map(async generator => {
296
+ const value = merged[generator.name];
237
297
 
238
298
  // Transform generator-specific overrides
239
299
  await transformConfig(value);
240
300
 
301
+ // A generator's own Markdown plugins add to the global ones. Set even
302
+ // when undefined, so the assignment below doesn't copy the global ones
303
+ value.markdown = configureMarkdown(
304
+ generator,
305
+ value.markdown,
306
+ merged.global
307
+ );
308
+
241
309
  // Assign from global (this populates missing values from global)
242
310
  leftHandAssign(value, merged.global);
243
311
  })
@@ -1,4 +1,5 @@
1
1
  import type { SemVer } from 'semver';
2
+
2
3
  import type { ReleaseEntry } from '#parsers/types';
3
4
 
4
5
  export type Configuration = {
@@ -61,4 +62,30 @@ export type GlobalConfiguration = {
61
62
 
62
63
  // Git ref (i.e. HEAD)
63
64
  ref: string;
65
+
66
+ // Markdown plugins added to the pipeline of each generator processing
67
+ // Markdown. A generator's own `markdown` adds to them, rather than
68
+ // replacing them.
69
+ markdown: MarkdownConfiguration;
70
+ };
71
+
72
+ // A plugin's module specifier (a package name, or a path relative to the file
73
+ // declaring it), alone or with its options
74
+ export type PluginEntry = string | [specifier: string, options?: unknown];
75
+
76
+ export type MarkdownConfiguration = {
77
+ // remark plugins, run on Markdown syntax trees
78
+ remarkPlugins: Array<PluginEntry>;
79
+
80
+ // rehype plugins, run on HTML syntax trees
81
+ rehypePlugins: Array<PluginEntry>;
82
+
83
+ // recma plugins, run on JavaScript syntax trees
84
+ recmaPlugins: Array<PluginEntry>;
85
+ };
86
+
87
+ // A generator's Markdown pipeline, where each list takes the configured
88
+ // plugins in place of its `'...'`
89
+ export type MarkdownPipeline = {
90
+ [List in keyof MarkdownConfiguration]?: Array<PluginEntry | '...'>;
64
91
  };
@@ -0,0 +1,42 @@
1
+ import { globSync } from 'node:fs';
2
+ import { relative, resolve, sep } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ // A path segment holding any glob syntax (wildcards, classes, braces or extglobs)
6
+ const GLOB_SEGMENT = /[*?[{]|[!+@]\(/;
7
+
8
+ /**
9
+ * Finds the files matching glob patterns, as POSIX paths relative to `cwd`
10
+ * (absolute patterns included). Directories are left out.
11
+ *
12
+ * @param {string | string[]} patterns - Glob patterns to match
13
+ * @param {{ cwd?: string | URL, ignore?: string[] }} [options]
14
+ * @returns {string[]}
15
+ */
16
+ export const findFiles = (patterns, { cwd = process.cwd(), ignore } = {}) => {
17
+ const root = cwd instanceof URL ? fileURLToPath(cwd) : cwd;
18
+
19
+ return globSync(patterns, { cwd: root, exclude: ignore, withFileTypes: true })
20
+ .filter(dirent => !dirent.isDirectory())
21
+ .map(dirent =>
22
+ relative(root, resolve(root, dirent.parentPath, dirent.name))
23
+ .split(sep)
24
+ .join('/')
25
+ );
26
+ };
27
+
28
+ /**
29
+ * Returns the directory part of a glob pattern before its first glob segment,
30
+ * e.g. `doc/api` for `doc/api/*.md`, or the parent directory of a plain path.
31
+ * Like `fs.globSync`, it reads backslashes as separators, never as escapes.
32
+ *
33
+ * @param {string} pattern - A glob pattern
34
+ * @returns {string}
35
+ */
36
+ export const globParent = pattern => {
37
+ const segments = pattern.replaceAll('\\', '/').split('/');
38
+ const index = segments.findIndex(segment => GLOB_SEGMENT.test(segment));
39
+ const parent = segments.slice(0, index === -1 ? -1 : index).join('/');
40
+
41
+ return parent || (segments[0] === '' ? '/' : '.');
42
+ };
@@ -1,5 +1,6 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import { extname, isAbsolute, join } from 'node:path';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, extname, isAbsolute, join, resolve } from 'node:path';
3
4
  import { pathToFileURL } from 'node:url';
4
5
 
5
6
  /**
@@ -28,6 +29,27 @@ export const loadFromURL = async url => {
28
29
  }
29
30
  };
30
31
 
32
+ /**
33
+ * Resolves an import specifier into a `file:` URL: relative paths resolve
34
+ * against the file it appears in, anything else as a package import
35
+ * specifier (e.g. `@node-core/doc-kit/config`).
36
+ *
37
+ * @param {string} specifier - A path, a package specifier, or a `file:` URL
38
+ * @param {string} filePath - The file it appears in
39
+ * @returns {string} A `file:` URL to import
40
+ */
41
+ export const resolveSpecifier = (specifier, filePath) => {
42
+ if (specifier.startsWith('file:')) {
43
+ return specifier;
44
+ }
45
+
46
+ if (specifier.startsWith('.') || isAbsolute(specifier)) {
47
+ return pathToFileURL(resolve(dirname(filePath), specifier)).href;
48
+ }
49
+
50
+ return pathToFileURL(createRequire(filePath).resolve(specifier)).href;
51
+ };
52
+
31
53
  /**
32
54
  * Dynamically imports a module from a URL, using JSON import assertion if applicable.
33
55
  *
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ // The plugin lists of a Markdown pipeline, one for each syntax tree
4
+ export const PLUGIN_LISTS = ['remarkPlugins', 'rehypePlugins', 'recmaPlugins'];
5
+
6
+ // Marks where a list of a generator's pipeline takes the configured plugins
7
+ export const CONFIGURED_PLUGINS = '...';
@@ -0,0 +1,232 @@
1
+ 'use strict';
2
+
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { getGeneratorModule } from '#generators/loader.mjs';
6
+ import { enforceArray } from '#utils/array.mjs';
7
+ import { resolveSpecifier } from '#utils/loaders.mjs';
8
+ import { isPlainObject } from '#utils/misc.mjs';
9
+
10
+ import { CONFIGURED_PLUGINS, PLUGIN_LISTS } from './constants.mjs';
11
+
12
+ /**
13
+ * The plugins of a Markdown pipeline, as `processor.use()` takes them.
14
+ *
15
+ * @typedef {Record<'remarkPlugins' | 'rehypePlugins' | 'recmaPlugins', import('unified').PluggableList>} MarkdownPlugins
16
+ */
17
+
18
+ // The pipeline each generator runs on this thread, by its name
19
+ const loadedPipelines = new Map();
20
+
21
+ /**
22
+ * Resolves the module specifiers of Markdown plugins (in the `markdown`
23
+ * option, or a generator's pipeline) from the file declaring them into
24
+ * `file:` URLs, which any thread can import.
25
+ *
26
+ * @param {import('../configuration/types').MarkdownPipeline} markdown - The plugins
27
+ * @param {string} label - Where they are declared, for errors (e.g. `global.markdown`)
28
+ * @param {string} filePath - The file declaring them
29
+ * @returns {import('../configuration/types').MarkdownPipeline}
30
+ */
31
+ export const resolveMarkdown = (markdown, label, filePath) => {
32
+ const resolved = { ...markdown };
33
+
34
+ for (const list of PLUGIN_LISTS) {
35
+ resolved[list] &&= enforceArray(markdown[list]).map((entry, index) => {
36
+ if (entry === CONFIGURED_PLUGINS) {
37
+ return entry;
38
+ }
39
+
40
+ const [specifier, ...options] = enforceArray(entry);
41
+
42
+ if (typeof specifier !== 'string') {
43
+ throw new TypeError(
44
+ `${label}.${list}[${index}] in ${filePath} must be a module ` +
45
+ 'specifier (a package name or a path), as Markdown is processed ' +
46
+ 'in worker threads, which import the plugins themselves.'
47
+ );
48
+ }
49
+
50
+ const url = resolveSpecifier(specifier, filePath);
51
+
52
+ return options.length === 0 ? url : [url, ...options];
53
+ });
54
+ }
55
+
56
+ return resolved;
57
+ };
58
+
59
+ /**
60
+ * Resolves the module specifiers of a generator's Markdown pipeline from the
61
+ * module it was loaded from.
62
+ *
63
+ * @param {GeneratorMetadata} generator - The generator
64
+ * @returns {import('../configuration/types').MarkdownPipeline}
65
+ */
66
+ const resolveMarkdownPipeline = generator => {
67
+ const module = getGeneratorModule(generator);
68
+
69
+ return resolveMarkdown(
70
+ generator.markdown,
71
+ `${generator.name}.markdown`,
72
+ module && fileURLToPath(module)
73
+ );
74
+ };
75
+
76
+ /**
77
+ * Merges options into others: arrays add up, plain objects merge, and other
78
+ * values replace the ones they are merged into.
79
+ *
80
+ * @param {unknown} options - The options merged into
81
+ * @param {unknown} added - The options merged
82
+ * @returns {unknown}
83
+ */
84
+ const mergeOptions = (options, added) => {
85
+ if (Array.isArray(options) && Array.isArray(added)) {
86
+ return [...options, ...added];
87
+ }
88
+
89
+ if (!isPlainObject(options) || !isPlainObject(added)) {
90
+ return added;
91
+ }
92
+
93
+ const merged = { ...options };
94
+
95
+ for (const [key, value] of Object.entries(added)) {
96
+ merged[key] = mergeOptions(options[key], value);
97
+ }
98
+
99
+ return merged;
100
+ };
101
+
102
+ /**
103
+ * Builds a list of a pipeline, with the configured plugins in place of its
104
+ * `'...'`. A configured plugin the list already has, or added earlier, isn't
105
+ * added again: its options merge into the ones it has.
106
+ *
107
+ * @param {Array<import('../configuration/types').PluginEntry | '...'>} [own] - The list
108
+ * @param {Array<import('../configuration/types').PluginEntry>} [configured] - The configured plugins
109
+ * @returns {Array<{ entry: import('../configuration/types').PluginEntry, added: boolean }>}
110
+ */
111
+ const configureList = (own = [], configured = []) => {
112
+ const plugins = own.map(entry => ({ entry, added: false }));
113
+ const added = [];
114
+
115
+ // Each plugin by its specifier: the first listing of it takes the options
116
+ const listed = new Map();
117
+
118
+ for (const plugin of plugins) {
119
+ const [specifier] = enforceArray(plugin.entry);
120
+
121
+ if (!listed.has(specifier)) {
122
+ listed.set(specifier, plugin);
123
+ }
124
+ }
125
+
126
+ for (const entry of configured) {
127
+ const [specifier, options] = enforceArray(entry);
128
+ const plugin = listed.get(specifier);
129
+
130
+ if (!plugin) {
131
+ const addedPlugin = { entry, added: true };
132
+
133
+ added.push(addedPlugin);
134
+ listed.set(specifier, addedPlugin);
135
+ } else if (options !== undefined) {
136
+ const [, listedOptions] = enforceArray(plugin.entry);
137
+
138
+ plugin.entry = [specifier, mergeOptions(listedOptions, options)];
139
+ }
140
+ }
141
+
142
+ return plugins.flatMap(plugin =>
143
+ plugin.entry === CONFIGURED_PLUGINS ? added : [plugin]
144
+ );
145
+ };
146
+
147
+ /**
148
+ * Imports a plugin as `processor.use()` takes it. Its module default-exports
149
+ * the plugin, a list of plugins, or a preset, or exports an async `load`,
150
+ * taking the plugin's options and returning it, for setup unified plugins
151
+ * can't do themselves.
152
+ *
153
+ * @param {import('../configuration/types').PluginEntry} entry - The plugin
154
+ * @returns {Promise<import('unified').Pluggable>}
155
+ */
156
+ const importPlugin = async entry => {
157
+ const [specifier, ...options] = enforceArray(entry);
158
+ const { default: plugin, load } = await import(specifier);
159
+
160
+ if (typeof load === 'function') {
161
+ return [await load(...options)];
162
+ }
163
+
164
+ if (typeof plugin === 'function') {
165
+ return [plugin, ...options];
166
+ }
167
+
168
+ // A list is used as a preset, not to be taken for a [plugin, options] pair
169
+ return Array.isArray(plugin) ? { plugins: plugin } : plugin;
170
+ };
171
+
172
+ /**
173
+ * Loads a generator's Markdown pipeline on the current thread, with the
174
+ * plugins configured for it. Loading the same configuration again does
175
+ * nothing.
176
+ *
177
+ * @param {GeneratorMetadata} generator - The generator
178
+ * @param {Partial<import('../configuration/types').MarkdownConfiguration>} [markdown] - The plugins configured for it
179
+ * @returns {Promise<void>}
180
+ */
181
+ export const loadMarkdownPlugins = async (generator, markdown = {}) => {
182
+ if (!generator.markdown) {
183
+ return;
184
+ }
185
+
186
+ const key = JSON.stringify(markdown);
187
+ const loaded = loadedPipelines.get(generator.name);
188
+
189
+ if (loaded?.generator === generator && loaded.key === key) {
190
+ return;
191
+ }
192
+
193
+ const pipeline = resolveMarkdownPipeline(generator);
194
+ const configured = {};
195
+ const own = {};
196
+
197
+ // The lists import at once, so a slow plugin (Shiki) doesn't hold the rest
198
+ const loading = PLUGIN_LISTS.map(async list => {
199
+ const plugins = configureList(pipeline[list], markdown[list]);
200
+ const imports = plugins.map(({ entry }) => importPlugin(entry));
201
+
202
+ const imported = await Promise.all(imports);
203
+
204
+ configured[list] = imported;
205
+ own[list] = imported.filter((_, index) => !plugins[index].added);
206
+ });
207
+
208
+ await Promise.all(loading);
209
+
210
+ loadedPipelines.set(generator.name, { generator, key, configured, own });
211
+ };
212
+
213
+ /**
214
+ * Gets the plugins of a generator's Markdown pipeline, as loaded on the
215
+ * current thread: with the configured plugins, or only its own (with their
216
+ * configured options).
217
+ *
218
+ * @param {string} generator - The name of the generator
219
+ * @param {boolean} [configured] - Whether with the configured plugins
220
+ * @returns {MarkdownPlugins}
221
+ */
222
+ export const getMarkdownPlugins = (generator, configured = true) => {
223
+ const loaded = loadedPipelines.get(generator);
224
+
225
+ if (!loaded) {
226
+ throw new Error(
227
+ `The Markdown pipeline of "${generator}" is not loaded on this thread`
228
+ );
229
+ }
230
+
231
+ return configured ? loaded.configured : loaded.own;
232
+ };
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ import remarkMdx from 'remark-mdx';
4
+ import { unified } from 'unified';
5
+
6
+ import { PLUGIN_LISTS } from './constants.mjs';
7
+ import { getMarkdownPlugins } from './plugins.mjs';
8
+
9
+ // The processors of each loaded pipeline, of Markdown and of MDX
10
+ const processors = new WeakMap();
11
+
12
+ /**
13
+ * Creates the processor of a pipeline: its remark plugins, then its rehype
14
+ * ones, then its recma ones. A processor of MDX knows its syntax too, and
15
+ * tells plugins it processes MDX through `this.data('mdx')`.
16
+ *
17
+ * @param {import('./plugins.mjs').MarkdownPlugins} plugins - The pipeline
18
+ * @param {boolean} mdx - Whether it processes MDX
19
+ * @returns {import('unified').Processor}
20
+ */
21
+ const createProcessor = (plugins, mdx) => {
22
+ const processor = unified().data('mdx', mdx);
23
+
24
+ if (mdx) {
25
+ processor.use(remarkMdx);
26
+ }
27
+
28
+ for (const list of PLUGIN_LISTS) {
29
+ processor.use(plugins[list]);
30
+ }
31
+
32
+ return processor;
33
+ };
34
+
35
+ /**
36
+ * Gets the processor of a generator's Markdown pipeline, as loaded on the
37
+ * current thread (see `loadMarkdownPlugins`).
38
+ *
39
+ * @param {string} generator - The name of the generator
40
+ * @param {Object} [options]
41
+ * @param {boolean} [options.mdx] - Whether it processes MDX
42
+ * @param {boolean} [options.configured] - Whether it runs the configured
43
+ * plugins, rather than only the generator's own
44
+ * @returns {import('unified').Processor}
45
+ */
46
+ export const getProcessor = (
47
+ generator,
48
+ { mdx = false, configured = true } = {}
49
+ ) => {
50
+ const plugins = getMarkdownPlugins(generator, configured);
51
+
52
+ if (!processors.has(plugins)) {
53
+ processors.set(plugins, {});
54
+ }
55
+
56
+ const created = processors.get(plugins);
57
+ const syntax = mdx ? 'mdx' : 'markdown';
58
+
59
+ return (created[syntax] ??= createProcessor(plugins, mdx));
60
+ };