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 +116 -20
- 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 +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# docusaurus-plugin-glossary
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/docusaurus-plugin-glossary)
|
|
4
|
+
[](https://github.com/mcclowes/docusaurus-plugin-glossary/actions/workflows/ci.yml)
|
|
5
|
+
|
|
6
|
+

|
|
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
|
|
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';`
|
|
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
|
|
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
|
-
├──
|
|
462
|
-
├──
|
|
463
|
-
│ ├──
|
|
464
|
-
│ └──
|
|
465
|
-
├──
|
|
466
|
-
│
|
|
467
|
-
├──
|
|
468
|
-
│ └──
|
|
469
|
-
│
|
|
470
|
-
│
|
|
471
|
-
└──
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docusaurus-plugin-glossary",
|
|
3
|
-
"version": "
|
|
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",
|