diff --git a/packages/breakpad_client/example/lib/main.dart b/packages/breakpad_client/example/lib/main.dart index 4264c9ed..15660073 100644 --- a/packages/breakpad_client/example/lib/main.dart +++ b/packages/breakpad_client/example/lib/main.dart @@ -1,7 +1,7 @@ import 'dart:ffi'; import 'package:breakpad_client/breakpad_client.dart' as breakpad_client; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; void main() { breakpad_client.setLogger((log) { diff --git a/packages/breakpad_client/example/pubspec.yaml b/packages/breakpad_client/example/pubspec.yaml index 6f64c93c..25bdb35d 100644 --- a/packages/breakpad_client/example/pubspec.yaml +++ b/packages/breakpad_client/example/pubspec.yaml @@ -29,6 +29,7 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: + material_ui: ^1.11.0 flutter: sdk: flutter breakpad_client: diff --git a/packages/bring_window_to_front/example/lib/main.dart b/packages/bring_window_to_front/example/lib/main.dart index 936f0d62..1907be66 100644 --- a/packages/bring_window_to_front/example/lib/main.dart +++ b/packages/bring_window_to_front/example/lib/main.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:bring_window_to_front/bring_window_to_front.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; void main() { runApp(const MyApp()); diff --git a/packages/bring_window_to_front/example/pubspec.yaml b/packages/bring_window_to_front/example/pubspec.yaml index 5ed38583..e0d20366 100644 --- a/packages/bring_window_to_front/example/pubspec.yaml +++ b/packages/bring_window_to_front/example/pubspec.yaml @@ -15,6 +15,7 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: + material_ui: ^1.11.0 flutter: sdk: flutter diff --git a/packages/common_crypto/test/hmac_test.dart b/packages/common_crypto/test/hmac_test.dart index eeed8c66..b22fd347 100644 --- a/packages/common_crypto/test/hmac_test.dart +++ b/packages/common_crypto/test/hmac_test.dart @@ -4,7 +4,7 @@ import 'dart:math'; import 'dart:typed_data'; import 'package:common_crypto/src/hmac.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart' show debugPrint; import 'package:flutter_test/flutter_test.dart'; import 'package:pointycastle/export.dart'; diff --git a/packages/desktop_drop/example/lib/common/app_theme.dart b/packages/desktop_drop/example/lib/common/app_theme.dart index 34e32ce6..3e12f253 100644 --- a/packages/desktop_drop/example/lib/common/app_theme.dart +++ b/packages/desktop_drop/example/lib/common/app_theme.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; class AppTheme { const AppTheme._(); diff --git a/packages/desktop_drop/example/lib/common/bookmark_tools.dart b/packages/desktop_drop/example/lib/common/bookmark_tools.dart index 4055e488..e404399d 100644 --- a/packages/desktop_drop/example/lib/common/bookmark_tools.dart +++ b/packages/desktop_drop/example/lib/common/bookmark_tools.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:shared_preferences/shared_preferences.dart'; class BookmarkTools extends StatefulWidget { diff --git a/packages/desktop_drop/example/lib/main.dart b/packages/desktop_drop/example/lib/main.dart index b30ee551..f3b010ec 100644 --- a/packages/desktop_drop/example/lib/main.dart +++ b/packages/desktop_drop/example/lib/main.dart @@ -2,7 +2,7 @@ import 'dart:math' as math; import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'common/app_theme.dart'; import 'common/received_item.dart'; diff --git a/packages/desktop_drop/example/lib/pages/drop_content.dart b/packages/desktop_drop/example/lib/pages/drop_content.dart index 31da0aee..9d04594e 100644 --- a/packages/desktop_drop/example/lib/pages/drop_content.dart +++ b/packages/desktop_drop/example/lib/pages/drop_content.dart @@ -4,7 +4,7 @@ import 'dart:ui' as ui; import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:flutter/services.dart'; import '../common/app_theme.dart'; diff --git a/packages/desktop_drop/example/lib/pages/drop_stage.dart b/packages/desktop_drop/example/lib/pages/drop_stage.dart index 67a0f73d..a9300d18 100644 --- a/packages/desktop_drop/example/lib/pages/drop_stage.dart +++ b/packages/desktop_drop/example/lib/pages/drop_stage.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:desktop_drop/desktop_drop.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:flutter/foundation.dart'; import '../common/app_theme.dart'; diff --git a/packages/desktop_drop/example/pubspec.yaml b/packages/desktop_drop/example/pubspec.yaml index 8789eaf0..1bca16a8 100644 --- a/packages/desktop_drop/example/pubspec.yaml +++ b/packages/desktop_drop/example/pubspec.yaml @@ -12,6 +12,7 @@ environment: flutter: ">=3.47.7" dependencies: + material_ui: ^1.11.0 flutter: sdk: flutter diff --git a/packages/desktop_drop/example/test/playground_test.dart b/packages/desktop_drop/example/test/playground_test.dart index 5e7cb89e..c0709a52 100644 --- a/packages/desktop_drop/example/test/playground_test.dart +++ b/packages/desktop_drop/example/test/playground_test.dart @@ -5,7 +5,7 @@ import 'package:desktop_drop_example/pages/drop_stage.dart'; import 'package:desktop_drop_example/pages/drop_content.dart'; import 'package:desktop_drop_example/common/received_item.dart'; import 'package:desktop_drop_example/main.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; diff --git a/packages/desktop_drop/lib/src/desktop_drop.dart b/packages/desktop_drop/lib/src/desktop_drop.dart index ad2ccf7e..5dbf49f7 100644 --- a/packages/desktop_drop/lib/src/desktop_drop.dart +++ b/packages/desktop_drop/lib/src/desktop_drop.dart @@ -1,5 +1,3 @@ -import 'dart:typed_data'; - import 'package:flutter/foundation.dart'; import 'drop_item.dart'; diff --git a/packages/desktop_keep_screen_on/example/lib/main.dart b/packages/desktop_keep_screen_on/example/lib/main.dart index ab49cde8..de87339b 100644 --- a/packages/desktop_keep_screen_on/example/lib/main.dart +++ b/packages/desktop_keep_screen_on/example/lib/main.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:desktop_keep_screen_on/desktop_keep_screen_on.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; void main() { runApp(const MyApp()); diff --git a/packages/desktop_keep_screen_on/example/pubspec.yaml b/packages/desktop_keep_screen_on/example/pubspec.yaml index 526330a9..193a4bf1 100644 --- a/packages/desktop_keep_screen_on/example/pubspec.yaml +++ b/packages/desktop_keep_screen_on/example/pubspec.yaml @@ -15,6 +15,7 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: + material_ui: ^1.11.0 flutter: sdk: flutter diff --git a/packages/desktop_lifecycle/example/lib/main.dart b/packages/desktop_lifecycle/example/lib/main.dart index e3161255..4ac229c3 100644 --- a/packages/desktop_lifecycle/example/lib/main.dart +++ b/packages/desktop_lifecycle/example/lib/main.dart @@ -1,5 +1,5 @@ import 'package:desktop_lifecycle/desktop_lifecycle.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; void main() { runApp(const MyApp()); diff --git a/packages/desktop_lifecycle/example/pubspec.yaml b/packages/desktop_lifecycle/example/pubspec.yaml index 924a4361..c503675f 100644 --- a/packages/desktop_lifecycle/example/pubspec.yaml +++ b/packages/desktop_lifecycle/example/pubspec.yaml @@ -16,6 +16,7 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: + material_ui: ^1.11.0 flutter: sdk: flutter diff --git a/packages/desktop_multi_window/example/lib/main.dart b/packages/desktop_multi_window/example/lib/main.dart index 470dc52b..3e0f382c 100644 --- a/packages/desktop_multi_window/example/lib/main.dart +++ b/packages/desktop_multi_window/example/lib/main.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:desktop_multi_window/desktop_multi_window.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:mixin_logger/mixin_logger.dart'; import 'package:window_manager/window_manager.dart'; diff --git a/packages/desktop_multi_window/example/lib/widgets/window_caption.dart b/packages/desktop_multi_window/example/lib/widgets/window_caption.dart index 73dd20a2..ac8e2342 100644 --- a/packages/desktop_multi_window/example/lib/widgets/window_caption.dart +++ b/packages/desktop_multi_window/example/lib/widgets/window_caption.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:window_manager/window_manager.dart'; class CustomWindowCaption extends StatefulWidget { diff --git a/packages/desktop_multi_window/example/lib/widgets/window_list.dart b/packages/desktop_multi_window/example/lib/widgets/window_list.dart index a5f3674d..0cf64500 100644 --- a/packages/desktop_multi_window/example/lib/widgets/window_list.dart +++ b/packages/desktop_multi_window/example/lib/widgets/window_list.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:desktop_multi_window/desktop_multi_window.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:flutter_multi_window_example/extensions/window_controller.dart'; import 'package:mixin_logger/mixin_logger.dart'; diff --git a/packages/desktop_multi_window/example/lib/windows/main_window.dart b/packages/desktop_multi_window/example/lib/windows/main_window.dart index 1c46a6e5..003b078a 100644 --- a/packages/desktop_multi_window/example/lib/windows/main_window.dart +++ b/packages/desktop_multi_window/example/lib/windows/main_window.dart @@ -1,5 +1,5 @@ import 'package:desktop_multi_window/desktop_multi_window.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:flutter_multi_window_example/extensions/window_controller.dart'; import 'package:mixin_logger/mixin_logger.dart'; diff --git a/packages/desktop_multi_window/example/lib/windows/sample_window.dart b/packages/desktop_multi_window/example/lib/windows/sample_window.dart index 7918156b..549efd65 100644 --- a/packages/desktop_multi_window/example/lib/windows/sample_window.dart +++ b/packages/desktop_multi_window/example/lib/windows/sample_window.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; class SampleWindow extends StatelessWidget { const SampleWindow({super.key}); diff --git a/packages/desktop_multi_window/example/lib/windows/video_player_window.dart b/packages/desktop_multi_window/example/lib/windows/video_player_window.dart index 633850b4..10710ca6 100644 --- a/packages/desktop_multi_window/example/lib/windows/video_player_window.dart +++ b/packages/desktop_multi_window/example/lib/windows/video_player_window.dart @@ -1,5 +1,5 @@ import 'package:desktop_multi_window/desktop_multi_window.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:mixin_logger/mixin_logger.dart'; import 'package:window_manager/window_manager.dart'; import 'package:video_player/video_player.dart'; diff --git a/packages/desktop_multi_window/example/pubspec.yaml b/packages/desktop_multi_window/example/pubspec.yaml index ef60ee4f..6d54b101 100644 --- a/packages/desktop_multi_window/example/pubspec.yaml +++ b/packages/desktop_multi_window/example/pubspec.yaml @@ -16,6 +16,7 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: + material_ui: ^1.11.0 flutter: sdk: flutter diff --git a/packages/desktop_webview_window/example/integration_test/src/background_scenarios.dart b/packages/desktop_webview_window/example/integration_test/src/background_scenarios.dart index 722344a5..7ce60619 100644 --- a/packages/desktop_webview_window/example/integration_test/src/background_scenarios.dart +++ b/packages/desktop_webview_window/example/integration_test/src/background_scenarios.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:desktop_webview_window/desktop_webview_window.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:webview_window_example/demo_page_server.dart'; diff --git a/packages/desktop_webview_window/example/lib/demo_app.dart b/packages/desktop_webview_window/example/lib/demo_app.dart index 2d42a2a9..7dd059db 100644 --- a/packages/desktop_webview_window/example/lib/demo_app.dart +++ b/packages/desktop_webview_window/example/lib/demo_app.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:desktop_webview_window/desktop_webview_window.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'demo_state.dart'; diff --git a/packages/desktop_webview_window/example/lib/demo_state.dart b/packages/desktop_webview_window/example/lib/demo_state.dart index 04366469..5ea3181e 100644 --- a/packages/desktop_webview_window/example/lib/demo_state.dart +++ b/packages/desktop_webview_window/example/lib/demo_state.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:desktop_webview_window/desktop_webview_window.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:flutter/services.dart'; import 'demo_page_server.dart'; diff --git a/packages/desktop_webview_window/example/pubspec.yaml b/packages/desktop_webview_window/example/pubspec.yaml index 2d3dbddf..b2447696 100644 --- a/packages/desktop_webview_window/example/pubspec.yaml +++ b/packages/desktop_webview_window/example/pubspec.yaml @@ -17,6 +17,7 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: + material_ui: ^1.11.0 flutter: sdk: flutter diff --git a/packages/desktop_webview_window/example/test/demo_test.dart b/packages/desktop_webview_window/example/test/demo_test.dart index 0d9e8a4e..ac4b7a51 100644 --- a/packages/desktop_webview_window/example/test/demo_test.dart +++ b/packages/desktop_webview_window/example/test/demo_test.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:desktop_webview_window/src/native_transport.dart'; import 'package:desktop_webview_window/src/webview_impl.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/packages/fts5_simple/example/lib/main.dart b/packages/fts5_simple/example/lib/main.dart index aa5235d8..15a0b823 100644 --- a/packages/fts5_simple/example/lib/main.dart +++ b/packages/fts5_simple/example/lib/main.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:fts5_simple/fts5_simple.dart'; import 'package:sqlite3/sqlite3.dart' hide Row; diff --git a/packages/fts5_simple/example/linux/flutter/generated_plugin_registrant.cc b/packages/fts5_simple/example/linux/flutter/generated_plugin_registrant.cc index a010d58c..13c84504 100644 --- a/packages/fts5_simple/example/linux/flutter/generated_plugin_registrant.cc +++ b/packages/fts5_simple/example/linux/flutter/generated_plugin_registrant.cc @@ -6,14 +6,14 @@ #include "generated_plugin_registrant.h" -#include #include +#include void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) fts5_simple_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "Fts5SimplePlugin"); - fts5_simple_plugin_register_with_registrar(fts5_simple_registrar); g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) fts5_simple_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Fts5SimplePlugin"); + fts5_simple_plugin_register_with_registrar(fts5_simple_registrar); } diff --git a/packages/fts5_simple/example/linux/flutter/generated_plugins.cmake b/packages/fts5_simple/example/linux/flutter/generated_plugins.cmake index b5f3aa85..170a9a83 100644 --- a/packages/fts5_simple/example/linux/flutter/generated_plugins.cmake +++ b/packages/fts5_simple/example/linux/flutter/generated_plugins.cmake @@ -3,8 +3,8 @@ # list(APPEND FLUTTER_PLUGIN_LIST - fts5_simple sqlite3_flutter_libs + fts5_simple ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/packages/fts5_simple/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/fts5_simple/example/macos/Flutter/GeneratedPluginRegistrant.swift index 14d17306..159f1020 100644 --- a/packages/fts5_simple/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/packages/fts5_simple/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,10 @@ import FlutterMacOS import Foundation -import fts5_simple import sqlite3_flutter_libs +import fts5_simple func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - Fts5SimplePlugin.register(with: registry.registrar(forPlugin: "Fts5SimplePlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) + Fts5SimplePlugin.register(with: registry.registrar(forPlugin: "Fts5SimplePlugin")) } diff --git a/packages/fts5_simple/example/pubspec.yaml b/packages/fts5_simple/example/pubspec.yaml index fb13071b..6696cffd 100644 --- a/packages/fts5_simple/example/pubspec.yaml +++ b/packages/fts5_simple/example/pubspec.yaml @@ -16,6 +16,7 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: + material_ui: ^1.11.0 flutter: sdk: flutter diff --git a/packages/fts5_simple/example/windows/flutter/generated_plugin_registrant.cc b/packages/fts5_simple/example/windows/flutter/generated_plugin_registrant.cc index 48dccc07..be527612 100644 --- a/packages/fts5_simple/example/windows/flutter/generated_plugin_registrant.cc +++ b/packages/fts5_simple/example/windows/flutter/generated_plugin_registrant.cc @@ -6,12 +6,12 @@ #include "generated_plugin_registrant.h" -#include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { - Fts5SimplePluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("Fts5SimplePlugin")); Sqlite3FlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); + Fts5SimplePluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("Fts5SimplePlugin")); } diff --git a/packages/fts5_simple/example/windows/flutter/generated_plugins.cmake b/packages/fts5_simple/example/windows/flutter/generated_plugins.cmake index 0c817a08..4d956710 100644 --- a/packages/fts5_simple/example/windows/flutter/generated_plugins.cmake +++ b/packages/fts5_simple/example/windows/flutter/generated_plugins.cmake @@ -3,8 +3,8 @@ # list(APPEND FLUTTER_PLUGIN_LIST - fts5_simple sqlite3_flutter_libs + fts5_simple ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/packages/mixin_logger/example/lib/main.dart b/packages/mixin_logger/example/lib/main.dart index 5bd91dd1..6f9d4d23 100644 --- a/packages/mixin_logger/example/lib/main.dart +++ b/packages/mixin_logger/example/lib/main.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:mixin_logger/mixin_logger.dart'; import 'package:path/path.dart' as p; diff --git a/packages/mixin_logger/example/pubspec.yaml b/packages/mixin_logger/example/pubspec.yaml index 7cf7d3db..40977a67 100644 --- a/packages/mixin_logger/example/pubspec.yaml +++ b/packages/mixin_logger/example/pubspec.yaml @@ -29,6 +29,7 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: + material_ui: ^1.11.0 flutter: sdk: flutter diff --git a/packages/mixin_markdown_widget/CHANGELOG.md b/packages/mixin_markdown_widget/CHANGELOG.md index e317814f..32810f48 100644 --- a/packages/mixin_markdown_widget/CHANGELOG.md +++ b/packages/mixin_markdown_widget/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.5.0 + +### Breaking changes + +- Require Flutter 3.47.0 / Dart 3.13.0 or later, and move from the deprecated `package:flutter/material.dart` to [`material_ui`](https://pub.dev/packages/material_ui). ([#558](https://github.com/MixinNetwork/flutter-plugins/pull/558)) +- `MarkdownThemeData` now extends the `material_ui` `ThemeExtension`, and the package reads `Theme`, `TextSelectionTheme`, and other Material ancestors from `material_ui`. Flutter's legacy Material types are separate types, so register the theme in a `material_ui` `ThemeData(extensions: [...])` and build the surrounding `MaterialApp`, `Theme`, and `Material` ancestors with `package:material_ui/material_ui.dart`. See the README. ([#558](https://github.com/MixinNetwork/flutter-plugins/pull/558)) + ## 0.4.0 ### Breaking changes diff --git a/packages/mixin_markdown_widget/README.md b/packages/mixin_markdown_widget/README.md index dfb5f2c5..2e9e3def 100644 --- a/packages/mixin_markdown_widget/README.md +++ b/packages/mixin_markdown_widget/README.md @@ -15,12 +15,22 @@ - Disable default scroll view (ListView) using `useColumn: true` for easy integration into existing custom scrollable layouts. - **Theming:** `MarkdownThemeData` allows deep customization of text styles, spacing, colors, and block decorations (e.g., hiding heading dividers via `showHeading1Divider`). +## Requirements and Material integration + +Requires Dart ^3.13.0 and Flutter >=3.47.0. This package uses +`package:material_ui/material_ui.dart` for its public Material types and widgets. +Use the same import in your application for `MaterialApp`, `Theme`, `ThemeData`, +`TextSelectionTheme`, and `Material` ancestors, including custom block builders. +`MarkdownThemeData` extends the `material_ui` `ThemeExtension` and can be +registered in its `ThemeData.extensions`. Flutter's legacy Material theme types +are separate types and cannot be used interchangeably with these APIs. + ## Usage ### Basic Rendering ```dart -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:mixin_markdown_widget/mixin_markdown_widget.dart'; class ExamplePage extends StatelessWidget { diff --git a/packages/mixin_markdown_widget/benchmark/incremental_append_benchmark.dart b/packages/mixin_markdown_widget/benchmark/incremental_append_benchmark.dart index 6112c324..02137f17 100644 --- a/packages/mixin_markdown_widget/benchmark/incremental_append_benchmark.dart +++ b/packages/mixin_markdown_widget/benchmark/incremental_append_benchmark.dart @@ -53,7 +53,8 @@ void main() { 'incremental parser timing: ${incrementalResult.timing.describe()}', ); if (incrementalResult.elapsed.inMicroseconds > 0) { - final speedup = fullResult.elapsed.inMicroseconds / + final speedup = + fullResult.elapsed.inMicroseconds / incrementalResult.elapsed.inMicroseconds; stdout.writeln('speedup: ${speedup.toStringAsFixed(2)}x'); } @@ -61,10 +62,7 @@ void main() { }); } -void _warmUp( - String initialSource, - List chunks, -) { +void _warmUp(String initialSource, List chunks) { _benchmarkFullParse(initialSource, chunks); _benchmarkIncrementalParse(initialSource, chunks); } @@ -116,28 +114,42 @@ String _buildInitialMarkdown({required int repetitions}) { r'\( a_i^2 + b_i^2 = c_i^2 \), and plain prose that should wrap ' 'across multiple visual lines in realistic desktop layouts.', ) - ..write('\n\n- Hot path with `parse_$index`, `scan_$index`, ' - '`normalize_$index`, `layout_$index`, `paint_$index`, ' - '`select_$index`, `copy_$index`, and `commit_$index` all in one line') ..write( - '\n- [x] Completed task with [trace](https://trace.example/$index)') + '\n\n- Hot path with `parse_$index`, `scan_$index`, ' + '`normalize_$index`, `layout_$index`, `paint_$index`, ' + '`select_$index`, `copy_$index`, and `commit_$index` all in one line', + ) + ..write( + '\n- [x] Completed task with [trace](https://trace.example/$index)', + ) ..write('\n- [ ] Pending task with nested context') - ..write('\n 1. Ordered child with `child_${index}_a` and ' - r'\( \Delta t < 16ms \)') + ..write( + '\n 1. Ordered child with `child_${index}_a` and ' + r'\( \Delta t < 16ms \)', + ) ..write('\n 2. Ordered child with **rich text** and more wrapping text') ..write( - '\n\n> Quote $index starts with `quoted_token_$index` and a link.') - ..write('\n> > Nested quote keeps **strong text**, _emphasis_, and ' - r'\( q_{n+1}=q_n+r \).') + '\n\n> Quote $index starts with `quoted_token_$index` and a link.', + ) + ..write( + '\n> > Nested quote keeps **strong text**, _emphasis_, and ' + r'\( q_{n+1}=q_n+r \).', + ) ..write('\n\nTerm $index') - ..write('\n: Definition includes ==mark==, H~2~O, 2^10^, ' - 'Cmd+K, and https://example.com/plain/$index.') + ..write( + '\n: Definition includes ==mark==, H~2~O, 2^10^, ' + 'Cmd+K, and https://example.com/plain/$index.', + ) ..write('\n\n| Metric | Value | Notes |') ..write('\n| :--- | ---: | :--- |') - ..write('\n| parse | ${index * 3 + 7} | `MarkdownDocumentParser` ' - 'with [range](https://range.example/$index) |') - ..write('\n| render | ${index * 5 + 11} | wraps `inline` code and ' - r'\( \sqrt{x^2+y^2} \) math |') + ..write( + '\n| parse | ${index * 3 + 7} | `MarkdownDocumentParser` ' + 'with [range](https://range.example/$index) |', + ) + ..write( + '\n| render | ${index * 5 + 11} | wraps `inline` code and ' + r'\( \sqrt{x^2+y^2} \) math |', + ) ..write('\n\n```dart') ..write('\nfinal tokens$index = [') ..write('\n for (var i = 0; i < 8; i++) "token_\$i",') @@ -147,8 +159,10 @@ String _buildInitialMarkdown({required int repetitions}) { ..write( '\n\n![Benchmark image $index](missing-image-$index.png?w=640&h=240)', ) - ..write('\n\n[^baseline-$index]: Footnote with `footnote_$index`, ' - '[reference](https://footnote.example/$index), and extra detail.'); + ..write( + '\n\n[^baseline-$index]: Footnote with `footnote_$index`, ' + '[reference](https://footnote.example/$index), and extra detail.', + ); } return buffer.toString(); } @@ -224,10 +238,7 @@ class _BenchmarkScenario { } class _BenchmarkResult { - const _BenchmarkResult({ - required this.elapsed, - required this.timing, - }); + const _BenchmarkResult({required this.elapsed, required this.timing}); final Duration elapsed; final _TimingSummary timing; diff --git a/packages/mixin_markdown_widget/benchmark/pretext_layout_benchmark.dart b/packages/mixin_markdown_widget/benchmark/pretext_layout_benchmark.dart index 75433f66..cfb3f0d8 100644 --- a/packages/mixin_markdown_widget/benchmark/pretext_layout_benchmark.dart +++ b/packages/mixin_markdown_widget/benchmark/pretext_layout_benchmark.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mixin_markdown_widget/mixin_markdown_widget.dart'; diff --git a/packages/mixin_markdown_widget/benchmark/selection_geometry_benchmark.dart b/packages/mixin_markdown_widget/benchmark/selection_geometry_benchmark.dart index 62da285b..45c43cf7 100644 --- a/packages/mixin_markdown_widget/benchmark/selection_geometry_benchmark.dart +++ b/packages/mixin_markdown_widget/benchmark/selection_geometry_benchmark.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mixin_markdown_widget/mixin_markdown_widget.dart'; @@ -183,65 +183,86 @@ String _buildComplexMarkdown({required int sections}) { '`mu_$index` `nu_$index` `xi_$index` `omicron_$index` `pi_$index`.', ) ..write( - '\n\n> Quote line one for section $index with **highlighted** context.') + '\n\n> Quote line one for section $index with **highlighted** context.', + ) + ..write( + '\n> Quote line two includes \$x_${index % 9}\$ and [nested links](https://quote.example/$index).', + ) ..write( - '\n> Quote line two includes \$x_${index % 9}\$ and [nested links](https://quote.example/$index).') - ..write('\n> > Nested quote line keeps `nested_quote_$index`, ' - '**bold**, _emphasis_, and more text for geometry traversal.') - ..write('\n> > > Deep quote line with ' - r'\( \sum_{i=0}^{n} i = n(n+1)/2 \)' - ' and `deep_$index`.') + '\n> > Nested quote line keeps `nested_quote_$index`, ' + '**bold**, _emphasis_, and more text for geometry traversal.', + ) + ..write( + '\n> > > Deep quote line with ' + r'\( \sum_{i=0}^{n} i = n(n+1)/2 \)' + ' and `deep_$index`.', + ) ..write('\n\n- Bullet one with detail for section $index') ..write( - '\n- Bullet two with `token_$index` and more copy to wrap across the viewport width') - ..write('\n- Bullet three contains many inline code spans: ' - '`a_$index`, `b_$index`, `c_$index`, `d_$index`, `e_$index`, ' - '`f_$index`, `g_$index`, `h_$index`, `i_$index`, `j_$index`, ' - '`k_$index`, and `l_$index` in one visual row candidate') + '\n- Bullet two with `token_$index` and more copy to wrap across the viewport width', + ) + ..write( + '\n- Bullet three contains many inline code spans: ' + '`a_$index`, `b_$index`, `c_$index`, `d_$index`, `e_$index`, ' + '`f_$index`, `g_$index`, `h_$index`, `i_$index`, `j_$index`, ' + '`k_$index`, and `l_$index` in one visual row candidate', + ) ..write('\n- [x] Checked task with [audit](https://tasks.example/$index)') ..write('\n- [ ] Pending task with **rich** text and `pending_$index`') ..write( - '\n - Nested child with extra words and \$y_${index % 11}\$ inline math') + '\n - Nested child with extra words and \$y_${index % 11}\$ inline math', + ) ..write('\n 1. Ordered nested child with `ordered_${index}_a`') - ..write('\n 2. Ordered nested child with `ordered_${index}_b` ' - 'and enough text to wrap onto another line') + ..write( + '\n 2. Ordered nested child with `ordered_${index}_b` ' + 'and enough text to wrap onto another line', + ) ..write('\n\nTerm $index') - ..write('\n: Definition paragraph with **rich text**, ==mark==, H~2~O, ' - '2^10^, Cmd+K, and a trailing sentence for wrapping.') ..write( - '\n\n![Selection image $index](missing-selection-$index.png?w=640&h=260)') + '\n: Definition paragraph with **rich text**, ==mark==, H~2~O, ' + '2^10^, Cmd+K, and a trailing sentence for wrapping.', + ) + ..write( + '\n\n![Selection image $index](missing-selection-$index.png?w=640&h=260)', + ) ..write('\n\n| Col A | Col B | Col C |') ..write('\n| --- | --- | --- |') ..write( - '\n| row $index | value ${index * 3} | note with [ref](https://table.example/$index) |') + '\n| row $index | value ${index * 3} | note with [ref](https://table.example/$index) |', + ) ..write( - '\n| row ${index + 1} | value ${index * 5} | another wrapped cell for coverage |') + '\n| row ${index + 1} | value ${index * 5} | another wrapped cell for coverage |', + ) ..write( - '\n| row ${index + 2} | `cell_${index}_a` `cell_${index}_b` `cell_${index}_c` | ' - r'\( \sqrt{x^2+y^2} \)' - ' mixed with **bold** and _emphasis_ |') + '\n| row ${index + 2} | `cell_${index}_a` `cell_${index}_b` `cell_${index}_c` | ' + r'\( \sqrt{x^2+y^2} \)' + ' mixed with **bold** and _emphasis_ |', + ) ..write('\n\n| Wide A | Wide B | Wide C | Wide D | Wide E |') ..write('\n| :--- | ---: | :---: | :--- | :--- |') - ..write('\n| `parse_$index` | ${index * 7} | ✅ | ' - '[trace](https://wide.example/$index) | long text that should push horizontal table logic |') - ..write('\n| `layout_$index` | ${index * 11} | ⚠️ | ' - r'\( x_{i+1}=x_i+\Delta \)' - ' | `a` `b` `c` `d` `e` in cell |') + ..write( + '\n| `parse_$index` | ${index * 7} | ✅ | ' + '[trace](https://wide.example/$index) | long text that should push horizontal table logic |', + ) + ..write( + '\n| `layout_$index` | ${index * 11} | ⚠️ | ' + r'\( x_{i+1}=x_i+\Delta \)' + ' | `a` `b` `c` `d` `e` in cell |', + ) ..write('\n\n```dart') ..write('\nString label$index = "section_$index";') ..write('\nint compute$index(int input) => input * ${index + 3};') ..write('\n```') - ..write('\n\n[^selection-$index]: Footnote body with `note_$index`, ' - '[source](https://footnote.example/$index), and text that selection can traverse.'); + ..write( + '\n\n[^selection-$index]: Footnote body with `note_$index`, ' + '[source](https://footnote.example/$index), and text that selection can traverse.', + ); } return buffer.toString(); } class _BenchmarkScenario { - const _BenchmarkScenario({ - required this.name, - required this.sections, - }); + const _BenchmarkScenario({required this.name, required this.sections}); final String name; final int sections; diff --git a/packages/mixin_markdown_widget/example/lib/ai_chat_demo.dart b/packages/mixin_markdown_widget/example/lib/ai_chat_demo.dart index 9e4d2b63..eb06c3ae 100644 --- a/packages/mixin_markdown_widget/example/lib/ai_chat_demo.dart +++ b/packages/mixin_markdown_widget/example/lib/ai_chat_demo.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:mixin_markdown_widget/mixin_markdown_widget.dart'; class AIChatDemoPage extends StatefulWidget { @@ -26,15 +26,14 @@ class _AIChatDemoPageState extends State { final random = Random(); // Pre-populate 20 messages (20 pairs) from the rich 5 presets. for (var i = 0; i < 20; i++) { - _messages.add(ChatMessage.static( - text: 'Detailed Inquiry regarding technical specification #${i + 1}', - isUser: true, - )); + _messages.add( + ChatMessage.static( + text: 'Detailed Inquiry regarding technical specification #${i + 1}', + isUser: true, + ), + ); final markdown = _presetMarkdown[random.nextInt(_presetMarkdown.length)]; - _messages.add(ChatMessage.static( - text: markdown, - isUser: false, - )); + _messages.add(ChatMessage.static(text: markdown, isUser: false)); } } @@ -361,17 +360,14 @@ Additionally, we support `Ctrl + C` and `underlined [^2]: This is the second footnote, testing multiple reference points. **End of Stress Test.** -''' +''', ]; void _handleSubmitted(String text) { if (text.trim().isEmpty) return; _textController.clear(); setState(() { - _messages.add(ChatMessage.static( - text: text, - isUser: true, - )); + _messages.add(ChatMessage.static(text: text, isUser: true)); _isTyping = true; }); _scrollToBottom(); @@ -424,10 +420,7 @@ Additionally, we support `Ctrl + C` and `underlined @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Advanced AI Chat Demo'), - elevation: 1, - ), + appBar: AppBar(title: const Text('Advanced AI Chat Demo'), elevation: 1), body: Column( children: [ Expanded( @@ -443,9 +436,13 @@ Additionally, we support `Ctrl + C` and `underlined if (_isTyping) const Padding( padding: EdgeInsets.all(8.0), - child: Text('AI is processing massive data...', - style: TextStyle( - fontStyle: FontStyle.italic, color: Colors.grey)), + child: Text( + 'AI is processing massive data...', + style: TextStyle( + fontStyle: FontStyle.italic, + color: Colors.grey, + ), + ), ), const Divider(height: 1), Container( @@ -463,8 +460,10 @@ Additionally, we support `Ctrl + C` and `underlined border: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(20)), ), - contentPadding: - EdgeInsets.symmetric(horizontal: 16, vertical: 8), + contentPadding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), ), ), ), @@ -485,12 +484,12 @@ class ChatMessage { /// Creates a message whose full text is known up front (user messages and /// pre-populated AI messages). ChatMessage.static({required this.text, required this.isUser}) - : markdownController = null; + : markdownController = null; /// Creates an AI message that will be populated incrementally via a stream. ChatMessage.streaming({required this.isUser}) - : text = '', - markdownController = MarkdownController(); + : text = '', + markdownController = MarkdownController(); final String text; final bool isUser; @@ -555,14 +554,12 @@ class _MessageTileState extends State<_MessageTile> { return Padding( padding: const EdgeInsets.symmetric(vertical: 12.0), child: Row( - mainAxisAlignment: - isUser ? MainAxisAlignment.end : MainAxisAlignment.start, + mainAxisAlignment: isUser + ? MainAxisAlignment.end + : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (!isUser) - const CircleAvatar( - child: Icon(Icons.terminal_rounded), - ), + if (!isUser) const CircleAvatar(child: Icon(Icons.terminal_rounded)), if (!isUser) const SizedBox(width: 12), Flexible( child: Container( @@ -586,16 +583,17 @@ class _MessageTileState extends State<_MessageTile> { ], ), child: isUser - ? Text(widget.message.text, - style: const TextStyle(fontWeight: FontWeight.w500)) + ? Text( + widget.message.text, + style: const TextStyle(fontWeight: FontWeight.w500), + ) : MarkdownWidget( controller: widget.message.markdownController ?? _controller, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - theme: MarkdownThemeData.fallback(context).copyWith( - maxContentWidth: double.infinity, - ), + theme: MarkdownThemeData.fallback(context) + .copyWith(maxContentWidth: double.infinity), ), ), ), diff --git a/packages/mixin_markdown_widget/example/lib/composite_selection_demo.dart b/packages/mixin_markdown_widget/example/lib/composite_selection_demo.dart index 9d25598b..c94158b0 100644 --- a/packages/mixin_markdown_widget/example/lib/composite_selection_demo.dart +++ b/packages/mixin_markdown_widget/example/lib/composite_selection_demo.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:mixin_markdown_widget/mixin_markdown_widget.dart'; class CompositeSelectionDemoPage extends StatefulWidget { @@ -84,10 +84,8 @@ No issues found! (ran in 1.3s) Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; - final markdownTheme = MarkdownThemeData.fallback(context).copyWith( - padding: EdgeInsets.zero, - maxContentWidth: double.infinity, - ); + final markdownTheme = MarkdownThemeData.fallback(context) + .copyWith(padding: EdgeInsets.zero, maxContentWidth: double.infinity); return Scaffold( appBar: AppBar( @@ -102,8 +100,9 @@ No issues found! (ran in 1.3s) animation: _selectionController, builder: (context, _) => IconButton( tooltip: 'Copy selected composite text', - onPressed: - _selectionController.hasSelection ? _copySelected : null, + onPressed: _selectionController.hasSelection + ? _copySelected + : null, icon: const Icon(Icons.content_copy_outlined), ), ), @@ -175,8 +174,7 @@ No issues found! (ran in 1.3s) SizedBox(height: 14), _ToolCallBlock( id: 'git-status', - summary: - 'Explored 2 files, searched once, ran 1 command', + summary: 'Explored 2 files, searched once, ran 1 command', command: 'git status --short', resultMarkdown: _gitStatusOutput, ), @@ -188,10 +186,8 @@ No issues found! (ran in 1.3s) SizedBox(height: 14), _ToolCallBlock( id: 'flutter-analyze', - summary: - 'Read 1 file, resolved dependencies, ran 1 command', - command: - 'flutter analyze example/lib/composite_selection_demo.dart', + summary: 'Read 1 file, resolved dependencies, ran 1 command', + command: 'flutter analyze example/lib/composite_selection_demo.dart', resultMarkdown: _analyzeOutput, ), SizedBox(height: 14), @@ -223,27 +219,24 @@ class _LineWithTwoSelectable extends StatelessWidget { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return DecoratedBox( - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.72), - border: Border.all(color: colorScheme.outlineVariant), - borderRadius: BorderRadius.circular(8), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.72), + border: Border.all(color: colorScheme.outlineVariant), + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: MixinSelectableRow( + selectionId: 'line-with-two-selectable', + spacing: 6, + children: const [ + MixinSelectableText('left'), + Icon(Icons.arrow_forward_rounded, size: 16), + MixinSelectableText('right'), + ], ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - child: MixinSelectableRow( - selectionId: 'line-with-two-selectable', - spacing: 6, - children: const [ - MixinSelectableText( - 'left', - ), - Icon(Icons.arrow_forward_rounded, size: 16), - MixinSelectableText( - 'right', - ), - ], - ), - )); + ), + ); } } @@ -285,15 +278,11 @@ class _ToolCallBlockState extends State<_ToolCallBlock> crossAxisAlignment: CrossAxisAlignment.start, children: [ InkWell( - onTap: () => setState( - () => _isToolCallExpanded = !_isToolCallExpanded, - ), + onTap: () => + setState(() => _isToolCallExpanded = !_isToolCallExpanded), borderRadius: BorderRadius.circular(6), child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 2, - vertical: 4, - ), + padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 4), child: Row( children: [ Icon( @@ -335,9 +324,7 @@ class _ToolCallBlockState extends State<_ToolCallBlock> child: DecoratedBox( decoration: BoxDecoration( color: colorScheme.surface.withValues(alpha: 0.72), - border: Border.all( - color: colorScheme.outlineVariant, - ), + border: Border.all(color: colorScheme.outlineVariant), borderRadius: BorderRadius.circular(6), ), child: Padding( @@ -395,12 +382,11 @@ class _ToolCallBlockState extends State<_ToolCallBlock> alignment: Alignment.topCenter, child: _isCommandExpanded ? Padding( - padding: const EdgeInsets.only( - top: 10, - ), + padding: const EdgeInsets.only(top: 10), child: ClipRRect( - borderRadius: - BorderRadius.circular(6), + borderRadius: BorderRadius.circular( + 6, + ), child: ConstrainedBox( constraints: const BoxConstraints( maxHeight: 200, diff --git a/packages/mixin_markdown_widget/example/lib/main.dart b/packages/mixin_markdown_widget/example/lib/main.dart index 7a1f7c70..11d017e9 100644 --- a/packages/mixin_markdown_widget/example/lib/main.dart +++ b/packages/mixin_markdown_widget/example/lib/main.dart @@ -1,20 +1,14 @@ -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:mixin_markdown_widget/mixin_markdown_widget.dart'; import 'ai_chat_demo.dart'; import 'composite_selection_demo.dart'; -enum _DemoLayoutMode { - split, - previewOnly, -} +enum _DemoLayoutMode { split, previewOnly } class _PreviewBackgroundPreset { - const _PreviewBackgroundPreset({ - required this.name, - required this.color, - }); + const _PreviewBackgroundPreset({required this.name, required this.color}); final String name; final Color color; @@ -320,9 +314,7 @@ This content was appended through `MarkdownController.appendChunk`. tooltip: 'AI Chat Demo', onPressed: () { Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const AIChatDemoPage(), - ), + MaterialPageRoute(builder: (context) => const AIChatDemoPage()), ); }, icon: const Icon(Icons.chat_outlined), @@ -383,8 +375,9 @@ This content was appended through `MarkdownController.appendChunk`. ), IconButton( tooltip: 'Commit stream draft', - onPressed: - _controller.streamingState.hasDraft ? _commitStream : null, + onPressed: _controller.streamingState.hasDraft + ? _commitStream + : null, icon: const Icon(Icons.done_all_rounded), ), IconButton( @@ -408,9 +401,8 @@ This content was appended through `MarkdownController.appendChunk`. minLines: null, keyboardType: TextInputType.multiline, textAlignVertical: TextAlignVertical.top, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontFamily: 'Menlo', - ), + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith(fontFamily: 'Menlo'), decoration: const InputDecoration( border: InputBorder.none, hintText: 'Type Markdown here...', @@ -454,10 +446,11 @@ This content was appended through `MarkdownController.appendChunk`. selectionController: _selectionController, theme: markdownTheme, imageBuilder: _buildMarkdownImage, - onTapLink: (destination, _, __) { + onTapLink: (destination, _, _) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Link tapped: $destination')), + content: Text('Link tapped: $destination'), + ), ); }, contextMenuBuilder: (context, details) { @@ -523,20 +516,20 @@ This content was appended through `MarkdownController.appendChunk`. child: _layoutMode == _DemoLayoutMode.previewOnly ? previewPanel : isWide - ? Row( - children: [ - Expanded(child: editorPanel), - const SizedBox(width: 20), - Expanded(child: previewPanel), - ], - ) - : Column( - children: [ - Expanded(child: editorPanel), - const SizedBox(height: 20), - Expanded(child: previewPanel), - ], - ), + ? Row( + children: [ + Expanded(child: editorPanel), + const SizedBox(width: 20), + Expanded(child: previewPanel), + ], + ) + : Column( + children: [ + Expanded(child: editorPanel), + const SizedBox(height: 20), + Expanded(child: previewPanel), + ], + ), ); }, ), @@ -601,10 +594,7 @@ This content was appended through `MarkdownController.appendChunk`. Row( children: [ Expanded( - child: Text( - 'Preview appearance', - style: textTheme.titleMedium, - ), + child: Text('Preview appearance', style: textTheme.titleMedium), ), IconButton( tooltip: 'Close preview appearance controls', @@ -712,9 +702,8 @@ This content was appended through `MarkdownController.appendChunk`. color: preset.color, shape: BoxShape.circle, border: Border.all( - color: ThemeData.estimateBrightnessForColor( - preset.color, - ) == + color: + ThemeData.estimateBrightnessForColor(preset.color) == Brightness.dark ? Colors.white.withValues(alpha: 0.45) : Colors.black.withValues(alpha: 0.12), @@ -723,7 +712,8 @@ This content was appended through `MarkdownController.appendChunk`. ), label: Text(preset.name), labelStyle: textTheme.labelMedium?.copyWith( - color: ThemeData.estimateBrightnessForColor(preset.color) == + color: + ThemeData.estimateBrightnessForColor(preset.color) == Brightness.dark ? const Color(0xFFF8FAFC) : const Color(0xFF1F2937), @@ -732,13 +722,15 @@ This content was appended through `MarkdownController.appendChunk`. : FontWeight.w500, ), backgroundColor: preset.color.withValues( - alpha: ThemeData.estimateBrightnessForColor(preset.color) == + alpha: + ThemeData.estimateBrightnessForColor(preset.color) == Brightness.dark ? 0.24 : 0.34, ), selectedColor: preset.color.withValues( - alpha: ThemeData.estimateBrightnessForColor(preset.color) == + alpha: + ThemeData.estimateBrightnessForColor(preset.color) == Brightness.dark ? 0.42 : 0.54, @@ -776,9 +768,9 @@ This content was appended through `MarkdownController.appendChunk`. void _copySelectedModelText() { _selectionController.copySelectionToClipboard(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Copied selected model text')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Copied selected model text'))); } void _resetDocument() { @@ -813,36 +805,28 @@ This content was appended through `MarkdownController.appendChunk`. final image = isSvg ? isNetwork - ? SvgPicture.network( - block.url, - width: width, - height: height, - fit: BoxFit.contain, - ) - : SvgPicture.asset( - path, - width: width, - height: height, - fit: BoxFit.contain, - ) + ? SvgPicture.network( + block.url, + width: width, + height: height, + fit: BoxFit.contain, + ) + : SvgPicture.asset( + path, + width: width, + height: height, + fit: BoxFit.contain, + ) : isNetwork - ? Image.network( - block.url, - width: width, - height: height, - fit: BoxFit.cover, - ) - : Image.asset( - path, - width: width, - height: height, - fit: BoxFit.cover, - ); - - return ClipRRect( - borderRadius: theme.imageBorderRadius, - child: image, - ); + ? Image.network( + block.url, + width: width, + height: height, + fit: BoxFit.cover, + ) + : Image.asset(path, width: width, height: height, fit: BoxFit.cover); + + return ClipRRect(borderRadius: theme.imageBorderRadius, child: image); } double? _dimensionFromQuery(Uri? uri, String key) { @@ -933,10 +917,7 @@ class _PaneShell extends StatelessWidget { if (subtitleBuilder != null) subtitleBuilder!(context) else if (subtitle != null) - Text( - subtitle!, - style: Theme.of(context).textTheme.bodyMedium, - ), + Text(subtitle!, style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 16), Expanded(child: child), ], diff --git a/packages/mixin_markdown_widget/example/pubspec.yaml b/packages/mixin_markdown_widget/example/pubspec.yaml index 82cb41fe..caaa9be5 100644 --- a/packages/mixin_markdown_widget/example/pubspec.yaml +++ b/packages/mixin_markdown_widget/example/pubspec.yaml @@ -5,10 +5,11 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: ^3.6.0 - flutter: ">=3.13.0" + sdk: ^3.13.0 + flutter: ">=3.47.0" dependencies: + material_ui: ^1.11.0 flutter: sdk: flutter flutter_svg: ^2.2.4 diff --git a/packages/mixin_markdown_widget/lib/src/clipboard/plain_text_serializer.dart b/packages/mixin_markdown_widget/lib/src/clipboard/plain_text_serializer.dart index 612b3083..c0d3fae0 100644 --- a/packages/mixin_markdown_widget/lib/src/clipboard/plain_text_serializer.dart +++ b/packages/mixin_markdown_widget/lib/src/clipboard/plain_text_serializer.dart @@ -85,9 +85,11 @@ class MarkdownPlainTextSerializer extends MarkdownCopySerializer { : DocumentRange(start: end, end: start); final sections = []; - for (var blockIndex = normalizedRange.start.blockIndex; - blockIndex <= normalizedRange.end.blockIndex; - blockIndex++) { + for ( + var blockIndex = normalizedRange.start.blockIndex; + blockIndex <= normalizedRange.end.blockIndex; + blockIndex++ + ) { final indexedBlock = _indexedBlockAt(document, blockIndex); if (indexedBlock.text.isEmpty) { continue; @@ -137,9 +139,11 @@ class MarkdownPlainTextSerializer extends MarkdownCopySerializer { if (first == null) { return null; } - for (var index = document.blocks.length - 1; - index > first.blockIndex; - index--) { + for ( + var index = document.blocks.length - 1; + index > first.blockIndex; + index-- + ) { final block = _indexedBlockAt(document, index); if (block.text.isNotEmpty) { return (first, block); @@ -157,15 +161,15 @@ class MarkdownPlainTextSerializer extends MarkdownCopySerializer { final targetBlockIndex = position.blockIndex < first.blockIndex ? first.blockIndex : position.blockIndex > last.blockIndex - ? last.blockIndex - : position.blockIndex; + ? last.blockIndex + : position.blockIndex; final candidate = _indexedBlockAt(document, targetBlockIndex); final block = candidate.text.isEmpty ? last : candidate; final targetOffset = position.textOffset < 0 ? 0 : position.textOffset > block.visibleTextLength - ? block.visibleTextLength - : position.textOffset; + ? block.visibleTextLength + : position.textOffset; final structure = block.structure; if (structure != null) { return structure.normalizePosition( @@ -195,13 +199,13 @@ class MarkdownPlainTextSerializer extends MarkdownCopySerializer { final lower = start < 0 ? 0 : start > block.text.length - ? block.text.length - : start; + ? block.text.length + : start; final upper = end < 0 ? 0 : end > block.text.length - ? block.text.length - : end; + ? block.text.length + : end; if (lower >= upper) { return ''; } diff --git a/packages/mixin_markdown_widget/lib/src/core/document.dart b/packages/mixin_markdown_widget/lib/src/core/document.dart index a67d18e2..f5670d41 100644 --- a/packages/mixin_markdown_widget/lib/src/core/document.dart +++ b/packages/mixin_markdown_widget/lib/src/core/document.dart @@ -31,24 +31,13 @@ enum MarkdownInlineKind { image, } -enum MarkdownTaskListItemState { - checked, - unchecked, -} +enum MarkdownTaskListItemState { checked, unchecked } -enum MarkdownTableColumnAlignment { - left, - center, - right, - none, -} +enum MarkdownTableColumnAlignment { left, center, right, none } @immutable class SourceRange { - const SourceRange({ - required this.start, - required this.end, - }); + const SourceRange({required this.start, required this.end}); final int start; final int end; @@ -56,14 +45,9 @@ class SourceRange { @immutable class MarkdownDocument { - const MarkdownDocument({ - required this.blocks, - this.version = 0, - }); + const MarkdownDocument({required this.blocks, this.version = 0}); - const MarkdownDocument.empty() - : blocks = const [], - version = 0; + const MarkdownDocument.empty() : blocks = const [], version = 0; final List blocks; final int version; @@ -71,11 +55,7 @@ class MarkdownDocument { @immutable abstract class BlockNode { - const BlockNode({ - required this.id, - required this.kind, - this.sourceRange, - }); + const BlockNode({required this.id, required this.kind, this.sourceRange}); final String id; final MarkdownBlockKind kind; @@ -128,10 +108,10 @@ class ListBlock extends BlockNode { this.startIndex = 1, super.sourceRange, }) : super( - kind: ordered - ? MarkdownBlockKind.orderedList - : MarkdownBlockKind.unorderedList, - ); + kind: ordered + ? MarkdownBlockKind.orderedList + : MarkdownBlockKind.unorderedList, + ); final bool ordered; final int startIndex; @@ -140,10 +120,7 @@ class ListBlock extends BlockNode { @immutable class ListItemNode { - const ListItemNode({ - required this.children, - this.taskState, - }); + const ListItemNode({required this.children, this.taskState}); final List children; final MarkdownTaskListItemState? taskState; @@ -162,10 +139,7 @@ class DefinitionListBlock extends BlockNode { @immutable class DefinitionListItemNode { - const DefinitionListItemNode({ - required this.term, - required this.definitions, - }); + const DefinitionListItemNode({required this.term, required this.definitions}); final List term; final List> definitions; @@ -225,10 +199,7 @@ class TableBlock extends BlockNode { @immutable class TableRowNode { - const TableRowNode({ - required this.cells, - required this.isHeader, - }); + const TableRowNode({required this.cells, required this.isHeader}); final List cells; final bool isHeader; @@ -262,18 +233,13 @@ class ImageBlock extends BlockNode { @immutable class ThematicBreakBlock extends BlockNode { - const ThematicBreakBlock({ - required super.id, - super.sourceRange, - }) : super(kind: MarkdownBlockKind.thematicBreak); + const ThematicBreakBlock({required super.id, super.sourceRange}) + : super(kind: MarkdownBlockKind.thematicBreak); } @immutable abstract class InlineNode { - const InlineNode({ - required this.kind, - this.sourceRange, - }); + const InlineNode({required this.kind, this.sourceRange}); final MarkdownInlineKind kind; final SourceRange? sourceRange; @@ -281,70 +247,56 @@ abstract class InlineNode { @immutable class TextInline extends InlineNode { - const TextInline({ - required this.text, - super.sourceRange, - }) : super(kind: MarkdownInlineKind.text); + const TextInline({required this.text, super.sourceRange}) + : super(kind: MarkdownInlineKind.text); final String text; } @immutable class EmphasisInline extends InlineNode { - const EmphasisInline({ - required this.children, - super.sourceRange, - }) : super(kind: MarkdownInlineKind.emphasis); + const EmphasisInline({required this.children, super.sourceRange}) + : super(kind: MarkdownInlineKind.emphasis); final List children; } @immutable class StrongInline extends InlineNode { - const StrongInline({ - required this.children, - super.sourceRange, - }) : super(kind: MarkdownInlineKind.strong); + const StrongInline({required this.children, super.sourceRange}) + : super(kind: MarkdownInlineKind.strong); final List children; } @immutable class StrikethroughInline extends InlineNode { - const StrikethroughInline({ - required this.children, - super.sourceRange, - }) : super(kind: MarkdownInlineKind.strikethrough); + const StrikethroughInline({required this.children, super.sourceRange}) + : super(kind: MarkdownInlineKind.strikethrough); final List children; } @immutable class HighlightInline extends InlineNode { - const HighlightInline({ - required this.children, - super.sourceRange, - }) : super(kind: MarkdownInlineKind.highlight); + const HighlightInline({required this.children, super.sourceRange}) + : super(kind: MarkdownInlineKind.highlight); final List children; } @immutable class SubscriptInline extends InlineNode { - const SubscriptInline({ - required this.children, - super.sourceRange, - }) : super(kind: MarkdownInlineKind.subscript); + const SubscriptInline({required this.children, super.sourceRange}) + : super(kind: MarkdownInlineKind.subscript); final List children; } @immutable class SuperscriptInline extends InlineNode { - const SuperscriptInline({ - required this.children, - super.sourceRange, - }) : super(kind: MarkdownInlineKind.superscript); + const SuperscriptInline({required this.children, super.sourceRange}) + : super(kind: MarkdownInlineKind.superscript); final List children; } @@ -377,10 +329,8 @@ class MathInline extends InlineNode { @immutable class InlineCode extends InlineNode { - const InlineCode({ - required this.text, - super.sourceRange, - }) : super(kind: MarkdownInlineKind.inlineCode); + const InlineCode({required this.text, super.sourceRange}) + : super(kind: MarkdownInlineKind.inlineCode); final String text; } @@ -388,22 +338,19 @@ class InlineCode extends InlineNode { @immutable class SoftBreakInline extends InlineNode { const SoftBreakInline({super.sourceRange}) - : super(kind: MarkdownInlineKind.softBreak); + : super(kind: MarkdownInlineKind.softBreak); } @immutable class HardBreakInline extends InlineNode { const HardBreakInline({super.sourceRange}) - : super(kind: MarkdownInlineKind.hardBreak); + : super(kind: MarkdownInlineKind.hardBreak); } @immutable class InlineImage extends InlineNode { - const InlineImage({ - required this.url, - this.alt, - super.sourceRange, - }) : super(kind: MarkdownInlineKind.image); + const InlineImage({required this.url, this.alt, super.sourceRange}) + : super(kind: MarkdownInlineKind.image); final String url; final String? alt; @@ -483,10 +430,7 @@ class DocumentPosition implements Comparable { @immutable class DocumentSelection { - const DocumentSelection({ - required this.base, - required this.extent, - }); + const DocumentSelection({required this.base, required this.extent}); final DocumentPosition base; final DocumentPosition extent; @@ -514,10 +458,7 @@ class DocumentSelection { @immutable class DocumentRange { - const DocumentRange({ - required this.start, - required this.end, - }); + const DocumentRange({required this.start, required this.end}); final DocumentPosition start; final DocumentPosition end; diff --git a/packages/mixin_markdown_widget/lib/src/parser/markdown_document_parser.dart b/packages/mixin_markdown_widget/lib/src/parser/markdown_document_parser.dart index d8ce0b99..ac64aa81 100644 --- a/packages/mixin_markdown_widget/lib/src/parser/markdown_document_parser.dart +++ b/packages/mixin_markdown_widget/lib/src/parser/markdown_document_parser.dart @@ -130,7 +130,8 @@ class MarkdownDocumentParser { // A hidden trailing comment may still be open across a blank line. // Reparse its owning source tail before treating appended text as visible. - final parseAppendedChunkAsNewBlocks = hasBlankLineBoundary && + final parseAppendedChunkAsNewBlocks = + hasBlankLineBoundary && !previousTailLines.any( MarkdownHtmlCommentBlockSyntax.openingPattern.hasMatch, ) && @@ -161,8 +162,9 @@ class MarkdownDocumentParser { return parseLines(sourceLines, version: version); } - final sourceOffset = - parseAppendedChunkAsNewBlocks ? previousSourceLength : lastRange.start; + final sourceOffset = parseAppendedChunkAsNewBlocks + ? previousSourceLength + : lastRange.start; final tailLines = parseAppendedChunkAsNewBlocks ? appendedLines : _appendLines(previousTailLines, appendedLines); @@ -210,10 +212,11 @@ class MarkdownDocumentParser { final tailBlocks = tailDocument.blocks; final tailFootnotes = tailBlocks.isNotEmpty && tailBlocks.last is FootnoteListBlock - ? tailBlocks.last as FootnoteListBlock - : null; - final tailContentLength = - tailFootnotes == null ? tailBlocks.length : tailBlocks.length - 1; + ? tailBlocks.last as FootnoteListBlock + : null; + final tailContentLength = tailFootnotes == null + ? tailBlocks.length + : tailBlocks.length - 1; final mergedFootnotes = tailFootnotes == null ? previousFootnotes : _mergeFootnoteLists(previousFootnotes, tailFootnotes); @@ -273,12 +276,10 @@ class MarkdownDocumentParser { final ranges = _measure( timing, (elapsed) => timing!.scanRangesMicros += elapsed, - () => _scanTopLevelBlockRanges( - lines, - sourceOffset: sourceOffset, - ), + () => _scanTopLevelBlockRanges(lines, sourceOffset: sourceOffset), ); - final blockRanges = ranges.length == blocks.length + 1 && + final blockRanges = + ranges.length == blocks.length + 1 && (blocks.isEmpty || blocks.last is! FootnoteListBlock) ? ranges.take(blocks.length).toList(growable: false) : ranges; @@ -607,7 +608,8 @@ class MarkdownDocumentParser { }) { final appendedStartsWithNewline = appendedLines.length > 1 && appendedLines.first.isEmpty; - final appendedStartsWithBlankLine = appendedLines.length > 2 && + final appendedStartsWithBlankLine = + appendedLines.length > 2 && appendedLines[0].isEmpty && appendedLines[1].isEmpty; if (previousSourceEndsWithBlankLine || appendedStartsWithBlankLine) { @@ -661,8 +663,9 @@ class MarkdownDocumentParser { continue; } - if (MarkdownHtmlCommentBlockSyntax.openingPattern - .hasMatch(lines[index])) { + if (MarkdownHtmlCommentBlockSyntax.openingPattern.hasMatch( + lines[index], + )) { hiddenStart ??= index; while (index < lines.length) { final closed = lines[index].contains('-->'); @@ -911,7 +914,8 @@ class MarkdownDocumentParser { final marker = fence[0]; final minimumLength = fence.length; final pattern = RegExp( - '^\\s{0,3}${RegExp.escape(marker)}{$minimumLength,}\\s*' r'$', + '^\\s{0,3}${RegExp.escape(marker)}{$minimumLength,}\\s*' + r'$', ); return pattern.hasMatch(line); } @@ -989,11 +993,7 @@ class MarkdownDocumentParser { return indent; } - int _lineEndOffset( - List lines, - List lineStarts, - int lineIndex, - ) { + int _lineEndOffset(List lines, List lineStarts, int lineIndex) { final contentEnd = lineStarts[lineIndex] + lines[lineIndex].length; if (lineIndex < lines.length - 1) { return contentEnd + 1; @@ -1003,16 +1003,20 @@ class MarkdownDocumentParser { static final RegExp _fenceStartPattern = RegExp(r'^\s{0,3}([`~]{3,}).*$'); static final RegExp _atxHeadingPattern = RegExp(r'^\s{0,3}#{1,6}(?:\s+|$)'); - static final RegExp _setextUnderlinePattern = - RegExp(r'^\s{0,3}(?:=+|-+)\s*$'); + static final RegExp _setextUnderlinePattern = RegExp( + r'^\s{0,3}(?:=+|-+)\s*$', + ); static final RegExp _blockquotePattern = RegExp(r'^\s{0,3}>\s?.*$'); - static final RegExp _listMarkerPattern = - RegExp(r'^\s{0,3}(?:[-+*]|\d+[.)])\s+'); + static final RegExp _listMarkerPattern = RegExp( + r'^\s{0,3}(?:[-+*]|\d+[.)])\s+', + ); static final RegExp _definitionMarkerPattern = RegExp(r'^\s{0,3}:\s?.*$'); - static final RegExp _footnoteDefinitionPattern = - RegExp(r'^\s{0,3}\[\^[^\]]+\]:'); - static final RegExp _definitionContinuationPattern = - RegExp(r'^(?: {2,}|\t).*$'); + static final RegExp _footnoteDefinitionPattern = RegExp( + r'^\s{0,3}\[\^[^\]]+\]:', + ); + static final RegExp _definitionContinuationPattern = RegExp( + r'^(?: {2,}|\t).*$', + ); static final RegExp _tableSeparatorPattern = RegExp( r'^\s*\|?(?:\s*:?-{3,}:?\s*\|)+\s*:?-{3,}:?\s*\|?\s*$', ); @@ -1101,18 +1105,16 @@ class _SegmentedBlockList extends ListBase { throw UnsupportedError('Cannot modify segmented block list contents.'); } - void _appendPrefixSegments( - List<_BlockSegment> output, - int prefixLength, - ) { + void _appendPrefixSegments(List<_BlockSegment> output, int prefixLength) { var remaining = prefixLength; var globalStart = 0; for (final segment in _segments) { if (remaining <= 0) { break; } - final segmentLength = - remaining < segment.length ? remaining : segment.length; + final segmentLength = remaining < segment.length + ? remaining + : segment.length; output.add( _BlockSegment( globalStart: globalStart, @@ -1168,13 +1170,9 @@ class _MarkdownParserTimingBuilder { class _MarkdownAstBuilder { _MarkdownAstBuilder({ Map? initialKindCounts, - Map? reusableIds, - _MarkdownParserTimingBuilder? timing, - }) : _timing = timing, - _reusableIds = reusableIds, - _kindCounters = { - ...?initialKindCounts, - }; + this._reusableIds, + this._timing, + }) : _kindCounters = {...?initialKindCounts}; final Map _kindCounters; final _MarkdownParserTimingBuilder? _timing; @@ -1249,7 +1247,8 @@ class _MarkdownAstBuilder { return QuoteBlock( id: _nextId(MarkdownBlockKind.quote, node.textContent), children: List.unmodifiable( - buildBlocks(node.children ?? const [])), + buildBlocks(node.children ?? const []), + ), ); case 'ul': return ListBlock( @@ -1289,8 +1288,10 @@ class _MarkdownAstBuilder { return _buildTable(node); case 'img': return ImageBlock( - id: _nextId(MarkdownBlockKind.image, - node.attributes['src'] ?? node.attributes['alt'] ?? ''), + id: _nextId( + MarkdownBlockKind.image, + node.attributes['src'] ?? node.attributes['alt'] ?? '', + ), url: node.attributes['src'] ?? '', alt: node.attributes['alt'], title: node.attributes['title'], @@ -1453,8 +1454,10 @@ class _MarkdownAstBuilder { _isCheckboxInput(paragraphChildren.first)) { final paragraph = md.Element('p', paragraphChildren.skip(1).toList()) ..attributes.addAll(first.attributes); - return List.unmodifiable( - [paragraph, ...nodes.skip(1)]); + return List.unmodifiable([ + paragraph, + ...nodes.skip(1), + ]); } } @@ -1479,7 +1482,8 @@ class _MarkdownAstBuilder { break; } terms.add( - List.unmodifiable(_buildInlines(termNode.children))); + List.unmodifiable(_buildInlines(termNode.children)), + ); index += 1; } @@ -1544,8 +1548,9 @@ class _MarkdownAstBuilder { for (final child in node.children ?? const []) { if (child is md.Element && child.tag == 'summary') { if (summary.isEmpty) { - summary = - List.unmodifiable(_buildInlines(child.children)); + summary = List.unmodifiable( + _buildInlines(child.children), + ); } continue; } @@ -1572,12 +1577,13 @@ class _MarkdownAstBuilder { orElse: () => node, ) : node; - final languageClass = - codeElement is md.Element ? codeElement.attributes['class'] : null; + final languageClass = codeElement is md.Element + ? codeElement.attributes['class'] + : null; final language = languageClass != null && languageClass.startsWith('language-') - ? languageClass.substring('language-'.length) - : null; + ? languageClass.substring('language-'.length) + : null; return CodeBlock( id: _nextId(MarkdownBlockKind.codeBlock, node.textContent), code: _normalizeCodeBlockText( @@ -1677,14 +1683,21 @@ class _MarkdownAstBuilder { if (alignments.length < cells.length + 1) { alignments.add(_parseAlignment(child.attributes['align'])); } - cells.add(TableCellNode( - inlines: - List.unmodifiable(_buildInlines(child.children)))); + cells.add( + TableCellNode( + inlines: List.unmodifiable( + _buildInlines(child.children), + ), + ), + ); } if (cells.isNotEmpty) { - rows.add(TableRowNode( + rows.add( + TableRowNode( cells: List.unmodifiable(cells), - isHeader: headerSection)); + isHeader: headerSection, + ), + ); } } @@ -1729,44 +1742,69 @@ class _MarkdownAstBuilder { switch (node.tag) { case 'i': case 'em': - inlines.add(EmphasisInline( - children: - List.unmodifiable(_buildInlines(node.children)))); + inlines.add( + EmphasisInline( + children: List.unmodifiable( + _buildInlines(node.children), + ), + ), + ); break; case 'b': case 'strong': - inlines.add(StrongInline( - children: - List.unmodifiable(_buildInlines(node.children)))); + inlines.add( + StrongInline( + children: List.unmodifiable( + _buildInlines(node.children), + ), + ), + ); break; case 's': case 'del': - inlines.add(StrikethroughInline( - children: - List.unmodifiable(_buildInlines(node.children)))); + inlines.add( + StrikethroughInline( + children: List.unmodifiable( + _buildInlines(node.children), + ), + ), + ); break; case 'mark': - inlines.add(HighlightInline( - children: - List.unmodifiable(_buildInlines(node.children)))); + inlines.add( + HighlightInline( + children: List.unmodifiable( + _buildInlines(node.children), + ), + ), + ); break; case 'sub': - inlines.add(SubscriptInline( - children: - List.unmodifiable(_buildInlines(node.children)))); + inlines.add( + SubscriptInline( + children: List.unmodifiable( + _buildInlines(node.children), + ), + ), + ); break; case 'sup': - inlines.add(SuperscriptInline( - children: - List.unmodifiable(_buildInlines(node.children)))); + inlines.add( + SuperscriptInline( + children: List.unmodifiable( + _buildInlines(node.children), + ), + ), + ); break; case 'a': inlines.add( LinkInline( destination: node.attributes['href'] ?? '', title: node.attributes['title'], - children: - List.unmodifiable(_buildInlines(node.children)), + children: List.unmodifiable( + _buildInlines(node.children), + ), ), ); break; @@ -1786,8 +1824,12 @@ class _MarkdownAstBuilder { inlines.add(const HardBreakInline()); break; case 'img': - inlines.add(InlineImage( - url: node.attributes['src'] ?? '', alt: node.attributes['alt'])); + inlines.add( + InlineImage( + url: node.attributes['src'] ?? '', + alt: node.attributes['alt'], + ), + ); break; default: final children = _buildInlines(node.children); @@ -2013,15 +2055,12 @@ class _MarkdownAstBuilder { } String _nextId(MarkdownBlockKind kind, String signature) { - return _measure( - (elapsed) => _timing!.nextIdMicros += elapsed, - () { - final nextCount = (_kindCounters[kind] ?? 0) + 1; - _kindCounters[kind] = nextCount; - final prefix = '${kind.name}-$nextCount'; - return _reusableIds?[prefix] ?? '$prefix-${_stableHash(signature)}'; - }, - ); + return _measure((elapsed) => _timing!.nextIdMicros += elapsed, () { + final nextCount = (_kindCounters[kind] ?? 0) + 1; + _kindCounters[kind] = nextCount; + final prefix = '${kind.name}-$nextCount'; + return _reusableIds?[prefix] ?? '$prefix-${_stableHash(signature)}'; + }); } int _stableHash(String value) { @@ -2034,10 +2073,7 @@ class _MarkdownAstBuilder { return hash; } - T _measure( - void Function(int elapsedMicros) record, - T Function() run, - ) { + T _measure(void Function(int elapsedMicros) record, T Function() run) { if (_timing == null) { return run(); } diff --git a/packages/mixin_markdown_widget/lib/src/parser/markdown_syntaxes.dart b/packages/mixin_markdown_widget/lib/src/parser/markdown_syntaxes.dart index 284e4f76..2e8d858e 100644 --- a/packages/mixin_markdown_widget/lib/src/parser/markdown_syntaxes.dart +++ b/packages/mixin_markdown_widget/lib/src/parser/markdown_syntaxes.dart @@ -4,33 +4,33 @@ import 'package:html/parser.dart' as html_parser; final List _markdownBlockSyntaxes = List.unmodifiable([ - const md.FencedCodeBlockSyntax(), - const MarkdownMathBlockSyntax(), - const MarkdownHtmlCommentBlockSyntax(), - const MarkdownHtmlBlockSyntax(), - const md.HeaderWithIdSyntax(), - const md.SetextHeaderWithIdSyntax(), - const md.TableSyntax(), - const md.UnorderedListWithCheckboxSyntax(), - const md.OrderedListWithCheckboxSyntax(), - const md.FootnoteDefSyntax(), - const MarkdownDefinitionListSyntax(), -]); + const md.FencedCodeBlockSyntax(), + const MarkdownMathBlockSyntax(), + const MarkdownHtmlCommentBlockSyntax(), + const MarkdownHtmlBlockSyntax(), + const md.HeaderWithIdSyntax(), + const md.SetextHeaderWithIdSyntax(), + const md.TableSyntax(), + const md.UnorderedListWithCheckboxSyntax(), + const md.OrderedListWithCheckboxSyntax(), + const md.FootnoteDefSyntax(), + const MarkdownDefinitionListSyntax(), + ]); final List _markdownInlineSyntaxes = List.unmodifiable([ - MarkdownDollarMathSyntax(), - MarkdownBackslashMathSyntax(), - MarkdownInlineHtmlTagSyntax(), - MarkdownHighlightSyntax(), - MarkdownSubscriptSyntax(), - MarkdownSuperscriptSyntax(), - MarkdownDoubleTildeStrikethroughSyntax(), - md.EmojiSyntax(), - md.ColorSwatchSyntax(), - md.AutolinkExtensionSyntax(), - md.InlineHtmlSyntax(), -]); + MarkdownDollarMathSyntax(), + MarkdownBackslashMathSyntax(), + MarkdownInlineHtmlTagSyntax(), + MarkdownHighlightSyntax(), + MarkdownSubscriptSyntax(), + MarkdownSuperscriptSyntax(), + MarkdownDoubleTildeStrikethroughSyntax(), + md.EmojiSyntax(), + md.ColorSwatchSyntax(), + md.AutolinkExtensionSyntax(), + md.InlineHtmlSyntax(), + ]); List buildMarkdownBlockSyntaxes() => _markdownBlockSyntaxes; @@ -139,9 +139,10 @@ class MarkdownHtmlBlockSyntax extends md.BlockSyntax { } int _openingTagCount(String line, String tag) { - return RegExp('<$tag(?:\\s+[^>]*)?>', caseSensitive: false) - .allMatches(line) - .length; + return RegExp( + '<$tag(?:\\s+[^>]*)?>', + caseSensitive: false, + ).allMatches(line).length; } int _closingTagCount(String line, String tag) { @@ -185,15 +186,14 @@ class MarkdownHtmlBlockSyntax extends md.BlockSyntax { ); } - final details = md.Element( - 'details', - [ - if (summary != null) summary, - ..._parseMarkdownFragment(bodySource), - ], - ); - if (RegExp(r'(^|\s)open(?:\s|=|$)', caseSensitive: false) - .hasMatch(attributesSource)) { + final details = md.Element('details', [ + ?summary, + ..._parseMarkdownFragment(bodySource), + ]); + if (RegExp( + r'(^|\s)open(?:\s|=|$)', + caseSensitive: false, + ).hasMatch(attributesSource)) { details.attributes['open'] = ''; } return [details]; @@ -294,8 +294,9 @@ class MarkdownHtmlBlockSyntax extends md.BlockSyntax { String tag, List children, ) { - final element = - children.isEmpty ? md.Element.empty(tag) : md.Element(tag, children); + final element = children.isEmpty + ? md.Element.empty(tag) + : md.Element(tag, children); for (final entry in node.attributes.entries) { element.attributes[entry.key.toString().toLowerCase()] = entry.value; } @@ -392,14 +393,17 @@ class MarkdownDefinitionListSyntax extends md.BlockSyntax { } children.add( - md.Element('dt', [md.UnparsedContent(termLine.trim())])); + md.Element('dt', [md.UnparsedContent(termLine.trim())]), + ); parser.advance(); while (!parser.isDone && _definitionPattern.hasMatch(parser.current.content)) { final lines = _parseDefinitionLines(parser); - final definitionChildren = md.BlockParser(lines, parser.document) - .parseLines(parentSyntax: this, disabledSetextHeading: true); + final definitionChildren = md.BlockParser( + lines, + parser.document, + ).parseLines(parentSyntax: this, disabledSetextHeading: true); children.add(md.Element('dd', definitionChildren)); } @@ -598,7 +602,8 @@ class MarkdownDollarMathSyntax extends md.InlineSyntax { return false; } - final isDisplay = startMatchPos + 1 < parser.source.length && + final isDisplay = + startMatchPos + 1 < parser.source.length && parser.source.codeUnitAt(startMatchPos + 1) == 0x24; final delimiter = isDisplay ? r'$$' : r'$'; final match = _extractDelimitedMath( @@ -676,13 +681,13 @@ class MarkdownBackslashMathSyntax extends md.InlineSyntax { class MarkdownDoubleTildeStrikethroughSyntax extends md.DelimiterSyntax { MarkdownDoubleTildeStrikethroughSyntax() - : super( - '~+', - requiresDelimiterRun: true, - allowIntraWord: true, - startCharacter: 0x7E, - tags: [md.DelimiterTag('del', 2)], - ); + : super( + '~+', + requiresDelimiterRun: true, + allowIntraWord: true, + startCharacter: 0x7E, + tags: [md.DelimiterTag('del', 2)], + ); } class MarkdownHighlightSyntax extends _DelimitedInlineSyntax { @@ -723,10 +728,8 @@ bool _hasBoundaryWhitespace(String content) { } abstract class _DelimitedInlineSyntax extends md.InlineSyntax { - _DelimitedInlineSyntax({ - required this.delimiter, - required this.tag, - }) : super(RegExp.escape(delimiter), startCharacter: delimiter.codeUnitAt(0)); + _DelimitedInlineSyntax({required this.delimiter, required this.tag}) + : super(RegExp.escape(delimiter), startCharacter: delimiter.codeUnitAt(0)); final String delimiter; final String tag; diff --git a/packages/mixin_markdown_widget/lib/src/render/builder/markdown_block_builder.dart b/packages/mixin_markdown_widget/lib/src/render/builder/markdown_block_builder.dart index 8441b02e..d7e8dab8 100644 --- a/packages/mixin_markdown_widget/lib/src/render/builder/markdown_block_builder.dart +++ b/packages/mixin_markdown_widget/lib/src/render/builder/markdown_block_builder.dart @@ -1,5 +1,6 @@ -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:flutter/services.dart'; + import 'dart:math' as math; import '../../clipboard/plain_text_serializer.dart'; @@ -38,10 +39,10 @@ class MarkdownBlockBuilder { this.codeBlockBuilder, this.bulletBuilder, this.onTapLink, - required Map cachedBlockRows, + required this._cachedBlockRows, required this.tableLayoutPlanCache, required this.codeHighlightCache, - }) : _cachedBlockRows = cachedBlockRows; + }); final MarkdownThemeData theme; final MarkdownSelectionController? selectionController; @@ -90,9 +91,7 @@ class MarkdownBlockBuilder { } final widget = Padding( - padding: EdgeInsets.only( - bottom: _spacingAfterTopLevelBlock(blockIndex), - ), + padding: EdgeInsets.only(bottom: _spacingAfterTopLevelBlock(blockIndex)), child: Align( alignment: AlignmentDirectional.topStart, child: ConstrainedBox( @@ -305,8 +304,8 @@ class MarkdownBlockBuilder { block.id, () => GlobalKey(debugLabel: block.id), ); - final captionDescriptor = - descriptorExtractor.buildImageCaptionDescriptor(block); + final captionDescriptor = descriptorExtractor + .buildImageCaptionDescriptor(block); final imageState = _buildImageVisual(context, block, hasCaption: true); return MarkdownImageBlockView( theme: theme, @@ -368,13 +367,15 @@ class MarkdownBlockBuilder { case MarkdownBlockKind.heading: final heading = block as HeadingBlock; final style = theme.headingStyleForLevel(heading.level); - final plainText = - MarkdownInlineBuilder.flattenInlineText(heading.inlines); + final plainText = MarkdownInlineBuilder.flattenInlineText( + heading.inlines, + ); final geometry = MarkdownTextLeafGeometry( runs: inlineBuilder.buildPretextRuns(style, heading.inlines), fallbackStyle: style, - textAlign: - MarkdownInlineBuilder.resolvedInlineTextAlign(heading.inlines), + textAlign: MarkdownInlineBuilder.resolvedInlineTextAlign( + heading.inlines, + ), ); return _buildTextLeafSpec( child: _wrapHeadingBlock( @@ -386,8 +387,9 @@ class MarkdownBlockBuilder { ); case MarkdownBlockKind.paragraph: final paragraph = block as ParagraphBlock; - final plainText = - MarkdownInlineBuilder.flattenInlineText(paragraph.inlines); + final plainText = MarkdownInlineBuilder.flattenInlineText( + paragraph.inlines, + ); if (_isStandaloneLineBreakBlock(paragraph)) { return SelectableBlockSpec( child: _buildStandaloneLineBreak(context), @@ -401,8 +403,9 @@ class MarkdownBlockBuilder { paragraph.inlines, ), fallbackStyle: theme.bodyStyle, - textAlign: - MarkdownInlineBuilder.resolvedInlineTextAlign(paragraph.inlines), + textAlign: MarkdownInlineBuilder.resolvedInlineTextAlign( + paragraph.inlines, + ), ); return _buildTextLeafSpec( child: _textLeaf(_topLevelLeafPath, geometry), @@ -411,30 +414,27 @@ class MarkdownBlockBuilder { ); case MarkdownBlockKind.quote: final quoteBlock = block as QuoteBlock; - final descriptor = - descriptorExtractor.buildSelectableDescriptorForBlock(quoteBlock); + final descriptor = descriptorExtractor + .buildSelectableDescriptorForBlock(quoteBlock); return _buildContainerSpec( block: quoteBlock, child: MarkdownQuoteBlockView( theme: theme, - child: _buildQuoteContent( - context, - quoteBlock, - path: const [], - ), + child: _buildQuoteContent(context, quoteBlock, path: const []), ), textSpan: descriptor.span, highlightBorderRadius: theme.quoteBorderRadius, selectionPaintOrder: SelectableBlockSelectionPaintOrder.aboveChild, selectionColor: _quoteSelectionColor, - repaintListenable: - _nestedScrollableRepaintListenableForBlocks(quoteBlock.children), + repaintListenable: _nestedScrollableRepaintListenableForBlocks( + quoteBlock.children, + ), ); case MarkdownBlockKind.orderedList: case MarkdownBlockKind.unorderedList: final listBlock = block as ListBlock; - final descriptor = - descriptorExtractor.buildSelectableDescriptorForBlock(listBlock); + final descriptor = descriptorExtractor + .buildSelectableDescriptorForBlock(listBlock); return _buildContainerSpec( block: listBlock, child: MarkdownListBlockView( @@ -451,8 +451,9 @@ class MarkdownBlockBuilder { bulletBuilder: bulletBuilder, ), textSpan: descriptor.span, - repaintListenable: - _nestedScrollableRepaintListenableForListBlock(listBlock), + repaintListenable: _nestedScrollableRepaintListenableForListBlock( + listBlock, + ), selectionPaintOrder: SelectableBlockSelectionPaintOrder.aboveChild, ); case MarkdownBlockKind.definitionList: @@ -490,21 +491,19 @@ class MarkdownBlockBuilder { ), ), textSpan: footnoteDescriptor.span, - repaintListenable: - _nestedScrollableRepaintListenableForListBlock(orderedFootnotes), + repaintListenable: _nestedScrollableRepaintListenableForListBlock( + orderedFootnotes, + ), highlightBorderRadius: BorderRadius.circular(8), ); case MarkdownBlockKind.details: final detailsBlock = block as DetailsBlock; - final descriptor = - descriptorExtractor.buildDetailsSelectableDescriptor(detailsBlock); + final descriptor = descriptorExtractor.buildDetailsSelectableDescriptor( + detailsBlock, + ); return _buildContainerSpec( block: detailsBlock, - child: _buildDetails( - context, - detailsBlock, - path: const [], - ), + child: _buildDetails(context, detailsBlock, path: const []), textSpan: descriptor.span, highlightBorderRadius: theme.quoteBorderRadius, selectionPaintOrder: SelectableBlockSelectionPaintOrder.aboveChild, @@ -591,8 +590,8 @@ class MarkdownBlockBuilder { ); case MarkdownBlockKind.table: final tableBlock = block as TableBlock; - final descriptor = - descriptorExtractor.buildSelectableDescriptorForBlock(tableBlock); + final descriptor = descriptorExtractor + .buildSelectableDescriptorForBlock(tableBlock); final scrollController = keysRegistry.tableScrollControllers .putIfAbsent(tableBlock.id, ScrollController.new); final viewportKey = keysRegistry.tableViewportKeys.putIfAbsent( @@ -622,11 +621,7 @@ class MarkdownBlockBuilder { ); case MarkdownBlockKind.thematicBreak: return SelectableBlockSpec( - child: Divider( - color: theme.dividerColor, - height: 1, - thickness: 1, - ), + child: Divider(color: theme.dividerColor, height: 1, thickness: 1), plainText: plainTextSerializer.serializeBlockText(block), hitTestBehavior: SelectableBlockHitTestBehavior.block, ); @@ -664,17 +659,17 @@ class MarkdownBlockBuilder { MarkdownContainerSelectionGeometry.rects(context, block, range), textOffsetResolver: (context, _, localPosition) => MarkdownContainerSelectionGeometry.offsetAt( - context, - block, - localPosition, - ), + context, + block, + localPosition, + ), selectionUnitRangeResolver: (context, _, localPosition, position) => MarkdownContainerSelectionGeometry.unitAt( - context, - block, - localPosition, - position, - ), + context, + block, + localPosition, + position, + ), ); } @@ -730,7 +725,8 @@ class MarkdownBlockBuilder { selectionStructure: selectionStructure, measurementPadding: measurementPadding, highlightBorderRadius: highlightBorderRadius, - selectionPaintOrder: selectionPaintOrder ?? + selectionPaintOrder: + selectionPaintOrder ?? (hasInlineSurface ? SelectableBlockSelectionPaintOrder.aboveChild : SelectableBlockSelectionPaintOrder.behindChild), @@ -804,8 +800,8 @@ class MarkdownBlockBuilder { final styleFontSize = style.fontSize; final scaledFontSize = bodyFontSize == null || contextFontSize == null || styleFontSize == null - ? styleFontSize ?? contextFontSize - : styleFontSize * contextFontSize / bodyFontSize; + ? styleFontSize ?? contextFontSize + : styleFontSize * contextFontSize / bodyFontSize; return style.copyWith( color: textStyleContext.color, fontSize: scaledFontSize, @@ -918,11 +914,7 @@ class MarkdownBlockBuilder { case MarkdownBlockKind.image: return _buildImage(context, block as ImageBlock); case MarkdownBlockKind.thematicBreak: - return Divider( - color: theme.dividerColor, - height: 1, - thickness: 1, - ); + return Divider(color: theme.dividerColor, height: 1, thickness: 1); } } @@ -933,11 +925,7 @@ class MarkdownBlockBuilder { }) { return MarkdownQuoteBlockView( theme: theme, - child: _buildQuoteContent( - context, - block, - path: path, - ), + child: _buildQuoteContent(context, block, path: path), ); } @@ -1046,10 +1034,10 @@ class MarkdownBlockBuilder { /// selection geometry reads. MarkdownListItemSlotBuilder _listItemSlotBuilder(List path) { return (index, role, child) => MarkdownSelectionSlot( - path: PathInBlock([...path, index]), - role: role, - child: child, - ); + path: PathInBlock([...path, index]), + role: role, + child: child, + ); } Widget _buildDefinitionList( @@ -1057,8 +1045,8 @@ class MarkdownBlockBuilder { DefinitionListBlock block, { required List path, }) { - final descriptor = - descriptorExtractor.buildDefinitionListSelectableDescriptor(block); + final descriptor = descriptorExtractor + .buildDefinitionListSelectableDescriptor(block); return _buildDescriptorTextWidget(descriptor, leafPath: _leafPath(path)); } @@ -1105,8 +1093,10 @@ class MarkdownBlockBuilder { child: _buildTextBlock( style: summaryStyle, inlines: block.summary, - inlineCodeStyle: - _styleForContext(theme.inlineCodeStyle, paragraphStyle), + inlineCodeStyle: _styleForContext( + theme.inlineCodeStyle, + paragraphStyle, + ), linkStyle: _linkStyleForContext(paragraphStyle), path: summaryPath, ), @@ -1185,10 +1175,7 @@ class MarkdownBlockBuilder { ); } - Widget _buildStandaloneLineBreak( - BuildContext context, { - TextStyle? style, - }) { + Widget _buildStandaloneLineBreak(BuildContext context, {TextStyle? style}) { final textScaler = MediaQuery.maybeTextScalerOf(context) ?? TextScaler.noScaling; return SizedBox( @@ -1209,16 +1196,14 @@ class MarkdownBlockBuilder { return textPainter.preferredLineHeight; } - Widget _wrapHeadingBlock({ - required int level, - required Widget child, - }) { + Widget _wrapHeadingBlock({required int level, required Widget child}) { if (level > 2) { return child; } - final showDivider = - level == 1 ? theme.showHeading1Divider : theme.showHeading2Divider; + final showDivider = level == 1 + ? theme.showHeading1Divider + : theme.showHeading2Divider; if (!showDivider) { return child; } @@ -1230,11 +1215,7 @@ class MarkdownBlockBuilder { children: [ child, SizedBox(height: dividerSpacing), - Divider( - color: theme.dividerColor, - height: 1, - thickness: 1, - ), + Divider(color: theme.dividerColor, height: 1, thickness: 1), ], ); } @@ -1287,9 +1268,12 @@ class MarkdownBlockBuilder { theme: theme, codeSpan: codeSpan, viewportKey: viewportKey, - scrollController: scrollController ?? - keysRegistry.codeBlockScrollControllers - .putIfAbsent(block.id, ScrollController.new), + scrollController: + scrollController ?? + keysRegistry.codeBlockScrollControllers.putIfAbsent( + block.id, + ScrollController.new, + ), onCopyCode: () { Clipboard.setData(ClipboardData(text: block.code)); }, @@ -1311,14 +1295,18 @@ class MarkdownBlockBuilder { switch (block.kind) { case MarkdownBlockKind.codeBlock: listenables.add( - keysRegistry.codeBlockScrollControllers - .putIfAbsent(block.id, ScrollController.new), + keysRegistry.codeBlockScrollControllers.putIfAbsent( + block.id, + ScrollController.new, + ), ); return; case MarkdownBlockKind.table: listenables.add( - keysRegistry.tableScrollControllers - .putIfAbsent(block.id, ScrollController.new), + keysRegistry.tableScrollControllers.putIfAbsent( + block.id, + ScrollController.new, + ), ); return; case MarkdownBlockKind.quote: @@ -1378,7 +1366,8 @@ class MarkdownBlockBuilder { GlobalKey? viewportKey, List path = const [], }) { - final resolvedViewportKey = viewportKey ?? + final resolvedViewportKey = + viewportKey ?? keysRegistry.tableViewportKeys.putIfAbsent( block.id, () => GlobalKey(debugLabel: 'table-viewport-${block.id}'), @@ -1387,41 +1376,42 @@ class MarkdownBlockBuilder { theme: theme, block: block, layoutPlan: tableLayoutPlanCache.planFor(block), - textWidgetBuilder: ( - context, - style, - inlines, - textAlign, { - required int rowIndex, - required int columnIndex, - bool alignInlineMathToBaseline = true, - TextStyle? inlineCodeStyle, - TextStyle? linkStyle, - }) { - final runs = inlineBuilder.buildPretextRuns( - style, - inlines, - alignInlineMathToBaseline: alignInlineMathToBaseline, - inlineCodeStyle: inlineCodeStyle, - linkStyle: linkStyle, - ); - return MarkdownSelectableLeaf( - path: PathInBlock([...path, rowIndex, columnIndex]), - geometry: MarkdownTextLeafGeometry( - runs: runs, - fallbackStyle: style, - textAlign: textAlign, - direct: true, - viewportKey: resolvedViewportKey, - ), - child: MarkdownPretextTextBlock.rich( - runs: runs, - fallbackStyle: style, - textAlign: textAlign, - intrinsicWidthSafe: true, - ), - ); - }, + textWidgetBuilder: + ( + context, + style, + inlines, + textAlign, { + required int rowIndex, + required int columnIndex, + bool alignInlineMathToBaseline = true, + TextStyle? inlineCodeStyle, + TextStyle? linkStyle, + }) { + final runs = inlineBuilder.buildPretextRuns( + style, + inlines, + alignInlineMathToBaseline: alignInlineMathToBaseline, + inlineCodeStyle: inlineCodeStyle, + linkStyle: linkStyle, + ); + return MarkdownSelectableLeaf( + path: PathInBlock([...path, rowIndex, columnIndex]), + geometry: MarkdownTextLeafGeometry( + runs: runs, + fallbackStyle: style, + textAlign: textAlign, + direct: true, + viewportKey: resolvedViewportKey, + ), + child: MarkdownPretextTextBlock.rich( + runs: runs, + fallbackStyle: style, + textAlign: textAlign, + intrinsicWidthSafe: true, + ), + ); + }, bodyStyle: _styleForContext(theme.bodyStyle, textStyleContext), tableHeaderStyle: _styleForContext( theme.tableHeaderStyle, @@ -1433,24 +1423,28 @@ class MarkdownBlockBuilder { ), linkStyle: _linkStyleForContext(textStyleContext), viewportKey: resolvedViewportKey, - scrollController: scrollController ?? - keysRegistry.tableScrollControllers - .putIfAbsent(block.id, ScrollController.new), + scrollController: + scrollController ?? + keysRegistry.tableScrollControllers.putIfAbsent( + block.id, + ScrollController.new, + ), ); } Widget _buildImage(BuildContext context, ImageBlock block) { if (imageBuilder != null) { - return _wrapLinkedImage( - block, - imageBuilder!(context, block, theme), - ); + return _wrapLinkedImage(block, imageBuilder!(context, block, theme)); } final caption = MarkdownDescriptorExtractor.imageCaptionText(block); - final captionDescriptor = - descriptorExtractor.buildImageCaptionDescriptor(block); - final imageState = - _buildImageVisual(context, block, hasCaption: caption.isNotEmpty); + final captionDescriptor = descriptorExtractor.buildImageCaptionDescriptor( + block, + ); + final imageState = _buildImageVisual( + context, + block, + hasCaption: caption.isNotEmpty, + ); return MarkdownImageBlockView( theme: theme, image: imageState.image, @@ -1463,8 +1457,11 @@ class MarkdownBlockBuilder { } ({Widget image, ValueNotifier hasErrorNotifier, bool hasKnownSize}) - _buildImageVisual(BuildContext context, ImageBlock block, - {bool hasCaption = false}) { + _buildImageVisual( + BuildContext context, + ImageBlock block, { + bool hasCaption = false, + }) { final uri = Uri.tryParse(block.url); double? w; double? h; @@ -1475,8 +1472,10 @@ class MarkdownBlockBuilder { if (hStr != null) h = double.tryParse(hStr); } final hasKnownSize = w != null && h != null; - final hasErrorNotifier = keysRegistry.imageErrorNotifiers - .putIfAbsent(block.id, () => ValueNotifier(false)); + final hasErrorNotifier = keysRegistry.imageErrorNotifiers.putIfAbsent( + block.id, + () => ValueNotifier(false), + ); if (imageBuilder != null) { return ( @@ -1520,15 +1519,18 @@ class MarkdownBlockBuilder { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.broken_image_outlined, - size: 18, - color: theme.bodyStyle.color?.withValues(alpha: 0.72)), + Icon( + Icons.broken_image_outlined, + size: 18, + color: theme.bodyStyle.color?.withValues(alpha: 0.72), + ), const SizedBox(width: 8), Flexible( child: Text( text, style: theme.bodyStyle.copyWith( - color: theme.bodyStyle.color?.withValues(alpha: 0.72)), + color: theme.bodyStyle.color?.withValues(alpha: 0.72), + ), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -1548,27 +1550,24 @@ class MarkdownBlockBuilder { errorBuilder: errorBuilder, ) : localImageProvider != null - ? Image( - image: localImageProvider, - width: w, - height: h, - fit: BoxFit.cover, - errorBuilder: errorBuilder, - ) - : Image.asset( - block.url, - width: w, - height: h, - fit: BoxFit.cover, - errorBuilder: errorBuilder, - ); + ? Image( + image: localImageProvider, + width: w, + height: h, + fit: BoxFit.cover, + errorBuilder: errorBuilder, + ) + : Image.asset( + block.url, + width: w, + height: h, + fit: BoxFit.cover, + errorBuilder: errorBuilder, + ); return ( image: _wrapLinkedImage( block, - ClipRRect( - borderRadius: theme.imageBorderRadius, - child: image, - ), + ClipRRect(borderRadius: theme.imageBorderRadius, child: image), ), hasErrorNotifier: hasErrorNotifier, hasKnownSize: hasKnownSize, @@ -1596,7 +1595,9 @@ class MarkdownBlockBuilder { } SelectableBlockSpec _buildCustomImageBuilderSpec( - BuildContext context, ImageBlock block) { + BuildContext context, + ImageBlock block, + ) { final caption = MarkdownDescriptorExtractor.imageCaptionText(block); final blockPlainText = caption.isEmpty ? block.url : caption; final imageState = _buildImageVisual(context, block); @@ -1630,10 +1631,7 @@ class MarkdownBlockBuilder { }) { final pretext = descriptor.pretext; if (pretext == null) { - return Text.rich( - descriptor.span, - style: pretext?.fallbackStyle, - ); + return Text.rich(descriptor.span, style: pretext?.fallbackStyle); } if (leafPath != null) { return _textLeaf( @@ -1650,8 +1648,9 @@ class MarkdownBlockBuilder { fallbackStyle: pretext.fallbackStyle, textAlign: pretext.textAlign, intrinsicWidthSafe: intrinsicWidthSafe, - preferDirectRichText: - markdownPretextCanUseDirectRichTextGeometry(pretext.runs), + preferDirectRichText: markdownPretextCanUseDirectRichTextGeometry( + pretext.runs, + ), ); } @@ -1701,19 +1700,12 @@ class MarkdownBlockBuilder { return structure; } - Widget _buildFootnoteListContainer({ - required Widget child, - }) { + Widget _buildFootnoteListContainer({required Widget child}) { return DecoratedBox( decoration: BoxDecoration( - border: Border( - top: BorderSide(color: theme.dividerColor), - ), - ), - child: Padding( - padding: const EdgeInsets.only(top: 12), - child: child, + border: Border(top: BorderSide(color: theme.dividerColor)), ), + child: Padding(padding: const EdgeInsets.only(top: 12), child: child), ); } diff --git a/packages/mixin_markdown_widget/lib/src/render/builder/markdown_inline_builder.dart b/packages/mixin_markdown_widget/lib/src/render/builder/markdown_inline_builder.dart index 6ee96f1e..985274a0 100644 --- a/packages/mixin_markdown_widget/lib/src/render/builder/markdown_inline_builder.dart +++ b/packages/mixin_markdown_widget/lib/src/render/builder/markdown_inline_builder.dart @@ -1,5 +1,5 @@ import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_math_fork/flutter_math.dart'; import '../../core/document.dart'; @@ -71,11 +71,11 @@ class MarkdownLinkRecognizers { links.add(inline); visitInlines(children); case EmphasisInline(:final children) || - StrongInline(:final children) || - StrikethroughInline(:final children) || - HighlightInline(:final children) || - SubscriptInline(:final children) || - SuperscriptInline(:final children): + StrongInline(:final children) || + StrikethroughInline(:final children) || + HighlightInline(:final children) || + SubscriptInline(:final children) || + SuperscriptInline(:final children): visitInlines(children); default: } @@ -128,10 +128,12 @@ class MarkdownInlineBuilder { final MarkdownTapLinkCallback? onTapLink; List buildPretextRuns( - TextStyle baseStyle, List inlines, - {bool alignInlineMathToBaseline = true, - TextStyle? inlineCodeStyle, - TextStyle? linkStyle}) { + TextStyle baseStyle, + List inlines, { + bool alignInlineMathToBaseline = true, + TextStyle? inlineCodeStyle, + TextStyle? linkStyle, + }) { return [ for (final inline in inlines) ..._buildPretextRun( @@ -145,10 +147,12 @@ class MarkdownInlineBuilder { } List _buildPretextRun( - TextStyle baseStyle, InlineNode inline, - {required bool alignInlineMathToBaseline, - TextStyle? inlineCodeStyle, - TextStyle? linkStyle}) { + TextStyle baseStyle, + InlineNode inline, { + required bool alignInlineMathToBaseline, + TextStyle? inlineCodeStyle, + TextStyle? linkStyle, + }) { switch (inline.kind) { case MarkdownInlineKind.text: return [ @@ -224,12 +228,12 @@ class MarkdownInlineBuilder { }); final style = _linkTextStyle(baseStyle, linkStyle); return buildPretextRuns( - style, - link.children, - alignInlineMathToBaseline: alignInlineMathToBaseline, - inlineCodeStyle: inlineCodeStyle, - linkStyle: linkStyle, - ) + style, + link.children, + alignInlineMathToBaseline: alignInlineMathToBaseline, + inlineCodeStyle: inlineCodeStyle, + linkStyle: linkStyle, + ) .map( (run) => MarkdownPretextInlineRun( text: run.text, @@ -278,10 +282,7 @@ class MarkdownInlineBuilder { case MarkdownInlineKind.softBreak: case MarkdownInlineKind.hardBreak: return [ - MarkdownPretextInlineRun( - text: '\n', - style: baseStyle, - ), + MarkdownPretextInlineRun(text: '\n', style: baseStyle), ]; case MarkdownInlineKind.image: final image = inline as InlineImage; @@ -298,8 +299,11 @@ class MarkdownInlineBuilder { } List buildInlineSpans( - TextStyle baseStyle, List inlines, - {TextStyle? inlineCodeStyle, TextStyle? linkStyle}) { + TextStyle baseStyle, + List inlines, { + TextStyle? inlineCodeStyle, + TextStyle? linkStyle, + }) { return [ for (final inline in inlines) ..._buildInlineSpan( @@ -461,10 +465,7 @@ class MarkdownInlineBuilder { ? image.alt!.trim() : image.url; return [ - TextSpan( - text: label, - style: _linkTextStyle(baseStyle, linkStyle), - ), + TextSpan(text: label, style: _linkTextStyle(baseStyle, linkStyle)), ]; } } @@ -472,7 +473,9 @@ class MarkdownInlineBuilder { TextStyle _linkTextStyle(TextStyle baseStyle, TextStyle? overrideStyle) { final style = overrideStyle ?? theme.linkStyle; final linkColor = style.color ?? baseStyle.color; - return baseStyle.merge(style).copyWith( + return baseStyle + .merge(style) + .copyWith( color: linkColor, decoration: style.decoration ?? baseStyle.decoration, decorationColor: @@ -488,7 +491,9 @@ class MarkdownInlineBuilder { TextStyle? overrideStyle, ) { final style = overrideStyle ?? theme.inlineCodeStyle; - return baseStyle.merge(style).copyWith( + return baseStyle + .merge(style) + .copyWith( color: baseStyle.color ?? style.color, fontSize: _scaledInlineFontSize(style, baseStyle), fontStyle: baseStyle.fontStyle ?? style.fontStyle, @@ -511,9 +516,7 @@ class MarkdownInlineBuilder { TextStyle highlightStyle(TextStyle baseStyle) { final accent = theme.linkStyle.color ?? theme.dividerColor; - return baseStyle.copyWith( - backgroundColor: accent.withValues(alpha: 0.18), - ); + return baseStyle.copyWith(backgroundColor: accent.withValues(alpha: 0.18)); } TextStyle subscriptStyle(TextStyle baseStyle) { @@ -554,10 +557,8 @@ class MarkdownInlineBuilder { math.tex, mathStyle: math.displayStyle ? MathStyle.display : MathStyle.text, textStyle: baseStyle, - onErrorFallback: (error) => Text( - math.tex, - style: _inlineCodeTextStyle(baseStyle, null), - ), + onErrorFallback: (error) => + Text(math.tex, style: _inlineCodeTextStyle(baseStyle, null)), ), ); final useBaselineAlignment = !math.displayStyle && alignToBaseline; diff --git a/packages/mixin_markdown_widget/lib/src/render/code_syntax_highlighter.dart b/packages/mixin_markdown_widget/lib/src/render/code_syntax_highlighter.dart index 6f1df5e7..59afcfa8 100644 --- a/packages/mixin_markdown_widget/lib/src/render/code_syntax_highlighter.dart +++ b/packages/mixin_markdown_widget/lib/src/render/code_syntax_highlighter.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:re_highlight/languages/all.dart'; import 'package:re_highlight/re_highlight.dart'; @@ -14,11 +14,17 @@ class MarkdownCodeSyntaxHighlighter { static const int _autoDetectMaxChars = 4000; static const int _maxSegmentCacheEntries = 128; - static final Map<_MarkdownHighlightRequestKey, - List<_MarkdownHighlightSegment>> _segmentCache = + static final Map< + _MarkdownHighlightRequestKey, + List<_MarkdownHighlightSegment> + > + _segmentCache = <_MarkdownHighlightRequestKey, List<_MarkdownHighlightSegment>>{}; - static final Map<_MarkdownHighlightRequestKey, - Future>> _pendingSegmentRequests = + static final Map< + _MarkdownHighlightRequestKey, + Future> + > + _pendingSegmentRequests = <_MarkdownHighlightRequestKey, Future>>{}; MarkdownCodeHighlightPresentation buildPlainTextPresentation({ @@ -87,10 +93,11 @@ class MarkdownCodeSyntaxHighlighter { shouldDegradeHighlight(source: source, theme: theme)) { return null; } - final segments = _segmentCache[_MarkdownHighlightRequestKey( - source: source, - language: _normalizeLanguage(language), - )]; + final segments = + _segmentCache[_MarkdownHighlightRequestKey( + source: source, + language: _normalizeLanguage(language), + )]; if (segments == null) { return null; } @@ -109,8 +116,10 @@ class MarkdownCodeSyntaxHighlighter { required MarkdownThemeData theme, String? language, }) { - return buildPlainTextPresentation(source: source, baseStyle: baseStyle) - .span; + return buildPlainTextPresentation( + source: source, + baseStyle: baseStyle, + ).span; } MarkdownCodeHighlightPresentation buildPresentation({ @@ -128,8 +137,10 @@ class MarkdownCodeSyntaxHighlighter { required MarkdownThemeData theme, String? language, }) { - return buildPlainTextPresentation(source: source, baseStyle: baseStyle) - .runs; + return buildPlainTextPresentation( + source: source, + baseStyle: baseStyle, + ).runs; } int lineCountOf(String source) { @@ -156,24 +167,24 @@ class MarkdownCodeSyntaxHighlighter { return pending; } - final request = compute, List>>( - _highlightSegmentsInBackground, - { - 'source': source, - 'language': language, - }, - ).then((response) { - final segments = response - .map(_MarkdownHighlightSegment.fromMessage) - .toList(growable: false); - if (_segmentCache.length >= _maxSegmentCacheEntries) { - _segmentCache.remove(_segmentCache.keys.first); - } - _segmentCache[cacheKey] = segments; - return segments; - }).whenComplete(() { - _pendingSegmentRequests.remove(cacheKey); - }); + final request = + compute, List>>( + _highlightSegmentsInBackground, + {'source': source, 'language': language}, + ) + .then((response) { + final segments = response + .map(_MarkdownHighlightSegment.fromMessage) + .toList(growable: false); + if (_segmentCache.length >= _maxSegmentCacheEntries) { + _segmentCache.remove(_segmentCache.keys.first); + } + _segmentCache[cacheKey] = segments; + return segments; + }) + .whenComplete(() { + _pendingSegmentRequests.remove(cacheKey); + }); _pendingSegmentRequests[cacheKey] = request; return request; } @@ -206,15 +217,17 @@ class MarkdownCodeSyntaxHighlighter { } var effectiveStyle = baseStyle; for (final scope in segment.scopes) { - effectiveStyle = effectiveStyle - .merge(_styleFor(scope, baseStyle: effectiveStyle, theme: theme)); + effectiveStyle = effectiveStyle.merge( + _styleFor(scope, baseStyle: effectiveStyle, theme: theme), + ); } if (runs.isNotEmpty && runs.last.style == effectiveStyle) { final last = runs.removeLast(); runs.add(last.copyWithText(last.text + segment.text)); } else { - runs.add(MarkdownPretextInlineRun( - text: segment.text, style: effectiveStyle)); + runs.add( + MarkdownPretextInlineRun(text: segment.text, style: effectiveStyle), + ); } if (children.isNotEmpty) { @@ -254,8 +267,9 @@ class MarkdownCodeSyntaxHighlighter { final classes = className.split(RegExp(r'\s+')); var style = baseStyle; for (final token in classes) { - style = - style.merge(_tokenStyle(token, baseStyle: baseStyle, theme: theme)); + style = style.merge( + _tokenStyle(token, baseStyle: baseStyle, theme: theme), + ); } return style; } @@ -342,9 +356,7 @@ class MarkdownCodeHighlightPresentation { } class MarkdownCodeHighlightCache extends ChangeNotifier { - MarkdownCodeHighlightCache({ - required MarkdownCodeSyntaxHighlighter highlighter, - }) : _highlighter = highlighter; + MarkdownCodeHighlightCache({required this._highlighter}); final MarkdownCodeSyntaxHighlighter _highlighter; final Map _entries = @@ -361,8 +373,11 @@ class MarkdownCodeHighlightCache extends ChangeNotifier { String? language, }) { final contentSignature = Object.hash(source, language); - final presentationSignature = - Object.hash(contentSignature, baseStyle, theme); + final presentationSignature = Object.hash( + contentSignature, + baseStyle, + theme, + ); final existing = _entries[blockId]; if (existing != null && existing.presentationSignature == presentationSignature) { @@ -424,16 +439,17 @@ class MarkdownCodeHighlightCache extends ChangeNotifier { // While a streamed block grows, show its last highlight with the new tail // in plain text instead of flashing the whole block back to plain. final highlightedPrefix = existing?.highlightedPrefix; - final placeholderPresentation = existing != null && + final placeholderPresentation = + existing != null && existing.contentSignature == contentSignature && existing.presentation.isHighlighted ? existing.presentation : highlightedPrefix?.extendTo( - source, - styleSignature: request.styleSignature, - baseStyle: baseStyle, - ) ?? - plainPresentation; + source, + styleSignature: request.styleSignature, + baseStyle: baseStyle, + ) ?? + plainPresentation; _entries[blockId] = _MarkdownCodeHighlightCacheEntry( request: request, contentSignature: contentSignature, @@ -661,10 +677,7 @@ class _MarkdownHighlightRequestKey { @immutable class _MarkdownHighlightSegment { - const _MarkdownHighlightSegment({ - required this.text, - required this.scopes, - }); + const _MarkdownHighlightSegment({required this.text, required this.scopes}); factory _MarkdownHighlightSegment.fromMessage(Map message) { final scopes = (message['scopes'] as List) @@ -680,9 +693,9 @@ class _MarkdownHighlightSegment { final List scopes; Map toMessage() => { - 'text': text, - 'scopes': scopes, - }; + 'text': text, + 'scopes': scopes, + }; } class _MarkdownHighlightSegmentRenderer implements HighlightRenderer { @@ -791,9 +804,11 @@ void cacheCodeHighlights(List> highlights) { source: highlight['source']! as String, language: highlight['language'] as String?, )] = (highlight['segments']! as List) - .map((segment) => _MarkdownHighlightSegment.fromMessage( - segment! as Map, - )) + .map( + (segment) => _MarkdownHighlightSegment.fromMessage( + segment! as Map, + ), + ) .toList(growable: false); } } diff --git a/packages/mixin_markdown_widget/lib/src/render/image_caption_layout.dart b/packages/mixin_markdown_widget/lib/src/render/image_caption_layout.dart index db41346b..8552aa78 100644 --- a/packages/mixin_markdown_widget/lib/src/render/image_caption_layout.dart +++ b/packages/mixin_markdown_widget/lib/src/render/image_caption_layout.dart @@ -18,7 +18,9 @@ class ImageCaptionLayout extends MultiChildRenderObjectWidget { @override void updateRenderObject( - BuildContext context, covariant RenderImageCaptionLayout renderObject) { + BuildContext context, + covariant RenderImageCaptionLayout renderObject, + ) { renderObject.spacing = spacing; } } @@ -29,7 +31,7 @@ class RenderImageCaptionLayout extends RenderBox with ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin { - RenderImageCaptionLayout({required double spacing}) : _spacing = spacing; + RenderImageCaptionLayout({required this._spacing}); double _spacing; double get spacing => _spacing; @@ -69,13 +71,16 @@ class RenderImageCaptionLayout extends RenderBox final dx = (imageSize.width - captionSize.width) / 2.0; captionParentData.offset = Offset(dx, imageSize.height + spacing); - size = BoxConstraints( - minWidth: constraints.minWidth, - maxWidth: constraints.maxWidth, - ).constrain(Size( - imageSize.width, - imageSize.height + spacing + captionSize.height, - )); + size = + BoxConstraints( + minWidth: constraints.minWidth, + maxWidth: constraints.maxWidth, + ).constrain( + Size( + imageSize.width, + imageSize.height + spacing + captionSize.height, + ), + ); } else { size = BoxConstraints( minWidth: constraints.minWidth, diff --git a/packages/mixin_markdown_widget/lib/src/render/markdown_block_widgets.dart b/packages/mixin_markdown_widget/lib/src/render/markdown_block_widgets.dart index 140a3085..a55b076e 100644 --- a/packages/mixin_markdown_widget/lib/src/render/markdown_block_widgets.dart +++ b/packages/mixin_markdown_widget/lib/src/render/markdown_block_widgets.dart @@ -2,7 +2,7 @@ import 'image_caption_layout.dart'; import 'dart:math' as math; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import '../core/document.dart'; import '../widgets/markdown_theme.dart'; @@ -56,10 +56,7 @@ typedef MarkdownTableWidgetBuilder = Widget Function( TableColumnWidth defaultColumnWidth, ); -enum MarkdownTableColumnSizing { - fixed, - flex, -} +enum MarkdownTableColumnSizing { fixed, flex } class MarkdownTableLayoutPlanCache { final Map _plans = @@ -71,10 +68,7 @@ class MarkdownTableLayoutPlanCache { return cached.plan; } final plan = MarkdownTableLayoutPlan.fromBlock(block); - _plans[block.id] = _CachedMarkdownTableLayoutPlan( - block: block, - plan: plan, - ); + _plans[block.id] = _CachedMarkdownTableLayoutPlan(block: block, plan: plan); return plan; } @@ -120,9 +114,11 @@ class MarkdownTableLayoutPlan { final colMaxChars = List.filled(columnCount, 0); for (final row in block.rows) { - for (var index = 0; - index < row.cells.length && index < columnCount; - index++) { + for ( + var index = 0; + index < row.cells.length && index < columnCount; + index++ + ) { final textLen = _estimateInlineTextLength(row.cells[index].inlines); if (textLen > colMaxChars[index]) { colMaxChars[index] = textLen; @@ -281,10 +277,7 @@ class MarkdownTableContentFrame extends StatelessWidget { borderColor: theme.tableBorderColor, borderRadius: borderRadius, ), - child: ClipRRect( - borderRadius: borderRadius, - child: child, - ), + child: ClipRRect(borderRadius: borderRadius, child: child), ); } } @@ -318,15 +311,13 @@ class MarkdownQuoteBlockView extends StatelessWidget { decoration: BoxDecoration( color: theme.quoteBorderColor, borderRadius: BorderRadius.horizontal( - left: theme.quoteBorderRadius.topLeft), + left: theme.quoteBorderRadius.topLeft, + ), ), child: SizedBox(width: theme.quoteBorderWidth), ), ), - Padding( - padding: theme.quotePadding, - child: _buildContent(), - ), + Padding(padding: theme.quotePadding, child: _buildContent()), ], ), ); @@ -604,8 +595,9 @@ class MarkdownCodeBlockView extends StatelessWidget { padding: EdgeInsets.zero, style: IconButton.styleFrom( backgroundColor: Colors.transparent, - foregroundColor: - theme.bodyStyle.color?.withValues(alpha: 0.72), + foregroundColor: theme.bodyStyle.color?.withValues( + alpha: 0.72, + ), ), icon: const Icon(Icons.copy_rounded, size: 18), onPressed: onCopyCode, diff --git a/packages/mixin_markdown_widget/lib/src/render/markdown_document_view.dart b/packages/mixin_markdown_widget/lib/src/render/markdown_document_view.dart index 8c5fd23f..cd30888e 100644 --- a/packages/mixin_markdown_widget/lib/src/render/markdown_document_view.dart +++ b/packages/mixin_markdown_widget/lib/src/render/markdown_document_view.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import '../clipboard/plain_text_serializer.dart'; import '../core/document.dart'; @@ -291,10 +291,10 @@ class _MarkdownDocumentViewState extends State { return null; } final block = widget.document.blocks[localBlockIndex]; - final upstream = _blockStateFor(block)?.isPointerOnPrecedingCharacter( - position.textOffset, - globalPosition, - ) ?? + final upstream = + _blockStateFor( + block, + )?.isPointerOnPrecedingCharacter(position.textOffset, globalPosition) ?? false; return StructuredBlockSelection.forBlock(block) .linkAt(position, upstream: upstream) @@ -306,8 +306,10 @@ class _MarkdownDocumentViewState extends State { return state is MarkdownSelectableBlockHandle ? state : null; } - DocumentPosition? _hitTestPosition(Offset globalPosition, - {required bool clamp}) { + DocumentPosition? _hitTestPosition( + Offset globalPosition, { + required bool clamp, + }) { for (final block in widget.document.blocks) { final hit = _blockStateFor(block)?.hitTestGlobal(globalPosition); if (hit != null) { @@ -329,13 +331,13 @@ class _MarkdownDocumentViewState extends State { final dx = globalPosition.dx < rect.left ? rect.left - globalPosition.dx : globalPosition.dx > rect.right - ? globalPosition.dx - rect.right - : 0.0; + ? globalPosition.dx - rect.right + : 0.0; final dy = globalPosition.dy < rect.top ? rect.top - globalPosition.dy : globalPosition.dy > rect.bottom - ? globalPosition.dy - rect.bottom - : 0.0; + ? globalPosition.dy - rect.bottom + : 0.0; final distance = dx * dx + dy * dy; if (distance < bestDistance) { bestDistance = distance; @@ -385,7 +387,9 @@ class _MarkdownDocumentViewState extends State { } void _selectSelectionUnitAt( - Offset globalPosition, DocumentPosition position) { + Offset globalPosition, + DocumentPosition position, + ) { final selectionController = widget.selectionController; if (selectionController == null) { return; @@ -398,10 +402,7 @@ class _MarkdownDocumentViewState extends State { return; } selectionController.setSelection( - blockState.selectSelectionUnit( - position, - globalPosition: globalPosition, - ), + blockState.selectSelectionUnit(position, globalPosition: globalPosition), ); } @@ -589,7 +590,7 @@ class _MarkdownDocumentViewState extends State { } Iterable - _inheritedSelectionAutoScrollTargets() sync* { + _inheritedSelectionAutoScrollTargets() sync* { if (!widget.useColumn) { yield MarkdownSelectionAutoScrollTarget( scrollController: _effectiveScrollController, diff --git a/packages/mixin_markdown_widget/lib/src/render/pretext_text_block.dart b/packages/mixin_markdown_widget/lib/src/render/pretext_text_block.dart index 14dd7fe3..97a4a34c 100644 --- a/packages/mixin_markdown_widget/lib/src/render/pretext_text_block.dart +++ b/packages/mixin_markdown_widget/lib/src/render/pretext_text_block.dart @@ -4,7 +4,7 @@ import 'dart:collection'; import 'dart:math' as math; import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:pretext/pretext.dart'; import 'package:pretext/src/segment.dart' as pretext_segment; @@ -22,8 +22,8 @@ class MarkdownPretextInlineRun { this.renderSpan, this.estimatedWidth, this.estimatedLineHeight, - }) : assert(renderSpan == null || decoration == null), - assert(renderSpan == null || allowCharacterWrap == false); + }) : assert(renderSpan == null || decoration == null), + assert(renderSpan == null || allowCharacterWrap == false); final String text; final TextStyle style; @@ -84,8 +84,8 @@ class MarkdownPretextTextBlock extends StatelessWidget { this.textAlign = TextAlign.start, this.intrinsicWidthSafe = false, this.preferDirectRichText = false, - }) : runs = null, - fallbackStyle = style; + }) : runs = null, + fallbackStyle = style; const MarkdownPretextTextBlock.rich({ super.key, @@ -94,8 +94,8 @@ class MarkdownPretextTextBlock extends StatelessWidget { this.textAlign = TextAlign.start, this.intrinsicWidthSafe = false, this.preferDirectRichText = false, - }) : text = '', - style = fallbackStyle; + }) : text = '', + style = fallbackStyle; final String text; final TextStyle style; @@ -110,7 +110,8 @@ class MarkdownPretextTextBlock extends StatelessWidget { final textScaler = MediaQuery.maybeTextScalerOf(context) ?? TextScaler.noScaling; final textDirection = Directionality.of(context); - final effectiveRuns = runs ?? + final effectiveRuns = + runs ?? [ MarkdownPretextInlineRun(text: text, style: style), ]; @@ -206,8 +207,11 @@ class MarkdownPretextTextBlock extends StatelessWidget { } final LinkedHashMap<_MarkdownPretextLayoutCacheKey, MarkdownPretextLayoutResult> - _markdownPretextLayoutCache = LinkedHashMap<_MarkdownPretextLayoutCacheKey, - MarkdownPretextLayoutResult>(); +_markdownPretextLayoutCache = + LinkedHashMap< + _MarkdownPretextLayoutCacheKey, + MarkdownPretextLayoutResult + >(); const int _markdownPretextLayoutCacheLimit = 192; @@ -242,13 +246,13 @@ class _MarkdownPretextLayoutCacheKey { @override int get hashCode => Object.hash( - runsSignature, - fallbackStyleHash, - maxWidth, - textScaleFactor, - textAlign, - textDirection, - ); + runsSignature, + fallbackStyleHash, + maxWidth, + textScaleFactor, + textAlign, + textDirection, + ); } int _markdownPretextRunsSignature(List runs) { @@ -286,11 +290,9 @@ void _storeCachedMarkdownPretextLayout( MarkdownPretextLayoutResult result, ) { _markdownPretextLayoutCache[key] = result; - while ( - _markdownPretextLayoutCache.length > _markdownPretextLayoutCacheLimit) { - _markdownPretextLayoutCache.remove( - _markdownPretextLayoutCache.keys.first, - ); + while (_markdownPretextLayoutCache.length > + _markdownPretextLayoutCacheLimit) { + _markdownPretextLayoutCache.remove(_markdownPretextLayoutCache.keys.first); } } @@ -517,11 +519,7 @@ MarkdownPretextLayoutResult computeMarkdownPretextLayoutFromRuns({ ) .toList(growable: false), ); - final result = layoutWithLines( - prepared, - safeMaxWidth, - lineHeight, - ); + final result = layoutWithLines(prepared, safeMaxWidth, lineHeight); final lines = []; var cursor = 0; @@ -531,13 +529,13 @@ MarkdownPretextLayoutResult computeMarkdownPretextLayoutFromRuns({ final visibleTextLength = carryLength + line.text.length; final fittedVisibleTextLength = _fitVisibleTextLengthToRenderedWidthFromOffset( - segments: segments, - startOffset: cursor, - endSegmentIndex: line.end.segmentIndex, - visibleTextLength: visibleTextLength, - maxRenderedWidth: safeMaxWidth, - textScaleFactor: textScaleFactor, - ); + segments: segments, + startOffset: cursor, + endSegmentIndex: line.end.segmentIndex, + visibleTextLength: visibleTextLength, + maxRenderedWidth: safeMaxWidth, + textScaleFactor: textScaleFactor, + ); final lineFragments = _buildLineFragmentsFromOffset( segments: segments, startOffset: cursor, @@ -546,11 +544,15 @@ MarkdownPretextLayoutResult computeMarkdownPretextLayoutFromRuns({ maxRenderedWidth: safeMaxWidth, textScaleFactor: textScaleFactor, ); - final renderedLineText = - lineFragments.map((fragment) => fragment.text).join(); + final renderedLineText = lineFragments + .map((fragment) => fragment.text) + .join(); final startOffset = cursor; - final visibleEndOffset = - _consumeVisibleText(plainText, cursor, renderedLineText); + final visibleEndOffset = _consumeVisibleText( + plainText, + cursor, + renderedLineText, + ); var endOffset = visibleEndOffset; while (endOffset < plainText.length && plainText[endOffset] == ' ') { endOffset += 1; @@ -558,8 +560,11 @@ MarkdownPretextLayoutResult computeMarkdownPretextLayoutFromRuns({ if (endOffset < plainText.length && plainText[endOffset] == '\n') { endOffset += 1; } - final originalVisibleEndOffset = - _consumeVisibleText(plainText, originalCursor, line.text); + final originalVisibleEndOffset = _consumeVisibleText( + plainText, + originalCursor, + line.text, + ); var originalEndOffset = originalVisibleEndOffset; while (originalEndOffset < plainText.length && plainText[originalEndOffset] == ' ') { @@ -611,13 +616,13 @@ MarkdownPretextLayoutResult computeMarkdownPretextLayoutFromRuns({ while (cursor < plainText.length) { final fittedVisibleTextLength = _fitVisibleTextLengthToRenderedWidthFromOffset( - segments: segments, - startOffset: cursor, - endSegmentIndex: segments.length, - visibleTextLength: plainText.length - cursor, - maxRenderedWidth: safeMaxWidth, - textScaleFactor: textScaleFactor, - ); + segments: segments, + startOffset: cursor, + endSegmentIndex: segments.length, + visibleTextLength: plainText.length - cursor, + maxRenderedWidth: safeMaxWidth, + textScaleFactor: textScaleFactor, + ); final lineFragments = _buildLineFragmentsFromOffset( segments: segments, startOffset: cursor, @@ -629,11 +634,15 @@ MarkdownPretextLayoutResult computeMarkdownPretextLayoutFromRuns({ if (lineFragments.isEmpty) { break; } - final renderedLineText = - lineFragments.map((fragment) => fragment.text).join(); + final renderedLineText = lineFragments + .map((fragment) => fragment.text) + .join(); final startOffset = cursor; - final visibleEndOffset = - _consumeVisibleText(plainText, cursor, renderedLineText); + final visibleEndOffset = _consumeVisibleText( + plainText, + cursor, + renderedLineText, + ); if (visibleEndOffset <= cursor) { break; } @@ -718,8 +727,10 @@ class MarkdownPretextLayoutResult { for (var lineIndex = 0; lineIndex < lines.length; lineIndex++) { final line = lines[lineIndex]; final selectionStart = math.max(range.start.textOffset, line.startOffset); - final selectionEnd = - math.min(range.end.textOffset, line.visibleEndOffset); + final selectionEnd = math.min( + range.end.textOffset, + line.visibleEndOffset, + ); if (selectionStart < selectionEnd && line.text.isNotEmpty) { final mergedBoxes = _mergeSelectionBoxes( _selectionBoxesForLine( @@ -732,12 +743,7 @@ class MarkdownPretextLayoutResult { ); for (final box in mergedBoxes) { rects.add( - Rect.fromLTRB( - box.left - 1.0, - box.top, - box.right + 1.0, - box.bottom, - ), + Rect.fromLTRB(box.left - 1.0, box.top, box.right + 1.0, box.bottom), ); } } @@ -760,8 +766,10 @@ class MarkdownPretextLayoutResult { : line.endOffset; } - final localDx = (localPosition.dx - line.leadingOffset) - .clamp(0.0, math.max(line.width, 0.0)); + final localDx = (localPosition.dx - line.leadingOffset).clamp( + 0.0, + math.max(line.width, 0.0), + ); return _textOffsetAtLineDx( line, localDx.toDouble(), @@ -834,8 +842,10 @@ class MarkdownPretextLayoutResult { required TextDirection textDirection, }) sync* { for (final segment in line.segments) { - final segmentSelectionStart = - math.max(selectionStart, segment.startOffset); + final segmentSelectionStart = math.max( + selectionStart, + segment.startOffset, + ); final segmentSelectionEnd = math.min(selectionEnd, segment.endOffset); if (segmentSelectionStart >= segmentSelectionEnd) { continue; @@ -843,21 +853,25 @@ class MarkdownPretextLayoutResult { final left = segmentSelectionStart == segment.startOffset ? segment.left : segment.left + - _horizontalOffsetForTextOffset( - segment, - segmentSelectionStart - segment.startOffset, - textDirection: textDirection, - ); + _horizontalOffsetForTextOffset( + segment, + segmentSelectionStart - segment.startOffset, + textDirection: textDirection, + ); final right = segmentSelectionEnd == segment.endOffset ? segment.right : segment.left + - _horizontalOffsetForTextOffset( - segment, - segmentSelectionEnd - segment.startOffset, - textDirection: textDirection, - ); - yield Rect.fromLTRB(left + line.leadingOffset, lineTop, - right + line.leadingOffset, lineTop + line.height); + _horizontalOffsetForTextOffset( + segment, + segmentSelectionEnd - segment.startOffset, + textDirection: textDirection, + ); + yield Rect.fromLTRB( + left + line.leadingOffset, + lineTop, + right + line.leadingOffset, + lineTop + line.height, + ); } } @@ -1020,14 +1034,18 @@ List _buildBreakableDecoratedFullSpans( style: run.style, decoration: run.decoration!.copyWith( borderRadius: BorderRadius.only( - topLeft: - slice.startsChunk ? baseBorderRadius.topLeft : Radius.zero, - bottomLeft: - slice.startsChunk ? baseBorderRadius.bottomLeft : Radius.zero, - topRight: - slice.endsChunk ? baseBorderRadius.topRight : Radius.zero, - bottomRight: - slice.endsChunk ? baseBorderRadius.bottomRight : Radius.zero, + topLeft: slice.startsChunk + ? baseBorderRadius.topLeft + : Radius.zero, + bottomLeft: slice.startsChunk + ? baseBorderRadius.bottomLeft + : Radius.zero, + topRight: slice.endsChunk + ? baseBorderRadius.topRight + : Radius.zero, + bottomRight: slice.endsChunk + ? baseBorderRadius.bottomRight + : Radius.zero, ), padding: EdgeInsets.only( left: slice.startsChunk ? run.decoration!.padding.left : 0, @@ -1061,8 +1079,9 @@ class _DecoratedInlineText extends LeafRenderObjectWidget { text, style, decoration, - (MediaQuery.maybeTextScalerOf(context) ?? TextScaler.noScaling) - .scale(1.0), + (MediaQuery.maybeTextScalerOf(context) ?? TextScaler.noScaling).scale( + 1.0, + ), View.of(context).devicePixelRatio, ); } @@ -1077,8 +1096,9 @@ class _DecoratedInlineText extends LeafRenderObjectWidget { ..style = style ..decoration = decoration ..textScaleFactor = - (MediaQuery.maybeTextScalerOf(context) ?? TextScaler.noScaling) - .scale(1.0) + (MediaQuery.maybeTextScalerOf(context) ?? TextScaler.noScaling).scale( + 1.0, + ) ..devicePixelRatio = View.of(context).devicePixelRatio; } } @@ -1167,11 +1187,7 @@ class _RenderDecoratedInlineText extends RenderBox { ..text = TextSpan(text: _text, style: _style) ..textScaler = TextScaler.linear(_textScaleFactor) ..layout(maxWidth: double.infinity); - _metrics = _measureTightTextMetrics( - _text, - _style, - _textScaleFactor, - ); + _metrics = _measureTightTextMetrics(_text, _style, _textScaleFactor); } Size _computeSize(BoxConstraints constraints) { @@ -1365,7 +1381,8 @@ List _buildLineSegmentsFromFragments( var cursor = 0.0; for (final fragment in fragments) { - final width = fragment.estimatedWidth ?? + final width = + fragment.estimatedWidth ?? measurer.measure( fragment.text, fragment.style, @@ -1410,9 +1427,11 @@ List<_MarkdownPretextLineFragment> _buildLineFragmentsFromOffset({ startSegmentIndex = 0; } - for (var index = startSegmentIndex; - index < endSegmentIndex && index < segments.length && remaining > 0; - index++) { + for ( + var index = startSegmentIndex; + index < endSegmentIndex && index < segments.length && remaining > 0; + index++ + ) { final segment = segments[index]; if (segment.kind == pretext_segment.SegmentKind.hardBreak || segment.displayText.isEmpty) { @@ -1446,7 +1465,8 @@ List<_MarkdownPretextLineFragment> _buildLineFragmentsFromOffset({ renderSpan: segment.renderSpan, decoration: segment.decoration, // A plain fragment of a whole segment has its measured width. - estimatedWidth: segment.renderSpan != null || + estimatedWidth: + segment.renderSpan != null || (segment.decoration == null && text.length == segment.displayText.length) ? segment.width @@ -1459,7 +1479,8 @@ List<_MarkdownPretextLineFragment> _buildLineFragmentsFromOffset({ ), startOffset: localStartOffset, endOffset: endOffset, - startsDecoratedChunk: segment.startsDecoratedChunk && + startsDecoratedChunk: + segment.startsDecoratedChunk && localStartOffset == segment.startOffset, endsDecoratedChunk: segment.endsDecoratedChunk && endOffset == segment.endOffset, @@ -1496,9 +1517,11 @@ int _fitVisibleTextLengthToRenderedWidthFromOffset({ required double textScaleFactor, }) { final startSegmentIndex = _segmentIndexForOffset(segments, startOffset); - for (var index = startSegmentIndex; - index < endSegmentIndex && index < segments.length; - index++) { + for ( + var index = startSegmentIndex; + index < endSegmentIndex && index < segments.length; + index++ + ) { if (segments[index].renderSpan != null) { return visibleTextLength; } @@ -1552,9 +1575,11 @@ int _previousBreakBoundaryVisibleLength({ final currentEndOffset = startOffset + currentVisibleLength; var previousBoundaryOffset = -1; - for (var index = _segmentIndexForOffset(segments, startOffset); - index < endSegmentIndex && index < segments.length; - index++) { + for ( + var index = _segmentIndexForOffset(segments, startOffset); + index < endSegmentIndex && index < segments.length; + index++ + ) { final segment = segments[index]; if (segment.kind == pretext_segment.SegmentKind.hardBreak || segment.displayText.isEmpty) { @@ -1562,8 +1587,10 @@ int _previousBreakBoundaryVisibleLength({ } if (segment.startOffset > startOffset && segment.startOffset < currentEndOffset) { - previousBoundaryOffset = - math.max(previousBoundaryOffset, segment.startOffset); + previousBoundaryOffset = math.max( + previousBoundaryOffset, + segment.startOffset, + ); } if (segment.endOffset >= currentEndOffset) { break; @@ -1676,7 +1703,7 @@ double _measureRenderedFragmentLineHeight( previousPadding = fragment.padding; final measured = fragment.renderSpan != null ? fragment.estimatedLineHeight ?? - _measureLineHeight(fragment.style, textScaleFactor) + _measureLineHeight(fragment.style, textScaleFactor) : _measureLineHeight( fragment.style, textScaleFactor, @@ -1733,8 +1760,10 @@ List<_MarkdownPretextLineFragment> _applyLineLocalDecoratedPadding( )), ); final horizontalPaddingBudget = math.max(maxRenderedWidth - baseWidth, 0.0); - final desiredHorizontalPadding = - fragments.fold(0, (current, fragment) { + final desiredHorizontalPadding = fragments.fold(0, ( + current, + fragment, + ) { final decoration = fragment.decoration; if (decoration == null) { return current; @@ -1772,19 +1801,23 @@ List<_MarkdownPretextLineFragment> _applyLineLocalDecoratedPadding( decoration: decoration.copyWith( borderRadius: BorderRadius.only( topLeft: isFirst ? decoration.borderRadius.topLeft : Radius.zero, - bottomLeft: - isFirst ? decoration.borderRadius.bottomLeft : Radius.zero, + bottomLeft: isFirst + ? decoration.borderRadius.bottomLeft + : Radius.zero, topRight: isLast ? decoration.borderRadius.topRight : Radius.zero, - bottomRight: - isLast ? decoration.borderRadius.bottomRight : Radius.zero, + bottomRight: isLast + ? decoration.borderRadius.bottomRight + : Radius.zero, ), ), padding: EdgeInsets.only( - left: currentFragment.padding.left + + left: + currentFragment.padding.left + (isFirst && !currentFragment.startsDecoratedChunk ? decoration.padding.left * horizontalPaddingScale : 0), - right: currentFragment.padding.right + + right: + currentFragment.padding.right + (isLast && !currentFragment.endsDecoratedChunk ? decoration.padding.right * horizontalPaddingScale : 0), @@ -1806,7 +1839,8 @@ double _measureMaxLineHeight( ) { var lineHeight = 0.0; for (final run in runs) { - final measured = run.estimatedLineHeight ?? + final measured = + run.estimatedLineHeight ?? _measureLineHeight( run.style, textScaleFactor, @@ -1878,11 +1912,12 @@ _TightTextMetrics _measureTightTextMetrics( String text, TextStyle style, double textScaleFactor, -) => - _cachedMeasure( - (#tightMetrics, text, style, textScaleFactor), - () => _measureUncachedTightTextMetrics(text, style, textScaleFactor), - ); +) => _cachedMeasure(( + #tightMetrics, + text, + style, + textScaleFactor, +), () => _measureUncachedTightTextMetrics(text, style, textScaleFactor)); _TightTextMetrics _measureUncachedTightTextMetrics( String text, @@ -1948,8 +1983,11 @@ double _horizontalOffsetForTextOffset( return segmentWidth * clampedOffset / segment.text.length; } final prefix = segment.text.substring(0, clampedOffset); - final textWidth = _measureInlineTextWidth(prefix, segment.style, - textDirection: textDirection); + final textWidth = _measureInlineTextWidth( + prefix, + segment.style, + textDirection: textDirection, + ); if (segment.decoration == null) { return textWidth; } @@ -2021,7 +2059,8 @@ class _MarkdownPretextSegmentBuilder { _MarkdownPretextMeasuredSegment( displayText: text, kind: pretext_segment.SegmentKind.word, - width: run.estimatedWidth ?? + width: + run.estimatedWidth ?? _measurer.measure(text, run.style, textScaleFactor), style: run.style, mouseCursor: run.mouseCursor, @@ -2046,11 +2085,7 @@ class _MarkdownPretextSegmentBuilder { : pretext_segment.SegmentKind.word, width: slice.isNewline ? 0 - : _measurer.measure( - slice.text, - run.style, - textScaleFactor, - ), + : _measurer.measure(slice.text, run.style, textScaleFactor), style: run.style, mouseCursor: run.mouseCursor, recognizer: run.recognizer, @@ -2186,7 +2221,8 @@ class _MarkdownPretextSegmentBuilder { ? pretext_segment.SegmentKind.space : pretext_segment.SegmentKind.word, // A tab shows as a space, so its text differs from the run text. - width: displayText.length == index - startIndex && + width: + displayText.length == index - startIndex && !displayText.contains('\t') ? _measurer.measureInRun( displayText, @@ -2574,10 +2610,12 @@ class _TextPainterSegmentMeasurer { _RunRangeWidths runWidths, int start, ) { - return _cachedMeasure( - (#inRun, segment, style, textScaleFactor), - () => runWidths.width(start, start + segment.length), - ); + return _cachedMeasure(( + #inRun, + segment, + style, + textScaleFactor, + ), () => runWidths.width(start, start + segment.length)); } double measure( diff --git a/packages/mixin_markdown_widget/lib/src/render/selectable_block.dart b/packages/mixin_markdown_widget/lib/src/render/selectable_block.dart index cc4f56ec..a6b47f06 100644 --- a/packages/mixin_markdown_widget/lib/src/render/selectable_block.dart +++ b/packages/mixin_markdown_widget/lib/src/render/selectable_block.dart @@ -1,21 +1,15 @@ import 'dart:math' as math; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:word_segmenter/word_segmenter.dart'; import '../core/document.dart'; import '../selection/structured_block_selection.dart'; import 'selection/markdown_selectable_leaf.dart'; -enum SelectableBlockHitTestBehavior { - text, - block, -} +enum SelectableBlockHitTestBehavior { text, block } -enum SelectableBlockSelectionPaintOrder { - behindChild, - aboveChild, -} +enum SelectableBlockSelectionPaintOrder { behindChild, aboveChild } @immutable class SelectableBlockSpec { @@ -49,25 +43,24 @@ class SelectableBlockSpec { BuildContext context, Size constraints, DocumentRange range, - )? selectionRectResolver; - final int? Function( - BuildContext context, - Size size, - Offset localPosition, - )? textOffsetResolver; + )? + selectionRectResolver; + final int? Function(BuildContext context, Size size, Offset localPosition)? + textOffsetResolver; final DocumentRange? Function( BuildContext context, Size size, Offset? localPosition, DocumentPosition position, - )? selectionUnitRangeResolver; + )? + selectionUnitRangeResolver; final StructuredBlockSelection? selectionStructure; final SelectableBlockSelectionPaintOrder selectionPaintOrder; final Color? selectionColor; final Listenable? repaintListenable; final EdgeInsets? selectionClipPadding; final Rect? Function(BuildContext context, Size size)? - selectionClipRectResolver; + selectionClipRectResolver; } class SelectableMarkdownBlock extends StatefulWidget { @@ -232,7 +225,8 @@ class SelectableMarkdownBlockState extends State widget.spec.plainText.isEmpty) { return selectWholeBlock(); } - final upstream = globalPosition != null && + final upstream = + globalPosition != null && isPointerOnPrecedingCharacter(position.textOffset, globalPosition); ({int start, int end}) wordRangeAt(String text, int offset) => markdownWordRangeAt(text, offset, upstream: upstream); @@ -261,8 +255,8 @@ class SelectableMarkdownBlockState extends State position.textOffset < 0 ? 0 : position.textOffset > widget.spec.plainText.length - ? widget.spec.plainText.length - : position.textOffset, + ? widget.spec.plainText.length + : position.textOffset, ); var start = word.start; var end = word.end; @@ -303,13 +297,19 @@ class SelectableMarkdownBlockState extends State final size = renderObject is RenderBox && renderObject.hasSize ? renderObject.size : Size.zero; - final localPosition = renderObject is RenderBox && + final localPosition = + renderObject is RenderBox && renderObject.hasSize && globalPosition != null ? renderObject.globalToLocal(globalPosition) : null; - final range = widget.spec.selectionUnitRangeResolver - ?.call(context, size, localPosition, position) ?? + final range = + widget.spec.selectionUnitRangeResolver?.call( + context, + size, + localPosition, + position, + ) ?? _resolveLineRangeWithTextPainter(position, size); if (range == null) { return selectWholeBlock(); @@ -381,7 +381,8 @@ class SelectableMarkdownBlockState extends State ); } final localPosition = renderObject.globalToLocal(globalPosition); - final bool preferEnd = localPosition.dy > renderObject.size.height / 2 || + final bool preferEnd = + localPosition.dy > renderObject.size.height / 2 || localPosition.dx > renderObject.size.width / 2; if ((Offset.zero & renderObject.size).contains(localPosition)) { return _hitTestLocal(localPosition, renderObject.size) ?? @@ -429,11 +430,13 @@ class SelectableMarkdownBlockState extends State ), ); final paintedChild = CustomPaint( - painter: widget.spec.selectionPaintOrder == + painter: + widget.spec.selectionPaintOrder == SelectableBlockSelectionPaintOrder.behindChild ? selectionPainter : null, - foregroundPainter: widget.spec.selectionPaintOrder == + foregroundPainter: + widget.spec.selectionPaintOrder == SelectableBlockSelectionPaintOrder.aboveChild ? selectionPainter : null, @@ -471,8 +474,8 @@ class SelectableMarkdownBlockState extends State final textOffset = resolvedOffset < 0 ? 0 : resolvedOffset > widget.spec.plainText.length - ? widget.spec.plainText.length - : resolvedOffset; + ? widget.spec.plainText.length + : resolvedOffset; final structure = widget.spec.selectionStructure; if (structure != null) { return structure.positionForDisplayedOffset( @@ -490,7 +493,8 @@ class SelectableMarkdownBlockState extends State if (widget.spec.hitTestBehavior == SelectableBlockHitTestBehavior.block || widget.spec.textSpan == null) { return _edgePosition( - preferEnd: localPosition.dy > size.height / 2 || + preferEnd: + localPosition.dy > size.height / 2 || localPosition.dx > size.width / 2, ); } @@ -501,17 +505,21 @@ class SelectableMarkdownBlockState extends State widget.spec.measurementPadding.top, ); final clampedOffset = Offset( - (localPosition.dx - contentOffset.dx) - .clamp(0.0, math.max(textPainter.width, 0.0)), - (localPosition.dy - contentOffset.dy) - .clamp(0.0, math.max(textPainter.height, 0.0)), + (localPosition.dx - contentOffset.dx).clamp( + 0.0, + math.max(textPainter.width, 0.0), + ), + (localPosition.dy - contentOffset.dy).clamp( + 0.0, + math.max(textPainter.height, 0.0), + ), ); final position = textPainter.getPositionForOffset(clampedOffset); final textOffset = position.offset < 0 ? 0 : position.offset > widget.spec.plainText.length - ? widget.spec.plainText.length - : position.offset; + ? widget.spec.plainText.length + : position.offset; final structure = widget.spec.selectionStructure; if (structure != null) { return structure.positionForDisplayedOffset( @@ -624,8 +632,10 @@ class SelectableMarkdownBlockState extends State ), ); } else if (widget.spec.textSpan != null) { - final textPainter = - _buildTextPainter(renderObject.size, Directionality.of(context)); + final textPainter = _buildTextPainter( + renderObject.size, + Directionality.of(context), + ); final contentOffset = Offset( widget.spec.measurementPadding.left, widget.spec.measurementPadding.top, @@ -665,10 +675,7 @@ class SelectableMarkdownBlockState extends State maxLines: null, ); final maxWidth = math - .max( - size.width - widget.spec.measurementPadding.horizontal, - 0, - ) + .max(size.width - widget.spec.measurementPadding.horizontal, 0) .toDouble(); textPainter.layout(maxWidth: maxWidth); return textPainter; @@ -686,12 +693,13 @@ class SelectableMarkdownBlockState extends State final clampedOffset = position.textOffset < 0 ? 0 : position.textOffset > widget.spec.plainText.length - ? widget.spec.plainText.length - : position.textOffset; + ? widget.spec.plainText.length + : position.textOffset; late final TextRange boundary; try { - boundary = - textPainter.getLineBoundary(TextPosition(offset: clampedOffset)); + boundary = textPainter.getLineBoundary( + TextPosition(offset: clampedOffset), + ); } on AssertionError { return null; } @@ -763,15 +771,18 @@ class _BlockSelectionPainter extends CustomPainter { if (spec.hitTestBehavior == SelectableBlockHitTestBehavior.text && spec.textSpan != null) { - final textPainter = TextPainter( - text: spec.textSpan, - textAlign: spec.textAlign, - textDirection: textDirection, - maxLines: null, - )..layout( - maxWidth: - math.max(size.width - spec.measurementPadding.horizontal, 0), - ); + final textPainter = + TextPainter( + text: spec.textSpan, + textAlign: spec.textAlign, + textDirection: textDirection, + maxLines: null, + )..layout( + maxWidth: math.max( + size.width - spec.measurementPadding.horizontal, + 0, + ), + ); final textSelection = TextSelection( baseOffset: range.start.textOffset, extentOffset: range.end.textOffset, @@ -845,10 +856,7 @@ class _BlockSelectionPainter extends CustomPainter { canvas.saveLayer(null, Paint()); for (var index = 0; index < normalizedRects.length; index++) { - canvas.drawRRect( - _selectionRRectForIndex(normalizedRects, index), - paint, - ); + canvas.drawRRect(_selectionRRectForIndex(normalizedRects, index), paint); } _fillInnerCornerTransitions(canvas, normalizedRects, paint); canvas.restore(); @@ -1025,48 +1033,53 @@ class _BlockSelectionPainter extends CustomPainter { final squareRect = switch (quadrant) { _SelectionCornerQuadrant.topLeft => Rect.fromLTRB( - corner.dx - radius, - corner.dy - radius, - corner.dx, - corner.dy, - ), + corner.dx - radius, + corner.dy - radius, + corner.dx, + corner.dy, + ), _SelectionCornerQuadrant.topRight => Rect.fromLTRB( - corner.dx, - corner.dy - radius, - corner.dx + radius, - corner.dy, - ), + corner.dx, + corner.dy - radius, + corner.dx + radius, + corner.dy, + ), _SelectionCornerQuadrant.bottomLeft => Rect.fromLTRB( - corner.dx - radius, - corner.dy, - corner.dx, - corner.dy + radius, - ), + corner.dx - radius, + corner.dy, + corner.dx, + corner.dy + radius, + ), _SelectionCornerQuadrant.bottomRight => Rect.fromLTRB( - corner.dx, - corner.dy, - corner.dx + radius, - corner.dy + radius, - ), + corner.dx, + corner.dy, + corner.dx + radius, + corner.dy + radius, + ), }; // Circle centered at the far corner of squareRect (opposite to corner) final center = switch (quadrant) { - _SelectionCornerQuadrant.topLeft => - Offset(corner.dx - radius, corner.dy - radius), - _SelectionCornerQuadrant.topRight => - Offset(corner.dx + radius, corner.dy - radius), - _SelectionCornerQuadrant.bottomLeft => - Offset(corner.dx - radius, corner.dy + radius), - _SelectionCornerQuadrant.bottomRight => - Offset(corner.dx + radius, corner.dy + radius), + _SelectionCornerQuadrant.topLeft => Offset( + corner.dx - radius, + corner.dy - radius, + ), + _SelectionCornerQuadrant.topRight => Offset( + corner.dx + radius, + corner.dy - radius, + ), + _SelectionCornerQuadrant.bottomLeft => Offset( + corner.dx - radius, + corner.dy + radius, + ), + _SelectionCornerQuadrant.bottomRight => Offset( + corner.dx + radius, + corner.dy + radius, + ), }; final oval = Path() ..addOval(Rect.fromCircle(center: center, radius: radius)); final clip = Path()..addRect(squareRect); - canvas.drawPath( - Path.combine(PathOperation.difference, clip, oval), - paint, - ); + canvas.drawPath(Path.combine(PathOperation.difference, clip, oval), paint); } List _mergeSelectionBoxes(List boxes) { @@ -1090,7 +1103,8 @@ class _BlockSelectionPainter extends CustomPainter { var current = sorted.first; for (final next in sorted.skip(1)) { - final sameLine = (next.top - current.top).abs() <= lineTolerance && + final sameLine = + (next.top - current.top).abs() <= lineTolerance && (next.bottom - current.bottom).abs() <= lineTolerance; final horizontalGap = next.left - current.right; if (sameLine && horizontalGap <= gapTolerance) { @@ -1112,12 +1126,7 @@ class _BlockSelectionPainter extends CustomPainter { } } -enum _SelectionCornerQuadrant { - topLeft, - topRight, - bottomLeft, - bottomRight, -} +enum _SelectionCornerQuadrant { topLeft, topRight, bottomLeft, bottomRight } /// The word around [offset] in [text], for double-click selection. /// diff --git a/packages/mixin_markdown_widget/lib/src/render/selection/markdown_descriptor_extractor.dart b/packages/mixin_markdown_widget/lib/src/render/selection/markdown_descriptor_extractor.dart index c913f513..384b1538 100644 --- a/packages/mixin_markdown_widget/lib/src/render/selection/markdown_descriptor_extractor.dart +++ b/packages/mixin_markdown_widget/lib/src/render/selection/markdown_descriptor_extractor.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import '../../clipboard/plain_text_serializer.dart'; import '../../core/document.dart'; @@ -37,10 +37,7 @@ class PretextTextDescriptor { @immutable class _PrefixedPretextRuns { - const _PrefixedPretextRuns({ - required this.plainText, - required this.runs, - }); + const _PrefixedPretextRuns({required this.plainText, required this.runs}); final String plainText; final List runs; @@ -160,8 +157,9 @@ class MarkdownDescriptorExtractor { return descriptorFromInlines( style, heading.inlines, - textAlign: - MarkdownInlineBuilder.resolvedInlineTextAlign(heading.inlines), + textAlign: MarkdownInlineBuilder.resolvedInlineTextAlign( + heading.inlines, + ), inlineCodeStyle: effectiveInlineCodeStyle, linkStyle: effectiveLinkStyle, ); @@ -170,8 +168,9 @@ class MarkdownDescriptorExtractor { return descriptorFromInlines( effectiveParagraphStyle, paragraph.inlines, - textAlign: - MarkdownInlineBuilder.resolvedInlineTextAlign(paragraph.inlines), + textAlign: MarkdownInlineBuilder.resolvedInlineTextAlign( + paragraph.inlines, + ), inlineCodeStyle: effectiveInlineCodeStyle, linkStyle: effectiveLinkStyle, ); @@ -230,14 +229,12 @@ class MarkdownDescriptorExtractor { }) { final itemDescriptors = [ for (var index = 0; index < block.items.length; index++) - if (_buildListItemDescriptor( + ?_buildListItemDescriptor( block, index, indentLevel: indentLevel, paragraphStyle: paragraphStyle, - ) - case final descriptor?) - descriptor, + ), ]; return joinSelectableTextDescriptors( itemDescriptors, @@ -251,8 +248,10 @@ class MarkdownDescriptorExtractor { ) { final itemDescriptors = []; for (final item in block.items) { - final termDescriptor = - descriptorFromInlines(definitionTermStyle, item.term); + final termDescriptor = descriptorFromInlines( + definitionTermStyle, + item.term, + ); final definitionDescriptors = []; for (final definition in item.definitions) { final childDescriptors = []; @@ -325,11 +324,7 @@ class MarkdownDescriptorExtractor { TextStyle? paragraphStyle, }) { final effectiveParagraphStyle = paragraphStyle ?? theme.bodyStyle; - final prefix = listItemPrefixText( - block, - index, - indentLevel: indentLevel, - ); + final prefix = listItemPrefixText(block, index, indentLevel: indentLevel); final contentDescriptor = buildListItemSelectableDescriptor( block.items[index], indentLevel: indentLevel + 1, @@ -400,8 +395,8 @@ class MarkdownDescriptorExtractor { final styleFontSize = style.fontSize; final scaledFontSize = bodyFontSize == null || contextFontSize == null || styleFontSize == null - ? styleFontSize ?? contextFontSize - : styleFontSize * contextFontSize / bodyFontSize; + ? styleFontSize ?? contextFontSize + : styleFontSize * contextFontSize / bodyFontSize; return style.copyWith( color: textStyleContext.color, fontSize: scaledFontSize, @@ -444,15 +439,17 @@ class MarkdownDescriptorExtractor { for (final row in block.rows) { final cellDescriptors = []; for (final cell in row.cells) { - cellDescriptors.add(descriptorFromInlines( - row.isHeader ? tableHeaderStyle : bodyStyle, - cell.inlines, - textAlign: MarkdownInlineBuilder.resolvedInlineTextAlign( + cellDescriptors.add( + descriptorFromInlines( + row.isHeader ? tableHeaderStyle : bodyStyle, cell.inlines, + textAlign: MarkdownInlineBuilder.resolvedInlineTextAlign( + cell.inlines, + ), + inlineCodeStyle: inlineCodeStyle, + linkStyle: linkStyle, ), - inlineCodeStyle: inlineCodeStyle, - linkStyle: linkStyle, - )); + ); } rowDescriptors.add( joinSelectableTextDescriptors( @@ -476,10 +473,8 @@ class MarkdownDescriptorExtractor { TextAlign textAlign = TextAlign.start, }) { return _descriptorFromSpan( - buildMarkdownPretextSpan( - runs: runs, - fallbackStyle: fallbackStyle, - ) as TextSpan, + buildMarkdownPretextSpan(runs: runs, fallbackStyle: fallbackStyle) + as TextSpan, plainText, pretext: PretextTextDescriptor( runs: runs, @@ -537,8 +532,9 @@ class MarkdownDescriptorExtractor { required String separator, required TextStyle separatorStyle, }) { - final nonEmpty = - descriptors.where((descriptor) => !descriptor.isEmpty).toList(); + final nonEmpty = descriptors + .where((descriptor) => !descriptor.isEmpty) + .toList(); if (nonEmpty.isEmpty) { return plainTextDescriptor('', separatorStyle); } @@ -560,7 +556,8 @@ class MarkdownDescriptorExtractor { for (var index = 0; index < nonEmpty.length; index++) { if (index > 0 && separator.isNotEmpty) { runs.add( - MarkdownPretextInlineRun(text: separator, style: separatorStyle)); + MarkdownPretextInlineRun(text: separator, style: separatorStyle), + ); } runs.addAll(nonEmpty[index].pretext!.runs); } @@ -651,8 +648,9 @@ class MarkdownDescriptorExtractor { var atLineStart = true; void writePrefix({required bool trimTrailing}) { - final prefixText = - trimTrailing ? pendingPrefix.trimRight() : pendingPrefix; + final prefixText = trimTrailing + ? pendingPrefix.trimRight() + : pendingPrefix; if (prefixText.isNotEmpty) { prefixedRuns.add( MarkdownPretextInlineRun(text: prefixText, style: prefixStyle), @@ -682,8 +680,9 @@ class MarkdownDescriptorExtractor { if (atLineStart) { writePrefix(trimTrailing: true); } - prefixedRuns - .add(MarkdownPretextInlineRun(text: '\n', style: run.style)); + prefixedRuns.add( + MarkdownPretextInlineRun(text: '\n', style: run.style), + ); buffer.write('\n'); atLineStart = true; pendingPrefix = continuationPrefix; diff --git a/packages/mixin_markdown_widget/lib/src/render/selection/markdown_selectable_leaf.dart b/packages/mixin_markdown_widget/lib/src/render/selection/markdown_selectable_leaf.dart index bf59ea76..4d917eca 100644 --- a/packages/mixin_markdown_widget/lib/src/render/selection/markdown_selectable_leaf.dart +++ b/packages/mixin_markdown_widget/lib/src/render/selection/markdown_selectable_leaf.dart @@ -24,8 +24,11 @@ enum MarkdownSelectionSlotRole { class MarkdownSelectionLeafRegistry { final Map _leaves = {}; - final Map<(PathInBlock, MarkdownSelectionSlotRole), - _MarkdownSelectionSlotState> _slots = + final Map< + (PathInBlock, MarkdownSelectionSlotRole), + _MarkdownSelectionSlotState + > + _slots = <(PathInBlock, MarkdownSelectionSlotRole), _MarkdownSelectionSlotState>{}; MarkdownSelectableLeafState? leaf(PathInBlock path) => _leaves[path]; @@ -356,10 +359,10 @@ class MarkdownTextLeafGeometry extends MarkdownLeafGeometry { ); return markdownPretextPlainOffsetForRenderOffset(runs, position.offset); } - return _layout(context, box.size.width).textOffsetAt( - localPosition, - textDirection: Directionality.of(context), - ); + return _layout( + context, + box.size.width, + ).textOffsetAt(localPosition, textDirection: Directionality.of(context)); } @override @@ -380,7 +383,7 @@ class MarkdownTextLeafGeometry extends MarkdownLeafGeometry { return localPosition == null ? layout.lineRangeForTextOffset(offset) : layout.visualLineRangeForLocalPosition(localPosition) ?? - layout.lineRangeForTextOffset(offset); + layout.lineRangeForTextOffset(offset); } /// Pretext geometry of the runs at [maxWidth], for when no live paragraph @@ -413,10 +416,10 @@ class MarkdownTextLeafGeometry extends MarkdownLeafGeometry { } int layoutOffsetAt(BuildContext context, double maxWidth, Offset position) { - return _layout(context, maxWidth).textOffsetAt( - position, - textDirection: Directionality.of(context), - ); + return _layout( + context, + maxWidth, + ).textOffsetAt(position, textDirection: Directionality.of(context)); } ({int start, int end})? layoutLineRangeAt( @@ -429,7 +432,7 @@ class MarkdownTextLeafGeometry extends MarkdownLeafGeometry { return localPosition == null ? layout.lineRangeForTextOffset(offset) : layout.visualLineRangeForLocalPosition(localPosition) ?? - layout.lineRangeForTextOffset(offset); + layout.lineRangeForTextOffset(offset); } RenderParagraph? _paragraphIn(RenderBox box) { @@ -508,10 +511,10 @@ class MarkdownTextLeafGeometry extends MarkdownLeafGeometry { preferEnd: false, ) : paragraph - .getPositionForOffset( - paragraph.globalToLocal(box.localToGlobal(localPosition)), - ) - .offset; + .getPositionForOffset( + paragraph.globalToLocal(box.localToGlobal(localPosition)), + ) + .offset; final boundary = _paragraphLineBoundary(paragraph, renderOffset); if (boundary == null) { return null; @@ -668,10 +671,10 @@ List<_LineExtent> _paragraphLineExtents(RenderParagraph paragraph) { contiguousLines.add(( top: previous != null ? (previous.baseline - - previous.ascent + - previous.height + - currentTop) / - 2.0 + previous.ascent + + previous.height + + currentTop) / + 2.0 : currentTop, bottom: next != null ? (currentBottom + next.baseline - next.ascent) / 2.0 @@ -725,22 +728,24 @@ List _normalizeBoxesToLineExtents( if (lineExtents.isEmpty) { return boxes; } - return boxes.map((box) { - for (final extent in lineExtents) { - final overlapTop = math.max(box.top, extent.top); - final overlapBottom = math.min(box.bottom, extent.bottom); - if (overlapBottom - overlapTop > 0.5) { - return TextBox.fromLTRBD( - box.left, - extent.top, - box.right, - extent.bottom, - box.direction, - ); - } - } - return box; - }).toList(growable: false); + return boxes + .map((box) { + for (final extent in lineExtents) { + final overlapTop = math.max(box.top, extent.top); + final overlapBottom = math.min(box.bottom, extent.bottom); + if (overlapBottom - overlapTop > 0.5) { + return TextBox.fromLTRBD( + box.left, + extent.top, + box.right, + extent.bottom, + box.direction, + ); + } + } + return box; + }) + .toList(growable: false); } /// Merges the boxes of each line into one box spanning them. @@ -762,7 +767,8 @@ List _mergeSameLineBoxes(List boxes) { final merged = []; var current = sorted.first; for (final next in sorted.skip(1)) { - final sameLine = (next.top - current.top).abs() <= lineTolerance && + final sameLine = + (next.top - current.top).abs() <= lineTolerance && (next.bottom - current.bottom).abs() <= lineTolerance; if (sameLine) { current = TextBox.fromLTRBD( diff --git a/packages/mixin_markdown_widget/lib/src/render/selection/markdown_selection_geometry.dart b/packages/mixin_markdown_widget/lib/src/render/selection/markdown_selection_geometry.dart index aec77c76..f7e44af8 100644 --- a/packages/mixin_markdown_widget/lib/src/render/selection/markdown_selection_geometry.dart +++ b/packages/mixin_markdown_widget/lib/src/render/selection/markdown_selection_geometry.dart @@ -58,7 +58,8 @@ abstract final class MarkdownContainerSelectionGeometry { return null; } final query = localPosition == null ? null : _Query.of(context, block); - final unit = query?.unitAt(root, localPosition!) ?? + final unit = + query?.unitAt(root, localPosition!) ?? _modelUnitAt(root, position.textOffset); if (unit == null) { return null; @@ -94,8 +95,8 @@ sealed class _Node { final class _LeafNode extends _Node { _LeafNode(super.path, this.leaf, {required this.leadsText}) - : start = leaf.displayedOffsetForLocalOffset(0), - end = leaf.segmentEnd; + : start = leaf.displayedOffsetForLocalOffset(0), + end = leaf.segmentEnd; final StructuredBlockSelectionLeaf leaf; @@ -183,12 +184,9 @@ class _Tree { _Node? _treeFor(BlockNode block) { return (_trees[block] ??= _Tree( - _TreeBuilder(StructuredBlockSelection.forBlock(block)).build( - block, - const [], - ), - )) - .root; + _TreeBuilder(StructuredBlockSelection.forBlock(block)) + .build(block, const []), + )).root; } /// Builds the selection tree of a block from its model, following the same @@ -215,7 +213,8 @@ class _TreeBuilder { return _LeafNode( PathInBlock(path), leaf, - leadsText: block.kind == MarkdownBlockKind.paragraph || + leadsText: + block.kind == MarkdownBlockKind.paragraph || block.kind == MarkdownBlockKind.heading, ); case MarkdownBlockKind.quote: @@ -251,11 +250,14 @@ class _TreeBuilder { final table = block as TableBlock; final cells = <_LeafNode>[ for (var row = 0; row < table.rows.length; row++) - for (var column = 0; - column < table.rows[row].cells.length; - column++) - if (structure - .leafForPath(PathInBlock([...path, row, column])) + for ( + var column = 0; + column < table.rows[row].cells.length; + column++ + ) + if (structure.leafForPath( + PathInBlock([...path, row, column]), + ) case final leaf?) _LeafNode(leaf.path, leaf, leadsText: false), ]; @@ -271,8 +273,9 @@ class _TreeBuilder { _Node? _list(ListBlock list, List path) { final items = <_ListItemNode>[]; for (var index = 0; index < list.items.length; index++) { - final marker = - structure.leafForPath(PathInBlock([...path, index, 0])); + final marker = structure.leafForPath( + PathInBlock([...path, index, 0]), + ); if (marker == null) { continue; } @@ -280,8 +283,11 @@ class _TreeBuilder { _Node? leadTextChild; final children = <_Node>[]; for (var child = 0; child < item.children.length; child++) { - final node = - build(item.children[child], [...path, index, child + 1]); + final node = build(item.children[child], [ + ...path, + index, + child + 1, + ]); if (node == null) { continue; } @@ -454,21 +460,23 @@ class _Query { continue; } - final contentRect = - _slotRect(item.path, MarkdownSelectionSlotRole.listContent); + final contentRect = _slotRect( + item.path, + MarkdownSelectionSlotRole.listContent, + ); final rowRect = _slotRect(item.path, MarkdownSelectionSlotRole.listRow); final contentRects = contentSelectionStart < contentSelectionEnd && contentRect != null - ? _childrenRects( - item.children, - contentSelectionStart, - contentSelectionEnd, - ) - : const []; + ? _childrenRects( + item.children, + contentSelectionStart, + contentSelectionEnd, + ) + : const []; final prefixRect = prefixSelectionStart < prefixSelectionEnd && rowRect != null - ? _listPrefixRect(list, item, rowRect, contentRect) - : null; + ? _listPrefixRect(list, item, rowRect, contentRect) + : null; if (contentSelectionStart >= contentSelectionEnd || contentRect == null) { if (prefixRect != null) { @@ -526,11 +534,7 @@ class _Query { Rect? _firstLineRect(_Node node) { final rects = this - .rects( - node, - node.start, - math.min(node.start + 1, node.end), - ) + .rects(node, node.start, math.min(node.start + 1, node.end)) .toList(); if (rects.isEmpty) { return null; @@ -632,7 +636,8 @@ class _Query { if (nearest == null || nearestRect == null) { return null; } - final preferEnd = position.dy > nearestRect.center.dy || + final preferEnd = + position.dy > nearestRect.center.dy || position.dx > nearestRect.center.dx; return preferEnd ? nearest.end : nearest.start; } @@ -643,8 +648,10 @@ class _Query { var bestDistance = double.infinity; for (final item in list.items) { - final contentRect = - _slotRect(item.path, MarkdownSelectionSlotRole.listContent); + final contentRect = _slotRect( + item.path, + MarkdownSelectionSlotRole.listContent, + ); int? childOffset; if (contentRect != null) { final childContainsPosition = item.children.any( @@ -706,7 +713,8 @@ class _Query { if (nearest == null || nearestRowRect == null) { return null; } - final preferEnd = position.dy > nearestRowRect.center.dy || + final preferEnd = + position.dy > nearestRowRect.center.dy || position.dx > nearestRowRect.center.dx; return preferEnd ? nearest.end : nearest.start; } @@ -721,14 +729,13 @@ class _Query { } /// The cell whose text is nearest to [position]. - ({ - _LeafNode node, - ({MarkdownSelectableLeafState state, Offset origin}) leaf, - })? _nearestCell(_TableNode table, Offset position) { + ({_LeafNode node, ({MarkdownSelectableLeafState state, Offset origin}) leaf})? + _nearestCell(_TableNode table, Offset position) { ({ _LeafNode node, ({MarkdownSelectableLeafState state, Offset origin}) leaf, - })? nearest; + })? + nearest; var bestDistance = double.infinity; for (final cell in table.cells) { final leaf = _leaf(cell); @@ -790,12 +797,15 @@ class _Query { return (start: item.start, end: item.end); } - final contentRect = - _slotRect(item.path, MarkdownSelectionSlotRole.listContent); + final contentRect = _slotRect( + item.path, + MarkdownSelectionSlotRole.listContent, + ); if (contentRect == null || !contentRect.contains(position)) { continue; } - final child = _childContaining(item.children, position) ?? + final child = + _childContaining(item.children, position) ?? _childContaining( item.children, _projectIntoContent(contentRect, position), @@ -834,12 +844,12 @@ double _distanceToRect(Offset point, Rect rect) { final dx = point.dx < rect.left ? rect.left - point.dx : point.dx > rect.right - ? point.dx - rect.right - : 0.0; + ? point.dx - rect.right + : 0.0; final dy = point.dy < rect.top ? rect.top - point.dy : point.dy > rect.bottom - ? point.dy - rect.bottom - : 0.0; + ? point.dy - rect.bottom + : 0.0; return dx * dx + dy * dy; } diff --git a/packages/mixin_markdown_widget/lib/src/render/selection/markdown_selection_gesture_detector.dart b/packages/mixin_markdown_widget/lib/src/render/selection/markdown_selection_gesture_detector.dart index 1d7f5f17..4ad6f7ef 100644 --- a/packages/mixin_markdown_widget/lib/src/render/selection/markdown_selection_gesture_detector.dart +++ b/packages/mixin_markdown_widget/lib/src/render/selection/markdown_selection_gesture_detector.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'dart:math' as math; + import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import '../../core/document.dart'; import '../../selection/selection_controller.dart'; @@ -67,7 +68,7 @@ class MarkdownSelectionGestureDetector extends StatefulWidget { final MarkdownSelectBlockCallback selectBlockAt; final MarkdownSelectSelectionUnitCallback selectSelectionUnitAt; final Iterable Function()? - additionalAutoScrollTargets; + additionalAutoScrollTargets; @override State createState() => @@ -97,8 +98,9 @@ class _MarkdownSelectionGestureDetectorState final candidates = <_AutoScrollCandidate>[]; final seenPositions = {}; - for (final target in widget.additionalAutoScrollTargets?.call() ?? - const []) { + for (final target + in widget.additionalAutoScrollTargets?.call() ?? + const []) { final candidate = _candidateForScrollable( position: target.scrollController.hasClients ? target.scrollController.position @@ -238,7 +240,8 @@ class _MarkdownSelectionGestureDetectorState if (!_isDraggingSelection) { return; } - final shouldClearSelection = _clearSelectionOnPointerUp && + final shouldClearSelection = + _clearSelectionOnPointerUp && _dragStartPointerPosition != null && (event.position - _dragStartPointerPosition!).distance < kTouchSlop; _isDraggingSelection = false; @@ -310,9 +313,10 @@ class _MarkdownSelectionGestureDetectorState continue; } final velocity = autoScroll.velocity; - final nextOffset = (position.pixels + - velocity * _autoScrollTickInterval.inMilliseconds / 1000) - .clamp(position.minScrollExtent, position.maxScrollExtent); + final nextOffset = + (position.pixels + + velocity * _autoScrollTickInterval.inMilliseconds / 1000) + .clamp(position.minScrollExtent, position.maxScrollExtent); if ((nextOffset - position.pixels).abs() < 0.5) { continue; } @@ -345,13 +349,17 @@ class _MarkdownSelectionGestureDetectorState final bestMatches = {}; for (final candidate in _autoScrollCandidates()) { - final velocity = - _autoScrollVelocityForCandidate(candidate, globalPosition); + final velocity = _autoScrollVelocityForCandidate( + candidate, + globalPosition, + ); if (velocity == 0) { continue; } - final resolved = - _ResolvedAutoScroll(candidate: candidate, velocity: velocity); + final resolved = _ResolvedAutoScroll( + candidate: candidate, + velocity: velocity, + ); final existing = bestMatches[candidate.axis]; if (existing == null || _isPreferredAutoScrollCandidate(resolved, existing)) { @@ -383,7 +391,8 @@ class _MarkdownSelectionGestureDetectorState case Axis.vertical: if (globalPosition.dy < viewportRect.top + _autoScrollActivationZone && position.pixels > position.minScrollExtent) { - final proximity = 1 - + final proximity = + 1 - ((globalPosition.dy - viewportRect.top) / _autoScrollActivationZone) .clamp(0.0, 1.0); @@ -392,16 +401,18 @@ class _MarkdownSelectionGestureDetectorState if (globalPosition.dy > viewportRect.bottom - _autoScrollActivationZone && position.pixels < position.maxScrollExtent) { - final proximity = ((globalPosition.dy - - (viewportRect.bottom - _autoScrollActivationZone)) / - _autoScrollActivationZone) - .clamp(0.0, 1.0); + final proximity = + ((globalPosition.dy - + (viewportRect.bottom - _autoScrollActivationZone)) / + _autoScrollActivationZone) + .clamp(0.0, 1.0); return _autoScrollSpeedForProximity(proximity); } case Axis.horizontal: if (globalPosition.dx < viewportRect.left + _autoScrollActivationZone && position.pixels > position.minScrollExtent) { - final proximity = 1 - + final proximity = + 1 - ((globalPosition.dx - viewportRect.left) / _autoScrollActivationZone) .clamp(0.0, 1.0); @@ -410,10 +421,11 @@ class _MarkdownSelectionGestureDetectorState if (globalPosition.dx > viewportRect.right - _autoScrollActivationZone && position.pixels < position.maxScrollExtent) { - final proximity = ((globalPosition.dx - - (viewportRect.right - _autoScrollActivationZone)) / - _autoScrollActivationZone) - .clamp(0.0, 1.0); + final proximity = + ((globalPosition.dx - + (viewportRect.right - _autoScrollActivationZone)) / + _autoScrollActivationZone) + .clamp(0.0, 1.0); return _autoScrollSpeedForProximity(proximity); } } @@ -520,8 +532,8 @@ class _MarkdownSelectionGestureDetectorState if (!mounted) { return null; } - final renderObject = - widget.scrollableKey.currentContext?.findRenderObject(); + final renderObject = widget.scrollableKey.currentContext + ?.findRenderObject(); if (renderObject is! RenderBox || !renderObject.hasSize) { return null; } @@ -571,10 +583,7 @@ class _AutoScrollCandidate { } class _ResolvedAutoScroll { - const _ResolvedAutoScroll({ - required this.candidate, - required this.velocity, - }); + const _ResolvedAutoScroll({required this.candidate, required this.velocity}); final _AutoScrollCandidate candidate; final double velocity; diff --git a/packages/mixin_markdown_widget/lib/src/render/shortcuts/markdown_shortcuts_scope.dart b/packages/mixin_markdown_widget/lib/src/render/shortcuts/markdown_shortcuts_scope.dart index d8a9d91f..475866d6 100644 --- a/packages/mixin_markdown_widget/lib/src/render/shortcuts/markdown_shortcuts_scope.dart +++ b/packages/mixin_markdown_widget/lib/src/render/shortcuts/markdown_shortcuts_scope.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter/services.dart'; import '../../core/document.dart'; @@ -37,49 +37,37 @@ class MarkdownShortcutsScope extends StatelessWidget { @override Widget build(BuildContext context) { final shortcuts = { + const SingleActivator(LogicalKeyboardKey.keyC, control: true): + const CopySelectionPlainTextIntent(), + const SingleActivator(LogicalKeyboardKey.keyC, meta: true): + const CopySelectionPlainTextIntent(), + const SingleActivator(LogicalKeyboardKey.keyA, control: true): + const SelectAllMarkdownIntent(), + const SingleActivator(LogicalKeyboardKey.keyA, meta: true): + const SelectAllMarkdownIntent(), const SingleActivator( LogicalKeyboardKey.keyC, control: true, - ): const CopySelectionPlainTextIntent(), - const SingleActivator( - LogicalKeyboardKey.keyC, - meta: true, - ): const CopySelectionPlainTextIntent(), - const SingleActivator( - LogicalKeyboardKey.keyA, - control: true, - ): const SelectAllMarkdownIntent(), - const SingleActivator( - LogicalKeyboardKey.keyA, - meta: true, - ): const SelectAllMarkdownIntent(), - const SingleActivator( - LogicalKeyboardKey.keyC, - control: true, - shift: true, - ): const CopyFullDocumentPlainTextIntent(), - const SingleActivator( - LogicalKeyboardKey.keyC, - meta: true, shift: true, ): const CopyFullDocumentPlainTextIntent(), + const SingleActivator(LogicalKeyboardKey.keyC, meta: true, shift: true): + const CopyFullDocumentPlainTextIntent(), if (selectionController.hasSelection) - const SingleActivator( - LogicalKeyboardKey.escape, - ): const ClearMarkdownSelectionIntent(), + const SingleActivator(LogicalKeyboardKey.escape): + const ClearMarkdownSelectionIntent(), }; return Actions( actions: >{ CopySelectionPlainTextIntent: CallbackAction( - onInvoke: (intent) { - if (selectionController.hasSelection) { - selectionController.copySelectionToClipboard(); - } - return null; - }, - ), + onInvoke: (intent) { + if (selectionController.hasSelection) { + selectionController.copySelectionToClipboard(); + } + return null; + }, + ), SelectAllMarkdownIntent: CallbackAction( onInvoke: (intent) { selectionController.selectAll(document); @@ -88,23 +76,20 @@ class MarkdownShortcutsScope extends StatelessWidget { ), ClearMarkdownSelectionIntent: CallbackAction( - onInvoke: (intent) { - selectionController.clear(); - return null; - }, - ), + onInvoke: (intent) { + selectionController.clear(); + return null; + }, + ), CopyFullDocumentPlainTextIntent: CallbackAction( - onInvoke: (intent) { - onCopyPlainText?.call(); - return null; - }, - ), + onInvoke: (intent) { + onCopyPlainText?.call(); + return null; + }, + ), }, - child: Shortcuts( - shortcuts: shortcuts, - child: child, - ), + child: Shortcuts(shortcuts: shortcuts, child: child), ); } } diff --git a/packages/mixin_markdown_widget/lib/src/selection/mixin_selection_area.dart b/packages/mixin_markdown_widget/lib/src/selection/mixin_selection_area.dart index 98e08ead..986ef0e6 100644 --- a/packages/mixin_markdown_widget/lib/src/selection/mixin_selection_area.dart +++ b/packages/mixin_markdown_widget/lib/src/selection/mixin_selection_area.dart @@ -1,6 +1,6 @@ import 'dart:math' as math; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:flutter/services.dart'; import '../clipboard/plain_text_serializer.dart'; @@ -47,8 +47,10 @@ class MixinSelectionArea extends StatefulWidget { this.selectionColor, this.contextMenuBuilder = defaultMarkdownContextMenuBuilder, this.enableCopyFullDocumentShortcut = true, - @Deprecated('Filter MarkdownContextMenuDetails.buttonItems in ' - 'contextMenuBuilder instead.') + @Deprecated( + 'Filter MarkdownContextMenuDetails.buttonItems in ' + 'contextMenuBuilder instead.', + ) this.showCopyAllInContextMenu = true, }); @@ -60,8 +62,10 @@ class MixinSelectionArea extends StatefulWidget { /// Builds the right-click menu; null shows none. final MarkdownContextMenuBuilder? contextMenuBuilder; final bool enableCopyFullDocumentShortcut; - @Deprecated('Filter MarkdownContextMenuDetails.buttonItems in ' - 'contextMenuBuilder instead.') + @Deprecated( + 'Filter MarkdownContextMenuDetails.buttonItems in ' + 'contextMenuBuilder instead.', + ) final bool showCopyAllInContextMenu; @override @@ -71,8 +75,9 @@ class MixinSelectionArea extends StatefulWidget { class _MixinSelectionAreaState extends State { final MarkdownPlainTextSerializer _plainTextSerializer = const MarkdownPlainTextSerializer(); - final GlobalKey _selectionAreaKey = - GlobalKey(debugLabel: 'mixin-selection-area'); + final GlobalKey _selectionAreaKey = GlobalKey( + debugLabel: 'mixin-selection-area', + ); final ScrollController _fallbackScrollController = ScrollController(); final Map _participants = {}; @@ -137,8 +142,9 @@ class _MixinSelectionAreaState extends State { _registrationOrder.remove(owner); _blockIndexOffsets.remove(owner); _blockCounts.remove(owner); - _orderedParticipants - .removeWhere((participant) => participant.owner == owner); + _orderedParticipants.removeWhere( + (participant) => participant.owner == owner, + ); _scheduleDocumentRefresh(); } @@ -209,8 +215,9 @@ class _MixinSelectionAreaState extends State { return leftDelta < 0 ? -1 : 1; } } - return (_registrationOrder[a.owner] ?? 0) - .compareTo(_registrationOrder[b.owner] ?? 0); + return (_registrationOrder[a.owner] ?? 0).compareTo( + _registrationOrder[b.owner] ?? 0, + ); } String? _linkDestinationAt(Offset globalPosition) { @@ -253,13 +260,13 @@ class _MixinSelectionAreaState extends State { final dx = globalPosition.dx < rect.left ? rect.left - globalPosition.dx : globalPosition.dx > rect.right - ? globalPosition.dx - rect.right - : 0.0; + ? globalPosition.dx - rect.right + : 0.0; final dy = globalPosition.dy < rect.top ? rect.top - globalPosition.dy : globalPosition.dy > rect.bottom - ? globalPosition.dy - rect.bottom - : 0.0; + ? globalPosition.dy - rect.bottom + : 0.0; final distance = dx * dx + dy * dy; if (distance < bestDistance) { bestDistance = distance; @@ -328,15 +335,15 @@ class _MixinSelectionAreaState extends State { @override Widget build(BuildContext context) { - final selectionColor = widget.selectionColor ?? + final selectionColor = + widget.selectionColor ?? TextSelectionTheme.of(context).selectionColor ?? MarkdownTheme.of(context).selectionColor; _controller.attachDocument(_document); return TextSelectionTheme( - data: TextSelectionTheme.of(context).copyWith( - selectionColor: selectionColor, - ), + data: TextSelectionTheme.of(context) + .copyWith(selectionColor: selectionColor), child: AnimatedBuilder( animation: _controller, builder: (context, _) { @@ -370,10 +377,7 @@ class _MixinSelectionAreaState extends State { selectBlockAt: _selectBlockAt, selectSelectionUnitAt: _selectSelectionUnitAt, additionalAutoScrollTargets: _autoScrollTargets, - child: KeyedSubtree( - key: _selectionAreaKey, - child: scopedChild, - ), + child: KeyedSubtree(key: _selectionAreaKey, child: scopedChild), ); }, ), @@ -479,9 +483,7 @@ class _MixinSelectableState extends State { return [ ParagraphBlock( id: 'mixin-selection-$blockIndex-${id.hashCode}', - inlines: [ - TextInline(text: widget.plainText), - ], + inlines: [TextInline(text: widget.plainText)], ), ]; }, @@ -564,8 +566,10 @@ class _MixinSelectableState extends State { DocumentPosition position, { Offset? globalPosition, }) { - return _blockKey.currentState - ?.selectWord(position, globalPosition: globalPosition) ?? + return _blockKey.currentState?.selectWord( + position, + globalPosition: globalPosition, + ) ?? DocumentSelection(base: position, extent: position); } @@ -595,8 +599,9 @@ class _MixinSelectableState extends State { } final blockIndex = registrar.blockIndexOffsetOf(this); - final selectionRange = - blockIndex == null ? null : registrar.controller.normalizedRange; + final selectionRange = blockIndex == null + ? null + : registrar.controller.normalizedRange; return SelectableMarkdownBlock( key: _blockKey, blockIndex: blockIndex ?? 0, @@ -677,9 +682,11 @@ class _MixinSelectableRowState extends State { var textOffset = 0; var hasVisualChildSinceLastText = false; - for (var childIndex = 0; - childIndex < widget.children.length; - childIndex++) { + for ( + var childIndex = 0; + childIndex < widget.children.length; + childIndex++ + ) { final child = widget.children[childIndex]; if (rowChildren.isNotEmpty && widget.spacing > 0) { rowChildren.add(SizedBox(width: widget.spacing)); @@ -868,7 +875,7 @@ class _MixinSelectableRowState extends State { required DocumentRange range, required List<_MixinSelectableRowTextBridge> bridges, required Map - resolvedSegmentsByIndex, + resolvedSegmentsByIndex, }) sync* { for (final bridge in bridges) { if (range.start.textOffset >= bridge.endOffset || @@ -1094,10 +1101,7 @@ class MixinSelectableText extends StatelessWidget { MarkdownPretextInlineRun(text: data, style: effectiveStyle), ]; - MarkdownPretextLayoutResult resolveLayout( - BuildContext context, - Size size, - ) { + MarkdownPretextLayoutResult resolveLayout(BuildContext context, Size size) { final textScaler = MediaQuery.maybeTextScalerOf(context) ?? TextScaler.noScaling; return computeMarkdownPretextLayoutFromRuns( @@ -1138,7 +1142,7 @@ class MixinSelectableText extends StatelessWidget { final lineRange = localPosition == null ? layout.lineRangeForTextOffset(position.textOffset) : layout.visualLineRangeForLocalPosition(localPosition) ?? - layout.lineRangeForTextOffset(position.textOffset); + layout.lineRangeForTextOffset(position.textOffset); if (lineRange == null) { return null; } diff --git a/packages/mixin_markdown_widget/lib/src/selection/selection_controller.dart b/packages/mixin_markdown_widget/lib/src/selection/selection_controller.dart index 9223ab57..2df2f2d6 100644 --- a/packages/mixin_markdown_widget/lib/src/selection/selection_controller.dart +++ b/packages/mixin_markdown_widget/lib/src/selection/selection_controller.dart @@ -5,9 +5,8 @@ import '../clipboard/plain_text_serializer.dart'; import '../core/document.dart'; class MarkdownSelectionController extends ChangeNotifier { - MarkdownSelectionController({ - MarkdownPlainTextSerializer? serializer, - }) : _serializer = serializer ?? const MarkdownPlainTextSerializer(); + MarkdownSelectionController({MarkdownPlainTextSerializer? serializer}) + : _serializer = serializer ?? const MarkdownPlainTextSerializer(); final MarkdownPlainTextSerializer _serializer; diff --git a/packages/mixin_markdown_widget/lib/src/selection/selection_host.dart b/packages/mixin_markdown_widget/lib/src/selection/selection_host.dart index 70d5aeb6..51f8c8b4 100644 --- a/packages/mixin_markdown_widget/lib/src/selection/selection_host.dart +++ b/packages/mixin_markdown_widget/lib/src/selection/selection_host.dart @@ -1,6 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter/services.dart'; import '../core/document.dart'; @@ -45,7 +45,7 @@ class MarkdownSelectionHost extends StatefulWidget { final bool showCopyAllInContextMenu; final VoidCallback? onCopyPlainText; final Iterable Function()? - additionalAutoScrollTargets; + additionalAutoScrollTargets; final bool isSelectable; @override @@ -53,8 +53,9 @@ class MarkdownSelectionHost extends StatefulWidget { } class _MarkdownSelectionHostState extends State { - final FocusNode _selectionFocusNode = - FocusNode(debugLabel: 'mixin_markdown_widget.selection'); + final FocusNode _selectionFocusNode = FocusNode( + debugLabel: 'mixin_markdown_widget.selection', + ); final ContextMenuController _contextMenuController = ContextMenuController(); ScrollPosition? _ancestorScrollPosition; @@ -109,10 +110,8 @@ class _MarkdownSelectionHostState extends State { _contextMenuController.show( context: context, // Clicks on the menu are not outside the selection. - contextMenuBuilder: (context) => TapRegion( - groupId: this, - child: builder(context, details), - ), + contextMenuBuilder: (context) => + TapRegion(groupId: this, child: builder(context, details)), ); } @@ -158,9 +157,9 @@ class _MarkdownSelectionHostState extends State { final range = selectionController.normalizedRange; final onCopyPlainText = widget.onCopyPlainText; VoidCallback hideAnd(VoidCallback action) => () { - _hideContextMenu(); - action(); - }; + _hideContextMenu(); + action(); + }; return [ if (range != null && range.start != range.end) ContextMenuButtonItem( @@ -176,8 +175,9 @@ class _MarkdownSelectionHostState extends State { ), ContextMenuButtonItem( type: ContextMenuButtonType.selectAll, - onPressed: - hideAnd(() => selectionController.selectAll(widget.document)), + onPressed: hideAnd( + () => selectionController.selectAll(widget.document), + ), ), if (onCopyPlainText != null && widget.showCopyAllInContextMenu) ContextMenuButtonItem( diff --git a/packages/mixin_markdown_widget/lib/src/selection/selection_registrar.dart b/packages/mixin_markdown_widget/lib/src/selection/selection_registrar.dart index 5bdabc37..ecf32aef 100644 --- a/packages/mixin_markdown_widget/lib/src/selection/selection_registrar.dart +++ b/packages/mixin_markdown_widget/lib/src/selection/selection_registrar.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import '../core/document.dart'; import '../render/selection/markdown_selection_gesture_detector.dart'; @@ -18,9 +18,7 @@ typedef MixinSelectionParticipantSelectWord = void Function( Offset? globalPosition, }); -typedef MixinSelectionParticipantSelectBlock = void Function( - int blockIndex, -); +typedef MixinSelectionParticipantSelectBlock = void Function(int blockIndex); typedef MixinSelectionParticipantSelectUnit = void Function( Offset globalPosition, @@ -51,7 +49,7 @@ class MixinSelectionParticipant { final MixinSelectionParticipantSelectUnit selectSelectionUnitAt; final String? Function(Offset globalPosition)? linkDestinationAt; final Iterable Function()? - autoScrollTargets; + autoScrollTargets; } class MixinSelectionRegistrar extends InheritedWidget { @@ -75,7 +73,7 @@ class MixinSelectionRegistrar extends InheritedWidget { final int documentVersion; final DocumentSelection? selection; final void Function(MixinSelectionParticipant participant) - registerParticipant; + registerParticipant; final void Function(Object owner) unregisterParticipant; final void Function(Object owner) participantChanged; final int? Function(Object owner) blockIndexOffsetOf; diff --git a/packages/mixin_markdown_widget/lib/src/selection/structured_block_selection.dart b/packages/mixin_markdown_widget/lib/src/selection/structured_block_selection.dart index 6881f73d..982f5d32 100644 --- a/packages/mixin_markdown_widget/lib/src/selection/structured_block_selection.dart +++ b/packages/mixin_markdown_widget/lib/src/selection/structured_block_selection.dart @@ -2,10 +2,7 @@ import 'package:flutter/foundation.dart'; import '../core/document.dart'; -enum StructuredSelectionAffinity { - upstream, - downstream, -} +enum StructuredSelectionAffinity { upstream, downstream } @immutable class StructuredBlockSelectionLeaf { @@ -53,8 +50,9 @@ class StructuredBlockSelectionLeaf { } int localOffsetForDisplayedOffset(int displayedOffset) { - final clampedDisplayed = - displayedOffset.clamp(segmentStart, segmentEnd).toInt(); + final clampedDisplayed = displayedOffset + .clamp(segmentStart, segmentEnd) + .toInt(); var relative = clampedDisplayed - segmentStart; if (relative <= leadingText.length + firstLinePrefix.length) { return 0; @@ -100,8 +98,9 @@ LinkInline? _linkContaining(List inlines, int offset) { StrikethroughInline(:final children) || HighlightInline(:final children) || SubscriptInline(:final children) || - SuperscriptInline(:final children) => - _linkContaining(children, offset - start), + SuperscriptInline( + :final children, + ) => _linkContaining(children, offset - start), _ => null, }; } @@ -122,8 +121,9 @@ class StructuredBlockSelection { // and shared by the renderer, the controller, and the serializer. static final Expando _cache = Expando('StructuredBlockSelection'); - static final Expando _serializedBlockTextCache = - Expando('serializedBlockText'); + static final Expando _serializedBlockTextCache = Expando( + 'serializedBlockText', + ); factory StructuredBlockSelection.forBlock(BlockNode block) { return _cache[block] ??= StructuredBlockSelection._fromBlueprints( @@ -179,8 +179,8 @@ class StructuredBlockSelection { late final Map _leavesByPath = { - for (final leaf in leaves) leaf.path: leaf, - }; + for (final leaf in leaves) leaf.path: leaf, + }; bool get isEmpty => leaves.isEmpty; @@ -224,8 +224,9 @@ class StructuredBlockSelection { StructuredSelectionAffinity affinity = StructuredSelectionAffinity.downstream, }) { - final clampedOffset = - position.textOffset.clamp(0, plainText.length).toInt(); + final clampedOffset = position.textOffset + .clamp(0, plainText.length) + .toInt(); final leaf = leafForPath(position.path); if (leaf != null && clampedOffset >= leaf.segmentStart && @@ -328,9 +329,10 @@ class StructuredBlockSelection { required int blockIndex, required DocumentPosition position, required ({int start, int end}) Function(String text, int offset) - wordRangeAt, + wordRangeAt, }) { - final leaf = leafForPath(position.path) ?? + final leaf = + leafForPath(position.path) ?? _leafContainingOffset(position.textOffset); if (leaf == null || leaf.visibleText.isEmpty) { return null; @@ -382,20 +384,17 @@ class StructuredBlockSelection { return leaves.isEmpty ? null : leaves.last; } - String serializeDisplayedRange({ - required int start, - required int end, - }) { + String serializeDisplayedRange({required int start, required int end}) { final lower = start < 0 ? 0 : start > plainText.length - ? plainText.length - : start; + ? plainText.length + : start; final upper = end < 0 ? 0 : end > plainText.length - ? plainText.length - : end; + ? plainText.length + : end; if (lower >= upper || leaves.isEmpty) { return ''; } @@ -409,8 +408,9 @@ class StructuredBlockSelection { continue; } - final localStart = - lower <= leaf.segmentStart ? 0 : lower - leaf.segmentStart; + final localStart = lower <= leaf.segmentStart + ? 0 + : lower - leaf.segmentStart; final localEnd = upper >= leaf.segmentEnd ? leaf.renderedText.length : upper - leaf.segmentStart; @@ -444,9 +444,7 @@ class _LeafBlueprint { final String exportText; final List inlines; - _LeafBlueprint copyWith({ - String? leadingText, - }) { + _LeafBlueprint copyWith({String? leadingText}) { return _LeafBlueprint( path: path, leadingText: leadingText ?? this.leadingText, @@ -503,8 +501,7 @@ class _StructuredBlockSelectionBuilder { exportText: exportText, inlines: switch (block) { HeadingBlock(:final inlines) || - ParagraphBlock(:final inlines) => - inlines, + ParagraphBlock(:final inlines) => inlines, _ => const [], }, ), @@ -524,8 +521,9 @@ class _StructuredBlockSelectionBuilder { final childLeaves = _buildBlock( details.children[index], pathPrefix: [...pathPrefix, index + 1], - firstLinePrefix: - leaves.isEmpty ? firstLinePrefix : continuationPrefix, + firstLinePrefix: leaves.isEmpty + ? firstLinePrefix + : continuationPrefix, continuationPrefix: continuationPrefix, listIndentLevel: listIndentLevel, ); @@ -545,8 +543,9 @@ class _StructuredBlockSelectionBuilder { final childLeaves = _buildBlock( quote.children[index], pathPrefix: [...pathPrefix, index], - firstLinePrefix: - leaves.isEmpty ? firstLinePrefix : continuationPrefix, + firstLinePrefix: leaves.isEmpty + ? firstLinePrefix + : continuationPrefix, continuationPrefix: continuationPrefix, listIndentLevel: listIndentLevel, ); @@ -605,23 +604,26 @@ class _StructuredBlockSelectionBuilder { final itemLeaves = <_LeafBlueprint>[]; var hasContent = false; - for (var childIndex = 0; - childIndex < item.children.length; - childIndex++) { + for ( + var childIndex = 0; + childIndex < item.children.length; + childIndex++ + ) { final childLeaves = _buildBlock( item.children[childIndex], pathPrefix: [...pathPrefix, itemIndex, childIndex + 1], - firstLinePrefix: - hasContent ? continuationPrefix + ' ' * markerText.length : '', + firstLinePrefix: hasContent + ? continuationPrefix + ' ' * markerText.length + : '', continuationPrefix: continuationPrefix + ' ' * markerText.length, listIndentLevel: item.children[childIndex].kind == MarkdownBlockKind.orderedList || - item.children[childIndex].kind == - MarkdownBlockKind.unorderedList || - item.children[childIndex].kind == - MarkdownBlockKind.footnoteList - ? listIndentLevel + 1 - : listIndentLevel, + item.children[childIndex].kind == + MarkdownBlockKind.unorderedList || + item.children[childIndex].kind == + MarkdownBlockKind.footnoteList + ? listIndentLevel + 1 + : listIndentLevel, ); if (childLeaves.isEmpty) { continue; @@ -642,8 +644,9 @@ class _StructuredBlockSelectionBuilder { _LeafBlueprint( path: PathInBlock([...pathPrefix, itemIndex, 0]), leadingText: '', - firstLinePrefix: - leaves.isEmpty ? firstLinePrefix : continuationPrefix, + firstLinePrefix: leaves.isEmpty + ? firstLinePrefix + : continuationPrefix, continuationPrefix: continuationPrefix, visibleText: hasContent ? markerText : markerText.trimRight(), exportText: hasContent ? markerText : markerText.trimRight(), @@ -669,16 +672,17 @@ class _StructuredBlockSelectionBuilder { for (var rowIndex = 0; rowIndex < block.rows.length; rowIndex++) { final row = block.rows[rowIndex]; for (var columnIndex = 0; columnIndex < row.cells.length; columnIndex++) { - final visibleText = - _flattenVisibleInlines(row.cells[columnIndex].inlines); + final visibleText = _flattenVisibleInlines( + row.cells[columnIndex].inlines, + ); if (visibleText.isEmpty) { continue; } final leadingText = rowIndex == 0 && columnIndex == 0 ? '' : columnIndex == 0 - ? '\n' - : '\t'; + ? '\n' + : '\t'; leaves.add( _LeafBlueprint( path: PathInBlock([...pathPrefix, rowIndex, columnIndex]), @@ -779,8 +783,7 @@ class _StructuredBlockSelectionBuilder { case MarkdownBlockKind.paragraph: return _flattenVisibleInlines((block as ParagraphBlock).inlines); case MarkdownBlockKind.quote: - return (block as QuoteBlock) - .children + return (block as QuoteBlock).children .map(_visibleBlockText) .where((value) => value.trim().isNotEmpty) .join('\n\n'); @@ -793,15 +796,17 @@ class _StructuredBlockSelectionBuilder { return _visibleDetailsText(block as DetailsBlock); case MarkdownBlockKind.footnoteList: return _visibleListText( - _footnoteListAsOrderedList(block as FootnoteListBlock)); + _footnoteListAsOrderedList(block as FootnoteListBlock), + ); case MarkdownBlockKind.codeBlock: return (block as CodeBlock).code; case MarkdownBlockKind.table: - return (block as TableBlock) - .rows - .map((row) => row.cells - .map((cell) => _flattenVisibleInlines(cell.inlines)) - .join('\t')) + return (block as TableBlock).rows + .map( + (row) => row.cells + .map((cell) => _flattenVisibleInlines(cell.inlines)) + .join('\t'), + ) .join('\n'); case MarkdownBlockKind.image: return _imageCaptionText(block as ImageBlock); @@ -874,8 +879,7 @@ class _StructuredBlockSelectionBuilder { case MarkdownBlockKind.paragraph: return _flattenExportInlines((block as ParagraphBlock).inlines); case MarkdownBlockKind.quote: - return (block as QuoteBlock) - .children + return (block as QuoteBlock).children .map(_exportBlockText) .where((value) => value.trim().isNotEmpty) .join('\n\n'); @@ -888,15 +892,17 @@ class _StructuredBlockSelectionBuilder { return _exportDetailsText(block as DetailsBlock); case MarkdownBlockKind.footnoteList: return _exportListText( - _footnoteListAsOrderedList(block as FootnoteListBlock)); + _footnoteListAsOrderedList(block as FootnoteListBlock), + ); case MarkdownBlockKind.codeBlock: return (block as CodeBlock).code; case MarkdownBlockKind.table: - return (block as TableBlock) - .rows - .map((row) => row.cells - .map((cell) => _flattenExportInlines(cell.inlines)) - .join('\t')) + return (block as TableBlock).rows + .map( + (row) => row.cells + .map((cell) => _flattenExportInlines(cell.inlines)) + .join('\t'), + ) .join('\n'); case MarkdownBlockKind.image: return _exportImageText(block as ImageBlock); @@ -909,11 +915,7 @@ class _StructuredBlockSelectionBuilder { final items = []; for (var index = 0; index < block.items.length; index++) { final item = block.items[index]; - final markerText = _listItemMarkerText( - block, - index, - indentLevel: 0, - ); + final markerText = _listItemMarkerText(block, index, indentLevel: 0); final content = item.children .map(_visibleBlockText) .where((value) => value.trim().isNotEmpty) @@ -925,9 +927,7 @@ class _StructuredBlockSelectionBuilder { items.add(markerText.trimRight()); continue; } - items.add( - _prefixMultiline(content, markerText, ' ' * markerText.length), - ); + items.add(_prefixMultiline(content, markerText, ' ' * markerText.length)); } return items.join('\n'); } @@ -936,11 +936,7 @@ class _StructuredBlockSelectionBuilder { final items = []; for (var index = 0; index < block.items.length; index++) { final item = block.items[index]; - final markerText = _listItemMarkerText( - block, - index, - indentLevel: 0, - ); + final markerText = _listItemMarkerText(block, index, indentLevel: 0); final content = item.children .map(_exportBlockText) .where((value) => value.trim().isNotEmpty) @@ -952,9 +948,7 @@ class _StructuredBlockSelectionBuilder { items.add(markerText.trimRight()); continue; } - items.add( - _prefixMultiline(content, markerText, ' ' * markerText.length), - ); + items.add(_prefixMultiline(content, markerText, ' ' * markerText.length)); } return items.join('\n'); } @@ -968,27 +962,33 @@ class _StructuredBlockSelectionBuilder { break; case MarkdownInlineKind.emphasis: buffer.write( - _flattenVisibleInlines((inline as EmphasisInline).children)); + _flattenVisibleInlines((inline as EmphasisInline).children), + ); break; case MarkdownInlineKind.strong: - buffer - .write(_flattenVisibleInlines((inline as StrongInline).children)); + buffer.write( + _flattenVisibleInlines((inline as StrongInline).children), + ); break; case MarkdownInlineKind.strikethrough: buffer.write( - _flattenVisibleInlines((inline as StrikethroughInline).children)); + _flattenVisibleInlines((inline as StrikethroughInline).children), + ); break; case MarkdownInlineKind.highlight: buffer.write( - _flattenVisibleInlines((inline as HighlightInline).children)); + _flattenVisibleInlines((inline as HighlightInline).children), + ); break; case MarkdownInlineKind.subscript: buffer.write( - _flattenVisibleInlines((inline as SubscriptInline).children)); + _flattenVisibleInlines((inline as SubscriptInline).children), + ); break; case MarkdownInlineKind.superscript: buffer.write( - _flattenVisibleInlines((inline as SuperscriptInline).children)); + _flattenVisibleInlines((inline as SuperscriptInline).children), + ); break; case MarkdownInlineKind.link: buffer.write(_flattenVisibleInlines((inline as LinkInline).children)); @@ -1021,27 +1021,33 @@ class _StructuredBlockSelectionBuilder { break; case MarkdownInlineKind.emphasis: buffer.write( - _flattenExportInlines((inline as EmphasisInline).children)); + _flattenExportInlines((inline as EmphasisInline).children), + ); break; case MarkdownInlineKind.strong: - buffer - .write(_flattenExportInlines((inline as StrongInline).children)); + buffer.write( + _flattenExportInlines((inline as StrongInline).children), + ); break; case MarkdownInlineKind.strikethrough: buffer.write( - _flattenExportInlines((inline as StrikethroughInline).children)); + _flattenExportInlines((inline as StrikethroughInline).children), + ); break; case MarkdownInlineKind.highlight: buffer.write( - _flattenExportInlines((inline as HighlightInline).children)); + _flattenExportInlines((inline as HighlightInline).children), + ); break; case MarkdownInlineKind.subscript: buffer.write( - _flattenExportInlines((inline as SubscriptInline).children)); + _flattenExportInlines((inline as SubscriptInline).children), + ); break; case MarkdownInlineKind.superscript: buffer.write( - _flattenExportInlines((inline as SuperscriptInline).children)); + _flattenExportInlines((inline as SuperscriptInline).children), + ); break; case MarkdownInlineKind.link: final link = inline as LinkInline; @@ -1096,8 +1102,8 @@ class _StructuredBlockSelectionBuilder { final label = block.alt?.trim().isNotEmpty == true ? block.alt!.trim() : block.title?.trim().isNotEmpty == true - ? block.title!.trim() - : block.url; + ? block.title!.trim() + : block.url; return label; } diff --git a/packages/mixin_markdown_widget/lib/src/streaming/streaming_state.dart b/packages/mixin_markdown_widget/lib/src/streaming/streaming_state.dart index 7d67dcd0..5c6ee7b2 100644 --- a/packages/mixin_markdown_widget/lib/src/streaming/streaming_state.dart +++ b/packages/mixin_markdown_widget/lib/src/streaming/streaming_state.dart @@ -7,25 +7,23 @@ class StreamingMarkdownState { const StreamingMarkdownState({ required this.committedBlocks, required this.draftBlock, - required String buffer, + required this._buffer, required this.version, - }) : _buffer = buffer, - _bufferProvider = null; + }) : _bufferProvider = null; const StreamingMarkdownState.lazyBuffer({ required this.committedBlocks, required this.draftBlock, - required String Function() bufferProvider, + required String Function() this._bufferProvider, required this.version, - }) : _buffer = '', - _bufferProvider = bufferProvider; + }) : _buffer = ''; const StreamingMarkdownState.empty() - : committedBlocks = const [], - draftBlock = null, - _buffer = '', - _bufferProvider = null, - version = 0; + : committedBlocks = const [], + draftBlock = null, + _buffer = '', + _bufferProvider = null, + version = 0; final List committedBlocks; final BlockNode? draftBlock; diff --git a/packages/mixin_markdown_widget/lib/src/widgets/markdown_controller.dart b/packages/mixin_markdown_widget/lib/src/widgets/markdown_controller.dart index 522f9847..81659c30 100644 --- a/packages/mixin_markdown_widget/lib/src/widgets/markdown_controller.dart +++ b/packages/mixin_markdown_widget/lib/src/widgets/markdown_controller.dart @@ -19,9 +19,9 @@ class MarkdownController extends ChangeNotifier { MarkdownDocument? document, MarkdownDocumentParser? parser, MarkdownCopySerializer? plainTextSerializer, - }) : _parser = parser ?? const MarkdownDocumentParser(), - _plainTextSerializer = - plainTextSerializer ?? const MarkdownPlainTextSerializer() { + }) : _parser = parser ?? const MarkdownDocumentParser(), + _plainTextSerializer = + plainTextSerializer ?? const MarkdownPlainTextSerializer() { _replaceData(data, document: document); } @@ -41,8 +41,7 @@ class MarkdownController extends ChangeNotifier { static List> highlightCodeBlocks( MarkdownDocument document, { int maxLines = 120, - }) => - highlighter.highlightCodeBlocks(document, maxLines: maxLines); + }) => highlighter.highlightCodeBlocks(document, maxLines: maxLines); /// Stores the result of [highlightCodeBlocks] on this isolate. static void cacheCodeHighlights(List> highlights) => diff --git a/packages/mixin_markdown_widget/lib/src/widgets/markdown_theme.dart b/packages/mixin_markdown_widget/lib/src/widgets/markdown_theme.dart index 705013b2..88ac75be 100644 --- a/packages/mixin_markdown_widget/lib/src/widgets/markdown_theme.dart +++ b/packages/mixin_markdown_widget/lib/src/widgets/markdown_theme.dart @@ -1,20 +1,16 @@ import 'dart:ui' show lerpDouble; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; class MarkdownTheme extends InheritedTheme { - const MarkdownTheme({ - super.key, - required this.data, - required super.child, - }); + const MarkdownTheme({super.key, required this.data, required super.child}); final MarkdownThemeData data; static MarkdownThemeData of(BuildContext context) { - final inheritedTheme = - context.dependOnInheritedWidgetOfExactType(); + final inheritedTheme = context + .dependOnInheritedWidgetOfExactType(); if (inheritedTheme != null) { return inheritedTheme.data; } @@ -34,15 +30,9 @@ class MarkdownTheme extends InheritedTheme { } } -enum MarkdownThemeForeground { - light, - dark, -} +enum MarkdownThemeForeground { light, dark } -enum MarkdownThemeDensity { - comfortable, - tight, -} +enum MarkdownThemeDensity { comfortable, tight } @immutable class _MarkdownThemePalette { @@ -141,10 +131,7 @@ class MarkdownThemeData extends ThemeExtension BuildContext context, { double maxContentWidth = 920, }) { - return MarkdownThemeData.themed( - context, - maxContentWidth: maxContentWidth, - ); + return MarkdownThemeData.themed(context, maxContentWidth: maxContentWidth); } factory MarkdownThemeData.tight( @@ -188,8 +175,9 @@ class MarkdownThemeData extends ThemeExtension ); return switch (density) { MarkdownThemeDensity.comfortable => comfortableTheme, - MarkdownThemeDensity.tight => - MarkdownThemeData._applyTightDensity(comfortableTheme), + MarkdownThemeDensity.tight => MarkdownThemeData._applyTightDensity( + comfortableTheme, + ), }; } @@ -208,7 +196,7 @@ class MarkdownThemeData extends ThemeExtension final colorScheme = theme.colorScheme; final borderColor = Color.lerp(colorScheme.outline, colorScheme.onSurface, 0.24) ?? - colorScheme.outline; + colorScheme.outline; return _MarkdownThemeScheme( colorScheme: colorScheme, palette: _MarkdownThemePalette( @@ -257,7 +245,8 @@ class MarkdownThemeData extends ThemeExtension const secondaryForeground = Color(0xFFB7C4D3); const headingForeground = Color(0xFFF8FBFF); const linkForeground = Color(0xFF7CCBFF); - final borderColor = Color.lerp(colorScheme.outline, foreground, 0.12) ?? + final borderColor = + Color.lerp(colorScheme.outline, foreground, 0.12) ?? colorScheme.outline; return _MarkdownThemeScheme( colorScheme: colorScheme, @@ -326,11 +315,15 @@ class MarkdownThemeData extends ThemeExtension listItemSpacing: 3, maxContentWidth: maxContentWidth, quotePadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), - inlineCodePadding: - const EdgeInsets.symmetric(horizontal: 5, vertical: 0.5), + inlineCodePadding: const EdgeInsets.symmetric( + horizontal: 5, + vertical: 0.5, + ), codeBlockPadding: const EdgeInsets.all(16), - tableCellPadding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + tableCellPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), inlineCodeBorderRadius: BorderRadius.circular(6), codeBlockBorderRadius: BorderRadius.circular(4), imageBorderRadius: BorderRadius.circular(6), @@ -354,7 +347,8 @@ class MarkdownThemeData extends ThemeExtension fontWeight: FontWeight.w700, color: palette.headingForeground, ), - heading1Style: textTheme.displaySmall?.copyWith( + heading1Style: + textTheme.displaySmall?.copyWith( fontWeight: FontWeight.w700, color: palette.headingForeground, ) ?? @@ -363,7 +357,8 @@ class MarkdownThemeData extends ThemeExtension fontWeight: FontWeight.w700, color: palette.headingForeground, ), - heading2Style: textTheme.headlineMedium?.copyWith( + heading2Style: + textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.w700, color: palette.headingForeground, ) ?? @@ -372,7 +367,8 @@ class MarkdownThemeData extends ThemeExtension fontWeight: FontWeight.w700, color: palette.headingForeground, ), - heading3Style: textTheme.headlineSmall?.copyWith( + heading3Style: + textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.w700, color: palette.headingForeground, ) ?? @@ -381,7 +377,8 @@ class MarkdownThemeData extends ThemeExtension fontWeight: FontWeight.w700, color: palette.headingForeground, ), - heading4Style: textTheme.titleLarge?.copyWith( + heading4Style: + textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w700, color: palette.headingForeground, ) ?? @@ -390,7 +387,8 @@ class MarkdownThemeData extends ThemeExtension fontWeight: FontWeight.w700, color: palette.headingForeground, ), - heading5Style: textTheme.titleMedium?.copyWith( + heading5Style: + textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, color: palette.headingForeground, ) ?? @@ -399,7 +397,8 @@ class MarkdownThemeData extends ThemeExtension fontWeight: FontWeight.w700, color: palette.headingForeground, ), - heading6Style: textTheme.titleSmall?.copyWith( + heading6Style: + textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w700, color: palette.headingForeground, ) ?? @@ -434,8 +433,10 @@ class MarkdownThemeData extends ThemeExtension blockSpacing: 9, listItemSpacing: 1.5, quotePadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), - inlineCodePadding: - const EdgeInsets.symmetric(horizontal: 4, vertical: 0.5), + inlineCodePadding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 0.5, + ), codeBlockPadding: const EdgeInsets.fromLTRB(12, 10, 12, 12), tableCellPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), inlineCodeBorderRadius: BorderRadius.circular(5), @@ -448,18 +449,30 @@ class MarkdownThemeData extends ThemeExtension inlineCodeStyle: baseTheme.inlineCodeStyle.copyWith(fontSize: codeSize), codeBlockStyle: baseTheme.codeBlockStyle.copyWith(fontSize: codeSize), tableHeaderStyle: baseTheme.tableHeaderStyle.copyWith(fontSize: bodySize), - heading1Style: - baseTheme.heading1Style.copyWith(fontSize: 30, height: 1.2), - heading2Style: - baseTheme.heading2Style.copyWith(fontSize: 25, height: 1.22), - heading3Style: - baseTheme.heading3Style.copyWith(fontSize: 21, height: 1.24), - heading4Style: - baseTheme.heading4Style.copyWith(fontSize: 18.5, height: 1.26), - heading5Style: - baseTheme.heading5Style.copyWith(fontSize: 17, height: 1.28), - heading6Style: - baseTheme.heading6Style.copyWith(fontSize: 16, height: 1.3), + heading1Style: baseTheme.heading1Style.copyWith( + fontSize: 30, + height: 1.2, + ), + heading2Style: baseTheme.heading2Style.copyWith( + fontSize: 25, + height: 1.22, + ), + heading3Style: baseTheme.heading3Style.copyWith( + fontSize: 21, + height: 1.24, + ), + heading4Style: baseTheme.heading4Style.copyWith( + fontSize: 18.5, + height: 1.26, + ), + heading5Style: baseTheme.heading5Style.copyWith( + fontSize: 17, + height: 1.28, + ), + heading6Style: baseTheme.heading6Style.copyWith( + fontSize: 16, + height: 1.3, + ), ); } @@ -621,7 +634,8 @@ class MarkdownThemeData extends ThemeExtension tableRowBackgroundColor ?? this.tableRowBackgroundColor, selectionColor: selectionColor ?? this.selectionColor, quoteBorderWidth: quoteBorderWidth ?? this.quoteBorderWidth, - imagePlaceholderBackgroundColor: imagePlaceholderBackgroundColor ?? + imagePlaceholderBackgroundColor: + imagePlaceholderBackgroundColor ?? this.imagePlaceholderBackgroundColor, showHeading1Divider: showHeading1Divider ?? this.showHeading1Divider, showHeading2Divider: showHeading2Divider ?? this.showHeading2Divider, @@ -639,62 +653,60 @@ class MarkdownThemeData extends ThemeExtension padding: EdgeInsetsGeometry.lerp(padding, other.padding, t) ?? padding, blockSpacing: lerpDouble(blockSpacing, other.blockSpacing, t) ?? blockSpacing, - listItemSpacing: lerpDouble(listItemSpacing, other.listItemSpacing, t) ?? + listItemSpacing: + lerpDouble(listItemSpacing, other.listItemSpacing, t) ?? listItemSpacing, - maxContentWidth: lerpDouble(maxContentWidth, other.maxContentWidth, t) ?? + maxContentWidth: + lerpDouble(maxContentWidth, other.maxContentWidth, t) ?? maxContentWidth, quotePadding: EdgeInsetsGeometry.lerp(quotePadding, other.quotePadding, t) ?? - quotePadding, + quotePadding, inlineCodePadding: EdgeInsets.lerp(inlineCodePadding, other.inlineCodePadding, t) ?? - inlineCodePadding, - codeBlockPadding: EdgeInsetsGeometry.lerp( + inlineCodePadding, + codeBlockPadding: + EdgeInsetsGeometry.lerp( codeBlockPadding, other.codeBlockPadding, t, ) ?? codeBlockPadding, - tableCellPadding: EdgeInsetsGeometry.lerp( + tableCellPadding: + EdgeInsetsGeometry.lerp( tableCellPadding, other.tableCellPadding, t, ) ?? tableCellPadding, - inlineCodeBorderRadius: BorderRadius.lerp( + inlineCodeBorderRadius: + BorderRadius.lerp( inlineCodeBorderRadius, other.inlineCodeBorderRadius, t, ) ?? inlineCodeBorderRadius, - codeBlockBorderRadius: BorderRadius.lerp( + codeBlockBorderRadius: + BorderRadius.lerp( codeBlockBorderRadius, other.codeBlockBorderRadius, t, ) ?? codeBlockBorderRadius, - imageBorderRadius: BorderRadius.lerp( - imageBorderRadius, - other.imageBorderRadius, - t, - ) ?? + imageBorderRadius: + BorderRadius.lerp(imageBorderRadius, other.imageBorderRadius, t) ?? imageBorderRadius, - quoteBorderRadius: BorderRadius.lerp( - quoteBorderRadius, - other.quoteBorderRadius, - t, - ) ?? + quoteBorderRadius: + BorderRadius.lerp(quoteBorderRadius, other.quoteBorderRadius, t) ?? quoteBorderRadius, - tableBorderRadius: BorderRadius.lerp( - tableBorderRadius, - other.tableBorderRadius, - t, - ) ?? + tableBorderRadius: + BorderRadius.lerp(tableBorderRadius, other.tableBorderRadius, t) ?? tableBorderRadius, imageCaptionSpacing: lerpDouble(imageCaptionSpacing, other.imageCaptionSpacing, t) ?? - imageCaptionSpacing, - codeBlockToolbarPadding: EdgeInsetsGeometry.lerp( + imageCaptionSpacing, + codeBlockToolbarPadding: + EdgeInsetsGeometry.lerp( codeBlockToolbarPadding, other.codeBlockToolbarPadding, t, @@ -705,37 +717,46 @@ class MarkdownThemeData extends ThemeExtension linkStyle: TextStyle.lerp(linkStyle, other.linkStyle, t) ?? linkStyle, inlineCodeStyle: TextStyle.lerp(inlineCodeStyle, other.inlineCodeStyle, t) ?? - inlineCodeStyle, - codeBlockStyle: TextStyle.lerp(codeBlockStyle, other.codeBlockStyle, t) ?? + inlineCodeStyle, + codeBlockStyle: + TextStyle.lerp(codeBlockStyle, other.codeBlockStyle, t) ?? codeBlockStyle, tableHeaderStyle: TextStyle.lerp(tableHeaderStyle, other.tableHeaderStyle, t) ?? - tableHeaderStyle, - heading1Style: TextStyle.lerp(heading1Style, other.heading1Style, t) ?? + tableHeaderStyle, + heading1Style: + TextStyle.lerp(heading1Style, other.heading1Style, t) ?? heading1Style, - heading2Style: TextStyle.lerp(heading2Style, other.heading2Style, t) ?? + heading2Style: + TextStyle.lerp(heading2Style, other.heading2Style, t) ?? heading2Style, - heading3Style: TextStyle.lerp(heading3Style, other.heading3Style, t) ?? + heading3Style: + TextStyle.lerp(heading3Style, other.heading3Style, t) ?? heading3Style, - heading4Style: TextStyle.lerp(heading4Style, other.heading4Style, t) ?? + heading4Style: + TextStyle.lerp(heading4Style, other.heading4Style, t) ?? heading4Style, - heading5Style: TextStyle.lerp(heading5Style, other.heading5Style, t) ?? + heading5Style: + TextStyle.lerp(heading5Style, other.heading5Style, t) ?? heading5Style, - heading6Style: TextStyle.lerp(heading6Style, other.heading6Style, t) ?? + heading6Style: + TextStyle.lerp(heading6Style, other.heading6Style, t) ?? heading6Style, quoteBackgroundColor: Color.lerp(quoteBackgroundColor, other.quoteBackgroundColor, t) ?? - quoteBackgroundColor, + quoteBackgroundColor, quoteBorderColor: Color.lerp(quoteBorderColor, other.quoteBorderColor, t) ?? - quoteBorderColor, - inlineCodeBackgroundColor: Color.lerp( + quoteBorderColor, + inlineCodeBackgroundColor: + Color.lerp( inlineCodeBackgroundColor, other.inlineCodeBackgroundColor, t, ) ?? inlineCodeBackgroundColor, - codeBlockBackgroundColor: Color.lerp( + codeBlockBackgroundColor: + Color.lerp( codeBlockBackgroundColor, other.codeBlockBackgroundColor, t, @@ -745,14 +766,16 @@ class MarkdownThemeData extends ThemeExtension Color.lerp(dividerColor, other.dividerColor, t) ?? dividerColor, tableBorderColor: Color.lerp(tableBorderColor, other.tableBorderColor, t) ?? - tableBorderColor, - tableHeaderBackgroundColor: Color.lerp( + tableBorderColor, + tableHeaderBackgroundColor: + Color.lerp( tableHeaderBackgroundColor, other.tableHeaderBackgroundColor, t, ) ?? tableHeaderBackgroundColor, - tableRowBackgroundColor: Color.lerp( + tableRowBackgroundColor: + Color.lerp( tableRowBackgroundColor, other.tableRowBackgroundColor, t, @@ -762,19 +785,23 @@ class MarkdownThemeData extends ThemeExtension Color.lerp(selectionColor, other.selectionColor, t) ?? selectionColor, quoteBorderWidth: lerpDouble(quoteBorderWidth, other.quoteBorderWidth, t) ?? - quoteBorderWidth, - imagePlaceholderBackgroundColor: Color.lerp( + quoteBorderWidth, + imagePlaceholderBackgroundColor: + Color.lerp( imagePlaceholderBackgroundColor, other.imagePlaceholderBackgroundColor, t, ) ?? imagePlaceholderBackgroundColor, - showHeading1Divider: - t < 0.5 ? showHeading1Divider : other.showHeading1Divider, - showHeading2Divider: - t < 0.5 ? showHeading2Divider : other.showHeading2Divider, - codeHighlightMaxLines: - t < 0.5 ? codeHighlightMaxLines : other.codeHighlightMaxLines, + showHeading1Divider: t < 0.5 + ? showHeading1Divider + : other.showHeading1Divider, + showHeading2Divider: t < 0.5 + ? showHeading2Divider + : other.showHeading2Divider, + codeHighlightMaxLines: t < 0.5 + ? codeHighlightMaxLines + : other.codeHighlightMaxLines, ); } @@ -789,8 +816,9 @@ class MarkdownThemeData extends ThemeExtension properties.add(IntProperty('codeHighlightMaxLines', codeHighlightMaxLines)); properties.add(ColorProperty('selectionColor', selectionColor)); properties.add(DiagnosticsProperty('bodyStyle', bodyStyle)); - properties - .add(DiagnosticsProperty('heading1Style', heading1Style)); + properties.add( + DiagnosticsProperty('heading1Style', heading1Style), + ); } @override diff --git a/packages/mixin_markdown_widget/lib/src/widgets/markdown_types.dart b/packages/mixin_markdown_widget/lib/src/widgets/markdown_types.dart index cbb5c76e..8ceab3cf 100644 --- a/packages/mixin_markdown_widget/lib/src/widgets/markdown_types.dart +++ b/packages/mixin_markdown_widget/lib/src/widgets/markdown_types.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import '../core/document.dart'; import '../selection/selection_controller.dart'; diff --git a/packages/mixin_markdown_widget/lib/src/widgets/markdown_widget.dart b/packages/mixin_markdown_widget/lib/src/widgets/markdown_widget.dart index b2a28be6..d3c4db7a 100644 --- a/packages/mixin_markdown_widget/lib/src/widgets/markdown_widget.dart +++ b/packages/mixin_markdown_widget/lib/src/widgets/markdown_widget.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import '../render/markdown_document_view.dart'; import '../selection/selection_controller.dart'; @@ -19,8 +19,10 @@ class MarkdownWidget extends StatefulWidget { this.useColumn = false, this.selectable = true, this.enableCopyFullDocumentShortcut = true, - @Deprecated('Filter MarkdownContextMenuDetails.buttonItems in ' - 'contextMenuBuilder instead.') + @Deprecated( + 'Filter MarkdownContextMenuDetails.buttonItems in ' + 'contextMenuBuilder instead.', + ) this.showCopyAllInContextMenu = true, this.selectionController, this.padding, @@ -30,9 +32,9 @@ class MarkdownWidget extends StatefulWidget { this.bulletBuilder, this.contextMenuBuilder = defaultMarkdownContextMenuBuilder, }) : assert( - (data == null) != (controller == null), - 'Provide exactly one of data or controller.', - ); + (data == null) != (controller == null), + 'Provide exactly one of data or controller.', + ); final String? data; final MarkdownController? controller; @@ -43,8 +45,10 @@ class MarkdownWidget extends StatefulWidget { final bool useColumn; final bool selectable; final bool enableCopyFullDocumentShortcut; - @Deprecated('Filter MarkdownContextMenuDetails.buttonItems in ' - 'contextMenuBuilder instead.') + @Deprecated( + 'Filter MarkdownContextMenuDetails.buttonItems in ' + 'contextMenuBuilder instead.', + ) final bool showCopyAllInContextMenu; final MarkdownSelectionController? selectionController; final EdgeInsetsGeometry? padding; @@ -61,7 +65,8 @@ class MarkdownWidget extends StatefulWidget { } @Deprecated( - 'Use MarkdownWidget instead. This misspelled alias will be removed.') + 'Use MarkdownWidget instead. This misspelled alias will be removed.', +) typedef MarkownWidget = MarkdownWidget; class _MarkdownWidgetState extends State { @@ -117,28 +122,28 @@ class _MarkdownWidgetState extends State { final selectionRegistrar = MixinSelectionRegistrar.maybeOf(context); final selectionController = widget.selectable ? (widget.selectionController ?? - selectionRegistrar?.controller ?? - _fallbackSelectionController) + selectionRegistrar?.controller ?? + _fallbackSelectionController) : null; - final usesInheritedSelectionController = widget.selectable && + final usesInheritedSelectionController = + widget.selectable && widget.selectionController == null && selectionRegistrar != null; final animation = selectionController == null ? _effectiveController.documentListenable - : Listenable.merge( - [ - _effectiveController.documentListenable, - selectionController, - ], - ); + : Listenable.merge([ + _effectiveController.documentListenable, + selectionController, + ]); return TextSelectionTheme( data: TextSelectionThemeData(selectionColor: theme.selectionColor), child: AnimatedBuilder( animation: animation, builder: (context, _) { if (!usesInheritedSelectionController) { - selectionController - ?.attachDocument(_effectiveController.document); + selectionController?.attachDocument( + _effectiveController.document, + ); } return MarkdownDocumentView( document: _effectiveController.document, diff --git a/packages/mixin_markdown_widget/pubspec.yaml b/packages/mixin_markdown_widget/pubspec.yaml index 82c91734..f6f92267 100644 --- a/packages/mixin_markdown_widget/pubspec.yaml +++ b/packages/mixin_markdown_widget/pubspec.yaml @@ -1,14 +1,15 @@ name: mixin_markdown_widget resolution: workspace description: A desktop-first Flutter Markdown reader widget with theming and streaming-friendly controller APIs. -version: 0.4.0 +version: 0.5.0 homepage: https://github.com/MixinNetwork/flutter-plugins/tree/main/packages/mixin_markdown_widget environment: - sdk: ^3.5.0 - flutter: ">=3.13.0" + sdk: ^3.13.0 + flutter: ">=3.47.0" dependencies: + material_ui: ^1.11.0 flutter: sdk: flutter html: ^0.15.6 diff --git a/packages/mixin_markdown_widget/test/github_html_test.dart b/packages/mixin_markdown_widget/test/github_html_test.dart index d12f9a8f..f695ebdd 100644 --- a/packages/mixin_markdown_widget/test/github_html_test.dart +++ b/packages/mixin_markdown_widget/test/github_html_test.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:mixin_markdown_widget/src/render/markdown_block_widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mixin_markdown_widget/mixin_markdown_widget.dart'; @@ -46,8 +46,10 @@ void main() { expect(quote.children.whereType(), hasLength(1)); final details = quote.children.whereType().single; expect(details.initiallyExpanded, isFalse); - expect((details.children.first as ParagraphBlock).inlines.single, - isA()); + expect( + (details.children.first as ParagraphBlock).inlines.single, + isA(), + ); expect(details.children.whereType().single.items, hasLength(2)); }); @@ -58,7 +60,9 @@ void main() { final two = controller.document.blocks.last; expect(two.sourceRange, isNotNull); expect( - source.substring(two.sourceRange!.start, two.sourceRange!.end), 'Two'); + source.substring(two.sourceRange!.start, two.sourceRange!.end), + 'Two', + ); controller.appendChunk(' more'); expect(controller.plainText, 'One\n\nTwo more'); @@ -76,18 +80,22 @@ void main() { addTearDown(controller.dispose); for (var end = 1; end <= reviewFixture.length; end += 1) { controller.appendChunk(reviewFixture.substring(end - 1, end)); - final reference = - MarkdownController(data: reviewFixture.substring(0, end)); + final reference = MarkdownController( + data: reviewFixture.substring(0, end), + ); expect(controller.plainText, reference.plainText, reason: 'offset $end'); reference.dispose(); } }); - testWidgets('GitHub review table and details render without HTML source', - (tester) async { - await tester.pumpWidget(const MaterialApp( - home: Scaffold(body: MarkdownWidget(data: reviewFixture)), - )); + testWidgets('GitHub review table and details render without HTML source', ( + tester, + ) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: MarkdownWidget(data: reviewFixture)), + ), + ); expect(find.textContaining('`'), ''); - expect(plainText('```html\n\n
\n```'), - contains('\n
')); + expect( + plainText('```html\n\n
\n```'), + contains('\n
'), + ); expect(plainText(r'\'), ''); expect(plainText('<!-- visible -->'), ''); }); @@ -128,12 +140,16 @@ void main() { for (final tag in ['
', '
', '
', '
']) { expect(plainText('Before${tag}After'), 'Before\nAfter'); final document = const MarkdownDocumentParser().parse('$tag\n\nAfter'); - expect((document.blocks.first as ParagraphBlock).inlines.single, - isA()); expect( - plainText( - '
More\n$tag\n\nAfter\n
'), - isNot(contains('(), + ); + expect( + plainText( + '
More\n$tag\n\nAfter\n
', + ), + isNot(contains(' - null; + }) => null; @override Future buildPresentationAsync({ @@ -39,7 +38,9 @@ class _FakeHighlighter extends MarkdownCodeSyntaxHighlighter { void finishNext(TextStyle baseStyle) { final source = requests[requests.length - _pending.length]; final style = baseStyle.copyWith(color: _keywordColor); - _pending.removeAt(0).complete( + _pending + .removeAt(0) + .complete( MarkdownCodeHighlightPresentation( span: TextSpan( style: baseStyle, @@ -71,41 +72,43 @@ void main() { } testWidgets( - 'a growing code block keeps its highlighted prefix as the placeholder', - (tester) async { - await pumpTheme(tester); - final highlighter = _FakeHighlighter(); - final cache = MarkdownCodeHighlightCache(highlighter: highlighter); - addTearDown(cache.dispose); - final baseStyle = theme.codeBlockStyle; - - MarkdownCodeHighlightPresentation resolve(String source) => cache.resolve( - blockId: 'code', - source: source, - baseStyle: baseStyle, - theme: theme, - language: 'dart', - ); - - expect(resolve('a = 1;').isHighlighted, isFalse); - highlighter.finishNext(baseStyle); - await tester.pump(); - expect(resolve('a = 1;').isHighlighted, isTrue); - - final placeholder = resolve('a = 1;\nb = 2;'); - expect(placeholder.isHighlighted, isFalse); - expect(placeholder.span.toPlainText(), 'a = 1;\nb = 2;'); - expect( - placeholder.runs.map((run) => (run.text, run.style.color)).toList(), - <(String, Color?)>[ - ('a = 1;', _keywordColor), - ('\nb = 2;', baseStyle.color), - ], - ); - }); - - testWidgets('a growing code block runs one highlight at a time', - (tester) async { + 'a growing code block keeps its highlighted prefix as the placeholder', + (tester) async { + await pumpTheme(tester); + final highlighter = _FakeHighlighter(); + final cache = MarkdownCodeHighlightCache(highlighter: highlighter); + addTearDown(cache.dispose); + final baseStyle = theme.codeBlockStyle; + + MarkdownCodeHighlightPresentation resolve(String source) => cache.resolve( + blockId: 'code', + source: source, + baseStyle: baseStyle, + theme: theme, + language: 'dart', + ); + + expect(resolve('a = 1;').isHighlighted, isFalse); + highlighter.finishNext(baseStyle); + await tester.pump(); + expect(resolve('a = 1;').isHighlighted, isTrue); + + final placeholder = resolve('a = 1;\nb = 2;'); + expect(placeholder.isHighlighted, isFalse); + expect(placeholder.span.toPlainText(), 'a = 1;\nb = 2;'); + expect( + placeholder.runs.map((run) => (run.text, run.style.color)).toList(), + <(String, Color?)>[ + ('a = 1;', _keywordColor), + ('\nb = 2;', baseStyle.color), + ], + ); + }, + ); + + testWidgets('a growing code block runs one highlight at a time', ( + tester, + ) async { await pumpTheme(tester); final highlighter = _FakeHighlighter(); final cache = MarkdownCodeHighlightCache(highlighter: highlighter); @@ -115,12 +118,12 @@ void main() { cache.addListener(() => notifications += 1); MarkdownCodeHighlightPresentation resolve(String source) => cache.resolve( - blockId: 'code', - source: source, - baseStyle: baseStyle, - theme: theme, - language: 'dart', - ); + blockId: 'code', + source: source, + baseStyle: baseStyle, + theme: theme, + language: 'dart', + ); resolve('a'); resolve('a = 1;'); @@ -148,8 +151,9 @@ void main() { expect(done.runs.single.text, 'a = 1;\nb = 2;'); }); - testWidgets('a code block that is not extended falls back to plain text', - (tester) async { + testWidgets('a code block that is not extended falls back to plain text', ( + tester, + ) async { await pumpTheme(tester); final highlighter = _FakeHighlighter(); final cache = MarkdownCodeHighlightCache(highlighter: highlighter); diff --git a/packages/mixin_markdown_widget/test/markdown_code_highlight_prepare_test.dart b/packages/mixin_markdown_widget/test/markdown_code_highlight_prepare_test.dart index 96e2471d..753e58e5 100644 --- a/packages/mixin_markdown_widget/test/markdown_code_highlight_prepare_test.dart +++ b/packages/mixin_markdown_widget/test/markdown_code_highlight_prepare_test.dart @@ -1,11 +1,12 @@ -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mixin_markdown_widget/mixin_markdown_widget.dart'; import 'package:mixin_markdown_widget/src/render/code_syntax_highlighter.dart'; void main() { - testWidgets('prepared highlights show in the first frame of a code block', - (tester) async { + testWidgets('prepared highlights show in the first frame of a code block', ( + tester, + ) async { late MarkdownThemeData theme; await tester.pumpWidget( MaterialApp( @@ -38,8 +39,10 @@ $long ``` '''); - final highlights = - MarkdownController.highlightCodeBlocks(document, maxLines: 1); + final highlights = MarkdownController.highlightCodeBlocks( + document, + maxLines: 1, + ); expect(highlights.map((highlight) => highlight['source']), [top, nested]); MarkdownController.cacheCodeHighlights(highlights); diff --git a/packages/mixin_markdown_widget/test/markdown_context_menu_test.dart b/packages/mixin_markdown_widget/test/markdown_context_menu_test.dart index 0a6e806c..1763b39f 100644 --- a/packages/mixin_markdown_widget/test/markdown_context_menu_test.dart +++ b/packages/mixin_markdown_widget/test/markdown_context_menu_test.dart @@ -1,5 +1,5 @@ import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mixin_markdown_widget/mixin_markdown_widget.dart'; @@ -64,8 +64,10 @@ List _mockClipboard(WidgetTester tester) { }, ); addTearDown( - () => tester.binding.defaultBinaryMessenger - .setMockMethodCallHandler(SystemChannels.platform, null), + () => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + SystemChannels.platform, + null, + ), ); return copied; } @@ -96,36 +98,46 @@ Future _pumpMarkdown( void main() { group('right click selection', () { - testWidgets( - 'selects the word under the pointer outside the selection', - (tester) async { - final selection = await _pumpMarkdown(tester); - final brave = - _characterCenter(tester, containing: 'Hello', character: 'v'); - // Left of `brave`: the menu opens to the right of the pointer. - final hello = - _characterCenter(tester, containing: 'Hello', character: 'H'); + testWidgets('selects the word under the pointer outside the selection', ( + tester, + ) async { + final selection = await _pumpMarkdown(tester); + final brave = _characterCenter( + tester, + containing: 'Hello', + character: 'v', + ); + // Left of `brave`: the menu opens to the right of the pointer. + final hello = _characterCenter( + tester, + containing: 'Hello', + character: 'H', + ); - await _rightClick(tester, brave); - expect(selection.selectedPlainText, 'brave'); - expect(_menu, findsOneWidget); - expect(find.text('Copy'), findsOneWidget); + await _rightClick(tester, brave); + expect(selection.selectedPlainText, 'brave'); + expect(_menu, findsOneWidget); + expect(find.text('Copy'), findsOneWidget); - await _rightClick(tester, hello); - expect(selection.selectedPlainText, 'Hello'); - }, - variant: TargetPlatformVariant.only(TargetPlatform.macOS), - ); + await _rightClick(tester, hello); + expect(selection.selectedPlainText, 'Hello'); + }, variant: TargetPlatformVariant.only(TargetPlatform.macOS)); testWidgets( 'keeps the selection under the pointer and clears it elsewhere', (tester) async { final selection = await _pumpMarkdown(tester); - final brave = - _characterCenter(tester, containing: 'Hello', character: 'v'); + final brave = _characterCenter( + tester, + containing: 'Hello', + character: 'v', + ); // Left of `brave`: the menu opens to the right of the pointer. - final hello = - _characterCenter(tester, containing: 'Hello', character: 'H'); + final hello = _characterCenter( + tester, + containing: 'Hello', + character: 'H', + ); await _doubleClick(tester, brave); expect(selection.selectedPlainText, 'brave'); @@ -142,137 +154,126 @@ void main() { variant: TargetPlatformVariant.only(TargetPlatform.windows), ); - testWidgets( - 'applies inside MixinSelectionArea', - (tester) async { - final selection = MarkdownSelectionController(); - addTearDown(selection.dispose); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: MixinSelectionArea( - controller: selection, - child: const MarkdownWidget(data: _document, useColumn: true), - ), + testWidgets('applies inside MixinSelectionArea', (tester) async { + final selection = MarkdownSelectionController(); + addTearDown(selection.dispose); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: MixinSelectionArea( + controller: selection, + child: const MarkdownWidget(data: _document, useColumn: true), ), ), - ); + ), + ); - await tester.pumpAndSettle(); - await _rightClick( - tester, - _characterCenter(tester, containing: 'Hello', character: 'v'), - ); - expect(selection.selectedPlainText, 'brave'); - expect(_menu, findsOneWidget); - }, - variant: TargetPlatformVariant.only(TargetPlatform.macOS), - ); + await tester.pumpAndSettle(); + await _rightClick( + tester, + _characterCenter(tester, containing: 'Hello', character: 'v'), + ); + expect(selection.selectedPlainText, 'brave'); + expect(_menu, findsOneWidget); + }, variant: TargetPlatformVariant.only(TargetPlatform.macOS)); }); group('menu', () { - testWidgets( - 'buttons run their action and close the menu', - (tester) async { - final selection = await _pumpMarkdown(tester); + testWidgets('buttons run their action and close the menu', (tester) async { + final selection = await _pumpMarkdown(tester); - await _rightClick( - tester, - _characterCenter(tester, containing: 'Hello', character: 'v'), - ); - await tester.tap(find.text('Select all')); - await tester.pumpAndSettle(); + await _rightClick( + tester, + _characterCenter(tester, containing: 'Hello', character: 'v'), + ); + await tester.tap(find.text('Select all')); + await tester.pumpAndSettle(); - expect(selection.selectedPlainText, contains('Second paragraph')); - expect(_menu, findsNothing); - }, - variant: TargetPlatformVariant.only(TargetPlatform.windows), - ); + expect(selection.selectedPlainText, contains('Second paragraph')); + expect(_menu, findsNothing); + }, variant: TargetPlatformVariant.only(TargetPlatform.windows)); - testWidgets( - 'Escape closes the menu and keeps the selection', - (tester) async { - final selection = await _pumpMarkdown(tester); - final brave = - _characterCenter(tester, containing: 'Hello', character: 'v'); - await _doubleClick(tester, brave); - await _rightClick(tester, brave); - expect(_menu, findsOneWidget); + testWidgets('Escape closes the menu and keeps the selection', ( + tester, + ) async { + final selection = await _pumpMarkdown(tester); + final brave = _characterCenter( + tester, + containing: 'Hello', + character: 'v', + ); + await _doubleClick(tester, brave); + await _rightClick(tester, brave); + expect(_menu, findsOneWidget); - await tester.sendKeyEvent(LogicalKeyboardKey.escape); - await tester.pumpAndSettle(); + await tester.sendKeyEvent(LogicalKeyboardKey.escape); + await tester.pumpAndSettle(); - expect(_menu, findsNothing); - expect(selection.selectedPlainText, 'brave'); - }, - variant: TargetPlatformVariant.only(TargetPlatform.windows), - ); + expect(_menu, findsNothing); + expect(selection.selectedPlainText, 'brave'); + }, variant: TargetPlatformVariant.only(TargetPlatform.windows)); - testWidgets( - 'scrolling closes the menu', - (tester) async { - final scrollController = ScrollController(); - addTearDown(scrollController.dispose); - await _pumpMarkdown( - tester, - data: [ - _document, - for (var index = 0; index < 60; index++) 'Line $index', - ].join('\n\n'), - scrollController: scrollController, - ); + testWidgets('scrolling closes the menu', (tester) async { + final scrollController = ScrollController(); + addTearDown(scrollController.dispose); + await _pumpMarkdown( + tester, + data: [ + _document, + for (var index = 0; index < 60; index++) 'Line $index', + ].join('\n\n'), + scrollController: scrollController, + ); - await _rightClick( - tester, - _characterCenter(tester, containing: 'Hello', character: 'v'), - ); - expect(_menu, findsOneWidget); + await _rightClick( + tester, + _characterCenter(tester, containing: 'Hello', character: 'v'), + ); + expect(_menu, findsOneWidget); - scrollController.jumpTo(120); - await tester.pumpAndSettle(); + scrollController.jumpTo(120); + await tester.pumpAndSettle(); - expect(_menu, findsNothing); - }, - variant: TargetPlatformVariant.only(TargetPlatform.windows), - ); + expect(_menu, findsNothing); + }, variant: TargetPlatformVariant.only(TargetPlatform.windows)); - testWidgets( - 'Copy copies the selection', - (tester) async { - final copied = _mockClipboard(tester); - await _pumpMarkdown(tester); - final brave = - _characterCenter(tester, containing: 'Hello', character: 'v'); - await _doubleClick(tester, brave); - await _rightClick(tester, brave); + testWidgets('Copy copies the selection', (tester) async { + final copied = _mockClipboard(tester); + await _pumpMarkdown(tester); + final brave = _characterCenter( + tester, + containing: 'Hello', + character: 'v', + ); + await _doubleClick(tester, brave); + await _rightClick(tester, brave); - await tester.tap(find.text('Copy')); - await tester.pumpAndSettle(); + await tester.tap(find.text('Copy')); + await tester.pumpAndSettle(); - expect(copied, ['brave']); - expect(_menu, findsNothing); - }, - variant: TargetPlatformVariant.only(TargetPlatform.windows), - ); + expect(copied, ['brave']); + expect(_menu, findsNothing); + }, variant: TargetPlatformVariant.only(TargetPlatform.windows)); - testWidgets( - 'a null builder shows no menu and keeps the selection', - (tester) async { - final selection = await _pumpMarkdown(tester, contextMenuBuilder: null); - final brave = - _characterCenter(tester, containing: 'Hello', character: 'v'); - await _doubleClick(tester, brave); + testWidgets('a null builder shows no menu and keeps the selection', ( + tester, + ) async { + final selection = await _pumpMarkdown(tester, contextMenuBuilder: null); + final brave = _characterCenter( + tester, + containing: 'Hello', + character: 'v', + ); + await _doubleClick(tester, brave); - await _rightClick( - tester, - _characterCenter(tester, containing: 'Second', character: 'S'), - ); + await _rightClick( + tester, + _characterCenter(tester, containing: 'Second', character: 'S'), + ); - expect(_menu, findsNothing); - expect(selection.selectedPlainText, 'brave'); - }, - variant: TargetPlatformVariant.only(TargetPlatform.windows), - ); + expect(_menu, findsNothing); + expect(selection.selectedPlainText, 'brave'); + }, variant: TargetPlatformVariant.only(TargetPlatform.windows)); }); group('links', () { @@ -299,85 +300,78 @@ void main() { 'https://example.com/cell', ), ]) { - testWidgets( - 'Copy link copies $name', - (tester) async { - final copied = _mockClipboard(tester); - await _pumpMarkdown(tester, data: data); - - await _rightClick( - tester, - _characterCenter( - tester, - containing: containing, - character: character, - ), - ); - await tester.tap(find.text('Copy link')); - await tester.pumpAndSettle(); - - expect(copied, [destination]); - expect(_menu, findsNothing); - }, - variant: TargetPlatformVariant.only(TargetPlatform.windows), - ); - } - - testWidgets( - 'plain text has no Copy link', - (tester) async { - await _pumpMarkdown( - tester, - data: 'Read [the docs](https://example.com/docs) first', - ); + testWidgets('Copy link copies $name', (tester) async { + final copied = _mockClipboard(tester); + await _pumpMarkdown(tester, data: data); await _rightClick( tester, - _characterCenter(tester, containing: 'Read', character: 'R'), + _characterCenter( + tester, + containing: containing, + character: character, + ), ); + await tester.tap(find.text('Copy link')); + await tester.pumpAndSettle(); - expect(_menu, findsOneWidget); - expect(find.text('Copy link'), findsNothing); - }, - variant: TargetPlatformVariant.only(TargetPlatform.windows), - ); - }); + expect(copied, [destination]); + expect(_menu, findsNothing); + }, variant: TargetPlatformVariant.only(TargetPlatform.windows)); + } - testWidgets( - 'custom builders get the details and can hide the menu', - (tester) async { - MarkdownContextMenuDetails? received; - final selection = await _pumpMarkdown( + testWidgets('plain text has no Copy link', (tester) async { + await _pumpMarkdown( tester, data: 'Read [the docs](https://example.com/docs) first', - contextMenuBuilder: (context, details) { - received = details; - return AdaptiveTextSelectionToolbar.buttonItems( - anchors: details.anchors, - buttonItems: [ - ...details.buttonItems, - ContextMenuButtonItem(label: 'Search', onPressed: details.hide), - ], - ); - }, ); - final target = - _characterCenter(tester, containing: 'Read', character: 'docs'); - await _rightClick(tester, target); - - expect(received!.linkDestination, 'https://example.com/docs'); - expect(received!.anchors.primaryAnchor, target); - expect(received!.selectionController, same(selection)); - expect( - received!.buttonItems.map((item) => item.type), - contains(ContextMenuButtonType.selectAll), + await _rightClick( + tester, + _characterCenter(tester, containing: 'Read', character: 'R'), ); - await tester.tap(find.text('Search')); - await tester.pumpAndSettle(); - expect(_menu, findsNothing); - }, - variant: TargetPlatformVariant.only(TargetPlatform.windows), - ); + expect(_menu, findsOneWidget); + expect(find.text('Copy link'), findsNothing); + }, variant: TargetPlatformVariant.only(TargetPlatform.windows)); + }); + + testWidgets('custom builders get the details and can hide the menu', ( + tester, + ) async { + MarkdownContextMenuDetails? received; + final selection = await _pumpMarkdown( + tester, + data: 'Read [the docs](https://example.com/docs) first', + contextMenuBuilder: (context, details) { + received = details; + return AdaptiveTextSelectionToolbar.buttonItems( + anchors: details.anchors, + buttonItems: [ + ...details.buttonItems, + ContextMenuButtonItem(label: 'Search', onPressed: details.hide), + ], + ); + }, + ); + final target = _characterCenter( + tester, + containing: 'Read', + character: 'docs', + ); + + await _rightClick(tester, target); + + expect(received!.linkDestination, 'https://example.com/docs'); + expect(received!.anchors.primaryAnchor, target); + expect(received!.selectionController, same(selection)); + expect( + received!.buttonItems.map((item) => item.type), + contains(ContextMenuButtonType.selectAll), + ); + + await tester.tap(find.text('Search')); + await tester.pumpAndSettle(); + expect(_menu, findsNothing); + }, variant: TargetPlatformVariant.only(TargetPlatform.windows)); } diff --git a/packages/mixin_markdown_widget/test/markdown_controller_document_test.dart b/packages/mixin_markdown_widget/test/markdown_controller_document_test.dart index 9a1cbdbc..ec23b6f6 100644 --- a/packages/mixin_markdown_widget/test/markdown_controller_document_test.dart +++ b/packages/mixin_markdown_widget/test/markdown_controller_document_test.dart @@ -4,7 +4,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mixin_markdown_widget/mixin_markdown_widget.dart'; void main() { - const data = '# Title\n\nSome **bold** text with `code`.\n\n> quote\n\n' + const data = + '# Title\n\nSome **bold** text with `code`.\n\n> quote\n\n' '| a | b |\n| - | - |\n| 1 | 2 |\n'; test('a document parsed in an isolate matches a controller parse', () async { diff --git a/packages/mixin_markdown_widget/test/markdown_controller_streaming_test.dart b/packages/mixin_markdown_widget/test/markdown_controller_streaming_test.dart index 3a47569f..8fdfc10a 100644 --- a/packages/mixin_markdown_widget/test/markdown_controller_streaming_test.dart +++ b/packages/mixin_markdown_widget/test/markdown_controller_streaming_test.dart @@ -33,10 +33,7 @@ void main() { expect(identical(controller.document.blocks.first, heading), isTrue); expect(controller.document.blocks.last.id, id); } - expect( - controller.plainText, - contains('Streaming text keeps growing.'), - ); + expect(controller.plainText, contains('Streaming text keeps growing.')); }); test('a streamed fenced code block keeps its id after it closes', () { @@ -48,7 +45,7 @@ void main() { for (final chunk in [ '\nfinal b = 2;', '\nfinal c = 3;', - '\n```' + '\n```', ]) { controller.appendChunk(chunk); final code = controller.document.blocks.last as CodeBlock; @@ -111,12 +108,10 @@ void main() { final streamedId = controller.document.blocks.last.id; controller.setData('# Title\n\nStreaming text, replaced'); - final parsed = const MarkdownDocumentParser() - .parse('# Title\n\nStreaming text, replaced'); - expect( - _blockIds(controller.document.blocks), - _blockIds(parsed.blocks), + final parsed = const MarkdownDocumentParser().parse( + '# Title\n\nStreaming text, replaced', ); + expect(_blockIds(controller.document.blocks), _blockIds(parsed.blocks)); expect(controller.document.blocks.last.id, isNot(streamedId)); }); }); diff --git a/packages/mixin_markdown_widget/test/markdown_link_recognizer_test.dart b/packages/mixin_markdown_widget/test/markdown_link_recognizer_test.dart index eb6d7410..8a7f9252 100644 --- a/packages/mixin_markdown_widget/test/markdown_link_recognizer_test.dart +++ b/packages/mixin_markdown_widget/test/markdown_link_recognizer_test.dart @@ -1,6 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mixin_markdown_widget/mixin_markdown_widget.dart'; import 'package:mixin_markdown_widget/src/render/pretext_text_block.dart'; @@ -112,8 +112,9 @@ void main() { expect(disposed.contains(recognizer), isTrue); }); - testWidgets('a new onTapLink closure keeps cached rows and is called', - (tester) async { + testWidgets('a new onTapLink closure keeps cached rows and is called', ( + tester, + ) async { final taps = []; var generation = 0; late StateSetter setState; @@ -150,8 +151,9 @@ void main() { expect(taps, ['3:https://example.com']); }); - testWidgets('removing onTapLink rebuilds rows without link recognizers', - (tester) async { + testWidgets('removing onTapLink rebuilds rows without link recognizers', ( + tester, + ) async { MarkdownTapLinkCallback? onTapLink = (destination, title, label) {}; late StateSetter setState; await tester.pumpWidget( diff --git a/packages/mixin_markdown_widget/test/markdown_material_theme_test.dart b/packages/mixin_markdown_widget/test/markdown_material_theme_test.dart new file mode 100644 index 00000000..1435ce12 --- /dev/null +++ b/packages/mixin_markdown_widget/test/markdown_material_theme_test.dart @@ -0,0 +1,43 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:material_ui/material_ui.dart'; +import 'package:mixin_markdown_widget/mixin_markdown_widget.dart'; + +void main() { + testWidgets('resolves markdown extensions from the material_ui theme', ( + tester, + ) async { + late MarkdownThemeData markdownTheme; + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + markdownTheme = MarkdownThemeData.fallback(context).copyWith( + bodyStyle: const TextStyle(color: Colors.red, fontSize: 19), + ); + return Theme( + data: ThemeData(extensions: [markdownTheme]), + child: Builder( + builder: (context) { + expect( + Theme.of(context).extension(), + same(markdownTheme), + ); + expect(MarkdownTheme.of(context), same(markdownTheme)); + return const Scaffold( + body: MarkdownWidget(data: 'Themed markdown'), + ); + }, + ), + ); + }, + ), + ), + ); + + expect( + MarkdownTheme.of(tester.element(find.byType(MarkdownWidget))), + same(markdownTheme), + ); + expect(tester.takeException(), isNull); + }); +} diff --git a/packages/mixin_markdown_widget/test/markdown_word_selection_test.dart b/packages/mixin_markdown_widget/test/markdown_word_selection_test.dart index f060b289..1639e762 100644 --- a/packages/mixin_markdown_widget/test/markdown_word_selection_test.dart +++ b/packages/mixin_markdown_widget/test/markdown_word_selection_test.dart @@ -1,12 +1,14 @@ import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mixin_markdown_widget/mixin_markdown_widget.dart'; import 'package:mixin_markdown_widget/src/render/selectable_block.dart'; /// The separator-only word rule that ASCII text keeps. ({int start, int end}) _asciiWordRangeAt(String text, int offset) { - const separators = r'.,;:!?()[]{}<>/\"' "'`|-+=*&"; + const separators = + r'.,;:!?()[]{}<>/\"' + "'`|-+=*&"; bool isWordCharacter(String character) => character.trim().isNotEmpty && !separators.contains(character); var start = offset; @@ -65,7 +67,8 @@ Future _resetTapCount(WidgetTester tester) async { void main() { testWidgets('ASCII text keeps the separator word rule', (tester) async { - const text = "foo_bar baz-qux (x.y) #tag don't a+b café_au " + const text = + "foo_bar baz-qux (x.y) #tag don't a+b café_au " 'path/to/file.dart'; for (var offset = 0; offset <= text.length; offset += 1) { final ascii = _asciiWordRangeAt(text, offset); @@ -109,8 +112,9 @@ void main() { } }); - testWidgets('double click selects a word in a Chinese paragraph', - (tester) async { + testWidgets('double click selects a word in a Chinese paragraph', ( + tester, + ) async { final selectionController = MarkdownSelectionController(); addTearDown(selectionController.dispose); await tester.pumpWidget( @@ -124,32 +128,21 @@ void main() { ), ); - await _doubleTapCharacter( - tester, - containing: '我们使用', - character: 'F', - ); + await _doubleTapCharacter(tester, containing: '我们使用', character: 'F'); expect(selectionController.selectedPlainText, 'Flutter'); await _resetTapCount(tester); - await _doubleTapCharacter( - tester, - containing: '我们使用', - character: '效', - ); + await _doubleTapCharacter(tester, containing: '我们使用', character: '效'); expect(selectionController.selectedPlainText, '效果'); await _resetTapCount(tester); - await _doubleTapCharacter( - tester, - containing: '我们使用', - character: '桌', - ); + await _doubleTapCharacter(tester, containing: '我们使用', character: '桌'); expect(selectionController.selectedPlainText, '桌面'); }); - testWidgets('double click selects a word in Chinese list text', - (tester) async { + testWidgets('double click selects a word in Chinese list text', ( + tester, + ) async { final selectionController = MarkdownSelectionController(); addTearDown(selectionController.dispose); await tester.pumpWidget( @@ -163,19 +156,11 @@ void main() { ), ); - await _doubleTapCharacter( - tester, - containing: '第一项', - character: 'F', - ); + await _doubleTapCharacter(tester, containing: '第一项', character: 'F'); expect(selectionController.selectedPlainText, 'Flutter'); await _resetTapCount(tester); - await _doubleTapCharacter( - tester, - containing: '第一项', - character: '项', - ); + await _doubleTapCharacter(tester, containing: '第一项', character: '项'); expect(selectionController.selectedPlainText, isNot(contains(':'))); expect(selectionController.selectedPlainText, contains('项')); }); diff --git a/packages/mixin_markdown_widget/test/mixin_markdown_widget_test.dart b/packages/mixin_markdown_widget/test/mixin_markdown_widget_test.dart index 952b94ac..4f79f02f 100644 --- a/packages/mixin_markdown_widget/test/mixin_markdown_widget_test.dart +++ b/packages/mixin_markdown_widget/test/mixin_markdown_widget_test.dart @@ -2,7 +2,7 @@ import 'dart:math' as math; import 'dart:io'; import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:flutter/services.dart'; import 'package:flutter_math_fork/flutter_math.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -184,8 +184,9 @@ String _inlinePlainText(List inlines) { buffer.write(_inlinePlainText((inline as StrongInline).children)); break; case MarkdownInlineKind.strikethrough: - buffer - .write(_inlinePlainText((inline as StrikethroughInline).children)); + buffer.write( + _inlinePlainText((inline as StrikethroughInline).children), + ); break; case MarkdownInlineKind.highlight: buffer.write(_inlinePlainText((inline as HighlightInline).children)); @@ -219,7 +220,8 @@ String _inlinePlainText(List inlines) { bool _hasMeaningfulHorizontalOverlap(Rect a, Rect b) { final verticalOverlap = (a.bottom <= b.top || b.bottom <= a.top) == false; - final horizontalOverlap = (a.right < b.left || b.right < a.left) == false && + final horizontalOverlap = + (a.right < b.left || b.right < a.left) == false && math.min(a.right, b.right) - math.max(a.left, b.left) > 1.0; return verticalOverlap && horizontalOverlap; } @@ -445,7 +447,9 @@ return value; '/MixinNetwork/flutter-plugins/new/main?filename=.github/instructions/*.instructions.md', ); expect( - _inlinePlainText(links[0].children), 'Add Copilot custom instructions'); + _inlinePlainText(links[0].children), + 'Add Copilot custom instructions', + ); expect( links[1].destination, 'https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot', @@ -453,26 +457,28 @@ return value; expect(_inlinePlainText(links[1].children), 'Learn how to get started'); }); - test('parses additional simple inline html tags into existing inline nodes', - () { - const input = ''' + test( + 'parses additional simple inline html tags into existing inline nodes', + () { + const input = ''' Before bold italic strike cmd span text small text underlined inserted after '''; - final document = const MarkdownDocumentParser().parse(input); - final paragraph = document.blocks.single as ParagraphBlock; - final flattened = - _flattenInlineNodes(paragraph.inlines).toList(growable: false); + final document = const MarkdownDocumentParser().parse(input); + final paragraph = document.blocks.single as ParagraphBlock; + final flattened = _flattenInlineNodes(paragraph.inlines) + .toList(growable: false); - expect(flattened.any((inline) => inline is StrongInline), isTrue); - expect(flattened.any((inline) => inline is EmphasisInline), isTrue); - expect(flattened.any((inline) => inline is StrikethroughInline), isTrue); - expect(flattened.any((inline) => inline is InlineCode), isTrue); - expect( - _inlinePlainText(paragraph.inlines), - 'Before bold italic strike cmd span text small text underlined inserted after', - ); - }); + expect(flattened.any((inline) => inline is StrongInline), isTrue); + expect(flattened.any((inline) => inline is EmphasisInline), isTrue); + expect(flattened.any((inline) => inline is StrikethroughInline), isTrue); + expect(flattened.any((inline) => inline is InlineCode), isTrue); + expect( + _inlinePlainText(paragraph.inlines), + 'Before bold italic strike cmd span text small text underlined inserted after', + ); + }, + ); test('uses collapsed details blocks by default', () { const block = DetailsBlock( @@ -531,8 +537,10 @@ from the workspace leaving a stale workspace.ref file. controller.plainText, contains('Release notes\n\npackage:build_runner v2.14.1'), ); - expect(controller.plainText, - contains('was removed from the workspace leaving a stale')); + expect( + controller.plainText, + contains('was removed from the workspace leaving a stale'), + ); }); test('parses markdown blocks nested inside html details', () { @@ -551,11 +559,15 @@ You can trigger Dependabot actions by commenting on this PR: final details = document.blocks.single as DetailsBlock; expect( - _inlinePlainText(details.summary), 'Dependabot commands and options'); + _inlinePlainText(details.summary), + 'Dependabot commands and options', + ); expect(details.children, hasLength(3)); expect(details.children[0], isA()); - expect((details.children[0] as ParagraphBlock).inlines.single, - isA()); + expect( + (details.children[0] as ParagraphBlock).inlines.single, + isA(), + ); expect(details.children[1], isA()); expect(details.children[2], isA()); @@ -563,8 +575,10 @@ You can trigger Dependabot actions by commenting on this PR: expect(list.ordered, isFalse); expect(list.items, hasLength(2)); final firstItem = list.items.first.children.single as ParagraphBlock; - expect(_inlinePlainText(firstItem.inlines), - '@dependabot rebase will rebase this PR'); + expect( + _inlinePlainText(firstItem.inlines), + '@dependabot rebase will rebase this PR', + ); }); test('preserves indented markdown inside html details', () { @@ -608,16 +622,15 @@ You can trigger Dependabot actions by commenting on this PR: expect(_inlinePlainText(paragraph.inlines), 'Before\nAfter'); }); - testWidgets('renders standalone html br without paragraph block spacing', - (tester) async { + testWidgets('renders standalone html br without paragraph block spacing', ( + tester, + ) async { await tester.pumpWidget( MaterialApp( home: Builder( builder: (context) { - final theme = MarkdownThemeData.fallback(context).copyWith( - blockSpacing: 80, - padding: EdgeInsets.zero, - ); + final theme = MarkdownThemeData.fallback(context) + .copyWith(blockSpacing: 80, padding: EdgeInsets.zero); return Scaffold( body: MarkdownWidget( data: ''' @@ -749,10 +762,7 @@ $$ .toList(growable: false); expect(mathInlines, hasLength(1)); expect(mathInlines.single.displayStyle, isFalse); - expect( - mathInlines.single.tex, - r'x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}', - ); + expect(mathInlines.single.tex, r'x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}'); final controller = MarkdownController(data: input); expect( @@ -792,23 +802,34 @@ $$ expect(controller.streamingState.committedBlocks, hasLength(2)); }); - test('assigns source ranges and reuses committed prefix blocks on append', - () { - final controller = MarkdownController(data: '# Title\n\nIntro'); - final initialHeading = controller.document.blocks.first as HeadingBlock; - final initialParagraph = controller.document.blocks[1] as ParagraphBlock; + test( + 'assigns source ranges and reuses committed prefix blocks on append', + () { + final controller = MarkdownController(data: '# Title\n\nIntro'); + final initialHeading = controller.document.blocks.first as HeadingBlock; + final initialParagraph = controller.document.blocks[1] as ParagraphBlock; - expect(initialHeading.sourceRange, isNotNull); - expect(initialHeading.sourceRange!.start, 0); + expect(initialHeading.sourceRange, isNotNull); + expect(initialHeading.sourceRange!.start, 0); - controller.appendChunk('\n\nParagraph'); + controller.appendChunk('\n\nParagraph'); - expect(controller.document.blocks, hasLength(3)); - expect(identical(controller.document.blocks.first, initialHeading), isTrue); - expect(identical(controller.document.blocks[1], initialParagraph), isTrue); - expect(controller.document.blocks.last.sourceRange, isNotNull); - expect(controller.document.blocks.last.sourceRange!.start, greaterThan(0)); - }); + expect(controller.document.blocks, hasLength(3)); + expect( + identical(controller.document.blocks.first, initialHeading), + isTrue, + ); + expect( + identical(controller.document.blocks[1], initialParagraph), + isTrue, + ); + expect(controller.document.blocks.last.sourceRange, isNotNull); + expect( + controller.document.blocks.last.sourceRange!.start, + greaterThan(0), + ); + }, + ); test('normalizes CRLF split across appended chunks', () { final controller = MarkdownController(data: 'Line\r'); @@ -834,25 +855,28 @@ $$ expect(documentNotifications, 1); }); - test('assigns source ranges for setext headings and indented code blocks', - () { - const input = 'Title\n=====\n\n final answer = 42;\n'; + test( + 'assigns source ranges for setext headings and indented code blocks', + () { + const input = 'Title\n=====\n\n final answer = 42;\n'; - final document = const MarkdownDocumentParser().parse(input); + final document = const MarkdownDocumentParser().parse(input); - expect(document.blocks, hasLength(2)); - expect(document.blocks.first, isA()); - expect(document.blocks.last, isA()); - expect(document.blocks.first.sourceRange, isNotNull); - expect(document.blocks.last.sourceRange, isNotNull); - expect( - document.blocks.last.sourceRange!.start, - greaterThan(document.blocks.first.sourceRange!.end), - ); - }); + expect(document.blocks, hasLength(2)); + expect(document.blocks.first, isA()); + expect(document.blocks.last, isA()); + expect(document.blocks.first.sourceRange, isNotNull); + expect(document.blocks.last.sourceRange, isNotNull); + expect( + document.blocks.last.sourceRange!.start, + greaterThan(document.blocks.first.sourceRange!.end), + ); + }, + ); test('keeps top-level fenced code range separate after a list', () { - const input = '# Benchmark\n' + const input = + '# Benchmark\n' '\n' 'Paragraph with a [link](https://example.com/0).\n' '\n' @@ -902,10 +926,14 @@ $$ expect(controller.document.blocks, hasLength(3)); expect( - identical(controller.document.blocks.first, initialParagraph), isTrue); + identical(controller.document.blocks.first, initialParagraph), + isTrue, + ); expect(controller.document.blocks[1], isA()); expect( - identical(controller.document.blocks.last, initialFootnotes), isTrue); + identical(controller.document.blocks.last, initialFootnotes), + isTrue, + ); expect(timings.single.parseLineCount, 3); }); @@ -925,7 +953,9 @@ $$ expect(controller.document.blocks, hasLength(3)); expect( - identical(controller.document.blocks.first, initialParagraph), isTrue); + identical(controller.document.blocks.first, initialParagraph), + isTrue, + ); expect(controller.document.blocks[1], isA()); final footnotes = controller.document.blocks.last as FootnoteListBlock; expect(footnotes.items, hasLength(2)); @@ -1057,30 +1087,32 @@ Reference[^note] expect(text.trimRight(), '1. Footnote link (https://example.com)'); }); - test('serializes a fully selected definition list with link destinations', - () { - const input = ''' + test( + 'serializes a fully selected definition list with link destinations', + () { + const input = ''' Term : before [link](https://example.com) after '''; - final controller = MarkdownController(data: input); - const serializer = MarkdownPlainTextSerializer(); - final definitionList = - controller.document.blocks.single as DefinitionListBlock; - final structure = StructuredBlockSelection.forBlock(definitionList); + final controller = MarkdownController(data: input); + const serializer = MarkdownPlainTextSerializer(); + final definitionList = + controller.document.blocks.single as DefinitionListBlock; + final structure = StructuredBlockSelection.forBlock(definitionList); - final text = serializer.serializeSelection( - controller.document, - DocumentSelection( - base: structure.startPosition(blockIndex: 0), - extent: structure.endPosition(blockIndex: 0), - ), - ); + final text = serializer.serializeSelection( + controller.document, + DocumentSelection( + base: structure.startPosition(blockIndex: 0), + extent: structure.endPosition(blockIndex: 0), + ), + ); - expect(structure.plainText, 'Term\n: before link after'); - expect(text, 'Term\n: before link (https://example.com) after'); - }); + expect(structure.plainText, 'Term\n: before link after'); + expect(text, 'Term\n: before link (https://example.com) after'); + }, + ); test('selection controller selects and exposes plain text', () { final controller = MarkdownController(data: '# Heading\n\nParagraph'); @@ -1114,9 +1146,7 @@ Term await tester.pumpWidget( MaterialApp( - home: Scaffold( - body: MarkdownWidget(controller: controller), - ), + home: Scaffold(body: MarkdownWidget(controller: controller)), ), ); @@ -1156,9 +1186,7 @@ Term expect(find.byType(Divider), findsNWidgets(2)); }); - testWidgets('uses pretext for rich inline paragraphs too', ( - tester, - ) async { + testWidgets('uses pretext for rich inline paragraphs too', (tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -1193,9 +1221,7 @@ Term await tester.pumpWidget( const MaterialApp( home: Scaffold( - body: MarkdownWidget( - data: r'Inline $a^2+b^2=c^2$ formula.', - ), + body: MarkdownWidget(data: r'Inline $a^2+b^2=c^2$ formula.'), ), ), ); @@ -1215,109 +1241,273 @@ Term expect(widgetSpans.single.baseline, TextBaseline.alphabetic); }); - testWidgets('math selection geometry tracks live widget bounds in paragraphs', - (tester) async { + testWidgets( + 'math selection geometry tracks live widget bounds in paragraphs', + (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: MarkdownWidget(data: r'Before $x^2$ after')), + ), + ); + + final blockFinder = find.byWidgetPredicate( + (widget) => + widget is SelectableMarkdownBlock && + widget.spec.plainText.contains('Before x^2 after'), + ); + final block = tester.widget(blockFinder); + final blockContext = tester.element(blockFinder); + final blockRenderBox = tester.renderObject(blockFinder); + final blockOrigin = blockRenderBox.localToGlobal(Offset.zero); + final mathRect = tester.getRect(find.byType(Math).first); + final mathStart = block.spec.plainText.indexOf('x^2'); + final mathEnd = mathStart + 'x^2'.length; + + final selectionRects = block.spec.selectionRectResolver!( + blockContext, + blockRenderBox.size, + DocumentRange( + start: DocumentPosition( + blockIndex: 0, + path: const PathInBlock([0]), + textOffset: mathStart, + ), + end: DocumentPosition( + blockIndex: 0, + path: const PathInBlock([0]), + textOffset: mathEnd, + ), + ), + ); + final globalSelectionRects = selectionRects + .map((rect) => rect.shift(blockOrigin)) + .toList(growable: false); + + expect(globalSelectionRects, isNotEmpty); + expect( + globalSelectionRects.any((rect) => rect.overlaps(mathRect.inflate(1))), + isTrue, + ); + final mathSelectionRect = globalSelectionRects.firstWhere( + (rect) => rect.overlaps(mathRect.inflate(1)), + ); + expect(mathSelectionRect.contains(mathRect.center), isTrue); + expect(mathSelectionRect.width, lessThan(mathRect.width + 16)); + expect(mathSelectionRect.left, lessThanOrEqualTo(mathRect.left + 6)); + expect(mathSelectionRect.right, greaterThanOrEqualTo(mathRect.right - 6)); + + final leftOffset = block.spec.textOffsetResolver!( + blockContext, + blockRenderBox.size, + blockRenderBox.globalToLocal(mathRect.centerLeft + const Offset(1, 0)), + ); + final rightOffset = block.spec.textOffsetResolver!( + blockContext, + blockRenderBox.size, + blockRenderBox.globalToLocal(mathRect.centerRight + const Offset(2, 0)), + ); + + expect(leftOffset, mathStart); + expect(rightOffset, mathEnd); + }, + ); + + testWidgets( + 'math selection geometry tracks live widget bounds after inline code', + (tester) async { + const data = r'test `math` a \( x = \frac{-b \pm \sqrt{b^2-4ac}}{2a} \)'; + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: MarkdownWidget(data: data)), + ), + ); + + final blockFinder = find.byWidgetPredicate( + (widget) => + widget is SelectableMarkdownBlock && + widget.spec.plainText.contains('test math a ') && + widget.spec.plainText.contains( + r'x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}', + ), + ); + final block = tester.widget(blockFinder); + final blockContext = tester.element(blockFinder); + final blockRenderBox = tester.renderObject(blockFinder); + final blockOrigin = blockRenderBox.localToGlobal(Offset.zero); + final mathRect = tester.getRect(find.byType(Math).first); + final mathStart = block.spec.plainText.indexOf( + r'x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}', + ); + final mathEnd = + mathStart + r'x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}'.length; + + final selectionRects = block.spec.selectionRectResolver!( + blockContext, + blockRenderBox.size, + DocumentRange( + start: DocumentPosition( + blockIndex: 0, + path: const PathInBlock([0]), + textOffset: mathStart, + ), + end: DocumentPosition( + blockIndex: 0, + path: const PathInBlock([0]), + textOffset: mathEnd, + ), + ), + ); + final globalSelectionRects = selectionRects + .map((rect) => rect.shift(blockOrigin)) + .toList(growable: false); + + expect(globalSelectionRects, isNotEmpty); + final mathSelectionRect = globalSelectionRects.firstWhere( + (rect) => rect.overlaps(mathRect.inflate(1)), + ); + expect(mathSelectionRect.contains(mathRect.center), isTrue); + expect(mathSelectionRect.width, lessThan(mathRect.width + 16)); + expect(mathSelectionRect.left, lessThanOrEqualTo(mathRect.left + 6)); + expect(mathSelectionRect.right, greaterThanOrEqualTo(mathRect.right - 6)); + + final leftOffset = block.spec.textOffsetResolver!( + blockContext, + blockRenderBox.size, + blockRenderBox.globalToLocal(mathRect.centerLeft + const Offset(1, 0)), + ); + final rightOffset = block.spec.textOffsetResolver!( + blockContext, + blockRenderBox.size, + blockRenderBox.globalToLocal(mathRect.centerRight + const Offset(2, 0)), + ); + + expect(leftOffset, mathStart); + expect(rightOffset, mathEnd); + }, + ); + + testWidgets('inline code stays compact when a paragraph also contains math', ( + tester, + ) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownWidget( - data: r'Before $x^2$ after', + data: r'test `math` a \( x = \frac{-b \pm \sqrt{b^2-4ac}}{2a} \)', ), ), ), ); - final blockFinder = find.byWidgetPredicate( - (widget) => - widget is SelectableMarkdownBlock && - widget.spec.plainText.contains('Before x^2 after'), - ); - final block = tester.widget(blockFinder); - final blockContext = tester.element(blockFinder); - final blockRenderBox = tester.renderObject(blockFinder); - final blockOrigin = blockRenderBox.localToGlobal(Offset.zero); - final mathRect = tester.getRect(find.byType(Math).first); - final mathStart = block.spec.plainText.indexOf('x^2'); - final mathEnd = mathStart + 'x^2'.length; + expect(_decoratedInlineTextFinder(), findsWidgets); + }); - final selectionRects = block.spec.selectionRectResolver!( - blockContext, - blockRenderBox.size, - DocumentRange( - start: DocumentPosition( - blockIndex: 0, - path: const PathInBlock([0]), - textOffset: mathStart, + testWidgets( + 'selection across text inline code and math does not produce overlapping boxes', + (tester) async { + const data = r'test `math` a \( x = \frac{-b \pm \sqrt{b^2-4ac}}{2a} \)'; + const mathText = r'x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}'; + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: MarkdownWidget(data: data)), ), - end: DocumentPosition( - blockIndex: 0, - path: const PathInBlock([0]), - textOffset: mathEnd, + ); + + final blockFinder = find.byWidgetPredicate( + (widget) => + widget is SelectableMarkdownBlock && + widget.spec.plainText.contains('test math a ') && + widget.spec.plainText.contains(mathText), + ); + final block = tester.widget(blockFinder); + final blockContext = tester.element(blockFinder); + final blockRenderBox = tester.renderObject(blockFinder); + final mathEnd = block.spec.plainText.indexOf(mathText) + mathText.length; + final selectionRects = block.spec.selectionRectResolver!( + blockContext, + blockRenderBox.size, + DocumentRange( + start: const DocumentPosition( + blockIndex: 0, + path: PathInBlock([0]), + textOffset: 0, + ), + end: DocumentPosition( + blockIndex: 0, + path: const PathInBlock([0]), + textOffset: mathEnd, + ), ), + ); + + expect(selectionRects, isNotEmpty); + final sortedRects = selectionRects.toList(growable: false) + ..sort((a, b) { + final topCompare = a.top.compareTo(b.top); + if (topCompare != 0) { + return topCompare; + } + return a.left.compareTo(b.left); + }); + for (var index = 0; index < sortedRects.length - 1; index++) { + final current = sortedRects[index]; + final next = sortedRects[index + 1]; + final sameLine = + (next.top - current.top).abs() <= 2.0 && + (next.bottom - current.bottom).abs() <= 2.0; + if (!sameLine) { + continue; + } + expect(current.overlaps(next), isFalse); + } + }, + ); + + testWidgets('pretext blocks with math paint selection above child', ( + tester, + ) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: MarkdownWidget(data: r'before $x^2$ after')), ), ); - final globalSelectionRects = selectionRects - .map((rect) => rect.shift(blockOrigin)) - .toList(growable: false); - expect(globalSelectionRects, isNotEmpty); - expect( - globalSelectionRects.any((rect) => rect.overlaps(mathRect.inflate(1))), - isTrue, - ); - final mathSelectionRect = globalSelectionRects.firstWhere( - (rect) => rect.overlaps(mathRect.inflate(1)), + final paragraphFinder = find.byWidgetPredicate( + (widget) => + widget is SelectableMarkdownBlock && + widget.spec.plainText.contains('before x^2 after'), ); - expect(mathSelectionRect.contains(mathRect.center), isTrue); - expect(mathSelectionRect.width, lessThan(mathRect.width + 16)); - expect(mathSelectionRect.left, lessThanOrEqualTo(mathRect.left + 6)); - expect(mathSelectionRect.right, greaterThanOrEqualTo(mathRect.right - 6)); + final paragraph = tester.widget(paragraphFinder); - final leftOffset = block.spec.textOffsetResolver!( - blockContext, - blockRenderBox.size, - blockRenderBox.globalToLocal(mathRect.centerLeft + const Offset(1, 0)), - ); - final rightOffset = block.spec.textOffsetResolver!( - blockContext, - blockRenderBox.size, - blockRenderBox.globalToLocal(mathRect.centerRight + const Offset(2, 0)), + expect( + paragraph.spec.selectionPaintOrder, + SelectableBlockSelectionPaintOrder.aboveChild, ); - - expect(leftOffset, mathStart); - expect(rightOffset, mathEnd); }); - testWidgets( - 'math selection geometry tracks live widget bounds after inline code', - (tester) async { - const data = r'test `math` a \( x = \frac{-b \pm \sqrt{b^2-4ac}}{2a} \)'; - + testWidgets('display math selection geometry tracks live widget bounds', ( + tester, + ) async { await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: MarkdownWidget( - data: data, - ), - ), + home: Scaffold(body: MarkdownWidget(data: r'$$x^2$$')), ), ); final blockFinder = find.byWidgetPredicate( (widget) => widget is SelectableMarkdownBlock && - widget.spec.plainText.contains('test math a ') && - widget.spec.plainText - .contains(r'x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}'), + widget.spec.plainText.contains('x^2'), ); final block = tester.widget(blockFinder); final blockContext = tester.element(blockFinder); final blockRenderBox = tester.renderObject(blockFinder); final blockOrigin = blockRenderBox.localToGlobal(Offset.zero); final mathRect = tester.getRect(find.byType(Math).first); - final mathStart = - block.spec.plainText.indexOf(r'x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}'); - final mathEnd = mathStart + r'x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}'.length; + final mathStart = block.spec.plainText.indexOf('x^2'); + final mathEnd = mathStart + 'x^2'.length; final selectionRects = block.spec.selectionRectResolver!( blockContext, @@ -1347,267 +1537,296 @@ Term expect(mathSelectionRect.width, lessThan(mathRect.width + 16)); expect(mathSelectionRect.left, lessThanOrEqualTo(mathRect.left + 6)); expect(mathSelectionRect.right, greaterThanOrEqualTo(mathRect.right - 6)); - - final leftOffset = block.spec.textOffsetResolver!( - blockContext, - blockRenderBox.size, - blockRenderBox.globalToLocal(mathRect.centerLeft + const Offset(1, 0)), - ); - final rightOffset = block.spec.textOffsetResolver!( - blockContext, - blockRenderBox.size, - blockRenderBox.globalToLocal(mathRect.centerRight + const Offset(2, 0)), - ); - - expect(leftOffset, mathStart); - expect(rightOffset, mathEnd); }); - testWidgets('inline code stays compact when a paragraph also contains math', - (tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Scaffold( - body: MarkdownWidget( - data: r'test `math` a \( x = \frac{-b \pm \sqrt{b^2-4ac}}{2a} \)', + testWidgets( + 'math selection geometry tracks live widget bounds inside lists', + (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: MarkdownWidget(data: r'- Before $x^2$ after')), + ), + ); + + final listBlockFinder = find.byWidgetPredicate( + (widget) => + widget is SelectableMarkdownBlock && + widget.spec.plainText.contains('- Before x^2 after'), + ); + final listBlock = tester.widget(listBlockFinder); + final listContext = tester.element(listBlockFinder); + final listRenderBox = tester.renderObject(listBlockFinder); + final listOrigin = listRenderBox.localToGlobal(Offset.zero); + final mathRect = tester.getRect(find.byType(Math).first); + final mathStart = listBlock.spec.plainText.indexOf('x^2'); + final mathEnd = mathStart + 'x^2'.length; + + final selectionRects = listBlock.spec.selectionRectResolver!( + listContext, + listRenderBox.size, + DocumentRange( + start: DocumentPosition( + blockIndex: 0, + path: const PathInBlock([0]), + textOffset: mathStart, + ), + end: DocumentPosition( + blockIndex: 0, + path: const PathInBlock([0]), + textOffset: mathEnd, ), ), - ), - ); + ); + final globalSelectionRects = selectionRects + .map((rect) => rect.shift(listOrigin)) + .toList(growable: false); - expect(_decoratedInlineTextFinder(), findsWidgets); - }); + expect(globalSelectionRects, isNotEmpty); + expect( + globalSelectionRects.any((rect) => rect.overlaps(mathRect.inflate(1))), + isTrue, + ); - testWidgets( - 'selection across text inline code and math does not produce overlapping boxes', - (tester) async { - const data = r'test `math` a \( x = \frac{-b \pm \sqrt{b^2-4ac}}{2a} \)'; - const mathText = r'x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}'; + final leftOffset = listBlock.spec.textOffsetResolver!( + listContext, + listRenderBox.size, + listRenderBox.globalToLocal(mathRect.centerLeft + const Offset(1, 0)), + ); + final rightOffset = listBlock.spec.textOffsetResolver!( + listContext, + listRenderBox.size, + listRenderBox.globalToLocal(mathRect.centerRight + const Offset(2, 0)), + ); + + expect(leftOffset, mathStart); + expect(rightOffset, mathEnd); + }, + ); + testWidgets('renders backslash inline math with surrounding whitespace', ( + tester, + ) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownWidget( - data: data, + data: r'Quadratic formula: \( x = \frac{-b \pm \sqrt{b^2-4ac}}{2a} \).', ), ), ), ); - final blockFinder = find.byWidgetPredicate( - (widget) => - widget is SelectableMarkdownBlock && - widget.spec.plainText.contains('test math a ') && - widget.spec.plainText.contains(mathText), - ); - final block = tester.widget(blockFinder); - final blockContext = tester.element(blockFinder); - final blockRenderBox = tester.renderObject(blockFinder); - final mathEnd = block.spec.plainText.indexOf(mathText) + mathText.length; - final selectionRects = block.spec.selectionRectResolver!( - blockContext, - blockRenderBox.size, - DocumentRange( - start: const DocumentPosition( - blockIndex: 0, - path: PathInBlock([0]), - textOffset: 0, - ), - end: DocumentPosition( - blockIndex: 0, - path: const PathInBlock([0]), - textOffset: mathEnd, - ), - ), - ); - - expect(selectionRects, isNotEmpty); - final sortedRects = selectionRects.toList(growable: false) - ..sort((a, b) { - final topCompare = a.top.compareTo(b.top); - if (topCompare != 0) { - return topCompare; - } - return a.left.compareTo(b.left); - }); - for (var index = 0; index < sortedRects.length - 1; index++) { - final current = sortedRects[index]; - final next = sortedRects[index + 1]; - final sameLine = (next.top - current.top).abs() <= 2.0 && - (next.bottom - current.bottom).abs() <= 2.0; - if (!sameLine) { - continue; - } - expect(current.overlaps(next), isFalse); - } + expect(find.byType(Math), findsOneWidget); + expect(find.textContaining(r'\('), findsNothing); + expect(find.textContaining(r'\)'), findsNothing); }); - testWidgets('pretext blocks with math paint selection above child', - (tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Scaffold( - body: MarkdownWidget(data: r'before $x^2$ after'), + testWidgets( + 'tap on links still triggers onTapLink when selection is enabled', + (tester) async { + String? tappedDestination; + String? tappedLabel; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: MarkdownWidget( + data: 'Visit [Example](https://example.com)', + onTapLink: (destination, title, label) { + tappedDestination = destination; + tappedLabel = label; + }, + ), + ), ), - ), - ); + ); - final paragraphFinder = find.byWidgetPredicate( - (widget) => - widget is SelectableMarkdownBlock && - widget.spec.plainText.contains('before x^2 after'), - ); - final paragraph = tester.widget(paragraphFinder); + final richTextFinder = find.byWidgetPredicate( + (widget) => + widget is RichText && + widget.text.toPlainText().contains('Visit Example'), + ); + final richText = tester.widget(richTextFinder); + final renderBox = tester.renderObject(richTextFinder); + final painter = TextPainter( + text: richText.text, + textDirection: TextDirection.ltr, + )..layout(maxWidth: renderBox.size.width); + final linkBoxes = painter.getBoxesForSelection( + const TextSelection(baseOffset: 6, extentOffset: 13), + ); - expect( - paragraph.spec.selectionPaintOrder, - SelectableBlockSelectionPaintOrder.aboveChild, - ); - }); + await tester.tapAt( + renderBox.localToGlobal(linkBoxes.first.toRect().center), + ); + await tester.pump(); - testWidgets('display math selection geometry tracks live widget bounds', ( - tester, - ) async { + expect(tappedDestination, 'https://example.com'); + expect(tappedLabel, 'Example'); + }, + ); + + testWidgets('renders html details as a collapsible block', (tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownWidget( - data: r'$$x^2$$', + data: ''' +
+Dependabot commands and options +
+ +You can trigger Dependabot actions by commenting on this PR: +- `@dependabot rebase` will rebase this PR +- `@dependabot recreate` will recreate this PR +
+''', ), ), ), ); - final blockFinder = find.byWidgetPredicate( - (widget) => - widget is SelectableMarkdownBlock && - widget.spec.plainText.contains('x^2'), - ); - final block = tester.widget(blockFinder); - final blockContext = tester.element(blockFinder); - final blockRenderBox = tester.renderObject(blockFinder); - final blockOrigin = blockRenderBox.localToGlobal(Offset.zero); - final mathRect = tester.getRect(find.byType(Math).first); - final mathStart = block.spec.plainText.indexOf('x^2'); - final mathEnd = mathStart + 'x^2'.length; + expect(find.textContaining('Dependabot commands and options'), findsOne); + expect(find.byType(MarkdownListBlockView), findsNothing); - final selectionRects = block.spec.selectionRectResolver!( - blockContext, - blockRenderBox.size, - DocumentRange( - start: DocumentPosition( - blockIndex: 0, - path: const PathInBlock([0]), - textOffset: mathStart, - ), - end: DocumentPosition( - blockIndex: 0, - path: const PathInBlock([0]), - textOffset: mathEnd, - ), - ), - ); - final globalSelectionRects = selectionRects - .map((rect) => rect.shift(blockOrigin)) - .toList(growable: false); + await tester.tap(find.byIcon(Icons.chevron_right_rounded)); + await tester.pumpAndSettle(); + await tester.pump(kDoubleTapTimeout + const Duration(milliseconds: 1)); - expect(globalSelectionRects, isNotEmpty); - final mathSelectionRect = globalSelectionRects.firstWhere( - (rect) => rect.overlaps(mathRect.inflate(1)), - ); - expect(mathSelectionRect.contains(mathRect.center), isTrue); - expect(mathSelectionRect.width, lessThan(mathRect.width + 16)); - expect(mathSelectionRect.left, lessThanOrEqualTo(mathRect.left + 6)); - expect(mathSelectionRect.right, greaterThanOrEqualTo(mathRect.right - 6)); + expect(find.byIcon(Icons.expand_more_rounded), findsOne); + expect(find.byType(MarkdownListBlockView), findsOne); }); - testWidgets('math selection geometry tracks live widget bounds inside lists', - (tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Scaffold( - body: MarkdownWidget( - data: r'- Before $x^2$ after', + testWidgets( + 'details paints selection backgrounds for nested quote and list', + (tester) async { + final selectionController = MarkdownSelectionController(); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: MarkdownWidget( + data: ''' +
+Release notes +
+Quoted line +
+
+
    +
  • List item
  • +
+
+''', + selectionController: selectionController, + ), ), ), - ), - ); + ); - final listBlockFinder = find.byWidgetPredicate( - (widget) => - widget is SelectableMarkdownBlock && - widget.spec.plainText.contains('- Before x^2 after'), - ); - final listBlock = tester.widget(listBlockFinder); - final listContext = tester.element(listBlockFinder); - final listRenderBox = tester.renderObject(listBlockFinder); - final listOrigin = listRenderBox.localToGlobal(Offset.zero); - final mathRect = tester.getRect(find.byType(Math).first); - final mathStart = listBlock.spec.plainText.indexOf('x^2'); - final mathEnd = mathStart + 'x^2'.length; + await tester.tap(find.byIcon(Icons.chevron_right_rounded)); + await tester.pumpAndSettle(); - final selectionRects = listBlock.spec.selectionRectResolver!( - listContext, - listRenderBox.size, - DocumentRange( - start: DocumentPosition( - blockIndex: 0, - path: const PathInBlock([0]), - textOffset: mathStart, + final detailsFinder = find.byWidgetPredicate( + (widget) => + widget is SelectableMarkdownBlock && + widget.spec.plainText.contains('Release notes') && + widget.spec.plainText.contains('Quoted line') && + widget.spec.plainText.contains('List item'), + ); + final detailsBlock = tester.widget( + detailsFinder, + ); + expect(detailsBlock.spec.selectionStructure, isNotNull); + expect(detailsBlock.spec.selectionColor, isNull); + final globalSelectionRects = _globalSelectionRectsForBlock( + tester, + detailsFinder, + start: 0, + end: detailsBlock.spec.plainText.length, + ); + final quotedTextRect = tester.getRect( + find.byWidgetPredicate( + (widget) => + widget is RichText && + widget.text.toPlainText().contains('Quoted line'), ), - end: DocumentPosition( - blockIndex: 0, - path: const PathInBlock([0]), - textOffset: mathEnd, + ); + final listTextRect = tester.getRect( + find.byWidgetPredicate( + (widget) => + widget is RichText && + widget.text.toPlainText().contains('List item'), ), - ), - ); - final globalSelectionRects = selectionRects - .map((rect) => rect.shift(listOrigin)) - .toList(growable: false); - - expect(globalSelectionRects, isNotEmpty); - expect( - globalSelectionRects.any((rect) => rect.overlaps(mathRect.inflate(1))), - isTrue, - ); + ); - final leftOffset = listBlock.spec.textOffsetResolver!( - listContext, - listRenderBox.size, - listRenderBox.globalToLocal(mathRect.centerLeft + const Offset(1, 0)), - ); - final rightOffset = listBlock.spec.textOffsetResolver!( - listContext, - listRenderBox.size, - listRenderBox.globalToLocal(mathRect.centerRight + const Offset(2, 0)), - ); + expect( + globalSelectionRects.any((rect) => rect.overlaps(quotedTextRect)), + isTrue, + ); + expect( + globalSelectionRects.any((rect) => rect.overlaps(listTextRect)), + isTrue, + ); - expect(leftOffset, mathStart); - expect(rightOffset, mathEnd); - }); + final detailsElement = tester.element(detailsFinder); + final detailsRenderBox = tester.renderObject(detailsFinder); + final listOffset = detailsBlock.spec.textOffsetResolver!( + detailsElement, + detailsRenderBox.size, + detailsRenderBox.globalToLocal(listTextRect.center), + ); + expect( + listOffset, + greaterThan(detailsBlock.spec.plainText.indexOf('List item')), + ); - testWidgets('renders backslash inline math with surrounding whitespace', ( - tester, - ) async { - await tester.pumpWidget( - const MaterialApp( - home: Scaffold( - body: MarkdownWidget( - data: - r'Quadratic formula: \( x = \frac{-b \pm \sqrt{b^2-4ac}}{2a} \).', + final listEnd = + detailsBlock.spec.plainText.indexOf('List item') + 'List item'.length; + selectionController.setSelection( + DocumentSelection( + base: detailsBlock.spec.selectionStructure!.startPosition( + blockIndex: 0, ), + extent: detailsBlock.spec.selectionStructure! + .positionForDisplayedOffset( + blockIndex: 0, + displayedOffset: listEnd, + ), ), - ), - ); + ); + await tester.pump(); - expect(find.byType(Math), findsOneWidget); - expect(find.textContaining(r'\('), findsNothing); - expect(find.textContaining(r'\)'), findsNothing); - }); + final detailsAfterSelection = tester.widget( + detailsFinder, + ); + final selectedGlobalRects = _globalSelectionRectsForBlock( + tester, + detailsFinder, + start: 0, + end: + detailsAfterSelection.spec.plainText.indexOf('List item') + + 'List item'.length, + ); + expect( + selectedGlobalRects.any((rect) => rect.overlaps(quotedTextRect)), + isTrue, + ); + expect( + selectedGlobalRects.any((rect) => rect.overlaps(listTextRect)), + isTrue, + ); + expect(tester.takeException(), isNull); + + final paintFinder = find.ancestor( + of: detailsFinder, + matching: find.byType(CustomPaint), + ); + final detailsPaint = tester.widget(paintFinder.first); + expect(detailsPaint.foregroundPainter, isNotNull); + }, + ); - testWidgets('tap on links still triggers onTapLink when selection is enabled', - ( + testWidgets('tap on inline html anchor tags triggers onTapLink', ( tester, ) async { String? tappedDestination; @@ -1617,7 +1836,7 @@ Term MaterialApp( home: Scaffold( body: MarkdownWidget( - data: 'Visit [Example](https://example.com)', + data: '💡 Learn how to get started.', onTapLink: (destination, title, label) { tappedDestination = destination; tappedLabel = label; @@ -1630,7 +1849,7 @@ Term final richTextFinder = find.byWidgetPredicate( (widget) => widget is RichText && - widget.text.toPlainText().contains('Visit Example'), + widget.text.toPlainText().contains('Learn how to get started'), ); final richText = tester.widget(richTextFinder); final renderBox = tester.renderObject(richTextFinder); @@ -1638,8 +1857,11 @@ Term text: richText.text, textDirection: TextDirection.ltr, )..layout(maxWidth: renderBox.size.width); + final plainText = richText.text.toPlainText(); + final start = plainText.indexOf('Learn how to get started'); + final end = start + 'Learn how to get started'.length; final linkBoxes = painter.getBoxesForSelection( - const TextSelection(baseOffset: 6, extentOffset: 13), + TextSelection(baseOffset: start, extentOffset: end), ); await tester.tapAt( @@ -1647,216 +1869,16 @@ Term ); await tester.pump(); - expect(tappedDestination, 'https://example.com'); - expect(tappedLabel, 'Example'); + expect( + tappedDestination, + 'https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot', + ); + expect(tappedLabel, 'Learn how to get started'); }); - testWidgets('renders html details as a collapsible block', (tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Scaffold( - body: MarkdownWidget( - data: ''' -
-Dependabot commands and options -
- -You can trigger Dependabot actions by commenting on this PR: -- `@dependabot rebase` will rebase this PR -- `@dependabot recreate` will recreate this PR -
-''', - ), - ), - ), - ); - - expect(find.textContaining('Dependabot commands and options'), findsOne); - expect(find.byType(MarkdownListBlockView), findsNothing); - - await tester.tap(find.byIcon(Icons.chevron_right_rounded)); - await tester.pumpAndSettle(); - await tester.pump(kDoubleTapTimeout + const Duration(milliseconds: 1)); - - expect(find.byIcon(Icons.expand_more_rounded), findsOne); - expect(find.byType(MarkdownListBlockView), findsOne); - }); - - testWidgets('details paints selection backgrounds for nested quote and list', - (tester) async { - final selectionController = MarkdownSelectionController(); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: MarkdownWidget( - data: ''' -
-Release notes -
-Quoted line -
-
-
    -
  • List item
  • -
-
-''', - selectionController: selectionController, - ), - ), - ), - ); - - await tester.tap(find.byIcon(Icons.chevron_right_rounded)); - await tester.pumpAndSettle(); - - final detailsFinder = find.byWidgetPredicate( - (widget) => - widget is SelectableMarkdownBlock && - widget.spec.plainText.contains('Release notes') && - widget.spec.plainText.contains('Quoted line') && - widget.spec.plainText.contains('List item'), - ); - final detailsBlock = tester.widget(detailsFinder); - expect(detailsBlock.spec.selectionStructure, isNotNull); - expect(detailsBlock.spec.selectionColor, isNull); - final globalSelectionRects = _globalSelectionRectsForBlock( - tester, - detailsFinder, - start: 0, - end: detailsBlock.spec.plainText.length, - ); - final quotedTextRect = tester.getRect( - find.byWidgetPredicate( - (widget) => - widget is RichText && - widget.text.toPlainText().contains('Quoted line'), - ), - ); - final listTextRect = tester.getRect( - find.byWidgetPredicate( - (widget) => - widget is RichText && - widget.text.toPlainText().contains('List item'), - ), - ); - - expect( - globalSelectionRects.any((rect) => rect.overlaps(quotedTextRect)), - isTrue, - ); - expect( - globalSelectionRects.any((rect) => rect.overlaps(listTextRect)), - isTrue, - ); - - final detailsElement = tester.element(detailsFinder); - final detailsRenderBox = tester.renderObject(detailsFinder); - final listOffset = detailsBlock.spec.textOffsetResolver!( - detailsElement, - detailsRenderBox.size, - detailsRenderBox.globalToLocal(listTextRect.center), - ); - expect( - listOffset, - greaterThan(detailsBlock.spec.plainText.indexOf('List item')), - ); - - final listEnd = - detailsBlock.spec.plainText.indexOf('List item') + 'List item'.length; - selectionController.setSelection( - DocumentSelection( - base: detailsBlock.spec.selectionStructure!.startPosition( - blockIndex: 0, - ), - extent: - detailsBlock.spec.selectionStructure!.positionForDisplayedOffset( - blockIndex: 0, - displayedOffset: listEnd, - ), - ), - ); - await tester.pump(); - - final detailsAfterSelection = - tester.widget(detailsFinder); - final selectedGlobalRects = _globalSelectionRectsForBlock( - tester, - detailsFinder, - start: 0, - end: detailsAfterSelection.spec.plainText.indexOf('List item') + - 'List item'.length, - ); - expect( - selectedGlobalRects.any((rect) => rect.overlaps(quotedTextRect)), - isTrue, - ); - expect( - selectedGlobalRects.any((rect) => rect.overlaps(listTextRect)), - isTrue, - ); - expect(tester.takeException(), isNull); - - final paintFinder = find.ancestor( - of: detailsFinder, - matching: find.byType(CustomPaint), - ); - final detailsPaint = tester.widget(paintFinder.first); - expect(detailsPaint.foregroundPainter, isNotNull); - }); - - testWidgets('tap on inline html anchor tags triggers onTapLink', - (tester) async { - String? tappedDestination; - String? tappedLabel; - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: MarkdownWidget( - data: - '💡 Learn how to get started.', - onTapLink: (destination, title, label) { - tappedDestination = destination; - tappedLabel = label; - }, - ), - ), - ), - ); - - final richTextFinder = find.byWidgetPredicate( - (widget) => - widget is RichText && - widget.text.toPlainText().contains('Learn how to get started'), - ); - final richText = tester.widget(richTextFinder); - final renderBox = tester.renderObject(richTextFinder); - final painter = TextPainter( - text: richText.text, - textDirection: TextDirection.ltr, - )..layout(maxWidth: renderBox.size.width); - final plainText = richText.text.toPlainText(); - final start = plainText.indexOf('Learn how to get started'); - final end = start + 'Learn how to get started'.length; - final linkBoxes = painter.getBoxesForSelection( - TextSelection(baseOffset: start, extentOffset: end), - ); - - await tester - .tapAt(renderBox.localToGlobal(linkBoxes.first.toRect().center)); - await tester.pump(); - - expect( - tappedDestination, - 'https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot', - ); - expect(tappedLabel, 'Learn how to get started'); - }); - - testWidgets('uses pretext for list items, quotes, and table cells', ( - tester, - ) async { + testWidgets('uses pretext for list items, quotes, and table cells', ( + tester, + ) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -1932,47 +1954,45 @@ Reference[^note] }); testWidgets( - 'renders multiple footnote references without swallowing body text', - (tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Scaffold( - body: MarkdownWidget( - data: ''' + 'renders multiple footnote references without swallowing body text', + (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: MarkdownWidget( + data: ''' Here is a statement with a footnote.[^1] Another reference can be added here.[^long] [^1]: This is a simple footnote. [^long]: This footnote contains a longer explanation to showcase how multiple lines can be formatted in a footnote. It supports Markdown formatting such as **bold** and *italic* text. ''', + ), ), ), - ), - ); + ); - final renderedText = tester - .widgetList(find.byType(RichText)) - .map((widget) => widget.text.toPlainText()) - .join('\n'); - expect( - renderedText, - contains('Here is a statement with a footnote.1 Another'), - ); - expect( - renderedText, - contains('reference can be added here.2'), - ); - expect( - find.textContaining('This is a simple footnote.', findRichText: true), - findsOneWidget, - ); - expect( - find.textContaining( - 'This footnote contains a longer explanation', - findRichText: true, - ), - findsOneWidget, - ); - }); + final renderedText = tester + .widgetList(find.byType(RichText)) + .map((widget) => widget.text.toPlainText()) + .join('\n'); + expect( + renderedText, + contains('Here is a statement with a footnote.1 Another'), + ); + expect(renderedText, contains('reference can be added here.2')); + expect( + find.textContaining('This is a simple footnote.', findRichText: true), + findsOneWidget, + ); + expect( + find.textContaining( + 'This footnote contains a longer explanation', + findRichText: true, + ), + findsOneWidget, + ); + }, + ); testWidgets('renders tables and code blocks with direct data input', ( tester, @@ -1989,9 +2009,7 @@ copy me await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: MarkdownWidget(data: input), - ), + home: Scaffold(body: MarkdownWidget(data: input)), ), ); @@ -2020,9 +2038,7 @@ copy me await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: MarkdownWidget(data: input), - ), + home: Scaffold(body: MarkdownWidget(data: input)), ), ); @@ -2050,9 +2066,7 @@ copy me await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: MarkdownWidget(data: input), - ), + home: Scaffold(body: MarkdownWidget(data: input)), ), ); @@ -2081,90 +2095,94 @@ copy me }); testWidgets( - 'table uses only internal grid lines and leaves outer frame to the content shell', - ( - tester, - ) async { - await tester.pumpWidget( - const MaterialApp( - home: Scaffold( - body: MarkdownWidget( - data: ''' + 'table uses only internal grid lines and leaves outer frame to the content shell', + (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: MarkdownWidget( + data: ''' | A | B | C | D | E | | --- | --- | --- | --- | --- | | alpha | beta | gamma | delta | epsilon | ''', + ), ), ), - ), - ); + ); - final table = tester.widget(find.byType(Table)); - final border = table.border!; - expect(border.top.style, BorderStyle.none); - expect(border.bottom.style, BorderStyle.none); - expect(border.left.style, BorderStyle.none); - expect(border.right.style, BorderStyle.none); - expect(border.horizontalInside.style, BorderStyle.solid); - expect(border.verticalInside.style, BorderStyle.solid); - }); + final table = tester.widget
(find.byType(Table)); + final border = table.border!; + expect(border.top.style, BorderStyle.none); + expect(border.bottom.style, BorderStyle.none); + expect(border.left.style, BorderStyle.none); + expect(border.right.style, BorderStyle.none); + expect(border.horizontalInside.style, BorderStyle.solid); + expect(border.verticalInside.style, BorderStyle.solid); + }, + ); testWidgets( - 'code blocks render without an outer border and keep copy in the top right', - (tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Scaffold( - body: MarkdownWidget( - data: ''' + 'code blocks render without an outer border and keep copy in the top right', + (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: MarkdownWidget( + data: ''' ```dart final settleResult = 42; ``` ''', + ), ), ), - ), - ); - - final theme = MarkdownTheme.of(tester.element(find.byType(MarkdownWidget))); - final codeBlockBox = tester - .widgetList(find.byType(DecoratedBox)) - .firstWhere((widget) { - final decoration = widget.decoration; - return decoration is BoxDecoration && - decoration.color != null && - decoration.borderRadius != null && - decoration.border == null; - }); - final decoration = codeBlockBox.decoration as BoxDecoration; - expect(decoration.border, isNull); - expect(decoration.color, theme.codeBlockBackgroundColor); - - final codeBlockFinder = find.byWidgetPredicate( - (widget) => - widget is DecoratedBox && - widget.decoration is BoxDecoration && - (widget.decoration as BoxDecoration).color == - theme.codeBlockBackgroundColor && - (widget.decoration as BoxDecoration).border == null, - ); - expect(codeBlockFinder, findsOneWidget); - - final copyButton = find.byTooltip('Copy code'); - expect(copyButton, findsOneWidget); - final buttonTopRight = tester.getTopRight(copyButton); - final codeBlockTopRight = tester.getTopRight(codeBlockFinder); - expect((buttonTopRight.dx - codeBlockTopRight.dx).abs(), lessThan(36)); - expect((buttonTopRight.dy - codeBlockTopRight.dy).abs(), lessThan(36)); + ); - final markdownWidth = tester.getSize(find.byType(MarkdownWidget)).width - - theme.padding.resolve(TextDirection.ltr).horizontal; - final codeBlockWidth = tester.getSize(codeBlockFinder).width; - expect(codeBlockWidth, closeTo(markdownWidth, 1.0)); - }); + final theme = MarkdownTheme.of( + tester.element(find.byType(MarkdownWidget)), + ); + final codeBlockBox = tester + .widgetList(find.byType(DecoratedBox)) + .firstWhere((widget) { + final decoration = widget.decoration; + return decoration is BoxDecoration && + decoration.color != null && + decoration.borderRadius != null && + decoration.border == null; + }); + final decoration = codeBlockBox.decoration as BoxDecoration; + expect(decoration.border, isNull); + expect(decoration.color, theme.codeBlockBackgroundColor); + + final codeBlockFinder = find.byWidgetPredicate( + (widget) => + widget is DecoratedBox && + widget.decoration is BoxDecoration && + (widget.decoration as BoxDecoration).color == + theme.codeBlockBackgroundColor && + (widget.decoration as BoxDecoration).border == null, + ); + expect(codeBlockFinder, findsOneWidget); + + final copyButton = find.byTooltip('Copy code'); + expect(copyButton, findsOneWidget); + final buttonTopRight = tester.getTopRight(copyButton); + final codeBlockTopRight = tester.getTopRight(codeBlockFinder); + expect((buttonTopRight.dx - codeBlockTopRight.dx).abs(), lessThan(36)); + expect((buttonTopRight.dy - codeBlockTopRight.dy).abs(), lessThan(36)); + + final markdownWidth = + tester.getSize(find.byType(MarkdownWidget)).width - + theme.padding.resolve(TextDirection.ltr).horizontal; + final codeBlockWidth = tester.getSize(codeBlockFinder).width; + expect(codeBlockWidth, closeTo(markdownWidth, 1.0)); + }, + ); - testWidgets('wraps compact tables inside the viewport when columns are few', - (tester) async { + testWidgets('wraps compact tables inside the viewport when columns are few', ( + tester, + ) async { const input = ''' | Name | Description | | --- | --- | @@ -2178,10 +2196,7 @@ final settleResult = 42; alignment: Alignment.topLeft, child: SizedBox( width: 320, - child: MarkdownWidget( - data: input, - selectable: false, - ), + child: MarkdownWidget(data: input, selectable: false), ), ), ), @@ -2199,8 +2214,10 @@ final settleResult = 42; // The table should be exactly taking its available width (320 max minus markdown container padding, ~288) expect(tableRenderBox.size.width, closeTo(288.0, 2.0)); - final descriptionHeaderFinder = - find.text('Description', findRichText: true); + final descriptionHeaderFinder = find.text( + 'Description', + findRichText: true, + ); final descriptionCellFinder = find.textContaining( 'This description is intentionally long', findRichText: true, @@ -2230,10 +2247,7 @@ final settleResult = 42; alignment: Alignment.topLeft, child: SizedBox( width: 320, - child: MarkdownWidget( - data: input, - selectable: false, - ), + child: MarkdownWidget(data: input, selectable: false), ), ), ), @@ -2261,9 +2275,7 @@ return value; await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: MarkdownWidget(data: input), - ), + home: Scaffold(body: MarkdownWidget(data: input)), ), ); @@ -2279,196 +2291,202 @@ return value; final richText = tester.widget(richTextFinder); final rootSpan = richText.text as TextSpan; expect( - _countStyledDescendantSpans(rootSpan, rootSpan.style), greaterThan(0)); + _countStyledDescendantSpans(rootSpan, rootSpan.style), + greaterThan(0), + ); }); testWidgets( - 'code blocks render plain text on the first frame, then highlight', - (tester) async { - const input = ''' + 'code blocks render plain text on the first frame, then highlight', + (tester) async { + const input = ''' ```dart const firstFrameValue = 42; return firstFrameValue; ``` '''; - await tester.pumpWidget( - const MaterialApp( - home: Scaffold( - body: MarkdownWidget(data: input), + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: MarkdownWidget(data: input)), ), - ), - ); + ); - final richTextFinder = find.byWidgetPredicate( - (widget) => - widget is RichText && - widget.text.toPlainText().contains('const firstFrameValue = 42;'), - ); - expect(richTextFinder, findsOneWidget); + final richTextFinder = find.byWidgetPredicate( + (widget) => + widget is RichText && + widget.text.toPlainText().contains('const firstFrameValue = 42;'), + ); + expect(richTextFinder, findsOneWidget); - var richText = tester.widget(richTextFinder); - var rootSpan = richText.text as TextSpan; - expect(_countStyledDescendantSpans(rootSpan, rootSpan.style), 0); + var richText = tester.widget(richTextFinder); + var rootSpan = richText.text as TextSpan; + expect(_countStyledDescendantSpans(rootSpan, rootSpan.style), 0); - await _pumpUntilCodeBlockHighlighted(tester, richTextFinder); + await _pumpUntilCodeBlockHighlighted(tester, richTextFinder); - richText = tester.widget(richTextFinder); - rootSpan = richText.text as TextSpan; - expect( - _countStyledDescendantSpans(rootSpan, rootSpan.style), greaterThan(0)); - }); + richText = tester.widget(richTextFinder); + rootSpan = richText.text as TextSpan; + expect( + _countStyledDescendantSpans(rootSpan, rootSpan.style), + greaterThan(0), + ); + }, + ); - testWidgets('disposing markdown view ignores pending code highlight results', - (tester) async { - const input = ''' + testWidgets( + 'disposing markdown view ignores pending code highlight results', + (tester) async { + const input = ''' ```dart const value = 42; return value; ``` '''; - await tester.pumpWidget( - const MaterialApp( - home: Scaffold( - body: MarkdownWidget(data: input), + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: MarkdownWidget(data: input)), ), - ), - ); + ); - await tester.pumpWidget(const SizedBox.shrink()); - await tester.pump(); - await tester.pump(); + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pump(); + await tester.pump(); - expect(tester.takeException(), isNull); - }); + expect(tester.takeException(), isNull); + }, + ); testWidgets( - 'code highlight cache keeps highlighted presentation visible while theme changes refresh styles', - (tester) async { - late MarkdownThemeData fallbackTheme; - late MarkdownThemeData tightTheme; + 'code highlight cache keeps highlighted presentation visible while theme changes refresh styles', + (tester) async { + late MarkdownThemeData fallbackTheme; + late MarkdownThemeData tightTheme; - await tester.pumpWidget( - MaterialApp( - home: Builder( - builder: (context) { - fallbackTheme = MarkdownThemeData.fallback(context); - tightTheme = MarkdownThemeData.tight(context); - return const SizedBox.shrink(); - }, + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + fallbackTheme = MarkdownThemeData.fallback(context); + tightTheme = MarkdownThemeData.tight(context); + return const SizedBox.shrink(); + }, + ), ), - ), - ); + ); - final cache = MarkdownCodeHighlightCache( - highlighter: const MarkdownCodeSyntaxHighlighter(), - ); - addTearDown(cache.dispose); + final cache = MarkdownCodeHighlightCache( + highlighter: const MarkdownCodeSyntaxHighlighter(), + ); + addTearDown(cache.dispose); - // The segment cache is shared by every test, so the source is unique. - const source = 'const themeValue = 7;\nreturn themeValue;'; + // The segment cache is shared by every test, so the source is unique. + const source = 'const themeValue = 7;\nreturn themeValue;'; - var presentation = cache.resolve( - blockId: 'block-1', - source: source, - baseStyle: fallbackTheme.codeBlockStyle, - theme: fallbackTheme, - language: 'dart', - ); - expect(presentation.isHighlighted, isFalse); + var presentation = cache.resolve( + blockId: 'block-1', + source: source, + baseStyle: fallbackTheme.codeBlockStyle, + theme: fallbackTheme, + language: 'dart', + ); + expect(presentation.isHighlighted, isFalse); - presentation = await _pumpUntilCacheHighlighted( - tester, - cache: cache, - blockId: 'block-1', - source: source, - baseStyle: fallbackTheme.codeBlockStyle, - theme: fallbackTheme, - language: 'dart', - ); - expect(presentation.isHighlighted, isTrue); + presentation = await _pumpUntilCacheHighlighted( + tester, + cache: cache, + blockId: 'block-1', + source: source, + baseStyle: fallbackTheme.codeBlockStyle, + theme: fallbackTheme, + language: 'dart', + ); + expect(presentation.isHighlighted, isTrue); - presentation = cache.resolve( - blockId: 'block-1', - source: source, - baseStyle: tightTheme.codeBlockStyle, - theme: tightTheme, - language: 'dart', - ); - expect(presentation.isHighlighted, isTrue); + presentation = cache.resolve( + blockId: 'block-1', + source: source, + baseStyle: tightTheme.codeBlockStyle, + theme: tightTheme, + language: 'dart', + ); + expect(presentation.isHighlighted, isTrue); - presentation = await _pumpUntilCacheHighlighted( - tester, - cache: cache, - blockId: 'block-1', - source: source, - baseStyle: tightTheme.codeBlockStyle, - theme: tightTheme, - language: 'dart', - ); - expect(presentation.isHighlighted, isTrue); - }); + presentation = await _pumpUntilCacheHighlighted( + tester, + cache: cache, + blockId: 'block-1', + source: source, + baseStyle: tightTheme.codeBlockStyle, + theme: tightTheme, + language: 'dart', + ); + expect(presentation.isHighlighted, isTrue); + }, + ); - testWidgets('a code block with cached segments highlights in its first frame', - (tester) async { - late MarkdownThemeData theme; - await tester.pumpWidget( - MaterialApp( - home: Builder( - builder: (context) { - theme = MarkdownThemeData.fallback(context); - return const SizedBox.shrink(); - }, + testWidgets( + 'a code block with cached segments highlights in its first frame', + (tester) async { + late MarkdownThemeData theme; + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + theme = MarkdownThemeData.fallback(context); + return const SizedBox.shrink(); + }, + ), ), - ), - ); + ); - const source = 'final remounted = true;\nprint(remounted);'; - final first = MarkdownCodeHighlightCache( - highlighter: const MarkdownCodeSyntaxHighlighter(), - ); - addTearDown(first.dispose); - expect( - first - .resolve( - blockId: 'block-1', - source: source, - baseStyle: theme.codeBlockStyle, - theme: theme, - language: 'dart', - ) - .isHighlighted, - isFalse, - ); - await _pumpUntilCacheHighlighted( - tester, - cache: first, - blockId: 'block-1', - source: source, - baseStyle: theme.codeBlockStyle, - theme: theme, - language: 'dart', - ); + const source = 'final remounted = true;\nprint(remounted);'; + final first = MarkdownCodeHighlightCache( + highlighter: const MarkdownCodeSyntaxHighlighter(), + ); + addTearDown(first.dispose); + expect( + first + .resolve( + blockId: 'block-1', + source: source, + baseStyle: theme.codeBlockStyle, + theme: theme, + language: 'dart', + ) + .isHighlighted, + isFalse, + ); + await _pumpUntilCacheHighlighted( + tester, + cache: first, + blockId: 'block-1', + source: source, + baseStyle: theme.codeBlockStyle, + theme: theme, + language: 'dart', + ); - // A view that mounts later, such as the same post in another list. - final second = MarkdownCodeHighlightCache( - highlighter: const MarkdownCodeSyntaxHighlighter(), - ); - addTearDown(second.dispose); - var notified = false; - second.addListener(() => notified = true); - final presentation = second.resolve( - blockId: 'block-1', - source: source, - baseStyle: theme.codeBlockStyle, - theme: theme, - language: 'dart', - ); - expect(presentation.isHighlighted, isTrue); - await tester.pump(); - expect(notified, isFalse); - }); + // A view that mounts later, such as the same post in another list. + final second = MarkdownCodeHighlightCache( + highlighter: const MarkdownCodeSyntaxHighlighter(), + ); + addTearDown(second.dispose); + var notified = false; + second.addListener(() => notified = true); + final presentation = second.resolve( + blockId: 'block-1', + source: source, + baseStyle: theme.codeBlockStyle, + theme: theme, + language: 'dart', + ); + expect(presentation.isHighlighted, isTrue); + await tester.pump(); + expect(notified, isFalse); + }, + ); testWidgets('code highlight link tokens keep the code block font size', ( tester, @@ -2533,10 +2551,7 @@ return value; nightTheme = MarkdownThemeData.night(context); currentTheme ??= fallbackTheme; return Scaffold( - body: MarkdownWidget( - data: input, - theme: currentTheme, - ), + body: MarkdownWidget(data: input, theme: currentTheme), ); }, ), @@ -2593,9 +2608,9 @@ return value; }); testWidgets( - 'long code blocks can degrade to plain text above the configured line limit', - (tester) async { - const input = ''' + 'long code blocks can degrade to plain text above the configured line limit', + (tester) async { + const input = ''' ```dart final a = 1; final b = 2; @@ -2603,34 +2618,34 @@ final c = a + b; ``` '''; - await tester.pumpWidget( - MaterialApp( - home: Builder( - builder: (context) => Scaffold( - body: MarkdownWidget( - data: input, - theme: MarkdownThemeData.fallback(context).copyWith( - codeHighlightMaxLines: 2, + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) => Scaffold( + body: MarkdownWidget( + data: input, + theme: MarkdownThemeData.fallback(context) + .copyWith(codeHighlightMaxLines: 2), ), ), ), ), - ), - ); + ); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(); - final richTextFinder = find.byWidgetPredicate( - (widget) => - widget is RichText && - widget.text.toPlainText().contains('final c = a + b;'), - ); - expect(richTextFinder, findsOneWidget); + final richTextFinder = find.byWidgetPredicate( + (widget) => + widget is RichText && + widget.text.toPlainText().contains('final c = a + b;'), + ); + expect(richTextFinder, findsOneWidget); - final richText = tester.widget(richTextFinder); - final rootSpan = richText.text as TextSpan; - expect(_countStyledDescendantSpans(rootSpan, rootSpan.style), 0); - }); + final richText = tester.widget(richTextFinder); + final rootSpan = richText.text as TextSpan; + expect(_countStyledDescendantSpans(rootSpan, rootSpan.style), 0); + }, + ); testWidgets('tight theme keeps normal text sizing while reducing spacing', ( tester, @@ -2681,10 +2696,7 @@ final c = a + b; ); expect(nightTheme.bodyStyle.color, isNotNull); - expect( - nightTheme.bodyStyle.color!.computeLuminance(), - greaterThan(0.75), - ); + expect(nightTheme.bodyStyle.color!.computeLuminance(), greaterThan(0.75)); expect( nightTheme.tableRowBackgroundColor.computeLuminance(), lessThan(0.02), @@ -2704,9 +2716,7 @@ final c = a + b; ) async { await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: MarkdownWidget(data: 'Inline `code` sample'), - ), + home: Scaffold(body: MarkdownWidget(data: 'Inline `code` sample')), ), ); @@ -2720,15 +2730,13 @@ final c = a + b; final inlineCodeFinder = _decoratedInlineTextFinder(); expect(inlineCodeFinder, findsWidgets); - final inlineCodeRenderBox = - tester.renderObject(inlineCodeFinder.first); + final inlineCodeRenderBox = tester.renderObject( + inlineCodeFinder.first, + ); final expectedHeight = _tightTextHeight('code', theme.bodyStyle.merge(theme.inlineCodeStyle)) + - theme.inlineCodePadding.vertical; - expect( - inlineCodeRenderBox.size.height, - closeTo(expectedHeight, 0.001), - ); + theme.inlineCodePadding.vertical; + expect(inlineCodeRenderBox.size.height, closeTo(expectedHeight, 0.001)); }); testWidgets('inline code prefers the default Mono font family', ( @@ -2736,9 +2744,7 @@ final c = a + b; ) async { await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: MarkdownWidget(data: 'Inline `code` sample'), - ), + home: Scaffold(body: MarkdownWidget(data: 'Inline `code` sample')), ), ); @@ -2756,16 +2762,16 @@ final c = a + b; ) async { await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: MarkdownWidget(data: '`popup`'), - ), + home: Scaffold(body: MarkdownWidget(data: '`popup`')), ), ); - final blockBox = - tester.renderObject(find.byType(MarkdownPretextTextBlock)); - final inlineCodeBox = - tester.renderObject(_decoratedInlineTextFinder().first); + final blockBox = tester.renderObject( + find.byType(MarkdownPretextTextBlock), + ); + final inlineCodeBox = tester.renderObject( + _decoratedInlineTextFinder().first, + ); final theme = MarkdownTheme.of(tester.element(find.byType(MarkdownWidget))); final textPainter = TextPainter( text: TextSpan(text: 'popup', style: theme.bodyStyle), @@ -2789,16 +2795,16 @@ final c = a + b; ) async { await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: MarkdownWidget(data: '- `popup`'), - ), + home: Scaffold(body: MarkdownWidget(data: '- `popup`')), ), ); - final blockBox = tester - .renderObject(find.byType(MarkdownPretextTextBlock).first); - final inlineCodeBox = - tester.renderObject(_decoratedInlineTextFinder().first); + final blockBox = tester.renderObject( + find.byType(MarkdownPretextTextBlock).first, + ); + final inlineCodeBox = tester.renderObject( + _decoratedInlineTextFinder().first, + ); final theme = MarkdownTheme.of(tester.element(find.byType(MarkdownWidget))); final textPainter = TextPainter( text: TextSpan(text: 'popup', style: theme.bodyStyle), @@ -2817,8 +2823,9 @@ final c = a + b; expect(inlineCodeRect.bottom, lessThanOrEqualTo(blockRect.bottom + 0.01)); }); - testWidgets('code blocks prefer the default Mono font family', - (tester) async { + testWidgets('code blocks prefer the default Mono font family', ( + tester, + ) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -2848,13 +2855,12 @@ const value = 42; expect(richText.softWrap, isFalse); }); - testWidgets('selecting text across inline code does not throw', - (tester) async { + testWidgets('selecting text across inline code does not throw', ( + tester, + ) async { await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: MarkdownWidget(data: '- before `code` after'), - ), + home: Scaffold(body: MarkdownWidget(data: '- before `code` after')), ), ); @@ -2880,13 +2886,12 @@ const value = 42; expect(tester.takeException(), isNull); }); - testWidgets('pretext blocks with inline code paint selection above child', - (tester) async { + testWidgets('pretext blocks with inline code paint selection above child', ( + tester, + ) async { await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: MarkdownWidget(data: 'before `code` after'), - ), + home: Scaffold(body: MarkdownWidget(data: 'before `code` after')), ), ); @@ -3146,8 +3151,10 @@ const value = 42; ); expect(layout.lines, hasLength(2)); - expect( - layout.lines.map((line) => line.text).toList(), ['ab', 'cd']); + expect(layout.lines.map((line) => line.text).toList(), [ + 'ab', + 'cd', + ]); }); test('breakable decorated inline wraps across lines', () { @@ -3178,14 +3185,21 @@ const value = 42; .toList(growable: false); expect(decoratedSegments, isNotEmpty); expect(decoratedSegments.first.padding.left, greaterThan(0)); - expect(decoratedSegments.first.padding.left, - lessThanOrEqualTo(padding.left)); + expect( + decoratedSegments.first.padding.left, + lessThanOrEqualTo(padding.left), + ); expect(decoratedSegments.last.padding.right, greaterThan(0)); - expect(decoratedSegments.last.padding.right, - lessThanOrEqualTo(padding.right)); - for (final middle in decoratedSegments.skip(1).take( - decoratedSegments.length > 2 ? decoratedSegments.length - 2 : 0, - )) { + expect( + decoratedSegments.last.padding.right, + lessThanOrEqualTo(padding.right), + ); + for (final middle + in decoratedSegments + .skip(1) + .take( + decoratedSegments.length > 2 ? decoratedSegments.length - 2 : 0, + )) { expect(middle.padding.left, 0); expect(middle.padding.right, 0); } @@ -3204,15 +3218,10 @@ const value = 42; wrapped.lines.map((line) => line.text).toList(growable: false), ['xxx', 'x ', 'to ', 'o ', 'xxx', 'x'], ); - expect( - wrapped.lines.map((line) => line.text).join(), - 'xxxx to o xxxx', - ); + expect(wrapped.lines.map((line) => line.text).join(), 'xxxx to o xxxx'); }); - test( - 'breakable decorated inline prefers identifier boundaries before character fallback', - () { + test('breakable decorated inline prefers identifier boundaries before character fallback', () { const baseStyle = TextStyle(fontSize: 14, height: 1.2); double measureWidth(String text) { @@ -3275,10 +3284,7 @@ const value = 42; final width = measureWidth('cachetoken') + 1; final layout = computeMarkdownPretextLayoutFromRuns( runs: const [ - MarkdownPretextInlineRun( - text: 'Use ', - style: baseStyle, - ), + MarkdownPretextInlineRun(text: 'Use ', style: baseStyle), MarkdownPretextInlineRun( text: 'cachetoken', style: baseStyle, @@ -3289,10 +3295,7 @@ const value = 42; padding: EdgeInsets.zero, ), ), - MarkdownPretextInlineRun( - text: ' after.', - style: baseStyle, - ), + MarkdownPretextInlineRun(text: ' after.', style: baseStyle), ], fallbackStyle: baseStyle, maxWidth: width, @@ -3309,10 +3312,7 @@ const value = 42; final layout = computeMarkdownPretextLayoutFromRuns( runs: const [ - MarkdownPretextInlineRun( - text: 'Use ', - style: baseStyle, - ), + MarkdownPretextInlineRun(text: 'Use ', style: baseStyle), MarkdownPretextInlineRun( text: 'cachetoken', style: baseStyle, @@ -3367,9 +3367,7 @@ const value = 42; MarkdownPretextInlineRun( text: 'x^2', style: baseStyle, - renderSpan: const WidgetSpan( - child: SizedBox(width: 36, height: 28), - ), + renderSpan: const WidgetSpan(child: SizedBox(width: 36, height: 28)), estimatedWidth: 36, estimatedLineHeight: 32, ), @@ -3390,42 +3388,38 @@ const value = 42; ); }); - test('direct rich text geometry is used for undecorated or render-span runs', - () { - const baseStyle = TextStyle(fontSize: 14, height: 1.2); + test( + 'direct rich text geometry is used for undecorated or render-span runs', + () { + const baseStyle = TextStyle(fontSize: 14, height: 1.2); - expect( - markdownPretextCanUseDirectRichTextGeometry( - const [ - MarkdownPretextInlineRun(text: 'plain text', style: baseStyle), - ], - ), - isTrue, - ); + expect( + markdownPretextCanUseDirectRichTextGeometry( + const [ + MarkdownPretextInlineRun(text: 'plain text', style: baseStyle), + ], + ), + isTrue, + ); - expect( - markdownPretextCanUseDirectRichTextGeometry( - [ + expect( + markdownPretextCanUseDirectRichTextGeometry([ MarkdownPretextInlineRun( text: 'x^2', style: baseStyle, - renderSpan: - const WidgetSpan(child: SizedBox(width: 20, height: 16)), + renderSpan: const WidgetSpan( + child: SizedBox(width: 20, height: 16), + ), estimatedWidth: 20, estimatedLineHeight: 18, ), - ], - ), - isTrue, - ); + ]), + isTrue, + ); - expect( - markdownPretextCanUseDirectRichTextGeometry( - [ - const MarkdownPretextInlineRun( - text: 'prefix ', - style: baseStyle, - ), + expect( + markdownPretextCanUseDirectRichTextGeometry([ + const MarkdownPretextInlineRun(text: 'prefix ', style: baseStyle), const MarkdownPretextInlineRun( text: 'inline_code', style: baseStyle, @@ -3439,19 +3433,18 @@ const value = 42; MarkdownPretextInlineRun( text: 'x^2', style: baseStyle, - renderSpan: - const WidgetSpan(child: SizedBox(width: 20, height: 16)), + renderSpan: const WidgetSpan( + child: SizedBox(width: 20, height: 16), + ), estimatedWidth: 20, estimatedLineHeight: 18, ), - ], - ), - isTrue, - ); + ]), + isTrue, + ); - expect( - markdownPretextRenderText( - [ + expect( + markdownPretextRenderText([ const MarkdownPretextInlineRun( text: 'inline_code', style: baseStyle, @@ -3465,34 +3458,35 @@ const value = 42; MarkdownPretextInlineRun( text: 'x^2', style: baseStyle, - renderSpan: - const WidgetSpan(child: SizedBox(width: 20, height: 16)), + renderSpan: const WidgetSpan( + child: SizedBox(width: 20, height: 16), + ), estimatedWidth: 20, estimatedLineHeight: 18, ), - ], - ), - String.fromCharCodes(List.filled(3, 0xFFFC)), - ); + ]), + String.fromCharCodes(List.filled(3, 0xFFFC)), + ); - expect( - markdownPretextCanUseDirectRichTextGeometry( - const [ - MarkdownPretextInlineRun( - text: 'inline_code', - style: baseStyle, - allowCharacterWrap: true, - decoration: MarkdownPretextInlineDecoration( - backgroundColor: Color(0xFFE9EDF2), - borderRadius: BorderRadius.all(Radius.circular(6)), - padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2), + expect( + markdownPretextCanUseDirectRichTextGeometry( + const [ + MarkdownPretextInlineRun( + text: 'inline_code', + style: baseStyle, + allowCharacterWrap: true, + decoration: MarkdownPretextInlineDecoration( + backgroundColor: Color(0xFFE9EDF2), + borderRadius: BorderRadius.all(Radius.circular(6)), + padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2), + ), ), - ), - ], - ), - isFalse, - ); - }); + ], + ), + isFalse, + ); + }, + ); testWidgets('table cells keep inline code compact when mixed with math', ( tester, @@ -3500,9 +3494,7 @@ const value = 42; await tester.pumpWidget( const MaterialApp( home: Scaffold( - body: MarkdownWidget( - data: '| Value |\n| --- |\n| `code` (x^2) |', - ), + body: MarkdownWidget(data: '| Value |\n| --- |\n| `code` (x^2) |'), ), ), ); @@ -3515,9 +3507,7 @@ const value = 42; matching: find.byWidgetPredicate( (widget) => widget is RichText && - widget.text.toPlainText().contains( - String.fromCharCode(0xFFFC), - ), + widget.text.toPlainText().contains(String.fromCharCode(0xFFFC)), ), ); expect(cellRichTextFinder, findsOneWidget); @@ -3535,9 +3525,7 @@ const value = 42; await tester.pumpWidget( const MaterialApp( home: Scaffold( - body: MarkdownWidget( - data: '| Formula |\n| --- |\n| value \$x^2\$ |', - ), + body: MarkdownWidget(data: '| Formula |\n| --- |\n| value \$x^2\$ |'), ), ), ); @@ -3553,8 +3541,8 @@ const value = 42; (widget) => widget is RichText && widget.text.toPlainText().contains( - String.fromCharCode(0xFFFC), - ), + String.fromCharCode(0xFFFC), + ), ), ) .first, @@ -3572,8 +3560,7 @@ const value = 42; const baseStyle = TextStyle(fontSize: 14, height: 1.2); const runs = [ MarkdownPretextInlineRun( - text: - 'This plain paragraph is intentionally long so it wraps across multiple lines without inline code.', + text: 'This plain paragraph is intentionally long so it wraps across multiple lines without inline code.', style: baseStyle, ), ]; @@ -3718,11 +3705,17 @@ const value = 42; } expect(lineDecoratedSegments.first.text, isNotEmpty); expect( - lineDecoratedSegments.first.padding.left, greaterThanOrEqualTo(0)); + lineDecoratedSegments.first.padding.left, + greaterThanOrEqualTo(0), + ); + expect( + lineDecoratedSegments.last.padding.right, + greaterThanOrEqualTo(0), + ); expect( - lineDecoratedSegments.last.padding.right, greaterThanOrEqualTo(0)); - expect(lineDecoratedSegments.last.padding.right, - lessThanOrEqualTo(padding.right)); + lineDecoratedSegments.last.padding.right, + lessThanOrEqualTo(padding.right), + ); } } }); @@ -3747,10 +3740,7 @@ const value = 42; padding: padding, ), ), - MarkdownPretextInlineRun( - text: ') used during ', - style: baseStyle, - ), + MarkdownPretextInlineRun(text: ') used during ', style: baseStyle), MarkdownPretextInlineRun( text: 'settle_result', style: baseStyle, @@ -3810,10 +3800,7 @@ const value = 42; padding: padding, ), ), - MarkdownPretextInlineRun( - text: '提示。', - style: baseStyle, - ), + MarkdownPretextInlineRun(text: '提示。', style: baseStyle), ]; for (final width in [232, 228, 224, 220, 216, 212, 208, 204]) { @@ -3841,11 +3828,7 @@ const value = 42; .where((segment) => segment.decoration != null) .map((segment) => segment.text) .join(); - expect( - decoratedText, - 'send_sol_for_rent', - reason: 'width=$width', - ); + expect(decoratedText, 'send_sol_for_rent', reason: 'width=$width'); } }); @@ -3871,10 +3854,7 @@ const value = 42; final codeLayout = computeMarkdownPretextLayoutFromRuns( runs: const [ - MarkdownPretextInlineRun( - text: '$prefix ', - style: baseStyle, - ), + MarkdownPretextInlineRun(text: '$prefix ', style: baseStyle), MarkdownPretextInlineRun( text: 'send_sol_for_rent', style: baseStyle, @@ -3885,10 +3865,7 @@ const value = 42; padding: padding, ), ), - MarkdownPretextInlineRun( - text: suffix, - style: baseStyle, - ), + MarkdownPretextInlineRun(text: suffix, style: baseStyle), ], fallbackStyle: baseStyle, maxWidth: width, @@ -3904,109 +3881,118 @@ const value = 42; }); testWidgets( - 'markdown inline code at paragraph tail does not shift the first line break', - (tester) async { - const inlineCodeMarkdown = - '某人发布了一份**《TEST 某功能校验说明》**文档,整理了某客户端转账页的逻辑,区分了三种场景,分别说明金额校验、可用余额计算、手续费校验规则,重点明确:只有某链真实租金场景,才会显示`rent_tip_token` 提示。'; - late MarkdownThemeData theme; + 'markdown inline code at paragraph tail does not shift the first line break', + (tester) async { + const inlineCodeMarkdown = + '某人发布了一份**《TEST 某功能校验说明》**文档,整理了某客户端转账页的逻辑,区分了三种场景,分别说明金额校验、可用余额计算、手续费校验规则,重点明确:只有某链真实租金场景,才会显示`rent_tip_token` 提示。'; + late MarkdownThemeData theme; - await tester.pumpWidget( - MaterialApp( - home: Builder( - builder: (context) { - theme = MarkdownThemeData.fallback(context); - return const SizedBox.shrink(); - }, + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + theme = MarkdownThemeData.fallback(context); + return const SizedBox.shrink(); + }, + ), ), - ), - ); - - List layoutLinesFromMarkdown(String markdown, double width) { - final parser = MarkdownDocumentParser(); - final document = parser.parse(markdown); - final paragraph = document.blocks.single as ParagraphBlock; - final builder = MarkdownInlineBuilder( - theme: theme, - recognizers: MarkdownLinkRecognizers(), ); - final runs = builder.buildPretextRuns(theme.bodyStyle, paragraph.inlines); - final layout = computeMarkdownPretextLayoutFromRuns( - runs: runs, - fallbackStyle: theme.bodyStyle, - maxWidth: width, - textScaleFactor: 1, - ); - return layout.lines.map((line) => line.text).toList(growable: false); - } - for (final width in [500]) { - final inlineCodeLines = - layoutLinesFromMarkdown(inlineCodeMarkdown, width); + List layoutLinesFromMarkdown(String markdown, double width) { + final parser = MarkdownDocumentParser(); + final document = parser.parse(markdown); + final paragraph = document.blocks.single as ParagraphBlock; + final builder = MarkdownInlineBuilder( + theme: theme, + recognizers: MarkdownLinkRecognizers(), + ); + final runs = builder.buildPretextRuns( + theme.bodyStyle, + paragraph.inlines, + ); + final layout = computeMarkdownPretextLayoutFromRuns( + runs: runs, + fallbackStyle: theme.bodyStyle, + maxWidth: width, + textScaleFactor: 1, + ); + return layout.lines.map((line) => line.text).toList(growable: false); + } - expect(inlineCodeLines, isNotEmpty, reason: 'width=$width'); - expect( - inlineCodeLines.first, - isNot('某人发布了一份**《TEST'), - reason: 'width=$width first=${inlineCodeLines.first}', - ); - expect( - inlineCodeLines.first, - contains('某功能'), - reason: 'width=$width first=${inlineCodeLines.first}', - ); - } - }); + for (final width in [500]) { + final inlineCodeLines = layoutLinesFromMarkdown( + inlineCodeMarkdown, + width, + ); - testWidgets( - 'widget rendering keeps the first list line stable with trailing inline code', - (tester) async { - const inlineCodeMarkdown = - '1. 某人发布了一份**《TEST 某功能校验说明》**文档,整理了某客户端转账页的逻辑,区分了三种场景,分别说明金额校验、可用余额计算、手续费校验规则,重点明确:只有某链真实租金场景,才会显示`rent_tip_token` 提示。'; + expect(inlineCodeLines, isNotEmpty, reason: 'width=$width'); + expect( + inlineCodeLines.first, + isNot('某人发布了一份**《TEST'), + reason: 'width=$width first=${inlineCodeLines.first}', + ); + expect( + inlineCodeLines.first, + contains('某功能'), + reason: 'width=$width first=${inlineCodeLines.first}', + ); + } + }, + ); - Future> renderedLines(String markdown, double width) async { - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Align( - alignment: Alignment.topLeft, - child: SizedBox( - width: width, - child: MarkdownWidget(data: markdown), + testWidgets( + 'widget rendering keeps the first list line stable with trailing inline code', + (tester) async { + const inlineCodeMarkdown = + '1. 某人发布了一份**《TEST 某功能校验说明》**文档,整理了某客户端转账页的逻辑,区分了三种场景,分别说明金额校验、可用余额计算、手续费校验规则,重点明确:只有某链真实租金场景,才会显示`rent_tip_token` 提示。'; + + Future> renderedLines(String markdown, double width) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: width, + child: MarkdownWidget(data: markdown), + ), ), ), ), - ), - ); - await tester.pumpAndSettle(); + ); + await tester.pumpAndSettle(); - final blockFinder = find.byType(MarkdownPretextTextBlock).first; - final blockWidget = tester.widget(blockFinder); - final blockRenderBox = tester.renderObject(blockFinder); - final layout = computeMarkdownPretextLayoutFromRuns( - runs: blockWidget.runs!, - fallbackStyle: blockWidget.fallbackStyle, - maxWidth: blockRenderBox.size.width, - textScaleFactor: 1, - ); - return layout.lines.map((line) => line.text).toList(growable: false); - } + final blockFinder = find.byType(MarkdownPretextTextBlock).first; + final blockWidget = tester.widget( + blockFinder, + ); + final blockRenderBox = tester.renderObject(blockFinder); + final layout = computeMarkdownPretextLayoutFromRuns( + runs: blockWidget.runs!, + fallbackStyle: blockWidget.fallbackStyle, + maxWidth: blockRenderBox.size.width, + textScaleFactor: 1, + ); + return layout.lines.map((line) => line.text).toList(growable: false); + } - for (final width in [500]) { - final inlineCodeLines = await renderedLines(inlineCodeMarkdown, width); + for (final width in [500]) { + final inlineCodeLines = await renderedLines(inlineCodeMarkdown, width); - expect(inlineCodeLines, isNotEmpty, reason: 'width=$width'); - expect( - inlineCodeLines.first, - isNot('某人发布了一份**《TEST'), - reason: 'width=$width first=${inlineCodeLines.first}', - ); - expect( - inlineCodeLines.first, - contains('某功能'), - reason: 'width=$width first=${inlineCodeLines.first}', - ); - } - }); + expect(inlineCodeLines, isNotEmpty, reason: 'width=$width'); + expect( + inlineCodeLines.first, + isNot('某人发布了一份**《TEST'), + reason: 'width=$width first=${inlineCodeLines.first}', + ); + expect( + inlineCodeLines.first, + contains('某功能'), + reason: 'width=$width first=${inlineCodeLines.first}', + ); + } + }, + ); testWidgets('reuses cached unchanged pretext block widgets on append', ( tester, @@ -4014,9 +4000,7 @@ const value = 42; final controller = MarkdownController(data: '# Hello\n\nBody'); await tester.pumpWidget( MaterialApp( - home: Scaffold( - body: MarkdownWidget(controller: controller), - ), + home: Scaffold(body: MarkdownWidget(controller: controller)), ), ); @@ -4045,12 +4029,12 @@ const value = 42; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(SystemChannels.platform, (methodCall) async { - if (methodCall.method == 'Clipboard.setData') { - final arguments = methodCall.arguments as Map; - copiedText = arguments['text'] as String?; - } - return null; - }); + if (methodCall.method == 'Clipboard.setData') { + final arguments = methodCall.arguments as Map; + copiedText = arguments['text'] as String?; + } + return null; + }); addTearDown(() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger @@ -4059,9 +4043,7 @@ const value = 42; await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: MarkdownWidget(data: '# Hello\n\nWorld'), - ), + home: Scaffold(body: MarkdownWidget(data: '# Hello\n\nWorld')), ), ); @@ -4109,8 +4091,9 @@ const value = 42; expect(selectionController.selectedPlainText, 'Hello\n\nWorld'); }); - testWidgets('dragging from blank space starts a text selection', - (tester) async { + testWidgets('dragging from blank space starts a text selection', ( + tester, + ) async { final selectionController = MarkdownSelectionController(); await tester.pumpWidget( @@ -4180,49 +4163,51 @@ const value = 42; expect(selectionController.hasSelection, isFalse); }); - testWidgets('clicking trailing blank space inside markdown clears selection', - ( - tester, - ) async { - final selectionController = MarkdownSelectionController(); + testWidgets( + 'clicking trailing blank space inside markdown clears selection', + (tester) async { + final selectionController = MarkdownSelectionController(); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: SizedBox( - width: 420, - child: MarkdownWidget( - data: '# Hello\n\nWorld', - selectionController: selectionController, + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 420, + child: MarkdownWidget( + data: '# Hello\n\nWorld', + selectionController: selectionController, + ), ), ), ), - ), - ); - - final start = tester.getTopLeft(find.text('Hello')) + const Offset(1, 8); - final end = tester.getBottomRight(find.text('World')) - const Offset(1, 8); - final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); - await gesture.addPointer(location: start); - await gesture.down(start); - await tester.pump(); - await gesture.moveTo(end); - await tester.pump(); - await gesture.up(); - await tester.pump(); - - expect(selectionController.hasSelection, isTrue); + ); - final pretextBox = tester - .renderObject(find.byType(MarkdownPretextTextBlock).first); - final trailingBlankTap = pretextBox.localToGlobal( - Offset(pretextBox.size.width - 4, pretextBox.size.height / 2), - ); - await tester.tapAt(trailingBlankTap); - await tester.pump(); + final start = tester.getTopLeft(find.text('Hello')) + const Offset(1, 8); + final end = + tester.getBottomRight(find.text('World')) - const Offset(1, 8); + final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: start); + await gesture.down(start); + await tester.pump(); + await gesture.moveTo(end); + await tester.pump(); + await gesture.up(); + await tester.pump(); + + expect(selectionController.hasSelection, isTrue); + + final pretextBox = tester.renderObject( + find.byType(MarkdownPretextTextBlock).first, + ); + final trailingBlankTap = pretextBox.localToGlobal( + Offset(pretextBox.size.width - 4, pretextBox.size.height / 2), + ); + await tester.tapAt(trailingBlankTap); + await tester.pump(); - expect(selectionController.hasSelection, isFalse); - }); + expect(selectionController.hasSelection, isFalse); + }, + ); testWidgets('clicking outside markdown clears selection', (tester) async { final selectionController = MarkdownSelectionController(); @@ -4240,10 +4225,7 @@ const value = 42; ), ), const Expanded( - child: ColoredBox( - key: outsideKey, - color: Colors.transparent, - ), + child: ColoredBox(key: outsideKey, color: Colors.transparent), ), ], ), @@ -4277,12 +4259,12 @@ const value = 42; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(SystemChannels.platform, (methodCall) async { - if (methodCall.method == 'Clipboard.setData') { - final arguments = methodCall.arguments as Map; - copiedText = arguments['text'] as String?; - } - return null; - }); + if (methodCall.method == 'Clipboard.setData') { + final arguments = methodCall.arguments as Map; + copiedText = arguments['text'] as String?; + } + return null; + }); addTearDown(() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger @@ -4291,9 +4273,7 @@ const value = 42; await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: MarkdownWidget(data: '# Hello\n\nWorld'), - ), + home: Scaffold(body: MarkdownWidget(data: '# Hello\n\nWorld')), ), ); @@ -4347,8 +4327,9 @@ const value = 42; expect(tester.takeException(), isNull); }); - testWidgets('drag selection auto-scrolls near the viewport edge', - (tester) async { + testWidgets('drag selection auto-scrolls near the viewport edge', ( + tester, + ) async { final selectionController = MarkdownSelectionController(); final buffer = StringBuffer(); for (var index = 0; index < 40; index++) { @@ -4387,8 +4368,9 @@ const value = 42; await gesture.up(); await tester.pumpAndSettle(); - final scrollableState = - tester.state(find.byType(Scrollable)); + final scrollableState = tester.state( + find.byType(Scrollable), + ); expect(scrollableState.position.pixels, greaterThan(0)); expect(selectionController.hasSelection, isTrue); expect(selectionController.selectedPlainText, contains('Paragraph 0')); @@ -4430,8 +4412,8 @@ const scrollTargetIdentifierWithVeryLongSuffixAndExtraCharactersForAutoScroll = (widget) => widget is RichText && widget.text.toPlainText().contains( - 'const scrollTargetIdentifierWithVeryLongSuffixAndExtraCharactersForAutoScroll = 42;', - ), + 'const scrollTargetIdentifierWithVeryLongSuffixAndExtraCharactersForAutoScroll = 42;', + ), ); final richText = tester.widget(richTextFinder); final renderBox = tester.renderObject(richTextFinder); @@ -4527,12 +4509,12 @@ const scrollTargetIdentifierWithVeryLongSuffixAndExtraCharactersForAutoScroll = TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(SystemChannels.platform, (methodCall) async { - if (methodCall.method == 'Clipboard.setData') { - final arguments = methodCall.arguments as Map; - copiedText = arguments['text'] as String?; - } - return null; - }); + if (methodCall.method == 'Clipboard.setData') { + final arguments = methodCall.arguments as Map; + copiedText = arguments['text'] as String?; + } + return null; + }); addTearDown(() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger @@ -4820,9 +4802,7 @@ const veryLongValueName = 42; ); final scrollable = tester - .widgetList( - find.byType(SingleChildScrollView), - ) + .widgetList(find.byType(SingleChildScrollView)) .firstWhere((widget) => widget.scrollDirection == Axis.horizontal); scrollable.controller!.jumpTo(60); await tester.pump(); @@ -5035,8 +5015,7 @@ const veryLongValueName = 42; body: SizedBox( width: 320, child: MarkdownWidget( - data: - '3. Use the theme button in the app bar to switch the reading surface.', + data: '3. Use the theme button in the app bar to switch the reading surface.', selectionController: selectionController, ), ), @@ -5078,61 +5057,60 @@ const veryLongValueName = 42; }); testWidgets( - 'wrapped list hit testing keeps content selection in the line gutter', ( - tester, - ) async { - final selectionController = MarkdownSelectionController(); + 'wrapped list hit testing keeps content selection in the line gutter', + (tester) async { + final selectionController = MarkdownSelectionController(); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: SizedBox( - width: 320, - child: MarkdownWidget( - data: - '3. Use the theme button in the app bar to switch the reading surface.', - selectionController: selectionController, + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 320, + child: MarkdownWidget( + data: '3. Use the theme button in the app bar to switch the reading surface.', + selectionController: selectionController, + ), ), ), ), - ), - ); + ); - final listBlockFinder = find.byWidgetPredicate( - (widget) => - widget is SelectableMarkdownBlock && - widget.spec.plainText.contains('Use the theme button'), - ); - final listBlock = tester.widget(listBlockFinder); - final listContext = tester.element(listBlockFinder); - final listSize = tester.getSize(listBlockFinder); - final fullSelectionRects = listBlock.spec.selectionRectResolver!( - listContext, - listSize, - DocumentRange( - start: const DocumentPosition( - blockIndex: 0, - path: PathInBlock([0]), - textOffset: 0, - ), - end: DocumentPosition( - blockIndex: 0, - path: const PathInBlock([0]), - textOffset: listBlock.spec.plainText.length, + final listBlockFinder = find.byWidgetPredicate( + (widget) => + widget is SelectableMarkdownBlock && + widget.spec.plainText.contains('Use the theme button'), + ); + final listBlock = tester.widget(listBlockFinder); + final listContext = tester.element(listBlockFinder); + final listSize = tester.getSize(listBlockFinder); + final fullSelectionRects = listBlock.spec.selectionRectResolver!( + listContext, + listSize, + DocumentRange( + start: const DocumentPosition( + blockIndex: 0, + path: PathInBlock([0]), + textOffset: 0, + ), + end: DocumentPosition( + blockIndex: 0, + path: const PathInBlock([0]), + textOffset: listBlock.spec.plainText.length, + ), ), - ), - )..sort((a, b) => a.top.compareTo(b.top)); + )..sort((a, b) => a.top.compareTo(b.top)); - expect(fullSelectionRects.length, greaterThanOrEqualTo(2)); - final secondLineRect = fullSelectionRects[1]; - final resolvedOffset = listBlock.spec.textOffsetResolver!( - listContext, - listSize, - Offset(secondLineRect.left - 2, secondLineRect.center.dy), - ); + expect(fullSelectionRects.length, greaterThanOrEqualTo(2)); + final secondLineRect = fullSelectionRects[1]; + final resolvedOffset = listBlock.spec.textOffsetResolver!( + listContext, + listSize, + Offset(secondLineRect.left - 2, secondLineRect.center.dy), + ); - expect(resolvedOffset, greaterThan(3)); - }); + expect(resolvedOffset, greaterThan(3)); + }, + ); test('pretext selection rects merge overlapping inline style boxes', () { final layout = computeMarkdownPretextLayoutFromRuns( @@ -5185,9 +5163,11 @@ const veryLongValueName = 42; ); for (var index = 0; index < rects.length; index++) { - for (var otherIndex = index + 1; - otherIndex < rects.length; - otherIndex++) { + for ( + var otherIndex = index + 1; + otherIndex < rects.length; + otherIndex++ + ) { expect( _hasMeaningfulHorizontalOverlap(rects[index], rects[otherIndex]), isFalse, @@ -5197,85 +5177,88 @@ const veryLongValueName = 42; }); testWidgets( - 'quote renders without raw markdown marker and remains selectable', ( - tester, - ) async { - final selectionController = MarkdownSelectionController(); + 'quote renders without raw markdown marker and remains selectable', + (tester) async { + final selectionController = MarkdownSelectionController(); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: MarkdownWidget( - data: ''' + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: MarkdownWidget( + data: ''' > Quoted line > Next line ''', - selectionController: selectionController, + selectionController: selectionController, + ), ), ), - ), - ); + ); - final richTextFinder = find.byWidgetPredicate( - (widget) => - widget is RichText && - widget.text.toPlainText().contains('Quoted line'), - ); - final rawMarkdownFinder = find.byWidgetPredicate( - (widget) => - widget is RichText && - widget.text.toPlainText().contains('> Quoted line'), - ); + final richTextFinder = find.byWidgetPredicate( + (widget) => + widget is RichText && + widget.text.toPlainText().contains('Quoted line'), + ); + final rawMarkdownFinder = find.byWidgetPredicate( + (widget) => + widget is RichText && + widget.text.toPlainText().contains('> Quoted line'), + ); - expect(rawMarkdownFinder, findsNothing); + expect(rawMarkdownFinder, findsNothing); - final richText = tester.widget(richTextFinder); - final theme = MarkdownTheme.of(tester.element(find.byType(MarkdownWidget))); - final quotedStyle = _styleForText(richText.text, 'Quoted line'); - expect(quotedStyle?.fontSize, theme.quoteStyle.fontSize); - expect(quotedStyle?.color, theme.quoteStyle.color); + final richText = tester.widget(richTextFinder); + final theme = MarkdownTheme.of( + tester.element(find.byType(MarkdownWidget)), + ); + final quotedStyle = _styleForText(richText.text, 'Quoted line'); + expect(quotedStyle?.fontSize, theme.quoteStyle.fontSize); + expect(quotedStyle?.color, theme.quoteStyle.color); - final renderBox = tester.renderObject(richTextFinder); - final painter = TextPainter( - text: richText.text, - textDirection: TextDirection.ltr, - )..layout(maxWidth: renderBox.size.width); + final renderBox = tester.renderObject(richTextFinder); + final painter = TextPainter( + text: richText.text, + textDirection: TextDirection.ltr, + )..layout(maxWidth: renderBox.size.width); - final startOffset = painter.getOffsetForCaret( - const TextPosition(offset: 0), - Rect.zero, - ); - final endOffset = painter.getOffsetForCaret( - const TextPosition(offset: 6), - Rect.zero, - ); - final start = renderBox.localToGlobal(startOffset + const Offset(1, 8)); - final end = renderBox.localToGlobal(endOffset + const Offset(1, 8)); - - final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); - await gesture.addPointer(location: start); - await gesture.down(start); - await tester.pump(); - await gesture.moveTo(end); - await tester.pump(); - await gesture.up(); - await tester.pump(); - - expect(selectionController.hasSelection, isTrue); - expect(selectionController.selectedPlainText, 'Quoted'); - - final paintFinder = find.ancestor( - of: richTextFinder, - matching: find.byType(CustomPaint), - ); - expect( - tester.widget(paintFinder.first).foregroundPainter, - isNotNull, - ); - }); + final startOffset = painter.getOffsetForCaret( + const TextPosition(offset: 0), + Rect.zero, + ); + final endOffset = painter.getOffsetForCaret( + const TextPosition(offset: 6), + Rect.zero, + ); + final start = renderBox.localToGlobal(startOffset + const Offset(1, 8)); + final end = renderBox.localToGlobal(endOffset + const Offset(1, 8)); + + final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: start); + await gesture.down(start); + await tester.pump(); + await gesture.moveTo(end); + await tester.pump(); + await gesture.up(); + await tester.pump(); + + expect(selectionController.hasSelection, isTrue); + expect(selectionController.selectedPlainText, 'Quoted'); + + final paintFinder = find.ancestor( + of: richTextFinder, + matching: find.byType(CustomPaint), + ); + expect( + tester.widget(paintFinder.first).foregroundPainter, + isNotNull, + ); + }, + ); - testWidgets('quote partial selection does not paint the next visual line', - (tester) async { + testWidgets('quote partial selection does not paint the next visual line', ( + tester, + ) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -5333,68 +5316,67 @@ const veryLongValueName = 42; }); testWidgets( - 'quote uses a separate rounded side bar with transparent background', ( - tester, - ) async { - late MarkdownThemeData theme; + 'quote uses a separate rounded side bar with transparent background', + (tester) async { + late MarkdownThemeData theme; - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Builder( - builder: (context) { - theme = MarkdownThemeData.fallback(context).copyWith( - quoteBackgroundColor: Colors.transparent, - quoteBorderRadius: BorderRadius.circular(4), - ); - return MarkdownWidget( - data: '> Quoted line', - theme: theme, - ); - }, + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + theme = MarkdownThemeData.fallback(context).copyWith( + quoteBackgroundColor: Colors.transparent, + quoteBorderRadius: BorderRadius.circular(4), + ); + return MarkdownWidget(data: '> Quoted line', theme: theme); + }, + ), ), ), - ), - ); + ); - final backgroundBox = tester.widget( - find - .ancestor( - of: find.text('Quoted line'), - matching: find.byType(DecoratedBox), - ) - .last, - ); - final backgroundDecoration = backgroundBox.decoration as BoxDecoration; - expect(backgroundDecoration.color, theme.quoteBackgroundColor); - expect(backgroundDecoration.borderRadius, theme.quoteBorderRadius); - expect(backgroundDecoration.border, isNull); - - final sideBar = tester.widget( - find.descendant( - of: find.byType(MarkdownQuoteBlockView), - matching: find.byWidgetPredicate((widget) { - if (widget is! DecoratedBox) { - return false; - } - final decoration = widget.decoration; - return decoration is BoxDecoration && - decoration.color == theme.quoteBorderColor; - }), - ), - ); - final sideBarDecoration = sideBar.decoration as BoxDecoration; - expect(sideBarDecoration.borderRadius, - BorderRadius.horizontal(left: theme.quoteBorderRadius.topLeft)); + final backgroundBox = tester.widget( + find + .ancestor( + of: find.text('Quoted line'), + matching: find.byType(DecoratedBox), + ) + .last, + ); + final backgroundDecoration = backgroundBox.decoration as BoxDecoration; + expect(backgroundDecoration.color, theme.quoteBackgroundColor); + expect(backgroundDecoration.borderRadius, theme.quoteBorderRadius); + expect(backgroundDecoration.border, isNull); + + final sideBar = tester.widget( + find.descendant( + of: find.byType(MarkdownQuoteBlockView), + matching: find.byWidgetPredicate((widget) { + if (widget is! DecoratedBox) { + return false; + } + final decoration = widget.decoration; + return decoration is BoxDecoration && + decoration.color == theme.quoteBorderColor; + }), + ), + ); + final sideBarDecoration = sideBar.decoration as BoxDecoration; + expect( + sideBarDecoration.borderRadius, + BorderRadius.horizontal(left: theme.quoteBorderRadius.topLeft), + ); - final sideBarSize = tester.widget( - find.descendant( - of: find.byWidget(sideBar), - matching: find.byType(SizedBox), - ), - ); - expect(sideBarSize.width, theme.quoteBorderWidth); - }); + final sideBarSize = tester.widget( + find.descendant( + of: find.byWidget(sideBar), + matching: find.byType(SizedBox), + ), + ); + expect(sideBarSize.width, theme.quoteBorderWidth); + }, + ); testWidgets('quote style applies to nested heading list and inline code', ( tester, @@ -5439,8 +5421,9 @@ const veryLongValueName = 42; widget.runs?.any((run) => run.text == 'code') == true, ), ); - final codeStyle = - listTextBlock.runs!.singleWhere((run) => run.text == 'code').style; + final codeStyle = listTextBlock.runs! + .singleWhere((run) => run.text == 'code') + .style; expect(codeStyle.color, theme.quoteStyle.color); expect(codeStyle.fontStyle, theme.quoteStyle.fontStyle); @@ -5460,8 +5443,10 @@ const veryLongValueName = 42; ) .first, ); - final descriptorLinkStyle = - _styleForText(quoteBlock.spec.textSpan!, 'quoted link'); + final descriptorLinkStyle = _styleForText( + quoteBlock.spec.textSpan!, + 'quoted link', + ); expect(descriptorLinkStyle?.color, theme.linkStyle.color); expect(descriptorLinkStyle?.decoration, theme.linkStyle.decoration); }); @@ -5472,9 +5457,7 @@ const veryLongValueName = 42; await tester.pumpWidget( const MaterialApp( home: Scaffold( - body: MarkdownWidget( - data: '## [Title `code`](https://example.com)', - ), + body: MarkdownWidget(data: '## [Title `code`](https://example.com)'), ), ), ); @@ -5488,13 +5471,15 @@ const veryLongValueName = 42; ), ); - final titleStyle = - headingTextBlock.runs!.singleWhere((run) => run.text == 'Title ').style; + final titleStyle = headingTextBlock.runs! + .singleWhere((run) => run.text == 'Title ') + .style; expect(titleStyle.color, theme.linkStyle.color); expect(titleStyle.fontSize, theme.heading2Style.fontSize); - final codeStyle = - headingTextBlock.runs!.singleWhere((run) => run.text == 'code').style; + final codeStyle = headingTextBlock.runs! + .singleWhere((run) => run.text == 'code') + .style; expect(codeStyle.color, theme.linkStyle.color); expect(codeStyle.fontFamily, theme.inlineCodeStyle.fontFamily); expect(codeStyle.fontSize, theme.heading2Style.fontSize); @@ -5560,8 +5545,9 @@ const veryLongValueName = 42; final globalSelectionRects = selectionRects .map((rect) => rect.shift(quoteOrigin)) .toList(growable: false); - final headingRenderBox = - tester.renderObject(headingFinder.first); + final headingRenderBox = tester.renderObject( + headingFinder.first, + ); final headingRect = headingRenderBox.localToGlobal(Offset.zero) & headingRenderBox.size; final nestedRenderBox = tester.renderObject(nestedFinder.first); @@ -5573,52 +5559,52 @@ const veryLongValueName = 42; expect(selectionController.selectedPlainText, contains('Nested line')); expect(selectionController.selectedPlainText, isNot(contains('>'))); expect(globalSelectionRects, hasLength(3)); + expect(globalSelectionRects.first.overlaps(headingRect.deflate(1)), isTrue); + expect(globalSelectionRects.last.overlaps(nestedRect.deflate(1)), isTrue); expect( - globalSelectionRects.first.overlaps(headingRect.deflate(1)), - isTrue, + globalSelectionRects.first.center.dy, + lessThan(nestedRect.center.dy), ); expect( - globalSelectionRects.last.overlaps(nestedRect.deflate(1)), - isTrue, + globalSelectionRects.last.center.dy, + greaterThan(headingRect.center.dy), ); - expect( - globalSelectionRects.first.center.dy, lessThan(nestedRect.center.dy)); - expect(globalSelectionRects.last.center.dy, - greaterThan(headingRect.center.dy)); }); testWidgets( - 'quoted code blocks subscribe selection overlay to nested horizontal scrolls', - (tester) async { - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: SizedBox( - width: 220, - child: MarkdownWidget( - data: ''' + 'quoted code blocks subscribe selection overlay to nested horizontal scrolls', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 220, + child: MarkdownWidget( + data: ''' > ```dart > const veryLongValueName = 42; > ``` ''', + ), ), ), ), - ), - ); + ); - final quoteFinder = find.byWidgetPredicate( - (widget) => - widget is SelectableMarkdownBlock && - widget.spec.plainText.contains('const veryLongValueName = 42;'), - ); - final quoteWidget = tester.widget(quoteFinder); + final quoteFinder = find.byWidgetPredicate( + (widget) => + widget is SelectableMarkdownBlock && + widget.spec.plainText.contains('const veryLongValueName = 42;'), + ); + final quoteWidget = tester.widget(quoteFinder); - expect(quoteWidget.spec.repaintListenable, isNotNull); - }); + expect(quoteWidget.spec.repaintListenable, isNotNull); + }, + ); - testWidgets('quoted code block selection is clipped to the nested viewport', - (tester) async { + testWidgets('quoted code block selection is clipped to the nested viewport', ( + tester, + ) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -5669,83 +5655,88 @@ const veryLongValueName = 42; }); testWidgets( - 'quoted code blocks merge adjacent syntax-highlight selection boxes', ( - tester, - ) async { - final selectionController = MarkdownSelectionController(); + 'quoted code blocks merge adjacent syntax-highlight selection boxes', + (tester) async { + final selectionController = MarkdownSelectionController(); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: MarkdownWidget( - data: ''' + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: MarkdownWidget( + data: ''' > ```python > for i in range(3): > print(i) > ``` ''', - selectionController: selectionController, + selectionController: selectionController, + ), ), ), - ), - ); - - final quoteFinder = find.byWidgetPredicate( - (widget) => - widget is SelectableMarkdownBlock && - widget.spec.plainText.contains('for i in range(3):'), - ); - final quoteWidget = tester.widget(quoteFinder); - final quoteElement = tester.element(quoteFinder); - final quoteRenderBox = tester.renderObject(quoteFinder); - final lineTextFinder = find.byWidgetPredicate( - (widget) => - widget is RichText && - widget.text.toPlainText().contains('for i in range(3):'), - ); - final lineRenderBox = tester.renderObject(lineTextFinder.first); - final lineRect = - lineRenderBox.localToGlobal(Offset.zero) & lineRenderBox.size; + ); - final selectionRects = quoteWidget.spec.selectionRectResolver!.call( - quoteElement, - quoteRenderBox.size, - DocumentRange( - start: const DocumentPosition( - blockIndex: 0, - path: PathInBlock([0]), - textOffset: 0, - ), - end: DocumentPosition( - blockIndex: 0, - path: const PathInBlock([0]), - textOffset: quoteWidget.spec.plainText.length, + final quoteFinder = find.byWidgetPredicate( + (widget) => + widget is SelectableMarkdownBlock && + widget.spec.plainText.contains('for i in range(3):'), + ); + final quoteWidget = tester.widget(quoteFinder); + final quoteElement = tester.element(quoteFinder); + final quoteRenderBox = tester.renderObject(quoteFinder); + final lineTextFinder = find.byWidgetPredicate( + (widget) => + widget is RichText && + widget.text.toPlainText().contains('for i in range(3):'), + ); + final lineRenderBox = tester.renderObject( + lineTextFinder.first, + ); + final lineRect = + lineRenderBox.localToGlobal(Offset.zero) & lineRenderBox.size; + + final selectionRects = quoteWidget.spec.selectionRectResolver!.call( + quoteElement, + quoteRenderBox.size, + DocumentRange( + start: const DocumentPosition( + blockIndex: 0, + path: PathInBlock([0]), + textOffset: 0, + ), + end: DocumentPosition( + blockIndex: 0, + path: const PathInBlock([0]), + textOffset: quoteWidget.spec.plainText.length, + ), ), - ), - ); - final quoteOrigin = quoteRenderBox.localToGlobal(Offset.zero); - final globalRects = selectionRects - .map((rect) => rect.shift(quoteOrigin)) - .where( - (rect) => - rect.bottom > lineRect.top + 1 && rect.top < lineRect.bottom - 1, - ) - .toList(growable: false); + ); + final quoteOrigin = quoteRenderBox.localToGlobal(Offset.zero); + final globalRects = selectionRects + .map((rect) => rect.shift(quoteOrigin)) + .where( + (rect) => + rect.bottom > lineRect.top + 1 && + rect.top < lineRect.bottom - 1, + ) + .toList(growable: false); - for (var index = 0; index < globalRects.length; index++) { - for (var otherIndex = index + 1; + for (var index = 0; index < globalRects.length; index++) { + for ( + var otherIndex = index + 1; otherIndex < globalRects.length; - otherIndex++) { - expect( - _hasMeaningfulHorizontalOverlap( - globalRects[index], - globalRects[otherIndex], - ), - isFalse, - ); + otherIndex++ + ) { + expect( + _hasMeaningfulHorizontalOverlap( + globalRects[index], + globalRects[otherIndex], + ), + isFalse, + ); + } } - } - }); + }, + ); testWidgets('quoted table selection is clipped to the nested viewport', ( tester, @@ -5801,80 +5792,83 @@ const veryLongValueName = 42; }); testWidgets( - 'quote gap below a list keeps downward selection anchored at the list end', - (tester) async { - final selectionController = MarkdownSelectionController(); + 'quote gap below a list keeps downward selection anchored at the list end', + (tester) async { + final selectionController = MarkdownSelectionController(); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: MarkdownWidget( - data: ''' + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: MarkdownWidget( + data: ''' > - First item > - Second item > > After block ''', - selectionController: selectionController, + selectionController: selectionController, + ), ), ), - ), - ); + ); - final secondFinder = find.byWidgetPredicate( - (widget) => - widget is RichText && - widget.text.toPlainText().contains('Second item'), - ); - final afterFinder = find.byWidgetPredicate( - (widget) => - widget is RichText && - widget.text.toPlainText().contains('After block'), - ); - final quoteFinder = find.byWidgetPredicate( - (widget) => - widget is SelectableMarkdownBlock && - widget.spec.plainText.contains('Second item') && - widget.spec.plainText.contains('After block'), - ); - final quoteWidget = tester.widget(quoteFinder); - final quoteElement = tester.element(quoteFinder); - final quoteRenderBox = tester.renderObject(quoteFinder); - final secondRenderBox = tester.renderObject(secondFinder.first); - final secondRect = - secondRenderBox.localToGlobal(Offset.zero) & secondRenderBox.size; - final afterRenderBox = tester.renderObject(afterFinder.first); - final afterRect = - afterRenderBox.localToGlobal(Offset.zero) & afterRenderBox.size; - final secondItemPoint = Offset( - secondRect.right - 2, - secondRect.center.dy, - ); - final gapPoint = Offset( - secondRect.center.dx, - (secondRect.bottom + afterRect.top) / 2, - ); - final secondItemOffset = quoteWidget.spec.textOffsetResolver!( - quoteElement, - quoteRenderBox.size, - quoteRenderBox.globalToLocal(secondItemPoint), - ); - final gapOffset = quoteWidget.spec.textOffsetResolver!( - quoteElement, - quoteRenderBox.size, - quoteRenderBox.globalToLocal(gapPoint), - ); - final afterOffset = quoteWidget.spec.plainText.indexOf('After block'); + final secondFinder = find.byWidgetPredicate( + (widget) => + widget is RichText && + widget.text.toPlainText().contains('Second item'), + ); + final afterFinder = find.byWidgetPredicate( + (widget) => + widget is RichText && + widget.text.toPlainText().contains('After block'), + ); + final quoteFinder = find.byWidgetPredicate( + (widget) => + widget is SelectableMarkdownBlock && + widget.spec.plainText.contains('Second item') && + widget.spec.plainText.contains('After block'), + ); + final quoteWidget = tester.widget(quoteFinder); + final quoteElement = tester.element(quoteFinder); + final quoteRenderBox = tester.renderObject(quoteFinder); + final secondRenderBox = tester.renderObject( + secondFinder.first, + ); + final secondRect = + secondRenderBox.localToGlobal(Offset.zero) & secondRenderBox.size; + final afterRenderBox = tester.renderObject(afterFinder.first); + final afterRect = + afterRenderBox.localToGlobal(Offset.zero) & afterRenderBox.size; + final secondItemPoint = Offset( + secondRect.right - 2, + secondRect.center.dy, + ); + final gapPoint = Offset( + secondRect.center.dx, + (secondRect.bottom + afterRect.top) / 2, + ); + final secondItemOffset = quoteWidget.spec.textOffsetResolver!( + quoteElement, + quoteRenderBox.size, + quoteRenderBox.globalToLocal(secondItemPoint), + ); + final gapOffset = quoteWidget.spec.textOffsetResolver!( + quoteElement, + quoteRenderBox.size, + quoteRenderBox.globalToLocal(gapPoint), + ); + final afterOffset = quoteWidget.spec.plainText.indexOf('After block'); - expect(secondItemOffset, isNotNull); - expect(gapOffset, isNotNull); + expect(secondItemOffset, isNotNull); + expect(gapOffset, isNotNull); - final resolvedSecondItemOffset = secondItemOffset!; - final resolvedGapOffset = gapOffset!; - expect(resolvedSecondItemOffset, greaterThan(0)); - expect(resolvedGapOffset, greaterThanOrEqualTo(resolvedSecondItemOffset)); - expect(resolvedGapOffset, lessThanOrEqualTo(afterOffset)); - }); + final resolvedSecondItemOffset = secondItemOffset!; + final resolvedGapOffset = gapOffset!; + expect(resolvedSecondItemOffset, greaterThan(0)); + expect(resolvedGapOffset, greaterThanOrEqualTo(resolvedSecondItemOffset)); + expect(resolvedGapOffset, lessThanOrEqualTo(afterOffset)); + }, + ); testWidgets('nested lists keep selection backgrounds aligned to item text', ( tester, @@ -5996,8 +5990,43 @@ const veryLongValueName = 42; }); testWidgets( - 'dragging inside an image caption with unknown size selects caption text', - ( + 'dragging inside an image caption with unknown size selects caption text', + (tester) async { + final selectionController = MarkdownSelectionController(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: MarkdownWidget( + data: '![Caption text](missing-image.png)', + selectionController: selectionController, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + final captionFinder = find.text('Caption text'); + expect(captionFinder, findsOneWidget); + + final start = tester.getTopLeft(captionFinder) + const Offset(1, 4); + final end = tester.getTopRight(captionFinder) + const Offset(-1, 4); + + final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: start); + await gesture.down(start); + await tester.pump(); + await gesture.moveTo(end); + await tester.pump(); + await gesture.up(); + await tester.pump(); + + expect(selectionController.hasSelection, isTrue); + expect(selectionController.selectedPlainText, 'Caption text'); + }, + ); + + testWidgets('custom imageBuilder blocks participate in selection', ( tester, ) async { final selectionController = MarkdownSelectionController(); @@ -6006,43 +6035,7 @@ const veryLongValueName = 42; MaterialApp( home: Scaffold( body: MarkdownWidget( - data: '![Caption text](missing-image.png)', - selectionController: selectionController, - ), - ), - ), - ); - await tester.pumpAndSettle(); - - final captionFinder = find.text('Caption text'); - expect(captionFinder, findsOneWidget); - - final start = tester.getTopLeft(captionFinder) + const Offset(1, 4); - final end = tester.getTopRight(captionFinder) + const Offset(-1, 4); - - final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); - await gesture.addPointer(location: start); - await gesture.down(start); - await tester.pump(); - await gesture.moveTo(end); - await tester.pump(); - await gesture.up(); - await tester.pump(); - - expect(selectionController.hasSelection, isTrue); - expect(selectionController.selectedPlainText, 'Caption text'); - }); - - testWidgets('custom imageBuilder blocks participate in selection', ( - tester, - ) async { - final selectionController = MarkdownSelectionController(); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: MarkdownWidget( - data: '![Custom caption](missing-image.png?w=400&h=200)', + data: '![Custom caption](missing-image.png?w=400&h=200)', selectionController: selectionController, imageBuilder: (context, block, theme) { return Container( @@ -6074,19 +6067,20 @@ const veryLongValueName = 42; expect(selectionController.selectedPlainText, 'Custom caption'); }); - testWidgets('dragging across table cells copies TSV selection', - (tester) async { + testWidgets('dragging across table cells copies TSV selection', ( + tester, + ) async { String? copiedText; final selectionController = MarkdownSelectionController(); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(SystemChannels.platform, (methodCall) async { - if (methodCall.method == 'Clipboard.setData') { - final arguments = methodCall.arguments as Map; - copiedText = arguments['text'] as String?; - } - return null; - }); + if (methodCall.method == 'Clipboard.setData') { + final arguments = methodCall.arguments as Map; + copiedText = arguments['text'] as String?; + } + return null; + }); addTearDown(() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger @@ -6165,425 +6159,439 @@ const veryLongValueName = 42; }); testWidgets( - 'triple click inside a table cell with inline math selects the whole cell', - (tester) async { - final selectionController = MarkdownSelectionController(); + 'triple click inside a table cell with inline math selects the whole cell', + (tester) async { + final selectionController = MarkdownSelectionController(); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: MarkdownWidget( - data: r''' + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: MarkdownWidget( + data: r''' | Feature | Description | Status | | :--- | :---: | ---: | | **Math** | Full LaTeX parsing & rendering (\( \alpha^2 \)) | ✅ | ''', - selectionController: selectionController, + selectionController: selectionController, + ), ), ), - ), - ); + ); - final target = tester.getCenter(find.textContaining('LaTeX')); + final target = tester.getCenter(find.textContaining('LaTeX')); - await _tripleTapAt(tester, target); + await _tripleTapAt(tester, target); - expect(selectionController.hasSelection, isTrue); - expect( - selectionController.selectedPlainText, - contains('Full LaTeX parsing & rendering'), - ); - expect(selectionController.selectedPlainText, contains(r'\alpha^2')); - expect(selectionController.selectedPlainText, isNot(contains('\t'))); - expect(selectionController.selectedPlainText, isNot(contains('\n'))); - }); + expect(selectionController.hasSelection, isTrue); + expect( + selectionController.selectedPlainText, + contains('Full LaTeX parsing & rendering'), + ); + expect(selectionController.selectedPlainText, contains(r'\alpha^2')); + expect(selectionController.selectedPlainText, isNot(contains('\t'))); + expect(selectionController.selectedPlainText, isNot(contains('\n'))); + }, + ); testWidgets( - 'table block refreshes cached selection visuals when the selection range changes', - (tester) async { - final selectionController = MarkdownSelectionController(); + 'table block refreshes cached selection visuals when the selection range changes', + (tester) async { + final selectionController = MarkdownSelectionController(); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: MarkdownWidget( - data: ''' + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: MarkdownWidget( + data: ''' | Name | Value | | --- | --- | | row | 42 | ''', - selectionController: selectionController, + selectionController: selectionController, + ), ), ), - ), - ); - - final tableBlockFinder = find.byWidgetPredicate( - (widget) => - widget is SelectableMarkdownBlock && - widget.spec.plainText == 'Name\tValue\nrow\t42', - ); - expect(tableBlockFinder, findsOneWidget); - - selectionController.setSelection( - const DocumentSelection( - base: DocumentPosition( - blockIndex: 0, - path: PathInBlock([0]), - textOffset: 0, - ), - extent: DocumentPosition( - blockIndex: 0, - path: PathInBlock([0]), - textOffset: 4, - ), - ), - ); - await tester.pump(); - - var tableBlock = tester.widget(tableBlockFinder); - expect(tableBlock.selectionRange, isNotNull); - expect(tableBlock.selectionRange!.start.textOffset, 0); - expect(tableBlock.selectionRange!.end.textOffset, 4); + ); - selectionController.setSelection( - const DocumentSelection( - base: DocumentPosition( - blockIndex: 0, - path: PathInBlock([0]), - textOffset: 15, + final tableBlockFinder = find.byWidgetPredicate( + (widget) => + widget is SelectableMarkdownBlock && + widget.spec.plainText == 'Name\tValue\nrow\t42', + ); + expect(tableBlockFinder, findsOneWidget); + + selectionController.setSelection( + const DocumentSelection( + base: DocumentPosition( + blockIndex: 0, + path: PathInBlock([0]), + textOffset: 0, + ), + extent: DocumentPosition( + blockIndex: 0, + path: PathInBlock([0]), + textOffset: 4, + ), ), - extent: DocumentPosition( - blockIndex: 0, - path: PathInBlock([0]), - textOffset: 17, + ); + await tester.pump(); + + var tableBlock = tester.widget(tableBlockFinder); + expect(tableBlock.selectionRange, isNotNull); + expect(tableBlock.selectionRange!.start.textOffset, 0); + expect(tableBlock.selectionRange!.end.textOffset, 4); + + selectionController.setSelection( + const DocumentSelection( + base: DocumentPosition( + blockIndex: 0, + path: PathInBlock([0]), + textOffset: 15, + ), + extent: DocumentPosition( + blockIndex: 0, + path: PathInBlock([0]), + textOffset: 17, + ), ), - ), - ); - await tester.pump(); + ); + await tester.pump(); - tableBlock = tester.widget(tableBlockFinder); - expect(tableBlock.selectionRange, isNotNull); - expect(tableBlock.selectionRange!.start.textOffset, 15); - expect(tableBlock.selectionRange!.end.textOffset, 17); - }); + tableBlock = tester.widget(tableBlockFinder); + expect(tableBlock.selectionRange, isNotNull); + expect(tableBlock.selectionRange!.start.textOffset, 15); + expect(tableBlock.selectionRange!.end.textOffset, 17); + }, + ); - testWidgets('triple click inside wrapped paragraph selects the current line', - ( - tester, - ) async { - final selectionController = MarkdownSelectionController(); + testWidgets( + 'triple click inside wrapped paragraph selects the current line', + (tester) async { + final selectionController = MarkdownSelectionController(); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Align( - alignment: Alignment.topLeft, - child: SizedBox( - width: 120, - child: MarkdownWidget( - data: 'alpha \nbeta \ngamma', - selectionController: selectionController, + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 120, + child: MarkdownWidget( + data: 'alpha \nbeta \ngamma', + selectionController: selectionController, + ), ), ), ), ), - ), - ); + ); - final paragraphFinder = find.textContaining('alpha', findRichText: true); - final rect = tester.getRect(paragraphFinder); - final target = Offset(rect.center.dx, rect.top + rect.height / 2); + final paragraphFinder = find.textContaining('alpha', findRichText: true); + final rect = tester.getRect(paragraphFinder); + final target = Offset(rect.center.dx, rect.top + rect.height / 2); - await tester.tapAt(target); - await tester.pump(); - await tester.tapAt(target); - await tester.pump(); - await tester.tapAt(target); - await tester.pump(); + await tester.tapAt(target); + await tester.pump(); + await tester.tapAt(target); + await tester.pump(); + await tester.tapAt(target); + await tester.pump(); - expect(selectionController.hasSelection, isTrue); - expect(selectionController.selectedPlainText, 'beta'); - }); + expect(selectionController.hasSelection, isTrue); + expect(selectionController.selectedPlainText, 'beta'); + }, + ); testWidgets( - 'triple click on wrapped kbd paragraph selects only the first visual line', - (tester) async { - final selectionController = MarkdownSelectionController(); + 'triple click on wrapped kbd paragraph selects only the first visual line', + (tester) async { + final selectionController = MarkdownSelectionController(); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Align( - alignment: Alignment.topLeft, - child: SizedBox( - width: 170, - child: MarkdownWidget( - data: - 'Press Ctrl + C to copy the current selection quickly.', - selectionController: selectionController, + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 170, + child: MarkdownWidget( + data: 'Press Ctrl + C to copy the current selection quickly.', + selectionController: selectionController, + ), ), ), ), ), - ), - ); - - final blockFinder = find.byWidgetPredicate( - (widget) => - widget is SelectableMarkdownBlock && - widget.spec.plainText.contains( - 'Press Ctrl + C to copy the current selection quickly.'), - ); - final block = tester.widget(blockFinder); - final blockElement = tester.element(blockFinder); - final blockRenderBox = tester.renderObject(blockFinder); - - final fullRects = block.spec.selectionRectResolver!( - blockElement, - blockRenderBox.size, - DocumentRange( - start: const DocumentPosition( - blockIndex: 0, - path: PathInBlock([0]), - textOffset: 0, - ), - end: DocumentPosition( - blockIndex: 0, - path: const PathInBlock([0]), - textOffset: block.spec.plainText.length, - ), - ), - )..sort((a, b) { - final topCompare = a.top.compareTo(b.top); - if (topCompare != 0) { - return topCompare; - } - return a.left.compareTo(b.left); - }); + ); - expect(fullRects.length, greaterThanOrEqualTo(2)); + final blockFinder = find.byWidgetPredicate( + (widget) => + widget is SelectableMarkdownBlock && + widget.spec.plainText.contains( + 'Press Ctrl + C to copy the current selection quickly.', + ), + ); + final block = tester.widget(blockFinder); + final blockElement = tester.element(blockFinder); + final blockRenderBox = tester.renderObject(blockFinder); - bool sameVisualLine(Rect a, Rect b) { - return (a.top - b.top).abs() <= 1.0 && (a.bottom - b.bottom).abs() <= 1.0; - } + final fullRects = + block.spec.selectionRectResolver!( + blockElement, + blockRenderBox.size, + DocumentRange( + start: const DocumentPosition( + blockIndex: 0, + path: PathInBlock([0]), + textOffset: 0, + ), + end: DocumentPosition( + blockIndex: 0, + path: const PathInBlock([0]), + textOffset: block.spec.plainText.length, + ), + ), + )..sort((a, b) { + final topCompare = a.top.compareTo(b.top); + if (topCompare != 0) { + return topCompare; + } + return a.left.compareTo(b.left); + }); + + expect(fullRects.length, greaterThanOrEqualTo(2)); + + bool sameVisualLine(Rect a, Rect b) { + return (a.top - b.top).abs() <= 1.0 && + (a.bottom - b.bottom).abs() <= 1.0; + } - final firstLineRects = fullRects - .where((rect) => sameVisualLine(rect, fullRects.first)) - .toList(growable: false); - final secondLineAnchor = - fullRects.firstWhere((rect) => !sameVisualLine(rect, fullRects.first)); - final secondLineRects = fullRects - .where((rect) => sameVisualLine(rect, secondLineAnchor)) - .toList(growable: false); - final secondLineTop = - secondLineRects.map((rect) => rect.top).reduce(math.min); - final secondLineStartOffset = block.spec.textOffsetResolver!( - blockElement, - blockRenderBox.size, - Offset(secondLineRects.first.left + 1, secondLineRects.first.center.dy), - ); + final firstLineRects = fullRects + .where((rect) => sameVisualLine(rect, fullRects.first)) + .toList(growable: false); + final secondLineAnchor = fullRects.firstWhere( + (rect) => !sameVisualLine(rect, fullRects.first), + ); + final secondLineRects = fullRects + .where((rect) => sameVisualLine(rect, secondLineAnchor)) + .toList(growable: false); + final secondLineTop = secondLineRects + .map((rect) => rect.top) + .reduce(math.min); + final secondLineStartOffset = block.spec.textOffsetResolver!( + blockElement, + blockRenderBox.size, + Offset(secondLineRects.first.left + 1, secondLineRects.first.center.dy), + ); - final target = blockRenderBox.localToGlobal(firstLineRects.first.center); + final target = blockRenderBox.localToGlobal(firstLineRects.first.center); - await tester.tapAt(target); - await tester.pump(); - await tester.tapAt(target); - await tester.pump(); - await tester.tapAt(target); - await tester.pump(); + await tester.tapAt(target); + await tester.pump(); + await tester.tapAt(target); + await tester.pump(); + await tester.tapAt(target); + await tester.pump(); - expect(selectionController.hasSelection, isTrue); - expect(selectionController.selectedPlainText.length, - lessThan(block.spec.plainText.length)); + expect(selectionController.hasSelection, isTrue); + expect( + selectionController.selectedPlainText.length, + lessThan(block.spec.plainText.length), + ); - final selectionRange = selectionController.normalizedRange!; - expect(secondLineStartOffset, isNotNull); - expect( - selectionRange.end.textOffset, - lessThanOrEqualTo(secondLineStartOffset!), - ); + final selectionRange = selectionController.normalizedRange!; + expect(secondLineStartOffset, isNotNull); + expect( + selectionRange.end.textOffset, + lessThanOrEqualTo(secondLineStartOffset!), + ); - final selectionRects = block.spec.selectionRectResolver!( - blockElement, - blockRenderBox.size, - selectionRange, - ); - expect(selectionRects, isNotEmpty); - for (final rect in selectionRects) { - expect(rect.bottom, lessThanOrEqualTo(secondLineTop + 0.5)); - } - }); + final selectionRects = block.spec.selectionRectResolver!( + blockElement, + blockRenderBox.size, + selectionRange, + ); + expect(selectionRects, isNotEmpty); + for (final rect in selectionRects) { + expect(rect.bottom, lessThanOrEqualTo(secondLineTop + 0.5)); + } + }, + ); testWidgets( - 'double click on copy and triple click on second line work for wrapped kbd sentence', - (tester) async { - final selectionController = MarkdownSelectionController(); - const markdown = - 'You can also use HTML tags like `` depending on your parser config: Press Ctrl + C to copy.'; + 'double click on copy and triple click on second line work for wrapped kbd sentence', + (tester) async { + final selectionController = MarkdownSelectionController(); + const markdown = + 'You can also use HTML tags like `` depending on your parser config: Press Ctrl + C to copy.'; - await tester.pumpWidget( - MaterialApp( - home: Builder( - builder: (context) { - final baseTheme = MarkdownThemeData.fallback(context); - return Scaffold( - body: Align( - alignment: Alignment.topLeft, - child: SizedBox( - width: 1280, - child: MarkdownWidget( - data: markdown, - padding: EdgeInsets.zero, - selectionController: selectionController, - theme: baseTheme.copyWith( - maxContentWidth: 1400, - bodyStyle: baseTheme.bodyStyle.copyWith( - fontSize: 16, - height: 1.25, - ), - inlineCodeStyle: baseTheme.inlineCodeStyle.copyWith( - fontSize: 15, - height: 1.25, + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + final baseTheme = MarkdownThemeData.fallback(context); + return Scaffold( + body: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 1280, + child: MarkdownWidget( + data: markdown, + padding: EdgeInsets.zero, + selectionController: selectionController, + theme: baseTheme.copyWith( + maxContentWidth: 1400, + bodyStyle: baseTheme.bodyStyle.copyWith( + fontSize: 16, + height: 1.25, + ), + inlineCodeStyle: baseTheme.inlineCodeStyle.copyWith( + fontSize: 15, + height: 1.25, + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), - ), - ); + ); - final blockFinder = find.byWidgetPredicate( - (widget) => - widget is SelectableMarkdownBlock && - widget.spec.plainText.contains( - 'You can also use HTML tags like depending on your parser config: Press Ctrl + C to copy.', - ), - ); - expect(blockFinder, findsOneWidget); + final blockFinder = find.byWidgetPredicate( + (widget) => + widget is SelectableMarkdownBlock && + widget.spec.plainText.contains( + 'You can also use HTML tags like depending on your parser config: Press Ctrl + C to copy.', + ), + ); + expect(blockFinder, findsOneWidget); - final block = tester.widget(blockFinder); - final blockElement = tester.element(blockFinder); - final blockRenderBox = tester.renderObject(blockFinder); - final pretextFinder = find.descendant( - of: blockFinder, - matching: find.byType(MarkdownPretextTextBlock), - ); - expect(pretextFinder, findsOneWidget); - final columnFinder = find.descendant( - of: pretextFinder, - matching: find.byType(Column), - ); - expect(columnFinder, findsOneWidget); - final lineBoxes = tester - .renderObjectList( - find.descendant(of: columnFinder, matching: find.byType(SizedBox)), - ) - .toList(growable: false); - expect(lineBoxes, hasLength(2)); - - final copyStart = block.spec.plainText.lastIndexOf('copy'); - final firstLineGlobalRect = - lineBoxes.first.localToGlobal(Offset.zero) & lineBoxes.first.size; - final secondLineGlobalRect = - lineBoxes[1].localToGlobal(Offset.zero) & lineBoxes[1].size; - final firstLineBottom = - blockRenderBox.globalToLocal(firstLineGlobalRect.bottomLeft).dy; - final secondLineRect = Rect.fromPoints( - blockRenderBox.globalToLocal(secondLineGlobalRect.topLeft), - blockRenderBox.globalToLocal(secondLineGlobalRect.bottomRight), - ); - final copyEnd = copyStart + 'copy'.length; - Offset? copyLocalTarget; - final sampleWidth = math.max(secondLineGlobalRect.width - 4, 1.0); - for (var step = 0; step <= 200; step++) { - final ratio = step / 200; - final sampleGlobal = Offset( - secondLineGlobalRect.right - 2 - sampleWidth * ratio, - secondLineGlobalRect.center.dy, - ); - final sample = blockRenderBox.globalToLocal(sampleGlobal); - final resolvedOffset = block.spec.textOffsetResolver!( - blockElement, - blockRenderBox.size, - sample, + final block = tester.widget(blockFinder); + final blockElement = tester.element(blockFinder); + final blockRenderBox = tester.renderObject(blockFinder); + final pretextFinder = find.descendant( + of: blockFinder, + matching: find.byType(MarkdownPretextTextBlock), ); - if (resolvedOffset != null && - resolvedOffset >= copyStart && - resolvedOffset <= copyEnd) { - copyLocalTarget = sample; - break; + expect(pretextFinder, findsOneWidget); + final columnFinder = find.descendant( + of: pretextFinder, + matching: find.byType(Column), + ); + expect(columnFinder, findsOneWidget); + final lineBoxes = tester + .renderObjectList( + find.descendant(of: columnFinder, matching: find.byType(SizedBox)), + ) + .toList(growable: false); + expect(lineBoxes, hasLength(2)); + + final copyStart = block.spec.plainText.lastIndexOf('copy'); + final firstLineGlobalRect = + lineBoxes.first.localToGlobal(Offset.zero) & lineBoxes.first.size; + final secondLineGlobalRect = + lineBoxes[1].localToGlobal(Offset.zero) & lineBoxes[1].size; + final firstLineBottom = blockRenderBox + .globalToLocal(firstLineGlobalRect.bottomLeft) + .dy; + final secondLineRect = Rect.fromPoints( + blockRenderBox.globalToLocal(secondLineGlobalRect.topLeft), + blockRenderBox.globalToLocal(secondLineGlobalRect.bottomRight), + ); + final copyEnd = copyStart + 'copy'.length; + Offset? copyLocalTarget; + final sampleWidth = math.max(secondLineGlobalRect.width - 4, 1.0); + for (var step = 0; step <= 200; step++) { + final ratio = step / 200; + final sampleGlobal = Offset( + secondLineGlobalRect.right - 2 - sampleWidth * ratio, + secondLineGlobalRect.center.dy, + ); + final sample = blockRenderBox.globalToLocal(sampleGlobal); + final resolvedOffset = block.spec.textOffsetResolver!( + blockElement, + blockRenderBox.size, + sample, + ); + if (resolvedOffset != null && + resolvedOffset >= copyStart && + resolvedOffset <= copyEnd) { + copyLocalTarget = sample; + break; + } } - } - expect(copyLocalTarget, isNotNull, - reason: - 'Expected to resolve a hit target for copy on the second rendered line.'); - final copyTarget = blockRenderBox.localToGlobal(copyLocalTarget!); + expect( + copyLocalTarget, + isNotNull, + reason: 'Expected to resolve a hit target for copy on the second rendered line.', + ); + final copyTarget = blockRenderBox.localToGlobal(copyLocalTarget!); - await _doubleTapAt(tester, copyTarget); + await _doubleTapAt(tester, copyTarget); - expect(selectionController.hasSelection, isTrue); - expect(selectionController.selectedPlainText, 'copy'); - final wordSelection = selectionController.normalizedRange!; - expect(wordSelection.start.textOffset, copyStart); - expect(wordSelection.end.textOffset, copyStart + 'copy'.length); + expect(selectionController.hasSelection, isTrue); + expect(selectionController.selectedPlainText, 'copy'); + final wordSelection = selectionController.normalizedRange!; + expect(wordSelection.start.textOffset, copyStart); + expect(wordSelection.end.textOffset, copyStart + 'copy'.length); - selectionController.clear(); - await tester.pump(); + selectionController.clear(); + await tester.pump(); - final secondLineStart = block.spec.textOffsetResolver!( - blockElement, - blockRenderBox.size, - Offset(secondLineRect.left + 1, secondLineRect.center.dy), - ); - final secondLineEnd = block.spec.textOffsetResolver!( - blockElement, - blockRenderBox.size, - Offset(secondLineRect.right - 1, secondLineRect.center.dy), - ); - expect(secondLineStart, isNotNull); - expect(secondLineEnd, isNotNull); - final secondLineStartOffset = secondLineStart!; - final secondLineEndOffset = secondLineEnd!; - expect(secondLineStartOffset, greaterThan(0)); - expect(secondLineEndOffset, greaterThan(secondLineStartOffset)); - final expectedSecondLine = block.spec.plainText - .substring(secondLineStartOffset, secondLineEndOffset) - .trimRight(); - final tripleTarget = blockRenderBox.localToGlobal( - Offset(secondLineRect.center.dx, secondLineRect.center.dy), - ); + final secondLineStart = block.spec.textOffsetResolver!( + blockElement, + blockRenderBox.size, + Offset(secondLineRect.left + 1, secondLineRect.center.dy), + ); + final secondLineEnd = block.spec.textOffsetResolver!( + blockElement, + blockRenderBox.size, + Offset(secondLineRect.right - 1, secondLineRect.center.dy), + ); + expect(secondLineStart, isNotNull); + expect(secondLineEnd, isNotNull); + final secondLineStartOffset = secondLineStart!; + final secondLineEndOffset = secondLineEnd!; + expect(secondLineStartOffset, greaterThan(0)); + expect(secondLineEndOffset, greaterThan(secondLineStartOffset)); + final expectedSecondLine = block.spec.plainText + .substring(secondLineStartOffset, secondLineEndOffset) + .trimRight(); + final tripleTarget = blockRenderBox.localToGlobal( + Offset(secondLineRect.center.dx, secondLineRect.center.dy), + ); - await _tripleTapAt(tester, tripleTarget); + await _tripleTapAt(tester, tripleTarget); - expect(selectionController.hasSelection, isTrue); - expect(selectionController.selectedPlainText, expectedSecondLine); - final lineSelection = selectionController.normalizedRange!; - expect( - lineSelection.start.textOffset, - greaterThanOrEqualTo(secondLineStartOffset), - ); - expect( - lineSelection.end.textOffset, - lessThanOrEqualTo(secondLineEndOffset), - ); + expect(selectionController.hasSelection, isTrue); + expect(selectionController.selectedPlainText, expectedSecondLine); + final lineSelection = selectionController.normalizedRange!; + expect( + lineSelection.start.textOffset, + greaterThanOrEqualTo(secondLineStartOffset), + ); + expect( + lineSelection.end.textOffset, + lessThanOrEqualTo(secondLineEndOffset), + ); - final selectionRects = block.spec.selectionRectResolver!( - blockElement, - blockRenderBox.size, - selectionController.normalizedRange!, - ); - expect(selectionRects, isNotEmpty); - for (final rect in selectionRects) { - expect(rect.top, greaterThanOrEqualTo(firstLineBottom - 0.5)); - } - }); + final selectionRects = block.spec.selectionRectResolver!( + blockElement, + blockRenderBox.size, + selectionController.normalizedRange!, + ); + expect(selectionRects, isNotEmpty); + for (final rect in selectionRects) { + expect(rect.top, greaterThanOrEqualTo(firstLineBottom - 0.5)); + } + }, + ); - testWidgets('triple click inside code block with empty lines stays on line', - (tester) async { + testWidgets('triple click inside code block with empty lines stays on line', ( + tester, + ) async { final selectionController = MarkdownSelectionController(); await tester.pumpWidget( @@ -6651,8 +6659,9 @@ def test_edge_case(): ); }); - testWidgets('triple click inside a code block does not throw', - (tester) async { + testWidgets('triple click inside a code block does not throw', ( + tester, + ) async { final selectionController = MarkdownSelectionController(); await tester.pumpWidget( @@ -6684,8 +6693,9 @@ return value; expect(selectionController.hasSelection, isTrue); }); - testWidgets('triple click inside a rich list item selects the current item', - (tester) async { + testWidgets('triple click inside a rich list item selects the current item', ( + tester, + ) async { final selectionController = MarkdownSelectionController(); await tester.pumpWidget( @@ -6715,19 +6725,19 @@ return value; }); testWidgets( - 'double click on blank area after a rich list line selects that line', - (tester) async { - final selectionController = MarkdownSelectionController(); + 'double click on blank area after a rich list line selects that line', + (tester) async { + final selectionController = MarkdownSelectionController(); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Align( - alignment: Alignment.topLeft, - child: SizedBox( - width: 500, - child: MarkdownWidget( - data: ''' + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 500, + child: MarkdownWidget( + data: ''' * *Italicized text* * **Bold emphasis** * ***Bold and italic*** @@ -6735,106 +6745,109 @@ return value; * `inline code snippets` * Link to [Flutter](https://flutter.dev), and an auto-link: ''', - selectionController: selectionController, + selectionController: selectionController, + ), ), ), ), ), - ), - ); + ); - final targetRect = tester.getRect(find.text('Bold emphasis')); - final documentRect = tester.getRect(find.byType(Scaffold)); - final target = Offset( - math.min(targetRect.right + 8, documentRect.right - 12), - targetRect.center.dy, - ); + final targetRect = tester.getRect(find.text('Bold emphasis')); + final documentRect = tester.getRect(find.byType(Scaffold)); + final target = Offset( + math.min(targetRect.right + 8, documentRect.right - 12), + targetRect.center.dy, + ); - await _doubleTapAt(tester, target); + await _doubleTapAt(tester, target); - expect(selectionController.hasSelection, isTrue); - expect(selectionController.selectedPlainText, '- Bold emphasis'); - }); + expect(selectionController.hasSelection, isTrue); + expect(selectionController.selectedPlainText, '- Bold emphasis'); + }, + ); testWidgets( - 'triple click on a parent list item does not include its nested sublist', - (tester) async { - final selectionController = MarkdownSelectionController(); + 'triple click on a parent list item does not include its nested sublist', + (tester) async { + final selectionController = MarkdownSelectionController(); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: MarkdownWidget( - data: ''' + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: MarkdownWidget( + data: ''' * Outer item * Inner item ''', - selectionController: selectionController, + selectionController: selectionController, + ), ), ), - ), - ); + ); - final target = tester.getCenter(find.text('Outer item')); + final target = tester.getCenter(find.text('Outer item')); - await tester.tapAt(target); - await tester.pump(); - await tester.tapAt(target); - await tester.pump(); - await tester.tapAt(target); - await tester.pump(); + await tester.tapAt(target); + await tester.pump(); + await tester.tapAt(target); + await tester.pump(); + await tester.tapAt(target); + await tester.pump(); - expect(selectionController.hasSelection, isTrue); - expect(selectionController.selectedPlainText, '- Outer item'); - }); + expect(selectionController.hasSelection, isTrue); + expect(selectionController.selectedPlainText, '- Outer item'); + }, + ); testWidgets( - 'triple click inside a list item code block selects the code block only', - (tester) async { - final selectionController = MarkdownSelectionController(); + 'triple click inside a list item code block selects the code block only', + (tester) async { + final selectionController = MarkdownSelectionController(); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: MarkdownWidget( - data: ''' + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: MarkdownWidget( + data: ''' * Parent item const child = 42; ''', - selectionController: selectionController, + selectionController: selectionController, + ), ), ), - ), - ); + ); - final target = tester.getCenter(find.textContaining('const child')); + final target = tester.getCenter(find.textContaining('const child')); - await tester.tapAt(target); - await tester.pump(); - await tester.tapAt(target); - await tester.pump(); - await tester.tapAt(target); - await tester.pump(); + await tester.tapAt(target); + await tester.pump(); + await tester.tapAt(target); + await tester.pump(); + await tester.tapAt(target); + await tester.pump(); - expect(selectionController.hasSelection, isTrue); - expect(selectionController.selectedPlainText, 'const child = 42;'); - }); + expect(selectionController.hasSelection, isTrue); + expect(selectionController.selectedPlainText, 'const child = 42;'); + }, + ); testWidgets( - 'triple click on ordered list lead line selects the current visual line with marker', - (tester) async { - final selectionController = MarkdownSelectionController(); + 'triple click on ordered list lead line selects the current visual line with marker', + (tester) async { + final selectionController = MarkdownSelectionController(); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Align( - alignment: Alignment.topLeft, - child: SizedBox( - width: 500, - child: MarkdownWidget( - data: r''' + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 500, + child: MarkdownWidget( + data: r''' ### Lists containing advanced blocks 1. **Code implementation:** @@ -6856,61 +6869,62 @@ return value; > Blockquotes can also live gracefully inside list items. The selection background will adapt to the indentation perfectly. ''', - selectionController: selectionController, + selectionController: selectionController, + ), ), ), ), ), - ), - ); + ); - final blockFinder = find.byWidgetPredicate( - (widget) => - widget is SelectableMarkdownBlock && - widget.spec.plainText.contains('Code implementation:') && - widget.spec.plainText.contains('Mathematical definitions:'), - ); - final block = tester.widget(blockFinder); - final leadStart = block.spec.plainText.indexOf('Code implementation:'); - final leadRect = _mergedRect( - _globalSelectionRectsForBlock( - tester, - blockFinder, - start: leadStart, - end: leadStart + 'Code implementation:'.length, - ), - ); - final target = leadRect.center; + final blockFinder = find.byWidgetPredicate( + (widget) => + widget is SelectableMarkdownBlock && + widget.spec.plainText.contains('Code implementation:') && + widget.spec.plainText.contains('Mathematical definitions:'), + ); + final block = tester.widget(blockFinder); + final leadStart = block.spec.plainText.indexOf('Code implementation:'); + final leadRect = _mergedRect( + _globalSelectionRectsForBlock( + tester, + blockFinder, + start: leadStart, + end: leadStart + 'Code implementation:'.length, + ), + ); + final target = leadRect.center; - await _tripleTapAt(tester, target); + await _tripleTapAt(tester, target); - expect(selectionController.hasSelection, isTrue); - final selectionRange = selectionController.normalizedRange!; - expect(selectionRange.start.path.segments, const [0, 0]); - expect(selectionRange.end.path.segments, const [0, 1]); - expect(selectionController.selectedPlainText, '1. Code implementation:'); - }); + expect(selectionController.hasSelection, isTrue); + final selectionRange = selectionController.normalizedRange!; + expect(selectionRange.start.path.segments, const [0, 0]); + expect(selectionRange.end.path.segments, const [0, 1]); + expect(selectionController.selectedPlainText, '1. Code implementation:'); + }, + ); testWidgets( - 'triple click on ordered list continuation sentence selects that full line', - (tester) async { - final selectionController = MarkdownSelectionController(); - tester.view.devicePixelRatio = 1.0; - tester.view.physicalSize = const Size(1800, 1200); - addTearDown(() { - tester.view.resetPhysicalSize(); - tester.view.resetDevicePixelRatio(); - }); + 'triple click on ordered list continuation sentence selects that full line', + (tester) async { + final selectionController = MarkdownSelectionController(); + tester.view.devicePixelRatio = 1.0; + tester.view.physicalSize = const Size(1800, 1200); + addTearDown(() { + tester.view.resetPhysicalSize(); + tester.view.resetDevicePixelRatio(); + }); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Align( - alignment: Alignment.topLeft, - child: SizedBox( - width: 1500, - child: MarkdownWidget( - data: r''' + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 1500, + child: MarkdownWidget( + data: r''' ### Lists containing advanced blocks 1. **Code implementation:** @@ -6932,63 +6946,65 @@ return value; > Blockquotes can also live gracefully inside list items. The selection background will adapt to the indentation perfectly. ''', - selectionController: selectionController, + selectionController: selectionController, + ), ), ), ), ), - ), - ); + ); - final blockFinder = find.byWidgetPredicate( - (widget) => - widget is SelectableMarkdownBlock && - widget.spec.plainText.contains('Code implementation:') && - widget.spec.plainText.contains( - 'Here is a quick way to compute a sum in JavaScript:', - ), - ); - final block = tester.widget(blockFinder); - final start = block.spec.plainText.indexOf( - 'Here is a quick way to compute a sum in JavaScript:', - ); - final sentenceRect = _mergedRect( - _globalSelectionRectsForBlock( - tester, - blockFinder, - start: start, - end: start + - 'Here is a quick way to compute a sum in JavaScript:'.length, - ), - ); - final target = sentenceRect.center; + final blockFinder = find.byWidgetPredicate( + (widget) => + widget is SelectableMarkdownBlock && + widget.spec.plainText.contains('Code implementation:') && + widget.spec.plainText.contains( + 'Here is a quick way to compute a sum in JavaScript:', + ), + ); + final block = tester.widget(blockFinder); + final start = block.spec.plainText.indexOf( + 'Here is a quick way to compute a sum in JavaScript:', + ); + final sentenceRect = _mergedRect( + _globalSelectionRectsForBlock( + tester, + blockFinder, + start: start, + end: + start + + 'Here is a quick way to compute a sum in JavaScript:'.length, + ), + ); + final target = sentenceRect.center; - await _tripleTapAt(tester, target); + await _tripleTapAt(tester, target); - expect(selectionController.hasSelection, isTrue); - final selectionRange = selectionController.normalizedRange!; - expect(selectionRange.start.path.segments, const [0, 1]); - expect(selectionRange.end.path.segments, const [0, 1]); - expect( - selectionController.selectedPlainText, - 'Here is a quick way to compute a sum in JavaScript:', - ); - }); + expect(selectionController.hasSelection, isTrue); + final selectionRange = selectionController.normalizedRange!; + expect(selectionRange.start.path.segments, const [0, 1]); + expect(selectionRange.end.path.segments, const [0, 1]); + expect( + selectionController.selectedPlainText, + 'Here is a quick way to compute a sum in JavaScript:', + ); + }, + ); testWidgets( - 'double click on ordered list continuation text selects the tapped word', - (tester) async { - final selectionController = MarkdownSelectionController(); + 'double click on ordered list continuation text selects the tapped word', + (tester) async { + final selectionController = MarkdownSelectionController(); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Align( - alignment: Alignment.topLeft, - child: SizedBox( - width: 900, - child: MarkdownWidget( - data: ''' + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 900, + child: MarkdownWidget( + data: ''' 1. **Code implementation:** Here is a quick way to compute a sum in JavaScript: @@ -6999,66 +7015,69 @@ return value; console.log(sum(5, 10)); // 15 ``` ''', - selectionController: selectionController, + selectionController: selectionController, + ), ), ), ), ), - ), - ); - - final sentenceFinder = - find.textContaining('Here is a quick way', findRichText: true); - final richText = tester.widget(sentenceFinder.first); - final renderBox = tester.renderObject(sentenceFinder.first); - final painter = TextPainter( - text: richText.text, - textDirection: TextDirection.ltr, - )..layout(maxWidth: renderBox.size.width); - final sentenceText = richText.text.toPlainText(); - final quickStart = sentenceText.indexOf('quick'); - final quickEnd = quickStart + 'quick'.length; - final quickStartOffset = painter.getOffsetForCaret( - TextPosition(offset: quickStart), - Rect.zero, - ); - final quickEndOffset = painter.getOffsetForCaret( - TextPosition(offset: quickEnd), - Rect.zero, - ); - final target = renderBox.localToGlobal( - Offset( - (quickStartOffset.dx + quickEndOffset.dx) / 2, - quickStartOffset.dy + painter.preferredLineHeight / 2, - ), - ); + ); - await tester.tapAt(target); - await tester.pump(); - await tester.tapAt(target); - await tester.pump(); + final sentenceFinder = find.textContaining( + 'Here is a quick way', + findRichText: true, + ); + final richText = tester.widget(sentenceFinder.first); + final renderBox = tester.renderObject(sentenceFinder.first); + final painter = TextPainter( + text: richText.text, + textDirection: TextDirection.ltr, + )..layout(maxWidth: renderBox.size.width); + final sentenceText = richText.text.toPlainText(); + final quickStart = sentenceText.indexOf('quick'); + final quickEnd = quickStart + 'quick'.length; + final quickStartOffset = painter.getOffsetForCaret( + TextPosition(offset: quickStart), + Rect.zero, + ); + final quickEndOffset = painter.getOffsetForCaret( + TextPosition(offset: quickEnd), + Rect.zero, + ); + final target = renderBox.localToGlobal( + Offset( + (quickStartOffset.dx + quickEndOffset.dx) / 2, + quickStartOffset.dy + painter.preferredLineHeight / 2, + ), + ); - expect(selectionController.hasSelection, isTrue); - final selectionRange = selectionController.normalizedRange!; - expect(selectionRange.start.path.segments, const [0, 1]); - expect(selectionRange.end.path.segments, const [0, 1]); - expect(selectionController.selectedPlainText, 'quick'); - }); + await tester.tapAt(target); + await tester.pump(); + await tester.tapAt(target); + await tester.pump(); + + expect(selectionController.hasSelection, isTrue); + final selectionRange = selectionController.normalizedRange!; + expect(selectionRange.start.path.segments, const [0, 1]); + expect(selectionRange.end.path.segments, const [0, 1]); + expect(selectionController.selectedPlainText, 'quick'); + }, + ); testWidgets( - 'triple click on ordered list continuation text selects the current visual line', - (tester) async { - final selectionController = MarkdownSelectionController(); + 'triple click on ordered list continuation text selects the current visual line', + (tester) async { + final selectionController = MarkdownSelectionController(); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Align( - alignment: Alignment.topLeft, - child: SizedBox( - width: 900, - child: MarkdownWidget( - data: ''' + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 900, + child: MarkdownWidget( + data: ''' 1. **Code implementation:** Here is a quick way to compute a sum in JavaScript: @@ -7069,75 +7088,78 @@ return value; console.log(sum(5, 10)); // 15 ``` ''', - selectionController: selectionController, + selectionController: selectionController, + ), ), ), ), ), - ), - ); - - final sentenceFinder = - find.textContaining('Here is a quick way', findRichText: true); - final richText = tester.widget(sentenceFinder.first); - final renderBox = tester.renderObject(sentenceFinder.first); - final painter = TextPainter( - text: richText.text, - textDirection: TextDirection.ltr, - )..layout(maxWidth: renderBox.size.width); - final sentenceText = richText.text.toPlainText(); - final quickStart = sentenceText.indexOf('quick'); - final quickEnd = quickStart + 'quick'.length; - final expectedLineBoundary = painter.getLineBoundary( - TextPosition(offset: quickStart), - ); - final expectedLine = sentenceText.substring( - expectedLineBoundary.start, - expectedLineBoundary.end, - ); - final quickStartOffset = painter.getOffsetForCaret( - TextPosition(offset: quickStart), - Rect.zero, - ); - final quickEndOffset = painter.getOffsetForCaret( - TextPosition(offset: quickEnd), - Rect.zero, - ); - final target = renderBox.localToGlobal( - Offset( - (quickStartOffset.dx + quickEndOffset.dx) / 2, - quickStartOffset.dy + painter.preferredLineHeight / 2, - ), - ); + ); - await tester.tapAt(target); - await tester.pump(); - await tester.tapAt(target); - await tester.pump(); - await tester.tapAt(target); - await tester.pump(); + final sentenceFinder = find.textContaining( + 'Here is a quick way', + findRichText: true, + ); + final richText = tester.widget(sentenceFinder.first); + final renderBox = tester.renderObject(sentenceFinder.first); + final painter = TextPainter( + text: richText.text, + textDirection: TextDirection.ltr, + )..layout(maxWidth: renderBox.size.width); + final sentenceText = richText.text.toPlainText(); + final quickStart = sentenceText.indexOf('quick'); + final quickEnd = quickStart + 'quick'.length; + final expectedLineBoundary = painter.getLineBoundary( + TextPosition(offset: quickStart), + ); + final expectedLine = sentenceText.substring( + expectedLineBoundary.start, + expectedLineBoundary.end, + ); + final quickStartOffset = painter.getOffsetForCaret( + TextPosition(offset: quickStart), + Rect.zero, + ); + final quickEndOffset = painter.getOffsetForCaret( + TextPosition(offset: quickEnd), + Rect.zero, + ); + final target = renderBox.localToGlobal( + Offset( + (quickStartOffset.dx + quickEndOffset.dx) / 2, + quickStartOffset.dy + painter.preferredLineHeight / 2, + ), + ); - expect(selectionController.hasSelection, isTrue); - final selectionRange = selectionController.normalizedRange!; - expect(selectionRange.start.path.segments, const [0, 1]); - expect(selectionRange.end.path.segments, const [0, 1]); - expect(selectionController.selectedPlainText, expectedLine); - }); + await tester.tapAt(target); + await tester.pump(); + await tester.tapAt(target); + await tester.pump(); + await tester.tapAt(target); + await tester.pump(); + + expect(selectionController.hasSelection, isTrue); + final selectionRange = selectionController.normalizedRange!; + expect(selectionRange.start.path.segments, const [0, 1]); + expect(selectionRange.end.path.segments, const [0, 1]); + expect(selectionController.selectedPlainText, expectedLine); + }, + ); testWidgets( - 'triple click on fenced code inside ordered list selects the current code line', - (tester) async { - final selectionController = MarkdownSelectionController(); + 'triple click on fenced code inside ordered list selects the current code line', + (tester) async { + final selectionController = MarkdownSelectionController(); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Align( - alignment: Alignment.topLeft, - child: SizedBox( - width: 900, - child: MarkdownWidget( - data: ''' + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 900, + child: MarkdownWidget( + data: ''' 1. **Code implementation:** Here is a quick way to compute a sum in JavaScript: @@ -7148,75 +7170,80 @@ return value; console.log(sum(5, 10)); // 15 ``` ''', - selectionController: selectionController, + selectionController: selectionController, + ), ), ), ), ), - ), - ); + ); - final codeFinder = find.textContaining('function sum', findRichText: true); - final codeRect = tester.getRect(codeFinder); - final target = Offset( - codeRect.left + 120, - codeRect.top + codeRect.height * 0.42, - ); + final codeFinder = find.textContaining( + 'function sum', + findRichText: true, + ); + final codeRect = tester.getRect(codeFinder); + final target = Offset( + codeRect.left + 120, + codeRect.top + codeRect.height * 0.42, + ); - await tester.tapAt(target); - await tester.pump(); - await tester.tapAt(target); - await tester.pump(); - await tester.tapAt(target); - await tester.pump(); + await tester.tapAt(target); + await tester.pump(); + await tester.tapAt(target); + await tester.pump(); + await tester.tapAt(target); + await tester.pump(); - expect(selectionController.hasSelection, isTrue); - expect(selectionController.selectedPlainText, ' return a + b;'); + expect(selectionController.hasSelection, isTrue); + expect(selectionController.selectedPlainText, ' return a + b;'); - // Verify selection background rects are within the selected code line. - final listFinder = find.byWidgetPredicate( - (widget) => - widget is SelectableMarkdownBlock && - widget.spec.plainText.contains('Code implementation'), - ); - final listWidget = tester.widget(listFinder); - final listElement = tester.element(listFinder); - final listRenderBox = tester.renderObject(listFinder); - final selectionRange = selectionController.normalizedRange!; - final selectionRects = listWidget.spec.selectionRectResolver!.call( - listElement, - listRenderBox.size, - selectionRange, - ); - expect(selectionRects, isNotEmpty); + // Verify selection background rects are within the selected code line. + final listFinder = find.byWidgetPredicate( + (widget) => + widget is SelectableMarkdownBlock && + widget.spec.plainText.contains('Code implementation'), + ); + final listWidget = tester.widget(listFinder); + final listElement = tester.element(listFinder); + final listRenderBox = tester.renderObject(listFinder); + final selectionRange = selectionController.normalizedRange!; + final selectionRects = listWidget.spec.selectionRectResolver!.call( + listElement, + listRenderBox.size, + selectionRange, + ); + expect(selectionRects, isNotEmpty); - final listOrigin = listRenderBox.localToGlobal(Offset.zero); - final globalSelectionRects = selectionRects - .map((rect) => rect.shift(listOrigin)) - .toList(growable: false); + final listOrigin = listRenderBox.localToGlobal(Offset.zero); + final globalSelectionRects = selectionRects + .map((rect) => rect.shift(listOrigin)) + .toList(growable: false); - // The selection rects must overlap the "return a + b" line vertically. - final codeRenderBox = tester.renderObject(codeFinder); - final codeTextRect = - codeRenderBox.localToGlobal(Offset.zero) & codeRenderBox.size; + // The selection rects must overlap the "return a + b" line vertically. + final codeRenderBox = tester.renderObject(codeFinder); + final codeTextRect = + codeRenderBox.localToGlobal(Offset.zero) & codeRenderBox.size; - // Compute the vertical band of the "return a + b;" line by using - // proportional line height within the code block text widget. - // The code block has 4 lines; "return a + b;" is the second line. - final lineHeight = codeTextRect.height / 4; - final returnLineTop = codeTextRect.top + lineHeight; - final returnLineBottom = codeTextRect.top + lineHeight * 2; + // Compute the vertical band of the "return a + b;" line by using + // proportional line height within the code block text widget. + // The code block has 4 lines; "return a + b;" is the second line. + final lineHeight = codeTextRect.height / 4; + final returnLineTop = codeTextRect.top + lineHeight; + final returnLineBottom = codeTextRect.top + lineHeight * 2; - for (final rect in globalSelectionRects) { - expect( - rect.top >= returnLineTop - 2 && rect.bottom <= returnLineBottom + 2, - isTrue, - reason: 'Selection rect $rect should be within the "return a + b;" ' - 'line band ($returnLineTop..$returnLineBottom), ' - 'but extends outside it.', - ); - } - }); + for (final rect in globalSelectionRects) { + expect( + rect.top >= returnLineTop - 2 && rect.bottom <= returnLineBottom + 2, + isTrue, + reason: + 'Selection rect $rect should be within the "return a + b;" ' + 'line band ($returnLineTop..$returnLineBottom), ' + 'but extends outside it.', + ); + } + }, + ); testWidgets('triple click inside a quote selects the current quoted block', ( tester, @@ -7391,8 +7418,10 @@ return value; ), ); final bananaTextTarget = bananaRect.center; - final bananaBlankTarget = - Offset(bananaRect.right + 2, bananaRect.center.dy); + final bananaBlankTarget = Offset( + bananaRect.right + 2, + bananaRect.center.dy, + ); await _tripleTapAt(tester, bananaTextTarget); @@ -7416,47 +7445,48 @@ return value; expect(selectionController.selectedPlainText, ' - Banana'); }); - testWidgets('triple click inside a nested quote selects the innermost quote', - ( - tester, - ) async { - final selectionController = MarkdownSelectionController(); + testWidgets( + 'triple click inside a nested quote selects the innermost quote', + (tester) async { + final selectionController = MarkdownSelectionController(); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: MarkdownWidget( - data: ''' + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: MarkdownWidget( + data: ''' > Outer quote > > Inner quote ''', - selectionController: selectionController, + selectionController: selectionController, + ), ), ), - ), - ); + ); - final target = tester.getCenter(find.text('Inner quote')); + final target = tester.getCenter(find.text('Inner quote')); - await tester.tapAt(target); - await tester.pump(); - await tester.tapAt(target); - await tester.pump(); - await tester.tapAt(target); - await tester.pump(); + await tester.tapAt(target); + await tester.pump(); + await tester.tapAt(target); + await tester.pump(); + await tester.tapAt(target); + await tester.pump(); - expect(selectionController.hasSelection, isTrue); - expect(selectionController.selectedPlainText, 'Inner quote'); - }); + expect(selectionController.hasSelection, isTrue); + expect(selectionController.selectedPlainText, 'Inner quote'); + }, + ); testWidgets( - 'selection auto-scroll falls back to ancestor scrollables for shrink-wrapped markdown widgets', - (tester) async { - final selectionController = MarkdownSelectionController(); - final outerScrollController = ScrollController(); - const targetLabel = 'Target message paragraph 1'; - - String buildMessage(String label) => ''' + 'selection auto-scroll falls back to ancestor scrollables for shrink-wrapped markdown widgets', + (tester) async { + final selectionController = MarkdownSelectionController(); + final outerScrollController = ScrollController(); + const targetLabel = 'Target message paragraph 1'; + + String buildMessage(String label) => + ''' $label $label continued with more content for drag selection. @@ -7465,216 +7495,231 @@ $label continued with more content for drag selection. - bullet b '''; - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: SizedBox( - height: 320, - child: ListView( - controller: outerScrollController, - children: List.generate(6, (index) { - final label = index == 1 ? targetLabel : 'Message ${index + 1}'; - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - child: MarkdownWidget( - data: buildMessage(label), - selectionController: - index == 1 ? selectionController : null, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - ), - ); - }), + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + height: 320, + child: ListView( + controller: outerScrollController, + children: List.generate(6, (index) { + final label = index == 1 + ? targetLabel + : 'Message ${index + 1}'; + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + child: MarkdownWidget( + data: buildMessage(label), + selectionController: index == 1 + ? selectionController + : null, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + ), + ); + }), + ), ), ), ), - ), - ); - - final outerListFinder = find.byWidgetPredicate( - (widget) => - widget is ListView && widget.controller == outerScrollController, - ); - final start = tester.getCenter(find.text(targetLabel)); - final viewport = tester.getRect(outerListFinder); - final end = Offset(start.dx, viewport.bottom + 80); - - final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); - await gesture.addPointer(location: start); - await gesture.down(start); - await tester.pump(); - await gesture.moveTo(end); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 250)); - - expect(outerScrollController.offset, greaterThan(0)); - expect(selectionController.hasSelection, isTrue); - expect(selectionController.selectedPlainText, contains(targetLabel)); + ); - await gesture.up(); - await tester.pump(); - }); + final outerListFinder = find.byWidgetPredicate( + (widget) => + widget is ListView && widget.controller == outerScrollController, + ); + final start = tester.getCenter(find.text(targetLabel)); + final viewport = tester.getRect(outerListFinder); + final end = Offset(start.dx, viewport.bottom + 80); + + final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: start); + await gesture.down(start); + await tester.pump(); + await gesture.moveTo(end); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 250)); + + expect(outerScrollController.offset, greaterThan(0)); + expect(selectionController.hasSelection, isTrue); + expect(selectionController.selectedPlainText, contains(targetLabel)); + + await gesture.up(); + await tester.pump(); + }, + ); testWidgets( - 'ancestor auto-scroll stops when the current shrink-wrapped markdown is already fully visible', - (tester) async { - final selectionController = MarkdownSelectionController(); - final outerScrollController = ScrollController(); - const targetLabel = 'Fully visible target'; - - String buildMessage(String label) => ''' + 'ancestor auto-scroll stops when the current shrink-wrapped markdown is already fully visible', + (tester) async { + final selectionController = MarkdownSelectionController(); + final outerScrollController = ScrollController(); + const targetLabel = 'Fully visible target'; + + String buildMessage(String label) => + ''' $label $label continued. '''; - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: SizedBox( - height: 320, - child: ListView( - controller: outerScrollController, - children: List.generate(10, (index) { - final label = index == 1 ? targetLabel : 'Message ${index + 1}'; - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - child: MarkdownWidget( - data: buildMessage(label), - selectionController: - index == 1 ? selectionController : null, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - ), - ); - }), + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + height: 320, + child: ListView( + controller: outerScrollController, + children: List.generate(10, (index) { + final label = index == 1 + ? targetLabel + : 'Message ${index + 1}'; + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + child: MarkdownWidget( + data: buildMessage(label), + selectionController: index == 1 + ? selectionController + : null, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + ), + ); + }), + ), ), ), ), - ), - ); - - final markdownFinder = find.ancestor( - of: find.text(targetLabel), - matching: find.byType(MarkdownWidget), - ); - final markdownRect = tester.getRect(markdownFinder); - final outerListFinder = find.byWidgetPredicate( - (widget) => - widget is ListView && widget.controller == outerScrollController, - ); - final viewport = tester.getRect(outerListFinder); - expect(markdownRect.bottom, lessThan(viewport.bottom)); - - final start = tester.getCenter(find.text(targetLabel)); - final end = Offset(start.dx, viewport.bottom - 8); - - final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); - await gesture.addPointer(location: start); - await gesture.down(start); - await tester.pump(); - await gesture.moveTo(end); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 250)); - - expect(outerScrollController.offset, 0); - expect(selectionController.hasSelection, isTrue); - expect(selectionController.selectedPlainText, contains(targetLabel)); + ); - await gesture.up(); - await tester.pump(); - }); + final markdownFinder = find.ancestor( + of: find.text(targetLabel), + matching: find.byType(MarkdownWidget), + ); + final markdownRect = tester.getRect(markdownFinder); + final outerListFinder = find.byWidgetPredicate( + (widget) => + widget is ListView && widget.controller == outerScrollController, + ); + final viewport = tester.getRect(outerListFinder); + expect(markdownRect.bottom, lessThan(viewport.bottom)); + + final start = tester.getCenter(find.text(targetLabel)); + final end = Offset(start.dx, viewport.bottom - 8); + + final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: start); + await gesture.down(start); + await tester.pump(); + await gesture.moveTo(end); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 250)); + + expect(outerScrollController.offset, 0); + expect(selectionController.hasSelection, isTrue); + expect(selectionController.selectedPlainText, contains(targetLabel)); + + await gesture.up(); + await tester.pump(); + }, + ); testWidgets( - 'ancestor auto-scroll stays idle for a fully visible chat-style markdown bubble', - (tester) async { - final outerScrollController = ScrollController(); - final selectionController = MarkdownSelectionController(); + 'ancestor auto-scroll stays idle for a fully visible chat-style markdown bubble', + (tester) async { + final outerScrollController = ScrollController(); + final selectionController = MarkdownSelectionController(); + + Widget buildAssistantBubble( + String text, { + MarkdownSelectionController? controller, + }) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const CircleAvatar(child: Icon(Icons.terminal_rounded)), + const SizedBox(width: 12), + Flexible( + child: Container( + padding: const EdgeInsets.all(16), + child: MarkdownWidget( + data: text, + selectionController: controller, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + ), + ), + ), + ], + ), + ); + } - Widget buildAssistantBubble(String text, - {MarkdownSelectionController? controller}) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const CircleAvatar(child: Icon(Icons.terminal_rounded)), - const SizedBox(width: 12), - Flexible( - child: Container( - padding: const EdgeInsets.all(16), - child: MarkdownWidget( - data: text, - selectionController: controller, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + height: 420, + child: ListView.builder( + controller: outerScrollController, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 20, ), + itemCount: 8, + itemBuilder: (context, index) { + if (index == 1) { + return buildAssistantBubble( + 'Visible bubble\n\nSecond paragraph.\n\n- bullet a\n- bullet b', + controller: selectionController, + ); + } + return buildAssistantBubble('Message ${index + 1}'); + }, ), ), - ], - ), - ); - } - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: SizedBox( - height: 420, - child: ListView.builder( - controller: outerScrollController, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), - itemCount: 8, - itemBuilder: (context, index) { - if (index == 1) { - return buildAssistantBubble( - 'Visible bubble\n\nSecond paragraph.\n\n- bullet a\n- bullet b', - controller: selectionController, - ); - } - return buildAssistantBubble('Message ${index + 1}'); - }, - ), ), ), - ), - ); - - final outerListFinder = find.byWidgetPredicate( - (widget) => - widget is ListView && widget.controller == outerScrollController, - ); - final markdownFinder = find.ancestor( - of: find.text('Visible bubble'), - matching: find.byType(MarkdownWidget), - ); - final markdownRect = tester.getRect(markdownFinder); - final viewportRect = tester.getRect(outerListFinder); - expect(markdownRect.bottom, lessThan(viewportRect.bottom)); - - final start = tester.getCenter(find.text('Visible bubble')); - final end = Offset(start.dx, viewportRect.bottom - 6); - - final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); - await gesture.addPointer(location: start); - await gesture.down(start); - await tester.pump(); - await gesture.moveTo(end); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 250)); - - expect(outerScrollController.offset, 0); - expect(selectionController.hasSelection, isTrue); + ); - await gesture.up(); - await tester.pump(); - }); + final outerListFinder = find.byWidgetPredicate( + (widget) => + widget is ListView && widget.controller == outerScrollController, + ); + final markdownFinder = find.ancestor( + of: find.text('Visible bubble'), + matching: find.byType(MarkdownWidget), + ); + final markdownRect = tester.getRect(markdownFinder); + final viewportRect = tester.getRect(outerListFinder); + expect(markdownRect.bottom, lessThan(viewportRect.bottom)); + + final start = tester.getCenter(find.text('Visible bubble')); + final end = Offset(start.dx, viewportRect.bottom - 6); + + final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: start); + await gesture.down(start); + await tester.pump(); + await gesture.moveTo(end); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 250)); + + expect(outerScrollController.offset, 0); + expect(selectionController.hasSelection, isTrue); + + await gesture.up(); + await tester.pump(); + }, + ); testWidgets('selection auto-scroll ignores pointer moves after unmount', ( tester, @@ -7792,15 +7837,15 @@ Intro }); testWidgets( - 'reverse dragging from following heading into a table keeps table selection rects visible', - (tester) async { - final selectionController = MarkdownSelectionController(); + 'reverse dragging from following heading into a table keeps table selection rects visible', + (tester) async { + final selectionController = MarkdownSelectionController(); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: MarkdownWidget( - data: r''' + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: MarkdownWidget( + data: r''' ## 4. Complex Tables Tables support varying alignments, complex cell contents, and inline styles. @@ -7814,69 +7859,73 @@ Tables support varying alignments, complex cell contents, and inline styles. ## 5. Media & Links ''', - selectionController: selectionController, + selectionController: selectionController, + ), ), ), - ), - ); - - final start = tester.getCenter(find.textContaining('Media & Links')) + - const Offset(0, 4); - final end = tester.getCenter(find.text('Feature')); - final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); - await gesture.addPointer(location: start); - await gesture.down(start); - await tester.pump(); - await gesture.moveTo(end); - await tester.pump(); - await gesture.up(); - await tester.pump(); + ); - expect(selectionController.hasSelection, isTrue); - expect( - selectionController.selectedPlainText, - contains('Description\tStatus'), - ); - expect( - selectionController.selectedPlainText, - contains('Code\tSyntax highlighting with re_highlight\t🚀 Built'), - ); + final start = + tester.getCenter(find.textContaining('Media & Links')) + + const Offset(0, 4); + final end = tester.getCenter(find.text('Feature')); + final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: start); + await gesture.down(start); + await tester.pump(); + await gesture.moveTo(end); + await tester.pump(); + await gesture.up(); + await tester.pump(); + + expect(selectionController.hasSelection, isTrue); + expect( + selectionController.selectedPlainText, + contains('Description\tStatus'), + ); + expect( + selectionController.selectedPlainText, + contains('Code\tSyntax highlighting with re_highlight\t🚀 Built'), + ); - final tableBlockFinder = find.byWidgetPredicate( - (widget) => - widget is SelectableMarkdownBlock && - widget.spec.plainText.contains('Feature\tDescription\tStatus'), - ); - expect(tableBlockFinder, findsOneWidget); + final tableBlockFinder = find.byWidgetPredicate( + (widget) => + widget is SelectableMarkdownBlock && + widget.spec.plainText.contains('Feature\tDescription\tStatus'), + ); + expect(tableBlockFinder, findsOneWidget); - final tableBlock = tester.widget(tableBlockFinder); - final selectionRange = selectionController.normalizedRange!; - final blockRange = DocumentRange( - start: DocumentPosition( - blockIndex: tableBlock.blockIndex, - path: const PathInBlock([0]), - textOffset: selectionRange.start.blockIndex == tableBlock.blockIndex - ? selectionRange.start.textOffset - : 0, - ), - end: DocumentPosition( - blockIndex: tableBlock.blockIndex, - path: const PathInBlock([0]), - textOffset: selectionRange.end.blockIndex == tableBlock.blockIndex - ? selectionRange.end.textOffset - : tableBlock.spec.plainText.length, - ), - ); + final tableBlock = tester.widget( + tableBlockFinder, + ); + final selectionRange = selectionController.normalizedRange!; + final blockRange = DocumentRange( + start: DocumentPosition( + blockIndex: tableBlock.blockIndex, + path: const PathInBlock([0]), + textOffset: selectionRange.start.blockIndex == tableBlock.blockIndex + ? selectionRange.start.textOffset + : 0, + ), + end: DocumentPosition( + blockIndex: tableBlock.blockIndex, + path: const PathInBlock([0]), + textOffset: selectionRange.end.blockIndex == tableBlock.blockIndex + ? selectionRange.end.textOffset + : tableBlock.spec.plainText.length, + ), + ); - final renderBox = tester.renderObject(tableBlockFinder); - final element = tester.element(tableBlockFinder); - final selectionRects = tableBlock.spec.selectionRectResolver!( - element, - renderBox.size, - blockRange, - ); - expect(selectionRects, isNotEmpty); - }); + final renderBox = tester.renderObject(tableBlockFinder); + final element = tester.element(tableBlockFinder); + final selectionRects = tableBlock.spec.selectionRectResolver!( + element, + renderBox.size, + blockRange, + ); + expect(selectionRects, isNotEmpty); + }, + ); testWidgets('MixinSelectionArea with MarkdownWidget settles', (tester) async { final selectionController = MarkdownSelectionController(); @@ -7908,8 +7957,9 @@ Tables support varying alignments, complex cell contents, and inline styles. expect(selectionController.selectedPlainText, 'Hello new world'); }); - testWidgets('MixinSelectionArea exposes public selectable widgets', - (tester) async { + testWidgets('MixinSelectionArea exposes public selectable widgets', ( + tester, + ) async { final selectionController = MarkdownSelectionController(); addTearDown(selectionController.dispose); @@ -7948,64 +7998,66 @@ Tables support varying alignments, complex cell contents, and inline styles. }); testWidgets( - 'MixinSelectionArea drags selection across selectable text widgets', - (tester) async { - final selectionController = MarkdownSelectionController(); - addTearDown(selectionController.dispose); + 'MixinSelectionArea drags selection across selectable text widgets', + (tester) async { + final selectionController = MarkdownSelectionController(); + addTearDown(selectionController.dispose); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: SizedBox( - width: 320, - child: MixinSelectionArea( - controller: selectionController, - child: const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MixinSelectableText('Alpha bravo'), - SizedBox(height: 8), - MixinSelectableText('Charlie delta'), - ], + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 320, + child: MixinSelectionArea( + controller: selectionController, + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MixinSelectableText('Alpha bravo'), + SizedBox(height: 8), + MixinSelectableText('Charlie delta'), + ], + ), ), ), ), ), - ), - ); - await tester.pump(); - - final firstBlockFinder = find.byWidgetPredicate( - (widget) => - widget is MixinSelectable && widget.plainText == 'Alpha bravo', - ); - final secondBlockFinder = find.byWidgetPredicate( - (widget) => - widget is MixinSelectable && widget.plainText == 'Charlie delta', - ); - expect(firstBlockFinder, findsOneWidget); - expect(secondBlockFinder, findsOneWidget); - - final start = - tester.getRect(firstBlockFinder).centerLeft + const Offset(1, 0); - final end = - tester.getRect(secondBlockFinder).centerRight - const Offset(1, 0); - final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); - await gesture.addPointer(location: start); - await gesture.down(start); - await tester.pump(); - await gesture.moveTo(end); - await tester.pump(); - await gesture.up(); - await tester.pump(); + ); + await tester.pump(); - expect(selectionController.hasSelection, isTrue); - expect(selectionController.selectedPlainText, contains('Alpha bravo')); - expect(selectionController.selectedPlainText, contains('Charlie delta')); - }); + final firstBlockFinder = find.byWidgetPredicate( + (widget) => + widget is MixinSelectable && widget.plainText == 'Alpha bravo', + ); + final secondBlockFinder = find.byWidgetPredicate( + (widget) => + widget is MixinSelectable && widget.plainText == 'Charlie delta', + ); + expect(firstBlockFinder, findsOneWidget); + expect(secondBlockFinder, findsOneWidget); + + final start = + tester.getRect(firstBlockFinder).centerLeft + const Offset(1, 0); + final end = + tester.getRect(secondBlockFinder).centerRight - const Offset(1, 0); + final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: start); + await gesture.down(start); + await tester.pump(); + await gesture.moveTo(end); + await tester.pump(); + await gesture.up(); + await tester.pump(); + + expect(selectionController.hasSelection, isTrue); + expect(selectionController.selectedPlainText, contains('Alpha bravo')); + expect(selectionController.selectedPlainText, contains('Charlie delta')); + }, + ); - testWidgets('MixinSelectableRow copies direct selectable text inline', - (tester) async { + testWidgets('MixinSelectableRow copies direct selectable text inline', ( + tester, + ) async { final selectionController = MarkdownSelectionController(); addTearDown(selectionController.dispose); @@ -8062,13 +8114,15 @@ Tables support varying alignments, complex cell contents, and inline styles. ); final origin = renderBox.localToGlobal(Offset.zero); final globalRects = rects.map((rect) => rect.shift(origin)); - final iconRect = - tester.getRect(find.byIcon(Icons.arrow_forward_rounded)).deflate(1); + final iconRect = tester + .getRect(find.byIcon(Icons.arrow_forward_rounded)) + .deflate(1); expect(globalRects.any((rect) => rect.overlaps(iconRect)), isFalse); }); - testWidgets('MixinSelectableRow drags across text split by an icon', - (tester) async { + testWidgets('MixinSelectableRow drags across text split by an icon', ( + tester, + ) async { final selectionController = MarkdownSelectionController(); addTearDown(selectionController.dispose); @@ -8111,50 +8165,47 @@ Tables support varying alignments, complex cell contents, and inline styles. expect(selectionController.selectedPlainText, 'left right'); }); - testWidgets('MixinSelectionArea selects across markdown and toolcall widgets', - (tester) async { - final selectionController = MarkdownSelectionController(); - addTearDown(selectionController.dispose); + testWidgets( + 'MixinSelectionArea selects across markdown and toolcall widgets', + (tester) async { + final selectionController = MarkdownSelectionController(); + addTearDown(selectionController.dispose); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: SizedBox( - width: 360, - child: MixinSelectionArea( - controller: selectionController, - child: const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MarkdownWidget( - data: 'First markdown', - useColumn: true, - ), - MixinSelectableText('Tool call output'), - MarkdownWidget( - data: 'Second markdown', - useColumn: true, - ), - ], + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 360, + child: MixinSelectionArea( + controller: selectionController, + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MarkdownWidget(data: 'First markdown', useColumn: true), + MixinSelectableText('Tool call output'), + MarkdownWidget(data: 'Second markdown', useColumn: true), + ], + ), ), ), ), ), - ), - ); - await tester.pump(); - await tester.pump(); + ); + await tester.pump(); + await tester.pump(); - selectionController.selectAll(); + selectionController.selectAll(); - expect( - selectionController.selectedPlainText, - 'First markdown\n\nTool call output\n\nSecond markdown', - ); - }); + expect( + selectionController.selectedPlainText, + 'First markdown\n\nTool call output\n\nSecond markdown', + ); + }, + ); - testWidgets('MixinSelectionArea drags across markdown and toolcall widgets', - (tester) async { + testWidgets('MixinSelectionArea drags across markdown and toolcall widgets', ( + tester, + ) async { final selectionController = MarkdownSelectionController(); addTearDown(selectionController.dispose); @@ -8168,15 +8219,9 @@ Tables support varying alignments, complex cell contents, and inline styles. child: const Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - MarkdownWidget( - data: 'First markdown', - useColumn: true, - ), + MarkdownWidget(data: 'First markdown', useColumn: true), MixinSelectableText('Tool call output'), - MarkdownWidget( - data: 'Second markdown', - useColumn: true, - ), + MarkdownWidget(data: 'Second markdown', useColumn: true), ], ), ), @@ -8187,9 +8232,11 @@ Tables support varying alignments, complex cell contents, and inline styles. await tester.pump(); await tester.pump(); - final start = tester.getRect(find.text('First markdown')).centerLeft + + final start = + tester.getRect(find.text('First markdown')).centerLeft + const Offset(1, 0); - final end = tester.getRect(find.text('Second markdown')).centerRight - + final end = + tester.getRect(find.text('Second markdown')).centerRight - const Offset(1, 0); final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(location: start); @@ -8207,80 +8254,83 @@ Tables support varying alignments, complex cell contents, and inline styles. }); testWidgets( - 'MixinSelectionArea auto-scrolls nested scrollable markdown participants', - (tester) async { - final selectionController = MarkdownSelectionController(); - final nestedScrollController = ScrollController(); - addTearDown(selectionController.dispose); - addTearDown(nestedScrollController.dispose); - - final buffer = StringBuffer(); - for (var index = 0; index < 40; index++) { - if (index > 0) { - buffer.writeln(); - buffer.writeln(); + 'MixinSelectionArea auto-scrolls nested scrollable markdown participants', + (tester) async { + final selectionController = MarkdownSelectionController(); + final nestedScrollController = ScrollController(); + addTearDown(selectionController.dispose); + addTearDown(nestedScrollController.dispose); + + final buffer = StringBuffer(); + for (var index = 0; index < 40; index++) { + if (index > 0) { + buffer.writeln(); + buffer.writeln(); + } + buffer.write('Nested paragraph $index'); } - buffer.write('Nested paragraph $index'); - } - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: SizedBox( - width: 360, - child: MixinSelectionArea( - controller: selectionController, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const MixinSelectableText('Before nested markdown'), - SizedBox( - height: 100, - child: MarkdownWidget( - data: buffer.toString(), - scrollController: nestedScrollController, + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 360, + child: MixinSelectionArea( + controller: selectionController, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const MixinSelectableText('Before nested markdown'), + SizedBox( + height: 100, + child: MarkdownWidget( + data: buffer.toString(), + scrollController: nestedScrollController, + ), ), - ), - const MixinSelectableText('After nested markdown'), - ], + const MixinSelectableText('After nested markdown'), + ], + ), ), ), ), ), - ), - ); - await tester.pump(); - - final nestedListFinder = find.byWidgetPredicate( - (widget) => - widget is ListView && widget.controller == nestedScrollController, - ); - final start = - tester.getTopLeft(find.text('Nested paragraph 0')) + const Offset(2, 8); - final edgeTarget = - tester.getBottomLeft(nestedListFinder) - const Offset(-4, 6); - - final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); - await gesture.addPointer(location: start); - await gesture.down(start); - await tester.pump(); - await gesture.moveTo(edgeTarget); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 250)); + ); + await tester.pump(); - expect(nestedScrollController.offset, greaterThan(0)); - expect(selectionController.hasSelection, isTrue); - expect( - selectionController.selectedPlainText, - contains('Nested paragraph 0'), - ); + final nestedListFinder = find.byWidgetPredicate( + (widget) => + widget is ListView && widget.controller == nestedScrollController, + ); + final start = + tester.getTopLeft(find.text('Nested paragraph 0')) + + const Offset(2, 8); + final edgeTarget = + tester.getBottomLeft(nestedListFinder) - const Offset(-4, 6); + + final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: start); + await gesture.down(start); + await tester.pump(); + await gesture.moveTo(edgeTarget); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 250)); + + expect(nestedScrollController.offset, greaterThan(0)); + expect(selectionController.hasSelection, isTrue); + expect( + selectionController.selectedPlainText, + contains('Nested paragraph 0'), + ); - await gesture.up(); - await tester.pump(); - }); + await gesture.up(); + await tester.pump(); + }, + ); - testWidgets('footnote backreference markers are not rendered', - (tester) async { + testWidgets('footnote backreference markers are not rendered', ( + tester, + ) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( diff --git a/packages/mixin_markdown_widget/test/selection_geometry_snapshot_test.dart b/packages/mixin_markdown_widget/test/selection_geometry_snapshot_test.dart index 1a0ad9d9..2d5043bc 100644 --- a/packages/mixin_markdown_widget/test/selection_geometry_snapshot_test.dart +++ b/packages/mixin_markdown_widget/test/selection_geometry_snapshot_test.dart @@ -10,7 +10,7 @@ // // and paste the printed literals back into [_cases]. -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mixin_markdown_widget/mixin_markdown_widget.dart'; import 'package:mixin_markdown_widget/src/render/selectable_block.dart'; @@ -24,11 +24,7 @@ const SelectableBlockSelectionPaintOrder _above = SelectableBlockSelectionPaintOrder.aboveChild; class _BlockPaint { - const _BlockPaint({ - required this.order, - required this.rects, - this.clip, - }); + const _BlockPaint({required this.order, required this.rects, this.clip}); final SelectableBlockSelectionPaintOrder order; final Rect? clip; @@ -198,9 +194,7 @@ const List<_Case> _cases = <_Case>[ expected: { 0: _BlockPaint( order: _behind, - rects: [ - Rect.fromLTRB(29.13, -0.45, 148.73, 20.55), - ], + rects: [Rect.fromLTRB(29.13, -0.45, 148.73, 20.55)], ), 1: _BlockPaint( order: _behind, @@ -327,10 +321,7 @@ Future _pump( child: SizedBox( width: 360, height: 600, - child: MarkdownWidget( - data: data, - selectionController: controller, - ), + child: MarkdownWidget(data: data, selectionController: controller), ), ), ), @@ -353,8 +344,10 @@ DocumentPosition _position( int blockIndex, int displayedOffset, ) { - final structure = - _blockState(tester, blockIndex).widget.spec.selectionStructure; + final structure = _blockState( + tester, + blockIndex, + ).widget.spec.selectionStructure; return structure?.positionForDisplayedOffset( blockIndex: blockIndex, displayedOffset: displayedOffset, @@ -419,14 +412,16 @@ String _rectLiteral(Rect rect) { } void _expectRect(Rect actual, Rect expected, String reason) { - final matches = (actual.left - expected.left).abs() <= _tolerance && + final matches = + (actual.left - expected.left).abs() <= _tolerance && (actual.top - expected.top).abs() <= _tolerance && (actual.right - expected.right).abs() <= _tolerance && (actual.bottom - expected.bottom).abs() <= _tolerance; expect( matches, isTrue, - reason: '$reason: expected ${_rectLiteral(expected)}, ' + reason: + '$reason: expected ${_rectLiteral(expected)}, ' 'got ${_rectLiteral(actual)}', ); } @@ -513,7 +508,8 @@ void main() { expect( paint.rects.length, expected.rects.length, - reason: '$reason rect count: ' + reason: + '$reason rect count: ' '${paint.rects.map(_rectLiteral).join(', ')}', ); for (var i = 0; i < expected.rects.length; i++) { @@ -524,27 +520,28 @@ void main() { } testWidgets( - 'horizontally scrolled code block paints selection in the foreground', - (tester) async { - final controller = await _pump( - tester, - '```dart\nfinal value = computeSomethingLongEnoughToScroll(alpha);\n```', - ); - controller.setSelection( - DocumentSelection( - base: _position(tester, 0, 0), - extent: _position(tester, 0, 10), - ), - ); - await tester.pump(); + 'horizontally scrolled code block paints selection in the foreground', + (tester) async { + final controller = await _pump( + tester, + '```dart\nfinal value = computeSomethingLongEnoughToScroll(alpha);\n```', + ); + controller.setSelection( + DocumentSelection( + base: _position(tester, 0, 0), + extent: _position(tester, 0, 10), + ), + ); + await tester.pump(); - final paint = find.descendant( - of: find.byType(SelectableMarkdownBlock), - matching: find.byWidgetPredicate( - (widget) => widget is CustomPaint && widget.foregroundPainter != null, - ), - ); - expect(paint, findsOneWidget); - expect(tester.widget(paint).painter, isNull); - }); + final paint = find.descendant( + of: find.byType(SelectableMarkdownBlock), + matching: find.byWidgetPredicate( + (widget) => widget is CustomPaint && widget.foregroundPainter != null, + ), + ); + expect(paint, findsOneWidget); + expect(tester.widget(paint).painter, isNull); + }, + ); } diff --git a/packages/mixin_markdown_widget/test/selection_gesture_edge_test.dart b/packages/mixin_markdown_widget/test/selection_gesture_edge_test.dart index a21abe4f..9bd97c6d 100644 --- a/packages/mixin_markdown_widget/test/selection_gesture_edge_test.dart +++ b/packages/mixin_markdown_widget/test/selection_gesture_edge_test.dart @@ -5,7 +5,7 @@ // `block[path]@displayedOffset`. import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -131,12 +131,12 @@ String? Function() _captureClipboard() { String? copiedText; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(SystemChannels.platform, (methodCall) async { - if (methodCall.method == 'Clipboard.setData') { - final arguments = methodCall.arguments as Map; - copiedText = arguments['text'] as String?; - } - return null; - }); + if (methodCall.method == 'Clipboard.setData') { + final arguments = methodCall.arguments as Map; + copiedText = arguments['text'] as String?; + } + return null; + }); addTearDown(() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(SystemChannels.platform, null); @@ -192,10 +192,7 @@ String _describe(MarkdownSelectionController controller) { '${_describePosition(selection.extent)} | "$text"'; } -void _expectSelection( - MarkdownSelectionController controller, - String expected, -) { +void _expectSelection(MarkdownSelectionController controller, String expected) { expect(_describe(controller), expected); } @@ -208,12 +205,12 @@ void main() { quote: [ '0[0]@0 -> 0[0]@8 | "First pa"', '0[0]@0 -> 0[0]@8 | "First pa"', - '0[0]@0 -> 0[1]@25 | "First paragraph\\n\\nSecond p"' + '0[0]@0 -> 0[1]@25 | "First paragraph\\n\\nSecond p"', ], list: [ '0[0, 1]@2 -> 0[0, 1]@7 | "First"', '0[0, 1]@2 -> 0[0, 1]@7 | "First"', - '0[0, 1]@2 -> 0[0, 2]@20 | "First item\\n Secon"' + '0[0, 1]@2 -> 0[0, 2]@20 | "First item\\n Secon"', ], }.entries) { final label = entry.key == quote ? 'quote' : 'list'; @@ -310,24 +307,18 @@ void main() { final block = _blockRect(tester, 0); final start = _charCenter(tester, 'The quick', 0); // The second rendered line spans y = 21..42 inside the block. - await _drag( - tester, - start, - () => Offset(block.right - 2, block.top + 31), + await _drag(tester, start, () => Offset(block.right - 2, block.top + 31)); + _expectSelection( + controller, + '0[0]@1 -> 0[0]@40 | "he quick brown fox jumps over the lazy "', ); - _expectSelection(controller, - '0[0]@1 -> 0[0]@40 | "he quick brown fox jumps over the lazy "'); }); testWidgets('short list item', (tester) async { final controller = await _pump(tester, '- short\n- other item'); final block = _blockRect(tester, 0); final start = _charCenter(tester, 'short', 0); - await _drag( - tester, - start, - () => Offset(block.right - 2, start.dy), - ); + await _drag(tester, start, () => Offset(block.right - 2, start.dy)); _expectSelection(controller, '0[0, 1]@3 -> 0[1, 0]@7 | "hort"'); }); @@ -338,18 +329,17 @@ void main() { ); final start = _charCenter(tester, 'Name', 0); final cell = _textRect(tester, 'ab'); - await _drag( - tester, - start, - () => Offset(cell.right + 8, cell.center.dy), - ); + await _drag(tester, start, () => Offset(cell.right + 8, cell.center.dy)); _expectSelection( - controller, '0[0, 0]@1 -> 0[1, 1]@13 | "ame\tValue\\nab"'); + controller, + '0[0, 0]@1 -> 0[1, 1]@13 | "ame\tValue\\nab"', + ); }); }); group('drag across top-level blocks', () { - const data = 'Intro paragraph\n\n' + const data = + 'Intro paragraph\n\n' '- one\n- two three\n\n' '> quoted text\n\n' '| alpha | beta |\n| - | - |\n| gamma | delta |'; @@ -361,8 +351,10 @@ void main() { _charCenter(tester, 'Intro', 1), () => _charCenter(tester, 'two three', 5), ); - _expectSelection(controller, - '0[0]@2 -> 1[1, 1]@14 | "tro paragraph\\n\\n- one\\n- two th"'); + _expectSelection( + controller, + '0[0]@2 -> 1[1, 1]@14 | "tro paragraph\\n\\n- one\\n- two th"', + ); }); testWidgets('from a quote into a table cell', (tester) async { @@ -372,21 +364,23 @@ void main() { _charCenter(tester, 'quoted', 2), () => _textRect(tester, 'delta').center, ); - _expectSelection(controller, - '2[0]@3 -> 3[1, 1]@20 | "ted text\\n\\nalpha\tbeta\\ngamma\tdel"'); + _expectSelection( + controller, + '2[0]@3 -> 3[1, 1]@20 | "ted text\\n\\nalpha\tbeta\\ngamma\tdel"', + ); }); - testWidgets('backwards from a table cell into a list item', ( - tester, - ) async { + testWidgets('backwards from a table cell into a list item', (tester) async { final controller = await _pump(tester, data); await _drag( tester, _textRect(tester, 'gamma').center, () => _charCenter(tester, 'one', 1), ); - _expectSelection(controller, - '3[1, 0]@14 -> 1[0, 1]@4 | "e\\n- two three\\n\\nquoted text\\n\\nalpha\tbeta\\ngam"'); + _expectSelection( + controller, + '3[1, 0]@14 -> 1[0, 1]@4 | "e\\n- two three\\n\\nquoted text\\n\\nalpha\tbeta\\ngam"', + ); }); testWidgets('ending in blank space below the document', (tester) async { @@ -396,8 +390,10 @@ void main() { _charCenter(tester, 'Intro', 0), () => const Offset(120, 560), ); - _expectSelection(controller, - '0[0]@1 -> 3[1, 1]@22 | "ntro paragraph\\n\\n- one\\n- two three\\n\\nquoted text\\n\\nalpha\tbeta\\ngamma\tdelta"'); + _expectSelection( + controller, + '0[0]@1 -> 3[1, 1]@22 | "ntro paragraph\\n\\n- one\\n- two three\\n\\nquoted text\\n\\nalpha\tbeta\\ngamma\tdelta"', + ); }); }); @@ -416,23 +412,23 @@ void main() { final expected = { 'nested list': ( '0[0, 2, 0, 1]@20 -> 0[0, 2, 0, 1]@23 | "pie"', - '0[0, 2, 0, 0]@10 -> 0[0, 2, 0, 1]@23 | " - Apple pie"' + '0[0, 2, 0, 0]@10 -> 0[0, 2, 0, 1]@23 | " - Apple pie"', ), 'deep nested list': ( '0[0, 2, 0, 2, 0, 1]@36 -> 0[0, 2, 0, 2, 0, 1]@41 | "Green"', - '0[0, 2, 0, 2, 0, 0]@30 -> 0[0, 2, 0, 2, 0, 1]@47 | " - Green apple"' + '0[0, 2, 0, 2, 0, 0]@30 -> 0[0, 2, 0, 2, 0, 1]@47 | " - Green apple"', ), 'nested quote': ( '0[1, 0]@13 -> 0[1, 0]@18 | "quote"', - '0[1, 0]@7 -> 0[1, 0]@25 | "inner quote words "' + '0[1, 0]@7 -> 0[1, 0]@25 | "inner quote words "', ), 'quote heading': ( '0[0]@0 -> 0[0]@5 | "Title"', - '0[0]@0 -> 0[0]@5 | "Title"' + '0[0]@0 -> 0[0]@5 | "Title"', ), 'table cell': ( '0[1, 0]@15 -> 0[1, 0]@18 | "one"', - '0[1, 0]@11 -> 0[1, 0]@18 | "row one"' + '0[1, 0]@11 -> 0[1, 0]@18 | "row one"', ), }; @@ -455,7 +451,8 @@ void main() { group('double click on the right half of the last character of a leaf', () { // The end offset of a leaf is also the start of the next one; the click // must select the word under the pointer, not the next leaf's word. - const sparseTable = '| Alpha | | Charlie |\n' + const sparseTable = + '| Alpha | | Charlie |\n' '| ----- | ----- | ------- |\n' '| | Bravo | z |'; @@ -495,12 +492,7 @@ void main() { 4, '0[0]@0 -> 0[0]@5 | "First"', ), - 'list item': ( - '- one\n- two', - 'one', - 2, - '0[0, 1]@2 -> 0[0, 1]@5 | "one"', - ), + 'list item': ('- one\n- two', 'one', 2, '0[0, 1]@2 -> 0[0, 1]@5 | "one"'), 'list item before a nested list': ( '- one\n - two', 'one', @@ -518,11 +510,7 @@ void main() { final (data, text, charIndex, expected) = entry.value; testWidgets(entry.key, (tester) async { final controller = await _pump(tester, data); - await _click( - tester, - _charRightHalf(tester, text, charIndex), - count: 2, - ); + await _click(tester, _charRightHalf(tester, text, charIndex), count: 2); _expectSelection(controller, expected); }); } @@ -532,8 +520,11 @@ void main() { ) async { final controller = await _pump(tester, sparseTable); final charlie = _textRect(tester, 'Charlie'); - await _click(tester, Offset(charlie.left + 1, charlie.center.dy), - count: 2); + await _click( + tester, + Offset(charlie.left + 1, charlie.center.dy), + count: 2, + ); _expectSelection(controller, '0[0, 2]@6 -> 0[0, 2]@13 | "Charlie"'); }); }); @@ -560,38 +551,44 @@ void main() { }); testWidgets( - 'copying a drag selection that spans a block scrolled out of a ListView', - (tester) async { - final copiedText = _captureClipboard(); - final paragraphs = List.generate(30, (index) => 'Paragraph $index'); - final expectedCopiedText = [ - paragraphs.first.substring(3), - ...paragraphs.sublist(1, 24), - paragraphs[24].substring(0, 7), - ].join('\n\n'); - final scrollController = ScrollController(); - addTearDown(scrollController.dispose); - final controller = await _pump( - tester, - paragraphs.join('\n\n'), - height: 240, - scrollController: scrollController, - ); + 'copying a drag selection that spans a block scrolled out of a ListView', + (tester) async { + final copiedText = _captureClipboard(); + final paragraphs = List.generate( + 30, + (index) => 'Paragraph $index', + ); + final expectedCopiedText = [ + paragraphs.first.substring(3), + ...paragraphs.sublist(1, 24), + paragraphs[24].substring(0, 7), + ].join('\n\n'); + final scrollController = ScrollController(); + addTearDown(scrollController.dispose); + final controller = await _pump( + tester, + paragraphs.join('\n\n'), + height: 240, + scrollController: scrollController, + ); - await _drag( - tester, - _charCenter(tester, 'Paragraph 0', 2), - () => _charCenter(tester, 'Paragraph 24', 6), - beforeEnd: () async { - scrollController.jumpTo(scrollController.position.maxScrollExtent); - await tester.pump(); - expect(_richTextContaining('Paragraph 0'), findsNothing); - }, - ); - _expectSelection(controller, - '0[0]@3 -> 24[0]@7 | "${expectedCopiedText.replaceAll('\n', r'\n')}"'); - - await _copy(tester); - expect(copiedText(), expectedCopiedText); - }); + await _drag( + tester, + _charCenter(tester, 'Paragraph 0', 2), + () => _charCenter(tester, 'Paragraph 24', 6), + beforeEnd: () async { + scrollController.jumpTo(scrollController.position.maxScrollExtent); + await tester.pump(); + expect(_richTextContaining('Paragraph 0'), findsNothing); + }, + ); + _expectSelection( + controller, + '0[0]@3 -> 24[0]@7 | "${expectedCopiedText.replaceAll('\n', r'\n')}"', + ); + + await _copy(tester); + expect(copiedText(), expectedCopiedText); + }, + ); } diff --git a/packages/ogg_opus_player/example/lib/main.dart b/packages/ogg_opus_player/example/lib/main.dart index 713ec361..2dc0ff09 100644 --- a/packages/ogg_opus_player/example/lib/main.dart +++ b/packages/ogg_opus_player/example/lib/main.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:audio_session/audio_session.dart'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:flutter/services.dart'; import 'package:ogg_opus_player/ogg_opus_player.dart'; import 'package:path/path.dart' as p; diff --git a/packages/ogg_opus_player/example/pubspec.yaml b/packages/ogg_opus_player/example/pubspec.yaml index 313cfe22..e9a33ae1 100644 --- a/packages/ogg_opus_player/example/pubspec.yaml +++ b/packages/ogg_opus_player/example/pubspec.yaml @@ -28,6 +28,7 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: + material_ui: ^1.11.0 flutter: sdk: flutter diff --git a/packages/ogg_opus_player/example/windows/flutter/generated_plugins.cmake b/packages/ogg_opus_player/example/windows/flutter/generated_plugins.cmake index 8c12e9e6..cd4b3a32 100644 --- a/packages/ogg_opus_player/example/windows/flutter/generated_plugins.cmake +++ b/packages/ogg_opus_player/example/windows/flutter/generated_plugins.cmake @@ -8,8 +8,8 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST jni - ogg_opus_player system_clock + ogg_opus_player ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/packages/pasteboard/example/lib/main.dart b/packages/pasteboard/example/lib/main.dart index 40ed3990..0ef25eef 100644 --- a/packages/pasteboard/example/lib/main.dart +++ b/packages/pasteboard/example/lib/main.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:pasteboard/pasteboard.dart'; void main() { diff --git a/packages/pasteboard/example/pubspec.yaml b/packages/pasteboard/example/pubspec.yaml index b0222399..b68ff23a 100644 --- a/packages/pasteboard/example/pubspec.yaml +++ b/packages/pasteboard/example/pubspec.yaml @@ -16,6 +16,7 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: + material_ui: ^1.11.0 flutter: sdk: flutter diff --git a/packages/ui_device/example/lib/main.dart b/packages/ui_device/example/lib/main.dart index bb2ea4b4..ef16a538 100644 --- a/packages/ui_device/example/lib/main.dart +++ b/packages/ui_device/example/lib/main.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:ui_device/ui_device.dart' as ui_device; void main() { diff --git a/packages/ui_device/example/pubspec.yaml b/packages/ui_device/example/pubspec.yaml index ab209a20..183b1a98 100644 --- a/packages/ui_device/example/pubspec.yaml +++ b/packages/ui_device/example/pubspec.yaml @@ -30,6 +30,7 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: + material_ui: ^1.11.0 flutter: sdk: flutter diff --git a/packages/web_qrcode/example/lib/main.dart b/packages/web_qrcode/example/lib/main.dart index 5d22cecb..1beeeec9 100644 --- a/packages/web_qrcode/example/lib/main.dart +++ b/packages/web_qrcode/example/lib/main.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:web_qrcode/web_qrcode.dart'; void main() { diff --git a/packages/web_qrcode/example/pubspec.yaml b/packages/web_qrcode/example/pubspec.yaml index 0a407c57..4d503d7c 100644 --- a/packages/web_qrcode/example/pubspec.yaml +++ b/packages/web_qrcode/example/pubspec.yaml @@ -16,6 +16,7 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: + material_ui: ^1.11.0 flutter: sdk: flutter diff --git a/packages/web_qrcode/lib/src/html5_qrcode.dart b/packages/web_qrcode/lib/src/html5_qrcode.dart index a00d62eb..4f0380f5 100644 --- a/packages/web_qrcode/lib/src/html5_qrcode.dart +++ b/packages/web_qrcode/lib/src/html5_qrcode.dart @@ -4,7 +4,7 @@ library; import 'dart:async'; import 'dart:js'; -import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:js/js.dart'; @JS() diff --git a/packages/web_qrcode/lib/web_qrcode.dart b/packages/web_qrcode/lib/web_qrcode.dart index 307dfc5c..e75a03a8 100644 --- a/packages/web_qrcode/lib/web_qrcode.dart +++ b/packages/web_qrcode/lib/web_qrcode.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:js_util'; -import 'package:flutter/cupertino.dart'; +import 'package:flutter/widgets.dart'; import 'src/html5_qrcode.dart'; diff --git a/packages/win_toast/example/lib/main.dart b/packages/win_toast/example/lib/main.dart index 52ff0f00..8315f23d 100644 --- a/packages/win_toast/example/lib/main.dart +++ b/packages/win_toast/example/lib/main.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:mixin_logger/mixin_logger.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; diff --git a/packages/win_toast/example/pubspec.yaml b/packages/win_toast/example/pubspec.yaml index 84661689..e65ea99f 100644 --- a/packages/win_toast/example/pubspec.yaml +++ b/packages/win_toast/example/pubspec.yaml @@ -8,6 +8,7 @@ environment: sdk: ^3.6.0 dependencies: + material_ui: ^1.11.0 flutter: sdk: flutter diff --git a/packages/win_toast/lib/win_toast.dart b/packages/win_toast/lib/win_toast.dart index 3b81912b..c9bbd8d3 100644 --- a/packages/win_toast/lib/win_toast.dart +++ b/packages/win_toast/lib/win_toast.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:win_toast/src/templates.dart'; diff --git a/packages/word_segmenter/example/lib/main.dart b/packages/word_segmenter/example/lib/main.dart index 27218353..9a558a09 100644 --- a/packages/word_segmenter/example/lib/main.dart +++ b/packages/word_segmenter/example/lib/main.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:word_segmenter/word_segmenter.dart'; void main() => runApp(const ExampleApp()); diff --git a/packages/word_segmenter/example/pubspec.yaml b/packages/word_segmenter/example/pubspec.yaml index 58cc8f11..33c1da3e 100644 --- a/packages/word_segmenter/example/pubspec.yaml +++ b/packages/word_segmenter/example/pubspec.yaml @@ -9,6 +9,7 @@ environment: sdk: ^3.6.0 dependencies: + material_ui: ^1.11.0 flutter: sdk: flutter word_segmenter: diff --git a/pubspec.lock b/pubspec.lock index 7009135a..0fb5de88 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d" + sha256: "1c5591cc6f252d5f440d96ab87c03e43504be552a3fa49d7cb7563f04c51fb0a" url: "https://pub.dev" source: hosted - version: "93.0.0" + version: "111.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b + sha256: "6c39bd5486a310e56217bed7cb1691377959f31be4227e41750107db7e6dd55c" url: "https://pub.dev" source: hosted - version: "10.0.1" + version: "14.7.0" ansi_styles: dependency: transitive description: @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: cli_launcher - sha256: d2dd1baf7ae4ff1550e731dcb8fe669a5c652903e833f75359c178222c44214e + sha256: ff37bcc6d5fa25ab7ddcf255a60900b28e4e1712d34495e5f7952e0982933913 url: "https://pub.dev" source: hosted - version: "0.3.4" + version: "0.3.4+1" cli_util: dependency: transitive description: @@ -125,10 +125,10 @@ packages: dependency: transitive description: name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + sha256: e51d50bca3217c9a9fa2b41a30e4a38971133f5f9ec7a3d57bae095007f1d28e url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" code_assets: dependency: transitive description: @@ -249,14 +249,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + cupertino_ui: + dependency: transitive + description: + name: cupertino_ui + sha256: "369e39a1c5c030a6212589c3da1809920b1c2cfade7b02aade7695bfa4b88c3a" + url: "https://pub.dev" + source: hosted + version: "1.1.6" dart_style: dependency: transitive description: name: dart_style - sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2" + sha256: a0e4f65339b31eb6a9e6a12e14b4682a48d46fbc54a82f4a702396fbdbac63a7 url: "https://pub.dev" source: hosted - version: "3.1.7" + version: "3.1.14" dbus: dependency: transitive description: @@ -323,6 +331,11 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_math_fork: dependency: transitive description: @@ -451,10 +464,10 @@ packages: dependency: transitive description: name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" url: "https://pub.dev" source: hosted - version: "0.20.2" + version: "0.20.3" io: dependency: transitive description: @@ -535,6 +548,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + listen: + dependency: transitive + description: + name: listen + sha256: "47501a08016a43fcad79252439d723f50f14f88fa7bfd8a177e0a417e5c9e1f2" + url: "https://pub.dev" + source: hosted + version: "1.0.1" logging: dependency: transitive description: @@ -567,14 +588,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.0" + material_ui: + dependency: transitive + description: + name: material_ui + sha256: "474c135fd46b56ff1d35ca982155e8863d28fbc6464d1192acf4ee53a738680e" + url: "https://pub.dev" + source: hosted + version: "1.11.0" melos: dependency: "direct dev" description: name: melos - sha256: "140e51d7990b5134663e64b39e19d3b64ead16382e69127c205942c91e752b16" + sha256: d0d9f8b8d566daf9b7a9bccd0b151d640c2d4b9c552b6fabf354328b5ad97080 url: "https://pub.dev" source: hosted - version: "8.9.0" + version: "8.10.1" meta: dependency: transitive description: @@ -859,10 +888,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + sha256: "9ac81c01ffd6a9cf9b2713288834fbaaf6aebde05ffdf02d380e68b8e06f27d8" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.7.0" quiver: dependency: transitive description: @@ -1072,10 +1101,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + sha256: "277654b3034d17ac6f9f1cb5595db011b1d5d41e8806866db28e0abaa101c490" url: "https://pub.dev" source: hosted - version: "1.12.1" + version: "1.12.2" stream_channel: dependency: transitive description: @@ -1128,26 +1157,26 @@ packages: dependency: transitive description: name: test - sha256: ca578dc12bb8b2f40b67b7d3bd2fac4f31c01a6ff7130a14e2597b919934507f + sha256: de5d145b0afff7921e5e788a880f52d7e5f3ae24068a202f6fd3b58e4ba26323 url: "https://pub.dev" source: hosted - version: "1.31.1" + version: "1.32.0" test_api: dependency: transitive description: name: test_api - sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" + sha256: "0a10344e901e5b2e63819567951cb6a06673ed6b84f40462188ff5a0c41f371f" url: "https://pub.dev" source: hosted - version: "0.7.12" + version: "0.7.14" test_core: dependency: transitive description: name: test_core - sha256: d2e98ec12998368dc59ddd47ab709f2cd55acd6b66dc7db764455a44082f4bc5 + sha256: "80f3fb49087454e07e7e07c67578cfdd156c8c3a5227d8b3f47c7b2d019c2e93" url: "https://pub.dev" source: hosted - version: "0.6.18" + version: "0.6.20" tuple: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 069aee5f..d218dc01 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,7 +43,7 @@ workspace: - packages/word_segmenter/example dev_dependencies: - melos: ^8.9.0 + melos: ^8.10.1 melos: scripts: