docusaurus-plugin-glossary 1.3.1 → 2.0.0

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/README.md CHANGED
@@ -246,9 +246,46 @@ module.exports = {
246
246
  };
247
247
  ```
248
248
 
249
+ ## How It Works
250
+
251
+ This plugin uses a **hybrid approach** combining build-time transformation and runtime enhancements, inspired by `docusaurus-plugin-image-zoom`:
252
+
253
+ ### Build-Time: Remark Plugin
254
+
255
+ The remark plugin automatically detects glossary terms in your markdown and:
256
+ 1. Transforms plain text terms into `<GlossaryTerm>` JSX components
257
+ 2. Automatically injects the necessary import statement (`import GlossaryTerm from '@theme/GlossaryTerm';`)
258
+ 3. This happens during the MDX compilation, before React renders anything
259
+
260
+ **No manual imports needed!** When you write:
261
+ ```markdown
262
+ Our API uses REST principles.
263
+ ```
264
+
265
+ The remark plugin transforms it to:
266
+ ```jsx
267
+ import GlossaryTerm from '@theme/GlossaryTerm';
268
+
269
+ Our <GlossaryTerm term="API">API</GlossaryTerm> uses <GlossaryTerm term="REST">REST</GlossaryTerm> principles.
270
+ ```
271
+
272
+ ### Runtime: Client Modules
273
+
274
+ The plugin uses Docusaurus's `getClientModules()` API to automatically load client-side code on every page. This ensures:
275
+ - Glossary term functionality is available globally without configuration
276
+ - Components initialize correctly on each route change
277
+ - No performance impact from manual module loading
278
+
279
+ ### Theme Components
280
+
281
+ The `GlossaryTerm` component is provided via the theme system (`@theme/GlossaryTerm`), making it:
282
+ - Available to all MDX files through automatic imports
283
+ - Swizzlable for custom styling and behavior
284
+ - Accessible to both the remark plugin and manual usage
285
+
249
286
  ## Docusaurus v3 Notes and Troubleshooting
250
287
 
251
- - **MDX imports**: The plugin injects `import GlossaryTerm from '@theme/GlossaryTerm';` automatically when it auto-links a term. If you’re writing MDX manually, you can also import and use it yourself:
288
+ - **MDX imports**: The plugin automatically injects `import GlossaryTerm from '@theme/GlossaryTerm';` when it auto-links a term. You can also use it manually in MDX:
252
289
 
253
290
  ```mdx
254
291
  import GlossaryTerm from '@theme/GlossaryTerm';
