-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Create index.tsx Swizzle component to add glossary tooltip function. * Add glossary tooltip function * Add styles for glossary tooltip * Add `scripts/generate-glossary-json.js` to build step * Add `/static/glossary.json` Ignore `/static/glossary.json` since we only need it when building the site in production. * Create GlossaryInjector.tsx * Create GlossaryTooltip.tsx * Create generate-glossary-json.js * Remove unnecessary ignore * Put glossary tooltip styles in ABC order * Refactor code to support tooltips in en-us and ja-jp * Enable i18n * Remove unnecessary code comments * Fix rendering of glossary tooltip On desktop: Increase padding around definitions. On mobile: Add styles to keep the tooltip within view on narrow screens. * Update naming for other tooltip style To avoid confusion with the glossary tooltip, this commit clarifies the naming for the tooltip for the question mark icon in the header that contains version and edition tags.
- Loading branch information
Showing
6 changed files
with
307 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
|
||
const glossaries = [ | ||
{ src: '../docs/glossary.mdx', output: '../build/docs/glossary.json' }, | ||
{ src: '../i18n/versioned_docs/ja-jp/docusaurus-plugin-content-docs/current/glossary.mdx', output: '../build/ja-jp/glossary.json' } | ||
]; | ||
|
||
const generateGlossaryJson = (glossaryFilePath, outputJsonPath) => { | ||
const glossaryContent = fs.readFileSync(glossaryFilePath, 'utf-8'); | ||
const glossaryLines = glossaryContent.split('\n'); | ||
|
||
let glossary = {}; | ||
let currentTerm = ''; | ||
|
||
glossaryLines.forEach((line) => { | ||
if (line.startsWith('## ')) { | ||
currentTerm = line.replace('## ', '').trim(); | ||
} else if (line.startsWith('# ')) { | ||
currentTerm = ''; // Reset the term for heading 1 lines. | ||
} else if (line.trim() !== '' && currentTerm !== '') { | ||
glossary[currentTerm] = line.trim(); | ||
} | ||
}); | ||
|
||
fs.writeFileSync(outputJsonPath, JSON.stringify(glossary, null, 2)); | ||
console.log(`${outputJsonPath} generated successfully.`); | ||
}; | ||
|
||
// Generate both glossaries. | ||
glossaries.forEach(({ src, output }) => generateGlossaryJson(path.join(__dirname, src), path.join(__dirname, output))); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import React, { useEffect, useState } from 'react'; | ||
import ReactDOM from 'react-dom'; | ||
import GlossaryTooltip from './GlossaryTooltip'; | ||
|
||
interface GlossaryInjectorProps { | ||
children: React.ReactNode; | ||
} | ||
|
||
const GlossaryInjector: React.FC<GlossaryInjectorProps> = ({ children }) => { | ||
const [glossary, setGlossary] = useState<{ [key: string]: string }>({}); | ||
|
||
useEffect(() => { | ||
const url = window.location.pathname; | ||
let glossaryPath = '/docs/glossary.json'; // Use the English version as the default glossary. | ||
|
||
if (process.env.NODE_ENV === 'production') { // The glossary tooltip works only in production environments. | ||
glossaryPath = url.startsWith('/ja-jp/docs') ? '/ja-jp/glossary.json' : '/docs/glossary.json'; | ||
} else { | ||
glossaryPath = url.startsWith('/ja-jp/docs') ? '/ja-jp/glossary.json' : '/docs/glossary.json'; | ||
} | ||
|
||
fetch(glossaryPath) | ||
.then((res) => { | ||
if (!res.ok) { | ||
throw new Error(`HTTP error! status: ${res.status}`); | ||
} | ||
return res.json(); | ||
}) | ||
.then(setGlossary) | ||
.catch((err) => console.error('Failed to load glossary:', err)); | ||
}, []); | ||
|
||
useEffect(() => { | ||
if (Object.keys(glossary).length === 0) return; | ||
|
||
// Sort terms in descending order by length to prioritize multi-word terms. | ||
const terms = Object.keys(glossary).sort((a, b) => b.length - a.length); | ||
const processedTerms = new Set<string>(); // Set to track processed terms. | ||
|
||
const wrapTermsInTooltips = (node: HTMLElement) => { | ||
const textNodes = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false); | ||
let currentNode: Node | null; | ||
|
||
const modifications: { originalNode: Node; newNodes: Node[] }[] = []; | ||
|
||
while ((currentNode = textNodes.nextNode())) { | ||
const parentElement = currentNode.parentElement; | ||
|
||
// Check if the parent element is a tab title. | ||
const isTabTitle = parentElement && parentElement.closest('.tabs__item'); // Adjust the selector as necessary. | ||
|
||
// Check if the parent element is a code block. | ||
const isCodeBlock = parentElement && parentElement.closest('.prism-code'); // Adjust the selector as necessary. | ||
|
||
// Check if the parent element is a Card. | ||
const isCard = parentElement && parentElement.closest('.card__body'); // Adjust the selector as necessary. | ||
|
||
// Check if the parent element is a Mermaid diagram. | ||
const isMermaidDiagram = parentElement && parentElement.closest('.docusaurus-mermaid-container'); // Adjust the selector as necessary. | ||
|
||
// Only wrap terms in tooltips if the parent is within the target div and not in headings or tab titles. | ||
if ( | ||
parentElement && | ||
parentElement.closest('.theme-doc-markdown.markdown') && | ||
!/^H[1-6]$/.test(parentElement.tagName) && // Skip headings (H1 to H6). | ||
!isTabTitle && // Skip tab titles. | ||
!isCodeBlock && // Skip code blocks. | ||
!isCard && // Skip Cards. | ||
!isMermaidDiagram // Skip Mermaid diagrams. | ||
) { | ||
let currentText = currentNode.textContent!; | ||
const newNodes: Node[] = []; | ||
let hasReplacements = false; | ||
|
||
// Create a regex pattern to match all terms (case-sensitive). | ||
const regexPattern = terms.map(term => `(${term})`).join('|'); | ||
const regex = new RegExp(regexPattern, 'g'); | ||
|
||
let lastIndex = 0; | ||
let match: RegExpExecArray | null; | ||
|
||
while ((match = regex.exec(currentText))) { | ||
const matchedTerm = match[0]; | ||
|
||
if (lastIndex < match.index) { | ||
newNodes.push(document.createTextNode(currentText.slice(lastIndex, match.index))); | ||
} | ||
|
||
const isFirstMention = !processedTerms.has(matchedTerm); | ||
const isLink = parentElement && parentElement.tagName === 'A'; // Check if the parent is a link. | ||
|
||
if (isFirstMention && !isLink) { | ||
// Create a tooltip only if it's the first mention and not a link. | ||
const tooltipWrapper = document.createElement('span'); | ||
tooltipWrapper.setAttribute('data-term', matchedTerm); | ||
tooltipWrapper.className = 'glossary-term'; | ||
|
||
const definition = glossary[matchedTerm]; // Exact match from glossary. | ||
|
||
ReactDOM.render( | ||
<GlossaryTooltip term={matchedTerm} definition={definition}> | ||
{matchedTerm} | ||
</GlossaryTooltip>, | ||
tooltipWrapper | ||
); | ||
|
||
newNodes.push(tooltipWrapper); | ||
processedTerms.add(matchedTerm); // Mark this term as processed. | ||
} else if (isLink) { | ||
// If it's a link, we skip this mention but do not mark it as processed. | ||
newNodes.push(document.createTextNode(matchedTerm)); | ||
} else { | ||
// If it's not the first mention, just add the plain text. | ||
newNodes.push(document.createTextNode(matchedTerm)); | ||
} | ||
|
||
lastIndex = match.index + matchedTerm.length; | ||
hasReplacements = true; | ||
} | ||
|
||
if (lastIndex < currentText.length) { | ||
newNodes.push(document.createTextNode(currentText.slice(lastIndex))); | ||
} | ||
|
||
if (hasReplacements) { | ||
modifications.push({ originalNode: currentNode, newNodes }); | ||
} | ||
} | ||
} | ||
|
||
// Replace the original nodes with new nodes. | ||
modifications.forEach(({ originalNode, newNodes }) => { | ||
const parentElement = originalNode.parentElement; | ||
if (parentElement) { | ||
newNodes.forEach((newNode) => { | ||
parentElement.insertBefore(newNode, originalNode); | ||
}); | ||
parentElement.removeChild(originalNode); | ||
} | ||
}); | ||
}; | ||
|
||
// Target the specific div with the class "theme-doc-markdown markdown". | ||
const targetDiv = document.querySelector('.theme-doc-markdown.markdown'); | ||
if (targetDiv) { | ||
wrapTermsInTooltips(targetDiv); | ||
} | ||
}, [glossary]); | ||
|
||
return <>{children}</>; | ||
}; | ||
|
||
export default GlossaryInjector; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import React, { useEffect, useRef, useState } from 'react'; | ||
|
||
interface GlossaryTooltipProps { | ||
term: string; | ||
definition: string; | ||
children: React.ReactNode; | ||
} | ||
|
||
const GlossaryTooltip: React.FC<GlossaryTooltipProps> = ({ term, definition, children }) => { | ||
const tooltipRef = useRef<HTMLDivElement>(null); | ||
const [tooltipPosition, setTooltipPosition] = useState<{ top: number; left: number } | null>(null); | ||
|
||
const handleMouseEnter = (event: React.MouseEvent) => { | ||
const target = event.currentTarget; | ||
|
||
// Get the bounding rectangle of the target element. | ||
const rect = target.getBoundingClientRect(); | ||
|
||
// Calculate tooltip position. | ||
const tooltipTop = rect.bottom + window.scrollY; // Position below the term. | ||
const tooltipLeft = rect.left + window.scrollX; // Align with the left edge of the term. | ||
|
||
setTooltipPosition({ top: tooltipTop, left: tooltipLeft }); | ||
}; | ||
|
||
const handleMouseLeave = () => { | ||
setTooltipPosition(null); | ||
}; | ||
|
||
return ( | ||
<> | ||
<span | ||
onMouseEnter={handleMouseEnter} | ||
onMouseLeave={handleMouseLeave} | ||
className="glossary-term" | ||
> | ||
{children} | ||
</span> | ||
|
||
{tooltipPosition && ( | ||
<div | ||
ref={tooltipRef} | ||
className="tooltip-glossary" | ||
style={{ | ||
top: tooltipPosition.top, | ||
left: tooltipPosition.left, | ||
position: 'absolute', | ||
}} | ||
> | ||
{definition} | ||
</div> | ||
)} | ||
</> | ||
); | ||
}; | ||
|
||
export default GlossaryTooltip; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import React from 'react'; | ||
import MDXContent from '@theme-original/MDXContent'; | ||
import type MDXContentType from '@theme/MDXContent'; | ||
import type {WrapperProps} from '@docusaurus/types'; | ||
import GlossaryInjector from '../../../src/components/GlossaryInjector'; | ||
|
||
type Props = WrapperProps<typeof MDXContentType>; | ||
|
||
export default function MDXContentWrapper(props: Props, { children }): JSX.Element { | ||
return ( | ||
<> | ||
<MDXContent {...props} /> | ||
<GlossaryInjector> | ||
{children} | ||
</GlossaryInjector> | ||
</> | ||
); | ||
} |