docusaurus-plugin-glossary 1.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 +320 -0
- package/components/GlossaryPage.js +138 -0
- package/components/GlossaryPage.module.css +189 -0
- package/components/GlossaryPage.test.js +205 -0
- package/index.js +70 -0
- package/package.json +64 -0
- package/theme/GlossaryTerm/index.js +49 -0
- package/theme/GlossaryTerm/index.test.js +116 -0
- package/theme/GlossaryTerm/styles.module.css +95 -0
package/README.md
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# docusaurus-plugin-glossary
|
|
2
|
+
|
|
3
|
+
A comprehensive Docusaurus plugin that provides glossary functionality with an auto-generated glossary page, searchable terms, and inline term tooltips.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Auto-generated Glossary Page**: Displays all terms alphabetically with letter navigation
|
|
8
|
+
- **Search Functionality**: Real-time search across terms and definitions
|
|
9
|
+
- **GlossaryTerm Component**: Inline component for linking terms with tooltip previews
|
|
10
|
+
- **Responsive Design**: Mobile-friendly UI with dark mode support
|
|
11
|
+
- **Related Terms**: Link between related glossary terms
|
|
12
|
+
- **Abbreviation Support**: Display full form of abbreviated terms
|
|
13
|
+
- **Customizable**: Configure glossary path and route
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
### For Local Use (Same Repository)
|
|
18
|
+
|
|
19
|
+
1. Copy the plugin directory to your Docusaurus site:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
src/plugins/docusaurus-plugin-glossary/
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
2. Add the plugin to your `docusaurus.config.js`:
|
|
26
|
+
```javascript
|
|
27
|
+
plugins: [
|
|
28
|
+
[
|
|
29
|
+
require.resolve('./src/plugins/docusaurus-plugin-glossary'),
|
|
30
|
+
{
|
|
31
|
+
glossaryPath: 'glossary/glossary.json', // optional, default: 'glossary/glossary.json'
|
|
32
|
+
routePath: '/glossary', // optional, default: '/glossary'
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
];
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### For Separate Package (To Publish)
|
|
39
|
+
|
|
40
|
+
To publish this as a separate npm package:
|
|
41
|
+
|
|
42
|
+
1. Create a new directory for the package:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
mkdir docusaurus-plugin-glossary
|
|
46
|
+
cd docusaurus-plugin-glossary
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
2. Copy the plugin files:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
docusaurus-plugin-glossary/
|
|
53
|
+
├── index.js
|
|
54
|
+
├── components/
|
|
55
|
+
│ ├── GlossaryPage.js
|
|
56
|
+
│ └── GlossaryPage.module.css
|
|
57
|
+
├── theme/
|
|
58
|
+
│ └── GlossaryTerm/
|
|
59
|
+
│ ├── index.js
|
|
60
|
+
│ └── styles.module.css
|
|
61
|
+
├── package.json
|
|
62
|
+
└── README.md
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
3. Create a `package.json`:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"name": "docusaurus-plugin-glossary",
|
|
70
|
+
"version": "1.0.0",
|
|
71
|
+
"description": "A Docusaurus plugin for creating and managing glossary terms",
|
|
72
|
+
"main": "index.js",
|
|
73
|
+
"keywords": ["docusaurus", "glossary", "plugin", "documentation"],
|
|
74
|
+
"peerDependencies": {
|
|
75
|
+
"@docusaurus/core": "^3.0.0",
|
|
76
|
+
"react": "^18.0.0",
|
|
77
|
+
"react-dom": "^18.0.0"
|
|
78
|
+
},
|
|
79
|
+
"dependencies": {
|
|
80
|
+
"fs-extra": "^11.0.0"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
4. Publish to npm:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npm publish
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
5. Install in your Docusaurus site:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm install docusaurus-plugin-glossary
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
6. Add to your `docusaurus.config.js`:
|
|
98
|
+
```javascript
|
|
99
|
+
plugins: [
|
|
100
|
+
[
|
|
101
|
+
'docusaurus-plugin-glossary',
|
|
102
|
+
{
|
|
103
|
+
glossaryPath: 'glossary/glossary.json',
|
|
104
|
+
routePath: '/glossary',
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
];
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Usage
|
|
111
|
+
|
|
112
|
+
### 1. Create a Glossary Data File
|
|
113
|
+
|
|
114
|
+
Create a JSON file at `glossary/glossary.json` (or your configured path):
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"description": "A collection of technical terms and their definitions",
|
|
119
|
+
"terms": [
|
|
120
|
+
{
|
|
121
|
+
"term": "API",
|
|
122
|
+
"abbreviation": "Application Programming Interface",
|
|
123
|
+
"definition": "A set of rules and protocols that allows different software applications to communicate with each other.",
|
|
124
|
+
"relatedTerms": ["REST", "GraphQL"]
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"term": "REST",
|
|
128
|
+
"abbreviation": "Representational State Transfer",
|
|
129
|
+
"definition": "An architectural style for designing networked applications.",
|
|
130
|
+
"relatedTerms": ["API", "HTTP"]
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 2. Glossary Data Structure
|
|
137
|
+
|
|
138
|
+
Each term object can include:
|
|
139
|
+
|
|
140
|
+
- `term` (required): The glossary term
|
|
141
|
+
- `definition` (required): The term's definition
|
|
142
|
+
- `abbreviation` (optional): The full form if the term is an abbreviation
|
|
143
|
+
- `relatedTerms` (optional): Array of related term names
|
|
144
|
+
- `id` (optional): Custom ID for linking (auto-generated from term if not provided)
|
|
145
|
+
|
|
146
|
+
### 3. Using the GlossaryTerm Component
|
|
147
|
+
|
|
148
|
+
Import and use the `GlossaryTerm` component in your MDX files:
|
|
149
|
+
|
|
150
|
+
```jsx
|
|
151
|
+
import GlossaryTerm from '@theme/GlossaryTerm';
|
|
152
|
+
|
|
153
|
+
This website uses an <GlossaryTerm term="API" definition="Application Programming Interface">API</GlossaryTerm> to fetch data.
|
|
154
|
+
|
|
155
|
+
// Or with default display (term name):
|
|
156
|
+
We use <GlossaryTerm term="REST" definition="Representational State Transfer" /> for our web services.
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The component features:
|
|
160
|
+
|
|
161
|
+
- Dotted underline styling
|
|
162
|
+
- Tooltip showing definition on hover
|
|
163
|
+
- Link to full glossary page entry
|
|
164
|
+
- Accessible with keyboard navigation
|
|
165
|
+
|
|
166
|
+
### 4. Accessing the Glossary Page
|
|
167
|
+
|
|
168
|
+
The glossary page is automatically available at `/glossary` (or your configured `routePath`).
|
|
169
|
+
|
|
170
|
+
Features:
|
|
171
|
+
|
|
172
|
+
- Alphabetical grouping with letter navigation
|
|
173
|
+
- Real-time search
|
|
174
|
+
- Related terms linking
|
|
175
|
+
- Responsive design
|
|
176
|
+
- Dark mode support
|
|
177
|
+
|
|
178
|
+
## Configuration Options
|
|
179
|
+
|
|
180
|
+
| Option | Type | Default | Description |
|
|
181
|
+
| -------------- | ------ | -------------------------- | ----------------------------------------------------- |
|
|
182
|
+
| `glossaryPath` | string | `'glossary/glossary.json'` | Path to glossary JSON file relative to site directory |
|
|
183
|
+
| `routePath` | string | `'/glossary'` | URL path for glossary page |
|
|
184
|
+
|
|
185
|
+
## Customization
|
|
186
|
+
|
|
187
|
+
### Styling
|
|
188
|
+
|
|
189
|
+
The plugin uses CSS modules for styling. You can override styles by:
|
|
190
|
+
|
|
191
|
+
1. Creating custom CSS in your site's `src/css/custom.css`:
|
|
192
|
+
|
|
193
|
+
```css
|
|
194
|
+
/* Override glossary term styles */
|
|
195
|
+
.glossaryTermWrapper .glossaryTerm {
|
|
196
|
+
border-bottom-color: #your-color;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* Override tooltip styles */
|
|
200
|
+
.glossaryTermWrapper .tooltip {
|
|
201
|
+
background: #your-background;
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
2. For advanced customization, you can swizzle the components:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
npm run swizzle docusaurus-plugin-glossary GlossaryPage -- --wrap
|
|
209
|
+
npm run swizzle docusaurus-plugin-glossary GlossaryTerm -- --wrap
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Adding to Navbar
|
|
213
|
+
|
|
214
|
+
To add the glossary to your navbar, update your `docusaurus.config.js`:
|
|
215
|
+
|
|
216
|
+
```javascript
|
|
217
|
+
themeConfig: {
|
|
218
|
+
navbar: {
|
|
219
|
+
items: [
|
|
220
|
+
{to: '/glossary', label: 'Glossary', position: 'left'},
|
|
221
|
+
// ... other items
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Examples
|
|
228
|
+
|
|
229
|
+
### Example 1: Technical Documentation
|
|
230
|
+
|
|
231
|
+
```json
|
|
232
|
+
{
|
|
233
|
+
"description": "Technical terms used in our documentation",
|
|
234
|
+
"terms": [
|
|
235
|
+
{
|
|
236
|
+
"term": "Webhook",
|
|
237
|
+
"definition": "An HTTP callback that occurs when something happens; a simple event-notification via HTTP POST.",
|
|
238
|
+
"relatedTerms": ["API", "HTTP"]
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Example 2: Using in MDX
|
|
245
|
+
|
|
246
|
+
```mdx
|
|
247
|
+
---
|
|
248
|
+
title: API Documentation
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
import GlossaryTerm from '@theme/GlossaryTerm';
|
|
252
|
+
|
|
253
|
+
# Getting Started with Our API
|
|
254
|
+
|
|
255
|
+
Our <GlossaryTerm term="API" definition="Application Programming Interface" />
|
|
256
|
+
uses <GlossaryTerm term="REST" definition="Representational State Transfer">RESTful</GlossaryTerm>
|
|
257
|
+
principles to provide a simple and consistent interface.
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Development
|
|
261
|
+
|
|
262
|
+
### File Structure
|
|
263
|
+
|
|
264
|
+
```
|
|
265
|
+
docusaurus-plugin-glossary/
|
|
266
|
+
├── index.js # Main plugin file
|
|
267
|
+
├── components/
|
|
268
|
+
│ ├── GlossaryPage.js # Glossary page component
|
|
269
|
+
│ └── GlossaryPage.module.css # Glossary page styles
|
|
270
|
+
├── theme/
|
|
271
|
+
│ └── GlossaryTerm/
|
|
272
|
+
│ ├── index.js # Term component
|
|
273
|
+
│ └── styles.module.css # Term styles
|
|
274
|
+
└── README.md
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Plugin Lifecycle
|
|
278
|
+
|
|
279
|
+
1. **loadContent**: Reads glossary JSON file
|
|
280
|
+
2. **contentLoaded**: Creates data file and adds route
|
|
281
|
+
3. **getThemePath**: Exposes theme components
|
|
282
|
+
4. **getPathsToWatch**: Watches glossary file for changes
|
|
283
|
+
|
|
284
|
+
## Troubleshooting
|
|
285
|
+
|
|
286
|
+
### Glossary page returns 404
|
|
287
|
+
|
|
288
|
+
- Ensure the plugin is properly configured in `docusaurus.config.js`
|
|
289
|
+
- Check that the `routePath` doesn't conflict with existing routes
|
|
290
|
+
- Run `npm run clear` to clear Docusaurus cache
|
|
291
|
+
|
|
292
|
+
### Glossary terms not showing
|
|
293
|
+
|
|
294
|
+
- Verify `glossary/glossary.json` exists at the correct path
|
|
295
|
+
- Check JSON syntax is valid
|
|
296
|
+
- Ensure `terms` array is properly formatted
|
|
297
|
+
|
|
298
|
+
### GlossaryTerm component not found
|
|
299
|
+
|
|
300
|
+
- Make sure you're importing from `@theme/GlossaryTerm`
|
|
301
|
+
- Try clearing cache with `npm run clear`
|
|
302
|
+
- Restart dev server
|
|
303
|
+
|
|
304
|
+
### Styles not applying
|
|
305
|
+
|
|
306
|
+
- Check for CSS conflicts in your custom CSS
|
|
307
|
+
- Ensure CSS modules are loading correctly
|
|
308
|
+
- Try clearing cache and rebuilding
|
|
309
|
+
|
|
310
|
+
## License
|
|
311
|
+
|
|
312
|
+
MIT
|
|
313
|
+
|
|
314
|
+
## Contributing
|
|
315
|
+
|
|
316
|
+
Contributions are welcome! Please open an issue or submit a pull request.
|
|
317
|
+
|
|
318
|
+
## Credits
|
|
319
|
+
|
|
320
|
+
Built for Docusaurus v3.x
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import React, { useState, useMemo } from 'react';
|
|
2
|
+
import Layout from '@theme/Layout';
|
|
3
|
+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
4
|
+
import styles from './GlossaryPage.module.css';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Groups glossary terms by their first letter
|
|
8
|
+
*/
|
|
9
|
+
function groupTermsByLetter(terms) {
|
|
10
|
+
const grouped = {};
|
|
11
|
+
|
|
12
|
+
terms.forEach(term => {
|
|
13
|
+
const firstLetter = term.term.charAt(0).toUpperCase();
|
|
14
|
+
if (!grouped[firstLetter]) {
|
|
15
|
+
grouped[firstLetter] = [];
|
|
16
|
+
}
|
|
17
|
+
grouped[firstLetter].push(term);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Sort each group alphabetically
|
|
21
|
+
Object.keys(grouped).forEach(letter => {
|
|
22
|
+
grouped[letter].sort((a, b) => a.term.localeCompare(b.term));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return grouped;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* GlossaryPage component - displays all glossary terms
|
|
30
|
+
*/
|
|
31
|
+
export default function GlossaryPage({ glossaryData }) {
|
|
32
|
+
const { siteConfig } = useDocusaurusContext();
|
|
33
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
34
|
+
|
|
35
|
+
const terms = glossaryData?.terms || [];
|
|
36
|
+
|
|
37
|
+
// Filter terms based on search
|
|
38
|
+
const filteredTerms = useMemo(() => {
|
|
39
|
+
if (!searchTerm) return terms;
|
|
40
|
+
|
|
41
|
+
const lowerSearch = searchTerm.toLowerCase();
|
|
42
|
+
return terms.filter(
|
|
43
|
+
term =>
|
|
44
|
+
term.term.toLowerCase().includes(lowerSearch) ||
|
|
45
|
+
term.definition.toLowerCase().includes(lowerSearch)
|
|
46
|
+
);
|
|
47
|
+
}, [terms, searchTerm]);
|
|
48
|
+
|
|
49
|
+
// Group terms by first letter
|
|
50
|
+
const groupedTerms = useMemo(() => {
|
|
51
|
+
return groupTermsByLetter(filteredTerms);
|
|
52
|
+
}, [filteredTerms]);
|
|
53
|
+
|
|
54
|
+
const letters = Object.keys(groupedTerms).sort();
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Layout title="Glossary" description="A glossary of terms and definitions">
|
|
58
|
+
<div className={styles.glossaryContainer}>
|
|
59
|
+
<header className={styles.glossaryHeader}>
|
|
60
|
+
<h1>Glossary</h1>
|
|
61
|
+
<p className={styles.glossaryDescription}>
|
|
62
|
+
{glossaryData?.description || 'A collection of terms and their definitions'}
|
|
63
|
+
</p>
|
|
64
|
+
|
|
65
|
+
<div className={styles.searchContainer}>
|
|
66
|
+
<input
|
|
67
|
+
type="text"
|
|
68
|
+
placeholder="Search terms..."
|
|
69
|
+
className={styles.searchInput}
|
|
70
|
+
value={searchTerm}
|
|
71
|
+
onChange={e => setSearchTerm(e.target.value)}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
</header>
|
|
75
|
+
|
|
76
|
+
{filteredTerms.length === 0 ? (
|
|
77
|
+
<div className={styles.noResults}>
|
|
78
|
+
<p>No terms found matching "{searchTerm}"</p>
|
|
79
|
+
</div>
|
|
80
|
+
) : (
|
|
81
|
+
<div className={styles.glossaryContent}>
|
|
82
|
+
{/* Letter navigation */}
|
|
83
|
+
<nav className={styles.letterNav}>
|
|
84
|
+
{letters.map(letter => (
|
|
85
|
+
<a key={letter} href={`#letter-${letter}`} className={styles.letterLink}>
|
|
86
|
+
{letter}
|
|
87
|
+
</a>
|
|
88
|
+
))}
|
|
89
|
+
</nav>
|
|
90
|
+
|
|
91
|
+
{/* Terms grouped by letter */}
|
|
92
|
+
{letters.map(letter => (
|
|
93
|
+
<section key={letter} id={`letter-${letter}`} className={styles.letterSection}>
|
|
94
|
+
<h2 className={styles.letterHeading}>{letter}</h2>
|
|
95
|
+
<dl className={styles.termList}>
|
|
96
|
+
{groupedTerms[letter].map((term, index) => (
|
|
97
|
+
<div
|
|
98
|
+
key={`${letter}-${index}`}
|
|
99
|
+
className={styles.termItem}
|
|
100
|
+
id={term.id || term.term.toLowerCase().replace(/\s+/g, '-')}
|
|
101
|
+
>
|
|
102
|
+
<dt className={styles.termName}>
|
|
103
|
+
{term.term}
|
|
104
|
+
{term.abbreviation && (
|
|
105
|
+
<span className={styles.abbreviation}> ({term.abbreviation})</span>
|
|
106
|
+
)}
|
|
107
|
+
</dt>
|
|
108
|
+
<dd className={styles.termDefinition}>
|
|
109
|
+
{term.definition}
|
|
110
|
+
{term.relatedTerms && term.relatedTerms.length > 0 && (
|
|
111
|
+
<div className={styles.relatedTerms}>
|
|
112
|
+
<strong>Related terms:</strong>{' '}
|
|
113
|
+
{term.relatedTerms.map((related, idx) => (
|
|
114
|
+
<React.Fragment key={idx}>
|
|
115
|
+
{idx > 0 && ', '}
|
|
116
|
+
<a href={`#${related.toLowerCase().replace(/\s+/g, '-')}`}>
|
|
117
|
+
{related}
|
|
118
|
+
</a>
|
|
119
|
+
</React.Fragment>
|
|
120
|
+
))}
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
</dd>
|
|
124
|
+
</div>
|
|
125
|
+
))}
|
|
126
|
+
</dl>
|
|
127
|
+
</section>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
<footer className={styles.glossaryFooter}>
|
|
133
|
+
<p>Total terms: {terms.length}</p>
|
|
134
|
+
</footer>
|
|
135
|
+
</div>
|
|
136
|
+
</Layout>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
.glossaryContainer {
|
|
2
|
+
max-width: 1200px;
|
|
3
|
+
margin: 0 auto;
|
|
4
|
+
padding: 2rem 1rem;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.glossaryHeader {
|
|
8
|
+
text-align: center;
|
|
9
|
+
margin-bottom: 3rem;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.glossaryHeader h1 {
|
|
13
|
+
font-size: 2.5rem;
|
|
14
|
+
margin-bottom: 1rem;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.glossaryDescription {
|
|
18
|
+
font-size: 1.1rem;
|
|
19
|
+
color: var(--ifm-color-emphasis-700);
|
|
20
|
+
margin-bottom: 2rem;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.searchContainer {
|
|
24
|
+
max-width: 500px;
|
|
25
|
+
margin: 0 auto;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.searchInput {
|
|
29
|
+
width: 100%;
|
|
30
|
+
padding: 0.75rem 1rem;
|
|
31
|
+
font-size: 1rem;
|
|
32
|
+
border: 2px solid var(--ifm-color-emphasis-300);
|
|
33
|
+
border-radius: 0.5rem;
|
|
34
|
+
background: var(--ifm-background-color);
|
|
35
|
+
color: var(--ifm-font-color-base);
|
|
36
|
+
transition: border-color 0.2s;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.searchInput:focus {
|
|
40
|
+
outline: none;
|
|
41
|
+
border-color: var(--ifm-color-primary);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.noResults {
|
|
45
|
+
text-align: center;
|
|
46
|
+
padding: 3rem 1rem;
|
|
47
|
+
color: var(--ifm-color-emphasis-600);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.glossaryContent {
|
|
51
|
+
margin-top: 2rem;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.letterNav {
|
|
55
|
+
display: flex;
|
|
56
|
+
flex-wrap: wrap;
|
|
57
|
+
gap: 0.5rem;
|
|
58
|
+
justify-content: center;
|
|
59
|
+
margin-bottom: 2rem;
|
|
60
|
+
padding: 1rem;
|
|
61
|
+
background: var(--ifm-color-emphasis-100);
|
|
62
|
+
border-radius: 0.5rem;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.letterLink {
|
|
66
|
+
display: inline-flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
justify-content: center;
|
|
69
|
+
width: 2rem;
|
|
70
|
+
height: 2rem;
|
|
71
|
+
font-weight: bold;
|
|
72
|
+
color: var(--ifm-color-primary);
|
|
73
|
+
text-decoration: none;
|
|
74
|
+
border-radius: 0.25rem;
|
|
75
|
+
transition: all 0.2s;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.letterLink:hover {
|
|
79
|
+
background: var(--ifm-color-primary);
|
|
80
|
+
color: white;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.letterSection {
|
|
84
|
+
margin-bottom: 3rem;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.letterHeading {
|
|
88
|
+
font-size: 2rem;
|
|
89
|
+
color: var(--ifm-color-primary);
|
|
90
|
+
border-bottom: 2px solid var(--ifm-color-emphasis-300);
|
|
91
|
+
padding-bottom: 0.5rem;
|
|
92
|
+
margin-bottom: 1.5rem;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.termList {
|
|
96
|
+
display: grid;
|
|
97
|
+
gap: 1.5rem;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.termItem {
|
|
101
|
+
padding: 1.5rem;
|
|
102
|
+
background: var(--ifm-card-background-color);
|
|
103
|
+
border: 1px solid var(--ifm-color-emphasis-200);
|
|
104
|
+
border-radius: 0.5rem;
|
|
105
|
+
transition: all 0.2s;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.termItem:hover {
|
|
109
|
+
border-color: var(--ifm-color-primary);
|
|
110
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.termName {
|
|
114
|
+
font-size: 1.25rem;
|
|
115
|
+
font-weight: bold;
|
|
116
|
+
color: var(--ifm-heading-color);
|
|
117
|
+
margin-bottom: 0.75rem;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.abbreviation {
|
|
121
|
+
font-size: 0.9rem;
|
|
122
|
+
color: var(--ifm-color-emphasis-600);
|
|
123
|
+
font-weight: normal;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.termDefinition {
|
|
127
|
+
margin: 0;
|
|
128
|
+
line-height: 1.6;
|
|
129
|
+
color: var(--ifm-color-emphasis-800);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.relatedTerms {
|
|
133
|
+
margin-top: 1rem;
|
|
134
|
+
padding-top: 1rem;
|
|
135
|
+
border-top: 1px solid var(--ifm-color-emphasis-200);
|
|
136
|
+
font-size: 0.9rem;
|
|
137
|
+
color: var(--ifm-color-emphasis-700);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.relatedTerms a {
|
|
141
|
+
color: var(--ifm-color-primary);
|
|
142
|
+
text-decoration: none;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.relatedTerms a:hover {
|
|
146
|
+
text-decoration: underline;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.glossaryFooter {
|
|
150
|
+
margin-top: 3rem;
|
|
151
|
+
padding-top: 2rem;
|
|
152
|
+
border-top: 1px solid var(--ifm-color-emphasis-200);
|
|
153
|
+
text-align: center;
|
|
154
|
+
color: var(--ifm-color-emphasis-600);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Dark mode adjustments */
|
|
158
|
+
[data-theme='dark'] .termItem {
|
|
159
|
+
background: var(--ifm-background-surface-color);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
[data-theme='dark'] .searchInput {
|
|
163
|
+
background: var(--ifm-background-surface-color);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* Responsive design */
|
|
167
|
+
@media (max-width: 768px) {
|
|
168
|
+
.glossaryHeader h1 {
|
|
169
|
+
font-size: 2rem;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.letterNav {
|
|
173
|
+
gap: 0.25rem;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.letterLink {
|
|
177
|
+
width: 1.75rem;
|
|
178
|
+
height: 1.75rem;
|
|
179
|
+
font-size: 0.9rem;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.termItem {
|
|
183
|
+
padding: 1rem;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.termName {
|
|
187
|
+
font-size: 1.1rem;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, within } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import GlossaryPage from './GlossaryPage';
|
|
5
|
+
|
|
6
|
+
// Mock CSS modules
|
|
7
|
+
jest.mock('./GlossaryPage.module.css', () => ({
|
|
8
|
+
glossaryContainer: 'glossaryContainer',
|
|
9
|
+
glossaryHeader: 'glossaryHeader',
|
|
10
|
+
glossaryDescription: 'glossaryDescription',
|
|
11
|
+
searchContainer: 'searchContainer',
|
|
12
|
+
searchInput: 'searchInput',
|
|
13
|
+
noResults: 'noResults',
|
|
14
|
+
glossaryContent: 'glossaryContent',
|
|
15
|
+
letterNav: 'letterNav',
|
|
16
|
+
letterLink: 'letterLink',
|
|
17
|
+
letterSection: 'letterSection',
|
|
18
|
+
letterHeading: 'letterHeading',
|
|
19
|
+
termList: 'termList',
|
|
20
|
+
termItem: 'termItem',
|
|
21
|
+
termName: 'termName',
|
|
22
|
+
abbreviation: 'abbreviation',
|
|
23
|
+
termDefinition: 'termDefinition',
|
|
24
|
+
relatedTerms: 'relatedTerms',
|
|
25
|
+
glossaryFooter: 'glossaryFooter',
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const mockGlossaryData = {
|
|
29
|
+
description: 'Test glossary',
|
|
30
|
+
terms: [
|
|
31
|
+
{
|
|
32
|
+
id: 'api',
|
|
33
|
+
term: 'API',
|
|
34
|
+
definition: 'Application Programming Interface',
|
|
35
|
+
abbreviation: 'API',
|
|
36
|
+
relatedTerms: ['REST'],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'rest',
|
|
40
|
+
term: 'REST',
|
|
41
|
+
definition: 'Representational State Transfer',
|
|
42
|
+
abbreviation: 'REST',
|
|
43
|
+
relatedTerms: ['API'],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'ml',
|
|
47
|
+
term: 'Machine Learning',
|
|
48
|
+
definition: 'A type of artificial intelligence',
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
describe('GlossaryPage', () => {
|
|
54
|
+
it('should render glossary with all terms', () => {
|
|
55
|
+
render(<GlossaryPage glossaryData={mockGlossaryData} />);
|
|
56
|
+
|
|
57
|
+
expect(screen.getByText('Glossary')).toBeInTheDocument();
|
|
58
|
+
expect(screen.getByText('Test glossary')).toBeInTheDocument();
|
|
59
|
+
// Use getAllBy since terms can appear multiple times (in term names and related links)
|
|
60
|
+
expect(screen.getAllByText('API').length).toBeGreaterThan(0);
|
|
61
|
+
expect(screen.getAllByText('REST').length).toBeGreaterThan(0);
|
|
62
|
+
expect(screen.getByText('Machine Learning')).toBeInTheDocument();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should show search input', () => {
|
|
66
|
+
render(<GlossaryPage glossaryData={mockGlossaryData} />);
|
|
67
|
+
|
|
68
|
+
const searchInput = screen.getByPlaceholderText('Search terms...');
|
|
69
|
+
expect(searchInput).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should filter terms by search query', async () => {
|
|
73
|
+
const user = userEvent.setup();
|
|
74
|
+
render(<GlossaryPage glossaryData={mockGlossaryData} />);
|
|
75
|
+
|
|
76
|
+
const searchInput = screen.getByPlaceholderText('Search terms...');
|
|
77
|
+
await user.type(searchInput, 'API');
|
|
78
|
+
|
|
79
|
+
expect(screen.getByText('API')).toBeInTheDocument();
|
|
80
|
+
expect(screen.queryByText('Machine Learning')).not.toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should filter by definition text', async () => {
|
|
84
|
+
const user = userEvent.setup();
|
|
85
|
+
render(<GlossaryPage glossaryData={mockGlossaryData} />);
|
|
86
|
+
|
|
87
|
+
const searchInput = screen.getByPlaceholderText('Search terms...');
|
|
88
|
+
await user.type(searchInput, 'artificial');
|
|
89
|
+
|
|
90
|
+
expect(screen.getByText('Machine Learning')).toBeInTheDocument();
|
|
91
|
+
expect(screen.queryByText('API')).not.toBeInTheDocument();
|
|
92
|
+
expect(screen.queryByText('REST')).not.toBeInTheDocument();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should show no results message when no matches', async () => {
|
|
96
|
+
const user = userEvent.setup();
|
|
97
|
+
render(<GlossaryPage glossaryData={mockGlossaryData} />);
|
|
98
|
+
|
|
99
|
+
const searchInput = screen.getByPlaceholderText('Search terms...');
|
|
100
|
+
await user.type(searchInput, 'NonexistentTerm');
|
|
101
|
+
|
|
102
|
+
expect(screen.getByText(/No terms found matching/)).toBeInTheDocument();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should group terms by first letter', () => {
|
|
106
|
+
render(<GlossaryPage glossaryData={mockGlossaryData} />);
|
|
107
|
+
|
|
108
|
+
// Check that terms are grouped under their first letter (multiple elements per letter)
|
|
109
|
+
expect(screen.getAllByText('A').length).toBeGreaterThan(0);
|
|
110
|
+
expect(screen.getAllByText('M').length).toBeGreaterThan(0);
|
|
111
|
+
expect(screen.getAllByText('R').length).toBeGreaterThan(0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should sort terms alphabetically within groups', () => {
|
|
115
|
+
const dataWithMultipleTerms = {
|
|
116
|
+
...mockGlossaryData,
|
|
117
|
+
terms: [
|
|
118
|
+
{ id: 'zebra', term: 'Zebra', definition: 'An animal' },
|
|
119
|
+
{ id: 'alpha', term: 'Alpha', definition: 'First letter' },
|
|
120
|
+
{ id: 'beta', term: 'Beta', definition: 'Second letter' },
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
render(<GlossaryPage glossaryData={dataWithMultipleTerms} />);
|
|
125
|
+
|
|
126
|
+
const terms = screen.getAllByText(/Zebra|Alpha|Beta/);
|
|
127
|
+
expect(terms).toHaveLength(3);
|
|
128
|
+
// Check Alpha is before Beta, and Beta is before Zebra
|
|
129
|
+
expect(terms[0]).toHaveTextContent('Alpha');
|
|
130
|
+
expect(terms[1]).toHaveTextContent('Beta');
|
|
131
|
+
expect(terms[2]).toHaveTextContent('Zebra');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should display abbreviations', () => {
|
|
135
|
+
render(<GlossaryPage glossaryData={mockGlossaryData} />);
|
|
136
|
+
|
|
137
|
+
// Check abbreviation appears in document
|
|
138
|
+
expect(screen.getAllByText('(API)').length).toBeGreaterThan(0);
|
|
139
|
+
expect(screen.getAllByText('(REST)').length).toBeGreaterThan(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should display related terms', () => {
|
|
143
|
+
render(<GlossaryPage glossaryData={mockGlossaryData} />);
|
|
144
|
+
|
|
145
|
+
// Related terms header appears multiple times (once per term with related terms)
|
|
146
|
+
expect(screen.getAllByText('Related terms:').length).toBeGreaterThan(0);
|
|
147
|
+
// REST appears as a term and in related links
|
|
148
|
+
expect(screen.getAllByText('REST').length).toBeGreaterThan(0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should show total term count in footer', () => {
|
|
152
|
+
render(<GlossaryPage glossaryData={mockGlossaryData} />);
|
|
153
|
+
|
|
154
|
+
expect(screen.getByText('Total terms: 3')).toBeInTheDocument();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should handle empty terms array', () => {
|
|
158
|
+
const emptyData = { terms: [] };
|
|
159
|
+
render(<GlossaryPage glossaryData={emptyData} />);
|
|
160
|
+
|
|
161
|
+
expect(screen.getByText('Glossary')).toBeInTheDocument();
|
|
162
|
+
expect(screen.getByText('Total terms: 0')).toBeInTheDocument();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should handle missing glossaryData gracefully', () => {
|
|
166
|
+
render(<GlossaryPage glossaryData={null} />);
|
|
167
|
+
|
|
168
|
+
expect(screen.getByText('Glossary')).toBeInTheDocument();
|
|
169
|
+
expect(screen.getByText('Total terms: 0')).toBeInTheDocument();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should handle missing description gracefully', () => {
|
|
173
|
+
const dataWithoutDescription = {
|
|
174
|
+
terms: mockGlossaryData.terms,
|
|
175
|
+
};
|
|
176
|
+
render(<GlossaryPage glossaryData={dataWithoutDescription} />);
|
|
177
|
+
|
|
178
|
+
expect(screen.getByText(/A collection of terms and their definitions/)).toBeInTheDocument();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should generate correct anchor links for terms', () => {
|
|
182
|
+
render(<GlossaryPage glossaryData={mockGlossaryData} />);
|
|
183
|
+
|
|
184
|
+
// Check navigation links are generated
|
|
185
|
+
const navLinks = screen.getAllByRole('link', { name: /^[AMR]$/ });
|
|
186
|
+
expect(navLinks.length).toBeGreaterThan(0);
|
|
187
|
+
expect(navLinks[0]).toHaveAttribute('href', '#letter-A');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should clear search when input is cleared', async () => {
|
|
191
|
+
const user = userEvent.setup();
|
|
192
|
+
render(<GlossaryPage glossaryData={mockGlossaryData} />);
|
|
193
|
+
|
|
194
|
+
const searchInput = screen.getByPlaceholderText('Search terms...');
|
|
195
|
+
await user.type(searchInput, 'Machine');
|
|
196
|
+
|
|
197
|
+
expect(screen.getByText('Machine Learning')).toBeInTheDocument();
|
|
198
|
+
|
|
199
|
+
await user.clear(searchInput);
|
|
200
|
+
|
|
201
|
+
expect(screen.getAllByText('API').length).toBeGreaterThan(0);
|
|
202
|
+
expect(screen.getAllByText('REST').length).toBeGreaterThan(0);
|
|
203
|
+
expect(screen.getByText('Machine Learning')).toBeInTheDocument();
|
|
204
|
+
});
|
|
205
|
+
});
|
package/index.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Docusaurus Glossary Plugin
|
|
6
|
+
*
|
|
7
|
+
* A plugin that provides glossary functionality with:
|
|
8
|
+
* - Glossary terms defined in a JSON file
|
|
9
|
+
* - Auto-generated glossary page
|
|
10
|
+
* - GlossaryTerm component for inline definitions
|
|
11
|
+
* - Tooltips on hover
|
|
12
|
+
*
|
|
13
|
+
* @param {object} context - Docusaurus context
|
|
14
|
+
* @param {object} options - Plugin options
|
|
15
|
+
* @param {string} options.glossaryPath - Path to glossary JSON file (default: 'glossary/glossary.json')
|
|
16
|
+
* @param {string} options.routePath - Route path for glossary page (default: '/glossary')
|
|
17
|
+
* @returns {object} Plugin object
|
|
18
|
+
*/
|
|
19
|
+
function glossaryPlugin(context, options = {}) {
|
|
20
|
+
const { glossaryPath = 'glossary/glossary.json', routePath = '/glossary' } = options;
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
name: 'docusaurus-plugin-glossary',
|
|
24
|
+
|
|
25
|
+
async loadContent() {
|
|
26
|
+
// Load glossary terms from JSON file
|
|
27
|
+
const glossaryFilePath = path.resolve(context.siteDir, glossaryPath);
|
|
28
|
+
|
|
29
|
+
if (await fs.pathExists(glossaryFilePath)) {
|
|
30
|
+
const glossaryData = await fs.readJson(glossaryFilePath);
|
|
31
|
+
return glossaryData;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.warn(`Glossary file not found at ${glossaryFilePath}. Using empty glossary.`);
|
|
35
|
+
return { terms: [] };
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async contentLoaded({ content, actions }) {
|
|
39
|
+
const { createData, addRoute } = actions;
|
|
40
|
+
|
|
41
|
+
// Create data file that can be imported by components
|
|
42
|
+
const glossaryDataPath = await createData('glossary-data.json', JSON.stringify(content));
|
|
43
|
+
|
|
44
|
+
// Add glossary page route
|
|
45
|
+
addRoute({
|
|
46
|
+
path: routePath,
|
|
47
|
+
component: '@site/src/plugins/docusaurus-plugin-glossary/components/GlossaryPage.js',
|
|
48
|
+
exact: true,
|
|
49
|
+
modules: {
|
|
50
|
+
glossaryData: glossaryDataPath,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
getThemePath() {
|
|
56
|
+
return path.resolve(__dirname, './theme');
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
getPathsToWatch() {
|
|
60
|
+
return [path.resolve(context.siteDir, glossaryPath)];
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
async postBuild({ outDir }) {
|
|
64
|
+
// You can add any post-build steps here if needed
|
|
65
|
+
console.log('Glossary plugin: Build completed');
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = glossaryPlugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "docusaurus-plugin-glossary",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A Docusaurus plugin for creating and managing glossary terms with auto-generated pages and inline tooltips",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.js",
|
|
8
|
+
"components/",
|
|
9
|
+
"theme/",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "jest",
|
|
15
|
+
"test:watch": "jest --watch",
|
|
16
|
+
"test:coverage": "jest --coverage",
|
|
17
|
+
"prepublishOnly": "npm test",
|
|
18
|
+
"version": "npm version",
|
|
19
|
+
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md}\"",
|
|
20
|
+
"format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,css,md}\""
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"docusaurus",
|
|
24
|
+
"glossary",
|
|
25
|
+
"plugin",
|
|
26
|
+
"documentation",
|
|
27
|
+
"terms",
|
|
28
|
+
"definitions",
|
|
29
|
+
"tooltip"
|
|
30
|
+
],
|
|
31
|
+
"author": "mcclowes",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/mcclowes/docusaurus-plugin-glossary.git"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/mcclowes/docusaurus-plugin-glossary/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/mcclowes/docusaurus-plugin-glossary#readme",
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@docusaurus/core": "^3.0.0",
|
|
43
|
+
"react": "^18.0.0",
|
|
44
|
+
"react-dom": "^18.0.0"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"fs-extra": "^11.0.0"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=16.14"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@babel/preset-env": "^7.28.5",
|
|
54
|
+
"@babel/preset-react": "^7.28.5",
|
|
55
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
56
|
+
"@testing-library/react": "^16.3.0",
|
|
57
|
+
"@testing-library/user-event": "^14.6.1",
|
|
58
|
+
"babel-jest": "^30.2.0",
|
|
59
|
+
"identity-obj-proxy": "^3.0.0",
|
|
60
|
+
"jest": "^30.2.0",
|
|
61
|
+
"jest-environment-jsdom": "^30.2.0",
|
|
62
|
+
"prettier": "^3.6.2"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import styles from './styles.module.css';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GlossaryTerm component - displays an inline term with tooltip
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import GlossaryTerm from '@theme/GlossaryTerm';
|
|
9
|
+
*
|
|
10
|
+
* <GlossaryTerm term="API" definition="Application Programming Interface" />
|
|
11
|
+
* or
|
|
12
|
+
* <GlossaryTerm term="API">custom display text</GlossaryTerm>
|
|
13
|
+
*
|
|
14
|
+
* @param {object} props
|
|
15
|
+
* @param {string} props.term - The glossary term
|
|
16
|
+
* @param {string} props.definition - The definition to show in tooltip
|
|
17
|
+
* @param {React.ReactNode} props.children - Optional custom display text
|
|
18
|
+
*/
|
|
19
|
+
export default function GlossaryTerm({ term, definition, children }) {
|
|
20
|
+
const [showTooltip, setShowTooltip] = useState(false);
|
|
21
|
+
|
|
22
|
+
const displayText = children || term;
|
|
23
|
+
const termId = term.toLowerCase().replace(/\s+/g, '-');
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<span className={styles.glossaryTermWrapper}>
|
|
27
|
+
<a
|
|
28
|
+
href={`/glossary#${termId}`}
|
|
29
|
+
className={styles.glossaryTerm}
|
|
30
|
+
onMouseEnter={() => setShowTooltip(true)}
|
|
31
|
+
onMouseLeave={() => setShowTooltip(false)}
|
|
32
|
+
onFocus={() => setShowTooltip(true)}
|
|
33
|
+
onBlur={() => setShowTooltip(false)}
|
|
34
|
+
aria-describedby={`tooltip-${termId}`}
|
|
35
|
+
>
|
|
36
|
+
{displayText}
|
|
37
|
+
</a>
|
|
38
|
+
{definition && (
|
|
39
|
+
<span
|
|
40
|
+
id={`tooltip-${termId}`}
|
|
41
|
+
className={`${styles.tooltip} ${showTooltip ? styles.tooltipVisible : ''}`}
|
|
42
|
+
role="tooltip"
|
|
43
|
+
>
|
|
44
|
+
<strong>{term}:</strong> {definition}
|
|
45
|
+
</span>
|
|
46
|
+
)}
|
|
47
|
+
</span>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, within } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import GlossaryTerm from './index';
|
|
5
|
+
|
|
6
|
+
describe('GlossaryTerm', () => {
|
|
7
|
+
it('should render term text', () => {
|
|
8
|
+
render(<GlossaryTerm term="API" definition="Application Programming Interface" />);
|
|
9
|
+
|
|
10
|
+
const link = screen.getByRole('link', { name: 'API' });
|
|
11
|
+
expect(link).toBeInTheDocument();
|
|
12
|
+
expect(link).toHaveAttribute('href', '/glossary#api');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should render custom children text', () => {
|
|
16
|
+
render(
|
|
17
|
+
<GlossaryTerm term="API" definition="Application Programming Interface">
|
|
18
|
+
Application Programming Interface
|
|
19
|
+
</GlossaryTerm>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const link = screen.getByRole('link', { name: 'Application Programming Interface' });
|
|
23
|
+
expect(link).toBeInTheDocument();
|
|
24
|
+
expect(link).toHaveAttribute('href', '/glossary#api');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should show tooltip on hover', async () => {
|
|
28
|
+
const user = userEvent.setup();
|
|
29
|
+
render(<GlossaryTerm term="API" definition="Application Programming Interface" />);
|
|
30
|
+
|
|
31
|
+
const link = screen.getByRole('link');
|
|
32
|
+
await user.hover(link);
|
|
33
|
+
|
|
34
|
+
const tooltip = screen.getByRole('tooltip');
|
|
35
|
+
expect(tooltip).toBeInTheDocument();
|
|
36
|
+
expect(tooltip).toHaveClass('tooltipVisible');
|
|
37
|
+
expect(tooltip).toHaveTextContent('API: Application Programming Interface');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should hide tooltip on mouse leave', async () => {
|
|
41
|
+
const user = userEvent.setup();
|
|
42
|
+
render(<GlossaryTerm term="API" definition="Application Programming Interface" />);
|
|
43
|
+
|
|
44
|
+
const link = screen.getByRole('link');
|
|
45
|
+
await user.hover(link);
|
|
46
|
+
|
|
47
|
+
let tooltip = screen.getByRole('tooltip');
|
|
48
|
+
expect(tooltip).toBeInTheDocument();
|
|
49
|
+
expect(tooltip).toHaveClass('tooltipVisible');
|
|
50
|
+
|
|
51
|
+
await user.unhover(link);
|
|
52
|
+
// Tooltip is still in DOM but hidden via CSS
|
|
53
|
+
tooltip = screen.getByRole('tooltip');
|
|
54
|
+
expect(tooltip).not.toHaveClass('tooltipVisible');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should show tooltip on focus', async () => {
|
|
58
|
+
const user = userEvent.setup();
|
|
59
|
+
render(<GlossaryTerm term="API" definition="Application Programming Interface" />);
|
|
60
|
+
|
|
61
|
+
const link = screen.getByRole('link');
|
|
62
|
+
await user.tab();
|
|
63
|
+
expect(link).toHaveFocus();
|
|
64
|
+
|
|
65
|
+
const tooltip = screen.getByRole('tooltip');
|
|
66
|
+
expect(tooltip).toBeInTheDocument();
|
|
67
|
+
expect(tooltip).toHaveClass('tooltipVisible');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should hide tooltip on blur', async () => {
|
|
71
|
+
const user = userEvent.setup();
|
|
72
|
+
render(<GlossaryTerm term="API" definition="Application Programming Interface" />);
|
|
73
|
+
|
|
74
|
+
const link = screen.getByRole('link');
|
|
75
|
+
await user.tab();
|
|
76
|
+
expect(link).toHaveFocus();
|
|
77
|
+
|
|
78
|
+
let tooltip = screen.getByRole('tooltip');
|
|
79
|
+
expect(tooltip).toBeInTheDocument();
|
|
80
|
+
expect(tooltip).toHaveClass('tooltipVisible');
|
|
81
|
+
|
|
82
|
+
await user.tab();
|
|
83
|
+
// Tooltip is still in DOM but hidden via CSS
|
|
84
|
+
tooltip = screen.getByRole('tooltip');
|
|
85
|
+
expect(tooltip).not.toHaveClass('tooltipVisible');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should generate correct ID from term with spaces', () => {
|
|
89
|
+
render(<GlossaryTerm term="Machine Learning" definition="A type of AI" />);
|
|
90
|
+
|
|
91
|
+
const link = screen.getByRole('link');
|
|
92
|
+
expect(link).toHaveAttribute('href', '/glossary#machine-learning');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should work without definition prop', () => {
|
|
96
|
+
render(<GlossaryTerm term="TestTerm" />);
|
|
97
|
+
|
|
98
|
+
const link = screen.getByRole('link', { name: 'TestTerm' });
|
|
99
|
+
expect(link).toBeInTheDocument();
|
|
100
|
+
expect(link).toHaveAttribute('href', '/glossary#testterm');
|
|
101
|
+
|
|
102
|
+
// No tooltip should be rendered when no definition
|
|
103
|
+
const tooltip = screen.queryByRole('tooltip');
|
|
104
|
+
expect(tooltip).not.toBeInTheDocument();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should have proper ARIA attributes', () => {
|
|
108
|
+
render(<GlossaryTerm term="API" definition="Application Programming Interface" />);
|
|
109
|
+
|
|
110
|
+
const link = screen.getByRole('link');
|
|
111
|
+
expect(link).toHaveAttribute('aria-describedby', 'tooltip-api');
|
|
112
|
+
|
|
113
|
+
const tooltip = screen.getByRole('tooltip');
|
|
114
|
+
expect(tooltip).toHaveAttribute('id', 'tooltip-api');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
.glossaryTermWrapper {
|
|
2
|
+
position: relative;
|
|
3
|
+
display: inline;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.glossaryTerm {
|
|
7
|
+
color: var(--ifm-color-primary);
|
|
8
|
+
text-decoration: none;
|
|
9
|
+
border-bottom: 1px dotted var(--ifm-color-primary);
|
|
10
|
+
cursor: help;
|
|
11
|
+
font-weight: 500;
|
|
12
|
+
transition: all 0.2s;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.glossaryTerm:hover {
|
|
16
|
+
color: var(--ifm-color-primary-dark);
|
|
17
|
+
border-bottom-style: solid;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.tooltip {
|
|
21
|
+
position: absolute;
|
|
22
|
+
bottom: 100%;
|
|
23
|
+
left: 50%;
|
|
24
|
+
transform: translateX(-50%) translateY(-8px);
|
|
25
|
+
padding: 0.75rem 1rem;
|
|
26
|
+
background: var(--ifm-background-surface-color);
|
|
27
|
+
color: var(--ifm-font-color-base);
|
|
28
|
+
border: 1px solid var(--ifm-color-emphasis-300);
|
|
29
|
+
border-radius: 0.5rem;
|
|
30
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
31
|
+
font-size: 0.875rem;
|
|
32
|
+
line-height: 1.4;
|
|
33
|
+
white-space: normal;
|
|
34
|
+
max-width: 300px;
|
|
35
|
+
min-width: 200px;
|
|
36
|
+
z-index: 1000;
|
|
37
|
+
opacity: 0;
|
|
38
|
+
pointer-events: none;
|
|
39
|
+
transition: opacity 0.2s;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.tooltip::after {
|
|
43
|
+
content: '';
|
|
44
|
+
position: absolute;
|
|
45
|
+
top: 100%;
|
|
46
|
+
left: 50%;
|
|
47
|
+
transform: translateX(-50%);
|
|
48
|
+
border: 6px solid transparent;
|
|
49
|
+
border-top-color: var(--ifm-background-surface-color);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.tooltip::before {
|
|
53
|
+
content: '';
|
|
54
|
+
position: absolute;
|
|
55
|
+
top: 100%;
|
|
56
|
+
left: 50%;
|
|
57
|
+
transform: translateX(-50%);
|
|
58
|
+
border: 7px solid transparent;
|
|
59
|
+
border-top-color: var(--ifm-color-emphasis-300);
|
|
60
|
+
margin-top: 1px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.tooltipVisible {
|
|
64
|
+
opacity: 1;
|
|
65
|
+
pointer-events: auto;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.tooltip strong {
|
|
69
|
+
display: block;
|
|
70
|
+
margin-bottom: 0.25rem;
|
|
71
|
+
color: var(--ifm-color-primary);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Dark mode adjustments */
|
|
75
|
+
[data-theme='dark'] .tooltip {
|
|
76
|
+
background: var(--ifm-background-surface-color);
|
|
77
|
+
border-color: var(--ifm-color-emphasis-400);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
[data-theme='dark'] .tooltip::after {
|
|
81
|
+
border-top-color: var(--ifm-background-surface-color);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
[data-theme='dark'] .tooltip::before {
|
|
85
|
+
border-top-color: var(--ifm-color-emphasis-400);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Responsive adjustments */
|
|
89
|
+
@media (max-width: 768px) {
|
|
90
|
+
.tooltip {
|
|
91
|
+
max-width: 250px;
|
|
92
|
+
min-width: 180px;
|
|
93
|
+
font-size: 0.8rem;
|
|
94
|
+
}
|
|
95
|
+
}
|