| 1 | import { memo, useEffect, useMemo, useState } from "react"; |
| 2 | import type { EditorProps } from "../CodeViewer"; |
| 3 | import { |
| 4 | escapeHtml, |
| 5 | highlightToHtml, |
| 6 | IDLE_HIGHLIGHT_MIN_BYTES, |
| 7 | shouldHighlightSource, |
| 8 | } from "../../lib/highlight"; |
| 9 | import { scheduleIdleTask } from "../../lib/idleTask"; |
| 10 | import { CopyButton } from "../CopyButton"; |
| 11 | |
| 12 | // HljsCode is the syntax-highlighted default behind the code editor seam. It |
| 13 | // renders highlight.js token markup into a <pre>; token colors live in styles.css |
| 14 | // (.hljs-*). To upgrade to a full editor, point CodeViewer.tsx's lazy import at a |
| 15 | // Monaco/CodeMirror module honoring the same EditorProps. |
| 16 | // |
| 17 | // Large blocks (≥ IDLE_HIGHLIGHT_MIN_BYTES, still under the skip caps) mount |
| 18 | // the COMPLETE plain text immediately and swap in highlighted HTML from an |
| 19 | // idle callback; the skip caps (MAX_HIGHLIGHT_BYTES / MAX_HIGHLIGHT_LINES) |
| 20 | // remain the plain-forever policy. Either way nothing is ever truncated. |
| 21 | const HljsCode = memo(function HljsCode({ value, copyValue, language, scrollMode, maxHeight, sourceSize }: EditorProps) { |
| 22 | const syntaxHighlight = useMemo( |
| 23 | () => shouldHighlightSource(value, sourceSize), |
| 24 | [sourceSize, value], |
| 25 | ); |
| 26 | const deferToIdle = syntaxHighlight && (sourceSize ?? value.length) >= IDLE_HIGHLIGHT_MIN_BYTES; |
| 27 | const [idleResult, setIdleResult] = useState<{ value: string; language?: string; html: string } | null>(null); |
| 28 | |
| 29 | useEffect(() => { |
| 30 | if (!deferToIdle) return; |
| 31 | let cancelled = false; |
| 32 | const cancelIdle = scheduleIdleTask(() => { |
| 33 | if (cancelled) return; |
| 34 | const html = highlightToHtml(value, language); |
| 35 | setIdleResult({ value, language, html }); |
| 36 | }); |
| 37 | return () => { |
| 38 | cancelled = true; |
| 39 | cancelIdle(); |
| 40 | }; |
| 41 | }, [deferToIdle, language, value]); |
| 42 | |
| 43 | const idleHtml = idleResult && idleResult.value === value && idleResult.language === language |
| 44 | ? idleResult.html |
| 45 | : null; |
| 46 | const html = useMemo(() => { |
| 47 | if (!deferToIdle) return highlightToHtml(value, syntaxHighlight ? language : undefined); |
| 48 | return idleHtml ?? escapeHtml(value); |
| 49 | }, [deferToIdle, idleHtml, language, syntaxHighlight, value]); |
| 50 | const highlighted = syntaxHighlight && (!deferToIdle || idleHtml !== null); |
| 51 | const bounded = scrollMode === "bounded" || (scrollMode !== "expand" && maxHeight != null); |
| 52 | return ( |
| 53 | <div className="code-block__wrap"> |
| 54 | <pre |
| 55 | className={`code hljs${bounded ? " code--scroll-y" : ""}`} |
| 56 | data-nested-scroll={bounded ? "" : undefined} |
| 57 | data-highlight-mode={highlighted ? "syntax" : "plain"} |
| 58 | data-lang={language} |
| 59 | style={maxHeight != null ? { maxHeight } : undefined} |
| 60 | > |
| 61 | <code dangerouslySetInnerHTML={{ __html: html }} /> |
| 62 | </pre> |
| 63 | <CopyButton text={copyValue ?? value} className="code-block__copy" /> |
| 64 | </div> |
| 65 | ); |
| 66 | }); |
| 67 | |
| 68 | export default HljsCode; |
| 69 |