| 1 |
import { useEffect, useRef, useState } from '@wordpress/element'; |
| 2 |
import { decodeEntities } from '@wordpress/html-entities'; |
| 3 |
import { __ } from '@wordpress/i18n'; |
| 4 |
import { Theme } from 'shiki'; |
| 5 |
import { useTheme } from '../../hooks/useTheme'; |
| 6 |
import { Lang } from '../../types'; |
| 7 |
import { fontFamilyLong, maybeClamp } from '../../util/fonts'; |
| 8 |
|
| 9 |
type ThemePreviewProps = { |
| 10 |
id: string; |
| 11 |
theme: Theme; |
| 12 |
lang: Lang; |
| 13 |
code: string; |
| 14 |
fontSize: string; |
| 15 |
lineHeight: string; |
| 16 |
fontFamily: string; |
| 17 |
clampFonts: boolean; |
| 18 |
onClick: () => void; |
| 19 |
}; |
| 20 |
export const ThemePreview = ({ |
| 21 |
id, |
| 22 |
theme, |
| 23 |
lang, |
| 24 |
onClick, |
| 25 |
code, |
| 26 |
fontSize, |
| 27 |
lineHeight, |
| 28 |
fontFamily, |
| 29 |
clampFonts, |
| 30 |
}: ThemePreviewProps) => { |
| 31 |
const [inView, setInView] = useState(false); |
| 32 |
const { highlighter, error, loading } = useTheme({ |
| 33 |
theme, |
| 34 |
// If no code is written yet, show a classic Carmack snippet |
| 35 |
lang: code ? lang : 'c', |
| 36 |
ready: inView, |
| 37 |
}); |
| 38 |
const [codeRendered, setCode] = useState(''); |
| 39 |
const [backgroundColor, setBg] = useState('#ffffff'); |
| 40 |
const observer = useRef<IntersectionObserver>( |
| 41 |
new IntersectionObserver(([entry]) => { |
| 42 |
if (entry.isIntersecting) { |
| 43 |
setInView(true); |
| 44 |
observer.current.disconnect(); |
| 45 |
} |
| 46 |
}), |
| 47 |
); |
| 48 |
const codeSnippet = ` |
| 49 |
float Q_rsqrt( float number ) |
| 50 |
{ |
| 51 |
long i; |
| 52 |
float x2, y; |
| 53 |
const float threehalfs = 1.5F; |
| 54 |
|
| 55 |
x2 = number * 0.5F; |
| 56 |
y = number; |
| 57 |
i = * ( long * ) &y; // evil floating point bit level hacking |
| 58 |
i = 0x5f3759df - ( i >> 1 ); // what the frack? |
| 59 |
y = * ( float * ) &i; |
| 60 |
y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration |
| 61 |
// y = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed |
| 62 |
|
| 63 |
return y; |
| 64 |
}`.trim(); |
| 65 |
|
| 66 |
useEffect(() => { |
| 67 |
if (!highlighter) return; |
| 68 |
if ((lang as string) === 'ansi' && code) { |
| 69 |
setCode(highlighter.ansiToHtml(code)); |
| 70 |
setBg(highlighter.getBackgroundColor()); |
| 71 |
return; |
| 72 |
} |
| 73 |
const hl = code |
| 74 |
? highlighter.codeToHtml(decodeEntities(code), { lang }) |
| 75 |
: highlighter.codeToHtml(decodeEntities(codeSnippet), { |
| 76 |
lang: 'c', |
| 77 |
}); |
| 78 |
setCode(hl); |
| 79 |
setBg(highlighter.getBackgroundColor()); |
| 80 |
}, [highlighter, lang, code, codeSnippet]); |
| 81 |
|
| 82 |
return ( |
| 83 |
<button |
| 84 |
id={id} |
| 85 |
type="button" |
| 86 |
onClick={onClick} |
| 87 |
className="cbp-theme-preview p-4 px-3 border flex items-start w-full text-left outline-none cursor-pointer no-underline ring-offset-2 ring-offset-white focus:shadow-none focus:ring-wp overflow-x-auto max-h-80 overflow-y-hidden" |
| 88 |
style={{ backgroundColor, minHeight: '50px' }}> |
| 89 |
{loading || error || !inView ? ( |
| 90 |
<span |
| 91 |
id={id} |
| 92 |
ref={(el) => { |
| 93 |
if (!el) return; |
| 94 |
observer.current.observe(el); |
| 95 |
}} |
| 96 |
style={{ minHeight: '200px' }} |
| 97 |
className="flex items-center justify-center p-6 w-full"> |
| 98 |
{error?.message || __('Loading...', 'code-block-pro')} |
| 99 |
</span> |
| 100 |
) : ( |
| 101 |
<span |
| 102 |
className="pointer-events-none" |
| 103 |
style={{ |
| 104 |
fontSize: maybeClamp(fontSize, clampFonts), |
| 105 |
fontFamily: fontFamilyLong(fontFamily), |
| 106 |
lineHeight: maybeClamp(lineHeight, clampFonts), |
| 107 |
}} |
| 108 |
dangerouslySetInnerHTML={{ __html: codeRendered }} |
| 109 |
/> |
| 110 |
)} |
| 111 |
</button> |
| 112 |
); |
| 113 |
}; |
| 114 |
|