Showing
2 changed files
with
185 additions
and
143 deletions
| 1 | import 'package:flutter/material.dart'; | 1 | import 'package:flutter/material.dart'; |
| 2 | +import 'package:flutter/services.dart'; | ||
| 2 | import 'package:flutter_math_fork/flutter_math.dart'; | 3 | import 'package:flutter_math_fork/flutter_math.dart'; |
| 3 | import 'package:gpt_markdown/custom_widgets/custom_divider.dart'; | 4 | import 'package:gpt_markdown/custom_widgets/custom_divider.dart'; |
| 4 | import 'package:gpt_markdown/custom_widgets/custom_error_image.dart'; | 5 | import 'package:gpt_markdown/custom_widgets/custom_error_image.dart'; |
| @@ -9,6 +10,9 @@ import 'md_widget.dart'; | @@ -9,6 +10,9 @@ import 'md_widget.dart'; | ||
| 9 | /// Markdown components | 10 | /// Markdown components |
| 10 | abstract class MarkdownComponent { | 11 | abstract class MarkdownComponent { |
| 11 | static List<MarkdownComponent> get components => [ | 12 | static List<MarkdownComponent> get components => [ |
| 13 | + CodeBlockMd(), | ||
| 14 | + NewLines(), | ||
| 15 | + TableMd(), | ||
| 12 | HTag(), | 16 | HTag(), |
| 13 | IndentMd(), | 17 | IndentMd(), |
| 14 | UnOrderedList(), | 18 | UnOrderedList(), |
| @@ -251,6 +255,29 @@ class HTag extends BlockMd { | @@ -251,6 +255,29 @@ class HTag extends BlockMd { | ||
| 251 | } | 255 | } |
| 252 | } | 256 | } |
| 253 | 257 | ||
| 258 | +class NewLines extends InlineMd { | ||
| 259 | + @override | ||
| 260 | + RegExp get exp => RegExp(r"\n\n+"); | ||
| 261 | + @override | ||
| 262 | + InlineSpan span( | ||
| 263 | + BuildContext context, | ||
| 264 | + String text, | ||
| 265 | + TextStyle? style, | ||
| 266 | + TextDirection textDirection, | ||
| 267 | + final void Function(String url, String title)? onLinkTab, | ||
| 268 | + final String Function(String tex)? latexWorkaround, | ||
| 269 | + final Widget Function(BuildContext context, String tex)? latexBuilder, | ||
| 270 | + ) { | ||
| 271 | + return TextSpan( | ||
| 272 | + text: "\n\n\n\n", | ||
| 273 | + style: TextStyle( | ||
| 274 | + fontSize: 6, | ||
| 275 | + color: style?.color, | ||
| 276 | + ), | ||
| 277 | + ); | ||
| 278 | + } | ||
| 279 | +} | ||
| 280 | + | ||
| 254 | /// Horizontal line component | 281 | /// Horizontal line component |
| 255 | class HrLine extends BlockMd { | 282 | class HrLine extends BlockMd { |
| 256 | @override | 283 | @override |
| @@ -760,6 +787,10 @@ class TableMd extends BlockMd { | @@ -760,6 +787,10 @@ class TableMd extends BlockMd { | ||
| 760 | .asMap(), | 787 | .asMap(), |
| 761 | ) | 788 | ) |
| 762 | .toList(); | 789 | .toList(); |
| 790 | + bool heading = RegExp( | ||
| 791 | + r"^\|.*?\|\n\|-[-\\ |]*?-\|$", | ||
| 792 | + multiLine: true, | ||
| 793 | + ).hasMatch(text.trim()); | ||
| 763 | int maxCol = 0; | 794 | int maxCol = 0; |
| 764 | for (final each in value) { | 795 | for (final each in value) { |
| 765 | if (maxCol < each.keys.length) { | 796 | if (maxCol < each.keys.length) { |
| @@ -770,26 +801,50 @@ class TableMd extends BlockMd { | @@ -770,26 +801,50 @@ class TableMd extends BlockMd { | ||
| 770 | return Text("", style: style); | 801 | return Text("", style: style); |
| 771 | } | 802 | } |
| 772 | return Table( | 803 | return Table( |
| 773 | - defaultVerticalAlignment: TableCellVerticalAlignment.middle, | ||
| 774 | textDirection: textDirection, | 804 | textDirection: textDirection, |
| 805 | + defaultColumnWidth: CustomTableColumnWidth(), | ||
| 806 | + defaultVerticalAlignment: TableCellVerticalAlignment.middle, | ||
| 775 | border: TableBorder.all( | 807 | border: TableBorder.all( |
| 776 | width: 1, | 808 | width: 1, |
| 777 | color: Theme.of(context).colorScheme.onSurface, | 809 | color: Theme.of(context).colorScheme.onSurface, |
| 778 | ), | 810 | ), |
| 779 | children: value | 811 | children: value |
| 812 | + .asMap() | ||
| 813 | + .entries | ||
| 780 | .map<TableRow>( | 814 | .map<TableRow>( |
| 781 | - (e) => TableRow( | 815 | + (entry) => TableRow( |
| 816 | + decoration: (heading) | ||
| 817 | + ? BoxDecoration( | ||
| 818 | + color: (entry.key == 0) | ||
| 819 | + ? Theme.of(context).colorScheme.surfaceVariant | ||
| 820 | + : null, | ||
| 821 | + ) | ||
| 822 | + : null, | ||
| 782 | children: List.generate( | 823 | children: List.generate( |
| 783 | maxCol, | 824 | maxCol, |
| 784 | - (index) => Center( | ||
| 785 | - child: MdWidget( | ||
| 786 | - (e[index] ?? "").trim(), | ||
| 787 | - textDirection: textDirection, | ||
| 788 | - onLinkTab: onLinkTab, | ||
| 789 | - style: style, | ||
| 790 | - latexWorkaround: latexWorkaround, | ||
| 791 | - ), | ||
| 792 | - ), | 825 | + (index) { |
| 826 | + var e = entry.value; | ||
| 827 | + String data = e[index] ?? ""; | ||
| 828 | + if (RegExp(r"^--+$").hasMatch(data.trim()) || | ||
| 829 | + data.trim().isEmpty) { | ||
| 830 | + return const SizedBox(); | ||
| 831 | + } | ||
| 832 | + | ||
| 833 | + return Center( | ||
| 834 | + child: Padding( | ||
| 835 | + padding: const EdgeInsets.symmetric( | ||
| 836 | + horizontal: 8, vertical: 4), | ||
| 837 | + child: MdWidget( | ||
| 838 | + (e[index] ?? "").trim(), | ||
| 839 | + textDirection: textDirection, | ||
| 840 | + onLinkTab: onLinkTab, | ||
| 841 | + style: style, | ||
| 842 | + latexWorkaround: latexWorkaround, | ||
| 843 | + latexBuilder: latexBuilder, | ||
| 844 | + ), | ||
| 845 | + ), | ||
| 846 | + ); | ||
| 847 | + }, | ||
| 793 | ), | 848 | ), |
| 794 | ), | 849 | ), |
| 795 | ) | 850 | ) |
| @@ -799,6 +854,105 @@ class TableMd extends BlockMd { | @@ -799,6 +854,105 @@ class TableMd extends BlockMd { | ||
| 799 | 854 | ||
| 800 | @override | 855 | @override |
| 801 | RegExp get exp => RegExp( | 856 | RegExp get exp => RegExp( |
| 802 | - r"(((\|[^\n\|]+\|)((([^\n\|]+\|)+)?))(\n(((\|[^\n\|]+\|)(([^\n\|]+\|)+)?)))+)?", | 857 | + r"^(((\|[^\n\|]+\|)((([^\n\|]+\|)+)?))(\n(((\|[^\n\|]+\|)(([^\n\|]+\|)+)?)))+)$", |
| 858 | + ); | ||
| 859 | +} | ||
| 860 | + | ||
| 861 | +class CodeBlockMd extends BlockMd { | ||
| 862 | + @override | ||
| 863 | + RegExp get exp => RegExp( | ||
| 864 | + r"\s*?```(.*?)\n((.*?)(:?\n\s*?```)|(.*)(:?\n```)?)$", | ||
| 865 | + multiLine: true, | ||
| 866 | + dotAll: true, | ||
| 803 | ); | 867 | ); |
| 868 | + @override | ||
| 869 | + Widget build( | ||
| 870 | + BuildContext context, | ||
| 871 | + String text, | ||
| 872 | + TextStyle? style, | ||
| 873 | + TextDirection textDirection, | ||
| 874 | + final void Function(String url, String title)? onLinkTab, | ||
| 875 | + final String Function(String tex)? latexWorkaround, | ||
| 876 | + final Widget Function(BuildContext context, String tex)? latexBuilder, | ||
| 877 | + ) { | ||
| 878 | + String codes = exp.firstMatch(text)?[2] ?? ""; | ||
| 879 | + String name = exp.firstMatch(text)?[1] ?? ""; | ||
| 880 | + codes = codes.replaceAll(r"```", "").trim(); | ||
| 881 | + return Padding( | ||
| 882 | + padding: const EdgeInsets.all(16.0), | ||
| 883 | + child: CodeField(name: name, codes: codes), | ||
| 884 | + ); | ||
| 885 | + } | ||
| 886 | +} | ||
| 887 | + | ||
| 888 | +class CodeField extends StatefulWidget { | ||
| 889 | + const CodeField({super.key, required this.name, required this.codes}); | ||
| 890 | + final String name; | ||
| 891 | + final String codes; | ||
| 892 | + | ||
| 893 | + @override | ||
| 894 | + State<CodeField> createState() => _CodeFieldState(); | ||
| 895 | +} | ||
| 896 | + | ||
| 897 | +class _CodeFieldState extends State<CodeField> { | ||
| 898 | + bool _copied = false; | ||
| 899 | + @override | ||
| 900 | + Widget build(BuildContext context) { | ||
| 901 | + return Material( | ||
| 902 | + color: Theme.of(context).colorScheme.onInverseSurface, | ||
| 903 | + shape: RoundedRectangleBorder( | ||
| 904 | + borderRadius: BorderRadius.circular(8), | ||
| 905 | + ), | ||
| 906 | + child: Column( | ||
| 907 | + crossAxisAlignment: CrossAxisAlignment.stretch, | ||
| 908 | + children: [ | ||
| 909 | + Row( | ||
| 910 | + children: [ | ||
| 911 | + Padding( | ||
| 912 | + padding: | ||
| 913 | + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), | ||
| 914 | + child: Text(widget.name), | ||
| 915 | + ), | ||
| 916 | + const Spacer(), | ||
| 917 | + TextButton.icon( | ||
| 918 | + style: TextButton.styleFrom( | ||
| 919 | + foregroundColor: Theme.of(context).colorScheme.onSurface, | ||
| 920 | + textStyle: const TextStyle( | ||
| 921 | + fontWeight: FontWeight.normal, | ||
| 922 | + ), | ||
| 923 | + ), | ||
| 924 | + onPressed: () async { | ||
| 925 | + await Clipboard.setData(ClipboardData(text: widget.codes)) | ||
| 926 | + .then((value) { | ||
| 927 | + setState(() { | ||
| 928 | + _copied = true; | ||
| 929 | + }); | ||
| 930 | + }); | ||
| 931 | + await Future.delayed(const Duration(seconds: 2)); | ||
| 932 | + setState(() { | ||
| 933 | + _copied = false; | ||
| 934 | + }); | ||
| 935 | + }, | ||
| 936 | + icon: Icon( | ||
| 937 | + (_copied) ? Icons.done : Icons.content_paste, | ||
| 938 | + size: 15, | ||
| 939 | + ), | ||
| 940 | + label: Text((_copied) ? "Copied!" : "Copy code"), | ||
| 941 | + ), | ||
| 942 | + ], | ||
| 943 | + ), | ||
| 944 | + const Divider( | ||
| 945 | + height: 1, | ||
| 946 | + ), | ||
| 947 | + SingleChildScrollView( | ||
| 948 | + scrollDirection: Axis.horizontal, | ||
| 949 | + padding: const EdgeInsets.all(16), | ||
| 950 | + child: Text( | ||
| 951 | + widget.codes, | ||
| 952 | + ), | ||
| 953 | + ), | ||
| 954 | + ], | ||
| 955 | + ), | ||
| 956 | + ); | ||
| 957 | + } | ||
| 804 | } | 958 | } |
| @@ -28,137 +28,25 @@ class MdWidget extends StatelessWidget { | @@ -28,137 +28,25 @@ class MdWidget extends StatelessWidget { | ||
| 28 | @override | 28 | @override |
| 29 | Widget build(BuildContext context) { | 29 | Widget build(BuildContext context) { |
| 30 | List<InlineSpan> list = []; | 30 | List<InlineSpan> list = []; |
| 31 | - exp.trim().splitMapJoin( | ||
| 32 | - RegExp(r"\n\n+"), | ||
| 33 | - onMatch: (p0) { | ||
| 34 | - list.add( | ||
| 35 | - TextSpan( | ||
| 36 | - text: "\n\n\n\n", | ||
| 37 | - style: TextStyle( | ||
| 38 | - fontSize: 6, | ||
| 39 | - color: style?.color, | ||
| 40 | - ), | ||
| 41 | - ), | ||
| 42 | - ); | ||
| 43 | - return ""; | ||
| 44 | - }, | ||
| 45 | - onNonMatch: (eachLn) { | ||
| 46 | - final RegExp table = RegExp( | ||
| 47 | - r"^(((\|[^\n\|]+\|)((([^\n\|]+\|)+)?))(\n(((\|[^\n\|]+\|)(([^\n\|]+\|)+)?)))+)$", | ||
| 48 | - ); | ||
| 49 | - if (table.hasMatch(eachLn.trim())) { | ||
| 50 | - final List<Map<int, String>> value = eachLn | ||
| 51 | - .trim() | ||
| 52 | - .split('\n') | ||
| 53 | - .map<Map<int, String>>( | ||
| 54 | - (e) => e | ||
| 55 | - .split('|') | ||
| 56 | - .where((element) => element.isNotEmpty) | ||
| 57 | - .toList() | ||
| 58 | - .asMap(), | ||
| 59 | - ) | ||
| 60 | - .toList(); | ||
| 61 | - bool heading = RegExp( | ||
| 62 | - r"^\|.*?\|\n\|-[-\\ |]*?-\|$", | ||
| 63 | - multiLine: true, | ||
| 64 | - ).hasMatch(eachLn.trim()); | ||
| 65 | - int maxCol = 0; | ||
| 66 | - for (final each in value) { | ||
| 67 | - if (maxCol < each.keys.length) { | ||
| 68 | - maxCol = each.keys.length; | ||
| 69 | - } | ||
| 70 | - } | ||
| 71 | - list.addAll( | ||
| 72 | - [ | ||
| 73 | - TextSpan( | ||
| 74 | - text: "\n ", | ||
| 75 | - style: TextStyle(height: 0, fontSize: 0, color: style?.color), | ||
| 76 | - ), | ||
| 77 | - WidgetSpan( | ||
| 78 | - child: Table( | ||
| 79 | - textDirection: textDirection, | ||
| 80 | - defaultColumnWidth: CustomTableColumnWidth(), | ||
| 81 | - defaultVerticalAlignment: TableCellVerticalAlignment.middle, | ||
| 82 | - border: TableBorder.all( | ||
| 83 | - width: 1, | ||
| 84 | - color: Theme.of(context).colorScheme.onSurface, | ||
| 85 | - ), | ||
| 86 | - children: value | ||
| 87 | - .asMap() | ||
| 88 | - .entries | ||
| 89 | - .map<TableRow>( | ||
| 90 | - (entry) => TableRow( | ||
| 91 | - decoration: (heading) | ||
| 92 | - ? BoxDecoration( | ||
| 93 | - color: (entry.key == 0) | ||
| 94 | - ? Theme.of(context) | ||
| 95 | - .colorScheme | ||
| 96 | - .surfaceVariant | ||
| 97 | - : null, | ||
| 98 | - ) | ||
| 99 | - : null, | ||
| 100 | - children: List.generate( | ||
| 101 | - maxCol, | ||
| 102 | - (index) { | ||
| 103 | - var e = entry.value; | ||
| 104 | - String data = e[index] ?? ""; | ||
| 105 | - if (RegExp(r"^--+$").hasMatch(data.trim()) || | ||
| 106 | - data.trim().isEmpty) { | ||
| 107 | - return const SizedBox(); | ||
| 108 | - } | ||
| 109 | - | ||
| 110 | - return Center( | ||
| 111 | - child: Padding( | ||
| 112 | - padding: const EdgeInsets.symmetric( | ||
| 113 | - horizontal: 8, vertical: 4), | ||
| 114 | - child: MdWidget( | ||
| 115 | - (e[index] ?? "").trim(), | ||
| 116 | - textDirection: textDirection, | ||
| 117 | - onLinkTab: onLinkTab, | ||
| 118 | - style: style, | ||
| 119 | - latexWorkaround: latexWorkaround, | ||
| 120 | - latexBuilder: latexBuilder, | ||
| 121 | - ), | ||
| 122 | - ), | ||
| 123 | - ); | ||
| 124 | - }, | ||
| 125 | - ), | ||
| 126 | - ), | ||
| 127 | - ) | ||
| 128 | - .toList(), | ||
| 129 | - ), | ||
| 130 | - ), | ||
| 131 | - TextSpan( | ||
| 132 | - text: "\n ", | ||
| 133 | - style: TextStyle(height: 0, fontSize: 0, color: style?.color), | ||
| 134 | - ), | ||
| 135 | - ], | ||
| 136 | - ); | ||
| 137 | - } else { | ||
| 138 | - list.addAll( | ||
| 139 | - MarkdownComponent.generate( | ||
| 140 | - context, | ||
| 141 | - eachLn.replaceAllMapped( | ||
| 142 | - RegExp( | ||
| 143 | - r"\\\[(.*?)\\\]|(\\begin.*?\\end{.*?})", | ||
| 144 | - multiLine: true, | ||
| 145 | - dotAll: true, | ||
| 146 | - ), (match) { | ||
| 147 | - // | ||
| 148 | - String body = | ||
| 149 | - (match[1] ?? match[2])?.replaceAll("\n", " ") ?? ""; | ||
| 150 | - return "\\[$body\\]"; | ||
| 151 | - }), | ||
| 152 | - style, | ||
| 153 | - textDirection, | ||
| 154 | - onLinkTab, | ||
| 155 | - latexWorkaround, | ||
| 156 | - latexBuilder, | ||
| 157 | - ), | ||
| 158 | - ); | ||
| 159 | - } | ||
| 160 | - return ""; | ||
| 161 | - }, | 31 | + list.addAll( |
| 32 | + MarkdownComponent.generate( | ||
| 33 | + context, | ||
| 34 | + exp.replaceAllMapped( | ||
| 35 | + RegExp( | ||
| 36 | + r"\\\[(.*?)\\\]|(\\begin.*?\\end{.*?})", | ||
| 37 | + multiLine: true, | ||
| 38 | + dotAll: true, | ||
| 39 | + ), (match) { | ||
| 40 | + // | ||
| 41 | + String body = (match[1] ?? match[2])?.replaceAll("\n", " ") ?? ""; | ||
| 42 | + return "\\[$body\\]"; | ||
| 43 | + }), | ||
| 44 | + style, | ||
| 45 | + textDirection, | ||
| 46 | + onLinkTab, | ||
| 47 | + latexWorkaround, | ||
| 48 | + latexBuilder, | ||
| 49 | + ), | ||
| 162 | ); | 50 | ); |
| 163 | return Text.rich( | 51 | return Text.rich( |
| 164 | TextSpan( | 52 | TextSpan( |
-
Please register or login to post a comment