顾海波

Merge remote-tracking branch 'github/main'

# Conflicts:
#	example/pubspec.lock
#	lib/gpt_markdown.dart
#	lib/markdown_component.dart
#	pubspec.yaml
  1 +{
  2 + "cSpell.words": [
  3 + "cupertino"
  4 + ]
  5 +}
  1 +## 1.1.4
  2 +
  3 +* 🔗 Fixed vertical alignment issue with link text rendering ([#92](https://github.com/Infinitix-LLC/gpt_markdown/issues/92))
  4 +* 📝 Resolved "null" rendering issue in ordered lists with multiple spaces and line breaks ([#89](https://github.com/Infinitix-LLC/gpt_markdown/issues/89))
  5 +* 🧹 Removed erroneous `trim()` from `CodeBlockMd` to preserve necessary whitespace in code blocks ([#99](https://github.com/Infinitix-LLC/gpt_markdown/issues/99))
  6 +* 🎨 Fixed heading style customization issue where custom colors in heading styles were not being applied ([#95](https://github.com/Infinitix-LLC/gpt_markdown/issues/95))
  7 +
  8 +## 1.1.3
  9 +
  10 +* Added `RadioGroup` widget for managing radio buttons.
  11 +* Updated to align with Flutter 3.35 by resolving the deprecations of `Radio.groupValue` and `Radio.onChanged`.
  12 +
  13 +## 1.1.2
  14 +
  15 +* 📊 Fixed table column alignment support ([#65](https://github.com/Infinitix-LLC/gpt_markdown/issues/65))
  16 +* 🎨 Added `tableBuilder` parameter to customize table rendering
  17 +* 🔗 Fixed text decoration color of link markdown component
  18 +
  19 +## 1.1.1
  20 +
  21 +* 🖼️ Fixed issue where images wrapped in links (e.g. `[![](img)](url)`) were not rendering properly (#72)
  22 +* 🔗 Resolved parsing errors for consecutive inline links without spacing (e.g. `[a](url)[b](url)`) (#34)
  23 +
  24 +## 1.1.0
  25 +
  26 +* Changed `onLinkTab` to `onLinkTap` fixed issues of newLine issues.
  27 +
  28 +## 1.0.20
  29 +
  30 +* Fix: support balanced parentheses in image and link URLs. [#68](https://github.com/Infinitix-LLC/gpt_markdown/pull/68)
  31 +
  32 +## 1.0.19
  33 +
  34 +* Performance improvements.
  35 +
  36 +## 1.0.18
  37 +
  38 +* dollarSignForLatex is added and by default it is false.
  39 +
  40 +## 1.0.17
  41 +
  42 +* Bloc components rendering inside table.
  43 +
1 ## 1.0.16 44 ## 1.0.16
2 45
3 * `IndentMd` and `BlockQuote` fixed. 46 * `IndentMd` and `BlockQuote` fixed.
@@ -4,6 +4,8 @@ @@ -4,6 +4,8 @@
4 4
5 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. 5 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.
6 6
  7 +gpt_markdown is a drop-in replacement for flutter_markdown, offering extended support for LaTeX, custom builders, and better AI integration for Flutter apps.
  8 +
7 ⭐ If you find this package helpful, please give it a like on [pub.dev](https://pub.dev/packages/gpt_markdown)! Your support means a lot! ⭐ 9 ⭐ If you find this package helpful, please give it a like on [pub.dev](https://pub.dev/packages/gpt_markdown)! Your support means a lot! ⭐
8 10
9 --- 11 ---
@@ -30,7 +32,7 @@ A comprehensive Flutter package for rendering rich Markdown and LaTeX content in @@ -30,7 +32,7 @@ A comprehensive Flutter package for rendering rich Markdown and LaTeX content in
30 | 🔗 Links | ✅ | 32 | 🔗 Links | ✅ |
31 | 📱 Selectable | ✅ | 33 | 📱 Selectable | ✅ |
32 | 🧩 Custom components | ✅ | | 34 | 🧩 Custom components | ✅ | |
33 -| 📎 Underline | | 🔜 | 35 +| 📎 Underline | ✅ | |
34 36
35 ## ✨ Key Features 37 ## ✨ Key Features
36 38
@@ -84,6 +86,11 @@ Render a wide variety of content with full Markdown and LaTeX support, including @@ -84,6 +86,11 @@ Render a wide variety of content with full Markdown and LaTeX support, including
84 *Italic text* 86 *Italic text*
85 ``` 87 ```
86 88
  89 +- <u>Underline text</u>
  90 +```
  91 +<u>Underline text</u>
  92 +```
  93 +
87 - heading texts 94 - heading texts
88 95
89 ``` 96 ```
@@ -139,6 +146,7 @@ return GptMarkdown( @@ -139,6 +146,7 @@ return GptMarkdown(
139 ''', 146 ''',
140 style: const TextStyle( 147 style: const TextStyle(
141 color: Colors.red, 148 color: Colors.red,
  149 + ),
142 ), 150 ),
143 151
144 ``` 152 ```
@@ -186,6 +194,7 @@ Markdown and LaTeX can be powerful tools for formatting text and mathematical ex @@ -186,6 +194,7 @@ Markdown and LaTeX can be powerful tools for formatting text and mathematical ex
186 194
187 <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"> 195 <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">
188 196
  197 +If you're using flutter_markdown and need more customization or LaTeX support, gpt_markdown is a great alternative.
189 198
190 ## 🔗 Additional Information 199 ## 🔗 Additional Information
191 200
@@ -123,5 +123,28 @@ darkTheme: ThemeData( @@ -123,5 +123,28 @@ darkTheme: ThemeData(
123 ), 123 ),
124 ``` 124 ```
125 125
  126 +Use `tableBuilder` to customize table rendering:
  127 +
  128 +```dart
  129 +GptMarkdown(
  130 + markdownText,
  131 + tableBuilder: (context, tableRows, textStyle, config) {
  132 + return Table(
  133 + border: TableBorder.all(
  134 + width: 1,
  135 + color: Colors.red,
  136 + ),
  137 + children: tableRows.map((e) {
  138 + return TableRow(
  139 + children: e.fields.map((e) {
  140 + return Text(e.data);
  141 + }).toList(),
  142 + );
  143 + }).toList(),
  144 + );
  145 + },
  146 +);
  147 +```
  148 +
126 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. 149 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.
127 150
@@ -75,7 +75,15 @@ class MyHomePage extends StatefulWidget { @@ -75,7 +75,15 @@ class MyHomePage extends StatefulWidget {
75 class _MyHomePageState extends State<MyHomePage> { 75 class _MyHomePageState extends State<MyHomePage> {
76 TextDirection _direction = TextDirection.ltr; 76 TextDirection _direction = TextDirection.ltr;
77 final TextEditingController _controller = TextEditingController( 77 final TextEditingController _controller = TextEditingController(
78 - text: r''' 78 + text: r'''This is a sample markdown document.
  79 +* **bold**
  80 +* *italic*
  81 +* **_bold and italic_**
  82 +* ~~strikethrough~~
  83 +* `code`
  84 +* [link](https://www.google.com)
  85 +
  86 +[![alt text](https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png)](link_url)
79 ```markdown 87 ```markdown
80 # Complex Markdown Document for Testing 88 # Complex Markdown Document for Testing
81 89
@@ -317,9 +325,16 @@ This document was created to test the robustness of Markdown parsers and to ensu @@ -317,9 +325,16 @@ This document was created to test the robustness of Markdown parsers and to ensu
317 325
318 bool writingMod = true; 326 bool writingMod = true;
319 bool selectable = false; 327 bool selectable = false;
  328 + bool useDollarSignsForLatex = false;
320 329
321 @override 330 @override
322 Widget build(BuildContext context) { 331 Widget build(BuildContext context) {
  332 +// var data = '''|asdfasfd|asdfasf|
  333 +// |---|---|
  334 +// |sohag|asdfasf|
  335 +// |asdfasf|asdfasf|
  336 +// ''';
  337 +
323 return GptMarkdownTheme( 338 return GptMarkdownTheme(
324 gptThemeData: GptMarkdownTheme.of(context).copyWith( 339 gptThemeData: GptMarkdownTheme.of(context).copyWith(
325 highlightColor: Colors.purple, 340 highlightColor: Colors.purple,
@@ -331,6 +346,22 @@ This document was created to test the robustness of Markdown parsers and to ensu @@ -331,6 +346,22 @@ This document was created to test the robustness of Markdown parsers and to ensu
331 IconButton( 346 IconButton(
332 onPressed: () { 347 onPressed: () {
333 setState(() { 348 setState(() {
  349 + useDollarSignsForLatex = !useDollarSignsForLatex;
  350 + });
  351 + },
  352 + icon: Icon(
  353 + Icons.monetization_on_outlined,
  354 + color: useDollarSignsForLatex
  355 + ? Theme.of(context).colorScheme.onSurfaceVariant
  356 + : Theme.of(context)
  357 + .colorScheme
  358 + .onSurface
  359 + .withValues(alpha: 0.38),
  360 + ),
  361 + ),
  362 + IconButton(
  363 + onPressed: () {
  364 + setState(() {
334 selectable = !selectable; 365 selectable = !selectable;
335 }); 366 });
336 }, 367 },
@@ -383,9 +414,10 @@ This document was created to test the robustness of Markdown parsers and to ensu @@ -383,9 +414,10 @@ This document was created to test the robustness of Markdown parsers and to ensu
383 Expanded( 414 Expanded(
384 child: ListView( 415 child: ListView(
385 children: [ 416 children: [
386 - AnimatedBuilder(  
387 - animation: _controller, 417 + ListenableBuilder(
  418 + listenable: _controller,
388 builder: (context, _) { 419 builder: (context, _) {
  420 + var data = _controller.text;
389 return Container( 421 return Container(
390 padding: const EdgeInsets.all(8), 422 padding: const EdgeInsets.all(8),
391 decoration: BoxDecoration( 423 decoration: BoxDecoration(
@@ -415,16 +447,19 @@ This document was created to test the robustness of Markdown parsers and to ensu @@ -415,16 +447,19 @@ This document was created to test the robustness of Markdown parsers and to ensu
415 child: Builder( 447 child: Builder(
416 builder: (context) { 448 builder: (context) {
417 Widget child = GptMarkdown( 449 Widget child = GptMarkdown(
418 - _controller.text, 450 + data,
419 textDirection: _direction, 451 textDirection: _direction,
420 - onLinkTab: (url, title) { 452 + onLinkTap: (url, title) {
421 debugPrint(url); 453 debugPrint(url);
422 debugPrint(title); 454 debugPrint(title);
423 }, 455 },
  456 + useDollarSignsForLatex:
  457 + useDollarSignsForLatex,
424 textAlign: TextAlign.justify, 458 textAlign: TextAlign.justify,
425 textScaler: const TextScaler.linear(1), 459 textScaler: const TextScaler.linear(1),
426 style: const TextStyle( 460 style: const TextStyle(
427 - fontSize: 15, 461 + // fontFamily: 'monospace',
  462 + // fontWeight: FontWeight.bold,
428 ), 463 ),
429 highlightBuilder: (context, text, style) { 464 highlightBuilder: (context, text, style) {
430 return Container( 465 return Container(
@@ -487,13 +522,13 @@ This document was created to test the robustness of Markdown parsers and to ensu @@ -487,13 +522,13 @@ This document was created to test the robustness of Markdown parsers and to ensu
487 RegExp(r"align\*"), 522 RegExp(r"align\*"),
488 (match) => "aligned"); 523 (match) => "aligned");
489 }, 524 },
490 - imageBuilder: (context, url) {  
491 - return Image.network(  
492 - url,  
493 - width: 100,  
494 - height: 100,  
495 - );  
496 - }, 525 + // imageBuilder: (context, url) {
  526 + // return Image.network(
  527 + // url,
  528 + // width: 100,
  529 + // height: 100,
  530 + // );
  531 + // },
497 latexBuilder: 532 latexBuilder:
498 (context, tex, textStyle, inline) { 533 (context, tex, textStyle, inline) {
499 if (tex.contains(r"\begin{tabular}")) { 534 if (tex.contains(r"\begin{tabular}")) {
@@ -579,47 +614,65 @@ This document was created to test the robustness of Markdown parsers and to ensu @@ -579,47 +614,65 @@ This document was created to test the robustness of Markdown parsers and to ensu
579 }, 614 },
580 linkBuilder: 615 linkBuilder:
581 (context, label, path, style) { 616 (context, label, path, style) {
582 - return Text( 617 + return Text.rich(
583 label, 618 label,
584 style: style.copyWith( 619 style: style.copyWith(
585 color: Colors.blue, 620 color: Colors.blue,
586 ), 621 ),
587 ); 622 );
588 }, 623 },
589 - components: [  
590 - CodeBlockMd(),  
591 - NewLines(),  
592 - BlockQuote(),  
593 - ImageMd(),  
594 - ATagMd(),  
595 - TableMd(),  
596 - HTag(),  
597 - UnOrderedList(),  
598 - OrderedList(),  
599 - RadioButtonMd(),  
600 - CheckBoxMd(),  
601 - HrLine(),  
602 - StrikeMd(),  
603 - BoldMd(),  
604 - ItalicMd(),  
605 - LatexMath(),  
606 - LatexMathMultiLine(),  
607 - HighlightedText(),  
608 - SourceTag(),  
609 - IndentMd(),  
610 - ],  
611 - inlineComponents: [  
612 - ImageMd(),  
613 - ATagMd(),  
614 - TableMd(),  
615 - StrikeMd(),  
616 - BoldMd(),  
617 - ItalicMd(),  
618 - LatexMath(),  
619 - LatexMathMultiLine(),  
620 - HighlightedText(),  
621 - SourceTag(),  
622 - ], 624 +
  625 + // tableBuilder: (context, tableRows,
  626 + // textStyle, config) {
  627 + // return Table(
  628 + // border: TableBorder.all(
  629 + // width: 1,
  630 + // color: Colors.red,
  631 + // ),
  632 + // children: tableRows.map((e) {
  633 + // return TableRow(
  634 + // children: e.fields.map((e) {
  635 + // return Text(e.data);
  636 + // }).toList(),
  637 + // );
  638 + // }).toList(),
  639 + // );
  640 + // },
  641 +
  642 + // components: [
  643 + // CodeBlockMd(),
  644 + // NewLines(),
  645 + // BlockQuote(),
  646 + // ImageMd(),
  647 + // ATagMd(),
  648 + // TableMd(),
  649 + // HTag(),
  650 + // UnOrderedList(),
  651 + // OrderedList(),
  652 + // RadioButtonMd(),
  653 + // CheckBoxMd(),
  654 + // HrLine(),
  655 + // StrikeMd(),
  656 + // BoldMd(),
  657 + // ItalicMd(),
  658 + // LatexMath(),
  659 + // LatexMathMultiLine(),
  660 + // HighlightedText(),
  661 + // SourceTag(),
  662 + // IndentMd(),
  663 + // ],
  664 + // inlineComponents: [
  665 + // ImageMd(),
  666 + // ATagMd(),
  667 + // TableMd(),
  668 + // StrikeMd(),
  669 + // BoldMd(),
  670 + // ItalicMd(),
  671 + // LatexMath(),
  672 + // LatexMathMultiLine(),
  673 + // HighlightedText(),
  674 + // SourceTag(),
  675 + // ],
623 // codeBuilder: (context, name, code, closed) { 676 // codeBuilder: (context, name, code, closed) {
624 // return Padding( 677 // return Padding(
625 // padding: const EdgeInsets.symmetric( 678 // padding: const EdgeInsets.symmetric(
1 -platform :osx, '10.14' 1 +platform :osx, '10.15'
2 2
3 # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 3 # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
4 ENV['COCOAPODS_DISABLE_STATS'] = 'true' 4 ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@@ -15,8 +15,8 @@ EXTERNAL SOURCES: @@ -15,8 +15,8 @@ EXTERNAL SOURCES:
15 15
16 SPEC CHECKSUMS: 16 SPEC CHECKSUMS:
17 desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 17 desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
18 - FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 18 + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
19 19
20 -PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 20 +PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3
21 21
22 COCOAPODS: 1.16.2 22 COCOAPODS: 1.16.2
@@ -553,7 +553,7 @@ @@ -553,7 +553,7 @@
553 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 553 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
554 GCC_WARN_UNUSED_FUNCTION = YES; 554 GCC_WARN_UNUSED_FUNCTION = YES;
555 GCC_WARN_UNUSED_VARIABLE = YES; 555 GCC_WARN_UNUSED_VARIABLE = YES;
556 - MACOSX_DEPLOYMENT_TARGET = 10.14; 556 + MACOSX_DEPLOYMENT_TARGET = 10.15;
557 MTL_ENABLE_DEBUG_INFO = NO; 557 MTL_ENABLE_DEBUG_INFO = NO;
558 SDKROOT = macosx; 558 SDKROOT = macosx;
559 SWIFT_COMPILATION_MODE = wholemodule; 559 SWIFT_COMPILATION_MODE = wholemodule;
@@ -632,7 +632,7 @@ @@ -632,7 +632,7 @@
632 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 632 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
633 GCC_WARN_UNUSED_FUNCTION = YES; 633 GCC_WARN_UNUSED_FUNCTION = YES;
634 GCC_WARN_UNUSED_VARIABLE = YES; 634 GCC_WARN_UNUSED_VARIABLE = YES;
635 - MACOSX_DEPLOYMENT_TARGET = 10.14; 635 + MACOSX_DEPLOYMENT_TARGET = 10.15;
636 MTL_ENABLE_DEBUG_INFO = YES; 636 MTL_ENABLE_DEBUG_INFO = YES;
637 ONLY_ACTIVE_ARCH = YES; 637 ONLY_ACTIVE_ARCH = YES;
638 SDKROOT = macosx; 638 SDKROOT = macosx;
@@ -679,7 +679,7 @@ @@ -679,7 +679,7 @@
679 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 679 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
680 GCC_WARN_UNUSED_FUNCTION = YES; 680 GCC_WARN_UNUSED_FUNCTION = YES;
681 GCC_WARN_UNUSED_VARIABLE = YES; 681 GCC_WARN_UNUSED_VARIABLE = YES;
682 - MACOSX_DEPLOYMENT_TARGET = 10.14; 682 + MACOSX_DEPLOYMENT_TARGET = 10.15;
683 MTL_ENABLE_DEBUG_INFO = NO; 683 MTL_ENABLE_DEBUG_INFO = NO;
684 SDKROOT = macosx; 684 SDKROOT = macosx;
685 SWIFT_COMPILATION_MODE = wholemodule; 685 SWIFT_COMPILATION_MODE = wholemodule;
@@ -72,7 +72,13 @@ class _CodeFieldState extends State<CodeField> { @@ -72,7 +72,13 @@ class _CodeFieldState extends State<CodeField> {
72 SingleChildScrollView( 72 SingleChildScrollView(
73 scrollDirection: Axis.horizontal, 73 scrollDirection: Axis.horizontal,
74 padding: const EdgeInsets.all(16), 74 padding: const EdgeInsets.all(16),
75 - child: Text(widget.codes), 75 + child: Text(
  76 + widget.codes,
  77 + style: TextStyle(
  78 + fontFamily: 'JetBrainsMono',
  79 + package: "gpt_markdown",
  80 + ),
  81 + ),
76 ), 82 ),
77 ], 83 ],
78 ), 84 ),
@@ -24,6 +24,7 @@ class CustomRb extends StatelessWidget { @@ -24,6 +24,7 @@ class CustomRb extends StatelessWidget {
24 return Directionality( 24 return Directionality(
25 textDirection: textDirection, 25 textDirection: textDirection,
26 child: Row( 26 child: Row(
  27 + mainAxisSize: MainAxisSize.min,
27 textBaseline: TextBaseline.alphabetic, 28 textBaseline: TextBaseline.alphabetic,
28 crossAxisAlignment: CrossAxisAlignment.baseline, 29 crossAxisAlignment: CrossAxisAlignment.baseline,
29 children: [ 30 children: [
@@ -35,16 +36,18 @@ class CustomRb extends StatelessWidget { @@ -35,16 +36,18 @@ class CustomRb extends StatelessWidget {
35 start: spacing, 36 start: spacing,
36 end: spacing, 37 end: spacing,
37 ), 38 ),
  39 + child: RadioGroup(
  40 + onChanged: (value) {},
  41 + groupValue: true,
38 child: Radio( 42 child: Radio(
39 value: value, 43 value: value,
40 - groupValue: true,  
41 materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, 44 materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
42 - onChanged: (value) {},  
43 ), 45 ),
44 ), 46 ),
45 ), 47 ),
46 ), 48 ),
47 - Expanded(child: child), 49 + ),
  50 + Flexible(child: child),
48 ], 51 ],
49 ), 52 ),
50 ); 53 );
@@ -75,6 +78,7 @@ class CustomCb extends StatelessWidget { @@ -75,6 +78,7 @@ class CustomCb extends StatelessWidget {
75 return Directionality( 78 return Directionality(
76 textDirection: textDirection, 79 textDirection: textDirection,
77 child: Row( 80 child: Row(
  81 + mainAxisSize: MainAxisSize.min,
78 textBaseline: TextBaseline.alphabetic, 82 textBaseline: TextBaseline.alphabetic,
79 crossAxisAlignment: CrossAxisAlignment.baseline, 83 crossAxisAlignment: CrossAxisAlignment.baseline,
80 children: [ 84 children: [
@@ -90,7 +94,7 @@ class CustomCb extends StatelessWidget { @@ -90,7 +94,7 @@ class CustomCb extends StatelessWidget {
90 ), 94 ),
91 ), 95 ),
92 ), 96 ),
93 - Expanded(child: child), 97 + Flexible(child: child),
94 ], 98 ],
95 ), 99 ),
96 ); 100 );
@@ -13,6 +13,9 @@ class LinkButton extends StatefulWidget { @@ -13,6 +13,9 @@ class LinkButton extends StatefulWidget {
13 /// The text of the link. 13 /// The text of the link.
14 final String text; 14 final String text;
15 15
  16 + /// The child of the link.
  17 + final Widget? child;
  18 +
16 /// The callback function to be called when the link is pressed. 19 /// The callback function to be called when the link is pressed.
17 final VoidCallback? onPressed; 20 final VoidCallback? onPressed;
18 21
@@ -40,6 +43,7 @@ class LinkButton extends StatefulWidget { @@ -40,6 +43,7 @@ class LinkButton extends StatefulWidget {
40 this.onPressed, 43 this.onPressed,
41 this.textStyle, 44 this.textStyle,
42 this.url, 45 this.url,
  46 + this.child,
43 }); 47 });
44 48
45 @override 49 @override
@@ -65,7 +69,9 @@ class _LinkButtonState extends State<LinkButton> { @@ -65,7 +69,9 @@ class _LinkButtonState extends State<LinkButton> {
65 onTapUp: (_) => _handlePress(false), 69 onTapUp: (_) => _handlePress(false),
66 onTapCancel: () => _handlePress(false), 70 onTapCancel: () => _handlePress(false),
67 onTap: widget.onPressed, 71 onTap: widget.onPressed,
68 - child: widget.config.getRich(TextSpan(text: widget.text, style: style)), 72 + child:
  73 + widget.child ??
  74 + widget.config.getRich(TextSpan(text: widget.text, style: style)),
69 ), 75 ),
70 ); 76 );
71 } 77 }
@@ -44,11 +44,20 @@ typedef LatexBuilder = @@ -44,11 +44,20 @@ typedef LatexBuilder =
44 typedef LinkBuilder = 44 typedef LinkBuilder =
45 Widget Function( 45 Widget Function(
46 BuildContext context, 46 BuildContext context,
47 - String text, 47 + InlineSpan text,
48 String url, 48 String url,
49 TextStyle style, 49 TextStyle style,
50 ); 50 );
51 51
  52 +/// A builder function for the table.
  53 +typedef TableBuilder =
  54 + Widget Function(
  55 + BuildContext context,
  56 + List<CustomTableRow> tableRows,
  57 + TextStyle textStyle,
  58 + GptMarkdownConfig config,
  59 + );
  60 +
52 /// A builder function for the highlight. 61 /// A builder function for the highlight.
53 typedef HighlightBuilder = 62 typedef HighlightBuilder =
54 Widget Function(BuildContext context, String text, TextStyle style); 63 Widget Function(BuildContext context, String text, TextStyle style);
@@ -61,12 +70,12 @@ typedef ImageBuilder = Widget Function(BuildContext context, String imageUrl); @@ -61,12 +70,12 @@ typedef ImageBuilder = Widget Function(BuildContext context, String imageUrl);
61 /// The [GptMarkdownConfig] class is used to configure the GPT Markdown component. 70 /// The [GptMarkdownConfig] class is used to configure the GPT Markdown component.
62 /// It takes a [style] parameter to set the style of the text, 71 /// It takes a [style] parameter to set the style of the text,
63 /// a [textDirection] parameter to set the direction of the text, 72 /// a [textDirection] parameter to set the direction of the text,
64 -/// and an optional [onLinkTab] parameter to handle link clicks. 73 +/// and an optional [onLinkTap] parameter to handle link clicks.
65 class GptMarkdownConfig { 74 class GptMarkdownConfig {
66 const GptMarkdownConfig({ 75 const GptMarkdownConfig({
67 this.style, 76 this.style,
68 this.textDirection = TextDirection.ltr, 77 this.textDirection = TextDirection.ltr,
69 - this.onLinkTab, 78 + this.onLinkTap,
70 this.textAlign, 79 this.textAlign,
71 this.textScaler, 80 this.textScaler,
72 this.latexWorkaround, 81 this.latexWorkaround,
@@ -83,6 +92,7 @@ class GptMarkdownConfig { @@ -83,6 +92,7 @@ class GptMarkdownConfig {
83 this.overflow, 92 this.overflow,
84 this.components, 93 this.components,
85 this.inlineComponents, 94 this.inlineComponents,
  95 + this.tableBuilder,
86 }); 96 });
87 97
88 /// The direction of the text. 98 /// The direction of the text.
@@ -98,7 +108,7 @@ class GptMarkdownConfig { @@ -98,7 +108,7 @@ class GptMarkdownConfig {
98 final TextScaler? textScaler; 108 final TextScaler? textScaler;
99 109
100 /// The callback function to handle link clicks. 110 /// The callback function to handle link clicks.
101 - final void Function(String url, String title)? onLinkTab; 111 + final void Function(String url, String title)? onLinkTap;
102 112
103 /// The LaTeX workaround. 113 /// The LaTeX workaround.
104 final String Function(String tex)? latexWorkaround; 114 final String Function(String tex)? latexWorkaround;
@@ -142,11 +152,14 @@ class GptMarkdownConfig { @@ -142,11 +152,14 @@ class GptMarkdownConfig {
142 /// The list of inline components. 152 /// The list of inline components.
143 final List<MarkdownComponent>? inlineComponents; 153 final List<MarkdownComponent>? inlineComponents;
144 154
  155 + /// The table builder.
  156 + final TableBuilder? tableBuilder;
  157 +
145 /// A copy of the configuration with the specified parameters. 158 /// A copy of the configuration with the specified parameters.
146 GptMarkdownConfig copyWith({ 159 GptMarkdownConfig copyWith({
147 TextStyle? style, 160 TextStyle? style,
148 TextDirection? textDirection, 161 TextDirection? textDirection,
149 - final void Function(String url, String title)? onLinkTab, 162 + final void Function(String url, String title)? onLinkTap,
150 final TextAlign? textAlign, 163 final TextAlign? textAlign,
151 final TextScaler? textScaler, 164 final TextScaler? textScaler,
152 final String Function(String tex)? latexWorkaround, 165 final String Function(String tex)? latexWorkaround,
@@ -163,11 +176,12 @@ class GptMarkdownConfig { @@ -163,11 +176,12 @@ class GptMarkdownConfig {
163 final UnOrderedListBuilder? unOrderedListBuilder, 176 final UnOrderedListBuilder? unOrderedListBuilder,
164 final List<MarkdownComponent>? components, 177 final List<MarkdownComponent>? components,
165 final List<MarkdownComponent>? inlineComponents, 178 final List<MarkdownComponent>? inlineComponents,
  179 + final TableBuilder? tableBuilder,
166 }) { 180 }) {
167 return GptMarkdownConfig( 181 return GptMarkdownConfig(
168 style: style ?? this.style, 182 style: style ?? this.style,
169 textDirection: textDirection ?? this.textDirection, 183 textDirection: textDirection ?? this.textDirection,
170 - onLinkTab: onLinkTab ?? this.onLinkTab, 184 + onLinkTap: onLinkTap ?? this.onLinkTap,
171 textAlign: textAlign ?? this.textAlign, 185 textAlign: textAlign ?? this.textAlign,
172 textScaler: textScaler ?? this.textScaler, 186 textScaler: textScaler ?? this.textScaler,
173 latexWorkaround: latexWorkaround ?? this.latexWorkaround, 187 latexWorkaround: latexWorkaround ?? this.latexWorkaround,
@@ -184,6 +198,7 @@ class GptMarkdownConfig { @@ -184,6 +198,7 @@ class GptMarkdownConfig {
184 unOrderedListBuilder: unOrderedListBuilder ?? this.unOrderedListBuilder, 198 unOrderedListBuilder: unOrderedListBuilder ?? this.unOrderedListBuilder,
185 components: components ?? this.components, 199 components: components ?? this.components,
186 inlineComponents: inlineComponents ?? this.inlineComponents, 200 inlineComponents: inlineComponents ?? this.inlineComponents,
  201 + tableBuilder: tableBuilder ?? this.tableBuilder,
187 ); 202 );
188 } 203 }
189 204
@@ -198,4 +213,27 @@ class GptMarkdownConfig { @@ -198,4 +213,27 @@ class GptMarkdownConfig {
198 overflow: overflow, 213 overflow: overflow,
199 ); 214 );
200 } 215 }
  216 +
  217 + /// A method to check if the configuration is the same.
  218 + bool isSame(GptMarkdownConfig other) {
  219 + return style == other.style &&
  220 + textAlign == other.textAlign &&
  221 + textScaler == other.textScaler &&
  222 + maxLines == other.maxLines &&
  223 + overflow == other.overflow &&
  224 + followLinkColor == other.followLinkColor &&
  225 + // latexWorkaround == other.latexWorkaround &&
  226 + // components == other.components &&
  227 + // inlineComponents == other.inlineComponents &&
  228 + // latexBuilder == other.latexBuilder &&
  229 + // sourceTagBuilder == other.sourceTagBuilder &&
  230 + // codeBuilder == other.codeBuilder &&
  231 + // orderedListBuilder == other.orderedListBuilder &&
  232 + // unOrderedListBuilder == other.unOrderedListBuilder &&
  233 + // linkBuilder == other.linkBuilder &&
  234 + // imageBuilder == other.imageBuilder &&
  235 + // highlightBuilder == other.highlightBuilder &&
  236 + // onLinkTap == other.onLinkTap &&
  237 + textDirection == other.textDirection;
  238 + }
201 } 239 }
@@ -38,6 +38,7 @@ class UnorderedListView extends StatelessWidget { @@ -38,6 +38,7 @@ class UnorderedListView extends StatelessWidget {
38 return Directionality( 38 return Directionality(
39 textDirection: textDirection, 39 textDirection: textDirection,
40 child: Row( 40 child: Row(
  41 + mainAxisSize: MainAxisSize.min,
41 textBaseline: TextBaseline.alphabetic, 42 textBaseline: TextBaseline.alphabetic,
42 crossAxisAlignment: CrossAxisAlignment.baseline, 43 crossAxisAlignment: CrossAxisAlignment.baseline,
43 children: [ 44 children: [
@@ -63,7 +64,7 @@ class UnorderedListView extends StatelessWidget { @@ -63,7 +64,7 @@ class UnorderedListView extends StatelessWidget {
63 ), 64 ),
64 ), 65 ),
65 ), 66 ),
66 - Expanded(child: child), 67 + Flexible(child: child),
67 ], 68 ],
68 ), 69 ),
69 ); 70 );
@@ -104,6 +105,7 @@ class OrderedListView extends StatelessWidget { @@ -104,6 +105,7 @@ class OrderedListView extends StatelessWidget {
104 return Directionality( 105 return Directionality(
105 textDirection: textDirection, 106 textDirection: textDirection,
106 child: Row( 107 child: Row(
  108 + mainAxisSize: MainAxisSize.min,
107 textBaseline: TextBaseline.alphabetic, 109 textBaseline: TextBaseline.alphabetic,
108 crossAxisAlignment: CrossAxisAlignment.baseline, 110 crossAxisAlignment: CrossAxisAlignment.baseline,
109 children: [ 111 children: [
@@ -111,7 +113,7 @@ class OrderedListView extends StatelessWidget { @@ -111,7 +113,7 @@ class OrderedListView extends StatelessWidget {
111 padding: EdgeInsetsDirectional.only(start: padding, end: spacing), 113 padding: EdgeInsetsDirectional.only(start: padding, end: spacing),
112 child: Text.rich(TextSpan(text: no), style: _style), 114 child: Text.rich(TextSpan(text: no), style: _style),
113 ), 115 ),
114 - Expanded(child: child), 116 + Flexible(child: child),
115 ], 117 ],
116 ), 118 ),
117 ); 119 );
No preview for this file type
@@ -30,7 +30,7 @@ class GptMarkdown extends StatelessWidget { @@ -30,7 +30,7 @@ class GptMarkdown extends StatelessWidget {
30 this.textAlign, 30 this.textAlign,
31 this.imageBuilder, 31 this.imageBuilder,
32 this.textScaler, 32 this.textScaler,
33 - this.onLinkTab, 33 + this.onLinkTap,
34 this.latexBuilder, 34 this.latexBuilder,
35 this.codeBuilder, 35 this.codeBuilder,
36 this.sourceTagBuilder, 36 this.sourceTagBuilder,
@@ -40,8 +40,10 @@ class GptMarkdown extends StatelessWidget { @@ -40,8 +40,10 @@ class GptMarkdown extends StatelessWidget {
40 this.overflow, 40 this.overflow,
41 this.orderedListBuilder, 41 this.orderedListBuilder,
42 this.unOrderedListBuilder, 42 this.unOrderedListBuilder,
  43 + this.tableBuilder,
43 this.components, 44 this.components,
44 this.inlineComponents, 45 this.inlineComponents,
  46 + this.useDollarSignsForLatex = false,
45 }); 47 });
46 48
47 /// The direction of the text. 49 /// The direction of the text.
@@ -60,7 +62,7 @@ class GptMarkdown extends StatelessWidget { @@ -60,7 +62,7 @@ class GptMarkdown extends StatelessWidget {
60 final TextScaler? textScaler; 62 final TextScaler? textScaler;
61 63
62 /// The callback function to handle link clicks. 64 /// The callback function to handle link clicks.
63 - final void Function(String url, String title)? onLinkTab; 65 + final void Function(String url, String title)? onLinkTap;
64 66
65 /// The LaTeX workaround. 67 /// The LaTeX workaround.
66 final String Function(String tex)? latexWorkaround; 68 final String Function(String tex)? latexWorkaround;
@@ -96,6 +98,12 @@ class GptMarkdown extends StatelessWidget { @@ -96,6 +98,12 @@ class GptMarkdown extends StatelessWidget {
96 /// The unordered list builder. 98 /// The unordered list builder.
97 final UnOrderedListBuilder? unOrderedListBuilder; 99 final UnOrderedListBuilder? unOrderedListBuilder;
98 100
  101 + /// Whether to use dollar signs for LaTeX.
  102 + final bool useDollarSignsForLatex;
  103 +
  104 + /// The table builder.
  105 + final TableBuilder? tableBuilder;
  106 +
99 /// The list of components. 107 /// The list of components.
100 /// ```dart 108 /// ```dart
101 /// List<MarkdownComponent> components = [ 109 /// List<MarkdownComponent> components = [
@@ -154,6 +162,7 @@ class GptMarkdown extends StatelessWidget { @@ -154,6 +162,7 @@ class GptMarkdown extends StatelessWidget {
154 @override 162 @override
155 Widget build(BuildContext context) { 163 Widget build(BuildContext context) {
156 String tex = data.trim(); 164 String tex = data.trim();
  165 + if (useDollarSignsForLatex) {
157 // print("texBefore:\n:$tex"); 166 // print("texBefore:\n:$tex");
158 167
159 //去除 $$前面的空格 会导致渲染出错 168 //去除 $$前面的空格 会导致渲染出错
@@ -176,15 +185,17 @@ class GptMarkdown extends StatelessWidget { @@ -176,15 +185,17 @@ class GptMarkdown extends StatelessWidget {
176 }, 185 },
177 ); 186 );
178 } 187 }
  188 + }
179 // tex = _removeExtraLinesInsideBlockLatex(tex); 189 // tex = _removeExtraLinesInsideBlockLatex(tex);
180 return ClipRRect( 190 return ClipRRect(
181 child: MdWidget( 191 child: MdWidget(
  192 + context,
182 tex, 193 tex,
183 true, 194 true,
184 config: GptMarkdownConfig( 195 config: GptMarkdownConfig(
185 textDirection: textDirection, 196 textDirection: textDirection,
186 style: style, 197 style: style,
187 - onLinkTab: onLinkTab, 198 + onLinkTap: onLinkTap,
188 textAlign: textAlign, 199 textAlign: textAlign,
189 textScaler: textScaler, 200 textScaler: textScaler,
190 followLinkColor: followLinkColor, 201 followLinkColor: followLinkColor,
@@ -201,6 +212,7 @@ class GptMarkdown extends StatelessWidget { @@ -201,6 +212,7 @@ class GptMarkdown extends StatelessWidget {
201 unOrderedListBuilder: unOrderedListBuilder, 212 unOrderedListBuilder: unOrderedListBuilder,
202 components: components, 213 components: components,
203 inlineComponents: inlineComponents, 214 inlineComponents: inlineComponents,
  215 + tableBuilder: tableBuilder,
204 ), 216 ),
205 ), 217 ),
206 ); 218 );
@@ -2,12 +2,11 @@ part of 'gpt_markdown.dart'; @@ -2,12 +2,11 @@ part of 'gpt_markdown.dart';
2 2
3 /// Markdown components 3 /// Markdown components
4 abstract class MarkdownComponent { 4 abstract class MarkdownComponent {
5 - static final List<MarkdownComponent> components = [ 5 + static List<MarkdownComponent> get globalComponents => [
6 CodeBlockMd(), 6 CodeBlockMd(),
  7 + LatexMathMultiLine(),
7 NewLines(), 8 NewLines(),
8 BlockQuote(), 9 BlockQuote(),
9 - ImageMd(),  
10 - ATagMd(),  
11 TableMd(), 10 TableMd(),
12 HTag(), 11 HTag(),
13 UnOrderedList(), 12 UnOrderedList(),
@@ -15,23 +14,17 @@ abstract class MarkdownComponent { @@ -15,23 +14,17 @@ abstract class MarkdownComponent {
15 RadioButtonMd(), 14 RadioButtonMd(),
16 CheckBoxMd(), 15 CheckBoxMd(),
17 HrLine(), 16 HrLine(),
18 - StrikeMd(),  
19 - BoldMd(),  
20 - ItalicMd(),  
21 - LatexMath(),  
22 - LatexMathMultiLine(),  
23 - HighlightedText(),  
24 - SourceTag(),  
25 IndentMd(), 17 IndentMd(),
26 ]; 18 ];
27 19
28 static final List<MarkdownComponent> inlineComponents = [ 20 static final List<MarkdownComponent> inlineComponents = [
29 - ImageMd(),  
30 ATagMd(), 21 ATagMd(),
  22 + ImageMd(),
31 TableMd(), 23 TableMd(),
32 StrikeMd(), 24 StrikeMd(),
33 BoldMd(), 25 BoldMd(),
34 ItalicMd(), 26 ItalicMd(),
  27 + UnderLineMd(),
35 LatexMath(), 28 LatexMath(),
36 LatexMathMultiLine(), 29 LatexMathMultiLine(),
37 HighlightedText(), 30 HighlightedText(),
@@ -47,7 +40,7 @@ abstract class MarkdownComponent { @@ -47,7 +40,7 @@ abstract class MarkdownComponent {
47 ) { 40 ) {
48 var components = 41 var components =
49 includeGlobalComponents 42 includeGlobalComponents
50 - ? config.components ?? MarkdownComponent.components 43 + ? config.components ?? MarkdownComponent.globalComponents
51 : config.inlineComponents ?? MarkdownComponent.inlineComponents; 44 : config.inlineComponents ?? MarkdownComponent.inlineComponents;
52 List<InlineSpan> spans = []; 45 List<InlineSpan> spans = [];
53 Iterable<String> regexes = components.map<String>((e) => e.exp.pattern); 46 Iterable<String> regexes = components.map<String>((e) => e.exp.pattern);
@@ -75,6 +68,14 @@ abstract class MarkdownComponent { @@ -75,6 +68,14 @@ abstract class MarkdownComponent {
75 return ""; 68 return "";
76 }, 69 },
77 onNonMatch: (p0) { 70 onNonMatch: (p0) {
  71 + if (p0.isEmpty) {
  72 + return "";
  73 + }
  74 + if (includeGlobalComponents) {
  75 + var newSpans = generate(context, p0, config.copyWith(), false);
  76 + spans.addAll(newSpans);
  77 + return "";
  78 + }
78 spans.add(TextSpan(text: p0, style: config.style)); 79 spans.add(TextSpan(text: p0, style: config.style));
79 return ""; 80 return "";
80 }, 81 },
@@ -112,7 +113,8 @@ abstract class BlockMd extends MarkdownComponent { @@ -112,7 +113,8 @@ abstract class BlockMd extends MarkdownComponent {
112 bool get inline => false; 113 bool get inline => false;
113 114
114 @override 115 @override
115 - RegExp get exp => RegExp(r'^\ *?' + expString, dotAll: true, multiLine: true); 116 + RegExp get exp =>
  117 + RegExp(r'^\ *?' + expString + r"$", dotAll: true, multiLine: true);
116 118
117 String get expString; 119 String get expString;
118 120
@@ -134,7 +136,10 @@ abstract class BlockMd extends MarkdownComponent { @@ -134,7 +136,10 @@ abstract class BlockMd extends MarkdownComponent {
134 child: child, 136 child: child,
135 ); 137 );
136 } 138 }
137 - child = Row(children: [Flexible(child: child)]); 139 + child = Row(
  140 + mainAxisSize: MainAxisSize.min,
  141 + children: [Flexible(child: child)],
  142 + );
138 return WidgetSpan( 143 return WidgetSpan(
139 child: child, 144 child: child,
140 alignment: PlaceholderAlignment.baseline, 145 alignment: PlaceholderAlignment.baseline,
@@ -164,6 +169,7 @@ class IndentMd extends BlockMd { @@ -164,6 +169,7 @@ class IndentMd extends BlockMd {
164 return Directionality( 169 return Directionality(
165 textDirection: config.textDirection, 170 textDirection: config.textDirection,
166 child: Row( 171 child: Row(
  172 + mainAxisSize: MainAxisSize.min,
167 children: [ 173 children: [
168 Flexible( 174 Flexible(
169 child: config.getRich( 175 child: config.getRich(
@@ -196,14 +202,15 @@ class HTag extends BlockMd { @@ -196,14 +202,15 @@ class HTag extends BlockMd {
196 var theme = GptMarkdownTheme.of(context); 202 var theme = GptMarkdownTheme.of(context);
197 var match = this.exp.firstMatch(text.trim()); 203 var match = this.exp.firstMatch(text.trim());
198 var conf = config.copyWith( 204 var conf = config.copyWith(
199 - style: [ 205 + style:
  206 + [
200 theme.h1, 207 theme.h1,
201 theme.h2, 208 theme.h2,
202 theme.h3, 209 theme.h3,
203 theme.h4, 210 theme.h4,
204 theme.h5, 211 theme.h5,
205 theme.h6, 212 theme.h6,
206 - ][match![1]!.length - 1]?.copyWith(color: config.style?.color), 213 + ][match![1]!.length - 1],
207 ); 214 );
208 return config.getRich( 215 return config.getRich(
209 TextSpan( 216 TextSpan(
@@ -257,7 +264,7 @@ class NewLines extends InlineMd { @@ -257,7 +264,7 @@ class NewLines extends InlineMd {
257 /// Horizontal line component 264 /// Horizontal line component
258 class HrLine extends BlockMd { 265 class HrLine extends BlockMd {
259 @override 266 @override
260 - String get expString => (r"(--)[-]+$"); 267 + String get expString => (r"⸻|((--)[-]+)$");
261 @override 268 @override
262 Widget build( 269 Widget build(
263 BuildContext context, 270 BuildContext context,
@@ -277,7 +284,6 @@ class HrLine extends BlockMd { @@ -277,7 +284,6 @@ class HrLine extends BlockMd {
277 class CheckBoxMd extends BlockMd { 284 class CheckBoxMd extends BlockMd {
278 @override 285 @override
279 String get expString => (r"\[((?:\x|\ ))\]\ (\S[^\n]*?)$"); 286 String get expString => (r"\[((?:\x|\ ))\]\ (\S[^\n]*?)$");
280 - get onLinkTab => null;  
281 287
282 @override 288 @override
283 Widget build( 289 Widget build(
@@ -289,7 +295,7 @@ class CheckBoxMd extends BlockMd { @@ -289,7 +295,7 @@ class CheckBoxMd extends BlockMd {
289 return CustomCb( 295 return CustomCb(
290 value: ("${match?[1]}" == "x"), 296 value: ("${match?[1]}" == "x"),
291 textDirection: config.textDirection, 297 textDirection: config.textDirection,
292 - child: MdWidget("${match?[2]}", false, config: config), 298 + child: MdWidget(context, "${match?[2]}", false, config: config),
293 ); 299 );
294 } 300 }
295 } 301 }
@@ -298,7 +304,6 @@ class CheckBoxMd extends BlockMd { @@ -298,7 +304,6 @@ class CheckBoxMd extends BlockMd {
298 class RadioButtonMd extends BlockMd { 304 class RadioButtonMd extends BlockMd {
299 @override 305 @override
300 String get expString => (r"\(((?:\x|\ ))\)\ (\S[^\n]*)$"); 306 String get expString => (r"\(((?:\x|\ ))\)\ (\S[^\n]*)$");
301 - get onLinkTab => null;  
302 307
303 @override 308 @override
304 Widget build( 309 Widget build(
@@ -310,7 +315,7 @@ class RadioButtonMd extends BlockMd { @@ -310,7 +315,7 @@ class RadioButtonMd extends BlockMd {
310 return CustomRb( 315 return CustomRb(
311 value: ("${match?[1]}" == "x"), 316 value: ("${match?[1]}" == "x"),
312 textDirection: config.textDirection, 317 textDirection: config.textDirection,
313 - child: MdWidget("${match?[2]}", false, config: config), 318 + child: MdWidget(context, "${match?[2]}", false, config: config),
314 ); 319 );
315 } 320 }
316 } 321 }
@@ -389,7 +394,7 @@ class UnOrderedList extends BlockMd { @@ -389,7 +394,7 @@ class UnOrderedList extends BlockMd {
389 ) { 394 ) {
390 var match = this.exp.firstMatch(text); 395 var match = this.exp.firstMatch(text);
391 396
392 - var child = MdWidget("${match?[1]?.trim()}", true, config: config); 397 + var child = MdWidget(context, "${match?[1]?.trim()}", true, config: config);
393 398
394 return config.unOrderedListBuilder?.call( 399 return config.unOrderedListBuilder?.call(
395 context, 400 context,
@@ -423,11 +428,11 @@ class OrderedList extends BlockMd { @@ -423,11 +428,11 @@ class OrderedList extends BlockMd {
423 String text, 428 String text,
424 final GptMarkdownConfig config, 429 final GptMarkdownConfig config,
425 ) { 430 ) {
426 - var match = this.exp.firstMatch(text.trim()); 431 + var match = this.exp.firstMatch(text);
427 432
428 - var no = "${match?[1]}"; 433 + var no = "${match?[1]}".trim();
429 434
430 - var child = MdWidget("${match?[2]?.trim()}", true, config: config); 435 + var child = MdWidget(context, "${match?[2]}".trim(), true, config: config);
431 return config.orderedListBuilder?.call( 436 return config.orderedListBuilder?.call(
432 context, 437 context,
433 no, 438 no,
@@ -779,7 +784,7 @@ class SourceTag extends InlineMd { @@ -779,7 +784,7 @@ class SourceTag extends InlineMd {
779 /// Link text component 784 /// Link text component
780 class ATagMd extends InlineMd { 785 class ATagMd extends InlineMd {
781 @override 786 @override
782 - RegExp get exp => RegExp(r"\[([^\s\*\[][^\n]*?[^\s]?)?\]\(([^\s\*]*[^\)])\)"); 787 + RegExp get exp => RegExp(r"(?<!\!)\[.*\]\([^\s]*\)");
783 788
784 @override 789 @override
785 InlineSpan span( 790 InlineSpan span(
@@ -787,24 +792,91 @@ class ATagMd extends InlineMd { @@ -787,24 +792,91 @@ class ATagMd extends InlineMd {
787 String text, 792 String text,
788 final GptMarkdownConfig config, 793 final GptMarkdownConfig config,
789 ) { 794 ) {
790 - var match = exp.firstMatch(text.trim());  
791 - if (match?[1] == null && match?[2] == null) { 795 + var bracketCount = 0;
  796 + var start = 1;
  797 + var end = 0;
  798 + for (var i = 0; i < text.length; i++) {
  799 + if (text[i] == '[') {
  800 + bracketCount++;
  801 + } else if (text[i] == ']') {
  802 + bracketCount--;
  803 + if (bracketCount == 0) {
  804 + end = i;
  805 + break;
  806 + }
  807 + }
  808 + }
  809 +
  810 + if (text[end + 1] != '(') {
792 return const TextSpan(); 811 return const TextSpan();
793 } 812 }
794 813
795 - final linkText = match?[1] ?? "";  
796 - final url = match?[2] ?? ""; 814 + // First try to find the basic pattern
  815 + // final basicMatch = RegExp(r'(?<!\!)\[(.*)\]\(').firstMatch(text.trim());
  816 + // if (basicMatch == null) {
  817 + // return const TextSpan();
  818 + // }
  819 +
  820 + final linkText = text.substring(start, end);
  821 + final urlStart = end + 2;
  822 +
  823 + // Now find the balanced closing parenthesis
  824 + int parenCount = 0;
  825 + int urlEnd = urlStart;
  826 +
  827 + for (int i = urlStart; i < text.length; i++) {
  828 + final char = text[i];
  829 +
  830 + if (char == '(') {
  831 + parenCount++;
  832 + } else if (char == ')') {
  833 + if (parenCount == 0) {
  834 + // This is the closing parenthesis of the link
  835 + urlEnd = i;
  836 + break;
  837 + } else {
  838 + parenCount--;
  839 + }
  840 + }
  841 + }
  842 +
  843 + if (urlEnd == urlStart) {
  844 + // No closing parenthesis found
  845 + return const TextSpan();
  846 + }
  847 +
  848 + final url = text.substring(urlStart, urlEnd).trim();
797 849
798 var builder = config.linkBuilder; 850 var builder = config.linkBuilder;
799 851
  852 + var ending = text.substring(urlEnd + 1);
  853 +
  854 + var endingSpans = MarkdownComponent.generate(
  855 + context,
  856 + ending,
  857 + config,
  858 + false,
  859 + );
  860 + var theme = GptMarkdownTheme.of(context);
  861 + var linkTextSpan = TextSpan(
  862 + children: MarkdownComponent.generate(context, linkText, config, false),
  863 + style: config.style?.copyWith(
  864 + color: theme.linkColor,
  865 + decorationColor: theme.linkColor,
  866 + ),
  867 + );
  868 +
800 // Use custom builder if provided 869 // Use custom builder if provided
  870 + WidgetSpan? child;
801 if (builder != null) { 871 if (builder != null) {
802 - return WidgetSpan( 872 + child = WidgetSpan(
  873 + baseline: TextBaseline.alphabetic,
  874 + alignment: PlaceholderAlignment.baseline,
803 child: GestureDetector( 875 child: GestureDetector(
804 - onTap: () => config.onLinkTab?.call(url, linkText), 876 + onTap: () => config.onLinkTap?.call(url, linkText),
805 child: builder( 877 child: builder(
806 context, 878 context,
807 - linkText, 879 + linkTextSpan,
808 url, 880 url,
809 config.style ?? const TextStyle(), 881 config.style ?? const TextStyle(),
810 ), 882 ),
@@ -813,25 +885,29 @@ class ATagMd extends InlineMd { @@ -813,25 +885,29 @@ class ATagMd extends InlineMd {
813 } 885 }
814 886
815 // Default rendering 887 // Default rendering
816 - var theme = GptMarkdownTheme.of(context);  
817 - return WidgetSpan( 888 + child ??= WidgetSpan(
  889 + alignment: PlaceholderAlignment.baseline,
  890 + baseline: TextBaseline.alphabetic,
818 child: LinkButton( 891 child: LinkButton(
819 hoverColor: theme.linkHoverColor, 892 hoverColor: theme.linkHoverColor,
820 color: theme.linkColor, 893 color: theme.linkColor,
821 onPressed: () { 894 onPressed: () {
822 - config.onLinkTab?.call(url, linkText); 895 + config.onLinkTap?.call(url, linkText);
823 }, 896 },
824 text: linkText, 897 text: linkText,
825 config: config, 898 config: config,
  899 + child: config.getRich(linkTextSpan),
826 ), 900 ),
827 ); 901 );
  902 + var textSpan = TextSpan(children: [child, ...endingSpans]);
  903 + return textSpan;
828 } 904 }
829 } 905 }
830 906
831 /// Image component 907 /// Image component
832 class ImageMd extends InlineMd { 908 class ImageMd extends InlineMd {
833 @override 909 @override
834 - RegExp get exp => RegExp(r"\!\[([^\s][^\n]*[^\s]?)?\]\(([^\s]+?)\)"); 910 + RegExp get exp => RegExp(r"\!\[[^\[\]]*\]\([^\s]*\)");
835 911
836 @override 912 @override
837 InlineSpan span( 913 InlineSpan span(
@@ -839,25 +915,59 @@ class ImageMd extends InlineMd { @@ -839,25 +915,59 @@ class ImageMd extends InlineMd {
839 String text, 915 String text,
840 final GptMarkdownConfig config, 916 final GptMarkdownConfig config,
841 ) { 917 ) {
842 - var match = exp.firstMatch(text.trim()); 918 + // First try to find the basic pattern
  919 + final basicMatch = RegExp(r'\!\[([^\[\]]*)\]\(').firstMatch(text.trim());
  920 + if (basicMatch == null) {
  921 + return const TextSpan();
  922 + }
  923 +
  924 + final altText = basicMatch.group(1) ?? '';
  925 + final urlStart = basicMatch.end;
  926 +
  927 + // Now find the balanced closing parenthesis
  928 + int parenCount = 0;
  929 + int urlEnd = urlStart;
  930 +
  931 + for (int i = urlStart; i < text.length; i++) {
  932 + final char = text[i];
  933 +
  934 + if (char == '(') {
  935 + parenCount++;
  936 + } else if (char == ')') {
  937 + if (parenCount == 0) {
  938 + // This is the closing parenthesis of the image
  939 + urlEnd = i;
  940 + break;
  941 + } else {
  942 + parenCount--;
  943 + }
  944 + }
  945 + }
  946 +
  947 + if (urlEnd == urlStart) {
  948 + // No closing parenthesis found
  949 + return const TextSpan();
  950 + }
  951 +
  952 + final url = text.substring(urlStart, urlEnd).trim();
  953 +
843 double? height; 954 double? height;
844 double? width; 955 double? width;
845 - if (match?[1] != null) {  
846 - var size = RegExp(  
847 - r"^([0-9]+)?x?([0-9]+)?",  
848 - ).firstMatch(match![1].toString().trim()); 956 + if (altText.isNotEmpty) {
  957 + var size = RegExp(r"^([0-9]+)?x?([0-9]+)?").firstMatch(altText.trim());
849 width = double.tryParse(size?[1]?.toString().trim() ?? 'a'); 958 width = double.tryParse(size?[1]?.toString().trim() ?? 'a');
850 height = double.tryParse(size?[2]?.toString().trim() ?? 'a'); 959 height = double.tryParse(size?[2]?.toString().trim() ?? 'a');
851 } 960 }
  961 +
852 final Widget image; 962 final Widget image;
853 if (config.imageBuilder != null) { 963 if (config.imageBuilder != null) {
854 - image = config.imageBuilder!(context, '${match?[2]}'); 964 + image = config.imageBuilder!(context, url);
855 } else { 965 } else {
856 image = SizedBox( 966 image = SizedBox(
857 width: width, 967 width: width,
858 height: height, 968 height: height,
859 child: Image( 969 child: Image(
860 - image: NetworkImage("${match?[2]}"), 970 + image: NetworkImage(url),
861 loadingBuilder: ( 971 loadingBuilder: (
862 BuildContext context, 972 BuildContext context,
863 Widget child, 973 Widget child,
@@ -909,19 +1019,80 @@ class TableMd extends BlockMd { @@ -909,19 +1019,80 @@ class TableMd extends BlockMd {
909 .asMap(), 1019 .asMap(),
910 ) 1020 )
911 .toList(); 1021 .toList();
912 - bool heading = RegExp(  
913 - r"^\|.*?\|\n\|-[-\\ |]*?-\|$",  
914 - multiLine: true,  
915 - ).hasMatch(text.trim()); 1022 +
  1023 + // Check if table has a header and separator row
  1024 + bool hasHeader = value.length >= 2;
  1025 + List<TextAlign> columnAlignments = [];
  1026 +
  1027 + if (hasHeader) {
  1028 + // Parse alignment from the separator row (second row)
  1029 + var separatorRow = value[1];
  1030 + columnAlignments = List.generate(separatorRow.length, (index) {
  1031 + String separator = separatorRow[index] ?? "";
  1032 + separator = separator.trim();
  1033 +
  1034 + // Check for alignment indicators
  1035 + bool hasLeftColon = separator.startsWith(':');
  1036 + bool hasRightColon = separator.endsWith(':');
  1037 +
  1038 + if (hasLeftColon && hasRightColon) {
  1039 + return TextAlign.center;
  1040 + } else if (hasRightColon) {
  1041 + return TextAlign.right;
  1042 + } else if (hasLeftColon) {
  1043 + return TextAlign.left;
  1044 + } else {
  1045 + return TextAlign.left; // Default alignment
  1046 + }
  1047 + });
  1048 + }
  1049 +
916 int maxCol = 0; 1050 int maxCol = 0;
917 for (final each in value) { 1051 for (final each in value) {
918 if (maxCol < each.keys.length) { 1052 if (maxCol < each.keys.length) {
919 maxCol = each.keys.length; 1053 maxCol = each.keys.length;
920 } 1054 }
921 } 1055 }
  1056 +
922 if (maxCol == 0) { 1057 if (maxCol == 0) {
923 return Text("", style: config.style); 1058 return Text("", style: config.style);
924 } 1059 }
  1060 +
  1061 + // Ensure we have alignment for all columns
  1062 + while (columnAlignments.length < maxCol) {
  1063 + columnAlignments.add(TextAlign.left);
  1064 + }
  1065 +
  1066 + var tableBuilder = config.tableBuilder;
  1067 +
  1068 + if (tableBuilder != null) {
  1069 + var customTable =
  1070 + List<CustomTableRow?>.generate(value.length, (index) {
  1071 + var isHeader = index == 0;
  1072 + var row = value[index];
  1073 + if (row.isEmpty) {
  1074 + return null;
  1075 + }
  1076 + if (index == 1) {
  1077 + return null;
  1078 + }
  1079 + var fields = List<CustomTableField>.generate(maxCol, (index) {
  1080 + var field = row[index];
  1081 + return CustomTableField(
  1082 + data: field ?? "",
  1083 + alignment: columnAlignments[index],
  1084 + );
  1085 + });
  1086 + return CustomTableRow(isHeader: isHeader, fields: fields);
  1087 + }).nonNulls.toList();
  1088 + return tableBuilder(
  1089 + context,
  1090 + customTable,
  1091 + config.style ?? const TextStyle(),
  1092 + config,
  1093 + );
  1094 + }
  1095 +
925 final controller = ScrollController(); 1096 final controller = ScrollController();
926 return Scrollbar( 1097 return Scrollbar(
927 controller: controller, 1098 controller: controller,
@@ -940,17 +1111,22 @@ class TableMd extends BlockMd { @@ -940,17 +1111,22 @@ class TableMd extends BlockMd {
940 value 1111 value
941 .asMap() 1112 .asMap()
942 .entries 1113 .entries
  1114 + .where((entry) {
  1115 + // Skip the separator row (second row) from rendering
  1116 + if (hasHeader && entry.key == 1) {
  1117 + return false;
  1118 + }
  1119 + return true;
  1120 + })
943 .map<TableRow>( 1121 .map<TableRow>(
944 (entry) => TableRow( 1122 (entry) => TableRow(
945 decoration: 1123 decoration:
946 - (heading) 1124 + (hasHeader && entry.key == 0)
947 ? BoxDecoration( 1125 ? BoxDecoration(
948 color: 1126 color:
949 - (entry.key == 0)  
950 - ? Theme.of( 1127 + Theme.of(
951 context, 1128 context,
952 - ).colorScheme.surfaceContainerHighest  
953 - : null, 1129 + ).colorScheme.surfaceContainerHighest,
954 ) 1130 )
955 : null, 1131 : null,
956 children: List.generate(maxCol, (index) { 1132 children: List.generate(maxCol, (index) {
@@ -961,19 +1137,41 @@ class TableMd extends BlockMd { @@ -961,19 +1137,41 @@ class TableMd extends BlockMd {
961 return const SizedBox(); 1137 return const SizedBox();
962 } 1138 }
963 1139
964 - return Center(  
965 - child: Padding( 1140 + // Apply alignment based on column alignment
  1141 + Widget content = Padding(
966 padding: const EdgeInsets.symmetric( 1142 padding: const EdgeInsets.symmetric(
967 horizontal: 8, 1143 horizontal: 8,
968 vertical: 4, 1144 vertical: 4,
969 ), 1145 ),
970 child: MdWidget( 1146 child: MdWidget(
  1147 + context,
971 (e[index] ?? "").trim(), 1148 (e[index] ?? "").trim(),
972 false, 1149 false,
973 config: config, 1150 config: config,
974 ), 1151 ),
975 - ),  
976 ); 1152 );
  1153 +
  1154 + // Wrap with alignment widget
  1155 + switch (columnAlignments[index]) {
  1156 + case TextAlign.center:
  1157 + content = Center(child: content);
  1158 + break;
  1159 + case TextAlign.right:
  1160 + content = Align(
  1161 + alignment: Alignment.centerRight,
  1162 + child: content,
  1163 + );
  1164 + break;
  1165 + case TextAlign.left:
  1166 + default:
  1167 + content = Align(
  1168 + alignment: Alignment.centerLeft,
  1169 + child: content,
  1170 + );
  1171 + break;
  1172 + }
  1173 +
  1174 + return content;
977 }), 1175 }),
978 ), 1176 ),
979 ) 1177 )
@@ -995,10 +1193,54 @@ class CodeBlockMd extends BlockMd { @@ -995,10 +1193,54 @@ class CodeBlockMd extends BlockMd {
995 ) { 1193 ) {
996 String codes = this.exp.firstMatch(text)?[2] ?? ""; 1194 String codes = this.exp.firstMatch(text)?[2] ?? "";
997 String name = this.exp.firstMatch(text)?[1] ?? ""; 1195 String name = this.exp.firstMatch(text)?[1] ?? "";
998 - codes = codes.replaceAll(r"```", "").trim(); 1196 + codes = codes.replaceAll(r"```", "");
999 bool closed = text.endsWith("```"); 1197 bool closed = text.endsWith("```");
1000 1198
1001 return config.codeBuilder?.call(context, name, codes, closed) ?? 1199 return config.codeBuilder?.call(context, name, codes, closed) ??
1002 CodeField(name: name, codes: codes); 1200 CodeField(name: name, codes: codes);
1003 } 1201 }
1004 } 1202 }
  1203 +
  1204 +class UnderLineMd extends InlineMd {
  1205 + @override
  1206 + RegExp get exp =>
  1207 + RegExp(r"<u>(.*?)(?:</u>|$)", multiLine: true, dotAll: true);
  1208 +
  1209 + @override
  1210 + InlineSpan span(
  1211 + BuildContext context,
  1212 + String text,
  1213 + final GptMarkdownConfig config,
  1214 + ) {
  1215 + var match = exp.firstMatch(text.trim());
  1216 + var conf = config.copyWith(
  1217 + style: (config.style ?? const TextStyle()).copyWith(
  1218 + decoration: TextDecoration.underline,
  1219 + decorationColor: config.style?.color,
  1220 + ),
  1221 + );
  1222 + return TextSpan(
  1223 + children: MarkdownComponent.generate(
  1224 + context,
  1225 + "${match?[1]}",
  1226 + conf,
  1227 + false,
  1228 + ),
  1229 + style: conf.style,
  1230 + );
  1231 + }
  1232 +}
  1233 +
  1234 +class CustomTableField {
  1235 + final String data;
  1236 + final TextAlign alignment;
  1237 +
  1238 + CustomTableField({required this.data, this.alignment = TextAlign.left});
  1239 +}
  1240 +
  1241 +class CustomTableRow {
  1242 + final bool isHeader;
  1243 + final List<CustomTableField> fields;
  1244 +
  1245 + CustomTableRow({this.isHeader = false, required this.fields});
  1246 +}
1 part of 'gpt_markdown.dart'; 1 part of 'gpt_markdown.dart';
2 2
3 /// It creates a markdown widget closed to each other. 3 /// It creates a markdown widget closed to each other.
4 -class MdWidget extends StatelessWidget { 4 +class MdWidget extends StatefulWidget {
5 const MdWidget( 5 const MdWidget(
  6 + this.context,
6 this.exp, 7 this.exp,
7 this.includeGlobalComponents, { 8 this.includeGlobalComponents, {
8 super.key, 9 super.key,
@@ -11,6 +12,7 @@ class MdWidget extends StatelessWidget { @@ -11,6 +12,7 @@ class MdWidget extends StatelessWidget {
11 12
12 /// The expression to be displayed. 13 /// The expression to be displayed.
13 final String exp; 14 final String exp;
  15 + final BuildContext context;
14 16
15 /// Whether to include global components. 17 /// Whether to include global components.
16 final bool includeGlobalComponents; 18 final bool includeGlobalComponents;
@@ -19,25 +21,46 @@ class MdWidget extends StatelessWidget { @@ -19,25 +21,46 @@ class MdWidget extends StatelessWidget {
19 final GptMarkdownConfig config; 21 final GptMarkdownConfig config;
20 22
21 @override 23 @override
22 - Widget build(BuildContext context) {  
23 - List<InlineSpan> list = MarkdownComponent.generate( 24 + State<MdWidget> createState() => _MdWidgetState();
  25 +}
  26 +
  27 +class _MdWidgetState extends State<MdWidget> {
  28 + List<InlineSpan> list = [];
  29 + @override
  30 + void initState() {
  31 + super.initState();
  32 + list = MarkdownComponent.generate(
  33 + widget.context,
  34 + widget.exp,
  35 + widget.config,
  36 + widget.includeGlobalComponents,
  37 + );
  38 + }
  39 +
  40 + @override
  41 + void didUpdateWidget(covariant MdWidget oldWidget) {
  42 + super.didUpdateWidget(oldWidget);
  43 + if (oldWidget.exp != widget.exp ||
  44 + !oldWidget.config.isSame(widget.config)) {
  45 + list = MarkdownComponent.generate(
24 context, 46 context,
25 - exp,  
26 - // .replaceAllMapped(  
27 - // RegExp(  
28 - // r"\\\[(.*?)\\\]|(\\begin.*?\\end{.*?})",  
29 - // multiLine: true,  
30 - // dotAll: true,  
31 - // ), (match) {  
32 - // //  
33 - // String body = (match[1] ?? match[2])?.replaceAll("\n", " ") ?? "";  
34 - // return "\\[$body\\]";  
35 - // }),  
36 - config,  
37 - includeGlobalComponents, 47 + widget.exp,
  48 + widget.config,
  49 + widget.includeGlobalComponents,
38 ); 50 );
39 - return config.getRich(  
40 - TextSpan(children: list, style: config.style?.copyWith()), 51 + }
  52 + }
  53 +
  54 + @override
  55 + Widget build(BuildContext context) {
  56 + // List<InlineSpan> list = MarkdownComponent.generate(
  57 + // context,
  58 + // widget.exp,
  59 + // widget.config,
  60 + // widget.includeGlobalComponents,
  61 + // );
  62 + return widget.config.getRich(
  63 + TextSpan(children: list, style: widget.config.style?.copyWith()),
41 ); 64 );
42 } 65 }
43 } 66 }
1 name: gpt_markdown 1 name: gpt_markdown
2 -description: "Powerful Markdown & LaTeX Renderer for Flutter: Rich Text, Math, Tables, Links, and Text Selection. Ideal for ChatGPT, Gemini, and more."  
3 -version: 1.0.16 2 +description: "Powerful Flutter Markdown & LaTeX Renderer: Rich Text, Math, Tables, Links, and Text Selection. Ideal for ChatGPT, Gemini, and more."
  3 +version: 1.1.4
4 homepage: https://github.com/Infinitix-LLC/gpt_markdown 4 homepage: https://github.com/Infinitix-LLC/gpt_markdown
5 5
6 environment: 6 environment:
@@ -14,9 +14,13 @@ dependencies: @@ -14,9 +14,13 @@ dependencies:
14 dev_dependencies: 14 dev_dependencies:
15 flutter_test: 15 flutter_test:
16 sdk: flutter 16 sdk: flutter
17 -# flutter_lints: ^5.0.0 17 +# flutter_lints: ^6.0.0
18 18
19 flutter: 19 flutter:
  20 + fonts:
  21 + - family: JetBrainsMono
  22 + fonts:
  23 + - asset: lib/fonts/JetBrainsMono-Regular.ttf
20 24
21 topics: 25 topics:
22 - markdown 26 - markdown
@@ -24,3 +28,13 @@ topics: @@ -24,3 +28,13 @@ topics:
24 - selectable 28 - selectable
25 - chatgpt 29 - chatgpt
26 - gemini 30 - gemini
  31 +
  32 +keywords:
  33 + - flutter
  34 + - markdown
  35 + - flutter markdown
  36 + - gpt
  37 + - latex
  38 + - chatgpt
  39 + - rich text
  40 + - ai