1+ import 'package:jaspr/dom.dart' ;
12import 'package:jaspr/server.dart' ;
23import 'package:jaspr_content/components/code_block.dart' ;
34import 'package:jaspr_content/jaspr_content.dart' ;
45import 'package:syntax_highlight_lite/syntax_highlight_lite.dart' hide Color;
56
6- /// A code block component that gracefully handles unsupported languages
7- /// by falling back to plain text rendering.
7+ /// A code block that renders syntax-highlighted code in **both** light and
8+ /// dark themes at server-render time, toggling visibility via CSS.
9+ ///
10+ /// Docusaurus uses `prism-react-renderer` with:
11+ /// - light: vsLight → [HighlighterTheme.loadLightTheme()] (Light VS + Light+)
12+ /// - dark: vsDark → [HighlighterTheme.loadDarkTheme()] (Dark VS + Dark+)
13+ ///
14+ /// Both [pre] elements are emitted into the HTML; CSS hides the inactive one:
15+ /// `.syntax-dark { display: none }`
16+ /// `[data-theme="dark"] .syntax-light { display: none }`
17+ /// `[data-theme="dark"] .syntax-dark { display: block }`
18+ ///
19+ /// Falls back to [CodeBlock.from] (plain, no highlighting) for any language
20+ /// not in [grammars] .
821class SafeCodeBlock extends CustomComponent {
922 SafeCodeBlock ({this .grammars = const {}}) : super .base ();
1023
1124 final Map <String , String > grammars;
1225
1326 bool _initialized = false ;
14- HighlighterTheme ? _theme;
27+ HighlighterTheme ? _lightTheme;
28+ HighlighterTheme ? _darkTheme;
1529
1630 @override
1731 Component ? create (Node node, NodesBuilder builder) {
@@ -36,23 +50,88 @@ class SafeCodeBlock extends CustomComponent {
3650
3751 final source = children? .map ((c) => c.innerText).join (' ' ) ?? '' ;
3852
39- // Fall back to plain rendering for unsupported languages.
53+ // Fall back to plain (unhighlighted) rendering for unsupported languages.
4054 if (language != null && ! _supportedLanguages.contains (language)) {
4155 return CodeBlock .from (source: source);
4256 }
4357
4458 return AsyncBuilder (
4559 builder: (context) async {
46- final highlighter = Highlighter (
47- language: language ?? 'dart' ,
48- theme: _theme ?? = await HighlighterTheme .loadDarkTheme (),
60+ _lightTheme ?? = await HighlighterTheme .loadLightTheme ();
61+ _darkTheme ?? = await HighlighterTheme .loadDarkTheme ();
62+
63+ final lang = language ?? 'dart' ;
64+ return _DualCodeBlock (
65+ source: source,
66+ lightHighlighter: Highlighter (language: lang, theme: _lightTheme! ),
67+ darkHighlighter: Highlighter (language: lang, theme: _darkTheme! ),
4968 );
50- return CodeBlock .from (source: source, highlighter: highlighter);
5169 },
5270 );
5371 }
5472 return null ;
5573 }
5674
5775 Set <String > get _supportedLanguages => {'dart' , ...grammars.keys};
76+
77+ @css
78+ static List <StyleRule > get styles => [
79+ // Light theme visible by default; dark theme hidden.
80+ css ('.syntax-dark' ).styles (display: Display .none),
81+ // Swap in dark mode.
82+ css ('[data-theme="dark"] .syntax-light' ).styles (display: Display .none),
83+ css ('[data-theme="dark"] .syntax-dark' ).styles (display: Display .block),
84+ ];
85+ }
86+
87+ /// Emits two [pre] blocks (one per theme) inside a `.code-block` wrapper.
88+ /// CSS decides which is shown based on `[data-theme]` .
89+ class _DualCodeBlock extends StatelessComponent {
90+ const _DualCodeBlock ({
91+ required this .source,
92+ required this .lightHighlighter,
93+ required this .darkHighlighter,
94+ });
95+
96+ final String source;
97+ final Highlighter lightHighlighter;
98+ final Highlighter darkHighlighter;
99+
100+ @override
101+ Component build (BuildContext context) {
102+ return div (classes: 'code-block' , [
103+ pre (classes: 'syntax-light' , [
104+ code ([_buildSpan (lightHighlighter.highlight (source))]),
105+ ]),
106+ pre (classes: 'syntax-dark' , [
107+ code ([_buildSpan (darkHighlighter.highlight (source))]),
108+ ]),
109+ ]);
110+ }
111+
112+ /// Converts a [TextSpan] tree from the highlighter into jaspr [Component] s
113+ /// with inline styles — mirrors `_CodeBlock.buildSpan` from jaspr_content.
114+ static Component _buildSpan (TextSpan textSpan) {
115+ Styles ? styles;
116+
117+ if (textSpan.style case final style? ) {
118+ styles = Styles (
119+ color: Color .value (style.foreground.argb & 0x00FFFFFF ),
120+ fontWeight: style.bold ? FontWeight .bold : null ,
121+ fontStyle: style.italic ? FontStyle .italic : null ,
122+ textDecoration: style.underline
123+ ? TextDecoration (line: TextDecorationLine .underline)
124+ : null ,
125+ );
126+ }
127+
128+ if (styles == null && textSpan.children.isEmpty) {
129+ return Component .text (textSpan.text ?? '' );
130+ }
131+
132+ return span (styles: styles, [
133+ if (textSpan.text != null ) Component .text (textSpan.text! ),
134+ for (final child in textSpan.children) _buildSpan (child),
135+ ]);
136+ }
58137}
0 commit comments