docusaurus-plugin-terminology 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Jürgen Schilke.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,213 @@
1
+ # docusaurus-plugin-terminology
2
+
3
+
4
+ ## Overview
5
+
6
+ Terminology/glossary plugin for [Docusaurus](https://github.com/facebook/docusaurus) 3 with:
7
+
8
+ - glossary generation
9
+ - term tooltips
10
+ - MDX support (Built on MDX AST transformations instead of raw HTML replacement for improved Docusaurus 3 compatibility)
11
+ - i18n support
12
+ - Tippy.js integration
13
+
14
+ This plugin is inspired by [@grnet/docusaurus-terminology](https://github.com/grnet/docusaurus-terminology). It's based on the same general concept but uses MDX remark transformations instead of webpack loader injection and uses a more robust handling of the terms' links.
15
+ Compatible with Docusaurus 3.x and React 18.
16
+ Tested with Docusaurus 3.7.0.
17
+
18
+
19
+ ## Features
20
+
21
+ - Define terms as Markdown files.
22
+ - Glossary component: Automatic glossary data aggregation from Markdown term files in `docs/glossary`.
23
+ - Tooltips on glossary links: Automatically transforms glossary links into interactive tooltip components.
24
+ - Locale-aware glossary support: Supports multiple locales and ensures fallback to the default locale for missing terms.
25
+ - Compatible with Docusaurus 3 + React 18
26
+ - SSR-safe MDX integration
27
+
28
+
29
+ ## Compatibility
30
+
31
+ | Package | Version |
32
+ |---|---|
33
+ | Docusaurus | 3.7.0+ |
34
+ | React | 18+ |
35
+ | Node.js | 18+ recommended |
36
+
37
+ The plugin supports `.md` and `.mdx` files.
38
+
39
+
40
+ ## Installation
41
+
42
+ Install the plugin using npm or yarn:
43
+
44
+ ```bash
45
+ npm install docusaurus-plugin-terminology
46
+ ```
47
+
48
+ or
49
+
50
+ ```bash
51
+ yarn add docusaurus-plugin-terminology
52
+ ```
53
+
54
+
55
+ ## Configuration
56
+
57
+ Add the plugin to `docusaurus.config.js`:
58
+
59
+ ```js
60
+ plugins: [
61
+ [
62
+ 'docusaurus-plugin-terminology',
63
+ {
64
+ // options
65
+ termsDir: 'terms', // default: 'terms'
66
+ routeBasePath: '/docs/terms/', // default: '/docs/terms/'
67
+ hideTermsFromSidebar: true, // when true, plugin will create _category_.yml to hide individual term pages
68
+ }
69
+ ]
70
+ ],
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Usage
76
+
77
+ ### Defining terms
78
+
79
+ Create a `terms` directory within `docs` (the plugin default).
80
+
81
+ Add Markdown files for your terms in that directory.
82
+ Structure example:
83
+
84
+ ```text
85
+ docs/
86
+ ├── terms/ # terms definitions
87
+ │ ├── ai.md
88
+ │ ├── css.md
89
+ │ ├── ftp.md
90
+ │ ├── html.md
91
+ │ └── ...
92
+ └── terms.md # glossary page (id: glossary)
93
+ ```
94
+
95
+
96
+ #### Example Term File
97
+
98
+ Each file should contain metadata in the frontmatter:
99
+
100
+ ```yaml
101
+ ---
102
+ title: AI - »Artificial Intelligence«
103
+ description: Intelligence of machines.
104
+ ---
105
+ ```
106
+ ```markdown
107
+ The capability of computational systems to perform tasks typically associated with human intelligence, such as learning, reasoning, problem-solving, perception, and decision-making. It is a field of research in engineering, mathematics and computer science that develops and studies methods and software that enable machines to perceive their environment and use learning and intelligence to take actions that maximize their chances of achieving defined goals.
108
+
109
+ *From [Artificial intelligence - Wikipedia](https://en.wikipedia.org/wiki/Artificial_intelligence#Techniques)*
110
+
111
+ ```
112
+
113
+ ### Linking terms
114
+
115
+ Link to terms in your Markdown files using the `/docs/terms/TERM_SLUG` format (or adjust to your `termsDir`).
116
+ Example:
117
+
118
+ ```markdown
119
+ [AI](/docs/terms/ai)
120
+ ```
121
+
122
+ The plugin will automatically transform these links into tooltips.
123
+
124
+
125
+ ### Glossary Page
126
+
127
+ The plugin provides a `Glossary` component that displays all glossary terms grouped alphabetically. You can use this component in your custom pages.
128
+
129
+ #### Create a glossary page
130
+
131
+ E.g. `glossary.md` (the file name doesn't matter)
132
+
133
+ ```markdown
134
+ ---
135
+ id: glossary
136
+ title: Glossary
137
+ ---
138
+
139
+ import Glossary from '@theme/Glossary';
140
+
141
+ <Glossary />
142
+ ```
143
+
144
+ ### i18n
145
+
146
+ Localized term definitions can be placed in:
147
+
148
+ ```text
149
+ i18n/de/docusaurus-plugin-content-docs/current/terms/
150
+ ```
151
+
152
+
153
+ ## How it works
154
+
155
+ 1. The plugin scans `docs/terms` (or the directory configured via `termsDir`)
156
+ 2. Term metadata is extracted from frontmatter
157
+ 3. Markdown links matching `/docs/terms/*` (or matching your `routeBasePath`) are transformed into MDX tooltip components
158
+ 4. Generates a static glossary JSON data source for tooltip rendering
159
+ 5. Glossary data is exposed globally to Docusaurus theme components
160
+
161
+
162
+ ### Internal Architecture
163
+ Markdown links
164
+ ↓
165
+ remark plugin transformation
166
+ ↓
167
+ <TermTooltip />
168
+ ↓
169
+ MDX component mapping
170
+ ↓
171
+ Tippy.js tooltip rendering
172
+
173
+
174
+ ### Styling
175
+
176
+ Override tooltip styles via CSS.
177
+
178
+
179
+ ## File Structure
180
+
181
+ The plugin's file structure is as follows:
182
+
183
+ ```
184
+ docusaurus-plugin-terminology
185
+ ├── remark
186
+ │ └── term-link-transformer.js
187
+ ├── theme
188
+ │ ├── Glossary.jsx
189
+ │ └── tooltip.js
190
+ ├── index.js
191
+ ├── LICENSE
192
+ └── README.md
193
+ ```
194
+
195
+ - `remark/term-link-transformer.js`: Transforms glossary term links in Markdown files into `TermTooltip` components.
196
+ - `theme/Glossary.jsx`: Renders the glossary page.
197
+ - `theme/tooltip.js`: Handles tooltips for glossary terms.
198
+ - `index.js`: Main plugin entry point.
199
+
200
+
201
+ ## Contributing
202
+
203
+ Contributions are welcome! Feel free to open issues or submit pull requests to improve the plugin.
204
+
205
+
206
+ ## License
207
+
208
+ This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details.
209
+
210
+
211
+ ## Acknowledgments
212
+
213
+ This plugin is inspired by [@grnet/docusaurus-terminology](https://github.com/grnet/docusaurus-terminology) and aims to provide a more robust and modern implementation.
package/index.js ADDED
@@ -0,0 +1,181 @@
1
+ // docusaurus-plugin-terminology/index.js
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const matter = require('gray-matter');
5
+
6
+ module.exports = function (context, options = {}) {
7
+
8
+ // Provide sensible defaults and read hide flag
9
+ options = Object.assign(
10
+ {
11
+ termsDir: 'terms',
12
+ routeBasePath: '/docs/terms/',
13
+ hideTermsFromSidebar: false,
14
+ },
15
+ options || {}
16
+ );
17
+
18
+ const termsDir = options.termsDir || 'terms';
19
+ const routeBasePath = options.routeBasePath || '/docs/terms/';
20
+ const hideTermsFromSidebar = options.hideTermsFromSidebar === true;
21
+
22
+ //console.log('[plugin] docusaurus-plugin-terminology loaded');
23
+ return {
24
+ name: 'docusaurus-plugin-terminology',
25
+
26
+ extendMarkdownOptions(mdOptions) {
27
+ mdOptions.remarkPlugins = mdOptions.remarkPlugins || [];
28
+ mdOptions.remarkPlugins.push([
29
+ require('./remark/term-link-transformer'),
30
+ {
31
+ termsDir,
32
+ routeBasePath,
33
+ },
34
+ ]);
35
+ //console.log('[plugin] extendMarkdownOptions: registered termLinkTransformer');
36
+ },
37
+
38
+ /**
39
+ * Optionally write a _category_.yml into the terms docs directory (and per-locale equivalents)
40
+ * so that the terms directory is represented by a single category entry that can be hidden
41
+ * (className: hidden). This prevents the individual generated term files from showing up
42
+ * in the main docs sidebar.
43
+ */
44
+ async loadContent() {
45
+ if (!hideTermsFromSidebar) return null;
46
+
47
+ const CATEGORY_FILENAME = '_category_.yml';
48
+ const makeCategoryYaml = (label = 'Glossar') => `label: ${label}
49
+ className: hidden
50
+ collapsed: true
51
+ link:
52
+ type: doc
53
+ id: glossary
54
+ `;
55
+
56
+ const writeIfMissing = (docsDir) => {
57
+ try {
58
+ if (!fs.existsSync(docsDir)) return;
59
+ const catPath = path.join(docsDir, CATEGORY_FILENAME);
60
+ if (fs.existsSync(catPath)) {
61
+ // Respect an existing file; do not overwrite
62
+ return;
63
+ }
64
+ fs.writeFileSync(catPath, makeCategoryYaml(), { encoding: 'utf8' });
65
+ console.info(`[docusaurus-plugin-terminology] wrote ${CATEGORY_FILENAME} to ${docsDir}`);
66
+ } catch (err) {
67
+ // Non-fatal: warn and continue
68
+ console.warn(`[docusaurus-plugin-terminology] failed to write ${CATEGORY_FILENAME} to ${docsDir}: ${err.message}`);
69
+ }
70
+ };
71
+
72
+ // default locale docs dir
73
+ const defaultDocsDir = path.resolve(context.siteDir, `docs/${termsDir}`);
74
+ writeIfMissing(defaultDocsDir);
75
+
76
+ // localized docs dirs (if i18n present)
77
+ const { i18n } = context || {};
78
+ if (i18n && Array.isArray(i18n.locales)) {
79
+ for (const locale of i18n.locales) {
80
+ if (locale === i18n.defaultLocale) continue;
81
+ const localizedDocsDir = path.resolve(
82
+ context.siteDir,
83
+ `i18n/${locale}/docusaurus-plugin-content-docs/current/${termsDir}`
84
+ );
85
+ writeIfMissing(localizedDocsDir);
86
+ }
87
+ }
88
+
89
+ return null;
90
+ },
91
+
92
+ /**
93
+ * Called when content has been loaded.
94
+ * Processes glossary terms data for different locales and generates a glossary JSON file.
95
+ * @param {Object} context - The context object containing siteDir, i18n, and actions.
96
+ */
97
+ async contentLoaded({ actions }) {
98
+ const { siteDir, i18n } = context;
99
+ const { defaultLocale, locales } = i18n;
100
+ const { setGlobalData, createData } = actions;
101
+
102
+ const termsData = {};
103
+
104
+ for (const locale of locales) {
105
+ const docsDir = locale === defaultLocale
106
+ ? path.resolve(siteDir, `docs/${termsDir}`)
107
+ : path.resolve(
108
+ siteDir,
109
+ `i18n/${locale}/docusaurus-plugin-content-docs/current/${termsDir}`
110
+ );
111
+
112
+ if (!fs.existsSync(docsDir)) continue;
113
+
114
+ const termFiles = fs.readdirSync(docsDir).filter((f) => (f.endsWith('.md') || f.endsWith('.mdx')) && f !== 'index.md');
115
+
116
+ const termsArray = [];
117
+
118
+ for (const filename of termFiles) {
119
+ const slug = filename.replace(/\.mdx?$/, '');
120
+ const filepath = path.join(docsDir, filename);
121
+ const fileContent = fs.readFileSync(filepath, 'utf-8');
122
+ const { data, content } = matter(fileContent);
123
+
124
+ termsArray.push({
125
+ slug,
126
+ name: data.title || slug,
127
+ description: data.description || content.trim().split('\n')[0],
128
+ });
129
+ }
130
+
131
+ // Sort termsArray alphabetically by name
132
+ termsArray.sort((a, b) => a.name.localeCompare(b.name));
133
+
134
+ // Convert sorted array back to object
135
+ termsData[locale] = termsArray.reduce((acc, term) => {
136
+ acc[term.slug] = {
137
+ name: term.name,
138
+ description: term.description,
139
+ };
140
+ return acc;
141
+ }, {});
142
+ }
143
+
144
+ // Fallback: fill in missing terms for other locales using default locale
145
+ for (const locale of Object.keys(termsData)) {
146
+ if (locale === defaultLocale) continue;
147
+ for (const [slug, entry] of Object.entries(termsData[defaultLocale] || {})) {
148
+ if (!termsData[locale]?.[slug]) {
149
+ if (!termsData[locale]) termsData[locale] = {};
150
+ termsData[locale][slug] = entry;
151
+ }
152
+ }
153
+ }
154
+
155
+ //console.log('[plugin] termsData:', termsData);
156
+ //console.log('Current locale:', i18n.currentLocale);
157
+
158
+ // Write static glossary JSON file (default locale only)
159
+ const outputJson = JSON.stringify(termsData[defaultLocale] || {}, null, 2);
160
+ const glossaryDataPath = await createData('glossary.json', outputJson);
161
+ //console.log('[plugin] glossary.json generated at:', glossaryDataPath);
162
+ //console.log('[plugin] setGlobalData terms:', termsData);
163
+
164
+ // Expose for use in components
165
+ setGlobalData({
166
+ terms: termsData,
167
+ glossaryDataPath,
168
+ routeBasePath,
169
+ termsDir,
170
+ });
171
+ },
172
+
173
+ getThemePath() {
174
+ return path.resolve(__dirname, './theme');
175
+ },
176
+
177
+ getClientModules() {
178
+ return [path.resolve(__dirname, './theme/tooltip.js')];
179
+ },
180
+ };
181
+ };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "docusaurus-plugin-terminology",
3
+ "version": "0.1.0",
4
+ "description": "Terminology and glossary plugin for Docusaurus 3",
5
+ "main": "index.js",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "docusaurus",
9
+ "docusaurus-plugin",
10
+ "glossary",
11
+ "terminology",
12
+ "tooltip",
13
+ "mdx"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/schilke/docusaurus-plugin-terminology.git"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/schilke/docusaurus-plugin-terminology/issues"
21
+ },
22
+ "homepage": "https://github.com/schilke/docusaurus-plugin-terminology#readme",
23
+ "peerDependencies": {
24
+ "@docusaurus/core": "^3.0.0",
25
+ "@docusaurus/preset-classic": "^3.0.0",
26
+ "react": "^18.0.0 || ^19.0.0",
27
+ "react-dom": "^18.0.0 || ^19.0.0"
28
+ },
29
+ "dependencies": {
30
+ "@tippyjs/react": "^4.2.6",
31
+ "gray-matter": "^4.0.3",
32
+ "unist-util-visit": "^5.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "jest": "^29.0.0",
36
+ "memfs": "^3.4.13"
37
+ },
38
+ "scripts": {
39
+ "test": "jest --color"
40
+ }
41
+ ,
42
+ "publishConfig": {
43
+ "access": "public"
44
+ }
45
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Transforms links to terms in Docusaurus MDX files
3
+ * to use a custom TermTooltip component.
4
+ * /docusaurus-plugin-terminology/remark/term-link-transformer.js
5
+ */
6
+
7
+ const { visit } = require('unist-util-visit');
8
+
9
+ /**
10
+ * Transforms links to glossary terms in the tree to MDX JSX elements.
11
+ * @returns {Function} A function that transforms links in the tree to MDX JSX elements.
12
+ */
13
+ function termLinkTransformer(options = {}) {
14
+
15
+ const routeBasePath =
16
+ options.routeBasePath || '/docs/terms/';
17
+ return (tree, file) => {
18
+ visit(tree, 'link', (node, index, parent) => {
19
+ const href = node.url || '';
20
+
21
+ // Match links like [TERM](/docs/glossary/TERM)
22
+ const escapedRoute = routeBasePath.replace(
23
+ /[.*+?^${}()|[\]\\]/g,
24
+ '\\$&'
25
+ );
26
+
27
+ const termRegex = new RegExp(
28
+ `^${escapedRoute}[\\w-]+$`
29
+ );
30
+
31
+ if (termRegex.test(href)) {
32
+ const termSlug = href.replace(routeBasePath, '');
33
+ const termName = node.children.map((n) => n.value || '').join('');
34
+
35
+ // Replace with a valid MDX JSX element instead of raw HTML
36
+ const jsxNode = {
37
+ type: 'mdxJsxTextElement',
38
+ name: 'TermTooltip',
39
+ attributes: [
40
+ {
41
+ type: 'mdxJsxAttribute',
42
+ name: 'term',
43
+ value: termSlug,
44
+ },
45
+ {
46
+ type: 'mdxJsxAttribute',
47
+ name: 'href',
48
+ value: href,
49
+ },
50
+ ],
51
+ children: [
52
+ {
53
+ type: 'text',
54
+ value: termName,
55
+ },
56
+ ],
57
+ };
58
+
59
+ parent.children[index] = jsxNode;
60
+ }
61
+ });
62
+ };
63
+ }
64
+ module.exports = termLinkTransformer;
package/src/index.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { Plugin } from '@docusaurus/types';
2
+
3
+ export interface TerminologyPluginOptions {
4
+ termsDir?: string;
5
+ routeBasePath?: string;
6
+ hideTermsFromSidebar?: boolean;
7
+ }
8
+
9
+ declare function pluginTerminology(context: any, options?: TerminologyPluginOptions): Plugin;
10
+
11
+ export default pluginTerminology;
@@ -0,0 +1,74 @@
1
+ // docusaurus-plugin-terminology/theme/Glossary.jsx
2
+ import React from 'react';
3
+ import { usePluginData } from '@docusaurus/useGlobalData';
4
+ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
5
+
6
+ export default function Glossary() {
7
+ const { i18n } = useDocusaurusContext();
8
+ const pluginData = usePluginData('docusaurus-plugin-terminology');
9
+
10
+ const localeTerms =
11
+ pluginData.terms?.[i18n.currentLocale] || {};
12
+
13
+ const routeBasePath =
14
+ pluginData.routeBasePath || '/docs/terms/';
15
+ const termEntries = Object.entries(localeTerms);
16
+
17
+ if (termEntries.length === 0) {
18
+ return <p>No glossary terms found.</p>;
19
+ }
20
+
21
+ // Group terms by their starting letter
22
+ const groupedTerms = termEntries.reduce((acc, [key, { name, description }]) => {
23
+ const firstLetter = name[0].toUpperCase();
24
+ if (!acc[firstLetter]) {
25
+ acc[firstLetter] = [];
26
+ }
27
+ acc[firstLetter].push({ key, name, description });
28
+ return acc;
29
+ }, {});
30
+
31
+ const alphabet = Array.from({ length: 26 }, (_, i) => String.fromCharCode(65 + i));
32
+
33
+ // Filter out letters with no terms
34
+ const filteredAlphabet = alphabet.filter((letter) => groupedTerms[letter]);
35
+
36
+ return (
37
+ <div className="container">
38
+ {/* A-Z Index */}
39
+ <div className="container margin-bottom--lg">
40
+ {filteredAlphabet.map((letter) => (
41
+ <a
42
+ className="button button--sm button--outline button--primary col col--1 margin--xs"
43
+ key={letter}
44
+ href={`#${letter}`}
45
+ >
46
+ {letter}
47
+ </a>
48
+ ))}
49
+ </div>
50
+
51
+ {/* Terms List */}
52
+ <div>
53
+ {filteredAlphabet.map((letter) => (
54
+ <div key={letter} id={letter} className="container margin-vert--xs padding-vert--lg shadow--md">
55
+ <h2>{letter}</h2>
56
+ <ul>
57
+ {groupedTerms[letter].map(({ key, name, description }) => (
58
+ <li
59
+ key={key}
60
+ style={{ listStyleType: 'none' }}
61
+ >
62
+ <strong>
63
+ <a href={`${routeBasePath}${key}`}>{name}</a>:
64
+ </strong>{' '}
65
+ {description}
66
+ </li>
67
+ ))}
68
+ </ul>
69
+ </div>
70
+ ))}
71
+ </div>
72
+ </div>
73
+ );
74
+ }
@@ -0,0 +1,30 @@
1
+ // docusaurus-plugin-terminology/theme/tooltip.js
2
+ import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
3
+ import tippy from 'tippy.js';
4
+ import 'tippy.js/dist/tippy.css';
5
+ //import 'tippy.js/themes/light-border.css';
6
+ import 'tippy.js/themes/material.css';
7
+
8
+ if (ExecutionEnvironment.canUseDOM) {
9
+ document.addEventListener('DOMContentLoaded', () => {
10
+ const locale = document.documentElement.lang || 'en';
11
+ const allTerms = window.Docusaurus?.__pluginGlobalData?.['docusaurus-plugin-terminology'] || {};
12
+ const terms = allTerms[locale] || {};
13
+
14
+ document.querySelectorAll('.term-tooltip').forEach((el) => {
15
+ const slug = el.dataset.term;
16
+ const info = terms[slug];
17
+ if (!info) return;
18
+
19
+ tippy(el, {
20
+ content: info.description,
21
+ allowHTML: true,
22
+ //theme: 'light-border',
23
+ theme: 'material',
24
+ placement: 'top-end',
25
+ delay: [100, 50],
26
+ maxWidth: 300,
27
+ });
28
+ });
29
+ });
30
+ }