返回 DeepSeek-Reasonix
HljsCode.tsx
根目录 / desktop / frontend / src / components / editors / HljsCode.tsx
1 import { Fragment, 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 resolveLang,
9 } from "../../lib/highlight";
10 import { splitHighlightedCodeLines } from "../../lib/highlightLines";
11 import { scheduleIdleTask } from "../../lib/idleTask";
12 import { CopyButton } from "../CopyButton";
13
14 // HljsCode is the syntax-highlighted default behind the code editor seam. It
15 // renders highlight.js token markup into a <pre>; token colors live in CodeSyntax.css
16 // (.hljs-*). To upgrade to a full editor, point CodeViewer.tsx's lazy import at a
17 // Monaco/CodeMirror module honoring the same EditorProps.
18 //
19 // Large blocks (≥ IDLE_HIGHLIGHT_MIN_BYTES, still under the skip caps) mount
20 // the COMPLETE plain text immediately and swap in highlighted HTML from an
21 // idle callback; the skip caps (MAX_HIGHLIGHT_BYTES / MAX_HIGHLIGHT_LINES)
22 // remain the plain-forever policy. Either way nothing is ever truncated.
23 const CodeLine = memo(function CodeLine({ html }: { html: string }) {
24 return <span className="code-block__line" dangerouslySetInnerHTML={{ __html: html }} />;
25 });
26
27 const HljsCode = memo(function HljsCode({ value, copyValue, language, scrollMode, maxHeight, sourceSize, showHeader }: EditorProps) {
28 const syntaxHighlight = useMemo(
29 () => Boolean(resolveLang(language)) && shouldHighlightSource(value, sourceSize),
30 [language, sourceSize, value],
31 );
32 const deferToIdle = syntaxHighlight && (sourceSize ?? value.length) >= IDLE_HIGHLIGHT_MIN_BYTES;
33 const [idleResult, setIdleResult] = useState<{ value: string; language?: string; html: string } | null>(null);
34
35 useEffect(() => {
36 if (!deferToIdle) return;
37 let cancelled = false;
38 const cancelIdle = scheduleIdleTask(() => {
39 if (cancelled) return;
40 const html = highlightToHtml(value, language);
41 setIdleResult({ value, language, html });
42 });
43 return () => {
44 cancelled = true;
45 cancelIdle();
46 };
47 }, [deferToIdle, language, value]);
48
49 const idleHtml = idleResult && idleResult.value === value && idleResult.language === language
50 ? idleResult.html
51 : null;
52 const html = useMemo(() => {
53 if (!deferToIdle) return highlightToHtml(value, syntaxHighlight ? language : undefined);
54 // While an appended revision waits for idle, retain its already-highlighted
55 // prefix. Replacements and language changes must never reuse stale markup.
56 if (idleHtml === null && idleResult && idleResult.language === language && value.startsWith(idleResult.value)) {
57 return idleResult.html + escapeHtml(value.slice(idleResult.value.length));
58 }
59 return idleHtml ?? escapeHtml(value);
60 }, [deferToIdle, idleHtml, idleResult, language, syntaxHighlight, value]);
61 const retainLines = showHeader && syntaxHighlight;
62 const lines = useMemo(() => retainLines ? splitHighlightedCodeLines(html) : [], [html, retainLines]);
63 const highlighted = syntaxHighlight && (!deferToIdle || idleHtml !== null);
64 const bounded = scrollMode === "bounded" || (scrollMode !== "expand" && maxHeight != null);
65 return (
66 <div className="code-block__wrap">
67 <pre
68 className={`code hljs${bounded ? " code--scroll-y" : ""}`}
69 data-nested-scroll={bounded ? "" : undefined}
70 data-highlight-mode={highlighted ? "syntax" : "plain"}
71 data-lang={language}
72 style={maxHeight != null ? { maxHeight } : undefined}
73 >
74 {retainLines ? <code>{lines.map((line, index) => <Fragment key={index}>
75 {index > 0 && "\n"}<CodeLine html={line} />
76 </Fragment>)}</code> : <code dangerouslySetInnerHTML={{ __html: html }} />}
77 </pre>
78 {!showHeader && <CopyButton text={copyValue ?? value} className="code-block__copy" />}
79 </div>
80 );
81 });
82
83 export default HljsCode;
84
84 lines Plain Text