@@ -261,7 +298,7 @@ module.exports = {
261
298
  - Ensure the plugin is listed in `plugins` AND the remark plugin is configured in your preset (see Step 2 above).
262
299
  - Visit `/glossary`. If the page or route fails to render, verify your `glossaryPath` file exists and contains a `terms` array.
263
300
  - Clear your Docusaurus cache with `npm run clear` and restart your dev server.
264
- - If you previously used a local patch for `1.0.0`, remove it when using `1.0.2+`; the plugin bundles the v3-compatible theme and remark integration.
301
+ - If you previously used an older version (v1.0.x), upgrade to the latest version; the plugin bundles the v3-compatible theme and remark integration.
265
302
 
266
303
  - **Opting out of auto-linking**: Simply don't configure the remark plugin in your preset. You can still use the `<GlossaryTerm />` component manually where you want explicit control.
267
304
 
@@ -454,38 +491,85 @@ principles to provide a simple and consistent interface.
454
491
 
455
492
  ## Development
456
493
 
494
+ This project is written in TypeScript and compiles to JavaScript. The source files are in `src/` and the compiled output goes to `lib/`.
495
+
457
496
  ### File Structure
458
497
 
459
498
  ```
460
499
  docusaurus-plugin-glossary/
461
- ├── index.js # Main plugin file
462
- ├── components/
463
- │ ├── GlossaryPage.js # Glossary page component
464
- │ └── GlossaryPage.module.css # Glossary page styles
465
- ├── remark/
466
- └── glossary-terms.js # Remark plugin for automatic term detection
467
- ├── theme/
468
- │ └── GlossaryTerm/
469
- ├── index.js # Term component
470
- └── styles.module.css # Term styles
471
- └── README.md
500
+ ├── src/
501
+ ├── index.ts # Main plugin entry point (TypeScript)
502
+ │ ├── client/
503
+ └── index.js # Client module for runtime initialization
504
+ ├── components/
505
+ │ ├── GlossaryPage.js # Glossary page component
506
+ │ │ ├── GlossaryPage.module.css
507
+ └── GlossaryPage.test.js
508
+ ├── remark/
509
+ └── glossary-terms.js # Remark plugin for automatic term detection
510
+ └── theme/
511
+ │ └── GlossaryTerm/
512
+ │ ├── index.js # Term component
513
+ │ ├── styles.module.css
514
+ │ └── index.test.js
515
+ ├── lib/ # Compiled output (generated by build)
516
+ │ ├── index.js # Main plugin file (compiled from src/index.ts)
517
+ │ ├── client/ # Copied from src/client/
518
+ │ ├── components/ # Copied from src/components/
519
+ │ ├── remark/ # Copied from src/remark/
520
+ │ └── theme/ # Copied from src/theme/
521
+ ├── __tests__/
522
+ │ └── plugin.test.js # Plugin lifecycle tests
523
+ ├── examples/
524
+ │ └── docusaurus-v3/ # Example Docusaurus site
525
+ ├── scripts/
526
+ │ ├── build.js # Build script (TypeScript + copy files)
527
+ │ ├── watch.js # Watch script for development
528
+ │ └── ...
529
+ └── package.json
530
+ ```
531
+
532
+ ### Building
533
+
534
+ The project uses TypeScript for the main plugin entry point (`src/index.ts`) and JavaScript for components. To build:
535
+
536
+ ```bash
537
+ npm run build
472
538
  ```
473
539
 
540
+ This will:
541
+ 1. Compile TypeScript (`src/index.ts`) to JavaScript (`lib/index.js`)
542
+ 2. Copy JavaScript, CSS, and test files from `src/` to `lib/`
543
+
544
+ For development, use:
545
+
546
+ ```bash
547
+ npm run watch
548
+ ```
549
+
550
+ This watches for changes and automatically rebuilds.
551
+
474
552
  ### Plugin Lifecycle
475
553
 
476
- 1. **loadContent**: Reads glossary JSON file
477
- 2. **contentLoaded**: Creates data file and adds route
478
- 3. **getThemePath**: Exposes theme components
479
- 4. **getPathsToWatch**: Watches glossary file for changes
554
+ The plugin follows Docusaurus plugin lifecycle hooks:
555
+
556
+ 1. **getClientModules**: Returns client modules that load automatically on every page (provides runtime initialization)
557
+ 2. **loadContent**: Reads glossary JSON file from the configured path
558
+ 3. **contentLoaded**: Creates data files for components and remark plugin, adds glossary page route
559
+ 4. **getThemePath**: Exposes theme components (`GlossaryTerm`)
560
+ 5. **getPathsToWatch**: Watches glossary file for changes during development
561
+ 6. **postBuild**: Optional post-build hook for additional processing
480
562
 
481
563
  ### Remark Plugin
482
564
 
483
- The remark plugin (`remark/glossary-terms.js`) automatically detects glossary terms in markdown files and replaces them with `GlossaryTerm` components. It:
565
+ The remark plugin (`remark/glossary-terms.js`) automatically detects glossary terms in markdown files and transforms them at build time. It:
484
566
 
485
567
  - Scans text nodes for glossary terms (case-insensitive, whole word matching)
486
- - Replaces matching terms with MDX components that show tooltips
568
+ - Replaces matching terms with `<GlossaryTerm>` MDX components
569
+ - Automatically injects the necessary import statement (`import GlossaryTerm from '@theme/GlossaryTerm';`)
487
570
  - Skips terms inside code blocks, links, or existing MDX components
488
571
  - Respects word boundaries to avoid partial matches
572
+ - Handles plural forms (e.g., "API" matches "APIs")
489
573
 
490
574
  ## Troubleshooting
491
575
 
@@ -534,3 +618,10 @@ Contributions are welcome! Please see our [Contributing Guide](CONTRIBUTING.md)
534
618
  ## Credits
535
619
 
536
620
  Built for Docusaurus v3.x
621
+
622
+ ## Technical Details
623
+
624
+ - **Language**: TypeScript (main plugin) + JavaScript (components)
625
+ - **Build System**: TypeScript compiler + custom build scripts
626
+ - **Package Entry Point**: `lib/index.js` (compiled from `src/index.ts`)
627
+ - **Exports**: Main plugin, remark plugin via package.json exports field
@@ -0,0 +1,35 @@
1
+ import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
2
+
3
+ /**
4
+ * Client module for glossary plugin
5
+ * This runs automatically on every page via getClientModules()
6
+ * Similar to docusaurus-plugin-image-zoom approach
7
+ */
8
+ export default (function () {
9
+ // Only run in browser environment
10
+ if (!ExecutionEnvironment.canUseDOM) {
11
+ return null;
12
+ }
13
+
14
+ return {
15
+ onRouteUpdate({ location }) {
16
+ // GlossaryTerm components handle their own tooltips via React
17
+ // This client module can handle any global initialization if needed
18
+
19
+ // Optional: Log for debugging
20
+ if (process.env.NODE_ENV !== 'production') {
21
+ const glossaryTerms = document.querySelectorAll('[data-glossary-term], .glossaryTerm');
22
+ if (glossaryTerms.length > 0) {
23
+ console.log(
24
+ `[glossary-plugin] Initialized ${glossaryTerms.length} glossary term(s) on route:`,
25
+ location.pathname
26
+ );
27
+ }
28
+ }
29
+
30
+ // Future enhancement: Could add DOM-based term detection here
31
+ // This would find plain text terms and add tooltips without requiring
32
+ // the remark plugin, similar to how image-zoom finds and enhances <img> tags
33
+ },
34
+ };
35
+ })();
package/lib/index.js CHANGED
@@ -13,48 +13,74 @@ function getDirname() {
13
13
  return '';
14
14
  }
15
15
  // Use cached value if available
16
- if (globalThis.__dirnameCache) {
17
- return globalThis.__dirnameCache;
16
+ const global = globalThis;
17
+ if (global.__dirnameCache) {
18
+ return global.__dirnameCache;
18
19
  }
19
- // In Jest/Babel transformed environment, __filename is available
20
- // @ts-ignore - __filename is available after Babel transforms ES modules to CommonJS
21
- if (typeof __filename !== 'undefined') {
22
- const computedDirname = path.dirname(__filename);
23
- globalThis.__dirnameCache = computedDirname;
24
- return computedDirname;
25
- }
26
- // Check if import.meta.url is available
27
- // Use try-catch to handle cases where import.meta is undefined (e.g., in Jest before transform)
28
- let hasImportMetaUrl = false;
29
- try {
30
- hasImportMetaUrl = typeof import.meta.url !== 'undefined';
31
- }
32
- catch {
33
- // import.meta is undefined (e.g., in Jest environment before Babel transform)
34
- return '';
35
- }
36
- if (!hasImportMetaUrl) {
20
+ // Use a lock to prevent race conditions when multiple calls happen concurrently
21
+ // This ensures only one execution path computes and sets the cache
22
+ if (global.__dirnameComputing) {
23
+ // Another call is already computing __dirname, wait and retry
24
+ // In practice, this should be rare since module initialization is typically sequential
25
+ let retries = 0;
26
+ while (global.__dirnameComputing && retries < 10) {
27
+ retries++;
28
+ // Busy wait with a small delay (not ideal but works for module init)
29
+ }
30
+ // Return cached value if available after waiting
31
+ if (global.__dirnameCache) {
32
+ return global.__dirnameCache;
33
+ }
34
+ // If still computing after retries, return empty to avoid deadlock
37
35
  return '';
38
36
  }
39
- // Try to compute __dirname using fileURLToPath via createRequire
40
- // This avoids webpack trying to bundle fileURLToPath at module load time
37
+ // Set computing flag to prevent concurrent execution
38
+ global.__dirnameComputing = true;
41
39
  try {
42
- const require = createRequire(import.meta.url);
43
- const urlModule = require('url');
44
- // Check if fileURLToPath is actually a function (webpack may provide a broken polyfill)
45
- if (urlModule && typeof urlModule.fileURLToPath === 'function') {
46
- const __filename = urlModule.fileURLToPath(import.meta.url);
40
+ // In Jest/Babel transformed environment, __filename is available
41
+ // @ts-ignore - __filename is available after Babel transforms ES modules to CommonJS
42
+ if (typeof __filename !== 'undefined') {
47
43
  const computedDirname = path.dirname(__filename);
48
- globalThis.__dirnameCache = computedDirname;
44
+ global.__dirnameCache = computedDirname;
49
45
  return computedDirname;
50
46
  }
51
- }
52
- catch (error) {
53
- // If webpack provides a broken polyfill or require fails, return empty
54
- // __dirname will be computed when the plugin function is called (server-side only)
47
+ // Check if import.meta.url is available
48
+ // Use try-catch to handle cases where import.meta is undefined (e.g., in Jest before transform)
49
+ let hasImportMetaUrl = false;
50
+ try {
51
+ hasImportMetaUrl = typeof import.meta.url !== 'undefined';
52
+ }
53
+ catch {
54
+ // import.meta is undefined (e.g., in Jest environment before Babel transform)
55
+ return '';
56
+ }
57
+ if (!hasImportMetaUrl) {
58
+ return '';
59
+ }
60
+ // Try to compute __dirname using fileURLToPath via createRequire
61
+ // This avoids webpack trying to bundle fileURLToPath at module load time
62
+ try {
63
+ const require = createRequire(import.meta.url);
64
+ const urlModule = require('url');
65
+ // Check if fileURLToPath is actually a function (webpack may provide a broken polyfill)
66
+ if (urlModule && typeof urlModule.fileURLToPath === 'function') {
67
+ const __filename = urlModule.fileURLToPath(import.meta.url);
68
+ const computedDirname = path.dirname(__filename);
69
+ global.__dirnameCache = computedDirname;
70
+ return computedDirname;
71
+ }
72
+ }
73
+ catch (error) {
74
+ // If webpack provides a broken polyfill or require fails, return empty
75
+ // __dirname will be computed when the plugin function is called (server-side only)
76
+ return '';
77
+ }
55
78
  return '';
56
79
  }
57
- return '';
80
+ finally {
81
+ // Always clear the computing flag
82
+ global.__dirnameComputing = false;
83
+ }
58
84
  }
59
85
  // Initialize __dirname at module load time, but handle webpack bundling gracefully
60
86
  let __dirname = '';
@@ -62,33 +88,51 @@ let peerDepsValidated = false;
62
88
  try {
63
89
  // Only compute __dirname if we're in Node.js (not during webpack bundling)
64
90
  if (typeof process !== 'undefined' && ((_a = process.versions) === null || _a === void 0 ? void 0 : _a.node)) {
65
- // In Jest/Babel transformed environment, __filename is available
66
- // @ts-ignore - __filename is available after Babel transforms ES modules to CommonJS
67
- if (typeof __filename !== 'undefined') {
68
- __dirname = path.dirname(__filename);
69
- validatePeerDependencies(__dirname);
70
- peerDepsValidated = true;
71
- }
72
- else {
73
- // Check if import.meta.url is available - use try-catch since import.meta might be undefined
74
- let hasImportMetaUrl = false;
91
+ const global = globalThis;
92
+ // Set lock to prevent concurrent getDirname() calls during module init
93
+ if (!global.__dirnameComputing) {
94
+ global.__dirnameComputing = true;
75
95
  try {
76
- hasImportMetaUrl = typeof import.meta.url !== 'undefined';
77
- }
78
- catch {
79
- // import.meta is undefined (e.g., in Jest environment before Babel transform)
80
- hasImportMetaUrl = false;
81
- }
82
- if (hasImportMetaUrl) {
83
- const require = createRequire(import.meta.url);
84
- const urlModule = require('url');
85
- // Check if fileURLToPath is actually a function (not a webpack polyfill)
86
- if (urlModule && typeof urlModule.fileURLToPath === 'function') {
87
- const __filename = urlModule.fileURLToPath(import.meta.url);
96
+ // In Jest/Babel transformed environment, __filename is available
97
+ // @ts-ignore - __filename is available after Babel transforms ES modules to CommonJS
98
+ if (typeof __filename !== 'undefined') {
88
99
  __dirname = path.dirname(__filename);
100
+ global.__dirnameCache = __dirname;
89
101
  validatePeerDependencies(__dirname);
90
102
  peerDepsValidated = true;
91
103
  }
104
+ else {
105
+ // Check if import.meta.url is available - use try-catch since import.meta might be undefined
106
+ let hasImportMetaUrl = false;
107
+ try {
108
+ hasImportMetaUrl = typeof import.meta.url !== 'undefined';
109
+ }
110
+ catch {
111
+ // import.meta is undefined (e.g., in Jest environment before Babel transform)
112
+ hasImportMetaUrl = false;
113
+ }
114
+ if (hasImportMetaUrl) {
115
+ const require = createRequire(import.meta.url);
116
+ const urlModule = require('url');
117
+ // Check if fileURLToPath is actually a function (not a webpack polyfill)
118
+ if (urlModule && typeof urlModule.fileURLToPath === 'function') {
119
+ const __filename = urlModule.fileURLToPath(import.meta.url);
120
+ __dirname = path.dirname(__filename);
121
+ global.__dirnameCache = __dirname;
122
+ validatePeerDependencies(__dirname);
123
+ peerDepsValidated = true;
124
+ }
125
+ }
126
+ }
127
+ }
128
+ finally {
129
+ global.__dirnameComputing = false;
130
+ }
131
+ }
132
+ else {
133
+ // Another module init is already computing, use cached value if available
134
+ if (global.__dirnameCache) {
135
+ __dirname = global.__dirnameCache;
92
136
  }
93
137
  }
94
138
  }
@@ -115,15 +159,33 @@ if (!peerDepsValidated && typeof process !== 'undefined' && ((_b = process.versi
115
159
  *
116
160
  * A plugin that provides glossary functionality with:
117
161
  * - Glossary terms defined in a JSON file
118
- * - Auto-generated glossary page
119
- * - GlossaryTerm component for inline definitions
120
- * - Tooltips on hover
121
- * - Automatic glossary term detection in markdown files (requires manual remark plugin configuration)
162
+ * - Auto-generated glossary page with term definitions
163
+ * - GlossaryTerm component for inline definitions with interactive tooltips
164
+ * - Automatic client-side initialization via getClientModules() (no manual imports needed)
165
+ * - Optional automatic glossary term detection in markdown files via remark plugin
166
+ *
167
+ * ## Basic Usage (Manual Term Markup)
168
+ *
169
+ * Just install the plugin - the GlossaryTerm component is automatically available:
170
+ * ```javascript
171
+ * module.exports = {
172
+ * plugins: [
173
+ * ['docusaurus-plugin-glossary', {
174
+ * glossaryPath: 'glossary/glossary.json',
175
+ * routePath: '/glossary',
176
+ * }],
177
+ * ],
178
+ * };
179
+ * ```
180
+ *
181
+ * Then use `<GlossaryTerm>` in your MDX files without importing:
182
+ * ```mdx
183
+ * <GlossaryTerm term="API">API</GlossaryTerm>
184
+ * ```
122
185
  *
123
- * IMPORTANT: To enable auto-linking of glossary terms, you must manually add the remark plugin
124
- * to your docusaurus.config.js using the getRemarkPlugin helper:
186
+ * ## Advanced Usage (Automatic Term Detection)
125
187
  *
126
- * Example:
188
+ * To automatically detect and link glossary terms in markdown, add the remark plugin:
127
189
  * ```javascript
128
190
  * const glossaryPlugin = require('docusaurus-plugin-glossary');
129
191
  *
@@ -138,14 +200,6 @@ if (!peerDepsValidated && typeof process !== 'undefined' && ((_b = process.versi
138
200
  * }, { siteDir: __dirname }),
139
201
  * ],
140
202
  * },
141
- * pages: {
142
- * remarkPlugins: [
143
- * glossaryPlugin.getRemarkPlugin({
144
- * glossaryPath: 'glossary/glossary.json',
145
- * routePath: '/glossary',
146
- * }, { siteDir: __dirname }),
147
- * ],
148
- * },
149
203
  * }],
150
204
  * ],
151
205
  * plugins: [
@@ -169,6 +223,11 @@ export default function glossaryPlugin(context, options = {}) {
169
223
  let glossaryDataCache = { terms: [] };
170
224
  return {
171
225
  name: 'docusaurus-plugin-glossary',
226
+ getClientModules() {
227
+ // Compute __dirname if not already set (for webpack bundling compatibility)
228
+ const pluginDirname = __dirname || getDirname();
229
+ return [path.resolve(pluginDirname, './client/index.js')];
230
+ },
172
231
  async loadContent() {
173
232
  // Load glossary terms from JSON file
174
233
  const glossaryFilePath = path.resolve(context.siteDir, glossaryPath);
@@ -224,6 +283,8 @@ export default function glossaryPlugin(context, options = {}) {
224
283
  }
225
284
  // Export remark plugin factory for use in markdown configuration
226
285
  export const remarkPlugin = remarkGlossaryTerms;
286
+ // Export cache clearing utility
287
+ export { clearGlossaryCache } from './remark/glossary-terms.js';
227
288
  /**
228
289
  * Helper function to get the configured remark plugin
229
290
  * This can be used in docusaurus.config.js markdown configuration
@@ -2,9 +2,18 @@ import { visit } from 'unist-util-visit';
2
2
  import path from 'path';
3
3
  import fs from 'fs';
4
4
 
5
+ // Cache for glossary data to avoid repeated synchronous file reads
6
+ // Key: absolute file path, Value: { terms, loadedAt }
7
+ const glossaryCache = new Map();
8
+ const CACHE_TTL = 5000; // 5 seconds TTL to allow for file changes during dev
9
+
5
10
  /**
6
11
  * Creates a remark plugin that automatically detects and replaces glossary terms in markdown
7
12
  *
13
+ * This plugin transforms plain text terms into <GlossaryTerm> JSX elements.
14
+ * The GlossaryTerm component is globally available via the MDXComponents theme wrapper,
15
+ * so no import injection is needed - MDX files can use it without explicit imports.
16
+ *
8
17
  * @param {object} options - Plugin options
9
18
  * @param {Array} options.terms - Array of glossary term objects with {term, definition}
10
19
  * @param {string} options.glossaryPath - Path to glossary JSON file (optional, if terms not provided)
@@ -20,16 +29,56 @@ export default function remarkGlossaryTerms({
20
29
  } = {}) {
21
30
  let glossaryTerms = terms;
22
31
 
23
- // If terms not provided, try to load from glossaryPath (synchronously)
32
+ // If terms not provided, try to load from glossaryPath with caching
24
33
  if (!glossaryTerms.length && glossaryPath && siteDir) {
25
34
  try {
26
35
  const glossaryFilePath = path.resolve(siteDir, glossaryPath);
27
- if (fs.existsSync(glossaryFilePath)) {
28
- const glossaryData = JSON.parse(fs.readFileSync(glossaryFilePath, 'utf8'));
29
- glossaryTerms = glossaryData.terms || [];
36
+ const now = Date.now();
37
+
38
+ // Check cache first to avoid repeated file reads
39
+ const cached = glossaryCache.get(glossaryFilePath);
40
+ if (cached && (now - cached.loadedAt) < CACHE_TTL) {
41
+ glossaryTerms = cached.terms;
42
+ } else {
43
+ // Cache miss or expired - load from file synchronously
44
+ // Note: This is synchronous I/O which can block the build process
45
+ // Consider passing terms directly to avoid this
46
+ if (fs.existsSync(glossaryFilePath)) {
47
+ const fileContent = fs.readFileSync(glossaryFilePath, 'utf8');
48
+ const glossaryData = JSON.parse(fileContent);
49
+ glossaryTerms = glossaryData.terms || [];
50
+
51
+ // Update cache
52
+ glossaryCache.set(glossaryFilePath, {
53
+ terms: glossaryTerms,
54
+ loadedAt: now,
55
+ });
56
+
57
+ // Log only once per file (when cache is first populated)
58
+ if (!cached && process.env.NODE_ENV !== 'production') {
59
+ console.log(`[glossary-plugin] Loaded ${glossaryTerms.length} terms from ${glossaryPath}`);
60
+ }
61
+ } else {
62
+ // File doesn't exist - cache empty result to avoid repeated checks
63
+ glossaryCache.set(glossaryFilePath, {
64
+ terms: [],
65
+ loadedAt: now,
66
+ });
67
+ if (process.env.NODE_ENV !== 'production') {
68
+ console.warn(`[glossary-plugin] Glossary file not found: ${glossaryPath}`);
69
+ }
70
+ }
30
71
  }
31
72
  } catch (error) {
32
- console.warn(`Failed to load glossary from ${glossaryPath}:`, error.message);
73
+ console.warn(`[glossary-plugin] Failed to load glossary from ${glossaryPath}:`, error.message);
74
+ // Cache the error to avoid repeated attempts
75
+ if (glossaryPath && siteDir) {
76
+ const glossaryFilePath = path.resolve(siteDir, glossaryPath);
77
+ glossaryCache.set(glossaryFilePath, {
78
+ terms: [],
79
+ loadedAt: Date.now(),
80
+ });
81
+ }
33
82
  }
34
83
  }
35
84
 
@@ -186,7 +235,8 @@ export default function remarkGlossaryTerms({
186
235
  return result.length > 0 ? result : [{ type: 'text', value: text }];
187
236
  }
188
237
 
189
- return tree => {
238
+ // Return the transformer function
239
+ const transformer = tree => {
190
240
  let usedGlossaryTerm = false;
191
241
  visit(tree, 'text', (node, index, parent) => {
192
242
  // Skip text nodes inside code blocks, links, or existing MDX components
@@ -213,6 +263,7 @@ export default function remarkGlossaryTerms({
213
263
  if (replacement.type === 'mdxJsxFlowElement') {
214
264
  // In paragraph context, we need mdxJsxTextElement instead
215
265
  if (parent.type === 'paragraph') {
266
+ usedGlossaryTerm = true;
216
267
  return {
217
268
  type: 'mdxJsxTextElement',
218
269
  name: replacement.name,
@@ -220,11 +271,6 @@ export default function remarkGlossaryTerms({
220
271
  children: replacement.children,
221
272
  };
222
273
  }
223
- }
224
- if (
225
- replacement.type === 'mdxJsxFlowElement' ||
226
- replacement.type === 'mdxJsxTextElement'
227
- ) {
228
274
  usedGlossaryTerm = true;
229
275
  }
230
276
  return replacement;
@@ -236,13 +282,12 @@ export default function remarkGlossaryTerms({
236
282
  }
237
283
  });
238
284
 
239
- // Inject MDX import for GlossaryTerm if we used it anywhere in this file
285
+ // Inject MDX import for GlossaryTerm if we used it
286
+ // The component is available via theme path, so we just need to import it
240
287
  if (usedGlossaryTerm) {
241
- // Create import node matching MDX v3 expectations
242
- // Both 'value' (the import string) and 'data.estree' (the parsed AST) are required
243
288
  const importNode = {
244
289
  type: 'mdxjsEsm',
245
- value: "import GlossaryTerm from '@theme/GlossaryTerm'",
290
+ value: 'import GlossaryTerm from "@theme/GlossaryTerm";',
246
291
  data: {
247
292
  estree: {
248
293
  type: 'Program',
@@ -259,7 +304,7 @@ export default function remarkGlossaryTerms({
259
304
  source: {
260
305
  type: 'Literal',
261
306
  value: '@theme/GlossaryTerm',
262
- raw: "'@theme/GlossaryTerm'",
307
+ raw: '"@theme/GlossaryTerm"',
263
308
  },
264
309
  },
265
310
  ],
@@ -267,38 +312,44 @@ export default function remarkGlossaryTerms({
267
312
  },
268
313
  };
269
314
 
270
- // Avoid duplicate imports if already present
271
- const hasExistingImport =
315
+ // Check for existing import
316
+ const hasImport =
272
317
  Array.isArray(tree.children) &&
273
- tree.children.some(n => {
274
- if (n.type !== 'mdxjsEsm') return false;
275
- // Check value string (for older MDX format)
276
- if (typeof n.value === 'string' && n.value.includes('@theme/GlossaryTerm')) {
277
- return true;
278
- }
279
- // Check estree data (for newer MDX format)
280
- if (n.data?.estree?.body) {
281
- return n.data.estree.body.some(
282
- stmt =>
283
- stmt.type === 'ImportDeclaration' && stmt.source?.value === '@theme/GlossaryTerm'
284
- );
285
- }
286
- return false;
287
- });
318
+ tree.children.some(n =>
319
+ n.type === 'mdxjsEsm' &&
320
+ (n.value?.includes('@theme/GlossaryTerm') ||
321
+ n.data?.estree?.body?.some(s => s.source?.value === '@theme/GlossaryTerm'))
322
+ );
288
323
 
289
- if (!hasExistingImport) {
290
- // Place import at the very beginning of the file (before all other nodes)
291
- // This ensures it's available when MDX compiles the JSX elements
292
- if (!Array.isArray(tree.children)) {
293
- tree.children = [];
294
- }
295
- // Insert at the very beginning (index 0) to ensure it's processed first
296
- tree.children.unshift(importNode);
297
- // Debug: verify import was added (remove in production)
298
- if (process.env.NODE_ENV !== 'production') {
299
- console.log('[glossary-plugin] Injected GlossaryTerm import');
324
+ if (!hasImport) {
325
+ if (!Array.isArray(tree.children)) tree.children = [];
326
+ let insertIndex = 0;
327
+ for (let i = 0; i < tree.children.length; i++) {
328
+ const node = tree.children[i];
329
+ if (node.type === 'yaml' || node.type === 'toml') {
330
+ insertIndex = i + 1;
331
+ } else {
332
+ break;
333
+ }
300
334
  }
335
+ tree.children.splice(insertIndex, 0, importNode);
301
336
  }
302
337
  }
303
338
  };
339
+
340
+ return transformer;
341
+ }
342
+
343
+ /**
344
+ * Clears the glossary cache
345
+ * Useful for testing or when you want to force a reload of glossary data
346
+ *
347
+ * @param {string} [filePath] - Optional specific file path to clear. If not provided, clears entire cache.
348
+ */
349
+ export function clearGlossaryCache(filePath) {
350
+ if (filePath) {
351
+ glossaryCache.delete(filePath);
352
+ } else {
353
+ glossaryCache.clear();
354
+ }
304
355
  }
@@ -122,7 +122,7 @@ export default function GlossaryTerm({ term, definition, routePath = '/glossary'
122
122
  : undefined
123
123
  }
124
124
  >
125
- <strong>{term}:</strong> {effectiveDefinition}
125
+ <strong>{term}</strong> {effectiveDefinition}
126
126
  </span>
127
127
  )}
128
128
  </span>
@@ -34,7 +34,7 @@ describe('GlossaryTerm', () => {
34
34
  const tooltip = screen.getByRole('tooltip');
35
35
  expect(tooltip).toBeInTheDocument();
36
36
  expect(tooltip).toHaveClass('tooltipVisible');
37
- expect(tooltip).toHaveTextContent('API: Application Programming Interface');
37
+ expect(tooltip).toHaveTextContent('API Application Programming Interface');
38
38
  });
39
39
 
40
40
  it('should hide tooltip on mouse leave', async () => {
package/package.json CHANGED
@@ -1,14 +1,20 @@
1
1
  {
2
2
  "name": "docusaurus-plugin-glossary",
3
- "version": "1.3.1",
3
+ "version": "2.0.0",
4
4
  "description": "A Docusaurus plugin for creating and managing glossary terms with auto-generated pages and inline tooltips",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
8
8
  ".": {
9
- "import": "./lib/index.js"
9
+ "import": "./lib/index.js",
10
+ "require": "./lib/index.js",
11
+ "default": "./lib/index.js"
10
12
  },
11
- "./remark/glossary-terms": "./lib/remark/glossary-terms.js"
13
+ "./remark/glossary-terms": {
14
+ "import": "./lib/remark/glossary-terms.js",
15
+ "require": "./lib/remark/glossary-terms.js",
16
+ "default": "./lib/remark/glossary-terms.js"
17
+ }
12
18
  },
13
19
  "files": [
14
20
  "lib/",
@@ -17,7 +23,7 @@
17
23
  ],
18
24
  "scripts": {
19
25
  "build": "tsc && node scripts/build.js",
20
- "watch": "node scripts/watch.js",
26
+ "watch": "tsc && node scripts/watch.js",
21
27
  "test": "jest",
22
28
  "test:watch": "jest --watch",
23
29
  "test:coverage": "jest --coverage",