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.
- package/CHANGELOG.md +59 -0
- package/README.md +151 -140
- package/index.js +69 -169
- package/package.json +43 -10
- package/plugin.js +342 -0
- package/src/index.d.ts +82 -4
- package/src/options.js +125 -0
- package/theme/Glossary.jsx +52 -46
- package/theme/MDXComponents/A.jsx +46 -0
- package/theme/TermTooltip.jsx +79 -0
- package/theme/styles.module.css +38 -0
- package/theme/terminology.jsx +99 -0
- package/theme/tooltip.css +36 -0
- package/theme-plugin.js +24 -0
- package/remark/term-link-transformer.js +0 -64
- package/theme/tooltip.js +0 -30
package/package.json
CHANGED
|
@@ -1,8 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docusaurus-plugin-terminology",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Terminology and glossary plugin for Docusaurus 3",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Terminology and glossary plugin for Docusaurus 3: term pages, glossary page and SSR-safe link tooltips",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"types": "src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./src/index.d.ts",
|
|
10
|
+
"default": "./index.js"
|
|
11
|
+
},
|
|
12
|
+
"./plugin": {
|
|
13
|
+
"default": "./plugin.js"
|
|
14
|
+
},
|
|
15
|
+
"./theme": {
|
|
16
|
+
"default": "./theme-plugin.js"
|
|
17
|
+
},
|
|
18
|
+
"./options": {
|
|
19
|
+
"default": "./src/options.js"
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"index.js",
|
|
25
|
+
"plugin.js",
|
|
26
|
+
"theme-plugin.js",
|
|
27
|
+
"src/",
|
|
28
|
+
"theme/",
|
|
29
|
+
"README.md",
|
|
30
|
+
"CHANGELOG.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"sideEffects": [
|
|
34
|
+
"**/*.css"
|
|
35
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18.0"
|
|
38
|
+
},
|
|
6
39
|
"license": "MIT",
|
|
7
40
|
"keywords": [
|
|
8
41
|
"docusaurus",
|
|
@@ -10,6 +43,7 @@
|
|
|
10
43
|
"glossary",
|
|
11
44
|
"terminology",
|
|
12
45
|
"tooltip",
|
|
46
|
+
"tippy",
|
|
13
47
|
"mdx"
|
|
14
48
|
],
|
|
15
49
|
"repository": {
|
|
@@ -21,24 +55,23 @@
|
|
|
21
55
|
},
|
|
22
56
|
"homepage": "https://github.com/schilke/docusaurus-plugin-terminology#readme",
|
|
23
57
|
"peerDependencies": {
|
|
24
|
-
"@docusaurus/core": "^3.
|
|
25
|
-
"@docusaurus/
|
|
58
|
+
"@docusaurus/core": "^3.7.0",
|
|
59
|
+
"@docusaurus/plugin-content-docs": "^3.7.0",
|
|
60
|
+
"@docusaurus/preset-classic": "^3.7.0",
|
|
26
61
|
"react": "^18.0.0 || ^19.0.0",
|
|
27
62
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
28
63
|
},
|
|
29
64
|
"dependencies": {
|
|
30
|
-
"@tippyjs/react": "^4.2.6",
|
|
31
65
|
"gray-matter": "^4.0.3",
|
|
32
|
-
"
|
|
66
|
+
"tippy.js": "^6.3.7"
|
|
33
67
|
},
|
|
34
68
|
"devDependencies": {
|
|
35
|
-
"jest": "^29.
|
|
36
|
-
"memfs": "^
|
|
69
|
+
"jest": "^29.7.0",
|
|
70
|
+
"memfs": "^4.17.0"
|
|
37
71
|
},
|
|
38
72
|
"scripts": {
|
|
39
73
|
"test": "jest --color"
|
|
40
|
-
}
|
|
41
|
-
,
|
|
74
|
+
},
|
|
42
75
|
"publishConfig": {
|
|
43
76
|
"access": "public"
|
|
44
77
|
}
|
package/plugin.js
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* docusaurus-plugin-terminology/plugin.js
|
|
5
|
+
*
|
|
6
|
+
* The content plugin: scans term definitions (Markdown/MDX), builds the
|
|
7
|
+
* locale-aware glossary data exposed to the client via `setGlobalData`, and,
|
|
8
|
+
* in legacy mode, hides the terms from the main docs sidebar.
|
|
9
|
+
*
|
|
10
|
+
* The public entry point of this package is the preset (`index.js`), which
|
|
11
|
+
* wires this plugin together with a dedicated docs instance rendering the
|
|
12
|
+
* terms under a top-level route (default `/terms/`).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const matter = require('gray-matter');
|
|
18
|
+
const {normalizeOptions} = require('./src/options');
|
|
19
|
+
|
|
20
|
+
const PLUGIN_NAME = 'docusaurus-plugin-terminology';
|
|
21
|
+
const CONTENT_DOCS_PLUGIN_NAME = 'docusaurus-plugin-content-docs';
|
|
22
|
+
const TERMS_DOCS_INSTANCE_ID = 'terminology';
|
|
23
|
+
const TERM_EXTENSIONS = new Set(['.md', '.mdx']);
|
|
24
|
+
|
|
25
|
+
const CATEGORY_FILENAME = '_category_.yml';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} filename
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
*/
|
|
31
|
+
function isTermFile(filename) {
|
|
32
|
+
const ext = path.extname(filename).toLowerCase();
|
|
33
|
+
if (!TERM_EXTENSIONS.has(ext)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const basename = path.basename(filename, ext);
|
|
37
|
+
// Skip index files and Docusaurus partials/doc-partials.
|
|
38
|
+
if (basename === 'index' || basename.startsWith('_')) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function collapseWhitespace(value) {
|
|
45
|
+
return String(value).replace(/\s+/g, ' ').trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Strip the most common Markdown inline syntax so a description reads well as
|
|
50
|
+
* a tooltip. This is intentionally conservative (no full Markdown parser).
|
|
51
|
+
*/
|
|
52
|
+
function stripMarkdown(value) {
|
|
53
|
+
return String(value)
|
|
54
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
55
|
+
.replace(/`([^`]*)`/g, '$1')
|
|
56
|
+
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
57
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
58
|
+
.replace(/<[^>]+>/g, ' ')
|
|
59
|
+
.replace(/(\*\*|__)(.*?)\1/g, '$2')
|
|
60
|
+
.replace(/(\*|_)(.*?)\1/g, '$2')
|
|
61
|
+
.replace(/~~(.*?)~~/g, '$1');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function truncate(value, maxLength = 320) {
|
|
65
|
+
if (value.length <= maxLength) {
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
return `${value.slice(0, maxLength - 1).replace(/\s+\S*$/, '')}…`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Derive a short, plain-text description for a term.
|
|
73
|
+
*/
|
|
74
|
+
function deriveDescription(data, content) {
|
|
75
|
+
const explicit = data.description ?? data.summary ?? data.excerpt;
|
|
76
|
+
if (typeof explicit === 'string' && explicit.trim() !== '') {
|
|
77
|
+
return truncate(collapseWhitespace(stripMarkdown(explicit)));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const paragraphs = String(content)
|
|
81
|
+
.split(/\n\s*\n/)
|
|
82
|
+
.map((paragraph) => paragraph.trim())
|
|
83
|
+
.filter(Boolean);
|
|
84
|
+
|
|
85
|
+
for (const paragraph of paragraphs) {
|
|
86
|
+
// Skip block-level constructs that don't make good tooltips.
|
|
87
|
+
if (/^(#{1,6}\s|>|```|---|\+\+\+|\||-\s|\*\s|\d+\.\s|:::)/.test(paragraph)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const text = collapseWhitespace(stripMarkdown(paragraph));
|
|
91
|
+
if (text !== '') {
|
|
92
|
+
return truncate(text);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return truncate(collapseWhitespace(stripMarkdown(content)));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Read every term file in a directory.
|
|
101
|
+
*
|
|
102
|
+
* @returns {Record<string, {slug: string, name: string, description: string, path: string}>|null}
|
|
103
|
+
* The terms keyed by slug, or `null` when the directory does not exist.
|
|
104
|
+
*/
|
|
105
|
+
function readTermsFromDir(dir) {
|
|
106
|
+
let entries;
|
|
107
|
+
try {
|
|
108
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
entries = fs.readdirSync(dir);
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const terms = {};
|
|
117
|
+
for (const filename of entries.filter(isTermFile).sort()) {
|
|
118
|
+
const filepath = path.join(dir, filename);
|
|
119
|
+
const slug = path.basename(filename, path.extname(filename));
|
|
120
|
+
|
|
121
|
+
let data = {};
|
|
122
|
+
let content = '';
|
|
123
|
+
try {
|
|
124
|
+
const raw = fs.readFileSync(filepath, 'utf8');
|
|
125
|
+
const parsed = matter(raw);
|
|
126
|
+
data = parsed.data || {};
|
|
127
|
+
content = parsed.content || '';
|
|
128
|
+
} catch (err) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`[${PLUGIN_NAME}] Failed to read term file "${filepath}": ${err.message}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
terms[slug] = {
|
|
135
|
+
slug,
|
|
136
|
+
name:
|
|
137
|
+
typeof data.title === 'string' && data.title.trim() !== ''
|
|
138
|
+
? data.title
|
|
139
|
+
: slug,
|
|
140
|
+
description: deriveDescription(data, content),
|
|
141
|
+
path: filepath,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return terms;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Candidate directories holding term files for a given locale, ordered by
|
|
150
|
+
* preference. Several i18n layouts are supported so that both the new
|
|
151
|
+
* top-level terms docs instance and legacy `docs/<termsDir>` layouts keep
|
|
152
|
+
* working.
|
|
153
|
+
*/
|
|
154
|
+
function getTermDirs({siteDir, termsDir, docsInstance, locale, defaultLocale}) {
|
|
155
|
+
const defaultDir = docsInstance
|
|
156
|
+
? path.join(siteDir, termsDir)
|
|
157
|
+
: path.join(siteDir, 'docs', termsDir);
|
|
158
|
+
|
|
159
|
+
if (locale === defaultLocale) {
|
|
160
|
+
return [defaultDir];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const i18nDir = path.join(siteDir, 'i18n', locale);
|
|
164
|
+
|
|
165
|
+
return docsInstance
|
|
166
|
+
? [
|
|
167
|
+
path.join(i18nDir, termsDir),
|
|
168
|
+
path.join(
|
|
169
|
+
i18nDir,
|
|
170
|
+
`${CONTENT_DOCS_PLUGIN_NAME}-${TERMS_DOCS_INSTANCE_ID}`,
|
|
171
|
+
'current',
|
|
172
|
+
),
|
|
173
|
+
path.join(
|
|
174
|
+
i18nDir,
|
|
175
|
+
CONTENT_DOCS_PLUGIN_NAME,
|
|
176
|
+
TERMS_DOCS_INSTANCE_ID,
|
|
177
|
+
'current',
|
|
178
|
+
),
|
|
179
|
+
]
|
|
180
|
+
: [
|
|
181
|
+
path.join(i18nDir, CONTENT_DOCS_PLUGIN_NAME, 'current', termsDir),
|
|
182
|
+
path.join(i18nDir, termsDir),
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function sortTerms(terms) {
|
|
187
|
+
const sorted = {};
|
|
188
|
+
for (const slug of Object.keys(terms || {}).sort((a, b) =>
|
|
189
|
+
terms[a].name.localeCompare(terms[b].name),
|
|
190
|
+
)) {
|
|
191
|
+
sorted[slug] = terms[slug];
|
|
192
|
+
}
|
|
193
|
+
return sorted;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function makeCategoryYaml({sidebarLabel, glossarySlug}) {
|
|
197
|
+
return `label: ${sidebarLabel}
|
|
198
|
+
className: hidden
|
|
199
|
+
collapsed: true
|
|
200
|
+
link:
|
|
201
|
+
type: doc
|
|
202
|
+
id: ${glossarySlug}
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function writeCategoryFileIfMissing(dir, options) {
|
|
207
|
+
try {
|
|
208
|
+
if (!fs.existsSync(dir)) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const categoryPath = path.join(dir, CATEGORY_FILENAME);
|
|
212
|
+
if (fs.existsSync(categoryPath)) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
fs.writeFileSync(categoryPath, makeCategoryYaml(options), {encoding: 'utf8'});
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.warn(
|
|
218
|
+
`[${PLUGIN_NAME}] failed to write ${CATEGORY_FILENAME} to ${dir}: ${err.message}`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {import('@docusaurus/types').LoadContext} context
|
|
225
|
+
* @param {object} options
|
|
226
|
+
* @returns {import('@docusaurus/types').Plugin}
|
|
227
|
+
*/
|
|
228
|
+
function terminologyPlugin(context, options) {
|
|
229
|
+
const normalized = normalizeOptions(options);
|
|
230
|
+
const {siteDir, i18n} = context;
|
|
231
|
+
const {defaultLocale, locales} = i18n;
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
name: PLUGIN_NAME,
|
|
235
|
+
|
|
236
|
+
getPathsToWatch() {
|
|
237
|
+
const watchPaths = [];
|
|
238
|
+
for (const locale of locales) {
|
|
239
|
+
for (const dir of getTermDirs({
|
|
240
|
+
siteDir,
|
|
241
|
+
termsDir: normalized.termsDir,
|
|
242
|
+
docsInstance: normalized.docsInstance,
|
|
243
|
+
locale,
|
|
244
|
+
defaultLocale,
|
|
245
|
+
})) {
|
|
246
|
+
if (fs.existsSync(dir)) {
|
|
247
|
+
watchPaths.push(path.join(dir, '**/*.{md,mdx}'));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return watchPaths;
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
async loadContent() {
|
|
255
|
+
// Legacy mode: hide the term docs from the main docs sidebar.
|
|
256
|
+
if (!normalized.docsInstance && normalized.hideTermsFromSidebar) {
|
|
257
|
+
for (const locale of locales) {
|
|
258
|
+
for (const dir of getTermDirs({
|
|
259
|
+
siteDir,
|
|
260
|
+
termsDir: normalized.termsDir,
|
|
261
|
+
docsInstance: false,
|
|
262
|
+
locale,
|
|
263
|
+
defaultLocale,
|
|
264
|
+
})) {
|
|
265
|
+
writeCategoryFileIfMissing(dir, normalized);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const termsByLocale = {};
|
|
271
|
+
|
|
272
|
+
for (const locale of locales) {
|
|
273
|
+
let merged = null;
|
|
274
|
+
for (const dir of getTermDirs({
|
|
275
|
+
siteDir,
|
|
276
|
+
termsDir: normalized.termsDir,
|
|
277
|
+
docsInstance: normalized.docsInstance,
|
|
278
|
+
locale,
|
|
279
|
+
defaultLocale,
|
|
280
|
+
})) {
|
|
281
|
+
const found = readTermsFromDir(dir);
|
|
282
|
+
if (found) {
|
|
283
|
+
merged = {...(merged || {}), ...found};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
termsByLocale[locale] = merged;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const defaultTerms = termsByLocale[defaultLocale] || {};
|
|
290
|
+
|
|
291
|
+
// Fall back to the default locale for missing localized terms.
|
|
292
|
+
for (const locale of locales) {
|
|
293
|
+
if (locale === defaultLocale) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
termsByLocale[locale] = {
|
|
297
|
+
...defaultTerms,
|
|
298
|
+
...(termsByLocale[locale] || {}),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
for (const locale of locales) {
|
|
303
|
+
termsByLocale[locale] = sortTerms(termsByLocale[locale]);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {termsByLocale};
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
async contentLoaded({content, actions}) {
|
|
310
|
+
const {setGlobalData, createData} = actions;
|
|
311
|
+
const terms = content?.termsByLocale || {};
|
|
312
|
+
|
|
313
|
+
// Static glossary JSON (handy for custom tooling / debugging).
|
|
314
|
+
const glossaryDataPath = await createData(
|
|
315
|
+
'glossary.json',
|
|
316
|
+
JSON.stringify(terms[defaultLocale] || {}, null, 2),
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
setGlobalData({
|
|
320
|
+
terms,
|
|
321
|
+
glossaryDataPath,
|
|
322
|
+
routeBasePath: normalized.routeBasePath,
|
|
323
|
+
termsDir: normalized.termsDir,
|
|
324
|
+
docsInstance: normalized.docsInstance,
|
|
325
|
+
defaultLocale,
|
|
326
|
+
});
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
terminologyPlugin.validateOptions = ({options}) => ({
|
|
332
|
+
...normalizeOptions(options),
|
|
333
|
+
// Docusaurus only injects a default `id` when options go through its Joi
|
|
334
|
+
// schema; custom validators must provide it themselves (see
|
|
335
|
+
// @docusaurus/core `doValidatePluginOptions`).
|
|
336
|
+
id: (options && options.id) || 'default',
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
module.exports = terminologyPlugin;
|
|
340
|
+
module.exports.default = terminologyPlugin;
|
|
341
|
+
module.exports.validateOptions = terminologyPlugin.validateOptions;
|
|
342
|
+
module.exports.PLUGIN_NAME = PLUGIN_NAME;
|
package/src/index.d.ts
CHANGED
|
@@ -1,11 +1,89 @@
|
|
|
1
|
-
import { Plugin } from '@docusaurus/types';
|
|
1
|
+
import type {LoadContext, Plugin, PluginModule, Preset} from '@docusaurus/types';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Options accepted by both the preset and the plugin.
|
|
5
|
+
*/
|
|
6
|
+
export interface TerminologyOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Directory containing the term definition files, relative to the site root.
|
|
9
|
+
*
|
|
10
|
+
* With `docsInstance: true` (default) this is a top-level directory, e.g.
|
|
11
|
+
* `terms` rendered at `/terms/<slug>`.
|
|
12
|
+
*
|
|
13
|
+
* With `docsInstance: false` (legacy) it is relative to your `docs/` folder,
|
|
14
|
+
* e.g. `terms` meaning `docs/terms`.
|
|
15
|
+
*
|
|
16
|
+
* @default 'terms'
|
|
17
|
+
*/
|
|
4
18
|
termsDir?: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Public URL prefix of the generated term pages. Used both to render the term
|
|
22
|
+
* pages and to detect term links in Markdown.
|
|
23
|
+
*
|
|
24
|
+
* @default '/terms/' (or '/docs/terms/' in legacy mode)
|
|
25
|
+
*/
|
|
5
26
|
routeBasePath?: string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* When `true` (default) a dedicated docs instance renders the term pages
|
|
30
|
+
* under `routeBasePath`. When `false`, term files are expected to live inside
|
|
31
|
+
* the main `docs/` folder (legacy behaviour).
|
|
32
|
+
*
|
|
33
|
+
* @default true
|
|
34
|
+
*/
|
|
35
|
+
docsInstance?: boolean;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Legacy mode only: write a `_category_.yml` file hiding the term docs from
|
|
39
|
+
* the main docs sidebar, linking to the glossary page instead.
|
|
40
|
+
*
|
|
41
|
+
* @default false
|
|
42
|
+
*/
|
|
6
43
|
hideTermsFromSidebar?: boolean;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Legacy mode only: label of the hidden sidebar category.
|
|
47
|
+
*
|
|
48
|
+
* @default 'Glossary'
|
|
49
|
+
*/
|
|
50
|
+
sidebarLabel?: string;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Legacy mode only: slug (doc id) of the glossary page the hidden category
|
|
54
|
+
* should link to.
|
|
55
|
+
*
|
|
56
|
+
* @default 'glossary'
|
|
57
|
+
*/
|
|
58
|
+
glossarySlug?: string;
|
|
7
59
|
}
|
|
8
60
|
|
|
9
|
-
|
|
61
|
+
/**
|
|
62
|
+
* The preset is the package's default export.
|
|
63
|
+
*/
|
|
64
|
+
export declare function terminologyPreset(
|
|
65
|
+
context: LoadContext,
|
|
66
|
+
options?: TerminologyOptions,
|
|
67
|
+
): Preset;
|
|
68
|
+
|
|
69
|
+
export default terminologyPreset;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The underlying content plugin (advanced/legacy usage).
|
|
73
|
+
*/
|
|
74
|
+
export declare const plugin: PluginModule & {
|
|
75
|
+
validateOptions: (args: {
|
|
76
|
+
options: TerminologyOptions;
|
|
77
|
+
validate: unknown;
|
|
78
|
+
}) => TerminologyOptions;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The theme-only plugin exposing `theme/` (advanced usage).
|
|
83
|
+
*/
|
|
84
|
+
export declare const theme: () => Plugin;
|
|
10
85
|
|
|
11
|
-
|
|
86
|
+
/**
|
|
87
|
+
* The id of the docs instance registered by the preset.
|
|
88
|
+
*/
|
|
89
|
+
export declare const TERMS_DOCS_INSTANCE_ID: 'terminology';
|
package/src/options.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared option normalization for `docusaurus-plugin-terminology`.
|
|
5
|
+
*
|
|
6
|
+
* This module is intentionally dependency-free (no Joi) so that it can be used
|
|
7
|
+
* both by the preset and by the plugin, and by unit tests with plain Node.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TERMS_DIR = 'terms';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_ROUTE_BASE_PATH_WITH_DOCS_INSTANCE = '/terms/';
|
|
13
|
+
const DEFAULT_ROUTE_BASE_PATH_LEGACY = '/docs/terms/';
|
|
14
|
+
|
|
15
|
+
function isPlainObject(value) {
|
|
16
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function fail(message) {
|
|
20
|
+
throw new Error(`[docusaurus-plugin-terminology] ${message}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Normalize a public route base path into the canonical form `/<segment>/`.
|
|
25
|
+
*/
|
|
26
|
+
function normalizeRouteBasePath(value, {docsInstance = true} = {}) {
|
|
27
|
+
const fallback = docsInstance
|
|
28
|
+
? DEFAULT_ROUTE_BASE_PATH_WITH_DOCS_INSTANCE
|
|
29
|
+
: DEFAULT_ROUTE_BASE_PATH_LEGACY;
|
|
30
|
+
|
|
31
|
+
if (value === undefined || value === null || value === '') {
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typeof value !== 'string') {
|
|
36
|
+
fail(`"routeBasePath" must be a string, received ${typeof value}.`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let route = value.trim();
|
|
40
|
+
if (route === '') {
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
43
|
+
if (!route.startsWith('/')) {
|
|
44
|
+
route = `/${route}`;
|
|
45
|
+
}
|
|
46
|
+
if (!route.endsWith('/')) {
|
|
47
|
+
route = `${route}/`;
|
|
48
|
+
}
|
|
49
|
+
return route;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Normalize the terms directory into a POSIX-ish path relative to the site root.
|
|
54
|
+
*/
|
|
55
|
+
function normalizeTermsDir(value) {
|
|
56
|
+
if (value === undefined || value === null || value === '') {
|
|
57
|
+
return DEFAULT_TERMS_DIR;
|
|
58
|
+
}
|
|
59
|
+
if (typeof value !== 'string') {
|
|
60
|
+
fail(`"termsDir" must be a string, received ${typeof value}.`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const dir = value.trim().replace(/\\/g, '/').replace(/\/+$/, '');
|
|
64
|
+
if (dir === '') {
|
|
65
|
+
return DEFAULT_TERMS_DIR;
|
|
66
|
+
}
|
|
67
|
+
if (dir.startsWith('/')) {
|
|
68
|
+
fail('"termsDir" must be relative to the site root, e.g. "terms".');
|
|
69
|
+
}
|
|
70
|
+
const segments = dir.split('/');
|
|
71
|
+
if (segments.includes('..')) {
|
|
72
|
+
fail('"termsDir" must not contain ".." segments.');
|
|
73
|
+
}
|
|
74
|
+
return dir;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validate and normalize all plugin/preset options.
|
|
79
|
+
*
|
|
80
|
+
* @param {object} [options]
|
|
81
|
+
* @returns {{
|
|
82
|
+
* termsDir: string,
|
|
83
|
+
* routeBasePath: string,
|
|
84
|
+
* docsInstance: boolean,
|
|
85
|
+
* hideTermsFromSidebar: boolean,
|
|
86
|
+
* sidebarLabel: string,
|
|
87
|
+
* glossarySlug: string,
|
|
88
|
+
* }}
|
|
89
|
+
*/
|
|
90
|
+
function normalizeOptions(options) {
|
|
91
|
+
if (options === undefined || options === null) {
|
|
92
|
+
options = {};
|
|
93
|
+
}
|
|
94
|
+
if (!isPlainObject(options)) {
|
|
95
|
+
fail(`options must be an object, received ${typeof options}.`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const docsInstance = options.docsInstance !== false;
|
|
99
|
+
|
|
100
|
+
const sidebarLabel =
|
|
101
|
+
typeof options.sidebarLabel === 'string' && options.sidebarLabel.trim() !== ''
|
|
102
|
+
? options.sidebarLabel
|
|
103
|
+
: 'Glossary';
|
|
104
|
+
|
|
105
|
+
const glossarySlug =
|
|
106
|
+
typeof options.glossarySlug === 'string' && options.glossarySlug.trim() !== ''
|
|
107
|
+
? options.glossarySlug
|
|
108
|
+
: 'glossary';
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
termsDir: normalizeTermsDir(options.termsDir),
|
|
112
|
+
routeBasePath: normalizeRouteBasePath(options.routeBasePath, {docsInstance}),
|
|
113
|
+
docsInstance,
|
|
114
|
+
hideTermsFromSidebar: options.hideTermsFromSidebar === true,
|
|
115
|
+
sidebarLabel,
|
|
116
|
+
glossarySlug,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
DEFAULT_TERMS_DIR,
|
|
122
|
+
normalizeOptions,
|
|
123
|
+
normalizeRouteBasePath,
|
|
124
|
+
normalizeTermsDir,
|
|
125
|
+
};
|