Created
December 4, 2024 12:36
-
-
Save xsahil03x/7c2385f7a7348e422634ef6c58e4386c to your computer and use it in GitHub Desktop.
CodeSyntaxHighlightBuilder
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'package:flutter_highlighting/flutter_highlighting.dart'; | |
import 'package:flutter_highlighting/themes/atom-one-dark.dart'; | |
import 'package:flutter_highlighting/themes/atom-one-light.dart'; | |
import 'package:flutter_markdown/flutter_markdown.dart'; | |
import 'package:highlighting/languages/all.dart'; | |
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; | |
/// {@template codeSyntaxHighlightBuilder} | |
/// A builder that highlights code blocks in a markdown body. It uses the | |
/// [HighlightView] widget to display the code block with syntax highlighting. | |
/// | |
/// The builder also displays a header with the language of the code block and a | |
/// copy button. | |
/// {@endtemplate} | |
class CodeSyntaxHighlightBuilder extends MarkdownElementBuilder { | |
/// {@macro codeSyntaxHighlightBuilder} | |
CodeSyntaxHighlightBuilder({ | |
this.styleSheet, | |
this.onCopyPressed, | |
}); | |
/// The stylesheet to use for the markdown body. | |
final MarkdownStyleSheet? styleSheet; | |
/// A callback that is called when the copy button is pressed. | |
final ValueSetter<String>? onCopyPressed; | |
String? _language; | |
@override | |
void visitElementBefore(md.Element element) { | |
// The code block is in the form of | |
// <pre><code class="language-dart">...</code></pre> | |
_language = switch (element) { | |
md.Element( | |
tag: 'pre', | |
children: [md.Element(tag: 'code', attributes: {'class': final clazz})], | |
) => | |
clazz.split('-').last, // 'language-dart' => 'dart' | |
_ => null, | |
}; | |
return super.visitElementBefore(element); | |
} | |
@override | |
Widget? visitText(md.Text text, TextStyle? preferredStyle) { | |
// If the language is set, we don't need to display the text node as it's | |
// going to be handled by the [visitElementAfterWithContext] method. | |
if (_language != null) return const SizedBox.shrink(); | |
return super.visitText(text, preferredStyle); | |
} | |
@override | |
Widget? visitElementAfterWithContext( | |
BuildContext context, | |
md.Element element, | |
TextStyle? preferredStyle, | |
TextStyle? parentStyle, | |
) { | |
// If the language is set, we can display the code block with syntax | |
// highlighting. | |
if (_language case final language?) { | |
final streamChatTheme = StreamChatTheme.of(context); | |
final highlightTheme = switch (Theme.of(context).brightness) { | |
Brightness.dark => atomOneDarkTheme, | |
Brightness.light => atomOneLightTheme, | |
}; | |
final textContent = element.textContent.trim(); | |
final languageId = allLanguages[language]?.id ?? 'plaintext'; | |
final preScrollController = ScrollController(); | |
return IntrinsicWidth( | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
crossAxisAlignment: CrossAxisAlignment.stretch, | |
children: [ | |
CodeBlockHeader( | |
language: language, | |
padding: const EdgeInsets.symmetric(horizontal: 8), | |
onCopyPressed: () => onCopyPressed?.call(textContent), | |
textStyle: streamChatTheme.otherMessageTheme.messageTextStyle, | |
), | |
Divider( | |
height: 0, | |
color: streamChatTheme.otherMessageTheme.messageBorderColor, | |
), | |
Scrollbar( | |
controller: preScrollController, | |
child: SingleChildScrollView( | |
controller: preScrollController, | |
scrollDirection: Axis.horizontal, | |
padding: styleSheet?.codeblockPadding, | |
child: HighlightView( | |
textContent, | |
languageId: languageId, | |
padding: const EdgeInsets.all(8), | |
theme: highlightTheme, | |
textStyle: preferredStyle?.copyWith( | |
fontFamily: 'monospace', | |
fontSize: preferredStyle.fontSize! * 0.85, | |
), | |
), | |
), | |
) | |
], | |
), | |
); | |
} | |
return super.visitElementAfterWithContext( | |
context, | |
element, | |
preferredStyle, | |
parentStyle, | |
); | |
} | |
} | |
/// {@template codeBlockHeader} | |
/// A widget that displays the header of a code block. | |
/// {@endtemplate} | |
class CodeBlockHeader extends StatelessWidget { | |
/// {@macro codeBlockHeader} | |
const CodeBlockHeader({ | |
super.key, | |
required this.language, | |
this.padding, | |
this.textStyle, | |
this.onCopyPressed, | |
this.backgroundColor, | |
}); | |
/// The language of the code block. | |
final String language; | |
/// The style to use for the text. | |
final TextStyle? textStyle; | |
/// The color of the background. | |
final Color? backgroundColor; | |
/// The padding around the header. | |
final EdgeInsetsGeometry? padding; | |
/// A callback that is called when the copy button is pressed. | |
final VoidCallback? onCopyPressed; | |
@override | |
Widget build(BuildContext context) { | |
return Container( | |
padding: padding, | |
color: backgroundColor, | |
child: Row( | |
mainAxisSize: MainAxisSize.min, | |
mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
children: [ | |
Text( | |
language, | |
style: textStyle, | |
), | |
CopyCodeButton( | |
onPressed: onCopyPressed, | |
), | |
], | |
), | |
); | |
} | |
} | |
/// {@template copyCodeButton} | |
/// A button that copies the code to the clipboard when pressed. | |
/// | |
/// The button displays a check icon when the code is copied successfully and | |
/// resets to the copy icon after 2 seconds. | |
/// {@endtemplate} | |
class CopyCodeButton extends StatefulWidget { | |
/// {@macro copyCodeButton} | |
const CopyCodeButton({ | |
super.key, | |
this.onPressed, | |
}); | |
/// A callback that is called when the button is pressed. | |
final VoidCallback? onPressed; | |
@override | |
State<CopyCodeButton> createState() => _CopyCodeButtonState(); | |
} | |
class _CopyCodeButtonState extends State<CopyCodeButton> { | |
bool _isCopied = false; | |
void _copyText() { | |
widget.onPressed?.call(); | |
setState(() => _isCopied = true); | |
// Reset the icon after 2 seconds | |
Future.delayed(const Duration(seconds: 2), () { | |
if (mounted) setState(() => _isCopied = false); | |
}); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return IconButton( | |
iconSize: 16, | |
icon: switch (_isCopied) { | |
true => const Icon(Icons.check_rounded), | |
false => const Icon(Icons.copy_all_rounded), | |
}, | |
onPressed: switch ((widget.onPressed, _isCopied)) { | |
(null, _) || (_, true) => null, | |
_ => _copyText, | |
}, | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment