@@ -25,9 +25,7 @@ import React, {
2525 Suspense ,
2626 useCallback ,
2727 useContext ,
28- useEffect ,
2928 useMemo ,
30- useRef ,
3129 useState ,
3230} from "react"
3331
@@ -73,26 +71,24 @@ import {
7371 StyledPreWrapper ,
7472 StyledStreamlitMarkdown ,
7573} from "./styled-components"
74+ import {
75+ type EmojiPlugin ,
76+ isLoadedPlugin ,
77+ type KatexPlugin ,
78+ loadKatexPlugin ,
79+ loadKatexStyles ,
80+ loadRehypeRaw ,
81+ loadRemarkEmoji ,
82+ type RawPlugin ,
83+ useLazyPlugin ,
84+ wrapRehypePlugin ,
85+ wrapRemarkPlugin ,
86+ } from "./utils"
7687
7788const StreamlitSyntaxHighlighter = lazy (
7889 ( ) => import ( "~lib/components/elements/CodeBlock/StreamlitSyntaxHighlighter" )
7990)
8091
81- // Lazy load katex dependencies
82- const loadKatexPlugin = ( ) : Promise < typeof import ( "rehype-katex" ) > =>
83- import ( "rehype-katex" )
84- const loadKatexStyles = once ( ( ) : void => {
85- void import ( "katex/dist/katex.min.css" )
86- } )
87-
88- // Lazy load rehype-raw (pulls in parse5)
89- const loadRehypeRaw = ( ) : Promise < typeof import ( "rehype-raw" ) > =>
90- import ( "rehype-raw" )
91-
92- // Lazy load remark-emoji (pulls in node-emoji and @sindresorhus/is)
93- const loadRemarkEmoji = ( ) : Promise < typeof import ( "remark-emoji" ) > =>
94- import ( "remark-emoji" )
95-
9692/**
9793 * Heuristic to determine if the markdown source contains emoji shortcodes that require remark-emoji.
9894 * Checks for patterns like :emoji_name: but excludes Streamlit's custom :material/ and
@@ -847,96 +843,60 @@ export const RenderedMarkdown = memo(function RenderedMarkdown({
847843 disableLinks,
848844} : Readonly < RenderedMarkdownProps > ) : ReactElement {
849845 const theme = useEmotionTheme ( )
850- type KatexPlugin = Awaited < ReturnType < typeof loadKatexPlugin > > [ "default" ]
851- type RawPlugin = Awaited < ReturnType < typeof loadRehypeRaw > > [ "default" ]
852- type EmojiPlugin = Awaited < ReturnType < typeof loadRemarkEmoji > > [ "default" ]
853- const [ katexPlugin , setKatexPlugin ] = useState < KatexPlugin | null > ( null )
854- const isLoadingKatexRef = useRef ( false )
855- const [ rawPlugin , setRawPlugin ] = useState < RawPlugin | null > ( null )
856- const isLoadingRawRef = useRef ( false )
857- const [ emojiPlugin , setEmojiPlugin ] = useState < EmojiPlugin | null > ( null )
858- const isLoadingEmojiRef = useRef ( false )
859846
860847 const needsKatex = useMemo ( ( ) => containsMathSyntax ( source ) , [ source ] )
861848 const needsEmoji = useMemo ( ( ) => containsEmojiShortcodes ( source ) , [ source ] )
862849
863- // Load katex plugin when needed
864- useEffect ( ( ) => {
865- let isMounted = true
866-
867- if ( needsKatex && ! katexPlugin && ! isLoadingKatexRef . current ) {
868- isLoadingKatexRef . current = true
869- loadKatexStyles ( )
870- void loadKatexPlugin ( )
871- . then ( module => {
872- if ( isMounted ) {
873- setKatexPlugin ( ( ) => module . default )
874- }
875- } )
876- . catch ( ( ) => {
877- // Silently fail - math will render as plain text
878- } )
879- . finally ( ( ) => {
880- isLoadingKatexRef . current = false
881- } )
882- }
850+ // Lazy load plugins only when needed
851+ const katexPlugin = useLazyPlugin < KatexPlugin > ( {
852+ key : "katex" ,
853+ needed : needsKatex ,
854+ load : loadKatexPlugin ,
855+ pluginName : "rehype-katex" ,
856+ onBeforeLoad : loadKatexStyles ,
857+ } )
858+
859+ const rawPlugin = useLazyPlugin < RawPlugin > ( {
860+ key : "raw" ,
861+ needed : allowHTML ,
862+ load : loadRehypeRaw ,
863+ pluginName : "rehype-raw" ,
864+ } )
865+
866+ const emojiPlugin = useLazyPlugin < EmojiPlugin > ( {
867+ key : "emoji" ,
868+ needed : needsEmoji ,
869+ load : loadRemarkEmoji ,
870+ pluginName : "remark-emoji" ,
871+ } )
883872
884- return ( ) => {
885- isMounted = false
886- }
887- } , [ needsKatex , katexPlugin ] )
888-
889- // Load rehype-raw plugin when HTML is allowed
890- useEffect ( ( ) => {
891- let isMounted = true
892-
893- if ( allowHTML && ! rawPlugin && ! isLoadingRawRef . current ) {
894- isLoadingRawRef . current = true
895- void loadRehypeRaw ( )
896- . then ( module => {
897- if ( isMounted ) {
898- setRawPlugin ( ( ) => module . default )
899- }
900- } )
901- . catch ( ( ) => {
902- // Silently fail - HTML will be escaped
903- } )
904- . finally ( ( ) => {
905- isLoadingRawRef . current = false
906- } )
907- }
873+ const colorMapping = useMemo ( ( ) => createColorMapping ( theme ) , [ theme ] )
908874
909- return ( ) => {
910- isMounted = false
911- }
912- } , [ allowHTML , rawPlugin ] )
913-
914- // Load remark-emoji plugin when emoji shortcodes are detected
915- useEffect ( ( ) => {
916- let isMounted = true
917-
918- if ( needsEmoji && ! emojiPlugin && ! isLoadingEmojiRef . current ) {
919- isLoadingEmojiRef . current = true
920- void loadRemarkEmoji ( )
921- . then ( module => {
922- if ( isMounted ) {
923- setEmojiPlugin ( ( ) => module . default )
924- }
925- } )
926- . catch ( ( ) => {
927- // Silently fail - emoji shortcodes will render as plain text
928- } )
929- . finally ( ( ) => {
930- isLoadingEmojiRef . current = false
931- } )
932- }
875+ // Wrap plugins once when they load, not on every render or when other deps change
876+ const wrappedKatexPlugin = useMemo (
877+ ( ) =>
878+ isLoadedPlugin ( katexPlugin )
879+ ? wrapRehypePlugin ( katexPlugin , "rehype-katex" )
880+ : null ,
881+ [ katexPlugin ]
882+ )
933883
934- return ( ) => {
935- isMounted = false
936- }
937- } , [ needsEmoji , emojiPlugin ] )
884+ const wrappedRawPlugin = useMemo (
885+ ( ) =>
886+ isLoadedPlugin ( rawPlugin )
887+ ? wrapRehypePlugin ( rawPlugin , "rehype-raw" )
888+ : null ,
889+ [ rawPlugin ]
890+ )
938891
939- const colorMapping = useMemo ( ( ) => createColorMapping ( theme ) , [ theme ] )
892+ const wrappedEmojiPlugin = useMemo (
893+ ( ) =>
894+ isLoadedPlugin ( emojiPlugin )
895+ ? // eslint-disable-next-line @typescript-eslint/no-explicit-any -- unified's Plugin type is more complex than our wrapper expects
896+ wrapRemarkPlugin ( emojiPlugin as any , "remark-emoji" )
897+ : null ,
898+ [ emojiPlugin ]
899+ )
940900
941901 const remarkPlugins = useMemo < PluggableList > ( ( ) => {
942902 const plugins : PluggableList = [
@@ -945,33 +905,30 @@ export const RenderedMarkdown = memo(function RenderedMarkdown({
945905 createRemarkMaterialIcons ( theme ) ,
946906 ]
947907
948- // Only add emoji plugin if it's loaded (lazy-loaded when emoji shortcodes detected)
949- if ( emojiPlugin ) {
950- plugins . push ( emojiPlugin )
908+ if ( needsEmoji && wrappedEmojiPlugin ) {
909+ plugins . push ( wrappedEmojiPlugin )
951910 }
952911
953912 return plugins
954- } , [ theme , colorMapping , emojiPlugin ] )
913+ } , [ theme , colorMapping , needsEmoji , wrappedEmojiPlugin ] )
955914
956915 const rehypePlugins = useMemo < PluggableList > ( ( ) => {
957916 const plugins : PluggableList = [ ]
958917
959- // Only add katex plugin if it's loaded
960- if ( katexPlugin ) {
961- plugins . push ( katexPlugin )
918+ if ( needsKatex && wrappedKatexPlugin ) {
919+ plugins . push ( wrappedKatexPlugin )
962920 }
963921
964- // Only add raw plugin if it's loaded and HTML is allowed
965- if ( allowHTML && rawPlugin ) {
966- plugins . push ( rawPlugin )
922+ if ( allowHTML && wrappedRawPlugin ) {
923+ plugins . push ( wrappedRawPlugin )
967924 }
968925
969926 // This plugin must run last to ensure the inline property is set correctly
970927 // and not overwritten by other plugins like rehypeRaw
971928 plugins . push ( rehypeSetCodeInlineProperty )
972929
973930 return plugins
974- } , [ allowHTML , katexPlugin , rawPlugin ] )
931+ } , [ allowHTML , needsKatex , wrappedKatexPlugin , wrappedRawPlugin ] )
975932
976933 const renderers = useMemo (
977934 ( ) =>
@@ -993,12 +950,14 @@ export const RenderedMarkdown = memo(function RenderedMarkdown({
993950 return disableLinks ? LINKS_DISALLOWED_ELEMENTS : LABEL_DISALLOWED_ELEMENTS
994951 } , [ isLabel , disableLinks ] )
995952
996- // Show skeleton while dependencies are loading
997- if (
998- ( needsKatex && ! katexPlugin ) ||
999- ( allowHTML && ! rawPlugin ) ||
1000- ( needsEmoji && ! emojiPlugin )
1001- ) {
953+ // Show skeleton while required plugins are still loading
954+ // A plugin is "loading" if it's needed but state is still null (not loaded, not failed)
955+ const isLoadingPlugins =
956+ ( needsKatex && katexPlugin === null ) ||
957+ ( allowHTML && rawPlugin === null ) ||
958+ ( needsEmoji && emojiPlugin === null )
959+
960+ if ( isLoadingPlugins ) {
1002961 return (
1003962 < ErrorBoundary >
1004963 < Skeleton
0 commit comments