docusaurus-plugin-glossary 1.3.2 → 2.0.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.
package/README.md CHANGED
@@ -1,8 +1,13 @@
1
1
  # docusaurus-plugin-glossary
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/docusaurus-plugin-glossary.svg)](https://www.npmjs.com/package/docusaurus-plugin-glossary)
4
+ [![ci](https://github.com/mcclowes/docusaurus-plugin-glossary/actions/workflows/ci.yml/badge.svg)](https://github.com/mcclowes/docusaurus-plugin-glossary/actions/workflows/ci.yml)
5
+
6
+ ![docusaurus-plugin-glossary banner](banner.png)
7
+
3
8
  A comprehensive Docusaurus plugin that provides glossary functionality with an auto-generated glossary page, searchable terms, and inline term tooltips.
4
9
 
5
- > Compatibility: Fully compatible with Docusaurus v3 (MDX v3). If you were on a v2-era fork, please upgrade to the latest 1.x release of this plugin.
10
+ > Compatibility: Fully compatible with Docusaurus v3 (MDX v3). If you were on a v2-era fork, please upgrade to the latest 2.x release of this plugin.
6
11
 
7
12
  ## Features
8
13
 
@@ -246,9 +251,46 @@ module.exports = {
246
251
  };
247
252
  ```
248
253
 
254
+ ## How It Works
255
+
256
+ This plugin uses a **hybrid approach** combining build-time transformation and runtime enhancements, inspired by `docusaurus-plugin-image-zoom`:
257
+
258
+ ### Build-Time: Remark Plugin
259
+
260
+ The remark plugin automatically detects glossary terms in your markdown and:
261
+ 1. Transforms plain text terms into `<GlossaryTerm>` JSX components
262
+ 2. Automatically injects the necessary import statement (`import GlossaryTerm from '@theme/GlossaryTerm';`)
263
+ 3. This happens during the MDX compilation, before React renders anything
264
+
265
+ **No manual imports needed!** When you write:
266
+ ```markdown
267
+ Our API uses REST principles.
268
+ ```
269
+
270
+ The remark plugin transforms it to:
271
+ ```jsx
272
+ import GlossaryTerm from '@theme/GlossaryTerm';
273
+
274
+ Our <GlossaryTerm term="API">API</GlossaryTerm> uses <GlossaryTerm term="REST">REST</GlossaryTerm> principles.
275
+ ```
276
+
277
+ ### Runtime: Client Modules
278
+
279
+ The plugin uses Docusaurus's `getClientModules()` API to automatically load client-side code on every page. This ensures:
280
+ - Glossary term functionality is available globally without configuration
281
+ - Components initialize correctly on each route change
282
+ - No performance impact from manual module loading
283
+
284
+ ### Theme Components
285
+
286
+ The `GlossaryTerm` component is provided via the theme system (`@theme/GlossaryTerm`), making it:
287
+ - Available to all MDX files through automatic imports
288
+ - Swizzlable for custom styling and behavior
289
+ - Accessible to both the remark plugin and manual usage
290
+
249
291
  ## Docusaurus v3 Notes and Troubleshooting
250
292
 
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:
293
+ - **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
294
 
253
295
  ```mdx
254
296
  import GlossaryTerm from '@theme/GlossaryTerm';
@@ -261,7 +303,7 @@ module.exports = {
261
303
  - Ensure the plugin is listed in `plugins` AND the remark plugin is configured in your preset (see Step 2 above).
262
304
  - Visit `/glossary`. If the page or route fails to render, verify your `glossaryPath` file exists and contains a `terms` array.
263
305
  - 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.
306
+ - 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
307
 
266
308
  - **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
309
 
@@ -454,38 +496,85 @@ principles to provide a simple and consistent interface.
454
496
 
455
497
  ## Development
456
498
 
499
+ This project is written in TypeScript and compiles to JavaScript. The source files are in `src/` and the compiled output goes to `lib/`.
500
+
457
501
  ### File Structure
458
502
 
459
503
  ```
460
504
  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
505
+ ├── src/
506
+ ├── index.ts # Main plugin entry point (TypeScript)
507
+ │ ├── client/
508
+ └── index.js # Client module for runtime initialization
509
+ ├── components/
510
+ │ ├── GlossaryPage.js # Glossary page component
511
+ │ │ ├── GlossaryPage.module.css
512
+ └── GlossaryPage.test.js
513
+ ├── remark/
514
+ └── glossary-terms.js # Remark plugin for automatic term detection
515
+ └── theme/
516
+ │ └── GlossaryTerm/
517
+ │ ├── index.js # Term component
518
+ │ ├── styles.module.css
519
+ │ └── index.test.js
520
+ ├── lib/ # Compiled output (generated by build)
521
+ │ ├── index.js # Main plugin file (compiled from src/index.ts)
522
+ │ ├── client/ # Copied from src/client/
523
+ │ ├── components/ # Copied from src/components/
524
+ │ ├── remark/ # Copied from src/remark/
525
+ │ └── theme/ # Copied from src/theme/
526
+ ├── __tests__/
527
+ │ └── plugin.test.js # Plugin lifecycle tests
528
+ ├── examples/
529
+ │ └── docusaurus-v3/ # Example Docusaurus site
530
+ ├── scripts/
531
+ │ ├── build.js # Build script (TypeScript + copy files)
532
+ │ ├── watch.js # Watch script for development
533
+ │ └── ...
534
+ └── package.json
535
+ ```
536
+
537
+ ### Building
538
+
539
+ The project uses TypeScript for the main plugin entry point (`src/index.ts`) and JavaScript for components. To build:
540
+
541
+ ```bash
542
+ npm run build
543
+ ```
544
+
545
+ This will:
546
+ 1. Compile TypeScript (`src/index.ts`) to JavaScript (`lib/index.js`)
547
+ 2. Copy JavaScript, CSS, and test files from `src/` to `lib/`
548
+
549
+ For development, use:
550
+
551
+ ```bash
552
+ npm run watch
472
553
  ```
473
554
 
555
+ This watches for changes and automatically rebuilds.
556
+
474
557
  ### Plugin Lifecycle
475
558
 
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
559
+ The plugin follows Docusaurus plugin lifecycle hooks:
560
+
561
+ 1. **getClientModules**: Returns client modules that load automatically on every page (provides runtime initialization)
562
+ 2. **loadContent**: Reads glossary JSON file from the configured path
563
+ 3. **contentLoaded**: Creates data files for components and remark plugin, adds glossary page route
564
+ 4. **getThemePath**: Exposes theme components (`GlossaryTerm`)
565
+ 5. **getPathsToWatch**: Watches glossary file for changes during development
566
+ 6. **postBuild**: Optional post-build hook for additional processing
480
567
 
481
568
  ### Remark Plugin
482
569
 
483
- The remark plugin (`remark/glossary-terms.js`) automatically detects glossary terms in markdown files and replaces them with `GlossaryTerm` components. It:
570
+ The remark plugin (`remark/glossary-terms.js`) automatically detects glossary terms in markdown files and transforms them at build time. It:
484
571
 
485
572
  - Scans text nodes for glossary terms (case-insensitive, whole word matching)
486
- - Replaces matching terms with MDX components that show tooltips
573
+ - Replaces matching terms with `<GlossaryTerm>` MDX components
574
+ - Automatically injects the necessary import statement (`import GlossaryTerm from '@theme/GlossaryTerm';`)
487
575
  - Skips terms inside code blocks, links, or existing MDX components
488
576
  - Respects word boundaries to avoid partial matches
577
+ - Handles plural forms (e.g., "API" matches "APIs")
489
578
 
490
579
  ## Troubleshooting
491
580
 
@@ -534,3 +623,10 @@ Contributions are welcome! Please see our [Contributing Guide](CONTRIBUTING.md)
534
623
  ## Credits
535
624
 
536
625
  Built for Docusaurus v3.x
626
+
627
+ ## Technical Details
628
+
629
+ - **Language**: TypeScript (main plugin) + JavaScript (components)
630
+ - **Build System**: TypeScript compiler + custom build scripts
631
+ - **Package Entry Point**: `lib/index.js` (compiled from `src/index.ts`)
632
+ - **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,6 +1,6 @@
1
1
  {
2
2
  "name": "docusaurus-plugin-glossary",
3
- "version": "1.3.2",
3
+ "version": "2.0.1",
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",
@@ -23,7 +23,7 @@
23
23
  ],
24
24
  "scripts": {
25
25
  "build": "tsc && node scripts/build.js",
26
- "watch": "node scripts/watch.js",
26
+ "watch": "tsc && node scripts/watch.js",
27
27
  "test": "jest",
28
28
  "test:watch": "jest --watch",
29
29
  "test:coverage": "jest --coverage",