返回 reveal.js
code.tsx
根目录 / react / src / components / code.tsx
1 import { useContext, useLayoutEffect, useMemo, useRef } from 'react';
2 import { RevealContext } from '../reveal-context';
3 import type { CodeProps } from '../types';
4
5 type HighlightPlugin = {
6 highlightBlock?: (block: HTMLElement) => void;
7 };
8
9 function normalizeCode(code: string) {
10 const lines = code.replace(/\r\n/g, '\n').split('\n');
11
12 while (lines.length && lines[0].trim().length === 0) lines.shift();
13 while (lines.length && lines[lines.length - 1].trim().length === 0) lines.pop();
14
15 if (!lines.length) return '';
16
17 const minIndent = lines
18 .filter((line) => line.trim().length > 0)
19 .reduce(
20 (acc, line) => Math.min(acc, line.match(/^\s*/)?.[0].length ?? 0),
21 Number.POSITIVE_INFINITY
22 );
23
24 return lines.map((line) => line.slice(minIndent)).join('\n');
25 }
26
27 function cleanupGeneratedFragments(block: HTMLElement) {
28 const pre = block.parentElement;
29 if (!pre) return;
30
31 // RevealHighlight creates extra <code.fragment> nodes for each highlight step (e.g. "1|3").
32 // Remove previously generated nodes before re-highlighting to avoid duplicate steps.
33 Array.from(pre.children).forEach((child) => {
34 if (
35 child !== block &&
36 child instanceof HTMLElement &&
37 child.tagName === 'CODE' &&
38 child.classList.contains('fragment')
39 ) {
40 child.remove();
41 }
42 });
43 }
44
45 export function Code({
46 children,
47 code,
48 language,
49 trim = true,
50 lineNumbers,
51 startFrom,
52 noEscape,
53 codeClassName,
54 codeStyle,
55 codeProps,
56 className,
57 style,
58 ...rest
59 }: CodeProps) {
60 const deck = useContext(RevealContext);
61 const codeRef = useRef<HTMLElement>(null);
62 const lastHighlightSignatureRef = useRef<string>('');
63
64 const rawCode = typeof code === 'string' ? code : typeof children === 'string' ? children : '';
65 const normalizedCode = useMemo(() => (trim ? normalizeCode(rawCode) : rawCode), [rawCode, trim]);
66 const lineNumbersValue =
67 lineNumbers === true
68 ? ''
69 : lineNumbers === false || lineNumbers == null
70 ? undefined
71 : String(lineNumbers);
72 const codeClasses = [language, codeClassName].filter(Boolean).join(' ');
73 const preClasses = ['code-wrapper', className].filter(Boolean).join(' ');
74
75 useLayoutEffect(() => {
76 const block = codeRef.current;
77 if (!block || !deck) return;
78
79 const plugin = deck.getPlugin?.('highlight') as HighlightPlugin | undefined;
80 if (!plugin || typeof plugin.highlightBlock !== 'function') return;
81
82 const highlightSignature = [
83 normalizedCode,
84 language || '',
85 codeClassName || '',
86 lineNumbersValue == null ? '__none__' : `lineNumbers:${lineNumbersValue}`,
87 startFrom == null ? '' : String(startFrom),
88 noEscape ? '1' : '0',
89 ].join('::');
90
91 if (
92 lastHighlightSignatureRef.current === highlightSignature &&
93 block.getAttribute('data-highlighted') === 'yes'
94 ) {
95 return;
96 }
97
98 cleanupGeneratedFragments(block);
99 block.textContent = normalizedCode;
100 block.removeAttribute('data-highlighted');
101 block.classList.remove('hljs');
102 block.classList.remove('has-highlights');
103
104 // Restore source attributes before each highlight call since RevealHighlight mutates
105 // data-line-numbers on the original block when it expands multi-step highlights.
106 if (lineNumbersValue == null) block.removeAttribute('data-line-numbers');
107 else block.setAttribute('data-line-numbers', lineNumbersValue);
108 if (startFrom == null) block.removeAttribute('data-ln-start-from');
109 else block.setAttribute('data-ln-start-from', String(startFrom));
110 if (noEscape) block.setAttribute('data-noescape', '');
111 else block.removeAttribute('data-noescape');
112
113 plugin.highlightBlock(block);
114
115 const slide = typeof block.closest === 'function' ? block.closest('section') : null;
116 if (slide && typeof deck.syncFragments === 'function') {
117 deck.syncFragments(slide as HTMLElement);
118 }
119
120 lastHighlightSignatureRef.current = highlightSignature;
121 }, [deck, normalizedCode, language, codeClassName, lineNumbersValue, startFrom, noEscape]);
122
123 return (
124 <pre className={preClasses} style={style} {...rest}>
125 <code
126 {...codeProps}
127 ref={codeRef}
128 className={codeClasses || undefined}
129 style={codeStyle}
130 data-line-numbers={lineNumbersValue}
131 data-ln-start-from={startFrom}
132 data-noescape={noEscape ? '' : undefined}
133 >
134 {normalizedCode}
135 </code>
136 </pre>
137 );
138 }
139
139 lines Plain Text