From 5d7449d7a3817bfb08dc9049f7865b1e8f0350dc Mon Sep 17 00:00:00 2001 From: Tolga Akcaoglu Date: Sun, 21 Dec 2025 08:57:07 +0300 Subject: [PATCH 1/3] feat: Initialize `gpt_markdown` package with core markdown rendering components, custom widgets, theme, and example project setup including `desktop_drop` plugin registration. --- example/pubspec.lock | 10 +- lib/custom_widgets/code_field.dart | 3 +- lib/markdown_component.dart | 5 +- lib/md_widget.dart | 27 +++-- lib/theme.dart | 17 ++++ test/gpt_markdown_test.dart | 158 ++++++++++++++++++++++++++++- 6 files changed, 195 insertions(+), 25 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 3b4f285..a183498 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -126,7 +126,7 @@ packages: path: ".." relative: true source: path - version: "1.1.3" + version: "1.1.4" http: dependency: transitive description: @@ -195,10 +195,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" nested: dependency: transitive description: @@ -288,10 +288,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" tuple: dependency: transitive description: diff --git a/lib/custom_widgets/code_field.dart b/lib/custom_widgets/code_field.dart index 79c2673..7f849c6 100644 --- a/lib/custom_widgets/code_field.dart +++ b/lib/custom_widgets/code_field.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:gpt_markdown/gpt_markdown.dart'; /// A widget that displays code with syntax highlighting and a copy button. /// @@ -27,7 +28,7 @@ class _CodeFieldState extends State { @override Widget build(BuildContext context) { return Material( - color: Theme.of(context).colorScheme.onInverseSurface, + color: GptMarkdownTheme.of(context).codeBlockBackgroundColor, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/lib/markdown_component.dart b/lib/markdown_component.dart index fca1afa..12103ec 100644 --- a/lib/markdown_component.dart +++ b/lib/markdown_component.dart @@ -273,10 +273,7 @@ class HrLine extends BlockMd { ) { var thickness = GptMarkdownTheme.of(context).hrLineThickness; var color = GptMarkdownTheme.of(context).hrLineColor; - return CustomDivider( - height: thickness, - color: config.style?.color ?? color, - ); + return CustomDivider(height: thickness, color: color); } } diff --git a/lib/md_widget.dart b/lib/md_widget.dart index 16ddb9d..81d9134 100644 --- a/lib/md_widget.dart +++ b/lib/md_widget.dart @@ -27,14 +27,9 @@ class MdWidget extends StatefulWidget { class _MdWidgetState extends State { List list = []; @override - void initState() { - super.initState(); - list = MarkdownComponent.generate( - widget.context, - widget.exp, - widget.config, - widget.includeGlobalComponents, - ); + void didChangeDependencies() { + super.didChangeDependencies(); + _updateList(); } @override @@ -42,15 +37,19 @@ class _MdWidgetState extends State { super.didUpdateWidget(oldWidget); if (oldWidget.exp != widget.exp || !oldWidget.config.isSame(widget.config)) { - list = MarkdownComponent.generate( - context, - widget.exp, - widget.config, - widget.includeGlobalComponents, - ); + _updateList(); } } + void _updateList() { + list = MarkdownComponent.generate( + context, + widget.exp, + widget.config, + widget.includeGlobalComponents, + ); + } + @override Widget build(BuildContext context) { // List list = MarkdownComponent.generate( diff --git a/lib/theme.dart b/lib/theme.dart index b826418..e9cdd0e 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -14,6 +14,7 @@ class GptMarkdownThemeData extends ThemeExtension { required this.hrLineColor, required this.linkColor, required this.linkHoverColor, + required this.codeBlockBackgroundColor, }); /// A factory constructor for `GptMarkdownThemeData`. @@ -30,6 +31,7 @@ class GptMarkdownThemeData extends ThemeExtension { Color? hrLineColor, Color? linkColor, Color? linkHoverColor, + Color? codeBlockBackgroundColor, }) { ThemeData themeData = switch (brightness) { Brightness.light => ThemeData.light(), @@ -68,6 +70,7 @@ class GptMarkdownThemeData extends ThemeExtension { hrLineColor: hrLineColor, linkColor: linkColor, linkHoverColor: linkHoverColor, + codeBlockBackgroundColor: codeBlockBackgroundColor, ); } @@ -87,6 +90,7 @@ class GptMarkdownThemeData extends ThemeExtension { hrLineColor: theme.colorScheme.outline, linkColor: Colors.blue, linkHoverColor: Colors.red, + codeBlockBackgroundColor: theme.colorScheme.onInverseSurface, ); } @@ -121,6 +125,9 @@ class GptMarkdownThemeData extends ThemeExtension { /// The color of the link when hovering. Color linkHoverColor; + /// The background color of the code block. + Color codeBlockBackgroundColor; + /// A method to copy the `GptMarkdownThemeData`. @override GptMarkdownThemeData copyWith({ @@ -135,6 +142,7 @@ class GptMarkdownThemeData extends ThemeExtension { Color? hrLineColor, Color? linkColor, Color? linkHoverColor, + Color? codeBlockBackgroundColor, }) { return GptMarkdownThemeData._( highlightColor: highlightColor ?? this.highlightColor, @@ -148,6 +156,8 @@ class GptMarkdownThemeData extends ThemeExtension { hrLineColor: hrLineColor ?? this.hrLineColor, linkColor: linkColor ?? this.linkColor, linkHoverColor: linkHoverColor ?? this.linkHoverColor, + codeBlockBackgroundColor: + codeBlockBackgroundColor ?? this.codeBlockBackgroundColor, ); } @@ -173,6 +183,13 @@ class GptMarkdownThemeData extends ThemeExtension { linkColor: Color.lerp(linkColor, other.linkColor, t) ?? linkColor, linkHoverColor: Color.lerp(linkHoverColor, other.linkHoverColor, t) ?? linkHoverColor, + codeBlockBackgroundColor: + Color.lerp( + codeBlockBackgroundColor, + other.codeBlockBackgroundColor, + t, + ) ?? + codeBlockBackgroundColor, ); } } diff --git a/test/gpt_markdown_test.dart b/test/gpt_markdown_test.dart index 0da434d..44f17a8 100644 --- a/test/gpt_markdown_test.dart +++ b/test/gpt_markdown_test.dart @@ -1,5 +1,161 @@ +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:gpt_markdown/gpt_markdown.dart'; +import 'package:gpt_markdown/custom_widgets/custom_divider.dart'; +import 'package:gpt_markdown/custom_widgets/link_button.dart'; void main() { - test('adds one to input values', () {}); + testWidgets('GptMarkdownTheme.of returns correct data', (tester) async { + const linkColor = Colors.green; + await tester.pumpWidget( + MaterialApp( + home: GptMarkdownTheme( + gptThemeData: GptMarkdownThemeData( + brightness: Brightness.light, + linkColor: linkColor, + ), + child: Builder( + builder: (context) { + final theme = GptMarkdownTheme.of(context); + if (theme.linkColor != linkColor) { + // This print will definitely show if logic is reached + print( + 'TEST DEBUG: Expected $linkColor, got ${theme.linkColor}', + ); + } + return Text('Test', style: TextStyle(color: theme.linkColor)); + }, + ), + ), + ), + ); + + expect(find.text('Test'), findsOneWidget); + final text = tester.widget(find.text('Test')); + expect(text.style?.color, linkColor); + }); + + testWidgets('GptMarkdownTheme.of returns correct data from extension', ( + tester, + ) async { + const linkColor = Colors.purple; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + extensions: [ + GptMarkdownThemeData( + brightness: Brightness.light, + linkColor: linkColor, + ), + ], + ), + home: Builder( + builder: (context) { + final theme = GptMarkdownTheme.of(context); + if (theme.linkColor != linkColor) { + print( + 'TEST DEBUG: Expected $linkColor from extension, got ${theme.linkColor}', + ); + } + return Text( + 'Test Extension', + style: TextStyle(color: theme.linkColor), + ); + }, + ), + ), + ); + + expect(find.text('Test Extension'), findsOneWidget); + final text = tester.widget(find.text('Test Extension')); + expect(text.style?.color, linkColor); + }); + + testWidgets('HrLine uses theme color even when GptMarkdown has style color', ( + tester, + ) async { + const hrColor = Colors.pink; + const textColor = Colors.grey; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: GptMarkdownTheme( + gptThemeData: GptMarkdownThemeData( + brightness: Brightness.light, + hrLineColor: hrColor, + ), + child: const GptMarkdown( + '---\n[Link](url)', + style: TextStyle(color: textColor), + ), + ), + ), + ), + ); + + final dividerFinder = find.byType(CustomDivider); + expect(dividerFinder, findsOneWidget); + final divider = tester.widget(dividerFinder); + expect(divider.color, hrColor); + + final linkButtonFinder = find.byType(LinkButton); + expect(linkButtonFinder, findsOneWidget); + final linkButton = tester.widget(linkButtonFinder); + // Should be default blue if not specified in theme, overriding global text color + expect(linkButton.color, Colors.blue); + }); + + testWidgets('CodeField uses codeBlockBackgroundColor from theme', ( + tester, + ) async { + const codeBgColor = Colors.yellow; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: GptMarkdownTheme( + gptThemeData: GptMarkdownThemeData( + brightness: Brightness.light, + codeBlockBackgroundColor: codeBgColor, + ), + // Markdown code block syntax + child: const GptMarkdown('```dart\ncode\n```'), + ), + ), + ), + ); + + // CodeField renders a Material widget for background + // We need to find the CodeField, then finding the Material widget inside it could be ambiguous + // because GptMarkdown might use Material elsewhere (less likely). + // Let's find by type CodeField from 'package:gpt_markdown/custom_widgets/code_field.dart' + // But it's not exported. + // However, we imported 'package:gpt_markdown/custom_widgets/code_field.dart' ? + // Check imports. CodeField is likely not imported in the test file yet. + // We can infer it by finding a Material with specific color if we are lazy, + // or properly import CodeField. + // Since CodeField is in lib/custom_widgets/code_field.dart and we can import it. + + final materialFinder = find.descendant( + of: find.byType( + Column, + ), // CodeField is a Column inside Material? No, Material -> Column. + matching: find.byType(Material), + ); + // CodeField struct: Material -> Column. + // So finding Material that wraps the code text. + // Let's grab the Material widget that is the first child of the CodeField (if we could find CodeField). + // Or just find all Material widgets and check if one has the color. + + final materials = tester.widgetList(find.byType(Material)); + // The Scaffold has Material, etc. + // We expect one Material with our color. + final matchingMaterial = materials.firstWhere( + (m) => m.color == codeBgColor, + orElse: () => throw Exception('No material with codeBgColor found'), + ); + + expect(matchingMaterial, isNotNull); + }); } From 0cb82d354bdf26c27e925bac970fe49090fddde4 Mon Sep 17 00:00:00 2001 From: Tolga Akcaoglu Date: Sun, 21 Dec 2025 09:54:57 +0300 Subject: [PATCH 2/3] feat: Add `MarkdownComponent` for structured markdown parsing and include theme integration tests. --- lib/markdown_component.dart | 63 ++++++++++++++++++++--------------- test/gpt_markdown_test.dart | 66 +++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 27 deletions(-) diff --git a/lib/markdown_component.dart b/lib/markdown_component.dart index 12103ec..b24d076 100644 --- a/lib/markdown_component.dart +++ b/lib/markdown_component.dart @@ -471,25 +471,22 @@ class HighlightedText extends InlineMd { ); } - var style = - config.style?.copyWith( - fontWeight: FontWeight.bold, - background: - Paint() - ..color = GptMarkdownTheme.of(context).highlightColor - ..strokeCap = StrokeCap.round - ..strokeJoin = StrokeJoin.round, - ) ?? - TextStyle( - fontWeight: FontWeight.bold, - background: - Paint() - ..color = GptMarkdownTheme.of(context).highlightColor - ..strokeCap = StrokeCap.round - ..strokeJoin = StrokeJoin.round, - ); - - return TextSpan(text: highlightedText, style: style); + return WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 5), + decoration: BoxDecoration( + color: GptMarkdownTheme.of(context).highlightColor, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + highlightedText, + style: + config.style?.copyWith(fontWeight: FontWeight.bold) ?? + const TextStyle(fontWeight: FontWeight.bold), + ), + ), + ); } } @@ -846,19 +843,31 @@ class ATagMd extends InlineMd { var ending = text.substring(urlEnd + 1); + var theme = GptMarkdownTheme.of(context); + var linkConfig = config.copyWith( + style: + config.style?.copyWith( + color: theme.linkColor, + decorationColor: theme.linkColor, + ) ?? + TextStyle(color: theme.linkColor, decorationColor: theme.linkColor), + ); + var endingSpans = MarkdownComponent.generate( context, ending, config, false, ); - var theme = GptMarkdownTheme.of(context); + var linkTextSpan = TextSpan( - children: MarkdownComponent.generate(context, linkText, config, false), - style: config.style?.copyWith( - color: theme.linkColor, - decorationColor: theme.linkColor, + children: MarkdownComponent.generate( + context, + linkText, + linkConfig, + false, ), + style: linkConfig.style, ); // Use custom builder if provided @@ -1100,7 +1109,7 @@ class TableMd extends BlockMd { defaultVerticalAlignment: TableCellVerticalAlignment.middle, border: TableBorder.all( width: 1, - color: Theme.of(context).colorScheme.onSurface, + color: GptMarkdownTheme.of(context).hrLineColor, ), children: value @@ -1119,9 +1128,9 @@ class TableMd extends BlockMd { (hasHeader && entry.key == 0) ? BoxDecoration( color: - Theme.of( + GptMarkdownTheme.of( context, - ).colorScheme.surfaceContainerHighest, + ).codeBlockBackgroundColor, ) : null, children: List.generate(maxCol, (index) { diff --git a/test/gpt_markdown_test.dart b/test/gpt_markdown_test.dart index 44f17a8..69cbf5e 100644 --- a/test/gpt_markdown_test.dart +++ b/test/gpt_markdown_test.dart @@ -158,4 +158,70 @@ void main() { expect(matchingMaterial, isNotNull); }); + + testWidgets('Table uses theme colors and HighlightedText has decoration', ( + tester, + ) async { + const hrColor = Colors.pink; + const codeBgColor = Colors.green; + const highlightColor = Colors.orange; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: GptMarkdownTheme( + gptThemeData: GptMarkdownThemeData( + brightness: Brightness.light, + hrLineColor: hrColor, + codeBlockBackgroundColor: codeBgColor, + highlightColor: highlightColor, + ), + child: const GptMarkdown('| Header |\n| -- |\n| Cell |\n\n`code`'), + ), + ), + ), + ); + + // Verify Table Border Color + final tableFinder = find.byType(Table); + expect(tableFinder, findsOneWidget); + final table = tester.widget(tableFinder); + expect(table.border?.top.color, hrColor); + + // Verify Header Color + // The header is a TableRow with a decoration. + // We cannot easily inspect TableRow directly from widget tree as it is not a widget. + // But we know Table usage: children -> TableRow. + // tester.widget
returns the Table widget which has children property which is List. + expect(table.children.first.decoration, isA()); + final decoration = table.children.first.decoration as BoxDecoration; + expect(decoration.color, codeBgColor); + + // Verify HighlightedText decoration + // It should be a Container with decoration inside a WidgetSpan + // RichText -> TextSpan -> WidgetSpan -> Container + final richTextFinder = + find.byType(RichText).last; // Last because Table might use RichText too + final richText = tester.widget(richTextFinder); + // traversing to find the WidgetSpan relative to `code` + // Implementation: WidgetSpan child is Container. + final containerFinder = find.byType(Container); + // There might be multiple containers (Table uses padding etc). + // Let's look for one with our highlight color. + final highlightContainer = tester + .widgetList(containerFinder) + .firstWhere( + (c) => + (c.decoration is BoxDecoration) && + (c.decoration as BoxDecoration).color == highlightColor, + orElse: () => throw Exception('Highlight container not found'), + ); + + final boxDec = highlightContainer.decoration as BoxDecoration; + expect(boxDec.borderRadius, BorderRadius.circular(4)); + expect( + highlightContainer.padding, + const EdgeInsets.symmetric(horizontal: 5), + ); + }); } From 702567267c36c1bce7b76451970040d6dacaa070 Mon Sep 17 00:00:00 2001 From: Tolga Akcaoglu Date: Tue, 23 Dec 2025 18:57:35 +0300 Subject: [PATCH 3/3] weight sacmaligini kaldirdim --- lib/markdown_component.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/markdown_component.dart b/lib/markdown_component.dart index b24d076..8922c6f 100644 --- a/lib/markdown_component.dart +++ b/lib/markdown_component.dart @@ -482,8 +482,8 @@ class HighlightedText extends InlineMd { child: Text( highlightedText, style: - config.style?.copyWith(fontWeight: FontWeight.bold) ?? - const TextStyle(fontWeight: FontWeight.bold), + config.style?.copyWith(color: Colors.grey, fontSize: 13) ?? + const TextStyle(color: Colors.grey, fontSize: 13), ), ), );