docusaurus-plugin-terminology 0.1.0 → 0.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.
@@ -1,74 +1,80 @@
1
- // docusaurus-plugin-terminology/theme/Glossary.jsx
2
1
  import React from 'react';
3
- import { usePluginData } from '@docusaurus/useGlobalData';
2
+ import Link from '@docusaurus/Link';
3
+ import {usePluginData} from '@docusaurus/useGlobalData';
4
4
  import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
5
+ import styles from './styles.module.css';
6
+ import {normalizeRouteBasePath} from './terminology';
5
7
 
8
+ /**
9
+ * A–Z glossary listing every term with its short description.
10
+ *
11
+ * Usage in any Markdown/MDX file:
12
+ *
13
+ * ```mdx
14
+ * import Glossary from '@theme/Glossary';
15
+ *
16
+ * <Glossary />
17
+ * ```
18
+ */
6
19
  export default function Glossary() {
7
- const { i18n } = useDocusaurusContext();
20
+ const {i18n} = useDocusaurusContext();
8
21
  const pluginData = usePluginData('docusaurus-plugin-terminology');
9
22
 
10
23
  const localeTerms =
11
- pluginData.terms?.[i18n.currentLocale] || {};
24
+ pluginData?.terms?.[i18n.currentLocale] ??
25
+ pluginData?.terms?.[i18n.defaultLocale] ??
26
+ {};
12
27
 
13
- const routeBasePath =
14
- pluginData.routeBasePath || '/docs/terms/';
15
- const termEntries = Object.entries(localeTerms);
28
+ const routeBasePath = normalizeRouteBasePath(
29
+ pluginData?.routeBasePath ?? '/terms/',
30
+ );
31
+
32
+ const termEntries = Object.entries(localeTerms).sort(([, a], [, b]) =>
33
+ a.name.localeCompare(b.name),
34
+ );
16
35
 
17
36
  if (termEntries.length === 0) {
18
37
  return <p>No glossary terms found.</p>;
19
38
  }
20
39
 
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 });
40
+ const groupedTerms = termEntries.reduce((acc, [slug, term]) => {
41
+ const firstLetter = (term.name[0] || '#').toUpperCase();
42
+ (acc[firstLetter] ||= []).push({slug, ...term});
28
43
  return acc;
29
44
  }, {});
30
45
 
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]);
46
+ const alphabet = Object.keys(groupedTerms).sort();
35
47
 
36
48
  return (
37
- <div className="container">
38
- {/* A-Z Index */}
39
- <div className="container margin-bottom--lg">
40
- {filteredAlphabet.map((letter) => (
49
+ <div>
50
+ <nav className={styles.alphabetNav} aria-label="Glossary index">
51
+ {alphabet.map((letter) => (
41
52
  <a
42
- className="button button--sm button--outline button--primary col col--1 margin--xs"
43
53
  key={letter}
44
- href={`#${letter}`}
45
- >
54
+ className="button button--sm button--outline button--primary"
55
+ href={`#${letter}`}>
46
56
  {letter}
47
57
  </a>
48
58
  ))}
49
- </div>
59
+ </nav>
50
60
 
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
- >
61
+ {alphabet.map((letter) => (
62
+ <section key={letter} id={letter} className={styles.glossarySection}>
63
+ <h2>{letter}</h2>
64
+ <dl className={styles.glossaryList}>
65
+ {groupedTerms[letter].map(({slug, name, description}) => (
66
+ <div key={slug} className={styles.glossaryEntry}>
67
+ <dt>
62
68
  <strong>
63
- <a href={`${routeBasePath}${key}`}>{name}</a>:
64
- </strong>{' '}
65
- {description}
66
- </li>
67
- ))}
68
- </ul>
69
- </div>
70
- ))}
71
- </div>
69
+ <Link to={`${routeBasePath}${slug}`}>{name}</Link>
70
+ </strong>
71
+ </dt>
72
+ <dd>{description}</dd>
73
+ </div>
74
+ ))}
75
+ </dl>
76
+ </section>
77
+ ))}
72
78
  </div>
