返回 reveal.js
markdown.tsx
根目录 / react / src / components / markdown.tsx
1 import {
2 useContext,
3 useEffect,
4 useLayoutEffect,
5 useRef,
6 useState,
7 type HTMLAttributes,
8 } from 'react';
9 import type { MarkdownProps } from '../types';
10 import { RevealContext } from '../reveal-context';
11 import { getSlideAttributes } from '../utils/slide-attributes';
12 import {
13 DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR,
14 DEFAULT_NOTES_SEPARATOR,
15 DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR,
16 DEFAULT_SLIDE_SEPARATOR,
17 DEFAULT_VERTICAL_SEPARATOR,
18 addAttributes,
19 buildMarkdownNodes,
20 createMarkedInstance,
21 getErrorMessage,
22 getSectionPropsSignature,
23 hashString,
24 normalizeMarkdownSource,
25 } from '../utils/markdown';
26
27 type MarkdownLeafSectionProps = {
28 html: string;
29 elementAttributesSeparator: string;
30 slideAttributesSeparator: string;
31 sectionProps?: HTMLAttributes<HTMLElement>;
32 };
33
34 type HighlightPlugin = {
35 highlightBlock?: (block: HTMLElement) => void;
36 };
37
38 function MarkdownLeafSection({
39 html,
40 elementAttributesSeparator,
41 slideAttributesSeparator,
42 sectionProps,
43 }: MarkdownLeafSectionProps) {
44 const deck = useContext(RevealContext);
45 const sectionRef = useRef<HTMLElement>(null);
46
47 useLayoutEffect(() => {
48 const section = sectionRef.current;
49 if (!section) return;
50
51 // React renders the raw HTML for us, then we run the same comment-based
52 // attribute pass as the core plugin to support `.slide:` and `.element:`.
53 addAttributes(section, section, null, elementAttributesSeparator, slideAttributesSeparator);
54 }, [html, elementAttributesSeparator, slideAttributesSeparator]);
55
56 useLayoutEffect(() => {
57 const section = sectionRef.current;
58 if (!section) return;
59
60 const plugin = deck?.getPlugin?.('highlight') as HighlightPlugin | undefined;
61 const highlightBlock = plugin?.highlightBlock;
62 if (typeof highlightBlock !== 'function') return;
63
64 let highlightedBlockCount = 0;
65 Array.from(section.querySelectorAll<HTMLElement>('pre code')).forEach((block) => {
66 if (block.getAttribute('data-highlighted') === 'yes') return;
67 highlightBlock(block);
68 highlightedBlockCount += 1;
69 });
70
71 const syncFragments = deck?.syncFragments;
72 if (highlightedBlockCount > 0 && typeof syncFragments === 'function') {
73 syncFragments(section);
74 }
75 });
76
77 return <section ref={sectionRef} {...sectionProps} dangerouslySetInnerHTML={{ __html: html }} />;
78 }
79
80 export function Markdown({
81 children,
82 markdown,
83 src,
84 charset,
85 separator = DEFAULT_SLIDE_SEPARATOR,
86 verticalSeparator = DEFAULT_VERTICAL_SEPARATOR,
87 notesSeparator = DEFAULT_NOTES_SEPARATOR,
88 elementAttributesSeparator = DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR,
89 slideAttributesSeparator = DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR,
90 options,
91 background,
92 backgroundImage,
93 backgroundVideo,
94 backgroundVideoLoop,
95 backgroundVideoMuted,
96 backgroundIframe,
97 backgroundColor,
98 backgroundGradient,
99 backgroundSize,
100 backgroundPosition,
101 backgroundRepeat,
102 backgroundOpacity,
103 backgroundTransition,
104 visibility,
105 autoAnimate,
106 autoAnimateId,
107 autoAnimateRestart,
108 autoAnimateUnmatched,
109 autoAnimateEasing,
110 autoAnimateDuration,
111 autoAnimateDelay,
112 transition,
113 transitionSpeed,
114 autoSlide,
115 notes,
116 backgroundInteractive,
117 preload,
118 ...rest
119 }: MarkdownProps) {
120 const [loadedMarkdown, setLoadedMarkdown] = useState<string | null>(null);
121 const [loadError, setLoadError] = useState<string | null>(null);
122
123 useEffect(() => {
124 if (!src) {
125 setLoadedMarkdown(null);
126 setLoadError(null);
127 return;
128 }
129
130 const abortController = new AbortController();
131 setLoadedMarkdown(null);
132 setLoadError(null);
133
134 void (async () => {
135 try {
136 const response = await fetch(src, { signal: abortController.signal });
137 if (!response.ok) {
138 throw new Error(`HTTP status ${response.status}`);
139 }
140
141 const source = charset
142 ? new TextDecoder(charset).decode(await response.arrayBuffer())
143 : await response.text();
144
145 setLoadedMarkdown(source);
146 } catch (error) {
147 if (abortController.signal.aborted) return;
148 setLoadError(getErrorMessage(error));
149 }
150 })();
151
152 return () => abortController.abort();
153 }, [src, charset]);
154
155 const slideAttributes = getSlideAttributes(rest, {
156 background,
157 backgroundImage,
158 backgroundVideo,
159 backgroundVideoLoop,
160 backgroundVideoMuted,
161 backgroundIframe,
162 backgroundColor,
163 backgroundGradient,
164 backgroundSize,
165 backgroundPosition,
166 backgroundRepeat,
167 backgroundOpacity,
168 backgroundTransition,
169 visibility,
170 autoAnimate,
171 autoAnimateId,
172 autoAnimateRestart,
173 autoAnimateUnmatched,
174 autoAnimateEasing,
175 autoAnimateDuration,
176 autoAnimateDelay,
177 transition,
178 transitionSpeed,
179 autoSlide,
180 notes,
181 backgroundInteractive,
182 preload,
183 });
184 const forwardedSectionSignature = hashString(getSectionPropsSignature(slideAttributes));
185
186 if (src && loadError) {
187 return (
188 <section data-state="alert">
189 {`ERROR: The attempt to fetch ${src} failed. ${loadError}.`}
190 <p>Remember that you need to serve the presentation from an HTTP server.</p>
191 </section>
192 );
193 }
194
195 if (src && loadedMarkdown == null) {
196 return null;
197 }
198
199 const source = src ? loadedMarkdown || '' : normalizeMarkdownSource(markdown ?? children ?? '');
200
201 try {
202 const markedInstance = createMarkedInstance(options);
203 const nodes = buildMarkdownNodes(
204 source,
205 markedInstance,
206 separator,
207 verticalSeparator,
208 notesSeparator
209 );
210 const attributeSeparatorSignature = hashString(
211 `${elementAttributesSeparator}\0${slideAttributesSeparator}`
212 );
213
214 return (
215 <>
216 {nodes.map((node) => {
217 if (node.type === 'slide') {
218 return (
219 <MarkdownLeafSection
220 key={`${node.key}-${forwardedSectionSignature}-${attributeSeparatorSignature}-${hashString(node.slide.html)}`}
221 html={node.slide.html}
222 elementAttributesSeparator={elementAttributesSeparator}
223 slideAttributesSeparator={slideAttributesSeparator}
224 sectionProps={slideAttributes}
225 />
226 );
227 }
228
229 return (
230 <section key={`${node.key}-${forwardedSectionSignature}`} {...slideAttributes}>
231 {node.slides.map((slide) => (
232 <MarkdownLeafSection
233 key={`${slide.key}-${attributeSeparatorSignature}-${hashString(slide.html)}`}
234 html={slide.html}
235 elementAttributesSeparator={elementAttributesSeparator}
236 slideAttributesSeparator={slideAttributesSeparator}
237 />
238 ))}
239 </section>
240 );
241 })}
242 </>
243 );
244 } catch (error) {
245 return (
246 <section data-state="alert">
247 {`ERROR: Failed to parse markdown. ${getErrorMessage(error)}.`}
248 </section>
249 );
250 }
251 }
252
252 lines Plain Text