saminsohag

readme updated

# 📦 GPT Markdown & LaTeX for Flutter
![Pub Version](https://img.shields.io/pub/v/gpt_markdown)
[![Pub Version](https://img.shields.io/pub/v/gpt_markdown)](https://pub.dev/packages/gpt_markdown) [![Pub Likes](https://img.shields.io/pub/likes/gpt_markdown)](https://pub.dev/packages/gpt_markdown) [![Pub Points](https://img.shields.io/pub/points/gpt_markdown)](https://pub.dev/packages/gpt_markdown) [![GitHub](https://img.shields.io/badge/github-gpt__markdown-blue?logo=github)](https://github.com/Infinitix-LLC/gpt_markdown)
A comprehensive Flutter package for rendering rich Markdown and LaTeX content in your apps, designed for seamless integration with AI outputs like ChatGPT and Gemini.
... ... @@ -8,71 +8,106 @@ A comprehensive Flutter package for rendering rich Markdown and LaTeX content in
---
## Supported Markdown Features
| ✨ Feature | ✅ Supported | 🔜 Upcoming |
| --- | --- | --- |
| 💻 Code Block | ✅ | |
| 📊 Table | ✅ | |
| 📝 Heading | ✅ | |
| 📌 Unordered List | ✅ | |
| 📋 Ordered List | ✅ | |
| 🔘 Radio Button | ✅ | |
| ☑️ Check Box | ✅ | |
| ➖ Horizontal Line | ✅ | |
| 🔢 Latex Math | ✅ | |
| ↩️ Indent | ✅ |
| 🖼️ Image | ✅ |
| ✨ Highlighted Text | ✅ |
| ✂️ Striked Text | ✅ |
| 🔵 Bold Text | ✅ |
| 📜 Italic Text | ✅ |
| 🔗 Links | ✅ |
| 📎 Underline | | 🔜 |
| 🧩 Custom components | | 🔜 |
## ✨ Key Features
Render a wide variety of content with full Markdown and LaTeX support, including:
- List
- Unordered list item
1. Ordered list item
```
- Unordered list item
1. Ordered list item
```
- Horizontal line
---
```
---
```
- Links
[<text here>](<href>)
```
[<text here>](<href>)
```
- Images with size
![<with>x<hight> someText](url)
```
![<with>x<hight> someText](url)
```
- Table
```
| Name | Roll |
|-------------|-------------|
| sohag | 1 |
```
| Name | Roll |
|-------------|-------------|
| sohag | 1 |
- Striked text
```
| Name | Roll |
|-------|------|
| sohag | 1 |
~~striked text~~
```
- Bold text
| Name | Roll |
|-------|------|
| sohag | 1 |
**Bold text**
- ~~Striked text~~
```
~~striked text~~
```
- Italic text
- **Bold text**
```
**Bold text**
```
*Italic text*
- *Italic text*
```
*Italic text*
```
- heading texts
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6
```
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6
```
- Latex formula `\(\frac a b\)` or `\[\frac ab\]`
\(\frac a b\)
```
\(\frac a b\)
```
- Radio button and checkbox
() Unchecked radio
(x) Checked radio
[] Unchecked checkbox
[x] Checked checkbox
```
() Unchecked radio
(x) Checked radio
[] Unchecked checkbox
[x] Checked checkbox
```
- You can also make the content selectable using `SelectiionArea` widget.
- You can also make the content selectable using `SelectionArea` widget.
## 🚀 Why Use GPT Markdown?
... ... @@ -90,7 +125,7 @@ flutter pub add gpt_markdown
## 📖 Usage
Check the documentation [here.](https://github.com/saminsohag/flutter_packages/tree/main/gpt_markdown/example)
Check the documentation [here.](https://github.com/Infinitix-LLC/gpt_markdown/tree/main/example)
```dart
import 'package:flutter/material.dart';
... ... @@ -145,6 +180,8 @@ You can also use LaTeX for mathematical expressions. Here's an example:
Markdown and LaTeX can be powerful tools for formatting text and mathematical expressions in your Flutter app. If you have any questions or need further assistance, feel free to ask!
```
### Output from gpt_markdown
<img width="614" alt="Screenshot 2024-02-15 at 4 13 59 AM" src="https://github.com/saminsohag/flutter_packages/assets/59507062/8f4a4068-a12c-45d1-a954-ebaf3822e754">
... ...
... ... @@ -73,3 +73,55 @@ class _MyHomePageState extends State<MyHomePage> {
}
```
Use `SelectableAdapter` to make any non selectable widget selectable.
```dart
SelectableAdapter(
selectedText: 'sin(x^2)',
child: Math.tex('sin(x^2)'),
);
```
Use `GptMarkdownTheme` widget and `GptMarkdownThemeData` to customize the GptMarkdown.
```dart
GptMarkdownTheme(
data: GptMarkdownThemeData.of(context).copyWith(
highlightColor: Colors.red,
),
child: GptMarkdown(
text,
),
);
```
In theme extension you can use `GptMarkdownThemeData` to customize the GptMarkdown.
```dart
theme: ThemeData(
useMaterial3: true,
brightness: Brightness.light,
colorSchemeSeed: Colors.blue,
extensions: [
GptMarkdownThemeData(
brightness: Brightness.light,
highlightColor: Colors.red,
),
],
),
darkTheme: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorSchemeSeed: Colors.blue,
extensions: [
GptMarkdownThemeData(
brightness: Brightness.dark,
highlightColor: Colors.red,
),
],
),
```
Please see the [README.md](https://github.com/Infinitix-LLC/gpt_markdown) and also [example](https://github.com/Infinitix-LLC/gpt_markdown/tree/main/example/lib/main.dart) app for more details.
... ...
... ... @@ -2,10 +2,10 @@ import 'dart:io';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:flutter/material.dart';
import 'package:gpt_markdown/custom_widgets/selectable_adapter.dart';
import 'package:gpt_markdown/gpt_markdown.dart';
import 'package:flutter_math_fork/flutter_math.dart';
import 'package:watcher/watcher.dart';
import 'selectable_adapter.dart';
void main() {
runApp(const MyApp());
... ...
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class SelectableAdapter extends StatelessWidget {
const SelectableAdapter(
{super.key, required this.selectedText, required this.child});
final Widget child;
final String selectedText;
@override
Widget build(BuildContext context) {
final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context);
if (registrar == null) {
return child;
}
return MouseRegion(
cursor: SystemMouseCursors.text,
child: _SelectableAdapter(
registrar: registrar,
selectedText: selectedText,
child: child,
),
);
}
}
class _SelectableAdapter extends SingleChildRenderObjectWidget {
const _SelectableAdapter({
required this.registrar,
required Widget child,
required this.selectedText,
}) : super(child: child);
final SelectionRegistrar registrar;
final String selectedText;
@override
_RenderSelectableAdapter createRenderObject(BuildContext context) {
return _RenderSelectableAdapter(
DefaultSelectionStyle.of(context).selectionColor!,
selectedText,
registrar,
);
}
@override
void updateRenderObject(
BuildContext context, _RenderSelectableAdapter renderObject) {
renderObject
..selectionColor = DefaultSelectionStyle.of(context).selectionColor!
..registrar = registrar;
}
}
class _RenderSelectableAdapter extends RenderProxyBox
with Selectable, SelectionRegistrant {
String selectionText;
_RenderSelectableAdapter(
Color selectionColor,
this.selectionText,
SelectionRegistrar registrar,
) : _selectionColor = selectionColor,
_geometry = ValueNotifier<SelectionGeometry>(_noSelection) {
this.registrar = registrar;
_geometry.addListener(markNeedsPaint);
}
static const SelectionGeometry _noSelection =
SelectionGeometry(status: SelectionStatus.none, hasContent: true);
final ValueNotifier<SelectionGeometry> _geometry;
Color get selectionColor => _selectionColor;
late Color _selectionColor;
set selectionColor(Color value) {
if (_selectionColor == value) {
return;
}
_selectionColor = value;
markNeedsPaint();
}
// ValueListenable APIs
@override
void addListener(VoidCallback listener) => _geometry.addListener(listener);
@override
void removeListener(VoidCallback listener) =>
_geometry.removeListener(listener);
@override
SelectionGeometry get value => _geometry.value;
// Selectable APIs.
@override
List<Rect> get boundingBoxes => <Rect>[paintBounds];
// Adjust this value to enlarge or shrink the selection highlight.
static const double _padding = 0.0;
Rect _getSelectionHighlightRect() {
return Rect.fromLTWH(0 - _padding, 0 - _padding, size.width + _padding * 2,
size.height + _padding * 2);
}
Offset? _start;
Offset? _end;
void _updateGeometry() {
if (_start == null || _end == null) {
_geometry.value = _noSelection;
return;
}
final Rect renderObjectRect = Rect.fromLTWH(0, 0, size.width, size.height);
final Rect selectionRect = Rect.fromPoints(_start!, _end!);
if (renderObjectRect.intersect(selectionRect).isEmpty) {
_geometry.value = _noSelection;
} else {
final Rect selectionRect = _getSelectionHighlightRect();
final SelectionPoint firstSelectionPoint = SelectionPoint(
localPosition: selectionRect.bottomLeft,
lineHeight: selectionRect.size.height,
handleType: TextSelectionHandleType.left,
);
final SelectionPoint secondSelectionPoint = SelectionPoint(
localPosition: selectionRect.bottomRight,
lineHeight: selectionRect.size.height,
handleType: TextSelectionHandleType.right,
);
final bool isReversed;
if (_start!.dy > _end!.dy) {
isReversed = true;
} else if (_start!.dy < _end!.dy) {
isReversed = false;
} else {
isReversed = _start!.dx > _end!.dx;
}
_geometry.value = SelectionGeometry(
status: SelectionStatus.uncollapsed,
hasContent: true,
startSelectionPoint:
isReversed ? secondSelectionPoint : firstSelectionPoint,
endSelectionPoint:
isReversed ? firstSelectionPoint : secondSelectionPoint,
selectionRects: <Rect>[selectionRect],
);
}
}
@override
SelectionResult dispatchSelectionEvent(SelectionEvent event) {
SelectionResult result = SelectionResult.none;
switch (event.type) {
case SelectionEventType.startEdgeUpdate:
case SelectionEventType.endEdgeUpdate:
final Rect renderObjectRect =
Rect.fromLTWH(0, 0, size.width, size.height);
// Normalize offset in case it is out side of the rect.
final Offset point =
globalToLocal((event as SelectionEdgeUpdateEvent).globalPosition);
final Offset adjustedPoint =
SelectionUtils.adjustDragOffset(renderObjectRect, point);
if (event.type == SelectionEventType.startEdgeUpdate) {
_start = adjustedPoint;
} else {
_end = adjustedPoint;
}
result = SelectionUtils.getResultBasedOnRect(renderObjectRect, point);
case SelectionEventType.clear:
_start = _end = null;
case SelectionEventType.selectAll:
case SelectionEventType.selectWord:
case SelectionEventType.selectParagraph:
_start = Offset.zero;
_end = Offset.infinite;
case SelectionEventType.granularlyExtendSelection:
result = SelectionResult.end;
final GranularlyExtendSelectionEvent extendSelectionEvent =
event as GranularlyExtendSelectionEvent;
// Initialize the offset it there is no ongoing selection.
if (_start == null || _end == null) {
if (extendSelectionEvent.forward) {
_start = _end = Offset.zero;
} else {
_start = _end = Offset.infinite;
}
}
// Move the corresponding selection edge.
final Offset newOffset =
extendSelectionEvent.forward ? Offset.infinite : Offset.zero;
if (extendSelectionEvent.isEnd) {
if (newOffset == _end) {
result = extendSelectionEvent.forward
? SelectionResult.next
: SelectionResult.previous;
}
_end = newOffset;
} else {
if (newOffset == _start) {
result = extendSelectionEvent.forward
? SelectionResult.next
: SelectionResult.previous;
}
_start = newOffset;
}
case SelectionEventType.directionallyExtendSelection:
result = SelectionResult.end;
final DirectionallyExtendSelectionEvent extendSelectionEvent =
event as DirectionallyExtendSelectionEvent;
// Convert to local coordinates.
final double horizontalBaseLine = globalToLocal(Offset(event.dx, 0)).dx;
final Offset newOffset;
final bool forward;
switch (extendSelectionEvent.direction) {
case SelectionExtendDirection.backward:
case SelectionExtendDirection.previousLine:
forward = false;
// Initialize the offset it there is no ongoing selection.
if (_start == null || _end == null) {
_start = _end = Offset.infinite;
}
// Move the corresponding selection edge.
if (extendSelectionEvent.direction ==
SelectionExtendDirection.previousLine ||
horizontalBaseLine < 0) {
newOffset = Offset.zero;
} else {
newOffset = Offset.infinite;
}
case SelectionExtendDirection.nextLine:
case SelectionExtendDirection.forward:
forward = true;
// Initialize the offset it there is no ongoing selection.
if (_start == null || _end == null) {
_start = _end = Offset.zero;
}
// Move the corresponding selection edge.
if (extendSelectionEvent.direction ==
SelectionExtendDirection.nextLine ||
horizontalBaseLine > size.width) {
newOffset = Offset.infinite;
} else {
newOffset = Offset.zero;
}
}
if (extendSelectionEvent.isEnd) {
if (newOffset == _end) {
result = forward ? SelectionResult.next : SelectionResult.previous;
}
_end = newOffset;
} else {
if (newOffset == _start) {
result = forward ? SelectionResult.next : SelectionResult.previous;
}
_start = newOffset;
}
}
_updateGeometry();
return result;
}
// This method is called when users want to copy selected content in this
// widget into clipboard.
@override
SelectedContent? getSelectedContent() {
return value.hasSelection
? SelectedContent(plainText: selectionText)
: null;
}
LayerLink? _startHandle;
LayerLink? _endHandle;
@override
void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle) {
if (_startHandle == startHandle && _endHandle == endHandle) {
return;
}
_startHandle = startHandle;
_endHandle = endHandle;
markNeedsPaint();
}
@override
void paint(PaintingContext context, Offset offset) {
super.paint(context, offset);
if (!_geometry.value.hasSelection) {
return;
}
// Draw the selection highlight.
final Paint selectionPaint = Paint()
..style = PaintingStyle.fill
..color = _selectionColor;
context.canvas
.drawRect(_getSelectionHighlightRect().shift(offset), selectionPaint);
// Push the layer links if any.
if (_startHandle != null) {
context.pushLayer(
LeaderLayer(
link: _startHandle!,
offset: offset + value.startSelectionPoint!.localPosition,
),
(PaintingContext context, Offset offset) {},
Offset.zero,
);
}
if (_endHandle != null) {
context.pushLayer(
LeaderLayer(
link: _endHandle!,
offset: offset + value.endSelectionPoint!.localPosition,
),
(PaintingContext context, Offset offset) {},
Offset.zero,
);
}
}
@override
void dispose() {
_geometry.dispose();
super.dispose();
}
}