Skip to content

Commit c09db7d

Browse files
committed
Refactor code to support tooltips in en-us and ja-jp
1 parent 9838eda commit c09db7d

File tree

2 files changed

+102
-50
lines changed

2 files changed

+102
-50
lines changed

scripts/generate-glossary-json.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
const fs = require('fs');
22
const path = require('path');
3-
const glossaryFilePath = path.join(__dirname, '../src/pages/glossary.mdx');
4-
const outputJsonPath = path.join(__dirname, '../static/glossary.json');
53

6-
const generateGlossaryJson = () => {
4+
const glossaries = [
5+
{ src: '../docs/glossary.mdx', output: '../build/docs/glossary.json' },
6+
{ src: '../i18n/versioned_docs/ja-jp/docusaurus-plugin-content-docs/current/glossary.mdx', output: '../build/ja-jp/glossary.json' }
7+
];
8+
9+
const generateGlossaryJson = (glossaryFilePath, outputJsonPath) => {
710
const glossaryContent = fs.readFileSync(glossaryFilePath, 'utf-8');
811
const glossaryLines = glossaryContent.split('\n');
912

@@ -12,20 +15,17 @@ const generateGlossaryJson = () => {
1215

1316
glossaryLines.forEach((line) => {
1417
if (line.startsWith('## ')) {
15-
// Extract the term (strip the '## ' prefix)
1618
currentTerm = line.replace('## ', '').trim();
1719
} else if (line.startsWith('# ')) {
18-
// Skip heading 1 (`# `) lines
19-
currentTerm = ''; // Reset the current term for heading 1 lines
20+
currentTerm = ''; // Reset the term for heading 1 lines.
2021
} else if (line.trim() !== '' && currentTerm !== '') {
21-
// Add the definition to the current term (only if a valid term is set)
2222
glossary[currentTerm] = line.trim();
2323
}
2424
});
2525

26-
// Write glossary to glossary.json
2726
fs.writeFileSync(outputJsonPath, JSON.stringify(glossary, null, 2));
28-
console.log('glossary.json generated successfully.');
27+
console.log(`${outputJsonPath} generated successfully.`);
2928
};
3029

31-
generateGlossaryJson();
30+
// Generate both glossaries.
31+
glossaries.forEach(({ src, output }) => generateGlossaryJson(path.join(__dirname, src), path.join(__dirname, output)));

src/components/GlossaryInjector.tsx

Lines changed: 92 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,89 +10,141 @@ const GlossaryInjector: React.FC<GlossaryInjectorProps> = ({ children }) => {
1010
const [glossary, setGlossary] = useState<{ [key: string]: string }>({});
1111

1212
useEffect(() => {
13-
// Fetch glossary.json on component mount.
14-
fetch('/glossary.json')
15-
.then((res) => res.json())
13+
const url = window.location.pathname;
14+
let glossaryPath = '/docs/glossary.json'; // Use the English version as the default glossary.
15+
16+
if (process.env.NODE_ENV === 'production') { // The glossary tooltip works only in production environments.
17+
glossaryPath = url.startsWith('/ja-jp/docs') ? '/ja-jp/glossary.json' : '/docs/glossary.json';
18+
} else {
19+
glossaryPath = url.startsWith('/ja-jp/docs') ? '/ja-jp/glossary.json' : '/docs/glossary.json';
20+
}
21+
22+
fetch(glossaryPath)
23+
.then((res) => {
24+
if (!res.ok) {
25+
throw new Error(`HTTP error! status: ${res.status}`);
26+
}
27+
return res.json();
28+
})
1629
.then(setGlossary)
1730
.catch((err) => console.error('Failed to load glossary:', err));
1831
}, []);
1932

2033
useEffect(() => {
2134
if (Object.keys(glossary).length === 0) return;
2235

23-
const terms = Object.keys(glossary);
36+
// Sort terms in descending order by length to prioritize multi-word terms.
37+
const terms = Object.keys(glossary).sort((a, b) => b.length - a.length);
38+
const processedTerms = new Set<string>(); // Set to track processed terms.
2439

2540
const wrapTermsInTooltips = (node: HTMLElement) => {
2641
const textNodes = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false);
2742
let currentNode: Node | null;
2843

29-
// Store modifications for later processing.
3044
const modifications: { originalNode: Node; newNodes: Node[] }[] = [];
3145

3246
while ((currentNode = textNodes.nextNode())) {
3347
const parentElement = currentNode.parentElement;
3448

35-
// Only wrap terms if the parent is a <p> or <li> element.
36-
if (parentElement && (parentElement.tagName.toLowerCase() === 'p' || parentElement.tagName.toLowerCase() === 'li')) {
37-
const newNodes: Node[] = []; // Array to hold the new nodes.
38-
let lastIndex = 0; // Track the last index for proper splitting.
49+
// Check if the parent element is a tab title.
50+
const isTabTitle = parentElement && parentElement.closest('.tabs__item'); // Adjust the selector as necessary.
3951

40-
terms.forEach((term) => {
41-
const regex = new RegExp(`\\b(${term})\\b`, 'gi'); // Case-insensitive match
42-
let match: RegExpExecArray | null;
52+
// Check if the parent element is a code block.
53+
const isCodeBlock = parentElement && parentElement.closest('.prism-code'); // Adjust the selector as necessary.
4354

44-
// If a match is found
45-
while ((match = regex.exec(currentNode.textContent || ''))) {
46-
// Add text before the match.
47-
if (lastIndex < match.index) {
48-
newNodes.push(document.createTextNode(currentNode.textContent!.slice(lastIndex, match.index)));
49-
}
55+
// Check if the parent element is a Card.
56+
const isCard = parentElement && parentElement.closest('.card__body'); // Adjust the selector as necessary.
5057

51-
// Create and push the GlossaryTooltip for the matched term.
52-
const tooltipWrapper = document.createElement('span');
53-
tooltipWrapper.setAttribute('data-term', term);
54-
tooltipWrapper.className = 'glossary-term'; // Add a class for styling.
58+
// Check if the parent element is a Mermaid diagram.
59+
const isMermaidDiagram = parentElement && parentElement.closest('.docusaurus-mermaid-container'); // Adjust the selector as necessary.
60+
61+
// Only wrap terms in tooltips if the parent is within the target div and not in headings or tab titles.
62+
if (
63+
parentElement &&
64+
parentElement.closest('.theme-doc-markdown.markdown') &&
65+
!/^H[1-6]$/.test(parentElement.tagName) && // Skip headings (H1 to H6).
66+
!isTabTitle && // Skip tab titles.
67+
!isCodeBlock && // Skip code blocks.
68+
!isCard && // Skip Cards.
69+
!isMermaidDiagram // Skip Mermaid diagrams.
70+
) {
71+
let currentText = currentNode.textContent!;
72+
const newNodes: Node[] = [];
73+
let hasReplacements = false;
5574

56-
// Get the definition for the matched term.
57-
const definition = glossary[term]; // Access the definition from the glossary.
75+
// Create a regex pattern to match all terms (case-sensitive).
76+
const regexPattern = terms.map(term => `(${term})`).join('|');
77+
const regex = new RegExp(regexPattern, 'g');
5878

59-
// Render the Tooltip with the definition text.
60-
ReactDOM.render(<GlossaryTooltip term={term} definition={definition}>{match[0]}</GlossaryTooltip>, tooltipWrapper);
79+
let lastIndex = 0;
80+
let match: RegExpExecArray | null;
6181

62-
newNodes.push(tooltipWrapper); // Push tooltip wrapper.
82+
while ((match = regex.exec(currentText))) {
83+
const matchedTerm = match[0];
6384

64-
// Update lastIndex to the end of the match.
65-
lastIndex = match.index + match[0].length;
85+
if (lastIndex < match.index) {
86+
newNodes.push(document.createTextNode(currentText.slice(lastIndex, match.index)));
6687
}
67-
});
6888

69-
// Add any remaining text after the last match.
70-
if (lastIndex < currentNode.textContent!.length) {
71-
newNodes.push(document.createTextNode(currentNode.textContent!.slice(lastIndex)));
89+
const isFirstMention = !processedTerms.has(matchedTerm);
90+
const isLink = parentElement && parentElement.tagName === 'A'; // Check if the parent is a link.
91+
92+
if (isFirstMention && !isLink) {
93+
// Create a tooltip only if it's the first mention and not a link.
94+
const tooltipWrapper = document.createElement('span');
95+
tooltipWrapper.setAttribute('data-term', matchedTerm);
96+
tooltipWrapper.className = 'glossary-term';
97+
98+
const definition = glossary[matchedTerm]; // Exact match from glossary.
99+
100+
ReactDOM.render(
101+
<GlossaryTooltip term={matchedTerm} definition={definition}>
102+
{matchedTerm}
103+
</GlossaryTooltip>,
104+
tooltipWrapper
105+
);
106+
107+
newNodes.push(tooltipWrapper);
108+
processedTerms.add(matchedTerm); // Mark this term as processed.
109+
} else if (isLink) {
110+
// If it's a link, we skip this mention but do not mark it as processed.
111+
newNodes.push(document.createTextNode(matchedTerm));
112+
} else {
113+
// If it's not the first mention, just add the plain text.
114+
newNodes.push(document.createTextNode(matchedTerm));
115+
}
116+
117+
lastIndex = match.index + matchedTerm.length;
118+
hasReplacements = true;
119+
}
120+
121+
if (lastIndex < currentText.length) {
122+
newNodes.push(document.createTextNode(currentText.slice(lastIndex)));
72123
}
73124

74-
// If new nodes are created, we need to store the modification.
75-
if (newNodes.length > 0) {
125+
if (hasReplacements) {
76126
modifications.push({ originalNode: currentNode, newNodes });
77127
}
78128
}
79129
}
80130

81-
// Apply the modifications outside of the tree walker iteration.
131+
// Replace the original nodes with new nodes.
82132
modifications.forEach(({ originalNode, newNodes }) => {
83133
const parentElement = originalNode.parentElement;
84134
if (parentElement) {
85135
newNodes.forEach((newNode) => {
86136
parentElement.insertBefore(newNode, originalNode);
87137
});
88-
parentElement.removeChild(originalNode); // Remove the original text node.
138+
parentElement.removeChild(originalNode);
89139
}
90140
});
91141
};
92142

93-
// Process <p> and <li> elements in the document body.
94-
const paragraphsAndLists = document.querySelectorAll('p, li');
95-
paragraphsAndLists.forEach((node) => wrapTermsInTooltips(node));
143+
// Target the specific div with the class "theme-doc-markdown markdown".
144+
const targetDiv = document.querySelector('.theme-doc-markdown.markdown');
145+
if (targetDiv) {
146+
wrapTermsInTooltips(targetDiv);
147+
}
96148
}, [glossary]);
97149

98150
return <>{children}</>;

0 commit comments

Comments
 (0)