73
79
  );
74
80
  }
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import OriginalA from '@theme-init/MDXComponents/A';
3
+ import {usePluginData} from '@docusaurus/useGlobalData';
4
+ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
5
+ import TermTooltip from '@theme/TermTooltip';
6
+ import {getTermSlug} from '@theme/terminology';
7
+
8
+ /**
9
+ * Override of the MDX `a` component.
10
+ *
11
+ * Plain Markdown links to term pages (e.g. `[AI](/terms/ai)`) are rendered as
12
+ * `<TermTooltip>`; every other link is delegated to the original Docusaurus
13
+ * implementation.
14
+ *
15
+ * This is what makes the plugin zero-config: no remark plugin, no MDX
16
+ * components swizzle, and no client-side DOM scanning. The term link is part of
17
+ * the server-rendered HTML, and the tooltip is attached on mount.
18
+ *
19
+ * @param {object} props
20
+ * @returns {React.ReactNode}
21
+ */
22
+ export default function A(props) {
23
+ const pluginData = usePluginData('docusaurus-plugin-terminology');
24
+ const {siteConfig, i18n} = useDocusaurusContext();
25
+
26
+ if (pluginData) {
27
+ const slug = getTermSlug(props.href, {
28
+ routeBasePath: pluginData.routeBasePath,
29
+ baseUrl: siteConfig.baseUrl,
30
+ locales: i18n.locales,
31
+ defaultLocale: i18n.defaultLocale,
32
+ currentLocale: i18n.currentLocale,
33
+ });
34
+
35
+ const hasEntry =
36
+ slug &&
37
+ (pluginData.terms?.[i18n.currentLocale]?.[slug] ||
38
+ pluginData.terms?.[i18n.defaultLocale]?.[slug]);
39
+
40
+ if (hasEntry) {
41
+ return <TermTooltip slug={slug} {...props} />;
42
+ }
43
+ }
44
+
45
+ return <OriginalA {...props} />;
46
+ }
@@ -0,0 +1,79 @@
1
+ import React, {useEffect, useRef} from 'react';
2
+ import Link from '@docusaurus/Link';
3
+ import {usePluginData} from '@docusaurus/useGlobalData';
4
+ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
5
+ import tippy from 'tippy.js';
6
+ import 'tippy.js/dist/tippy.css';
7
+ import './tooltip.css';
8
+ import styles from './styles.module.css';
9
+
10
+ /**
11
+ * Resolve the glossary entry for a slug, falling back to the default locale.
12
+ */
13
+ function resolveEntry(terms, locale, defaultLocale, slug) {
14
+ if (!slug) {
15
+ return undefined;
16
+ }
17
+ return terms?.[locale]?.[slug] ?? terms?.[defaultLocale]?.[slug];
18
+ }
19
+
20
+ /**
21
+ * A term link that renders a tippy.js tooltip with the term definition.
22
+ *
23
+ * SSR-safe: the `<a>` is part of the server-rendered markup (good for SEO and
24
+ * no-JS), while the tooltip itself is attached on mount — as tooltips are
25
+ * inherently an interactive, client-only enhancement.
26
+ */
27
+ export default function TermTooltip({slug, href, children, className, ...rest}) {
28
+ const {i18n} = useDocusaurusContext();
29
+ const pluginData = usePluginData('docusaurus-plugin-terminology');
30
+
31
+ const entry = resolveEntry(
32
+ pluginData?.terms,
33
+ i18n.currentLocale,
34
+ i18n.defaultLocale,
35
+ slug,
36
+ );
37
+ const content = entry?.description;
38
+
39
+ // Attach to a wrapper element rather than the anchor: `@docusaurus/Link` only
40
+ // forwards refs on React 19, and this must also work on React 18.
41
+ const wrapperRef = useRef(null);
42
+
43
+ useEffect(() => {
44
+ const target = wrapperRef.current;
45
+ if (!target || !content) {
46
+ return undefined;
47
+ }
48
+
49
+ const instance = tippy(target, {
50
+ content,
51
+ allowHTML: false,
52
+ delay: [120, 0],
53
+ duration: [150, 120],
54
+ placement: 'top',
55
+ maxWidth: 360,
56
+ theme: 'terminology',
57
+ // Anchoring to an inline element needs inline positioning.
58
+ inlinePositioning: true,
59
+ // Keep the link usable (and the tooltip visible) while hovering it.
60
+ interactive: false,
61
+ appendTo: () => document.body,
62
+ });
63
+
64
+ return () => {
65
+ instance.destroy();
66
+ };
67
+ }, [content]);
68
+
69
+ return (
70
+ <span ref={wrapperRef} className={styles.termWrapper}>
71
+ <Link
72
+ {...rest}
73
+ to={href}
74
+ className={[styles.termLink, className].filter(Boolean).join(' ')}>
75
+ {children}
76
+ </Link>
77
+ </span>
78
+ );
79
+ }
@@ -0,0 +1,38 @@
1
+ .termWrapper {
2
+ white-space: normal;
3
+ }
4
+
5
+ .termLink {
6
+ text-decoration: underline;
7
+ text-decoration-style: dotted;
8
+ text-underline-offset: 0.2em;
9
+ cursor: help;
10
+ }
11
+
12
+ .alphabetNav {
13
+ display: flex;
14
+ flex-wrap: wrap;
15
+ gap: 0.25rem;
16
+ margin-bottom: 1.5rem;
17
+ }
18
+
19
+ .glossarySection {
20
+ margin-bottom: 1.5rem;
21
+ scroll-margin-top: 4rem;
22
+ }
23
+
24
+ .glossaryList {
25
+ margin: 0;
26
+ }
27
+
28
+ .glossaryEntry {
29
+ margin-bottom: 0.75rem;
30
+ }
31
+
32
+ .glossaryEntry dt {
33
+ font-weight: 600;
34
+ }
35
+
36
+ .glossaryEntry dd {
37
+ margin: 0.15rem 0 0;
38
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * docusaurus-plugin-terminology/theme/terminology.js
3
+ *
4
+ * Client-side helpers shared by the theme components.
5
+ */
6
+
7
+ /**
8
+ * Canonicalize a route base path to the form `/<segment>/`.
9
+ *
10
+ * @param {string} [routeBasePath]
11
+ * @returns {string}
12
+ */
13
+ export function normalizeRouteBasePath(routeBasePath = '/terms/') {
14
+ let route = String(routeBasePath || '').trim();
15
+ if (route === '') {
16
+ return '/';
17
+ }
18
+ if (!route.startsWith('/')) {
19
+ route = `/${route}`;
20
+ }
21
+ if (!route.endsWith('/')) {
22
+ route = `${route}/`;
23
+ }
24
+ return route;
25
+ }
26
+
27
+ /**
28
+ * Extract the term slug from a link href, or `null` when it is not a term link.
29
+ *
30
+ * Handles absolute/relative URLs, the site `baseUrl`, and locale prefixes.
31
+ *
32
+ * @param {string|undefined} href
33
+ * @param {{
34
+ * routeBasePath?: string,
35
+ * baseUrl?: string,
36
+ * locales?: string[],
37
+ * defaultLocale?: string,
38
+ * currentLocale?: string,
39
+ * }} [options]
40
+ * @returns {string|null}
41
+ */
42
+ export function getTermSlug(href, options = {}) {
43
+ if (!href || typeof href !== 'string') {
44
+ return null;
45
+ }
46
+
47
+ let pathname = href;
48
+
49
+ // Strip a protocol/origin if present.
50
+ if (/^[a-z][a-z\d+.-]*:\/\//i.test(pathname)) {
51
+ try {
52
+ pathname = new URL(pathname).pathname;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ // Drop query string and hash.
59
+ pathname = pathname.split('#')[0].split('?')[0];
60
+ if (!pathname) {
61
+ return null;
62
+ }
63
+
64
+ const {baseUrl, routeBasePath, locales = [], defaultLocale} = options;
65
+
66
+ // Strip the site baseUrl (e.g. `/my-site/`).
67
+ if (baseUrl && baseUrl !== '/') {
68
+ const normalizedBase = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
69
+ if (pathname === baseUrl.replace(/\/$/, '')) {
70
+ pathname = '/';
71
+ } else if (pathname.startsWith(normalizedBase)) {
72
+ pathname = `/${pathname.slice(normalizedBase.length)}`;
73
+ }
74
+ }
75
+
76
+ // Strip a locale prefix (e.g. `/de/terms/ai` -> `/terms/ai`).
77
+ const nonDefaultLocales = locales.filter(
78
+ (locale) => locale && locale !== defaultLocale,
79
+ );
80
+ for (const locale of nonDefaultLocales) {
81
+ if (pathname === `/${locale}`) {
82
+ pathname = '/';
83
+ break;
84
+ }
85
+ if (pathname.startsWith(`/${locale}/`)) {
86
+ pathname = pathname.slice(locale.length + 1);
87
+ break;
88
+ }
89
+ }
90
+
91
+ const base = normalizeRouteBasePath(routeBasePath);
92
+ if (!pathname.startsWith(base)) {
93
+ return null;
94
+ }
95
+
96
+ const rest = pathname.slice(base.length);
97
+ const slug = rest.split('/')[0];
98
+ return slug || null;
99
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Tooltip styling for term links.
3
+ * Applied on top of `tippy.js/dist/tippy.css`.
4
+ */
5
+
6
+ .tippy-box[data-theme~='terminology'] {
7
+ background-color: var(--ifm-background-surface-color, #fff);
8
+ color: var(--ifm-font-color-base, #1c1e21);
9
+ border: 1px solid var(--ifm-color-emphasis-300, rgba(0, 0, 0, 0.1));
10
+ border-radius: var(--ifm-global-radius, 0.4rem);
11
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.15);
12
+ font-size: 0.85rem;
13
+ line-height: 1.45;
14
+ text-align: left;
15
+ padding: 0.4rem 0.6rem;
16
+ }
17
+
18
+ .tippy-box[data-theme~='terminology'] .tippy-content {
19
+ padding: 0;
20
+ }
21
+
22
+ .tippy-box[data-theme~='terminology'][data-placement^='top'] > .tippy-arrow::before {
23
+ border-top-color: var(--ifm-color-emphasis-300, rgba(0, 0, 0, 0.1));
24
+ }
25
+
26
+ .tippy-box[data-theme~='terminology'][data-placement^='bottom'] > .tippy-arrow::before {
27
+ border-bottom-color: var(--ifm-color-emphasis-300, rgba(0, 0, 0, 0.1));
28
+ }
29
+
30
+ .tippy-box[data-theme~='terminology'][data-placement^='left'] > .tippy-arrow::before {
31
+ border-left-color: var(--ifm-color-emphasis-300, rgba(0, 0, 0, 0.1));
32
+ }
33
+
34
+ .tippy-box[data-theme~='terminology'][data-placement^='right'] > .tippy-arrow::before {
35
+ border-right-color: var(--ifm-color-emphasis-300, rgba(0, 0, 0, 0.1));
36
+ }
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * docusaurus-plugin-terminology/theme-plugin.js
5
+ *
6
+ * A tiny theme-only plugin whose sole job is to expose the package's `theme/`
7
+ * directory to Docusaurus.
8
+ *
9
+ * It is registered by the preset under `themes`, i.e. with a higher priority
10
+ * than `@docusaurus/theme-classic`. This is what allows the package to override
11
+ * the MDX `a` component (SSR-safe link tooltips) without requiring users to
12
+ * swizzle or register anything themselves.
13
+ */
14
+
15
+ const path = require('path');
16
+
17
+ module.exports = function terminologyTheme() {
18
+ return {
19
+ name: 'docusaurus-plugin-terminology-theme',
20
+ getThemePath() {
21
+ return path.resolve(__dirname, 'theme');
22
+ },
23
+ };
24
+ };
@@ -1,64 +0,0 @@
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/theme/tooltip.js DELETED
@@ -1,30 +0,0 @@
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
- }