streaming_markdown 0.0.1
streaming_markdown: ^0.0.1 copied to clipboard
A production-ready Flutter package for animated markdown rendering with ChatGPT-style typing animation, supporting customizable animation speed, chunk size, multiple animation modes, and an extensible [...]
Animated Markdown Renderer #
A production-ready Flutter package for animated markdown rendering with ChatGPT-style typing animation, supporting customizable animation speed, chunk size, multiple animation modes, and an extensible custom widget system.
Features #
- ✨ ChatGPT-style typing animation - Smooth, configurable typing effects
- 📡 Real-time streaming support - Stream markdown content as it arrives
- 🎨 Multiple animation modes - Character, word, token, or custom chunk-based
- 🎯 Preset configurations - Pre-configured styles matching popular AI assistants (ChatGPT, Claude, Grok, etc.)
- 🧩 Extensible custom widgets - Easy to add custom inline widgets with custom syntax patterns
- 📝 Full Markdown support - All standard markdown features
- 🎯 Inline widgets in lists - Custom widgets work seamlessly within list items
- ⚡ Performance optimized - Automatic throttling for large texts
- 🎛️ Fully configurable - Control speed, chunk size, and more
Installation #
Add this to your package's pubspec.yaml file:
dependencies:
streaming_markdown:
path: ../streaming_markdown # or use pub.dev version when published
Quick Start #
import 'package:streaming_markdown/streaming_markdown.dart';
AnimatedMarkdown(
markdown: '''
# Hello World
This is **animated** markdown with [[button:Click Me]] inline!
- List item with [[chip:Tag]] inline widget
- Another item with [[button:Action]] button
- Nested list with [[chip:Nested]] chip
''',
config: AnimationConfig(
mode: AnimationMode.word,
wordDelay: Duration(milliseconds: 50),
chunkSize: 5,
),
customBuilders: {
'button': ButtonBuilder(),
'chip': ChipBuilder(),
},
)
Usage #
Basic Usage #
AnimatedMarkdown(
markdown: '# Hello\nThis is animated markdown!',
)
Custom Animation Configuration #
AnimatedMarkdown(
markdown: 'Your markdown here',
config: AnimationConfig(
mode: AnimationMode.character, // or .word, .token, .custom
charDelay: Duration(milliseconds: 15),
wordDelay: Duration(milliseconds: 50),
tokenDelay: Duration(milliseconds: 30),
chunkSize: 1, // For batch rendering
throttleThreshold: 1000, // Performance optimization
),
)
Streaming Content #
AnimatedMarkdown supports real-time streaming of markdown content via a Stream<String>. This is perfect for LLM responses, API streams, or any real-time content delivery. The widget automatically animates content as it arrives from the stream.
Basic Streaming:
Stream<String> markdownStream = getMarkdownStream();
AnimatedMarkdown(
stream: markdownStream,
config: AnimationConfig(
mode: AnimationMode.token, // Token mode is optimal for streaming
tokenDelay: Duration(milliseconds: 30),
),
)
With Custom Builders:
AnimatedMarkdown(
stream: markdownStream,
config: AnimationConfig.chatGPT,
customBuilders: {
'button': ButtonBuilder(),
'chip': ChipBuilder(),
},
)
Creating a Stream:
Stream<String> createMarkdownStream() {
final controller = StreamController<String>();
// Simulate streaming chunks
Future.microtask(() async {
final chunks = ['# Hello\n', 'This is **streamed** ', 'content!\n'];
for (final chunk in chunks) {
await Future.delayed(Duration(milliseconds: 100));
controller.add(chunk);
}
await controller.close();
});
return controller.stream;
}
The controller automatically handles stream completion and continues animating until all content is displayed. See the example/ directory for a complete streaming example.
Animation Modes #
Character Mode
Smooth, character-by-character animation:
AnimationConfig(
mode: AnimationMode.character,
charDelay: Duration(milliseconds: 15),
)
Word Mode
Faster, word-by-word animation (default):
AnimationConfig(
mode: AnimationMode.word,
wordDelay: Duration(milliseconds: 50),
)
Token Mode
Optimal for LLM streaming:
AnimationConfig(
mode: AnimationMode.token,
tokenDelay: Duration(milliseconds: 30),
)
Custom Mode
Configurable chunk-based rendering:
AnimationConfig(
mode: AnimationMode.custom,
chunkSize: 10, // Characters per chunk
charDelay: Duration(milliseconds: 20),
)
Preset Configurations #
AnimationConfig provides preset configurations that match popular AI assistant typing styles. These presets are optimized for different use cases:
ChatGPT Style: Character-based animation with smooth unit animations and slide effects:
AnimatedMarkdown(
markdown: 'Your content',
config: AnimationConfig.chatGPT,
)
Claude Style: Word-based animation with faster timing, optimized for quick responses:
AnimatedMarkdown(
markdown: 'Your content',
config: AnimationConfig.claude,
)
Grok Style: Fast token-based animation optimized for quick responses and real-time interactions:
AnimatedMarkdown(
markdown: 'Your content',
config: AnimationConfig.grok,
)
Perplexity Style: Fast word-based animation optimized for quick information delivery:
AnimatedMarkdown(
markdown: 'Your content',
config: AnimationConfig.perplexity,
)
Gemini Style: Moderate word-based animation with slightly slower pacing:
AnimatedMarkdown(
markdown: 'Your content',
config: AnimationConfig.gemini,
)
Copilot Style: Very fast token-based animation optimized for code generation and technical content:
AnimatedMarkdown(
markdown: 'Your content',
config: AnimationConfig.copilot,
)
All Available Presets:
AnimationConfig.chatGPT- Character mode with smooth animationsAnimationConfig.claude- Word mode with fast timingAnimationConfig.grok- Token mode for quick responsesAnimationConfig.perplexity- Word mode for information deliveryAnimationConfig.gemini- Word mode with moderate pacingAnimationConfig.copilot- Token mode for code generation
Custom Widgets #
Inline Widgets
Inline widgets work within text and list items:
Buttons:
AnimatedMarkdown(
markdown: 'Click [[button:Click Me]] here',
customBuilders: {
'button': ButtonBuilder(
onPressed: (label) => print('Button pressed: $label'),
),
},
)
Chips:
AnimatedMarkdown(
markdown: 'Tag: [[chip:Important]]',
customBuilders: {
'chip': ChipBuilder(
onPressed: (label) => print('Chip pressed: $label'),
color: Colors.blue,
),
},
)
In List Items:
AnimatedMarkdown(
markdown: '''
- Item with [[button:Action]] button
- Another with [[chip:Tag]] chip
- Nested with [[button:Nested]] button
''',
customBuilders: {
'button': ButtonBuilder(),
'chip': ChipBuilder(),
},
)
Manual Control #
Control animation by setting autoStart: false and using a MarkdownTypingController directly:
final controller = MarkdownTypingController(
fullText: 'Your text',
config: AnimationConfig(mode: AnimationMode.word),
);
// Start animation
controller.start();
// Control animation
controller.pause();
controller.resume();
controller.stop();
controller.reset();
controller.jumpToEnd();
Creating Custom Builders #
The package provides a flexible builder system for creating custom markdown widgets. There are two base classes you can extend:
CustomMarkdownBuilder #
The base class for all custom builders. Use this when you need full control over widget building:
class MyCustomBuilder extends CustomMarkdownBuilder {
@override
Widget? buildWidget(md.Element element, TextStyle? style) {
return Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(4),
),
child: Text(element.textContent),
);
}
}
InlineBuilder #
Extend InlineBuilder for inline widgets that appear within text and list items. This is the recommended approach for most custom widgets:
class MyInlineBuilder extends InlineBuilder {
@override
Widget buildInline(md.Element element, TextStyle? style) {
return Container(
padding: EdgeInsets.all(4),
child: Text(element.textContent),
);
}
}
Custom Syntax Patterns #
By default, custom builders use the pattern [[key:content]]. You can customize this pattern using the customSyntaxPatterns parameter:
AnimatedMarkdown(
markdown: 'Citation [1] and [2] in text',
customBuilders: {
'citation': CitationBuilder(),
},
customSyntaxPatterns: {
'citation': r'\[(\d+)\]', // Custom regex pattern
},
)
The custom pattern should include at least one capture group to extract the content. The first capture group will be used as the content.
Example: Citation Builder with Custom Pattern
class CitationBuilder extends InlineBuilder {
final Function(String)? onPressed;
CitationBuilder({this.onPressed});
@override
Widget buildInline(md.Element element, TextStyle? style) {
return GestureDetector(
onTap: () => onPressed?.call(element.textContent),
child: Text(
'[${element.textContent}]',
style: style?.copyWith(
color: Colors.blue,
decoration: TextDecoration.underline,
),
),
);
}
}
// Usage with custom pattern
AnimatedMarkdown(
markdown: 'This is content with citation [1] and [2].',
customBuilders: {
'citation': CitationBuilder(
onPressed: (number) => print('Citation $number clicked'),
),
},
customSyntaxPatterns: {
'citation': r'\[(\d+)\]', // Matches [1], [2], etc.
},
)
Custom Syntax (Advanced) #
For more complex syntax requirements, you can create a custom syntax parser:
class MyInlineSyntax extends CustomInlineSyntax {
MyInlineSyntax() : super(RegExp(r'\[\[mywidget:(.*?)\]\]'));
@override
md.Node parseMatch(md.InlineParser parser, Match match) {
final String content = match.group(1)?.trim() ?? '';
return md.Element.text('mywidget', content);
}
}
However, for most use cases, using customSyntaxPatterns with InlineBuilder is simpler and recommended.
Registering Builders #
Register your custom builders in AnimatedMarkdown or MarkdownRenderer:
AnimatedMarkdown(
markdown: 'Content with [[mywidget:Custom Content]]',
customBuilders: {
'mywidget': MyInlineBuilder(),
},
customSyntaxPatterns: {
'mywidget': r'\[\[mywidget:(.*?)\]\]', // Optional custom pattern
},
)
API Reference #
AnimatedMarkdown #
Main widget for animated markdown rendering.
Properties:
markdown(String?) - Markdown text to animate (eithermarkdownorstreammust be provided)stream(Streamconfig(AnimationConfig?) - Animation configurationcustomBuilders(Map<String, CustomMarkdownBuilder>?) - Custom widget builderscustomSyntaxPatterns(Map<String, String>?) - Custom regex patterns for builders. Maps builder keys to their regex patterns. If a builder key has a custom pattern, it will be used instead of the default[[key:content]]patternselectable(bool) - Whether text is selectable (default: true)styleSheet(MarkdownStyleSheet?) - Custom markdown style sheetextensionSet(ExtensionSet?) - Custom extension set for markdown parsing. If not provided, uses GitHub Flavored Markdown as defaultsyntaxHighlighter(SyntaxHighlighter?) - Syntax highlighter for code blocksonTapLink(MarkdownTapLinkCallback?) - Callback for handling link tapsshrinkWrap(bool) - Whether the widget should take the minimum height that wraps its content (default: true)softLineBreak(bool) - Whether to handle soft line breaks (default: false)autoStart(bool) - Whether to auto-start animation (default: true)
AnimationConfig #
Configuration for typing animation.
Properties:
mode(AnimationMode) - Animation mode (default: AnimationMode.word)charDelay(Duration) - Delay between characters (default: 15ms)wordDelay(Duration) - Delay between words (default: 50ms)tokenDelay(Duration) - Delay between tokens (default: 30ms)chunkSize(int) - Chunk size for batch rendering (default: 1)throttleThreshold(int) - Threshold for performance throttling (default: 1000)
MarkdownTypingController #
Controller for managing typing animation. Supports both static text and streaming content.
Constructor:
MarkdownTypingController({String? fullText, Stream<String>? stream, AnimationConfig? config})- Creates a controller with either static text (fullText) or a stream (stream). Exactly one must be provided.
Methods:
start()- Start the animationpause()- Pause the animationresume()- Resume the animationstop()- Stop the animationreset()- Reset to beginningjumpToEnd()- Jump to end immediately
Properties:
fullText(String) - Full text to animate (accumulated text from stream or initial text)text(String) - Current displayed textisRunning(bool) - Whether animation is runningisPaused(bool) - Whether animation is pausedprogress(double) - Progress (0.0 to 1.0)isComplete(bool) - Whether animation is complete (for streams, checks if stream is completed)
MarkdownRenderer #
Renders markdown with custom builders and syntaxes.
Properties:
data(String, required) - Markdown text to rendercustomBuilders(Map<String, CustomMarkdownBuilder>?) - Custom widget builderscustomSyntaxPatterns(Map<String, String>?) - Custom regex patterns for builders. Maps builder keys to their regex patterns. If a builder key has a custom pattern, it will be used instead of the default[[key:content]]patternselectable(bool) - Whether text is selectable (default: true)styleSheet(MarkdownStyleSheet?) - Custom markdown style sheetanimationConfig(AnimationConfig?) - Animation configuration for unit-level animationsextensionSet(ExtensionSet?) - Custom extension set for markdown parsing. If not provided, uses GitHub Flavored Markdown as defaultsyntaxHighlighter(SyntaxHighlighter?) - Syntax highlighter for code blocksonTapLink(MarkdownTapLinkCallback?) - Callback for handling link tapsshrinkWrap(bool) - Whether the widget should take the minimum height that wraps its content (default: true)softLineBreak(bool) - Whether to handle soft line breaks (default: false)
Built-in Builders #
ButtonBuilder- Inline button widgetChipBuilder- Inline chip/tag widgetMathBuilder- Math/LaTeX widget (placeholder)
Builder Base Classes #
CustomMarkdownBuilder- Base class for all custom markdown buildersInlineBuilder- Base class for inline widgets that appear within text and list items
Performance Tips #
- Use word mode for faster animation on large texts
- Increase chunk size for batch rendering
- Set throttleThreshold to enable automatic throttling for very long texts
- Disable autoStart and control manually for better performance
Examples #
See the example/ directory for complete examples.
Contributing #
Contributions are welcome! Please feel free to submit a Pull Request.
License #
This project is licensed under the MIT License.