Showing
4 changed files
with
108 additions
and
26 deletions
@@ -99,6 +99,7 @@ class MarkdownHelper { | @@ -99,6 +99,7 @@ class MarkdownHelper { | ||
99 | 99 | ||
100 | You can use Markdown to format text easily. Here are some examples: | 100 | You can use Markdown to format text easily. Here are some examples: |
101 | 101 | ||
102 | +- `Highlighted Text`: `This text is highlighted` | ||
102 | - **Bold Text**: **This text is bold** | 103 | - **Bold Text**: **This text is bold** |
103 | - *Italic Text*: *This text is italicized* | 104 | - *Italic Text*: *This text is italicized* |
104 | - [Link](https://www.example.com): [This is a link](https://www.example.com) | 105 | - [Link](https://www.example.com): [This is a link](https://www.example.com) |
@@ -266,6 +267,29 @@ Markdown and LaTeX can be powerful tools for formatting text and mathematical ex | @@ -266,6 +267,29 @@ Markdown and LaTeX can be powerful tools for formatting text and mathematical ex | ||
266 | style: const TextStyle( | 267 | style: const TextStyle( |
267 | fontSize: 15, | 268 | fontSize: 15, |
268 | ), | 269 | ), |
270 | + highlightBuilder: (context, text, style) { | ||
271 | + return Container( | ||
272 | + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), | ||
273 | + decoration: BoxDecoration( | ||
274 | + color: Theme.of(context).colorScheme.secondaryContainer, | ||
275 | + borderRadius: BorderRadius.circular(4), | ||
276 | + border: Border.all( | ||
277 | + color: Theme.of(context).colorScheme.secondary.withOpacity(0.5), | ||
278 | + width: 1, | ||
279 | + ), | ||
280 | + ), | ||
281 | + child: Text( | ||
282 | + text, | ||
283 | + style: TextStyle( | ||
284 | + color: Theme.of(context).colorScheme.onSecondaryContainer, | ||
285 | + fontFamily: 'monospace', | ||
286 | + fontWeight: FontWeight.bold, | ||
287 | + fontSize: style.fontSize != null ? style.fontSize! * 0.9 : 13.5, | ||
288 | + height: style.height, | ||
289 | + ), | ||
290 | + ), | ||
291 | + ); | ||
292 | + }, | ||
269 | latexWorkaround: (tex) { | 293 | latexWorkaround: (tex) { |
270 | List<String> stack = []; | 294 | List<String> stack = []; |
271 | tex = tex.splitMapJoin( | 295 | tex = tex.splitMapJoin( |
@@ -377,6 +401,20 @@ Markdown and LaTeX can be powerful tools for formatting text and mathematical ex | @@ -377,6 +401,20 @@ Markdown and LaTeX can be powerful tools for formatting text and mathematical ex | ||
377 | ); | 401 | ); |
378 | }, | 402 | }, |
379 | ); | 403 | ); |
404 | + codeBuilder: (context, name, code) { | ||
405 | + return Padding( | ||
406 | + padding: const EdgeInsets.symmetric(horizontal: 16), | ||
407 | + child: Text( | ||
408 | + code.trim(), | ||
409 | + style: TextStyle( | ||
410 | + fontFamily: 'JetBrains Mono', | ||
411 | + fontSize: 14, | ||
412 | + height: 1.5, | ||
413 | + color: Theme.of(context).colorScheme.onSurface, | ||
414 | + ), | ||
415 | + ), | ||
416 | + ); | ||
417 | + }; | ||
380 | if (selectable) { | 418 | if (selectable) { |
381 | child = SelectionArea( | 419 | child = SelectionArea( |
382 | child: child, | 420 | child: child, |
@@ -12,6 +12,8 @@ class GptMarkdownConfig { | @@ -12,6 +12,8 @@ class GptMarkdownConfig { | ||
12 | this.followLinkColor = false, | 12 | this.followLinkColor = false, |
13 | this.codeBuilder, | 13 | this.codeBuilder, |
14 | this.sourceTagBuilder, | 14 | this.sourceTagBuilder, |
15 | + this.highlightBuilder, | ||
16 | + this.linkBuilder, | ||
15 | this.maxLines, | 17 | this.maxLines, |
16 | this.overflow, | 18 | this.overflow, |
17 | }); | 19 | }); |
@@ -32,6 +34,8 @@ class GptMarkdownConfig { | @@ -32,6 +34,8 @@ class GptMarkdownConfig { | ||
32 | codeBuilder; | 34 | codeBuilder; |
33 | final int? maxLines; | 35 | final int? maxLines; |
34 | final TextOverflow? overflow; | 36 | final TextOverflow? overflow; |
37 | + final Widget Function(BuildContext context, String text, TextStyle style)? highlightBuilder; | ||
38 | + final Widget Function(BuildContext context, String text, String url, TextStyle style)? linkBuilder; | ||
35 | 39 | ||
36 | GptMarkdownConfig copyWith({ | 40 | GptMarkdownConfig copyWith({ |
37 | TextStyle? style, | 41 | TextStyle? style, |
@@ -51,6 +55,8 @@ class GptMarkdownConfig { | @@ -51,6 +55,8 @@ class GptMarkdownConfig { | ||
51 | codeBuilder, | 55 | codeBuilder, |
52 | final int? maxLines, | 56 | final int? maxLines, |
53 | final TextOverflow? overflow, | 57 | final TextOverflow? overflow, |
58 | + final Widget Function(BuildContext context, String text, TextStyle style)? highlightBuilder, | ||
59 | + final Widget Function(BuildContext context, String text, String url, TextStyle style)? linkBuilder, | ||
54 | }) { | 60 | }) { |
55 | return GptMarkdownConfig( | 61 | return GptMarkdownConfig( |
56 | style: style ?? this.style, | 62 | style: style ?? this.style, |
@@ -65,6 +71,8 @@ class GptMarkdownConfig { | @@ -65,6 +71,8 @@ class GptMarkdownConfig { | ||
65 | sourceTagBuilder: sourceTagBuilder ?? this.sourceTagBuilder, | 71 | sourceTagBuilder: sourceTagBuilder ?? this.sourceTagBuilder, |
66 | maxLines: maxLines ?? this.maxLines, | 72 | maxLines: maxLines ?? this.maxLines, |
67 | overflow: overflow ?? this.overflow, | 73 | overflow: overflow ?? this.overflow, |
74 | + highlightBuilder: highlightBuilder ?? this.highlightBuilder, | ||
75 | + linkBuilder: linkBuilder ?? this.linkBuilder, | ||
68 | ); | 76 | ); |
69 | } | 77 | } |
70 | 78 |
@@ -34,6 +34,8 @@ class GptMarkdown extends StatelessWidget { | @@ -34,6 +34,8 @@ class GptMarkdown extends StatelessWidget { | ||
34 | this.latexBuilder, | 34 | this.latexBuilder, |
35 | this.codeBuilder, | 35 | this.codeBuilder, |
36 | this.sourceTagBuilder, | 36 | this.sourceTagBuilder, |
37 | + this.highlightBuilder, | ||
38 | + this.linkBuilder, | ||
37 | this.maxLines, | 39 | this.maxLines, |
38 | this.overflow, | 40 | this.overflow, |
39 | }); | 41 | }); |
@@ -53,6 +55,8 @@ class GptMarkdown extends StatelessWidget { | @@ -53,6 +55,8 @@ class GptMarkdown extends StatelessWidget { | ||
53 | final Widget Function(BuildContext context, String name, String code)? | 55 | final Widget Function(BuildContext context, String name, String code)? |
54 | codeBuilder; | 56 | codeBuilder; |
55 | final Widget Function(BuildContext, String, TextStyle)? sourceTagBuilder; | 57 | final Widget Function(BuildContext, String, TextStyle)? sourceTagBuilder; |
58 | + final Widget Function(BuildContext context, String text, TextStyle style)? highlightBuilder; | ||
59 | + final Widget Function(BuildContext context, String text, String url, TextStyle style)? linkBuilder; | ||
56 | 60 | ||
57 | @override | 61 | @override |
58 | Widget build(BuildContext context) { | 62 | Widget build(BuildContext context) { |
@@ -93,6 +97,8 @@ class GptMarkdown extends StatelessWidget { | @@ -93,6 +97,8 @@ class GptMarkdown extends StatelessWidget { | ||
93 | maxLines: maxLines, | 97 | maxLines: maxLines, |
94 | overflow: overflow, | 98 | overflow: overflow, |
95 | sourceTagBuilder: sourceTagBuilder, | 99 | sourceTagBuilder: sourceTagBuilder, |
100 | + highlightBuilder: highlightBuilder, | ||
101 | + linkBuilder: linkBuilder, | ||
96 | ), | 102 | ), |
97 | )); | 103 | )); |
98 | } | 104 | } |
@@ -422,25 +422,37 @@ class HighlightedText extends InlineMd { | @@ -422,25 +422,37 @@ class HighlightedText extends InlineMd { | ||
422 | final GptMarkdownConfig config, | 422 | final GptMarkdownConfig config, |
423 | ) { | 423 | ) { |
424 | var match = exp.firstMatch(text.trim()); | 424 | var match = exp.firstMatch(text.trim()); |
425 | - var conf = config.copyWith( | ||
426 | - style: config.style?.copyWith( | ||
427 | - fontWeight: FontWeight.bold, | ||
428 | - background: Paint() | ||
429 | - ..color = GptMarkdownTheme.of(context).highlightColor | ||
430 | - ..strokeCap = StrokeCap.round | ||
431 | - ..strokeJoin = StrokeJoin.round, | ||
432 | - ) ?? | ||
433 | - TextStyle( | ||
434 | - fontWeight: FontWeight.bold, | ||
435 | - background: Paint() | ||
436 | - ..color = GptMarkdownTheme.of(context).highlightColor | ||
437 | - ..strokeCap = StrokeCap.round | ||
438 | - ..strokeJoin = StrokeJoin.round, | ||
439 | - ), | ||
440 | - ); | 425 | + var highlightedText = match?[1] ?? ""; |
426 | + | ||
427 | + if (config.highlightBuilder != null) { | ||
428 | + return WidgetSpan( | ||
429 | + alignment: PlaceholderAlignment.middle, | ||
430 | + child: config.highlightBuilder!( | ||
431 | + context, | ||
432 | + highlightedText, | ||
433 | + config.style ?? const TextStyle(), | ||
434 | + ), | ||
435 | + ); | ||
436 | + } | ||
437 | + | ||
438 | + var style = config.style?.copyWith( | ||
439 | + fontWeight: FontWeight.bold, | ||
440 | + background: Paint() | ||
441 | + ..color = GptMarkdownTheme.of(context).highlightColor | ||
442 | + ..strokeCap = StrokeCap.round | ||
443 | + ..strokeJoin = StrokeJoin.round, | ||
444 | + ) ?? | ||
445 | + TextStyle( | ||
446 | + fontWeight: FontWeight.bold, | ||
447 | + background: Paint() | ||
448 | + ..color = GptMarkdownTheme.of(context).highlightColor | ||
449 | + ..strokeCap = StrokeCap.round | ||
450 | + ..strokeJoin = StrokeJoin.round, | ||
451 | + ); | ||
452 | + | ||
441 | return TextSpan( | 453 | return TextSpan( |
442 | - text: match?[1], | ||
443 | - style: conf.style, | 454 | + text: highlightedText, |
455 | + style: style, | ||
444 | ); | 456 | ); |
445 | } | 457 | } |
446 | } | 458 | } |
@@ -721,15 +733,35 @@ class ATagMd extends InlineMd { | @@ -721,15 +733,35 @@ class ATagMd extends InlineMd { | ||
721 | if (match?[1] == null && match?[2] == null) { | 733 | if (match?[1] == null && match?[2] == null) { |
722 | return const TextSpan(); | 734 | return const TextSpan(); |
723 | } | 735 | } |
736 | + | ||
737 | + final linkText = match?[1] ?? ""; | ||
738 | + final url = match?[2] ?? ""; | ||
739 | + | ||
740 | + // Use custom builder if provided | ||
741 | + if (config.linkBuilder != null) { | ||
742 | + return WidgetSpan( | ||
743 | + child: GestureDetector( | ||
744 | + onTap: () => config.onLinkTab?.call(url, linkText), | ||
745 | + child: config.linkBuilder!( | ||
746 | + context, | ||
747 | + linkText, | ||
748 | + url, | ||
749 | + config.style ?? const TextStyle(), | ||
750 | + ), | ||
751 | + ), | ||
752 | + ); | ||
753 | + } | ||
754 | + | ||
755 | + // Default rendering | ||
724 | var theme = GptMarkdownTheme.of(context); | 756 | var theme = GptMarkdownTheme.of(context); |
725 | return WidgetSpan( | 757 | return WidgetSpan( |
726 | child: LinkButton( | 758 | child: LinkButton( |
727 | hoverColor: theme.linkHoverColor, | 759 | hoverColor: theme.linkHoverColor, |
728 | color: theme.linkColor, | 760 | color: theme.linkColor, |
729 | onPressed: () { | 761 | onPressed: () { |
730 | - config.onLinkTab?.call("${match?[2]}", "${match?[1]}"); | 762 | + config.onLinkTab?.call(url, linkText); |
731 | }, | 763 | }, |
732 | - text: match?[1] ?? "", | 764 | + text: linkText, |
733 | config: config, | 765 | config: config, |
734 | ), | 766 | ), |
735 | ); | 767 | ); |
@@ -892,11 +924,9 @@ class CodeBlockMd extends BlockMd { | @@ -892,11 +924,9 @@ class CodeBlockMd extends BlockMd { | ||
892 | String codes = this.exp.firstMatch(text)?[2] ?? ""; | 924 | String codes = this.exp.firstMatch(text)?[2] ?? ""; |
893 | String name = this.exp.firstMatch(text)?[1] ?? ""; | 925 | String name = this.exp.firstMatch(text)?[1] ?? ""; |
894 | codes = codes.replaceAll(r"```", "").trim(); | 926 | codes = codes.replaceAll(r"```", "").trim(); |
895 | - return Padding( | ||
896 | - padding: const EdgeInsets.all(16.0), | ||
897 | - child: config.codeBuilder != null | ||
898 | - ? config.codeBuilder?.call(context, name, codes) | ||
899 | - : CodeField(name: name, codes: codes), | ||
900 | - ); | 927 | + |
928 | + return config.codeBuilder != null | ||
929 | + ? config.codeBuilder!(context, name, codes) | ||
930 | + : CodeField(name: name, codes: codes); | ||
901 | } | 931 | } |
902 | } | 932 | } |
-
Please register or login to post a comment