docusaurus-plugin-glossary 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,6 +7,7 @@ A comprehensive Docusaurus plugin that provides glossary functionality with an a
7
7
  - **Auto-generated Glossary Page**: Displays all terms alphabetically with letter navigation
8
8
  - **Search Functionality**: Real-time search across terms and definitions
9
9
  - **GlossaryTerm Component**: Inline component for linking terms with tooltip previews
10
+ - **Automatic Term Detection**: Automatically detect and link glossary terms in markdown files with tooltips
10
11
  - **Responsive Design**: Mobile-friendly UI with dark mode support
11
12
  - **Related Terms**: Link between related glossary terms
12
13
  - **Abbreviation Support**: Display full form of abbreviated terms
@@ -24,15 +25,44 @@ A comprehensive Docusaurus plugin that provides glossary functionality with an a
24
25
 
25
26
  2. Add the plugin to your `docusaurus.config.js`:
26
27
  ```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
- },
28
+ const glossaryPlugin = require.resolve('./src/plugins/docusaurus-plugin-glossary');
29
+
30
+ module.exports = {
31
+ // ... other config
32
+ plugins: [
33
+ [
34
+ glossaryPlugin,
35
+ {
36
+ glossaryPath: 'glossary/glossary.json', // optional, default: 'glossary/glossary.json'
37
+ routePath: '/glossary', // optional, default: '/glossary'
38
+ autoLinkTerms: true, // optional, default: true - automatically link terms in markdown
39
+ },
40
+ ],
34
41
  ],
35
- ];
42
+ // ... other config
43
+ };
44
+ ```
45
+
46
+ 3. **Enable automatic term detection** by adding the remark plugin to your markdown configuration:
47
+ ```javascript
48
+ const glossaryPlugin = require('./src/plugins/docusaurus-plugin-glossary');
49
+
50
+ module.exports = {
51
+ // ... other config
52
+ markdown: {
53
+ remarkPlugins: [
54
+ [
55
+ glossaryPlugin.remarkPlugin,
56
+ {
57
+ glossaryPath: 'glossary/glossary.json',
58
+ routePath: '/glossary',
59
+ siteDir: process.cwd(), // or your site directory
60
+ },
61
+ ],
62
+ ],
63
+ },
64
+ // ... other config
65
+ };
36
66
  ```
37
67
 
38
68
  ### For Separate Package (To Publish)
@@ -54,6 +84,8 @@ To publish this as a separate npm package:
54
84
  ├── components/
55
85
  │ ├── GlossaryPage.js
56
86
  │ └── GlossaryPage.module.css
87
+ ├── remark/
88
+ │ └── glossary-terms.js
57
89
  ├── theme/
58
90
  │ └── GlossaryTerm/
59
91
  │ ├── index.js
@@ -70,6 +102,14 @@ To publish this as a separate npm package:
70
102
  "version": "1.0.0",
71
103
  "description": "A Docusaurus plugin for creating and managing glossary terms",
72
104
  "main": "index.js",
105
+ "files": [
106
+ "index.js",
107
+ "components/",
108
+ "theme/",
109
+ "remark/",
110
+ "README.md",
111
+ "LICENSE"
112
+ ],
73
113
  "keywords": ["docusaurus", "glossary", "plugin", "documentation"],
74
114
  "peerDependencies": {
75
115
  "@docusaurus/core": "^3.0.0",
@@ -77,7 +117,11 @@ To publish this as a separate npm package:
77
117
  "react-dom": "^18.0.0"
78
118
  },
79
119
  "dependencies": {
80
- "fs-extra": "^11.0.0"
120
+ "fs-extra": "^11.0.0",
121
+ "unist-util-visit": "^5.0.0"
122
+ },
123
+ "engines": {
124
+ "node": ">=16.14"
81
125
  }
82
126
  }
83
127
  ```
@@ -96,15 +140,43 @@ To publish this as a separate npm package:
96
140
 
97
141
  6. Add to your `docusaurus.config.js`:
98
142
  ```javascript
