Skip to content

Commit adabdc1

Browse files
fix: render dual-theme syntax-highlighted code blocks for light/dark modes
1 parent c9c2f10 commit adabdc1

2 files changed

Lines changed: 89 additions & 8 deletions

File tree

site_jaspr/lib/components/safe_code_block.dart

Lines changed: 87 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
1+
import 'package:jaspr/dom.dart';
12
import 'package:jaspr/server.dart';
23
import 'package:jaspr_content/components/code_block.dart';
34
import 'package:jaspr_content/jaspr_content.dart';
45
import '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].
821
class 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
}

site_jaspr/lib/main.server.options.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import 'package:site_jaspr/components/edit_page_link.dart' as _edit_page_link;
2222
import 'package:site_jaspr/components/icon_link.dart' as _icon_link;
2323
import 'package:site_jaspr/components/nav_link.dart' as _nav_link;
2424
import 'package:site_jaspr/components/page_navigation.dart' as _page_navigation;
25+
import 'package:site_jaspr/components/safe_code_block.dart' as _safe_code_block;
2526
import 'package:site_jaspr/components/site_footer.dart' as _site_footer;
2627

2728
/// Default [ServerOptions] for use with your Jaspr project.
@@ -71,6 +72,7 @@ ServerOptions get defaultServerOptions => ServerOptions(
7172
..._icon_link.IconLink.styles,
7273
..._nav_link.NavLink.styles,
7374
..._page_navigation.PageNavigation.styles,
75+
..._safe_code_block.SafeCodeBlock.styles,
7476
..._site_footer.SiteFooter.styles,
7577
],
7678
);

0 commit comments

Comments
 (0)