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 +110 -19
- package/lib/client/index.js +35 -0
- package/lib/index.js +131 -70
- package/lib/remark/glossary-terms.js +95 -44
- package/lib/theme/GlossaryTerm/index.js +1 -1
- package/lib/theme/GlossaryTerm/index.test.js +1 -1
- package/package.json +10 -4
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';`
|
|
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
|
|
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
|
-
├──
|
|
462
|
-
├──
|
|
463
|
-
│ ├──
|
|
464
|
-
│ └──
|
|
465
|
-
├──
|
|
466
|
-
│
|
|
467
|
-
├──
|
|
468
|
-
│ └──
|
|
469
|
-
│
|
|
470
|
-
│
|
|
471
|
-
└──
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
17
|
-
|
|
16
|
+
const global = globalThis;
|
|
17
|
+
if (global.__dirnameCache) {
|
|
18
|
+
return global.__dirnameCache;
|
|
18
19
|
}
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
//
|
|
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
|
-
//
|
|
40
|
-
|
|
37
|
+
// Set computing flag to prevent concurrent execution
|
|
38
|
+
global.__dirnameComputing = true;
|
|
41
39
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
44
|
+
global.__dirnameCache = computedDirname;
|
|
49
45
|
return computedDirname;
|
|
50
46
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
//
|
|
67
|
-
if (
|
|
68
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
* -
|
|
121
|
-
* -
|
|
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
|
-
*
|
|
124
|
-
* to your docusaurus.config.js using the getRemarkPlugin helper:
|
|
186
|
+
* ## Advanced Usage (Automatic Term Detection)
|
|
125
187
|
*
|
|
126
|
-
*
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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: "
|
|
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
|
-
//
|
|
271
|
-
const
|
|
315
|
+
// Check for existing import
|
|
316
|
+
const hasImport =
|
|
272
317
|
Array.isArray(tree.children) &&
|
|
273
|
-
tree.children.some(n =>
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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 (!
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
tree.children
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
}
|
|
@@ -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
|
|
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": "
|
|
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":
|
|
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",
|