99
- plugins: [
100
- [
101
- 'docusaurus-plugin-glossary',
102
- {
103
- glossaryPath: 'glossary/glossary.json',
104
- routePath: '/glossary',
105
- },
143
+ const glossaryPlugin = require('docusaurus-plugin-glossary');
144
+
145
+ module.exports = {
146
+ // ... other config
147
+ plugins: [
148
+ [
149
+ 'docusaurus-plugin-glossary',
150
+ {
151
+ glossaryPath: 'glossary/glossary.json',
152
+ routePath: '/glossary',
153
+ },
154
+ ],
106
155
  ],
107
- ];
156
+ // ... other config
157
+ };
158
+ ```
159
+
160
+ 7. **Enable automatic term detection** by adding the remark plugin to your markdown configuration:
161
+ ```javascript
162
+ const glossaryPlugin = require('docusaurus-plugin-glossary');
163
+
164
+ module.exports = {
165
+ // ... other config
166
+ markdown: {
167
+ remarkPlugins: [
168
+ [
169
+ glossaryPlugin.remarkPlugin,
170
+ {
171
+ glossaryPath: 'glossary/glossary.json',
172
+ routePath: '/glossary',
173
+ siteDir: process.cwd(), // or your site directory
174
+ },
175
+ ],
176
+ ],
177
+ },
178
+ // ... other config
179
+ };
108
180
  ```
109
181
 
110
182
  ## Usage
@@ -143,9 +215,26 @@ Each term object can include:
143
215
  - `relatedTerms` (optional): Array of related term names
144
216
  - `id` (optional): Custom ID for linking (auto-generated from term if not provided)
145
217
 
146
- ### 3. Using the GlossaryTerm Component
218
+ ### 3. Automatic Term Detection
219
+
220
+ When the remark plugin is configured (see Installation), glossary terms are automatically detected and linked in all markdown files. Simply write your content normally:
221
+
222
+ ```markdown
223
+ Our API uses REST principles to provide a simple interface.
224
+
225
+ This project supports webhooks for real-time notifications.
226
+ ```
227
+
228
+ Terms like "API", "REST", and "webhooks" will automatically be detected if they're defined in your glossary and will appear with:
229
+ - Dotted underline styling
230
+ - Tooltip showing definition on hover
231
+ - Link to full glossary page entry
232
+
233
+ **Note**: Automatic detection works for whole words only and respects word boundaries. Terms inside code blocks, links, or existing MDX components are not processed.
234
+
235
+ ### 4. Using the GlossaryTerm Component Manually
147
236
 
148
- Import and use the `GlossaryTerm` component in your MDX files:
237
+ For more control or when automatic detection isn't sufficient, you can manually import and use the `GlossaryTerm` component in your MDX files:
149
238
 
150
239
  ```jsx
151
240
  import GlossaryTerm from '@theme/GlossaryTerm';
@@ -163,7 +252,7 @@ The component features:
163
252
  - Link to full glossary page entry
164
253
  - Accessible with keyboard navigation
165
254
 
166
- ### 4. Accessing the Glossary Page
255
+ ### 5. Accessing the Glossary Page
167
256
 
168
257
  The glossary page is automatically available at `/glossary` (or your configured `routePath`).
169
258
 
@@ -177,10 +266,11 @@ Features:
177
266
 
178
267
  ## Configuration Options
179
268
 
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 |
269
+ | Option | Type | Default | Description |
270
+ | -------------- | ------- | -------------------------- | ----------------------------------------------------- |
271
+ | `glossaryPath` | string | `'glossary/glossary.json'` | Path to glossary JSON file relative to site directory |
272
+ | `routePath` | string | `'/glossary'` | URL path for glossary page |
273
+ | `autoLinkTerms`| boolean | `true` | Enable automatic term detection in markdown (requires remark plugin configuration) |
184
274
 
185
275
  ## Customization
186
276
 
@@ -241,7 +331,24 @@ themeConfig: {
241
331
  }
242
332
  ```
243
333
 
244
- ### Example 2: Using in MDX
334
+ ### Example 2: Automatic Term Detection
335
+
336
+ With the remark plugin configured, you can simply write markdown normally:
337
+
338
+ ```markdown
339
+ ---
340
+ title: API Documentation
341
+ ---
342
+
343
+ # Getting Started with Our API
344
+
345
+ Our API uses RESTful principles to provide a simple and consistent interface.
346
+ Webhooks are supported for real-time event notifications.
347
+ ```
348
+
349
+ The terms "API", "RESTful", and "Webhooks" will automatically be detected and linked if they're defined in your glossary.
350
+
351
+ ### Example 3: Using in MDX Manually
245
352
 
246
353
  ```mdx
247
354
  ---
@@ -267,6 +374,8 @@ docusaurus-plugin-glossary/
267
374
  ├── components/
268
375
  │ ├── GlossaryPage.js # Glossary page component
269
376
  │ └── GlossaryPage.module.css # Glossary page styles
377
+ ├── remark/
378
+ │ └── glossary-terms.js # Remark plugin for automatic term detection
270
379
  ├── theme/
271
380
  │ └── GlossaryTerm/
272
381
  │ ├── index.js # Term component
@@ -281,6 +390,15 @@ docusaurus-plugin-glossary/
281
390
  3. **getThemePath**: Exposes theme components
282
391
  4. **getPathsToWatch**: Watches glossary file for changes
283
392
 
393
+ ### Remark Plugin
394
+
395
+ The remark plugin (`remark/glossary-terms.js`) automatically detects glossary terms in markdown files and replaces them with `GlossaryTerm` components. It:
396
+
397
+ - Scans text nodes for glossary terms (case-insensitive, whole word matching)
398
+ - Replaces matching terms with MDX components that show tooltips
399
+ - Skips terms inside code blocks, links, or existing MDX components
400
+ - Respects word boundaries to avoid partial matches
401
+
284
402
  ## Troubleshooting
285
403
 
286
404
  ### Glossary page returns 404
@@ -301,6 +419,14 @@ docusaurus-plugin-glossary/
301
419
  - Try clearing cache with `npm run clear`
302
420
  - Restart dev server
303
421
 
422
+ ### Automatic term detection not working
423
+
424
+ - Ensure the remark plugin is configured in `markdown.remarkPlugins` in `docusaurus.config.js`
425
+ - Check that `glossaryPath` and `siteDir` are correctly configured in the remark plugin options
426
+ - Verify your glossary file exists and contains terms
427
+ - Try clearing cache with `npm run clear` and restarting the dev server
428
+ - Note that terms inside code blocks, links, or MDX components are not processed
429
+
304
430
  ### Styles not applying
305
431
 
306
432
  - Check for CSS conflicts in your custom CSS
package/index.js CHANGED
@@ -9,15 +9,23 @@ const fs = require('fs-extra');
9
9
  * - Auto-generated glossary page
10
10
  * - GlossaryTerm component for inline definitions
11
11
  * - Tooltips on hover
12
+ * - Automatic glossary term detection in markdown files
12
13
  *
13
14
  * @param {object} context - Docusaurus context
14
15
  * @param {object} options - Plugin options
15
16
  * @param {string} options.glossaryPath - Path to glossary JSON file (default: 'glossary/glossary.json')
16
17
  * @param {string} options.routePath - Route path for glossary page (default: '/glossary')
18
+ * @param {boolean} options.autoLinkTerms - Automatically link glossary terms in markdown (default: true)
17
19
  * @returns {object} Plugin object
18
20
  */
19
21
  function glossaryPlugin(context, options = {}) {
20
- const { glossaryPath = 'glossary/glossary.json', routePath = '/glossary' } = options;
22
+ const {
23
+ glossaryPath = 'glossary/glossary.json',
24
+ routePath = '/glossary',
25
+ autoLinkTerms = true
26
+ } = options;
27
+
28
+ let glossaryDataCache = { terms: [] };
21
29
 
22
30
  return {
23
31
  name: 'docusaurus-plugin-glossary',
@@ -28,10 +36,12 @@ function glossaryPlugin(context, options = {}) {
28
36
 
29
37
  if (await fs.pathExists(glossaryFilePath)) {
30
38
  const glossaryData = await fs.readJson(glossaryFilePath);
39
+ glossaryDataCache = glossaryData;
31
40
  return glossaryData;
32
41
  }
33
42
 
34
43
  console.warn(`Glossary file not found at ${glossaryFilePath}. Using empty glossary.`);
44
+ glossaryDataCache = { terms: [] };
35
45
  return { terms: [] };
36
46
  },
37
47
 
@@ -40,11 +50,20 @@ function glossaryPlugin(context, options = {}) {
40
50
 
41
51
  // Create data file that can be imported by components
42
52
  const glossaryDataPath = await createData('glossary-data.json', JSON.stringify(content));
53
+
54
+ // Create a data file for the remark plugin to access glossary terms
55
+ const remarkGlossaryDataPath = await createData(
56
+ 'remark-glossary-data.json',
57
+ JSON.stringify({
58
+ terms: content.terms || [],
59
+ routePath: routePath,
60
+ })
61
+ );
43
62
 
44
63
  // Add glossary page route
45
64
  addRoute({
46
65
  path: routePath,
47
- component: '@site/src/plugins/docusaurus-plugin-glossary/components/GlossaryPage.js',
66
+ component: path.join(__dirname, 'components/GlossaryPage.js'),
48
67
  exact: true,
49
68
  modules: {
50
69
  glossaryData: glossaryDataPath,
@@ -67,4 +86,32 @@ function glossaryPlugin(context, options = {}) {
67
86
  };
68
87
  }
69
88
 
89
+ // Export remark plugin factory for use in markdown configuration
90
+ glossaryPlugin.remarkPlugin = require('./remark/glossary-terms');
91
+
92
+ /**
93
+ * Helper function to get the configured remark plugin
94
+ * This can be used in docusaurus.config.js markdown configuration
95
+ *
96
+ * @param {object} pluginOptions - Plugin options from docusaurus.config.js
97
+ * @param {object} context - Docusaurus context
98
+ * @returns {function} Configured remark plugin
99
+ */
100
+ glossaryPlugin.getRemarkPlugin = function(pluginOptions, context) {
101
+ const {
102
+ glossaryPath = 'glossary/glossary.json',
103
+ routePath = '/glossary',
104
+ siteDir = context.siteDir
105
+ } = pluginOptions;
106
+
107
+ return [
108
+ require('./remark/glossary-terms'),
109
+ {
110
+ glossaryPath,
111
+ routePath,
112
+ siteDir,
113
+ }
114
+ ];
115
+ };
116
+
70
117
  module.exports = glossaryPlugin;
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "docusaurus-plugin-glossary",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A Docusaurus plugin for creating and managing glossary terms with auto-generated pages and inline tooltips",
5
5
  "main": "index.js",
6
6
  "files": [
7
7
  "index.js",
8
8
  "components/",
9
9
  "theme/",
10
+ "remark/",
10
11
  "README.md",
11
12
  "LICENSE"
12
13
  ],
@@ -44,7 +45,8 @@
44
45
  "react-dom": "^18.0.0"
45
46
  },
46
47
  "dependencies": {
47
- "fs-extra": "^11.0.0"
48
+ "fs-extra": "^11.0.0",
49
+ "unist-util-visit": "^5.0.0"
48
50
  },
49
51
  "engines": {
50
52
  "node": ">=16.14"
@@ -0,0 +1,214 @@
1
+ const visit = require('unist-util-visit');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ /**
6
+ * Creates a remark plugin that automatically detects and replaces glossary terms in markdown
7
+ *
8
+ * @param {object} options - Plugin options
9
+ * @param {Array} options.terms - Array of glossary term objects with {term, definition}
10
+ * @param {string} options.glossaryPath - Path to glossary JSON file (optional, if terms not provided)
11
+ * @param {string} options.routePath - Route path to glossary page (default: '/glossary')
12
+ * @param {string} options.siteDir - Docusaurus site directory (required if using glossaryPath)
13
+ * @returns {function} Remark plugin function
14
+ */
15
+ function remarkGlossaryTerms({
16
+ terms = [],
17
+ glossaryPath = null,
18
+ routePath = '/glossary',
19
+ siteDir = null
20
+ } = {}) {
21
+ let glossaryTerms = terms;
22
+
23
+ // If terms not provided, try to load from glossaryPath (synchronously)
24
+ if (!glossaryTerms.length && glossaryPath && siteDir) {
25
+ try {
26
+ const glossaryFilePath = path.resolve(siteDir, glossaryPath);
27
+ if (fs.existsSync(glossaryFilePath)) {
28
+ const glossaryData = JSON.parse(fs.readFileSync(glossaryFilePath, 'utf8'));
29
+ glossaryTerms = glossaryData.terms || [];
30
+ }
31
+ } catch (error) {
32
+ console.warn(`Failed to load glossary from ${glossaryPath}:`, error.message);
33
+ }
34
+ }
35
+
36
+ // Build a map of terms for efficient lookup
37
+ // Key: lowercase term, Value: term object with original case
38
+ const termMap = new Map();
39
+ glossaryTerms.forEach(termObj => {
40
+ if (termObj.term) {
41
+ termMap.set(termObj.term.toLowerCase(), termObj);
42
+ }
43
+ });
44
+
45
+ // Sort terms by length (longest first) to avoid partial matches
46
+ // e.g., "Application Programming Interface" should match before "API"
47
+ const sortedTerms = Array.from(termMap.entries()).sort(
48
+ (a, b) => b[0].length - a[0].length
49
+ );
50
+
51
+ // If no terms, return a no-op transformer
52
+ if (sortedTerms.length === 0) {
53
+ return (tree) => tree;
54
+ }
55
+
56
+ /**
57
+ * Recursively replace glossary terms in text
58
+ * Returns an array of text nodes and MDX components
59
+ */
60
+ function replaceTermsInText(text, position) {
61
+ if (!text || !sortedTerms.length) {
62
+ return [{ type: 'text', value: text }];
63
+ }
64
+
65
+ const result = [];
66
+ let lastIndex = 0;
67
+ let textLower = text.toLowerCase();
68
+
69
+ // Find all matches
70
+ const matches = [];
71
+ for (const [lowerTerm, termObj] of sortedTerms) {
72
+ const term = termObj.term;
73
+ let searchIndex = 0;
74
+
75
+ while (searchIndex < textLower.length) {
76
+ const index = textLower.indexOf(lowerTerm, searchIndex);
77
+ if (index === -1) break;
78
+
79
+ // Check if it's a whole word match
80
+ const beforeChar = index > 0 ? textLower[index - 1] : ' ';
81
+ const afterChar = index + lowerTerm.length < textLower.length
82
+ ? textLower[index + lowerTerm.length]
83
+ : ' ';
84
+
85
+ // Word boundary check (alphanumeric characters)
86
+ const isWordBoundary =
87
+ !/\w/.test(beforeChar) && !/\w/.test(afterChar);
88
+
89
+ if (isWordBoundary) {
90
+ matches.push({
91
+ index,
92
+ length: term.length,
93
+ term: term,
94
+ termObj: termObj,
95
+ // Store original case from the text
96
+ originalText: text.substring(index, index + term.length)
97
+ });
98
+ }
99
+
100
+ searchIndex = index + 1;
101
+ }
102
+ }
103
+
104
+ // Sort matches by index
105
+ matches.sort((a, b) => a.index - b.index);
106
+
107
+ // Remove overlapping matches (keep the first one)
108
+ const nonOverlappingMatches = [];
109
+ let lastMatchEnd = 0;
110
+ for (const match of matches) {
111
+ if (match.index >= lastMatchEnd) {
112
+ nonOverlappingMatches.push(match);
113
+ lastMatchEnd = match.index + match.length;
114
+ }
115
+ }
116
+
117
+ // Build result array
118
+ for (const match of nonOverlappingMatches) {
119
+ // Add text before match
120
+ if (match.index > lastIndex) {
121
+ result.push({
122
+ type: 'text',
123
+ value: text.substring(lastIndex, match.index)
124
+ });
125
+ }
126
+
127
+ // Add MDX component for glossary term
128
+ result.push({
129
+ type: 'mdxJsxFlowElement',
130
+ name: 'GlossaryTerm',
131
+ attributes: [
132
+ {
133
+ type: 'mdxJsxAttribute',
134
+ name: 'term',
135
+ value: match.termObj.term
136
+ },
137
+ {
138
+ type: 'mdxJsxAttribute',
139
+ name: 'definition',
140
+ value: match.termObj.definition || ''
141
+ },
142
+ {
143
+ type: 'mdxJsxAttribute',
144
+ name: 'routePath',
145
+ value: routePath
146
+ }
147
+ ],
148
+ children: [
149
+ {
150
+ type: 'text',
151
+ value: match.originalText
152
+ }
153
+ ]
154
+ });
155
+
156
+ lastIndex = match.index + match.length;
157
+ }
158
+
159
+ // Add remaining text
160
+ if (lastIndex < text.length) {
161
+ result.push({
162
+ type: 'text',
163
+ value: text.substring(lastIndex)
164
+ });
165
+ }
166
+
167
+ return result.length > 0 ? result : [{ type: 'text', value: text }];
168
+ }
169
+
170
+ return (tree) => {
171
+ visit(tree, 'text', (node, index, parent) => {
172
+ // Skip text nodes inside code blocks, links, or existing MDX components
173
+ if (
174
+ parent.type === 'code' ||
175
+ parent.type === 'inlineCode' ||
176
+ parent.type === 'link' ||
177
+ parent.type === 'mdxJsxFlowElement' ||
178
+ parent.type === 'mdxJsxTextElement'
179
+ ) {
180
+ return;
181
+ }
182
+
183
+ // Replace terms in text node
184
+ const replacements = replaceTermsInText(node.value);
185
+
186
+ // If we have replacements, replace the single text node with multiple nodes
187
+ if (replacements.length > 1 ||
188
+ (replacements.length === 1 && replacements[0].type !== 'text')) {
189
+ // Convert to text elements for paragraph context if needed
190
+ const newNodes = replacements.map(replacement => {
191
+ if (replacement.type === 'mdxJsxFlowElement') {
192
+ // In paragraph context, we need mdxJsxTextElement instead
193
+ if (parent.type === 'paragraph') {
194
+ return {
195
+ type: 'mdxJsxTextElement',
196
+ name: replacement.name,
197
+ attributes: replacement.attributes,
198
+ children: replacement.children
199
+ };
200
+ }
201
+ }
202
+ return replacement;
203
+ });
204
+
205
+ // Replace the single node with multiple nodes
206
+ parent.children.splice(index, 1, ...newNodes);
207
+ return index + newNodes.length - 1; // Return new index to continue
208
+ }
209
+ });
210
+ };
211
+ }
212
+
213
+ module.exports = remarkGlossaryTerms;
214
+
@@ -14,9 +14,10 @@ import styles from './styles.module.css';
14
14
  * @param {object} props
15
15
  * @param {string} props.term - The glossary term
16
16
  * @param {string} props.definition - The definition to show in tooltip
17
+ * @param {string} props.routePath - Route path to glossary page (default: '/glossary')
17
18
  * @param {React.ReactNode} props.children - Optional custom display text
18
19
  */
19
- export default function GlossaryTerm({ term, definition, children }) {
20
+ export default function GlossaryTerm({ term, definition, routePath = '/glossary', children }) {
20
21
  const [showTooltip, setShowTooltip] = useState(false);
21
22
 
22
23
  const displayText = children || term;
@@ -25,7 +26,7 @@ export default function GlossaryTerm({ term, definition, children }) {
25
26
  return (
26
27
  <span className={styles.glossaryTermWrapper}>
27
28
  <a
28
- href={`/glossary#${termId}`}
29
+ href={`${routePath}#${termId}`}
29
30
  className={styles.glossaryTerm}
30
31
  onMouseEnter={() => setShowTooltip(true)}
31
32
  onMouseLeave={() => setShowTooltip(false)}