docusaurus-plugin-glossary 3.1.0 → 3.2.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.
Files changed (50) hide show
  1. package/dist/chunk-4CUFUKUA.js +109 -0
  2. package/dist/chunk-4CUFUKUA.js.map +1 -0
  3. package/dist/chunk-PEB4Y6RI.js +311 -0
  4. package/dist/chunk-PEB4Y6RI.js.map +1 -0
  5. package/dist/chunk-SNP37IVL.js +212 -0
  6. package/dist/chunk-SNP37IVL.js.map +1 -0
  7. package/dist/client/index.cjs +55 -0
  8. package/dist/client/index.cjs.map +1 -0
  9. package/dist/client/index.js +10 -21
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/components/GlossaryPage.cjs +130 -0
  12. package/dist/components/GlossaryPage.cjs.map +1 -0
  13. package/dist/components/GlossaryPage.js +74 -113
  14. package/dist/components/GlossaryPage.js.map +1 -0
  15. package/dist/index.cjs +659 -0
  16. package/dist/index.cjs.map +1 -0
  17. package/dist/index.d.cts +173 -0
  18. package/dist/index.d.ts +82 -11
  19. package/dist/index.js +23 -173
  20. package/dist/index.js.map +1 -0
  21. package/dist/preset.cjs +710 -0
  22. package/dist/preset.cjs.map +1 -0
  23. package/dist/preset.d.cts +98 -0
  24. package/dist/preset.d.ts +8 -7
  25. package/dist/preset.js +79 -143
  26. package/dist/preset.js.map +1 -0
  27. package/dist/remark/glossary-terms.cjs +345 -0
  28. package/dist/remark/glossary-terms.cjs.map +1 -0
  29. package/dist/remark/glossary-terms.js +9 -440
  30. package/dist/remark/glossary-terms.js.map +1 -0
  31. package/dist/theme/GlossaryTerm/index.cjs +138 -0
  32. package/dist/theme/GlossaryTerm/index.cjs.map +1 -0
  33. package/dist/theme/GlossaryTerm/index.js +56 -90
  34. package/dist/theme/GlossaryTerm/index.js.map +1 -0
  35. package/dist/validation.cjs +238 -0
  36. package/dist/validation.cjs.map +1 -0
  37. package/dist/validation.d.cts +2 -0
  38. package/dist/validation.d.ts +2 -44
  39. package/dist/validation.js +11 -256
  40. package/dist/validation.js.map +1 -0
  41. package/package.json +25 -30
  42. package/dist/components/GlossaryPage.test.js +0 -205
  43. package/dist/index.d.ts.map +0 -1
  44. package/dist/preset.d.ts.map +0 -1
  45. package/dist/remark/glossary-terms.d.ts +0 -28
  46. package/dist/remark/glossary-terms.d.ts.map +0 -1
  47. package/dist/theme/GlossaryTerm/index.test.js +0 -143
  48. package/dist/validation.d.ts.map +0 -1
  49. /package/dist/{components/GlossaryPage.module.css → GlossaryPage.module-M4DEUP4X.module.css} +0 -0
  50. /package/dist/{theme/GlossaryTerm/styles.module.css → styles.module-N7ME3MWS.module.css} +0 -0
