diff --git a/.github/workflows/flutter.yaml b/.github/workflows/flutter.yaml
index e40f26d..594bec8 100644
--- a/.github/workflows/flutter.yaml
+++ b/.github/workflows/flutter.yaml
@@ -8,10 +8,36 @@ jobs:
name: Validate
runs-on: macos-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
- name: Run Flutter validation script
run: ./validate.sh
shell: bash
+
+ integration-android:
+ name: Android integration tests
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: '17'
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: stable
+ - name: Enable KVM
+ run: |
+ echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
+ | sudo tee /etc/udev/rules.d/99-kvm4all.rules
+ sudo udevadm control --reload-rules
+ sudo udevadm trigger --name-match=kvm
+ - name: Run integration tests on Android emulator
+ uses: reactivecircus/android-emulator-runner@v2
+ with:
+ api-level: 34
+ arch: x86_64
+ profile: pixel_6
+ script: cd example && flutter pub get && flutter test integration_test
diff --git a/.gitignore b/.gitignore
index d9fe855..93ca1bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,6 +25,7 @@
**/doc/api/
.dart_tool/
.flutter-plugins
+.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
@@ -37,3 +38,7 @@ lib/generated_plugin_registrant.dart
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
example/android/app/.cxx/Debug
example/ios/Flutter/ephemeral
+
+# Local symlink to the sibling flutter_markdown_plus_latex package, used for
+# cross-package development. Not part of this repository.
+flutter_markdown_plus_latex
diff --git a/.pubignore b/.pubignore
index e10928a..8291c82 100644
--- a/.pubignore
+++ b/.pubignore
@@ -1,3 +1,6 @@
+# Local symlink to the sibling flutter_markdown_plus_latex package (dev only)
+flutter_markdown_plus_latex/
+
# Example build artifacts
example/build/
example/.dart_tool/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 557c102..ccb1127 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,25 @@
+## 1.0.8
+
+* Fixed custom `builders` and `paddingBuilders` for the `hr` tag being ignored
+* Fixed bold, italic, and link formatting not rendering inside blockquotes
+* Forced a consistent line height within each text block so mixed font weights (e.g. bold) no longer shift line height, while headers and other blocks keep their own height
+* Updated the default blockquote styling to be theme-aware (uses `surfaceContainerHighest` with a primary-coloured left border) for better light/dark mode support
+* Added a `noScroll` option to the `Markdown` widget to render content in a non-scrolling `Column` for use inside an existing scroll view
+* Added an integration test suite (`example/integration_test/`) that runs on a device or emulator, covering link taps, scrolling, task-list checkboxes, text selection, and example app launch
+* Added `scripts/integration_test.sh` and a GitHub Actions job that runs the integration tests on an Android emulator
+* Migrated linting to `package:flutter_lints`, retaining strict type analysis and required public API documentation
+* Updated dependencies: `markdown` to `^7.3.1`, `mockito` to `^5.7.0`, and pinned `leak_tracker_flutter_testing`
+* Aligned the example app's Flutter constraint with the package and added a `documentation` link to the pubspec
+* Updated the example app's Android Gradle Plugin to 8.6.0 (Flutter's current minimum) and removed an unused Google Cloud Artifact Registry Gradle plugin
+* Excluded the locally symlinked `flutter_markdown_plus_latex` package from analysis and publishing
+
+**Thanks to our contributors:**
+* [@joelbrostrom](https://github.com/joelbrostrom) for fixing custom builders on the `hr` tag (#131)
+* [@vsaase](https://github.com/vsaase) for fixing inline formatting inside blockquotes (#128)
+* [@domhel](https://github.com/domhel) for theme-aware blockquote styling (#42)
+* [@szuwest](https://github.com/szuwest) for the strong text line-height fix (#130)
+* [@senlinjun](https://github.com/senlinjun) for the `noScroll` option (#38)
+
## 1.0.7
* Improved pub.dev score by making example app source code available in published package
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 68be6a0..cd005be 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -1,9 +1,10 @@
-# Specify analysis options.
+# Analysis options for flutter_markdown_plus.
#
-# This file is a copy of analysis_options.yaml from flutter repo
-# as of 2023-12-18, but with some modifications marked with
-# "DIFFERENT FROM FLUTTER/FLUTTER" below. The file is expected to
-# be kept in sync with the master file from the flutter repo.
+# Builds on the standard package:flutter_lints rule set (the convention for
+# published Flutter packages), layered with a few stricter settings this package
+# relies on: type-strict analysis and required public API documentation.
+
+include: package:flutter_lints/flutter.yaml
analyzer:
language:
@@ -11,235 +12,28 @@ analyzer:
strict-inference: true
strict-raw-types: true
errors:
- # allow deprecated members (we do this because otherwise we have to annotate
- # every member in every test, assert, etc, when we or the Dart SDK deprecates
- # something (https://github.com/flutter/flutter/issues/143312)
+ # Allow deprecated members: otherwise every usage would need annotating each
+ # time the Dart SDK or Flutter deprecates something we still support.
+ # https://github.com/flutter/flutter/issues/143312
deprecated_member_use: ignore
deprecated_member_use_from_same_package: ignore
- exclude: # DIFFERENT FROM FLUTTER/FLUTTER
- # Ignore generated files
+ exclude:
+ # Generated files.
- '**/*.g.dart'
- '**/*.mocks.dart' # Mockito @GenerateMocks
-
+ # Locally symlinked sibling package (flutter_markdown_plus_latex) used for
+ # cross-package development; not part of this package.
+ - 'flutter_markdown_plus_latex/**'
+
formatter:
page_width: 120
linter:
rules:
- # This list is derived from the list of all available lints located at
- # https://github.com/dart-lang/linter/blob/main/example/all.yaml
- - always_declare_return_types
- - always_put_control_body_on_new_line
- # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219
- - always_specify_types
- # - always_use_package_imports # we do this commonly
- - annotate_overrides
- # - avoid_annotating_with_dynamic # conflicts with always_specify_types
- - avoid_bool_literals_in_conditional_expressions
- # - avoid_catches_without_on_clauses # blocked on https://github.com/dart-lang/linter/issues/3023
- # - avoid_catching_errors # blocked on https://github.com/dart-lang/linter/issues/3023
- # - avoid_classes_with_only_static_members # we do this commonly for `abstract final class`es
- - avoid_double_and_int_checks
- - avoid_dynamic_calls
- - avoid_empty_else
- - avoid_equals_and_hash_code_on_mutable_classes
- - avoid_escaping_inner_quotes
- - avoid_field_initializers_in_const_classes
- # - avoid_final_parameters # incompatible with prefer_final_parameters
- - avoid_function_literals_in_foreach_calls
- # - avoid_implementing_value_types # see https://github.com/dart-lang/linter/issues/4558
- - avoid_init_to_null
- - avoid_js_rounded_ints
- # - avoid_multiple_declarations_per_line # seems to be a stylistic choice we don't subscribe to
- # - avoid_positional_boolean_parameters # would have been nice to enable this but by now there's too many places that break it
- - avoid_print
- # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356)
- - avoid_redundant_argument_values
- - avoid_relative_lib_imports
- - avoid_renaming_method_parameters
- - avoid_return_types_on_setters
- - avoid_returning_null_for_void
- # - avoid_returning_this # there are enough valid reasons to return `this` that this lint ends up with too many false positives
- - avoid_setters_without_getters
- - avoid_shadowing_type_parameters
- - avoid_single_cascade_in_expression_statements
- - avoid_slow_async_io
- - avoid_type_to_string
- - avoid_types_as_parameter_names
- # - avoid_types_on_closure_parameters # conflicts with always_specify_types
- - avoid_unnecessary_containers
- - avoid_unused_constructor_parameters
- - avoid_void_async
- # - avoid_web_libraries_in_flutter # we use web libraries in web-specific code, and our tests prevent us from using them elsewhere
- - await_only_futures
- - camel_case_extensions
- - camel_case_types
- - cancel_subscriptions
- # - cascade_invocations # doesn't match the typical style of this repo
- - cast_nullable_to_non_nullable
- # - close_sinks # not reliable enough
- - collection_methods_unrelated_type
- - combinators_ordering
- # - comment_references # blocked on https://github.com/dart-lang/linter/issues/1142
- - conditional_uri_does_not_exist
- # - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204
- - control_flow_in_finally
- - curly_braces_in_flow_control_structures
- - dangling_library_doc_comments
- - depend_on_referenced_packages
- - deprecated_consistency
- # - deprecated_member_use_from_same_package # we allow self-references to deprecated members
- # - diagnostic_describe_all_properties # enabled only at the framework level (packages/flutter/lib)
- - directives_ordering
- # - discarded_futures # too many false positives, similar to unawaited_futures
- # - do_not_use_environment # there are appropriate times to use the environment, especially in our tests and build logic
- - empty_catches
- - empty_constructor_bodies
- - empty_statements
- - eol_at_end_of_file
- - exhaustive_cases
- - file_names
- - flutter_style_todos
- - hash_and_equals
- - implementation_imports
- - implicit_call_tearoffs
- - implicit_reopen
- - invalid_case_patterns
- # - join_return_with_assignment # not required by flutter style
- - leading_newlines_in_multiline_strings
- - library_annotations
- - library_names
- - library_prefixes
- - library_private_types_in_public_api
- # - lines_longer_than_80_chars # not required by flutter style
- - literal_only_boolean_expressions
- # - matching_super_parameters # blocked on https://github.com/dart-lang/language/issues/2509
- - missing_code_block_language_in_doc_comment
- - missing_whitespace_between_adjacent_strings
- - no_adjacent_strings_in_list
- - no_default_cases
- - no_duplicate_case_values
- - no_leading_underscores_for_library_prefixes
- - no_leading_underscores_for_local_identifiers
- - no_literal_bool_comparisons
- - no_logic_in_create_state
- - no_runtimeType_toString # DIFFERENT FROM FLUTTER/FLUTTER
- - no_self_assignments
- - no_wildcard_variable_uses
- - non_constant_identifier_names
- - noop_primitive_operations
- - null_check_on_nullable_type_parameter
- - null_closures
- # - omit_local_variable_types # opposite of always_specify_types
- # - one_member_abstracts # too many false positives
- - only_throw_errors # this does get disabled in a few places where we have legacy code that uses strings et al
- - overridden_fields
- # - package_api_docs # Deprecated (https://github.com/dart-lang/linter/issues/5107)
- - package_names
- - package_prefixed_library_names
- # - parameter_assignments # we do this commonly
- - prefer_adjacent_string_concatenation
- - prefer_asserts_in_initializer_lists
- # - prefer_asserts_with_message # not required by flutter style
- - prefer_collection_literals
- - prefer_conditional_assignment
- - prefer_const_constructors
- - prefer_const_constructors_in_immutables
- - prefer_const_declarations
- - prefer_const_literals_to_create_immutables
- # - prefer_constructors_over_static_methods # far too many false positives
- - prefer_contains
- # - prefer_double_quotes # opposite of prefer_single_quotes
- # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md#consider-using--for-short-functions-and-methods
- - prefer_final_fields
- - prefer_final_in_for_each
- - prefer_final_locals
- # - prefer_final_parameters # adds too much verbosity
- - prefer_for_elements_to_map_fromIterable
- - prefer_foreach
- - prefer_function_declarations_over_variables
- - prefer_generic_function_type_aliases
- - prefer_if_elements_to_conditional_expressions
- - prefer_if_null_operators
- - prefer_initializing_formals
- - prefer_inlined_adds
- # - prefer_int_literals # conflicts with https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md#use-double-literals-for-double-constants
- - prefer_interpolation_to_compose_strings
- - prefer_is_empty
- - prefer_is_not_empty
- - prefer_is_not_operator
- - prefer_iterable_whereType
- - prefer_mixin
- # - prefer_null_aware_method_calls # "call()" is confusing to people new to the language since it's not documented anywhere
- - prefer_null_aware_operators
+ # The public API of a published package must be documented.
+ - public_member_api_docs
+ # Missing awaits have caused production issues in plugins.
+ - unawaited_futures
- prefer_relative_imports
- prefer_single_quotes
- - prefer_spread_collections
- - prefer_typing_uninitialized_variables
- - prefer_void_to_null
- - provide_deprecation_message
- - public_member_api_docs # DIFFERENT FROM FLUTTER/FLUTTER
- - recursive_getters
- # - require_trailing_commas # would be nice, but requires a lot of manual work: 10,000+ code locations would need to be reformatted by hand after bulk fix is applied
- - secure_pubspec_urls
- - sized_box_for_whitespace
- - sized_box_shrink_expand
- - slash_for_doc_comments
- - sort_child_properties_last
- - sort_constructors_first
- - sort_pub_dependencies # DIFFERENT FROM FLUTTER/FLUTTER: Flutter's use case for not sorting does not apply to this repository.
- - sort_unnamed_constructors_first
- - test_types_in_equals
- - throw_in_finally
- - tighten_type_of_initializing_formals
- # - type_annotate_public_apis # subset of always_specify_types
- - type_init_formals
- - type_literal_in_constant_pattern
- - unawaited_futures # DIFFERENT FROM FLUTTER/FLUTTER: It's disabled there for "too many false positives"; that's not an issue here, and missing awaits have caused production issues in plugins.
- - unnecessary_await_in_return
- - unnecessary_brace_in_string_interps
- - unnecessary_breaks
- - unnecessary_const
- - unnecessary_constructor_name
- # - unnecessary_final # conflicts with prefer_final_locals
- - unnecessary_getters_setters
- # - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498
- - unnecessary_late
- - unnecessary_library_directive
- - unnecessary_new
- - unnecessary_null_aware_assignments
- - unnecessary_null_aware_operator_on_extension_on_nullable
- - unnecessary_null_checks
- - unnecessary_null_in_if_null_operators
- - unnecessary_nullable_for_final_variable_declarations
- - unnecessary_overrides
- - unnecessary_parenthesis
- # - unnecessary_raw_strings # what's "necessary" is a matter of opinion; consistency across strings can help readability more than this lint
- - unnecessary_statements
- - unnecessary_string_escapes
- - unnecessary_string_interpolations
- - unnecessary_this
- - unnecessary_to_list_in_spreads
- - unreachable_from_main
- - unrelated_type_equality_checks
- - use_build_context_synchronously
- - use_colored_box
- # - use_decorated_box # leads to bugs: DecoratedBox and Container are not equivalent (Container inserts extra padding)
- - use_enums
- - use_full_hex_values_for_flutter_colors
- - use_function_type_syntax_for_parameters
- - use_if_null_to_convert_nulls_to_bools
- - use_is_even_rather_than_modulo
- - use_key_in_widget_constructors
- - use_late_for_private_fields_and_variables
- - use_named_constants
- - use_raw_strings
- - use_rethrow_when_possible
- - use_setters_to_change_properties
- # - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182
- - use_string_in_part_of_directives
- - use_super_parameters
- - use_test_throws_matchers
- # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review
- - valid_regexps
- - void_checks
+ - sort_pub_dependencies
diff --git a/example/android/gradle.properties b/example/android/gradle.properties
index 598d13f..f2122b2 100644
--- a/example/android/gradle.properties
+++ b/example/android/gradle.properties
@@ -1,3 +1,7 @@
org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true
+# This builtInKotlin flag was added automatically by Flutter migrator
+android.builtInKotlin=false
+# This newDsl flag was added automatically by Flutter migrator
+android.newDsl=false
diff --git a/example/android/settings.gradle b/example/android/settings.gradle
index 57000f7..15c796f 100644
--- a/example/android/settings.gradle
+++ b/example/android/settings.gradle
@@ -19,9 +19,8 @@ pluginManagement {
// See https://github.com/flutter/flutter/blob/master/docs/ecosystem/Plugins-and-Packages-repository-structure.md#gradle-structure for more info.
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
- id "com.android.application" version "8.2.1" apply false
+ id "com.android.application" version "8.6.0" apply false
id "org.jetbrains.kotlin.android" version "1.9.0" apply false
- id "com.google.cloud.artifactregistry.gradle-plugin" version "2.2.1"
}
include ":app"
diff --git a/example/integration_test/app_render_test.dart b/example/integration_test/app_render_test.dart
new file mode 100644
index 0000000..0450c0f
--- /dev/null
+++ b/example/integration_test/app_render_test.dart
@@ -0,0 +1,88 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_markdown_example/main.dart' as app;
+import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+
+import 'markdown_harness.dart';
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ group('flutter_markdown_plus on-device rendering', () {
+ testWidgets('tapping a link fires onTapLink with the href', (WidgetTester tester) async {
+ String? tappedHref;
+ await tester.pumpWidget(
+ buildMarkdownApp(
+ data: '[tap me](https://example.com)',
+ onTapLink: (String text, String? href, String title) => tappedHref = href,
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.text('tap me'));
+ await tester.pumpAndSettle();
+
+ expect(tappedHref, 'https://example.com');
+ });
+
+ testWidgets('a long document scrolls to reveal off-screen content', (WidgetTester tester) async {
+ final StringBuffer buffer = StringBuffer();
+ for (int i = 0; i < 80; i++) {
+ buffer.writeln('Paragraph $i with enough text to occupy vertical space on screen.');
+ buffer.writeln();
+ }
+ buffer.writeln('THE_BOTTOM_SENTINEL');
+
+ await tester.pumpWidget(buildMarkdownApp(data: buffer.toString()));
+ await tester.pumpAndSettle();
+
+ // The sentinel is below the fold and the ListView has not built it yet.
+ expect(find.text('THE_BOTTOM_SENTINEL'), findsNothing);
+
+ await tester.drag(find.byType(Markdown), const Offset(0, -8000));
+ await tester.pumpAndSettle();
+
+ expect(find.text('THE_BOTTOM_SENTINEL'), findsOneWidget);
+ });
+
+ testWidgets('GFM task list renders checkbox icons', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ buildMarkdownBodyApp(data: '- [ ] todo item\n- [x] done item'),
+ );
+ await tester.pumpAndSettle();
+
+ expect(find.byIcon(Icons.check_box_outline_blank), findsOneWidget);
+ expect(find.byIcon(Icons.check_box), findsOneWidget);
+ expect(find.text('todo item'), findsOneWidget);
+ expect(find.text('done item'), findsOneWidget);
+ });
+
+ testWidgets('selectable markdown surfaces the selection toolbar on long press', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ buildMarkdownApp(
+ data: 'A selectable paragraph of text.',
+ selectable: true,
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ await tester.longPress(find.textContaining('selectable paragraph'));
+ await tester.pumpAndSettle();
+
+ // The Material selection toolbar exposes a Copy action once text is selected.
+ expect(find.text('Copy'), findsWidgets);
+ });
+
+ testWidgets('the example app launches and lists its demos', (WidgetTester tester) async {
+ app.main();
+ await tester.pumpAndSettle();
+
+ expect(find.text('Markdown Demos'), findsOneWidget);
+ });
+ });
+}
diff --git a/example/integration_test/markdown_harness.dart b/example/integration_test/markdown_harness.dart
new file mode 100644
index 0000000..703ddaa
--- /dev/null
+++ b/example/integration_test/markdown_harness.dart
@@ -0,0 +1,41 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
+
+/// Wraps a scrollable [Markdown] widget in a minimal [MaterialApp] so
+/// integration tests can pump a deterministic widget tree without depending on
+/// the example app's demo navigation.
+Widget buildMarkdownApp({
+ required String data,
+ MarkdownTapLinkCallback? onTapLink,
+ bool selectable = false,
+}) {
+ return MaterialApp(
+ home: Scaffold(
+ body: SafeArea(
+ child: Markdown(
+ data: data,
+ onTapLink: onTapLink,
+ selectable: selectable,
+ ),
+ ),
+ ),
+ );
+}
+
+/// Wraps a non-scrolling [MarkdownBody] in a minimal [MaterialApp] for cases
+/// where the rendered content fits on screen (e.g. checkbox rendering).
+Widget buildMarkdownBodyApp({required String data}) {
+ return MaterialApp(
+ home: Scaffold(
+ body: SafeArea(
+ child: SingleChildScrollView(
+ child: MarkdownBody(data: data),
+ ),
+ ),
+ ),
+ );
+}
diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist
index 7c56964..391a902 100644
--- a/example/ios/Flutter/AppFrameworkInfo.plist
+++ b/example/ios/Flutter/AppFrameworkInfo.plist
@@ -20,7 +20,5 @@
????
CFBundleVersion
1.0
- MinimumOSVersion
- 12.0
diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj
index 3c5ddc8..33b4f90 100644
--- a/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/example/ios/Runner.xcodeproj/project.pbxproj
@@ -13,6 +13,7 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+ 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -42,6 +43,7 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -49,6 +51,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -58,6 +61,7 @@
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
+ 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
@@ -102,6 +106,9 @@
/* Begin PBXNativeTarget section */
97C146ED1CF9000F007C117D /* Runner */ = {
+ packageProductDependencies = (
+ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
+ );
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
@@ -125,6 +132,9 @@
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
+ packageReferences = (
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
+ );
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1510;
@@ -275,7 +285,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -349,7 +359,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -398,7 +408,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -469,6 +479,18 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
+/* Begin XCLocalSwiftPackageReference section */
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
+ isa = XCLocalSwiftPackageReference;
+ relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
+ };
+/* End XCLocalSwiftPackageReference section */
+/* Begin XCSwiftPackageProductDependency section */
+ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = FlutterGeneratedPluginSwiftPackage;
+ };
+/* End XCSwiftPackageProductDependency section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index fc5ae03..0bd6d42 100644
--- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -5,6 +5,24 @@
+
+
+
+
+
+
+
+
+
+
=3.8.0-0 <4.0.0"
+ dart: ">=3.10.0-0 <4.0.0"
flutter: ">=3.27.1"
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index 71bf8dc..cebb540 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -4,7 +4,7 @@ publish_to: none
environment:
sdk: ^3.4.0
- flutter: ">=3.22.0"
+ flutter: ">=3.27.1"
dependencies:
flutter:
@@ -23,6 +23,8 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
+ integration_test:
+ sdk: flutter
flutter:
assets:
diff --git a/lib/flutter_markdown_plus.dart b/lib/flutter_markdown_plus.dart
index 12ed0a9..8e764d7 100644
--- a/lib/flutter_markdown_plus.dart
+++ b/lib/flutter_markdown_plus.dart
@@ -3,7 +3,7 @@
// found in the LICENSE file.
/// A library to render markdown formatted text.
-library flutter_markdown_plus;
+library;
export 'src/builder.dart';
export 'src/style_sheet.dart';
diff --git a/lib/src/builder.dart b/lib/src/builder.dart
index 31a7c95..4f75a5f 100644
--- a/lib/src/builder.dart
+++ b/lib/src/builder.dart
@@ -358,7 +358,7 @@ class MarkdownBuilder implements md.NodeVisitor {
} else {
child = _buildRichText(
TextSpan(
- style: _isInBlockquote ? styleSheet.blockquote : _inlines.last.style,
+ style: _inlines.last.style,
text: trimText(text.text),
recognizer: _linkHandlers.isNotEmpty ? _linkHandlers.last : null,
),
@@ -474,7 +474,16 @@ class MarkdownBuilder implements md.NodeVisitor {
child: child,
);
} else if (tag == 'hr') {
- child = Container(decoration: styleSheet.horizontalRuleDecoration);
+ if (!builders.containsKey(tag)) {
+ child = Container(decoration: styleSheet.horizontalRuleDecoration);
+ }
+ }
+
+ if (tag == 'hr' && paddingBuilders.containsKey(tag)) {
+ child = Padding(
+ padding: paddingBuilders[tag]!.getPadding(),
+ child: child,
+ );
}
_addBlockChild(child);
@@ -726,7 +735,7 @@ class MarkdownBuilder implements md.NodeVisitor {
if (_inlines.isEmpty) {
_inlines.add(_InlineElement(
tag,
- style: tag != null ? styleSheet.styles[tag] : null,
+ style: _isInBlockquote ? styleSheet.blockquote : (tag != null ? styleSheet.styles[tag] : null),
));
}
}
@@ -980,11 +989,25 @@ class MarkdownBuilder implements md.NodeVisitor {
Widget _buildRichText(TextSpan text, {TextAlign? textAlign, String? key}) {
//Adding a unique key prevents the problem of using the same link handler for text spans with the same text
final Key k = key == null ? UniqueKey() : Key(key);
+ // Force a consistent line height within each text block, derived from the
+ // span's own base style so headers/blockquotes keep their correct height
+ // while mixed font weights within a block no longer shift line height.
+ final TextStyle? baseStyle = text.style ?? styleSheet.p;
+ final StrutStyle? strutStyle = baseStyle != null
+ ? StrutStyle(
+ fontFamily: baseStyle.fontFamily,
+ fontSize: baseStyle.fontSize ?? styleSheet.p?.fontSize,
+ height: baseStyle.height ?? styleSheet.p?.height,
+ leading: 0,
+ forceStrutHeight: true,
+ )
+ : null;
if (selectable) {
return SelectableText.rich(
text,
textScaler: styleSheet.textScaler,
textAlign: textAlign ?? TextAlign.start,
+ strutStyle: strutStyle,
onSelectionChanged: onSelectionChanged != null
? (TextSelection selection, SelectionChangedCause? cause) =>
onSelectionChanged!(text.text, selection, cause)
@@ -997,6 +1020,7 @@ class MarkdownBuilder implements md.NodeVisitor {
text,
textScaler: styleSheet.textScaler,
textAlign: textAlign ?? TextAlign.start,
+ strutStyle: strutStyle,
key: k,
);
}
diff --git a/lib/src/style_sheet.dart b/lib/src/style_sheet.dart
index 9927731..eb9f1ed 100644
--- a/lib/src/style_sheet.dart
+++ b/lib/src/style_sheet.dart
@@ -143,10 +143,16 @@ class MarkdownStyleSheet {
tableColumnWidth: const FlexColumnWidth(),
tableCellsPadding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
tableCellsDecoration: const BoxDecoration(),
- blockquotePadding: const EdgeInsets.all(8.0),
+ blockquotePadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
blockquoteDecoration: BoxDecoration(
- color: Colors.blue.shade100,
- borderRadius: BorderRadius.circular(2.0),
+ color: theme.colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(3),
+ border: Border(
+ left: BorderSide(
+ color: theme.colorScheme.primary,
+ width: 3,
+ ),
+ ),
),
codeblockPadding: const EdgeInsets.all(8.0),
codeblockDecoration: BoxDecoration(
@@ -318,10 +324,16 @@ class MarkdownStyleSheet {
tableColumnWidth: const FlexColumnWidth(),
tableCellsPadding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
tableCellsDecoration: const BoxDecoration(),
- blockquotePadding: const EdgeInsets.all(8.0),
+ blockquotePadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
blockquoteDecoration: BoxDecoration(
- color: Colors.blue.shade100,
- borderRadius: BorderRadius.circular(2.0),
+ color: theme.colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(3),
+ border: Border(
+ left: BorderSide(
+ color: theme.colorScheme.primary,
+ width: 3,
+ ),
+ ),
),
codeblockPadding: const EdgeInsets.all(8.0),
codeblockDecoration: BoxDecoration(
diff --git a/lib/src/widget.dart b/lib/src/widget.dart
index 5e8af27..1bb99aa 100644
--- a/lib/src/widget.dart
+++ b/lib/src/widget.dart
@@ -522,6 +522,7 @@ class Markdown extends MarkdownWidget {
this.controller,
this.physics,
this.shrinkWrap = false,
+ this.noScroll = false,
super.softLineBreak,
});
@@ -544,8 +545,20 @@ class Markdown extends MarkdownWidget {
/// See also: [ScrollView.shrinkWrap]
final bool shrinkWrap;
+ /// If true, the markdown is rendered in a non-scrolling [Column] instead of
+ /// a scrolling [ListView]. Use this when the widget is already inside a
+ /// scrolling parent.
+ final bool noScroll;
+
@override
Widget build(BuildContext context, List? children) {
+ if (noScroll) {
+ final List childrenWithPadding = [];
+ for (final Widget child in children ?? []) {
+ childrenWithPadding.add(Padding(padding: padding, child: child));
+ }
+ return Column(children: childrenWithPadding);
+ }
return ListView(
padding: padding,
controller: controller,
diff --git a/pubspec.lock b/pubspec.lock
index ce71bba..23b1e5c 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -5,18 +5,18 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
- sha256: f0bb5d1648339c8308cc0b9838d8456b3cfe5c91f9dc1a735b4d003269e5da9a
+ sha256: cd6add6f846f35fb79f3c315296703c1a24f3cfd7f4739d91a74961c1c7e9f1b
url: "https://pub.dev"
source: hosted
- version: "88.0.0"
+ version: "100.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
- sha256: "0b7b9c329d2879f8f05d6c05b32ee9ec025f39b077864bdb5ac9a7b63418a98f"
+ sha256: "6ba98576948803398b69e3a444df24eacdbe12ed699c7014e120ea38552debbf"
url: "https://pub.dev"
source: hosted
- version: "8.1.1"
+ version: "13.0.0"
args:
dependency: transitive
description:
@@ -45,10 +45,10 @@ packages:
dependency: transitive
description:
name: build
- sha256: dfb67ccc9a78c642193e0c2d94cb9e48c2c818b3178a86097d644acdcde6a8d9
+ sha256: a156715e7cd728130c592f30552575908aae5b100005fbc1f0fb16b3c03a3d10
url: "https://pub.dev"
source: hosted
- version: "4.0.2"
+ version: "4.0.6"
built_collection:
dependency: transitive
description:
@@ -69,10 +69,10 @@ packages:
dependency: transitive
description:
name: characters
- sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
+ sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
- version: "1.4.0"
+ version: "1.4.1"
clock:
dependency: transitive
description:
@@ -117,10 +117,10 @@ packages:
dependency: transitive
description:
name: dart_style
- sha256: c87dfe3d56f183ffe9106a18aebc6db431fc7c98c31a54b952a77f3d54a85697
+ sha256: "59d53ef8eaed9d288ed9767618e2b31c4fa0383a127db59d5eb2e737a7638a60"
url: "https://pub.dev"
source: hosted
- version: "3.1.2"
+ version: "3.1.9"
fake_async:
dependency: transitive
description:
@@ -150,6 +150,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ flutter_lints:
+ dependency: "direct dev"
+ description:
+ name: flutter_lints
+ sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -187,6 +195,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
+ lints:
+ dependency: transitive
+ description:
+ name: lints
+ sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.1.0"
logging:
dependency: transitive
description:
@@ -199,42 +215,42 @@ packages:
dependency: "direct main"
description:
name: markdown
- sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"
+ sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9
url: "https://pub.dev"
source: hosted
- version: "7.3.0"
+ version: "7.3.1"
matcher:
dependency: transitive
description:
name: matcher
- sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
+ sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
- version: "0.12.17"
+ version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
- sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
+ sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
- version: "0.11.1"
+ version: "0.13.0"
meta:
dependency: "direct main"
description:
name: meta
- sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
+ sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev"
source: hosted
- version: "1.17.0"
+ version: "1.18.0"
mockito:
dependency: "direct dev"
description:
name: mockito
- sha256: "4feb43bc4eb6c03e832f5fcd637d1abb44b98f9cfa245c58e27382f58859f8f6"
+ sha256: c8d040d754367108fbe482dcb79dd72b8fe60ac6727abd15b4783c5560297ee6
url: "https://pub.dev"
source: hosted
- version: "5.5.1"
+ version: "5.7.0"
package_config:
dependency: transitive
description:
@@ -268,10 +284,10 @@ packages:
dependency: transitive
description:
name: source_gen
- sha256: "9098ab86015c4f1d8af6486b547b11100e73b193e1899015033cb3e14ad20243"
+ sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02
url: "https://pub.dev"
source: hosted
- version: "4.0.2"
+ version: "4.2.3"
source_span:
dependency: transitive
description:
@@ -324,10 +340,10 @@ packages:
dependency: transitive
description:
name: test_api
- sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
+ sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev"
source: hosted
- version: "0.7.7"
+ version: "0.7.11"
typed_data:
dependency: transitive
description:
@@ -369,5 +385,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
- dart: ">=3.8.0-0 <4.0.0"
+ dart: ">=3.11.0 <4.0.0"
flutter: ">=3.27.1"
diff --git a/pubspec.yaml b/pubspec.yaml
index 351a9d7..60ef218 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -4,7 +4,8 @@ description: A Markdown renderer for Flutter. Create rich text output,
formatted with simple Markdown tags.
repository: https://github.com/foresightmobile/flutter_markdown_plus
issue_tracker: https://github.com/foresightmobile/flutter_markdown_plus/issues
-version: 1.0.7
+documentation: https://github.com/foresightmobile/flutter_markdown_plus#readme
+version: 1.0.8
environment:
sdk: ^3.4.0
@@ -13,15 +14,16 @@ environment:
dependencies:
flutter:
sdk: flutter
- markdown: ^7.3.0
+ markdown: ^7.3.1
meta: ^1.16.0
path: ^1.9.1
dev_dependencies:
+ flutter_lints: ^6.0.0
flutter_test:
sdk: flutter
- leak_tracker_flutter_testing: any
- mockito: ^5.5.0
+ leak_tracker_flutter_testing: ^3.0.10
+ mockito: ^5.7.0
standard_message_codec: ^0.0.1+4
topics:
diff --git a/scripts/integration_test.sh b/scripts/integration_test.sh
new file mode 100755
index 0000000..bf3273b
--- /dev/null
+++ b/scripts/integration_test.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+set -e -x
+
+###############################################################################
+# Runs the example app's integration tests on a connected device/emulator.
+#
+# Locally: ANDROID_DEVICE=emulator-5554 sh ./scripts/integration_test.sh
+# CI: device id is omitted so the single booted emulator is auto-selected.
+###############################################################################
+
+cd "$(dirname "$0")/../example"
+
+flutter pub get
+
+if [ -n "$ANDROID_DEVICE" ]; then
+ flutter test integration_test -d "$ANDROID_DEVICE"
+else
+ flutter test integration_test
+fi
diff --git a/test/blockquote_test.dart b/test/blockquote_test.dart
index 487bedc..aa4c010 100644
--- a/test/blockquote_test.dart
+++ b/test/blockquote_test.dart
@@ -87,19 +87,48 @@ void defineTests() {
theme.textTheme.bodyMedium!.color,
);
- /// Markdown guide
+ /// Markdown guide (link - should use link color)
expect(styledTextParts[1].text, 'Markdown guide');
- expect(styledTextParts[1].style!.color, styleSheet.blockquote!.color);
+ expect(styledTextParts[1].style!.color, Colors.blue);
/// and this is
+ expect(styledTextParts[2].text, ' and this is ');
expect(
styledTextParts[2].style!.color,
theme.textTheme.bodyMedium!.color,
);
- /// bold
- expect(styledTextParts[2].text, ' and this is bold and italic');
- expect(styledTextParts[2].style!.fontWeight, FontWeight.w400);
+ /// bold (should be bold)
+ expect(styledTextParts[3].text, 'bold');
+ expect(styledTextParts[3].style!.fontWeight, FontWeight.bold);
+
+ /// and
+ expect(styledTextParts[4].text, ' and ');
+
+ /// italic (should be italic)
+ expect(styledTextParts[5].text, 'italic');
+ expect(styledTextParts[5].style!.fontStyle, FontStyle.italic);
+ },
+ );
+
+ testWidgets(
+ 'bold text in blockquote renders with bold font weight',
+ (WidgetTester tester) async {
+ await tester.pumpWidget(
+ boilerplate(
+ const MarkdownBody(data: '> **bold text**'),
+ ),
+ );
+
+ final Text quoteText = tester.widget(find.byType(Text));
+ final TextSpan textSpan = quoteText.textSpan! as TextSpan;
+
+ // The bold span is either the root span directly (when it's the only
+ // element) or the first child span.
+ final TextSpan boldSpan = textSpan.children != null ? textSpan.children!.cast().first : textSpan;
+
+ expect(boldSpan.text, 'bold text');
+ expect(boldSpan.style!.fontWeight, FontWeight.bold);
},
);
});
diff --git a/test/horizontal_rule_test.dart b/test/horizontal_rule_test.dart
index 03a465a..79c4311 100644
--- a/test/horizontal_rule_test.dart
+++ b/test/horizontal_rule_test.dart
@@ -2,9 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'package:flutter/widgets.dart';
+import 'package:flutter/material.dart';
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
import 'package:flutter_test/flutter_test.dart';
+import 'package:markdown/markdown.dart' as md;
import 'utils.dart';
void main() => defineTests();
@@ -84,5 +85,68 @@ void defineTests() {
]);
},
);
+ testWidgets(
+ 'custom builder for hr is respected',
+ (WidgetTester tester) async {
+ const String data = '---';
+ await tester.pumpWidget(
+ boilerplate(
+ MarkdownBody(
+ data: data,
+ builders: {
+ 'hr': _CustomHrBuilder(),
+ },
+ ),
+ ),
+ );
+
+ expect(find.byType(ColoredBox), findsOneWidget);
+ final ColoredBox box = tester.widget(find.byType(ColoredBox));
+ expect(box.color, Colors.red);
+ },
+ );
+
+ testWidgets(
+ 'paddingBuilders for hr is applied',
+ (WidgetTester tester) async {
+ const String data = '---';
+ const double paddingX = 16.0;
+ await tester.pumpWidget(
+ boilerplate(
+ MarkdownBody(
+ data: data,
+ paddingBuilders: {
+ 'hr': _CustomPaddingBuilder(paddingX),
+ },
+ ),
+ ),
+ );
+
+ final Finder paddingFinder = find.byType(Padding);
+ final List paddings = tester.widgetList(paddingFinder).toList();
+ final bool hasHrPadding = paddings.any(
+ (Padding p) => p.padding.along(Axis.horizontal) == paddingX * 2,
+ );
+ expect(hasHrPadding, isTrue);
+ },
+ );
});
}
+
+class _CustomHrBuilder extends MarkdownElementBuilder {
+ @override
+ Widget visitElementAfter(md.Element element, TextStyle? preferredStyle) {
+ return const ColoredBox(color: Colors.red, child: SizedBox(height: 2, width: double.infinity));
+ }
+}
+
+class _CustomPaddingBuilder extends MarkdownPaddingBuilder {
+ _CustomPaddingBuilder(this.paddingX);
+
+ final double paddingX;
+
+ @override
+ EdgeInsets getPadding() {
+ return EdgeInsets.symmetric(horizontal: paddingX);
+ }
+}
diff --git a/test/scrollable_test.dart b/test/scrollable_test.dart
index a865cca..557ad6c 100644
--- a/test/scrollable_test.dart
+++ b/test/scrollable_test.dart
@@ -191,5 +191,51 @@ void defineTests() {
expect(scrollViews.first.controller, isNot(equals(scrollViews.last.controller)));
},
);
+
+ testWidgets(
+ 'noScroll renders content in a non-scrolling Column',
+ (WidgetTester tester) async {
+ const String data = '# A\n\npara one\n\npara two';
+
+ await tester.pumpWidget(
+ boilerplate(
+ const Markdown(data: data, noScroll: true),
+ ),
+ );
+
+ // No scroll view should be present when noScroll is true.
+ expect(find.byType(Scrollable), findsNothing);
+
+ // The content should be laid out in at least one Column.
+ expect(find.byType(Column), findsWidgets);
+
+ // Each direct child rendered by the noScroll branch is wrapped in Padding.
+ final Finder columnFinder = find.descendant(
+ of: find.byType(Markdown),
+ matching: find.byType(Column),
+ );
+ final Column column = tester.widgetList(columnFinder).first;
+ expect(column.children, isNotEmpty);
+ for (final Widget child in column.children) {
+ expect(child, isA());
+ }
+ },
+ );
+
+ testWidgets(
+ 'noScroll defaults to false and keeps the scrolling ListView',
+ (WidgetTester tester) async {
+ const String data = '# A\n\npara one\n\npara two';
+
+ await tester.pumpWidget(
+ boilerplate(
+ const Markdown(data: data),
+ ),
+ );
+
+ // With default args the ListView still scrolls.
+ expect(find.byType(Scrollable), findsOneWidget);
+ },
+ );
});
}
diff --git a/test/text_test.dart b/test/text_test.dart
index 72f0efc..4ba6839 100644
--- a/test/text_test.dart
+++ b/test/text_test.dart
@@ -397,4 +397,80 @@ void defineTests() {
expectTextStrings(widgets, ['strikethrough']);
});
});
+
+ group('Strut style', () {
+ testWidgets(
+ 'paragraph rich text forces a consistent strut height across font weights',
+ (WidgetTester tester) async {
+ // A bold span should not change the paragraph line height. The builder
+ // applies a forced strut height derived from the span's base style so
+ // the line height is consistent regardless of font weight (see PR #130).
+ const double paragraphFontSize = 17.0;
+ final MarkdownStyleSheet styleSheet = MarkdownStyleSheet(
+ p: const TextStyle(fontSize: paragraphFontSize, height: 1.5),
+ );
+
+ await tester.pumpWidget(
+ boilerplate(
+ MarkdownBody(
+ data: 'normal **bold** text',
+ styleSheet: styleSheet,
+ ),
+ ),
+ );
+
+ final Text text = tester.widget(find.byType(Text));
+ expect(text.strutStyle, isNotNull);
+ expect(text.strutStyle!.forceStrutHeight, isTrue);
+ expect(text.strutStyle!.fontSize, paragraphFontSize);
+ expect(text.strutStyle!.height, 1.5);
+ },
+ );
+
+ testWidgets(
+ 'header strut is derived from the header style, not the paragraph style',
+ (WidgetTester tester) async {
+ // Regression guard: _buildRichText is the generic rich-text builder used
+ // for headers/blockquotes/list items as well as paragraphs. The strut
+ // must follow each block's own style so a large header is not forced
+ // down to the (smaller) paragraph size, which would clip the header.
+ const double headerFontSize = 32.0;
+ const double paragraphFontSize = 14.0;
+ final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
+ final MarkdownStyleSheet styleSheet = MarkdownStyleSheet.fromTheme(theme).merge(
+ MarkdownStyleSheet(
+ h1: const TextStyle(fontSize: headerFontSize),
+ p: const TextStyle(fontSize: paragraphFontSize),
+ ),
+ );
+
+ await tester.pumpWidget(
+ boilerplate(
+ MarkdownBody(
+ data: '# Heading\n\nbody',
+ styleSheet: styleSheet,
+ ),
+ ),
+ );
+
+ // The first Text is the header, the second is the paragraph body.
+ final List texts = tester.widgetList(find.byType(Text)).toList();
+ expect(texts.length, 2);
+
+ final Text headerText = texts.first;
+ final Text bodyText = texts.last;
+
+ expect(headerText.strutStyle, isNotNull);
+ expect(headerText.strutStyle!.forceStrutHeight, isTrue);
+ expect(headerText.strutStyle!.fontSize, headerFontSize);
+
+ expect(bodyText.strutStyle, isNotNull);
+ expect(bodyText.strutStyle!.fontSize, paragraphFontSize);
+
+ // The header strut must be taller than the paragraph strut, otherwise
+ // the header text would be vertically clipped.
+ expect(headerText.strutStyle!.fontSize! > bodyText.strutStyle!.fontSize!, isTrue);
+ },
+ );
+ });
}