|
| 1 | +import React, { useEffect, useState } from 'react'; |
| 2 | +import ReactDOM from 'react-dom'; |
| 3 | +import GlossaryTooltip from './GlossaryTooltip'; |
| 4 | + |
| 5 | +interface GlossaryInjectorProps { |
| 6 | + children: React.ReactNode; |
| 7 | +} |
| 8 | + |
| 9 | +const GlossaryInjector: React.FC<GlossaryInjectorProps> = ({ children }) => { |
| 10 | + const [glossary, setGlossary] = useState<{ [key: string]: string }>({}); |
| 11 | + |
| 12 | + useEffect(() => { |
| 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 | + }) |
| 29 | + .then(setGlossary) |
| 30 | + .catch((err) => console.error('Failed to load glossary:', err)); |
| 31 | + }, []); |
| 32 | + |
| 33 | + useEffect(() => { |
| 34 | + if (Object.keys(glossary).length === 0) return; |
| 35 | + |
| 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. |
| 39 | + |
| 40 | + const wrapTermsInTooltips = (node: HTMLElement) => { |
| 41 | + const textNodes = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false); |
| 42 | + let currentNode: Node | null; |
| 43 | + |
| 44 | + const modifications: { originalNode: Node; newNodes: Node[] }[] = []; |
| 45 | + |
| 46 | + while ((currentNode = textNodes.nextNode())) { |
| 47 | + const parentElement = currentNode.parentElement; |
| 48 | + |
| 49 | + // Check if the parent element is a tab title. |
| 50 | + const isTabTitle = parentElement && parentElement.closest('.tabs__item'); // Adjust the selector as necessary. |
| 51 | + |
| 52 | + // Check if the parent element is a code block. |
| 53 | + const isCodeBlock = parentElement && parentElement.closest('.prism-code'); // Adjust the selector as necessary. |
| 54 | + |
| 55 | + // Check if the parent element is a Card. |
| 56 | + const isCard = parentElement && parentElement.closest('.card__body'); // Adjust the selector as necessary. |
| 57 | + |
| 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; |
| 74 | + |
| 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'); |
| 78 | + |
| 79 | + let lastIndex = 0; |
| 80 | + let match: RegExpExecArray | null; |
| 81 | + |
| 82 | + while ((match = regex.exec(currentText))) { |
| 83 | + const matchedTerm = match[0]; |
| 84 | + |
| 85 | + if (lastIndex < match.index) { |
| 86 | + newNodes.push(document.createTextNode(currentText.slice(lastIndex, match.index))); |
| 87 | + } |
| 88 | + |
| 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))); |
| 123 | + } |
| 124 | + |
| 125 | + if (hasReplacements) { |
| 126 | + modifications.push({ originalNode: currentNode, newNodes }); |
| 127 | + } |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + // Replace the original nodes with new nodes. |
| 132 | + modifications.forEach(({ originalNode, newNodes }) => { |
| 133 | + const parentElement = originalNode.parentElement; |
| 134 | + if (parentElement) { |
| 135 | + newNodes.forEach((newNode) => { |
| 136 | + parentElement.insertBefore(newNode, originalNode); |
| 137 | + }); |
| 138 | + parentElement.removeChild(originalNode); |
| 139 | + } |
| 140 | + }); |
| 141 | + }; |
| 142 | + |
| 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 | + } |
| 148 | + }, [glossary]); |
| 149 | + |
| 150 | + return <>{children}</>; |
| 151 | +}; |
| 152 | + |
| 153 | +export default GlossaryInjector; |
0 commit comments