@@ -1,440 +1,9 @@
1
- import { visit } from 'unist-util-visit';
2
- import path from 'path';
3
- import fs from 'fs';
4
-
5
- /**
6
- * Simple validation for glossary terms loaded from file
7
- * Returns only valid terms with required fields
8
- *
9
- * @param {unknown} data - The parsed JSON data
10
- * @param {string} filePath - Path to the file (for error messages)
11
- * @returns {{ terms: Array<{term: string, definition: string}>, errors: string[] }}
12
- */
13
- function validateGlossaryTerms(data, _filePath) {
14
- const errors = [];
15
- const validTerms = [];
16
-
17
- if (data === null || data === undefined) {
18
- errors.push(`Glossary data is null or undefined`);
19
- return { terms: [], errors };
20
- }
21
-
22
- if (typeof data !== 'object') {
23
- errors.push(`Glossary data must be an object, got ${typeof data}`);
24
- return { terms: [], errors };
25
- }
26
-
27
- if (!('terms' in data)) {
28
- errors.push(`Glossary data must contain a "terms" array`);
29
- return { terms: [], errors };
30
- }
31
-
32
- if (!Array.isArray(data.terms)) {
33
- errors.push(`Field "terms" must be an array, got ${typeof data.terms}`);
34
- return { terms: [], errors };
35
- }
36
-
37
- data.terms.forEach((term, index) => {
38
- if (term === null || term === undefined || typeof term !== 'object') {
39
- errors.push(`terms[${index}]: Term must be an object`);
40
- return;
41
- }
42
-
43
- if (typeof term.term !== 'string' || term.term.trim() === '') {
44
- errors.push(`terms[${index}]: Missing or invalid "term" field`);
45
- return;
46
- }
47
-
48
- if (typeof term.definition !== 'string') {
49
- errors.push(`terms[${index}]: Missing or invalid "definition" field`);
50
- return;
51
- }
52
-
53
- validTerms.push(term);
54
- });
55
-
56
- return { terms: validTerms, errors };
57
- }
58
-
59
- // Cache for glossary data to avoid repeated synchronous file reads
60
- // Key: absolute file path, Value: { terms, loadedAt }
61
- const glossaryCache = new Map();
62
- const CACHE_TTL = 5000; // 5 seconds TTL to allow for file changes during dev
63
-
64
- /**
65
- * Creates a remark plugin that automatically detects and replaces glossary terms in markdown
66
- *
67
- * This plugin transforms plain text terms into <GlossaryTerm> JSX elements.
68
- * The GlossaryTerm component is globally available via the MDXComponents theme wrapper,
69
- * so no import injection is needed - MDX files can use it without explicit imports.
70
- *
71
- * @param {object} options - Plugin options
72
- * @param {Array} options.terms - Array of glossary term objects with {term, definition}
73
- * @param {string} options.glossaryPath - Path to glossary JSON file (optional, if terms not provided)
74
- * @param {string} options.routePath - Route path to glossary page (default: '/glossary')
75
- * @param {string} options.siteDir - Docusaurus site directory (required if using glossaryPath)
76
- * @returns {function} Remark plugin function
77
- */
78
- export default function remarkGlossaryTerms({
79
- terms = [],
80
- glossaryPath = null,
81
- routePath = '/glossary',
82
- siteDir = null,
83
- } = {}) {
84
- let glossaryTerms = terms;
85
-
86
- // If terms not provided, try to load from glossaryPath with caching
87
- if (!glossaryTerms.length && glossaryPath && siteDir) {
88
- try {
89
- const glossaryFilePath = path.resolve(siteDir, glossaryPath);
90
- const now = Date.now();
91
-
92
- // Check cache first to avoid repeated file reads
93
- const cached = glossaryCache.get(glossaryFilePath);
94
- if (cached && now - cached.loadedAt < CACHE_TTL) {
95
- glossaryTerms = cached.terms;
96
- } else {
97
- // Cache miss or expired - load from file synchronously
98
- // Note: This is synchronous I/O which can block the build process
99
- // Consider passing terms directly to avoid this
100
- if (fs.existsSync(glossaryFilePath)) {
101
- const fileContent = fs.readFileSync(glossaryFilePath, 'utf8');
102
- let glossaryData;
103
- try {
104
- glossaryData = JSON.parse(fileContent);
105
- } catch (parseError) {
106
- console.error(
107
- `[glossary-plugin] Failed to parse glossary JSON at ${glossaryPath}:`,
108
- parseError.message
109
- );
110
- glossaryCache.set(glossaryFilePath, {
111
- terms: [],
112
- loadedAt: now,
113
- });
114
- return tree => tree;
115
- }
116
-
117
- // Validate glossary data
118
- const { terms: validTerms, errors } = validateGlossaryTerms(glossaryData, glossaryPath);
119
-
120
- if (errors.length > 0) {
121
- console.warn(`[glossary-plugin] Glossary validation errors in ${glossaryPath}:`);
122
- errors.forEach(err => console.warn(` - ${err}`));
123
- if (validTerms.length > 0) {
124
- console.warn(`[glossary-plugin] Proceeding with ${validTerms.length} valid term(s).`);
125
- }
126
- }
127
-
128
- glossaryTerms = validTerms;
129
-
130
- // Update cache
131
- glossaryCache.set(glossaryFilePath, {
132
- terms: glossaryTerms,
133
- loadedAt: now,
134
- });
135
-
136
- // Log only once per file (when cache is first populated)
137
- if (!cached && process.env.NODE_ENV !== 'production') {
138
- console.log(
139
- `[glossary-plugin] Loaded ${glossaryTerms.length} terms from ${glossaryPath}`
140
- );
141
- }
142
- } else {
143
- // File doesn't exist - cache empty result to avoid repeated checks
144
- glossaryCache.set(glossaryFilePath, {
145
- terms: [],
146
- loadedAt: now,
147
- });
148
- if (process.env.NODE_ENV !== 'production') {
149
- console.warn(`[glossary-plugin] Glossary file not found: ${glossaryPath}`);
150
- }
151
- }
152
- }
153
- } catch (error) {
154
- console.warn(
155
- `[glossary-plugin] Failed to load glossary from ${glossaryPath}:`,
156
- error.message
157
- );
158
- // Cache the error to avoid repeated attempts
159
- if (glossaryPath && siteDir) {
160
- const glossaryFilePath = path.resolve(siteDir, glossaryPath);
161
- glossaryCache.set(glossaryFilePath, {
162
- terms: [],
163
- loadedAt: Date.now(),
164
- });
165
- }
166
- }
167
- }
168
-
169
- // Build a map of terms for efficient lookup, skipping terms with autoLink: false
170
- // Key: lowercase term, Value: term object with original case
171
- const termMap = new Map();
172
- glossaryTerms.forEach(termObj => {
173
- if (termObj.term && termObj.autoLink !== false) {
174
- termMap.set(termObj.term.toLowerCase(), termObj);
175
- }
176
- });
177
-
178
- // Sort terms by length (longest first) to avoid partial matches
179
- // e.g., "Application Programming Interface" should match before "API"
180
- const sortedTerms = Array.from(termMap.entries()).sort((a, b) => b[0].length - a[0].length);
181
-
182
- // If no terms, return a no-op transformer
183
- if (sortedTerms.length === 0) {
184
- return tree => tree;
185
- }
186
-
187
- /**
188
- * Recursively replace glossary terms in text
189
- * Returns an array of text nodes and MDX components
190
- */
191
- function replaceTermsInText(text) {
192
- if (!text || !sortedTerms.length) {
193
- return [{ type: 'text', value: text }];
194
- }
195
-
196
- const result = [];
197
- let lastIndex = 0;
198
- const textLower = text.toLowerCase();
199
-
200
- // Find all matches
201
- const matches = [];
202
- for (const [lowerTerm, termObj] of sortedTerms) {
203
- const term = termObj.term;
204
- let searchIndex = 0;
205
-
206
- while (searchIndex < textLower.length) {
207
- const index = textLower.indexOf(lowerTerm, searchIndex);
208
- if (index === -1) break;
209
-
210
- // Check if it's a whole word match, with simple plural tolerance ('s' or 'es')
211
- const beforeChar = index > 0 ? textLower[index - 1] : ' ';
212
- const afterIndex = index + lowerTerm.length;
213
- const afterChar = afterIndex < textLower.length ? textLower[afterIndex] : ' ';
214
-
215
- let matchLength = term.length;
216
- let isWordBoundary = !/\w/.test(beforeChar) && !/\w/.test(afterChar);
217
-
218
- // Allow trailing 's' plural (e.g., webhook -> webhooks)
219
- if (!isWordBoundary && afterChar === 's') {
220
- const nextChar = afterIndex + 1 < textLower.length ? textLower[afterIndex + 1] : ' ';
221
- if (!/\w/.test(nextChar)) {
222
- isWordBoundary = true;
223
- matchLength = term.length + 1;
224
- }
225
- }
226
-
227
- // Allow trailing 'es' plural (e.g., API -> APIs, box -> boxes)
228
- if (
229
- !isWordBoundary &&
230
- afterChar === 'e' &&
231
- afterIndex + 1 < textLower.length &&
232
- textLower[afterIndex + 1] === 's'
233
- ) {
234
- const nextChar = afterIndex + 2 < textLower.length ? textLower[afterIndex + 2] : ' ';
235
- if (!/\w/.test(nextChar)) {
236
- isWordBoundary = true;
237
- matchLength = term.length + 2;
238
- }
239
- }
240
-
241
- if (isWordBoundary) {
242
- matches.push({
243
- index,
244
- length: matchLength,
245
- term: term,
246
- termObj: termObj,
247
- // Store original case from the text
248
- originalText: text.substring(index, index + matchLength),
249
- });
250
- }
251
-
252
- searchIndex = index + 1;
253
- }
254
- }
255
-
256
- // Sort matches by index
257
- matches.sort((a, b) => a.index - b.index);
258
-
259
- // Remove overlapping matches (keep the first one)
260
- const nonOverlappingMatches = [];
261
- let lastMatchEnd = 0;
262
- for (const match of matches) {
263
- if (match.index >= lastMatchEnd) {
264
- nonOverlappingMatches.push(match);
265
- lastMatchEnd = match.index + match.length;
266
- }
267
- }
268
-
269
- // Build result array
270
- for (const match of nonOverlappingMatches) {
271
- // Add text before match
272
- if (match.index > lastIndex) {
273
- result.push({
274
- type: 'text',
275
- value: text.substring(lastIndex, match.index),
276
- });
277
- }
278
-
279
- // Add MDX component for glossary term
280
- result.push({
281
- type: 'mdxJsxFlowElement',
282
- name: 'GlossaryTerm',
283
- attributes: [
284
- {
285
- type: 'mdxJsxAttribute',
286
- name: 'term',
287
- value: match.termObj.term,
288
- },
289
- {
290
- type: 'mdxJsxAttribute',
291
- name: 'definition',
292
- value: match.termObj.definition || '',
293
- },
294
- {
295
- type: 'mdxJsxAttribute',
296
- name: 'routePath',
297
- value: routePath,
298
- },
299
- ],
300
- children: [
301
- {
302
- type: 'text',
303
- value: match.originalText,
304
- },
305
- ],
306
- });
307
-
308
- lastIndex = match.index + match.length;
309
- }
310
-
311
- // Add remaining text
312
- if (lastIndex < text.length) {
313
- result.push({
314
- type: 'text',
315
- value: text.substring(lastIndex),
316
- });
317
- }
318
-
319
- return result.length > 0 ? result : [{ type: 'text', value: text }];
320
- }
321
-
322
- // Return the transformer function
323
- const transformer = tree => {
324
- let usedGlossaryTerm = false;
325
- visit(tree, 'text', (node, index, parent) => {
326
- // Skip text nodes inside code blocks, links, or existing MDX components
327
- if (
328
- parent.type === 'code' ||
329
- parent.type === 'inlineCode' ||
330
- parent.type === 'link' ||
331
- parent.type === 'mdxJsxFlowElement' ||
332
- parent.type === 'mdxJsxTextElement'
333
- ) {
334
- return;
335
- }
336
-
337
- // Replace terms in text node
338
- const replacements = replaceTermsInText(node.value);
339
-
340
- // If we have replacements, replace the single text node with multiple nodes
341
- if (
342
- replacements.length > 1 ||
343
- (replacements.length === 1 && replacements[0].type !== 'text')
344
- ) {
345
- // Convert to text elements for paragraph context if needed
346
- const newNodes = replacements.map(replacement => {
347
- if (replacement.type === 'mdxJsxFlowElement') {
348
- // In paragraph context, we need mdxJsxTextElement instead
349
- if (parent.type === 'paragraph') {
350
- usedGlossaryTerm = true;
351
- return {
352
- type: 'mdxJsxTextElement',
353
- name: replacement.name,
354
- attributes: replacement.attributes,
355
- children: replacement.children,
356
- };
357
- }
358
- usedGlossaryTerm = true;
359
- }
360
- return replacement;
361
- });
362
-
363
- // Replace the single node with multiple nodes
364
- parent.children.splice(index, 1, ...newNodes);
365
- return index + newNodes.length - 1; // Return new index to continue
366
- }
367
- });
368
-
369
- // Inject MDX import for GlossaryTerm if we used it
370
- // The component is available via theme path, so we just need to import it
371
- if (usedGlossaryTerm) {
372
- const importNode = {
373
- type: 'mdxjsEsm',
374
- value: 'import GlossaryTerm from "@theme/GlossaryTerm";',
375
- data: {
376
- estree: {
377
- type: 'Program',
378
- sourceType: 'module',
379
- body: [
380
- {
381
- type: 'ImportDeclaration',
382
- specifiers: [
383
- {
384
- type: 'ImportDefaultSpecifier',
385
- local: { type: 'Identifier', name: 'GlossaryTerm' },
386
- },
387
- ],
388
- source: {
389
- type: 'Literal',
390
- value: '@theme/GlossaryTerm',
391
- raw: '"@theme/GlossaryTerm"',
392
- },
393
- },
394
- ],
395
- },
396
- },
397
- };
398
-
399
- // Check for existing import
400
- const hasImport =
401
- Array.isArray(tree.children) &&
402
- tree.children.some(
403
- n =>
404
- n.type === 'mdxjsEsm' &&
405
- (n.value?.includes('@theme/GlossaryTerm') ||
406
- n.data?.estree?.body?.some(s => s.source?.value === '@theme/GlossaryTerm'))
407
- );
408
-
409
- if (!hasImport) {
410
- if (!Array.isArray(tree.children)) tree.children = [];
411
- let insertIndex = 0;
412
- for (let i = 0; i < tree.children.length; i++) {
413
- const node = tree.children[i];
414
- if (node.type === 'yaml' || node.type === 'toml') {
415
- insertIndex = i + 1;
416
- } else {
417
- break;
418
- }
419
- }
420
- tree.children.splice(insertIndex, 0, importNode);
421
- }
422
- }
423
- };
424
-
425
- return transformer;
426
- }
427
-
428
- /**
429
- * Clears the glossary cache
430
- * Useful for testing or when you want to force a reload of glossary data
431
- *
432
- * @param {string} [filePath] - Optional specific file path to clear. If not provided, clears entire cache.
433
- */
434
- export function clearGlossaryCache(filePath) {
435
- if (filePath) {
436
- glossaryCache.delete(filePath);
437
- } else {
438
- glossaryCache.clear();
439
- }
440
- }
1
+ import {
2
+ clearGlossaryCache,
3
+ remarkGlossaryTerms
4
+ } from "../chunk-PEB4Y6RI.js";
5
+ export {
6
+ clearGlossaryCache,
7
+ remarkGlossaryTerms as default
8
+ };
9
+ //# sourceMappingURL=glossary-terms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/theme/GlossaryTerm/index.js
31
+ var GlossaryTerm_exports = {};
32
+ __export(GlossaryTerm_exports, {
33
+ default: () => GlossaryTerm
34
+ });
35
+ module.exports = __toCommonJS(GlossaryTerm_exports);
36
+ var import_react = __toESM(require("react"), 1);
37
+ var import_useGlobalData = require("@docusaurus/useGlobalData");
38
+ var import_styles = __toESM(require("../../styles.module-N7ME3MWS.module.css"), 1);
39
+ var import_jsx_runtime = require("react/jsx-runtime");
40
+ function GlossaryTerm({ term, definition, routePath = "/glossary", children }) {
41
+ const [showTooltip, setShowTooltip] = (0, import_react.useState)(false);
42
+ const [tooltipStyle, setTooltipStyle] = (0, import_react.useState)(null);
43
+ const [placement, setPlacement] = (0, import_react.useState)("top");
44
+ const wrapperRef = (0, import_react.useRef)(null);
45
+ const tooltipRef = (0, import_react.useRef)(null);
46
+ const updatePosition = (0, import_react.useCallback)(() => {
47
+ if (!wrapperRef.current || !tooltipRef.current) return;
48
+ const wrapperRect = wrapperRef.current.getBoundingClientRect();
49
+ const tooltipRect = tooltipRef.current.getBoundingClientRect();
50
+ const viewportWidth = window.innerWidth;
51
+ const viewportHeight = window.innerHeight;
52
+ const preferredGap = 8;
53
+ const hasSpaceAbove = wrapperRect.top >= tooltipRect.height + preferredGap;
54
+ const hasSpaceBelow = viewportHeight - wrapperRect.bottom >= tooltipRect.height + preferredGap;
55
+ const nextPlacement = hasSpaceAbove || !hasSpaceBelow ? "top" : "bottom";
56
+ let top;
57
+ if (nextPlacement === "top") {
58
+ top = wrapperRect.top - tooltipRect.height - preferredGap;
59
+ } else {
60
+ top = wrapperRect.bottom + preferredGap;
61
+ }
62
+ const horizontalMargin = 8;
63
+ let left = wrapperRect.left + wrapperRect.width / 2 - tooltipRect.width / 2;
64
+ left = Math.max(
65
+ horizontalMargin,
66
+ Math.min(left, viewportWidth - tooltipRect.width - horizontalMargin)
67
+ );
68
+ setPlacement(nextPlacement);
69
+ setTooltipStyle({ top: Math.max(4, top), left });
70
+ }, []);
71
+ (0, import_react.useEffect)(() => {
72
+ if (!showTooltip) return;
73
+ let rafId2;
74
+ const rafId1 = requestAnimationFrame(() => {
75
+ rafId2 = requestAnimationFrame(() => {
76
+ updatePosition();
77
+ });
78
+ });
79
+ const onScroll = () => updatePosition();
80
+ const onResize = () => updatePosition();
81
+ window.addEventListener("scroll", onScroll, true);
82
+ window.addEventListener("resize", onResize);
83
+ return () => {
84
+ cancelAnimationFrame(rafId1);
85
+ if (rafId2) cancelAnimationFrame(rafId2);
86
+ window.removeEventListener("scroll", onScroll, true);
87
+ window.removeEventListener("resize", onResize);
88
+ };
89
+ }, [showTooltip, updatePosition]);
90
+ const pluginData = (0, import_useGlobalData.usePluginData)("docusaurus-plugin-glossary");
91
+ const effectiveDefinition = (0, import_react.useMemo)(() => {
92
+ if (definition && typeof definition === "string" && definition.length > 0) {
93
+ return definition;
94
+ }
95
+ const terms = pluginData && pluginData.terms || [];
96
+ const found = terms.find(
97
+ (t) => typeof t.term === "string" && t.term.toLowerCase() === String(term).toLowerCase()
98
+ );
99
+ return found && found.definition ? found.definition : void 0;
100
+ }, [definition, pluginData, term]);
101
+ const effectiveRoutePath = (0, import_react.useMemo)(() => {
102
+ if (routePath && typeof routePath === "string" && routePath.length > 0) return routePath;
103
+ return pluginData && pluginData.routePath || "/glossary";
104
+ }, [pluginData, routePath]);
105
+ const displayText = children || term;
106
+ const termId = term.toLowerCase().replace(/\s+/g, "-");
107
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { ref: wrapperRef, className: import_styles.default.glossaryTermWrapper, children: [
108
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
109
+ "a",
110
+ {
111
+ href: `${effectiveRoutePath}#${termId}`,
112
+ className: import_styles.default.glossaryTerm,
113
+ onMouseEnter: () => setShowTooltip(true),
114
+ onMouseLeave: () => setShowTooltip(false),
115
+ onFocus: () => setShowTooltip(true),
116
+ onBlur: () => setShowTooltip(false),
117
+ "aria-describedby": `tooltip-${termId}`,
118
+ children: displayText
119
+ }
120
+ ),
121
+ effectiveDefinition && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
122
+ "span",
123
+ {
124
+ ref: tooltipRef,
125
+ id: `tooltip-${termId}`,
126
+ className: `${import_styles.default.tooltip} ${showTooltip ? import_styles.default.tooltipVisible : ""} ${placement === "top" ? import_styles.default.tooltipTop : import_styles.default.tooltipBottom} ${import_styles.default.tooltipFloating}`,
127
+ role: "tooltip",
128
+ style: showTooltip && tooltipStyle ? { top: `${tooltipStyle.top}px`, left: `${tooltipStyle.left}px` } : void 0,
129
+ children: [
130
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: term }),
131
+ " ",
132
+ effectiveDefinition
133
+ ]
134
+ }
135
+ )
136
+ ] });
137
+ }
138
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/theme/GlossaryTerm/index.js"],"sourcesContent":["import React, { useMemo, useState, useRef, useEffect, useCallback } from 'react';\nimport { usePluginData } from '@docusaurus/useGlobalData';\nimport styles from './styles.module.css';\n\n/**\n * GlossaryTerm component - displays an inline term with tooltip\n *\n * Usage:\n * import GlossaryTerm from '@theme/GlossaryTerm';\n *\n * <GlossaryTerm term=\"API\" definition=\"Application Programming Interface\" />\n * or\n * <GlossaryTerm term=\"API\">custom display text</GlossaryTerm>\n *\n * @param {object} props\n * @param {string} props.term - The glossary term\n * @param {string} props.definition - The definition to show in tooltip\n * @param {string} props.routePath - Route path to glossary page (default: '/glossary')\n * @param {React.ReactNode} props.children - Optional custom display text\n */\nexport default function GlossaryTerm({ term, definition, routePath = '/glossary', children }) {\n const [showTooltip, setShowTooltip] = useState(false);\n const [tooltipStyle, setTooltipStyle] = useState(null);\n const [placement, setPlacement] = useState('top'); // 'top' | 'bottom'\n const wrapperRef = useRef(null);\n const tooltipRef = useRef(null);\n\n const updatePosition = useCallback(() => {\n if (!wrapperRef.current || !tooltipRef.current) return;\n const wrapperRect = wrapperRef.current.getBoundingClientRect();\n const tooltipRect = tooltipRef.current.getBoundingClientRect();\n\n const viewportWidth = window.innerWidth;\n const viewportHeight = window.innerHeight;\n\n const preferredGap = 8; // px\n\n // Decide top vs bottom based on available space\n const hasSpaceAbove = wrapperRect.top >= tooltipRect.height + preferredGap;\n const hasSpaceBelow = viewportHeight - wrapperRect.bottom >= tooltipRect.height + preferredGap;\n const nextPlacement = hasSpaceAbove || !hasSpaceBelow ? 'top' : 'bottom';\n\n let top;\n if (nextPlacement === 'top') {\n top = wrapperRect.top - tooltipRect.height - preferredGap;\n } else {\n top = wrapperRect.bottom + preferredGap;\n }\n\n // Center horizontally on the wrapper, then clamp within viewport with margin\n const horizontalMargin = 8;\n let left = wrapperRect.left + wrapperRect.width / 2 - tooltipRect.width / 2;\n left = Math.max(\n horizontalMargin,\n Math.min(left, viewportWidth - tooltipRect.width - horizontalMargin)\n );\n\n setPlacement(nextPlacement);\n setTooltipStyle({ top: Math.max(4, top), left });\n }, []);\n\n useEffect(() => {\n if (!showTooltip) return;\n\n // Use double requestAnimationFrame to ensure DOM is fully rendered and layout is complete\n // This ensures tooltipRef.current is available and has proper dimensions\n let rafId2;\n const rafId1 = requestAnimationFrame(() => {\n rafId2 = requestAnimationFrame(() => {\n updatePosition();\n });\n });\n\n const onScroll = () => updatePosition();\n const onResize = () => updatePosition();\n window.addEventListener('scroll', onScroll, true);\n window.addEventListener('resize', onResize);\n return () => {\n cancelAnimationFrame(rafId1);\n if (rafId2) cancelAnimationFrame(rafId2);\n window.removeEventListener('scroll', onScroll, true);\n window.removeEventListener('resize', onResize);\n };\n }, [showTooltip, updatePosition]);\n\n // Pull definition/route from plugin global data if not provided\n const pluginData = usePluginData('docusaurus-plugin-glossary');\n const effectiveDefinition = useMemo(() => {\n if (definition && typeof definition === 'string' && definition.length > 0) {\n return definition;\n }\n const terms = (pluginData && pluginData.terms) || [];\n const found = terms.find(\n t => typeof t.term === 'string' && t.term.toLowerCase() === String(term).toLowerCase()\n );\n return found && found.definition ? found.definition : undefined;\n }, [definition, pluginData, term]);\n\n const effectiveRoutePath = useMemo(() => {\n if (routePath && typeof routePath === 'string' && routePath.length > 0) return routePath;\n return (pluginData && pluginData.routePath) || '/glossary';\n }, [pluginData, routePath]);\n\n const displayText = children || term;\n const termId = term.toLowerCase().replace(/\\s+/g, '-');\n\n return (\n <span ref={wrapperRef} className={styles.glossaryTermWrapper}>\n <a\n href={`${effectiveRoutePath}#${termId}`}\n className={styles.glossaryTerm}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onBlur={() => setShowTooltip(false)}\n aria-describedby={`tooltip-${termId}`}\n >\n {displayText}\n </a>\n {effectiveDefinition && (\n <span\n ref={tooltipRef}\n id={`tooltip-${termId}`}\n className={\n `${styles.tooltip} ${showTooltip ? styles.tooltipVisible : ''} ` +\n `${placement === 'top' ? styles.tooltipTop : styles.tooltipBottom} ` +\n `${styles.tooltipFloating}`\n }\n role=\"tooltip\"\n style={\n showTooltip && tooltipStyle\n ? { top: `${tooltipStyle.top}px`, left: `${tooltipStyle.left}px` }\n : undefined\n }\n >\n <strong>{term}</strong> {effectiveDefinition}\n </span>\n )}\n </span>\n );\n}\n"],"mappings":"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","names":["styles"]}