Skip to content

Instantly share code, notes, and snippets.

@xsahil03x
Created December 4, 2024 12:36
Show Gist options
  • Save xsahil03x/7c2385f7a7348e422634ef6c58e4386c to your computer and use it in GitHub Desktop.
Save xsahil03x/7c2385f7a7348e422634ef6c58e4386c to your computer and use it in GitHub Desktop.
CodeSyntaxHighlightBuilder
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