add new dependency
This commit is contained in:
parent
24d048b4ab
commit
4e6d3eb9b7
40 changed files with 14690 additions and 1 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
adaptive_number: ea9178fdd4d82ac45cf0ec966ac870dae661124f
|
adaptive_number: ea9178fdd4d82ac45cf0ec966ac870dae661124f
|
||||||
dots_indicator: 508f5883ac79bdbc10254092de3f28f571d261cd
|
dots_indicator: 508f5883ac79bdbc10254092de3f28f571d261cd
|
||||||
ed25519_edwards: 7353ba759ea9f4646cbf481c2ef949625c8ce4cf
|
ed25519_edwards: 7353ba759ea9f4646cbf481c2ef949625c8ce4cf
|
||||||
|
flutter_markdown_plus: dc1185c933fbf9dba559ef6c91586ff1503be3ee
|
||||||
flutter_sharing_intent: aa1672f547d6579585fa27df0b28ffa2a2544aaa
|
flutter_sharing_intent: aa1672f547d6579585fa27df0b28ffa2a2544aaa
|
||||||
hand_signature: 1beedb164d093643365b0832277c377353c7464f
|
hand_signature: 1beedb164d093643365b0832277c377353c7464f
|
||||||
hashlib: bc9c2f8dd7bbc72f47ccab0ce1111d40259c49bc
|
hashlib: bc9c2f8dd7bbc72f47ccab0ce1111d40259c49bc
|
||||||
|
|
|
||||||
|
|
@ -55,4 +55,8 @@ restart_app:
|
||||||
git: https://github.com/gabrimatic/restart_app
|
git: https://github.com/gabrimatic/restart_app
|
||||||
|
|
||||||
no_screenshot:
|
no_screenshot:
|
||||||
git: https://github.com/FlutterPlaza/no_screenshot.git
|
git: https://github.com/FlutterPlaza/no_screenshot.git
|
||||||
|
|
||||||
|
|
||||||
|
flutter_markdown_plus:
|
||||||
|
git: https://github.com/foresightmobile/flutter_markdown_plus.git
|
||||||
25
flutter_markdown_plus/LICENSE
Normal file
25
flutter_markdown_plus/LICENSE
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following
|
||||||
|
disclaimer in the documentation and/or other materials provided
|
||||||
|
with the distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived
|
||||||
|
from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
10
flutter_markdown_plus/lib/flutter_markdown_plus.dart
Normal file
10
flutter_markdown_plus/lib/flutter_markdown_plus.dart
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
/// A library to render markdown formatted text.
|
||||||
|
library flutter_markdown_plus;
|
||||||
|
|
||||||
|
export 'src/builder.dart';
|
||||||
|
export 'src/style_sheet.dart';
|
||||||
|
export 'src/widget.dart';
|
||||||
119
flutter_markdown_plus/lib/src/_functions_io.dart
Normal file
119
flutter_markdown_plus/lib/src/_functions_io.dart
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart' show CupertinoTheme;
|
||||||
|
import 'package:flutter/material.dart' show Theme;
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'style_sheet.dart';
|
||||||
|
import 'widget.dart';
|
||||||
|
|
||||||
|
/// Type for a function that creates image widgets.
|
||||||
|
typedef ImageBuilder = Widget Function(Uri uri, String? imageDirectory, double? width, double? height);
|
||||||
|
|
||||||
|
/// A default image builder handling http/https, resource, and file URLs.
|
||||||
|
// ignore: prefer_function_declarations_over_variables
|
||||||
|
final ImageBuilder kDefaultImageBuilder = (
|
||||||
|
Uri uri,
|
||||||
|
String? imageDirectory,
|
||||||
|
double? width,
|
||||||
|
double? height,
|
||||||
|
) {
|
||||||
|
if (uri.scheme == 'http' || uri.scheme == 'https') {
|
||||||
|
return Image.network(
|
||||||
|
uri.toString(),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
errorBuilder: kDefaultImageErrorWidgetBuilder,
|
||||||
|
);
|
||||||
|
} else if (uri.scheme == 'data') {
|
||||||
|
return _handleDataSchemeUri(uri, width, height);
|
||||||
|
} else if (uri.scheme == 'resource') {
|
||||||
|
return Image.asset(
|
||||||
|
uri.path,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
errorBuilder: kDefaultImageErrorWidgetBuilder,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final Uri fileUri = imageDirectory != null ? Uri.parse(imageDirectory + uri.toString()) : uri;
|
||||||
|
if (fileUri.scheme == 'http' || fileUri.scheme == 'https') {
|
||||||
|
return Image.network(
|
||||||
|
fileUri.toString(),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
errorBuilder: kDefaultImageErrorWidgetBuilder,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
return Image.file(
|
||||||
|
File.fromUri(fileUri),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
errorBuilder: kDefaultImageErrorWidgetBuilder,
|
||||||
|
);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
// Handle any invalid file URI's.
|
||||||
|
return Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return kDefaultImageErrorWidgetBuilder(context, error, stackTrace);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A default error widget builder for handling image errors.
|
||||||
|
// ignore: prefer_function_declarations_over_variables
|
||||||
|
final ImageErrorWidgetBuilder kDefaultImageErrorWidgetBuilder = (
|
||||||
|
BuildContext context,
|
||||||
|
Object error,
|
||||||
|
StackTrace? stackTrace,
|
||||||
|
) {
|
||||||
|
return const SizedBox();
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A default style sheet generator.
|
||||||
|
final MarkdownStyleSheet Function(BuildContext, MarkdownStyleSheetBaseTheme?)
|
||||||
|
// ignore: prefer_function_declarations_over_variables
|
||||||
|
kFallbackStyle = (
|
||||||
|
BuildContext context,
|
||||||
|
MarkdownStyleSheetBaseTheme? baseTheme,
|
||||||
|
) {
|
||||||
|
MarkdownStyleSheet result;
|
||||||
|
switch (baseTheme) {
|
||||||
|
case MarkdownStyleSheetBaseTheme.platform:
|
||||||
|
result = (Platform.isIOS || Platform.isMacOS)
|
||||||
|
? MarkdownStyleSheet.fromCupertinoTheme(CupertinoTheme.of(context))
|
||||||
|
: MarkdownStyleSheet.fromTheme(Theme.of(context));
|
||||||
|
case MarkdownStyleSheetBaseTheme.cupertino:
|
||||||
|
result = MarkdownStyleSheet.fromCupertinoTheme(CupertinoTheme.of(context));
|
||||||
|
case MarkdownStyleSheetBaseTheme.material:
|
||||||
|
// ignore: no_default_cases
|
||||||
|
default:
|
||||||
|
result = MarkdownStyleSheet.fromTheme(Theme.of(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.copyWith(
|
||||||
|
textScaler: MediaQuery.textScalerOf(context),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Widget _handleDataSchemeUri(Uri uri, final double? width, final double? height) {
|
||||||
|
final String mimeType = uri.data!.mimeType;
|
||||||
|
if (mimeType.startsWith('image/')) {
|
||||||
|
return Image.memory(
|
||||||
|
uri.data!.contentAsBytes(),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
errorBuilder: kDefaultImageErrorWidgetBuilder,
|
||||||
|
);
|
||||||
|
} else if (mimeType.startsWith('text/')) {
|
||||||
|
return Text(uri.data!.contentAsString());
|
||||||
|
}
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
124
flutter_markdown_plus/lib/src/_functions_web.dart
Normal file
124
flutter_markdown_plus/lib/src/_functions_web.dart
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:js_interop';
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart' show CupertinoTheme;
|
||||||
|
import 'package:flutter/material.dart' show Theme;
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
import 'style_sheet.dart';
|
||||||
|
import 'widget.dart';
|
||||||
|
|
||||||
|
/// Type for a function that creates image widgets.
|
||||||
|
typedef ImageBuilder = Widget Function(Uri uri, String? imageDirectory, double? width, double? height);
|
||||||
|
|
||||||
|
/// A default image builder handling http/https, resource, data, and file URLs.
|
||||||
|
// ignore: prefer_function_declarations_over_variables
|
||||||
|
final ImageBuilder kDefaultImageBuilder = (
|
||||||
|
Uri uri,
|
||||||
|
String? imageDirectory,
|
||||||
|
double? width,
|
||||||
|
double? height,
|
||||||
|
) {
|
||||||
|
if (uri.scheme == 'http' || uri.scheme == 'https') {
|
||||||
|
return Image.network(
|
||||||
|
uri.toString(),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
errorBuilder: kDefaultImageErrorWidgetBuilder,
|
||||||
|
);
|
||||||
|
} else if (uri.scheme == 'data') {
|
||||||
|
return _handleDataSchemeUri(uri, width, height);
|
||||||
|
} else if (uri.scheme == 'resource') {
|
||||||
|
return Image.asset(
|
||||||
|
uri.path,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
errorBuilder: kDefaultImageErrorWidgetBuilder,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final Uri fileUri;
|
||||||
|
|
||||||
|
if (imageDirectory != null) {
|
||||||
|
try {
|
||||||
|
fileUri = Uri.parse(p.join(imageDirectory, uri.toString()));
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
// Handle any invalid file URI's.
|
||||||
|
return Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return kDefaultImageErrorWidgetBuilder(context, error, stackTrace);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fileUri = uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileUri.scheme == 'http' || fileUri.scheme == 'https') {
|
||||||
|
return Image.network(
|
||||||
|
fileUri.toString(),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
errorBuilder: kDefaultImageErrorWidgetBuilder,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final String src = p.join(p.current, fileUri.toString());
|
||||||
|
return Image.network(
|
||||||
|
src,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
errorBuilder: kDefaultImageErrorWidgetBuilder,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A default error widget builder for handling image errors.
|
||||||
|
// ignore: prefer_function_declarations_over_variables
|
||||||
|
final ImageErrorWidgetBuilder kDefaultImageErrorWidgetBuilder = (
|
||||||
|
BuildContext context,
|
||||||
|
Object error,
|
||||||
|
StackTrace? stackTrace,
|
||||||
|
) {
|
||||||
|
return const SizedBox();
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A default style sheet generator.
|
||||||
|
final MarkdownStyleSheet Function(BuildContext, MarkdownStyleSheetBaseTheme?)
|
||||||
|
// ignore: prefer_function_declarations_over_variables
|
||||||
|
kFallbackStyle = (
|
||||||
|
BuildContext context,
|
||||||
|
MarkdownStyleSheetBaseTheme? baseTheme,
|
||||||
|
) {
|
||||||
|
final MarkdownStyleSheet result = switch (baseTheme) {
|
||||||
|
MarkdownStyleSheetBaseTheme.platform when _userAgent.toDart.contains('Mac OS X') =>
|
||||||
|
MarkdownStyleSheet.fromCupertinoTheme(CupertinoTheme.of(context)),
|
||||||
|
MarkdownStyleSheetBaseTheme.cupertino => MarkdownStyleSheet.fromCupertinoTheme(CupertinoTheme.of(context)),
|
||||||
|
_ => MarkdownStyleSheet.fromTheme(Theme.of(context)),
|
||||||
|
};
|
||||||
|
|
||||||
|
return result.copyWith(
|
||||||
|
textScaler: MediaQuery.textScalerOf(context),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Widget _handleDataSchemeUri(Uri uri, final double? width, final double? height) {
|
||||||
|
final String mimeType = uri.data!.mimeType;
|
||||||
|
if (mimeType.startsWith('image/')) {
|
||||||
|
return Image.memory(
|
||||||
|
uri.data!.contentAsBytes(),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
errorBuilder: kDefaultImageErrorWidgetBuilder,
|
||||||
|
);
|
||||||
|
} else if (mimeType.startsWith('text/')) {
|
||||||
|
return Text(uri.data!.contentAsString());
|
||||||
|
}
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
@JS('window.navigator.userAgent')
|
||||||
|
external JSString get _userAgent;
|
||||||
1033
flutter_markdown_plus/lib/src/builder.dart
Normal file
1033
flutter_markdown_plus/lib/src/builder.dart
Normal file
File diff suppressed because it is too large
Load diff
856
flutter_markdown_plus/lib/src/style_sheet.dart
Normal file
856
flutter_markdown_plus/lib/src/style_sheet.dart
Normal file
|
|
@ -0,0 +1,856 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Defines which [TextStyle] objects to use for which Markdown elements.
|
||||||
|
class MarkdownStyleSheet {
|
||||||
|
/// Creates an explicit mapping of [TextStyle] objects to Markdown elements.
|
||||||
|
MarkdownStyleSheet({
|
||||||
|
this.a,
|
||||||
|
this.p,
|
||||||
|
this.pPadding,
|
||||||
|
this.code,
|
||||||
|
this.h1,
|
||||||
|
this.h1Padding,
|
||||||
|
this.h2,
|
||||||
|
this.h2Padding,
|
||||||
|
this.h3,
|
||||||
|
this.h3Padding,
|
||||||
|
this.h4,
|
||||||
|
this.h4Padding,
|
||||||
|
this.h5,
|
||||||
|
this.h5Padding,
|
||||||
|
this.h6,
|
||||||
|
this.h6Padding,
|
||||||
|
this.em,
|
||||||
|
this.strong,
|
||||||
|
this.del,
|
||||||
|
this.blockquote,
|
||||||
|
this.img,
|
||||||
|
this.checkbox,
|
||||||
|
this.blockSpacing,
|
||||||
|
this.listIndent,
|
||||||
|
this.listBullet,
|
||||||
|
this.listBulletPadding,
|
||||||
|
this.tableHead,
|
||||||
|
this.tableBody,
|
||||||
|
this.tableHeadAlign,
|
||||||
|
this.tablePadding,
|
||||||
|
this.tableBorder,
|
||||||
|
this.tableColumnWidth,
|
||||||
|
this.tableScrollbarThumbVisibility,
|
||||||
|
this.tableCellsPadding,
|
||||||
|
this.tableCellsDecoration,
|
||||||
|
this.tableHeadCellsPadding,
|
||||||
|
this.tableHeadCellsDecoration,
|
||||||
|
this.tableVerticalAlignment = TableCellVerticalAlignment.middle,
|
||||||
|
this.blockquotePadding,
|
||||||
|
this.blockquoteDecoration,
|
||||||
|
this.codeblockPadding,
|
||||||
|
this.codeblockDecoration,
|
||||||
|
this.horizontalRuleDecoration,
|
||||||
|
this.textAlign = WrapAlignment.start,
|
||||||
|
this.h1Align = WrapAlignment.start,
|
||||||
|
this.h2Align = WrapAlignment.start,
|
||||||
|
this.h3Align = WrapAlignment.start,
|
||||||
|
this.h4Align = WrapAlignment.start,
|
||||||
|
this.h5Align = WrapAlignment.start,
|
||||||
|
this.h6Align = WrapAlignment.start,
|
||||||
|
this.unorderedListAlign = WrapAlignment.start,
|
||||||
|
this.orderedListAlign = WrapAlignment.start,
|
||||||
|
this.blockquoteAlign = WrapAlignment.start,
|
||||||
|
this.codeblockAlign = WrapAlignment.start,
|
||||||
|
this.superscriptFontFeatureTag,
|
||||||
|
@Deprecated('Use textScaler instead.') this.textScaleFactor,
|
||||||
|
TextScaler? textScaler,
|
||||||
|
}) : assert(
|
||||||
|
textScaler == null || textScaleFactor == null,
|
||||||
|
'textScaleFactor is deprecated and cannot be specified when textScaler is specified.',
|
||||||
|
),
|
||||||
|
textScaler = textScaler ??
|
||||||
|
// Internally, only textScaler is used, so convert the scale factor
|
||||||
|
// to a linear scaler.
|
||||||
|
(textScaleFactor == null ? null : TextScaler.linear(textScaleFactor)),
|
||||||
|
_styles = <String, TextStyle?>{
|
||||||
|
'a': a,
|
||||||
|
'p': p,
|
||||||
|
'li': p,
|
||||||
|
'code': code,
|
||||||
|
'pre': p,
|
||||||
|
'h1': h1,
|
||||||
|
'h2': h2,
|
||||||
|
'h3': h3,
|
||||||
|
'h4': h4,
|
||||||
|
'h5': h5,
|
||||||
|
'h6': h6,
|
||||||
|
'em': em,
|
||||||
|
'strong': strong,
|
||||||
|
'del': del,
|
||||||
|
'blockquote': blockquote,
|
||||||
|
'img': img,
|
||||||
|
'table': p,
|
||||||
|
'th': tableHead,
|
||||||
|
'tr': tableBody,
|
||||||
|
'td': tableBody,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Creates a [MarkdownStyleSheet] from the [TextStyle]s in the provided [ThemeData].
|
||||||
|
factory MarkdownStyleSheet.fromTheme(ThemeData theme) {
|
||||||
|
assert(theme.textTheme.bodyMedium?.fontSize != null);
|
||||||
|
return MarkdownStyleSheet(
|
||||||
|
a: const TextStyle(color: Colors.blue),
|
||||||
|
p: theme.textTheme.bodyMedium,
|
||||||
|
pPadding: EdgeInsets.zero,
|
||||||
|
code: theme.textTheme.bodyMedium!.copyWith(
|
||||||
|
backgroundColor: theme.cardTheme.color,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: theme.textTheme.bodyMedium!.fontSize! * 0.85,
|
||||||
|
),
|
||||||
|
h1: theme.textTheme.headlineSmall,
|
||||||
|
h1Padding: EdgeInsets.zero,
|
||||||
|
h2: theme.textTheme.titleLarge,
|
||||||
|
h2Padding: EdgeInsets.zero,
|
||||||
|
h3: theme.textTheme.titleMedium,
|
||||||
|
h3Padding: EdgeInsets.zero,
|
||||||
|
h4: theme.textTheme.bodyLarge,
|
||||||
|
h4Padding: EdgeInsets.zero,
|
||||||
|
h5: theme.textTheme.bodyLarge,
|
||||||
|
h5Padding: EdgeInsets.zero,
|
||||||
|
h6: theme.textTheme.bodyLarge,
|
||||||
|
h6Padding: EdgeInsets.zero,
|
||||||
|
em: const TextStyle(fontStyle: FontStyle.italic),
|
||||||
|
strong: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
del: const TextStyle(decoration: TextDecoration.lineThrough),
|
||||||
|
blockquote: theme.textTheme.bodyMedium,
|
||||||
|
img: theme.textTheme.bodyMedium,
|
||||||
|
checkbox: theme.textTheme.bodyMedium!.copyWith(
|
||||||
|
color: theme.primaryColor,
|
||||||
|
),
|
||||||
|
blockSpacing: 8.0,
|
||||||
|
listIndent: 24.0,
|
||||||
|
listBullet: theme.textTheme.bodyMedium,
|
||||||
|
listBulletPadding: const EdgeInsets.only(right: 4),
|
||||||
|
tableHead: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
tableBody: theme.textTheme.bodyMedium,
|
||||||
|
tableHeadAlign: TextAlign.center,
|
||||||
|
tablePadding: const EdgeInsets.only(bottom: 4.0),
|
||||||
|
tableBorder: TableBorder.all(
|
||||||
|
color: theme.dividerColor,
|
||||||
|
),
|
||||||
|
tableColumnWidth: const FlexColumnWidth(),
|
||||||
|
tableCellsPadding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||||
|
tableCellsDecoration: const BoxDecoration(),
|
||||||
|
blockquotePadding: const EdgeInsets.all(8.0),
|
||||||
|
blockquoteDecoration: BoxDecoration(
|
||||||
|
color: Colors.blue.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(2.0),
|
||||||
|
),
|
||||||
|
codeblockPadding: const EdgeInsets.all(8.0),
|
||||||
|
codeblockDecoration: BoxDecoration(
|
||||||
|
color: theme.cardTheme.color ?? theme.cardColor,
|
||||||
|
borderRadius: BorderRadius.circular(2.0),
|
||||||
|
),
|
||||||
|
horizontalRuleDecoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(
|
||||||
|
width: 5.0,
|
||||||
|
color: theme.dividerColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [MarkdownStyleSheet] from the [TextStyle]s in the provided [CupertinoThemeData].
|
||||||
|
factory MarkdownStyleSheet.fromCupertinoTheme(CupertinoThemeData theme) {
|
||||||
|
assert(theme.textTheme.textStyle.fontSize != null);
|
||||||
|
return MarkdownStyleSheet(
|
||||||
|
a: theme.textTheme.textStyle.copyWith(
|
||||||
|
color: theme.brightness == Brightness.dark ? CupertinoColors.link.darkColor : CupertinoColors.link.color,
|
||||||
|
),
|
||||||
|
p: theme.textTheme.textStyle,
|
||||||
|
pPadding: EdgeInsets.zero,
|
||||||
|
code: theme.textTheme.textStyle.copyWith(
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: theme.textTheme.textStyle.fontSize! * 0.85,
|
||||||
|
),
|
||||||
|
h1: theme.textTheme.textStyle.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: theme.textTheme.textStyle.fontSize! + 10,
|
||||||
|
),
|
||||||
|
h1Padding: EdgeInsets.zero,
|
||||||
|
h2: theme.textTheme.textStyle.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: theme.textTheme.textStyle.fontSize! + 8,
|
||||||
|
),
|
||||||
|
h2Padding: EdgeInsets.zero,
|
||||||
|
h3: theme.textTheme.textStyle.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: theme.textTheme.textStyle.fontSize! + 6,
|
||||||
|
),
|
||||||
|
h3Padding: EdgeInsets.zero,
|
||||||
|
h4: theme.textTheme.textStyle.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: theme.textTheme.textStyle.fontSize! + 4,
|
||||||
|
),
|
||||||
|
h4Padding: EdgeInsets.zero,
|
||||||
|
h5: theme.textTheme.textStyle.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: theme.textTheme.textStyle.fontSize! + 2,
|
||||||
|
),
|
||||||
|
h5Padding: EdgeInsets.zero,
|
||||||
|
h6: theme.textTheme.textStyle.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
h6Padding: EdgeInsets.zero,
|
||||||
|
em: theme.textTheme.textStyle.copyWith(
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
strong: theme.textTheme.textStyle.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
del: theme.textTheme.textStyle.copyWith(
|
||||||
|
decoration: TextDecoration.lineThrough,
|
||||||
|
),
|
||||||
|
blockquote: theme.textTheme.textStyle,
|
||||||
|
img: theme.textTheme.textStyle,
|
||||||
|
checkbox: theme.textTheme.textStyle.copyWith(
|
||||||
|
color: theme.primaryColor,
|
||||||
|
),
|
||||||
|
blockSpacing: 8,
|
||||||
|
listIndent: 24,
|
||||||
|
listBullet: theme.textTheme.textStyle,
|
||||||
|
listBulletPadding: const EdgeInsets.only(right: 4),
|
||||||
|
tableHead: theme.textTheme.textStyle.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
tableBody: theme.textTheme.textStyle,
|
||||||
|
tableHeadAlign: TextAlign.center,
|
||||||
|
tablePadding: const EdgeInsets.only(bottom: 8),
|
||||||
|
tableBorder: TableBorder.all(color: CupertinoColors.separator, width: 0),
|
||||||
|
tableColumnWidth: const FlexColumnWidth(),
|
||||||
|
tableCellsPadding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||||
|
tableCellsDecoration: BoxDecoration(
|
||||||
|
color: theme.brightness == Brightness.dark
|
||||||
|
? CupertinoColors.systemGrey6.darkColor
|
||||||
|
: CupertinoColors.systemGrey6.color,
|
||||||
|
),
|
||||||
|
blockquotePadding: const EdgeInsets.all(16),
|
||||||
|
blockquoteDecoration: BoxDecoration(
|
||||||
|
color: theme.brightness == Brightness.dark
|
||||||
|
? CupertinoColors.systemGrey6.darkColor
|
||||||
|
: CupertinoColors.systemGrey6.color,
|
||||||
|
border: Border(
|
||||||
|
left: BorderSide(
|
||||||
|
color: theme.brightness == Brightness.dark
|
||||||
|
? CupertinoColors.systemGrey4.darkColor
|
||||||
|
: CupertinoColors.systemGrey4.color,
|
||||||
|
width: 4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
codeblockPadding: const EdgeInsets.all(8),
|
||||||
|
codeblockDecoration: BoxDecoration(
|
||||||
|
color: theme.brightness == Brightness.dark
|
||||||
|
? CupertinoColors.systemGrey6.darkColor
|
||||||
|
: CupertinoColors.systemGrey6.color,
|
||||||
|
),
|
||||||
|
horizontalRuleDecoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(
|
||||||
|
color: theme.brightness == Brightness.dark
|
||||||
|
? CupertinoColors.systemGrey4.darkColor
|
||||||
|
: CupertinoColors.systemGrey4.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [MarkdownStyle] from the [TextStyle]s in the provided [ThemeData].
|
||||||
|
///
|
||||||
|
/// This constructor uses larger fonts for the headings than in
|
||||||
|
/// [MarkdownStyle.fromTheme].
|
||||||
|
factory MarkdownStyleSheet.largeFromTheme(ThemeData theme) {
|
||||||
|
return MarkdownStyleSheet(
|
||||||
|
a: const TextStyle(color: Colors.blue),
|
||||||
|
p: theme.textTheme.bodyMedium,
|
||||||
|
pPadding: EdgeInsets.zero,
|
||||||
|
code: theme.textTheme.bodyMedium!.copyWith(
|
||||||
|
backgroundColor: theme.cardTheme.color,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: theme.textTheme.bodyMedium!.fontSize! * 0.85,
|
||||||
|
),
|
||||||
|
h1: theme.textTheme.displayMedium,
|
||||||
|
h1Padding: EdgeInsets.zero,
|
||||||
|
h2: theme.textTheme.displaySmall,
|
||||||
|
h2Padding: EdgeInsets.zero,
|
||||||
|
h3: theme.textTheme.headlineMedium,
|
||||||
|
h3Padding: EdgeInsets.zero,
|
||||||
|
h4: theme.textTheme.headlineSmall,
|
||||||
|
h4Padding: EdgeInsets.zero,
|
||||||
|
h5: theme.textTheme.titleLarge,
|
||||||
|
h5Padding: EdgeInsets.zero,
|
||||||
|
h6: theme.textTheme.titleMedium,
|
||||||
|
h6Padding: EdgeInsets.zero,
|
||||||
|
em: const TextStyle(fontStyle: FontStyle.italic),
|
||||||
|
strong: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
del: const TextStyle(decoration: TextDecoration.lineThrough),
|
||||||
|
blockquote: theme.textTheme.bodyMedium,
|
||||||
|
img: theme.textTheme.bodyMedium,
|
||||||
|
checkbox: theme.textTheme.bodyMedium!.copyWith(
|
||||||
|
color: theme.primaryColor,
|
||||||
|
),
|
||||||
|
blockSpacing: 8.0,
|
||||||
|
listIndent: 24.0,
|
||||||
|
listBullet: theme.textTheme.bodyMedium,
|
||||||
|
listBulletPadding: const EdgeInsets.only(right: 4),
|
||||||
|
tableHead: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
tableBody: theme.textTheme.bodyMedium,
|
||||||
|
tableHeadAlign: TextAlign.center,
|
||||||
|
tablePadding: const EdgeInsets.only(bottom: 4.0),
|
||||||
|
tableBorder: TableBorder.all(
|
||||||
|
color: theme.dividerColor,
|
||||||
|
),
|
||||||
|
tableColumnWidth: const FlexColumnWidth(),
|
||||||
|
tableCellsPadding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||||
|
tableCellsDecoration: const BoxDecoration(),
|
||||||
|
blockquotePadding: const EdgeInsets.all(8.0),
|
||||||
|
blockquoteDecoration: BoxDecoration(
|
||||||
|
color: Colors.blue.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(2.0),
|
||||||
|
),
|
||||||
|
codeblockPadding: const EdgeInsets.all(8.0),
|
||||||
|
codeblockDecoration: BoxDecoration(
|
||||||
|
color: theme.cardTheme.color ?? theme.cardColor,
|
||||||
|
borderRadius: BorderRadius.circular(2.0),
|
||||||
|
),
|
||||||
|
horizontalRuleDecoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(
|
||||||
|
width: 5.0,
|
||||||
|
color: theme.dividerColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [MarkdownStyleSheet] based on the current style, with the
|
||||||
|
/// provided parameters overridden.
|
||||||
|
MarkdownStyleSheet copyWith({
|
||||||
|
TextStyle? a,
|
||||||
|
TextStyle? p,
|
||||||
|
EdgeInsets? pPadding,
|
||||||
|
TextStyle? code,
|
||||||
|
TextStyle? h1,
|
||||||
|
EdgeInsets? h1Padding,
|
||||||
|
TextStyle? h2,
|
||||||
|
EdgeInsets? h2Padding,
|
||||||
|
TextStyle? h3,
|
||||||
|
EdgeInsets? h3Padding,
|
||||||
|
TextStyle? h4,
|
||||||
|
EdgeInsets? h4Padding,
|
||||||
|
TextStyle? h5,
|
||||||
|
EdgeInsets? h5Padding,
|
||||||
|
TextStyle? h6,
|
||||||
|
EdgeInsets? h6Padding,
|
||||||
|
TextStyle? em,
|
||||||
|
TextStyle? strong,
|
||||||
|
TextStyle? del,
|
||||||
|
TextStyle? blockquote,
|
||||||
|
TextStyle? img,
|
||||||
|
TextStyle? checkbox,
|
||||||
|
double? blockSpacing,
|
||||||
|
double? listIndent,
|
||||||
|
TextStyle? listBullet,
|
||||||
|
EdgeInsets? listBulletPadding,
|
||||||
|
TextStyle? tableHead,
|
||||||
|
TextStyle? tableBody,
|
||||||
|
TextAlign? tableHeadAlign,
|
||||||
|
EdgeInsets? tablePadding,
|
||||||
|
TableBorder? tableBorder,
|
||||||
|
TableColumnWidth? tableColumnWidth,
|
||||||
|
bool? tableScrollbarThumbVisibility,
|
||||||
|
EdgeInsets? tableCellsPadding,
|
||||||
|
Decoration? tableCellsDecoration,
|
||||||
|
EdgeInsets? tableHeadCellsPadding,
|
||||||
|
Decoration? tableHeadCellsDecoration,
|
||||||
|
TableCellVerticalAlignment? tableVerticalAlignment,
|
||||||
|
EdgeInsets? blockquotePadding,
|
||||||
|
Decoration? blockquoteDecoration,
|
||||||
|
EdgeInsets? codeblockPadding,
|
||||||
|
Decoration? codeblockDecoration,
|
||||||
|
Decoration? horizontalRuleDecoration,
|
||||||
|
WrapAlignment? textAlign,
|
||||||
|
WrapAlignment? h1Align,
|
||||||
|
WrapAlignment? h2Align,
|
||||||
|
WrapAlignment? h3Align,
|
||||||
|
WrapAlignment? h4Align,
|
||||||
|
WrapAlignment? h5Align,
|
||||||
|
WrapAlignment? h6Align,
|
||||||
|
WrapAlignment? unorderedListAlign,
|
||||||
|
WrapAlignment? orderedListAlign,
|
||||||
|
WrapAlignment? blockquoteAlign,
|
||||||
|
WrapAlignment? codeblockAlign,
|
||||||
|
String? superscriptFontFeatureTag,
|
||||||
|
@Deprecated('Use textScaler instead.') double? textScaleFactor,
|
||||||
|
TextScaler? textScaler,
|
||||||
|
}) {
|
||||||
|
assert(
|
||||||
|
textScaler == null || textScaleFactor == null,
|
||||||
|
'textScaleFactor is deprecated and cannot be specified when textScaler is specified.',
|
||||||
|
);
|
||||||
|
// If either of textScaler or textScaleFactor is non-null, pass null for the
|
||||||
|
// other instead of the previous value, since only one is allowed.
|
||||||
|
final TextScaler? newTextScaler = textScaler ?? (textScaleFactor == null ? this.textScaler : null);
|
||||||
|
final double? nextTextScaleFactor = textScaleFactor ?? (textScaler == null ? this.textScaleFactor : null);
|
||||||
|
return MarkdownStyleSheet(
|
||||||
|
a: a ?? this.a,
|
||||||
|
p: p ?? this.p,
|
||||||
|
pPadding: pPadding ?? this.pPadding,
|
||||||
|
code: code ?? this.code,
|
||||||
|
h1: h1 ?? this.h1,
|
||||||
|
h1Padding: h1Padding ?? this.h1Padding,
|
||||||
|
h2: h2 ?? this.h2,
|
||||||
|
h2Padding: h2Padding ?? this.h2Padding,
|
||||||
|
h3: h3 ?? this.h3,
|
||||||
|
h3Padding: h3Padding ?? this.h3Padding,
|
||||||
|
h4: h4 ?? this.h4,
|
||||||
|
h4Padding: h4Padding ?? this.h4Padding,
|
||||||
|
h5: h5 ?? this.h5,
|
||||||
|
h5Padding: h5Padding ?? this.h5Padding,
|
||||||
|
h6: h6 ?? this.h6,
|
||||||
|
h6Padding: h6Padding ?? this.h6Padding,
|
||||||
|
em: em ?? this.em,
|
||||||
|
strong: strong ?? this.strong,
|
||||||
|
del: del ?? this.del,
|
||||||
|
blockquote: blockquote ?? this.blockquote,
|
||||||
|
img: img ?? this.img,
|
||||||
|
checkbox: checkbox ?? this.checkbox,
|
||||||
|
blockSpacing: blockSpacing ?? this.blockSpacing,
|
||||||
|
listIndent: listIndent ?? this.listIndent,
|
||||||
|
listBullet: listBullet ?? this.listBullet,
|
||||||
|
listBulletPadding: listBulletPadding ?? this.listBulletPadding,
|
||||||
|
tableHead: tableHead ?? this.tableHead,
|
||||||
|
tableBody: tableBody ?? this.tableBody,
|
||||||
|
tableHeadAlign: tableHeadAlign ?? this.tableHeadAlign,
|
||||||
|
tablePadding: tablePadding ?? this.tablePadding,
|
||||||
|
tableBorder: tableBorder ?? this.tableBorder,
|
||||||
|
tableColumnWidth: tableColumnWidth ?? this.tableColumnWidth,
|
||||||
|
tableScrollbarThumbVisibility: tableScrollbarThumbVisibility,
|
||||||
|
tableCellsPadding: tableCellsPadding ?? this.tableCellsPadding,
|
||||||
|
tableCellsDecoration: tableCellsDecoration ?? this.tableCellsDecoration,
|
||||||
|
tableHeadCellsPadding: tableHeadCellsPadding ?? this.tableHeadCellsPadding,
|
||||||
|
tableHeadCellsDecoration: tableHeadCellsDecoration ?? this.tableHeadCellsDecoration,
|
||||||
|
tableVerticalAlignment: tableVerticalAlignment ?? this.tableVerticalAlignment,
|
||||||
|
blockquotePadding: blockquotePadding ?? this.blockquotePadding,
|
||||||
|
blockquoteDecoration: blockquoteDecoration ?? this.blockquoteDecoration,
|
||||||
|
codeblockPadding: codeblockPadding ?? this.codeblockPadding,
|
||||||
|
codeblockDecoration: codeblockDecoration ?? this.codeblockDecoration,
|
||||||
|
horizontalRuleDecoration: horizontalRuleDecoration ?? this.horizontalRuleDecoration,
|
||||||
|
textAlign: textAlign ?? this.textAlign,
|
||||||
|
h1Align: h1Align ?? this.h1Align,
|
||||||
|
h2Align: h2Align ?? this.h2Align,
|
||||||
|
h3Align: h3Align ?? this.h3Align,
|
||||||
|
h4Align: h4Align ?? this.h4Align,
|
||||||
|
h5Align: h5Align ?? this.h5Align,
|
||||||
|
h6Align: h6Align ?? this.h6Align,
|
||||||
|
unorderedListAlign: unorderedListAlign ?? this.unorderedListAlign,
|
||||||
|
orderedListAlign: orderedListAlign ?? this.orderedListAlign,
|
||||||
|
blockquoteAlign: blockquoteAlign ?? this.blockquoteAlign,
|
||||||
|
codeblockAlign: codeblockAlign ?? this.codeblockAlign,
|
||||||
|
superscriptFontFeatureTag: superscriptFontFeatureTag ?? this.superscriptFontFeatureTag,
|
||||||
|
textScaler: newTextScaler,
|
||||||
|
textScaleFactor: nextTextScaleFactor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new text style that is a combination of this style and the given
|
||||||
|
/// [other] style.
|
||||||
|
MarkdownStyleSheet merge(MarkdownStyleSheet? other) {
|
||||||
|
if (other == null) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return copyWith(
|
||||||
|
a: a!.merge(other.a),
|
||||||
|
p: p!.merge(other.p),
|
||||||
|
pPadding: other.pPadding,
|
||||||
|
code: code!.merge(other.code),
|
||||||
|
h1: h1!.merge(other.h1),
|
||||||
|
h1Padding: other.h1Padding,
|
||||||
|
h2: h2!.merge(other.h2),
|
||||||
|
h2Padding: other.h2Padding,
|
||||||
|
h3: h3!.merge(other.h3),
|
||||||
|
h3Padding: other.h3Padding,
|
||||||
|
h4: h4!.merge(other.h4),
|
||||||
|
h4Padding: other.h4Padding,
|
||||||
|
h5: h5!.merge(other.h5),
|
||||||
|
h5Padding: other.h5Padding,
|
||||||
|
h6: h6!.merge(other.h6),
|
||||||
|
h6Padding: other.h6Padding,
|
||||||
|
em: em!.merge(other.em),
|
||||||
|
strong: strong!.merge(other.strong),
|
||||||
|
del: del!.merge(other.del),
|
||||||
|
blockquote: blockquote!.merge(other.blockquote),
|
||||||
|
img: img!.merge(other.img),
|
||||||
|
checkbox: checkbox!.merge(other.checkbox),
|
||||||
|
blockSpacing: other.blockSpacing,
|
||||||
|
listIndent: other.listIndent,
|
||||||
|
listBullet: listBullet!.merge(other.listBullet),
|
||||||
|
listBulletPadding: other.listBulletPadding,
|
||||||
|
tableHead: tableHead!.merge(other.tableHead),
|
||||||
|
tableBody: tableBody!.merge(other.tableBody),
|
||||||
|
tableHeadAlign: other.tableHeadAlign,
|
||||||
|
tablePadding: other.tablePadding,
|
||||||
|
tableBorder: other.tableBorder,
|
||||||
|
tableColumnWidth: other.tableColumnWidth,
|
||||||
|
tableScrollbarThumbVisibility: other.tableScrollbarThumbVisibility,
|
||||||
|
tableCellsPadding: other.tableCellsPadding,
|
||||||
|
tableCellsDecoration: other.tableCellsDecoration,
|
||||||
|
tableHeadCellsPadding: other.tableHeadCellsPadding,
|
||||||
|
tableHeadCellsDecoration: other.tableHeadCellsDecoration,
|
||||||
|
tableVerticalAlignment: other.tableVerticalAlignment,
|
||||||
|
blockquotePadding: other.blockquotePadding,
|
||||||
|
blockquoteDecoration: other.blockquoteDecoration,
|
||||||
|
codeblockPadding: other.codeblockPadding,
|
||||||
|
codeblockDecoration: other.codeblockDecoration,
|
||||||
|
horizontalRuleDecoration: other.horizontalRuleDecoration,
|
||||||
|
textAlign: other.textAlign,
|
||||||
|
h1Align: other.h1Align,
|
||||||
|
h2Align: other.h2Align,
|
||||||
|
h3Align: other.h3Align,
|
||||||
|
h4Align: other.h4Align,
|
||||||
|
h5Align: other.h5Align,
|
||||||
|
h6Align: other.h6Align,
|
||||||
|
unorderedListAlign: other.unorderedListAlign,
|
||||||
|
orderedListAlign: other.orderedListAlign,
|
||||||
|
blockquoteAlign: other.blockquoteAlign,
|
||||||
|
codeblockAlign: other.codeblockAlign,
|
||||||
|
textScaleFactor: other.textScaleFactor,
|
||||||
|
superscriptFontFeatureTag: other.superscriptFontFeatureTag,
|
||||||
|
// Only one of textScaler and textScaleFactor can be passed. If
|
||||||
|
// other.textScaleFactor is non-null, then the sheet was created with a
|
||||||
|
// textScaleFactor and the textScaler was derived from that, so should be
|
||||||
|
// ignored so that the textScaleFactor continues to be set.
|
||||||
|
textScaler: other.textScaleFactor == null ? other.textScaler : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The [TextStyle] to use for `a` elements.
|
||||||
|
final TextStyle? a;
|
||||||
|
|
||||||
|
/// The [TextStyle] to use for `p` elements.
|
||||||
|
final TextStyle? p;
|
||||||
|
|
||||||
|
/// The padding to use for `p` elements.
|
||||||
|
final EdgeInsets? pPadding;
|
||||||
|
|
||||||
|
/// The [TextStyle] to use for `code` elements.
|
||||||
|
final TextStyle? code;
|
||||||
|
|
||||||
|
/// The [TextStyle] to use for `h1` elements.
|
||||||
|
final TextStyle? h1;
|
||||||
|
|
||||||
|
/// The padding to use for `h1` elements.
|
||||||
|
final EdgeInsets? h1Padding;
|
||||||
|
|
||||||
|
/// The [TextStyle] to use for `h2` elements.
|
||||||
|
final TextStyle? h2;
|
||||||
|
|
||||||
|
/// The padding to use for `h2` elements.
|
||||||
|
final EdgeInsets? h2Padding;
|
||||||
|
|
||||||
|
/// The [TextStyle] to use for `h3` elements.
|
||||||
|
final TextStyle? h3;
|
||||||
|
|
||||||
|
/// The padding to use for `h3` elements.
|
||||||
|
final EdgeInsets? h3Padding;
|
||||||
|
|
||||||
|
/// The [TextStyle] to use for `h4` elements.
|
||||||
|
final TextStyle? h4;
|
||||||
|
|
||||||
|
/// The padding to use for `h4` elements.
|
||||||
|
final EdgeInsets? h4Padding;
|
||||||
|
|
||||||
|
/// The [TextStyle] to use for `h5` elements.
|
||||||
|
final TextStyle? h5;
|
||||||
|
|
||||||
|
/// The padding to use for `h5` elements.
|
||||||
|
final EdgeInsets? h5Padding;
|
||||||
|
|
||||||
|
/// The [TextStyle] to use for `h6` elements.
|
||||||
|
final TextStyle? h6;
|
||||||
|
|
||||||
|
/// The padding to use for `h6` elements.
|
||||||
|
final EdgeInsets? h6Padding;
|
||||||
|
|
||||||
|
/// The [TextStyle] to use for `em` elements.
|
||||||
|
final TextStyle? em;
|
||||||
|
|
||||||
|
/// The [TextStyle] to use for `strong` elements.
|
||||||
|
final TextStyle? strong;
|
||||||
|
|
||||||
|
/// The [TextStyle] to use for `del` elements.
|
||||||
|
final TextStyle? del;
|
||||||
|
|
||||||
|
/// The [TextStyle] to use for `blockquote` elements.
|
||||||
|
final TextStyle? blockquote;
|
||||||
|
|
||||||
|
/// The [TextStyle] to use for `img` elements.
|
||||||
|
final TextStyle? img;
|
||||||
|
|
||||||
|
/// The [TextStyle] to use for `input` elements.
|
||||||
|
final TextStyle? checkbox;
|
||||||
|
|
||||||
|
/// The amount of vertical space to use between block-level elements.
|
||||||
|
final double? blockSpacing;
|
||||||
|
|
||||||
|
/// The amount of horizontal space to indent list items.
|
||||||
|
final double? listIndent;
|
||||||
|
|
||||||
|
/// The [TextStyle] to use for bullets.
|
||||||
|
final TextStyle? listBullet;
|
||||||
|
|
||||||
|
/// The padding to use for bullets.
|
||||||
|
final EdgeInsets? listBulletPadding;
|
||||||
|
|
||||||
|
/// The [TextStyle] to use for `th` elements.
|
||||||
|
final TextStyle? tableHead;
|
||||||
|
|
||||||
|
/// The [TextStyle] to use for `td` elements.
|
||||||
|
final TextStyle? tableBody;
|
||||||
|
|
||||||
|
/// The [TextAlign] to use for `th` elements.
|
||||||
|
final TextAlign? tableHeadAlign;
|
||||||
|
|
||||||
|
/// The padding to use for `table` elements.
|
||||||
|
final EdgeInsets? tablePadding;
|
||||||
|
|
||||||
|
/// The [TableBorder] to use for `table` elements.
|
||||||
|
final TableBorder? tableBorder;
|
||||||
|
|
||||||
|
/// The [TableColumnWidth] to use for `th` and `td` elements.
|
||||||
|
final TableColumnWidth? tableColumnWidth;
|
||||||
|
|
||||||
|
/// The scrollbar thumbVisibility when the table is scrollable.
|
||||||
|
final bool? tableScrollbarThumbVisibility;
|
||||||
|
|
||||||
|
/// The padding to use for `th` and `td` elements.
|
||||||
|
final EdgeInsets? tableCellsPadding;
|
||||||
|
|
||||||
|
/// The decoration to use for `th` and `td` elements.
|
||||||
|
final Decoration? tableCellsDecoration;
|
||||||
|
|
||||||
|
/// The padding to use for `th` elements.
|
||||||
|
///
|
||||||
|
/// If null, defaults to [tableCellsPadding].
|
||||||
|
final EdgeInsets? tableHeadCellsPadding;
|
||||||
|
|
||||||
|
/// The decoration to use for `th` elements.
|
||||||
|
///
|
||||||
|
/// If null, defaults to [tableCellsDecoration].
|
||||||
|
final Decoration? tableHeadCellsDecoration;
|
||||||
|
|
||||||
|
/// The [TableCellVerticalAlignment] to use for `th` and `td` elements.
|
||||||
|
final TableCellVerticalAlignment tableVerticalAlignment;
|
||||||
|
|
||||||
|
/// The padding to use for `blockquote` elements.
|
||||||
|
final EdgeInsets? blockquotePadding;
|
||||||
|
|
||||||
|
/// The decoration to use behind `blockquote` elements.
|
||||||
|
final Decoration? blockquoteDecoration;
|
||||||
|
|
||||||
|
/// The padding to use for `pre` elements.
|
||||||
|
final EdgeInsets? codeblockPadding;
|
||||||
|
|
||||||
|
/// The decoration to use behind for `pre` elements.
|
||||||
|
final Decoration? codeblockDecoration;
|
||||||
|
|
||||||
|
/// The decoration to use for `hr` elements.
|
||||||
|
final Decoration? horizontalRuleDecoration;
|
||||||
|
|
||||||
|
/// The [WrapAlignment] to use for normal text. Defaults to start.
|
||||||
|
final WrapAlignment textAlign;
|
||||||
|
|
||||||
|
/// The [WrapAlignment] to use for h1 text. Defaults to start.
|
||||||
|
final WrapAlignment h1Align;
|
||||||
|
|
||||||
|
/// The [WrapAlignment] to use for h2 text. Defaults to start.
|
||||||
|
final WrapAlignment h2Align;
|
||||||
|
|
||||||
|
/// The [WrapAlignment] to use for h3 text. Defaults to start.
|
||||||
|
final WrapAlignment h3Align;
|
||||||
|
|
||||||
|
/// The [WrapAlignment] to use for h4 text. Defaults to start.
|
||||||
|
final WrapAlignment h4Align;
|
||||||
|
|
||||||
|
/// The [WrapAlignment] to use for h5 text. Defaults to start.
|
||||||
|
final WrapAlignment h5Align;
|
||||||
|
|
||||||
|
/// The [WrapAlignment] to use for h6 text. Defaults to start.
|
||||||
|
final WrapAlignment h6Align;
|
||||||
|
|
||||||
|
/// The [WrapAlignment] to use for an unordered list. Defaults to start.
|
||||||
|
final WrapAlignment unorderedListAlign;
|
||||||
|
|
||||||
|
/// The [WrapAlignment] to use for an ordered list. Defaults to start.
|
||||||
|
final WrapAlignment orderedListAlign;
|
||||||
|
|
||||||
|
/// The [WrapAlignment] to use for a blockquote. Defaults to start.
|
||||||
|
final WrapAlignment blockquoteAlign;
|
||||||
|
|
||||||
|
/// The [WrapAlignment] to use for a code block. Defaults to start.
|
||||||
|
final WrapAlignment codeblockAlign;
|
||||||
|
|
||||||
|
/// The text scaler to use in textual elements.
|
||||||
|
final TextScaler? textScaler;
|
||||||
|
|
||||||
|
/// The text scale factor to use in textual elements.
|
||||||
|
///
|
||||||
|
/// This will be non-null only if the sheet was created with the deprecated
|
||||||
|
/// [textScaleFactor] instead of [textScaler].
|
||||||
|
@Deprecated('Use textScaler instead.')
|
||||||
|
final double? textScaleFactor;
|
||||||
|
|
||||||
|
/// Custom font feature tag for font which does not support `sups'
|
||||||
|
/// feature to create superscript in footnotes.
|
||||||
|
final String? superscriptFontFeatureTag;
|
||||||
|
|
||||||
|
/// A [Map] from element name to the corresponding [TextStyle] object.
|
||||||
|
Map<String, TextStyle?> get styles => _styles;
|
||||||
|
Map<String, TextStyle?> _styles;
|
||||||
|
|
||||||
|
@override
|
||||||
|
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (other.runtimeType != MarkdownStyleSheet) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return other is MarkdownStyleSheet &&
|
||||||
|
other.a == a &&
|
||||||
|
other.p == p &&
|
||||||
|
other.pPadding == pPadding &&
|
||||||
|
other.code == code &&
|
||||||
|
other.h1 == h1 &&
|
||||||
|
other.h1Padding == h1Padding &&
|
||||||
|
other.h2 == h2 &&
|
||||||
|
other.h2Padding == h2Padding &&
|
||||||
|
other.h3 == h3 &&
|
||||||
|
other.h3Padding == h3Padding &&
|
||||||
|
other.h4 == h4 &&
|
||||||
|
other.h4Padding == h4Padding &&
|
||||||
|
other.h5 == h5 &&
|
||||||
|
other.h5Padding == h5Padding &&
|
||||||
|
other.h6 == h6 &&
|
||||||
|
other.h6Padding == h6Padding &&
|
||||||
|
other.em == em &&
|
||||||
|
other.strong == strong &&
|
||||||
|
other.del == del &&
|
||||||
|
other.blockquote == blockquote &&
|
||||||
|
other.img == img &&
|
||||||
|
other.checkbox == checkbox &&
|
||||||
|
other.blockSpacing == blockSpacing &&
|
||||||
|
other.listIndent == listIndent &&
|
||||||
|
other.listBullet == listBullet &&
|
||||||
|
other.listBulletPadding == listBulletPadding &&
|
||||||
|
other.tableHead == tableHead &&
|
||||||
|
other.tableBody == tableBody &&
|
||||||
|
other.tableHeadAlign == tableHeadAlign &&
|
||||||
|
other.tablePadding == tablePadding &&
|
||||||
|
other.tableBorder == tableBorder &&
|
||||||
|
other.tableColumnWidth == tableColumnWidth &&
|
||||||
|
other.tableCellsPadding == tableCellsPadding &&
|
||||||
|
other.tableCellsDecoration == tableCellsDecoration &&
|
||||||
|
other.tableHeadCellsPadding == tableHeadCellsPadding &&
|
||||||
|
other.tableHeadCellsDecoration == tableHeadCellsDecoration &&
|
||||||
|
other.tableVerticalAlignment == tableVerticalAlignment &&
|
||||||
|
other.blockquotePadding == blockquotePadding &&
|
||||||
|
other.blockquoteDecoration == blockquoteDecoration &&
|
||||||
|
other.codeblockPadding == codeblockPadding &&
|
||||||
|
other.codeblockDecoration == codeblockDecoration &&
|
||||||
|
other.horizontalRuleDecoration == horizontalRuleDecoration &&
|
||||||
|
other.textAlign == textAlign &&
|
||||||
|
other.h1Align == h1Align &&
|
||||||
|
other.h2Align == h2Align &&
|
||||||
|
other.h3Align == h3Align &&
|
||||||
|
other.h4Align == h4Align &&
|
||||||
|
other.h5Align == h5Align &&
|
||||||
|
other.h6Align == h6Align &&
|
||||||
|
other.unorderedListAlign == unorderedListAlign &&
|
||||||
|
other.orderedListAlign == orderedListAlign &&
|
||||||
|
other.blockquoteAlign == blockquoteAlign &&
|
||||||
|
other.codeblockAlign == codeblockAlign &&
|
||||||
|
other.superscriptFontFeatureTag == superscriptFontFeatureTag &&
|
||||||
|
other.textScaler == textScaler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||||
|
int get hashCode {
|
||||||
|
return Object.hashAll(<Object?>[
|
||||||
|
a,
|
||||||
|
p,
|
||||||
|
pPadding,
|
||||||
|
code,
|
||||||
|
h1,
|
||||||
|
h1Padding,
|
||||||
|
h2,
|
||||||
|
h2Padding,
|
||||||
|
h3,
|
||||||
|
h3Padding,
|
||||||
|
h4,
|
||||||
|
h4Padding,
|
||||||
|
h5,
|
||||||
|
h5Padding,
|
||||||
|
h6,
|
||||||
|
h6Padding,
|
||||||
|
em,
|
||||||
|
strong,
|
||||||
|
del,
|
||||||
|
blockquote,
|
||||||
|
img,
|
||||||
|
checkbox,
|
||||||
|
blockSpacing,
|
||||||
|
listIndent,
|
||||||
|
listBullet,
|
||||||
|
listBulletPadding,
|
||||||
|
tableHead,
|
||||||
|
tableBody,
|
||||||
|
tableHeadAlign,
|
||||||
|
tablePadding,
|
||||||
|
tableBorder,
|
||||||
|
tableColumnWidth,
|
||||||
|
tableCellsPadding,
|
||||||
|
tableCellsDecoration,
|
||||||
|
tableHeadCellsPadding,
|
||||||
|
tableHeadCellsDecoration,
|
||||||
|
tableVerticalAlignment,
|
||||||
|
blockquotePadding,
|
||||||
|
blockquoteDecoration,
|
||||||
|
codeblockPadding,
|
||||||
|
codeblockDecoration,
|
||||||
|
horizontalRuleDecoration,
|
||||||
|
textAlign,
|
||||||
|
h1Align,
|
||||||
|
h2Align,
|
||||||
|
h3Align,
|
||||||
|
h4Align,
|
||||||
|
h5Align,
|
||||||
|
h6Align,
|
||||||
|
unorderedListAlign,
|
||||||
|
orderedListAlign,
|
||||||
|
blockquoteAlign,
|
||||||
|
codeblockAlign,
|
||||||
|
textScaler,
|
||||||
|
textScaleFactor,
|
||||||
|
superscriptFontFeatureTag,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
589
flutter_markdown_plus/lib/src/widget.dart
Normal file
589
flutter_markdown_plus/lib/src/widget.dart
Normal file
|
|
@ -0,0 +1,589 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
|
import '../flutter_markdown_plus.dart';
|
||||||
|
import '_functions_io.dart' if (dart.library.js_interop) '_functions_web.dart';
|
||||||
|
|
||||||
|
/// Signature for callbacks used by [MarkdownWidget] when
|
||||||
|
/// [MarkdownWidget.selectable] is set to true and the user changes selection.
|
||||||
|
///
|
||||||
|
/// The callback will return the entire block of text available for selection,
|
||||||
|
/// along with the current [selection] and the [cause] of the selection change.
|
||||||
|
/// This is a wrapper of [SelectionChangedCallback] with additional context
|
||||||
|
/// [text] for the caller to process.
|
||||||
|
///
|
||||||
|
/// Used by [MarkdownWidget.onSelectionChanged]
|
||||||
|
typedef MarkdownOnSelectionChangedCallback = void Function(
|
||||||
|
String? text, TextSelection selection, SelectionChangedCause? cause);
|
||||||
|
|
||||||
|
/// Signature for callbacks used by [MarkdownWidget] when the user taps a link.
|
||||||
|
/// The callback will return the link text, destination, and title from the
|
||||||
|
/// Markdown link tag in the document.
|
||||||
|
///
|
||||||
|
/// Used by [MarkdownWidget.onTapLink].
|
||||||
|
typedef MarkdownTapLinkCallback = void Function(String text, String? href, String title);
|
||||||
|
|
||||||
|
/// Signature for custom image widget.
|
||||||
|
///
|
||||||
|
/// Used by [MarkdownWidget.imageBuilder]
|
||||||
|
typedef MarkdownImageBuilder = Widget Function(Uri uri, String? title, String? alt);
|
||||||
|
|
||||||
|
/// Signature for custom checkbox widget.
|
||||||
|
///
|
||||||
|
/// Used by [MarkdownWidget.checkboxBuilder]
|
||||||
|
typedef MarkdownCheckboxBuilder = Widget Function(bool value);
|
||||||
|
|
||||||
|
/// Signature for custom bullet widget.
|
||||||
|
///
|
||||||
|
/// Used by [MarkdownWidget.bulletBuilder]
|
||||||
|
typedef MarkdownBulletBuilder = Widget Function(
|
||||||
|
MarkdownBulletParameters parameters,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// An parameters of [MarkdownBulletBuilder].
|
||||||
|
///
|
||||||
|
/// Used by [MarkdownWidget.bulletBuilder]
|
||||||
|
class MarkdownBulletParameters {
|
||||||
|
/// Creates a new instance of [MarkdownBulletParameters].
|
||||||
|
const MarkdownBulletParameters({
|
||||||
|
required this.index,
|
||||||
|
required this.style,
|
||||||
|
required this.nestLevel,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The index of the bullet on that nesting level.
|
||||||
|
final int index;
|
||||||
|
|
||||||
|
/// The style of the bullet.
|
||||||
|
final BulletStyle style;
|
||||||
|
|
||||||
|
/// The nest level of the bullet.
|
||||||
|
final int nestLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enumeration sent to the user when calling [MarkdownBulletBuilder]
|
||||||
|
///
|
||||||
|
/// Use this to differentiate the bullet styling when building your own.
|
||||||
|
enum BulletStyle {
|
||||||
|
/// An ordered list.
|
||||||
|
orderedList,
|
||||||
|
|
||||||
|
/// An unordered list.
|
||||||
|
unorderedList,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a format [TextSpan] given a string.
|
||||||
|
///
|
||||||
|
/// Used by [MarkdownWidget] to highlight the contents of `pre` elements.
|
||||||
|
abstract class SyntaxHighlighter {
|
||||||
|
// ignore: one_member_abstracts
|
||||||
|
/// Returns the formatted [TextSpan] for the given string.
|
||||||
|
TextSpan format(String source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An interface for an element builder.
|
||||||
|
abstract class MarkdownElementBuilder {
|
||||||
|
/// For block syntax has to return true.
|
||||||
|
///
|
||||||
|
/// By default returns false.
|
||||||
|
bool isBlockElement() => false;
|
||||||
|
|
||||||
|
/// Called when an Element has been reached, before its children have been
|
||||||
|
/// visited.
|
||||||
|
void visitElementBefore(md.Element element) {}
|
||||||
|
|
||||||
|
/// Called when a text node has been reached.
|
||||||
|
///
|
||||||
|
/// If [MarkdownWidget.styleSheet] has a style of this tag, will passing
|
||||||
|
/// to [preferredStyle].
|
||||||
|
///
|
||||||
|
/// If you needn't build a widget, return null.
|
||||||
|
Widget? visitText(md.Text text, TextStyle? preferredStyle) => null;
|
||||||
|
|
||||||
|
/// Called when an Element has been reached, after its children have been
|
||||||
|
/// visited.
|
||||||
|
///
|
||||||
|
/// If [MarkdownWidget.styleSheet] has a style with this tag, it will be
|
||||||
|
/// passed as [preferredStyle].
|
||||||
|
///
|
||||||
|
/// If parent element has [TextStyle] set, it will be passed as
|
||||||
|
/// [parentStyle].
|
||||||
|
///
|
||||||
|
/// If a widget build isn't needed, return null.
|
||||||
|
Widget? visitElementAfterWithContext(
|
||||||
|
BuildContext context,
|
||||||
|
md.Element element,
|
||||||
|
TextStyle? preferredStyle,
|
||||||
|
TextStyle? parentStyle,
|
||||||
|
) {
|
||||||
|
return visitElementAfter(element, preferredStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when an Element has been reached, after its children have been
|
||||||
|
/// visited.
|
||||||
|
///
|
||||||
|
/// If [MarkdownWidget.styleSheet] has a style of this tag, will passing
|
||||||
|
/// to [preferredStyle].
|
||||||
|
///
|
||||||
|
/// If you needn't build a widget, return null.
|
||||||
|
@Deprecated('Use visitElementAfterWithContext() instead.')
|
||||||
|
Widget? visitElementAfter(md.Element element, TextStyle? preferredStyle) => null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enum to specify which theme being used when creating [MarkdownStyleSheet]
|
||||||
|
///
|
||||||
|
/// [material] - create MarkdownStyleSheet based on MaterialTheme
|
||||||
|
/// [cupertino] - create MarkdownStyleSheet based on CupertinoTheme
|
||||||
|
/// [platform] - create MarkdownStyleSheet based on the Platform where the
|
||||||
|
/// is running on. Material on Android and Cupertino on iOS
|
||||||
|
enum MarkdownStyleSheetBaseTheme {
|
||||||
|
/// Creates a MarkdownStyleSheet based on MaterialTheme.
|
||||||
|
material,
|
||||||
|
|
||||||
|
/// Creates a MarkdownStyleSheet based on CupertinoTheme.
|
||||||
|
cupertino,
|
||||||
|
|
||||||
|
/// Creates a MarkdownStyleSheet whose theme is based on the current platform.
|
||||||
|
platform,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enumeration of alignment strategies for the cross axis of list items.
|
||||||
|
enum MarkdownListItemCrossAxisAlignment {
|
||||||
|
/// Uses [CrossAxisAlignment.baseline] for the row the bullet and the list
|
||||||
|
/// item are placed in.
|
||||||
|
///
|
||||||
|
/// This alignment will ensure that the bullet always lines up with
|
||||||
|
/// the list text on the baseline.
|
||||||
|
///
|
||||||
|
/// However, note that this alignment does not support intrinsic height
|
||||||
|
/// measurements because [RenderFlex] does not support it for
|
||||||
|
/// [CrossAxisAlignment.baseline].
|
||||||
|
/// See https://github.com/flutter/flutter_markdown_plus/issues/311 for cases,
|
||||||
|
/// where this might be a problem for you.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// * [start], which allows for intrinsic height measurements.
|
||||||
|
baseline,
|
||||||
|
|
||||||
|
/// Uses [CrossAxisAlignment.start] for the row the bullet and the list item
|
||||||
|
/// are placed in.
|
||||||
|
///
|
||||||
|
/// This alignment will ensure that intrinsic height measurements work.
|
||||||
|
///
|
||||||
|
/// However, note that this alignment might not line up the bullet with the
|
||||||
|
/// list text in the way you would expect in certain scenarios.
|
||||||
|
/// See https://github.com/flutter/flutter_markdown_plus/issues/169 for example
|
||||||
|
/// cases that do not produce expected results.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// * [baseline], which will position the bullet and list item on the
|
||||||
|
/// baseline.
|
||||||
|
start,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A base class for widgets that parse and display Markdown.
|
||||||
|
///
|
||||||
|
/// Supports all standard Markdown from the original
|
||||||
|
/// [Markdown specification](https://github.github.com/gfm/).
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [Markdown], which is a scrolling container of Markdown.
|
||||||
|
/// * [MarkdownBody], which is a non-scrolling container of Markdown.
|
||||||
|
/// * <https://github.github.com/gfm/>
|
||||||
|
abstract class MarkdownWidget extends StatefulWidget {
|
||||||
|
/// Creates a widget that parses and displays Markdown.
|
||||||
|
///
|
||||||
|
/// The [data] argument must not be null.
|
||||||
|
const MarkdownWidget({
|
||||||
|
super.key,
|
||||||
|
required this.data,
|
||||||
|
this.selectable = false,
|
||||||
|
this.styleSheet,
|
||||||
|
this.styleSheetTheme = MarkdownStyleSheetBaseTheme.material,
|
||||||
|
this.syntaxHighlighter,
|
||||||
|
this.onSelectionChanged,
|
||||||
|
this.onTapLink,
|
||||||
|
this.onTapText,
|
||||||
|
this.imageDirectory,
|
||||||
|
this.blockSyntaxes,
|
||||||
|
this.inlineSyntaxes,
|
||||||
|
this.extensionSet,
|
||||||
|
this.imageBuilder,
|
||||||
|
this.checkboxBuilder,
|
||||||
|
this.bulletBuilder,
|
||||||
|
this.builders = const <String, MarkdownElementBuilder>{},
|
||||||
|
this.paddingBuilders = const <String, MarkdownPaddingBuilder>{},
|
||||||
|
this.fitContent = false,
|
||||||
|
this.listItemCrossAxisAlignment = MarkdownListItemCrossAxisAlignment.baseline,
|
||||||
|
this.softLineBreak = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The Markdown to display.
|
||||||
|
final String data;
|
||||||
|
|
||||||
|
/// If true, the text is selectable.
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
final bool selectable;
|
||||||
|
|
||||||
|
/// The styles to use when displaying the Markdown.
|
||||||
|
///
|
||||||
|
/// If null, the styles are inferred from the current [Theme].
|
||||||
|
final MarkdownStyleSheet? styleSheet;
|
||||||
|
|
||||||
|
/// Setting to specify base theme for MarkdownStyleSheet
|
||||||
|
///
|
||||||
|
/// Default to [MarkdownStyleSheetBaseTheme.material]
|
||||||
|
final MarkdownStyleSheetBaseTheme? styleSheetTheme;
|
||||||
|
|
||||||
|
/// The syntax highlighter used to color text in `pre` elements.
|
||||||
|
///
|
||||||
|
/// If null, the [MarkdownStyleSheet.code] style is used for `pre` elements.
|
||||||
|
final SyntaxHighlighter? syntaxHighlighter;
|
||||||
|
|
||||||
|
/// Called when the user taps a link.
|
||||||
|
final MarkdownTapLinkCallback? onTapLink;
|
||||||
|
|
||||||
|
/// Called when the user changes selection when [selectable] is set to true.
|
||||||
|
final MarkdownOnSelectionChangedCallback? onSelectionChanged;
|
||||||
|
|
||||||
|
/// Default tap handler used when [selectable] is set to true
|
||||||
|
final VoidCallback? onTapText;
|
||||||
|
|
||||||
|
/// The base directory holding images referenced by Img tags with local or network file paths.
|
||||||
|
final String? imageDirectory;
|
||||||
|
|
||||||
|
/// Collection of custom block syntax types to be used parsing the Markdown data.
|
||||||
|
final List<md.BlockSyntax>? blockSyntaxes;
|
||||||
|
|
||||||
|
/// Collection of custom inline syntax types to be used parsing the Markdown data.
|
||||||
|
final List<md.InlineSyntax>? inlineSyntaxes;
|
||||||
|
|
||||||
|
/// Markdown syntax extension set
|
||||||
|
///
|
||||||
|
/// Defaults to [md.ExtensionSet.gitHubFlavored]
|
||||||
|
final md.ExtensionSet? extensionSet;
|
||||||
|
|
||||||
|
/// Call when build an image widget.
|
||||||
|
final MarkdownImageBuilder? imageBuilder;
|
||||||
|
|
||||||
|
/// Call when build a checkbox widget.
|
||||||
|
final MarkdownCheckboxBuilder? checkboxBuilder;
|
||||||
|
|
||||||
|
/// Called when building a bullet
|
||||||
|
final MarkdownBulletBuilder? bulletBuilder;
|
||||||
|
|
||||||
|
/// Render certain tags, usually used with [extensionSet]
|
||||||
|
///
|
||||||
|
/// For example, we will add support for `sub` tag:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// builders: {
|
||||||
|
/// 'sub': SubscriptBuilder(),
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The `SubscriptBuilder` is a subclass of [MarkdownElementBuilder].
|
||||||
|
final Map<String, MarkdownElementBuilder> builders;
|
||||||
|
|
||||||
|
/// Add padding for different tags (use only for block elements and img)
|
||||||
|
///
|
||||||
|
/// For example, we will add padding for `img` tag:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// paddingBuilders: {
|
||||||
|
/// 'img': ImgPaddingBuilder(),
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The `ImgPaddingBuilder` is a subclass of [MarkdownPaddingBuilder].
|
||||||
|
final Map<String, MarkdownPaddingBuilder> paddingBuilders;
|
||||||
|
|
||||||
|
/// Whether to allow the widget to fit the child content.
|
||||||
|
final bool fitContent;
|
||||||
|
|
||||||
|
/// Controls the cross axis alignment for the bullet and list item content
|
||||||
|
/// in lists.
|
||||||
|
///
|
||||||
|
/// Defaults to [MarkdownListItemCrossAxisAlignment.baseline], which
|
||||||
|
/// does not allow for intrinsic height measurements.
|
||||||
|
final MarkdownListItemCrossAxisAlignment listItemCrossAxisAlignment;
|
||||||
|
|
||||||
|
/// The soft line break is used to identify the spaces at the end of aline of
|
||||||
|
/// text and the leading spaces in the immediately following the line of text.
|
||||||
|
///
|
||||||
|
/// Default these spaces are removed in accordance with the Markdown
|
||||||
|
/// specification on soft line breaks when lines of text are joined.
|
||||||
|
final bool softLineBreak;
|
||||||
|
|
||||||
|
/// Subclasses should override this function to display the given children,
|
||||||
|
/// which are the parsed representation of [data].
|
||||||
|
@protected
|
||||||
|
Widget build(BuildContext context, List<Widget>? children);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MarkdownWidget> createState() => _MarkdownWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MarkdownWidgetState extends State<MarkdownWidget> implements MarkdownBuilderDelegate {
|
||||||
|
List<Widget>? _children;
|
||||||
|
final List<GestureRecognizer> _recognizers = <GestureRecognizer>[];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
_parseMarkdown();
|
||||||
|
super.didChangeDependencies();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(MarkdownWidget oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.data != oldWidget.data || widget.styleSheet != oldWidget.styleSheet) {
|
||||||
|
_parseMarkdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_disposeRecognizers();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _parseMarkdown() {
|
||||||
|
final MarkdownStyleSheet fallbackStyleSheet = kFallbackStyle(context, widget.styleSheetTheme);
|
||||||
|
final MarkdownStyleSheet styleSheet = fallbackStyleSheet.merge(widget.styleSheet);
|
||||||
|
|
||||||
|
_disposeRecognizers();
|
||||||
|
|
||||||
|
final md.Document document = md.Document(
|
||||||
|
blockSyntaxes: widget.blockSyntaxes,
|
||||||
|
inlineSyntaxes: widget.inlineSyntaxes,
|
||||||
|
extensionSet: widget.extensionSet ?? md.ExtensionSet.gitHubFlavored,
|
||||||
|
encodeHtml: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Parse the source Markdown data into nodes of an Abstract Syntax Tree.
|
||||||
|
final List<String> lines = const LineSplitter().convert(widget.data);
|
||||||
|
final List<md.Node> astNodes = document.parseLines(lines);
|
||||||
|
|
||||||
|
// Configure a Markdown widget builder to traverse the AST nodes and
|
||||||
|
// create a widget tree based on the elements.
|
||||||
|
final MarkdownBuilder builder = MarkdownBuilder(
|
||||||
|
delegate: this,
|
||||||
|
selectable: widget.selectable,
|
||||||
|
styleSheet: styleSheet,
|
||||||
|
imageDirectory: widget.imageDirectory,
|
||||||
|
imageBuilder: widget.imageBuilder,
|
||||||
|
checkboxBuilder: widget.checkboxBuilder,
|
||||||
|
bulletBuilder: widget.bulletBuilder,
|
||||||
|
builders: widget.builders,
|
||||||
|
paddingBuilders: widget.paddingBuilders,
|
||||||
|
fitContent: widget.fitContent,
|
||||||
|
listItemCrossAxisAlignment: widget.listItemCrossAxisAlignment,
|
||||||
|
onSelectionChanged: widget.onSelectionChanged,
|
||||||
|
onTapText: widget.onTapText,
|
||||||
|
softLineBreak: widget.softLineBreak,
|
||||||
|
);
|
||||||
|
|
||||||
|
_children = builder.build(astNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _disposeRecognizers() {
|
||||||
|
if (_recognizers.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final List<GestureRecognizer> localRecognizers = List<GestureRecognizer>.from(_recognizers);
|
||||||
|
_recognizers.clear();
|
||||||
|
for (final GestureRecognizer recognizer in localRecognizers) {
|
||||||
|
recognizer.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
GestureRecognizer createLink(String text, String? href, String title) {
|
||||||
|
final TapGestureRecognizer recognizer = TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
if (widget.onTapLink != null) {
|
||||||
|
widget.onTapLink!(text, href, title);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_recognizers.add(recognizer);
|
||||||
|
return recognizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TextSpan formatText(MarkdownStyleSheet styleSheet, String code) {
|
||||||
|
code = code.replaceAll(RegExp(r'\n$'), '');
|
||||||
|
if (widget.syntaxHighlighter != null) {
|
||||||
|
return widget.syntaxHighlighter!.format(code);
|
||||||
|
}
|
||||||
|
return TextSpan(style: styleSheet.code, text: code);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => widget.build(context, _children);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A non-scrolling widget that parses and displays Markdown.
|
||||||
|
///
|
||||||
|
/// Supports all GitHub Flavored Markdown from the
|
||||||
|
/// [specification](https://github.github.com/gfm/).
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [Markdown], which is a scrolling container of Markdown.
|
||||||
|
/// * <https://github.github.com/gfm/>
|
||||||
|
class MarkdownBody extends MarkdownWidget {
|
||||||
|
/// Creates a non-scrolling widget that parses and displays Markdown.
|
||||||
|
const MarkdownBody({
|
||||||
|
super.key,
|
||||||
|
required super.data,
|
||||||
|
super.selectable,
|
||||||
|
super.styleSheet,
|
||||||
|
super.styleSheetTheme = null,
|
||||||
|
super.syntaxHighlighter,
|
||||||
|
super.onSelectionChanged,
|
||||||
|
super.onTapLink,
|
||||||
|
super.onTapText,
|
||||||
|
super.imageDirectory,
|
||||||
|
super.blockSyntaxes,
|
||||||
|
super.inlineSyntaxes,
|
||||||
|
super.extensionSet,
|
||||||
|
super.imageBuilder,
|
||||||
|
super.checkboxBuilder,
|
||||||
|
super.bulletBuilder,
|
||||||
|
super.builders,
|
||||||
|
super.paddingBuilders,
|
||||||
|
super.listItemCrossAxisAlignment,
|
||||||
|
this.shrinkWrap = true,
|
||||||
|
super.fitContent = true,
|
||||||
|
super.softLineBreak,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// If [shrinkWrap] is `true`, [MarkdownBody] will take the minimum height
|
||||||
|
/// that wraps its content. Otherwise, [MarkdownBody] will expand to the
|
||||||
|
/// maximum allowed height.
|
||||||
|
final bool shrinkWrap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, List<Widget>? children) {
|
||||||
|
if (children!.length == 1 && shrinkWrap) {
|
||||||
|
return children.single;
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: shrinkWrap ? MainAxisSize.min : MainAxisSize.max,
|
||||||
|
crossAxisAlignment: fitContent ? CrossAxisAlignment.start : CrossAxisAlignment.stretch,
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A scrolling widget that parses and displays Markdown.
|
||||||
|
///
|
||||||
|
/// Supports all GitHub Flavored Markdown from the
|
||||||
|
/// [specification](https://github.github.com/gfm/).
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [MarkdownBody], which is a non-scrolling container of Markdown.
|
||||||
|
/// * <https://github.github.com/gfm/>
|
||||||
|
class Markdown extends MarkdownWidget {
|
||||||
|
/// Creates a scrolling widget that parses and displays Markdown.
|
||||||
|
const Markdown({
|
||||||
|
super.key,
|
||||||
|
required super.data,
|
||||||
|
super.selectable,
|
||||||
|
super.styleSheet,
|
||||||
|
super.styleSheetTheme = null,
|
||||||
|
super.syntaxHighlighter,
|
||||||
|
super.onSelectionChanged,
|
||||||
|
super.onTapLink,
|
||||||
|
super.onTapText,
|
||||||
|
super.imageDirectory,
|
||||||
|
super.blockSyntaxes,
|
||||||
|
super.inlineSyntaxes,
|
||||||
|
super.extensionSet,
|
||||||
|
super.imageBuilder,
|
||||||
|
super.checkboxBuilder,
|
||||||
|
super.bulletBuilder,
|
||||||
|
super.builders,
|
||||||
|
super.paddingBuilders,
|
||||||
|
super.listItemCrossAxisAlignment,
|
||||||
|
this.padding = const EdgeInsets.all(16.0),
|
||||||
|
this.controller,
|
||||||
|
this.physics,
|
||||||
|
this.shrinkWrap = false,
|
||||||
|
super.softLineBreak,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The amount of space by which to inset the children.
|
||||||
|
final EdgeInsets padding;
|
||||||
|
|
||||||
|
/// An object that can be used to control the position to which this scroll view is scrolled.
|
||||||
|
///
|
||||||
|
/// See also: [ScrollView.controller]
|
||||||
|
final ScrollController? controller;
|
||||||
|
|
||||||
|
/// How the scroll view should respond to user input.
|
||||||
|
///
|
||||||
|
/// See also: [ScrollView.physics]
|
||||||
|
final ScrollPhysics? physics;
|
||||||
|
|
||||||
|
/// Whether the extent of the scroll view in the scroll direction should be
|
||||||
|
/// determined by the contents being viewed.
|
||||||
|
///
|
||||||
|
/// See also: [ScrollView.shrinkWrap]
|
||||||
|
final bool shrinkWrap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, List<Widget>? children) {
|
||||||
|
return ListView(
|
||||||
|
padding: padding,
|
||||||
|
controller: controller,
|
||||||
|
physics: physics,
|
||||||
|
shrinkWrap: shrinkWrap,
|
||||||
|
children: children!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse [task list items](https://github.github.com/gfm/#task-list-items-extension-).
|
||||||
|
///
|
||||||
|
/// This class is no longer used as Markdown now supports checkbox syntax natively.
|
||||||
|
@Deprecated('Use [OrderedListWithCheckBoxSyntax] or [UnorderedListWithCheckBoxSyntax]')
|
||||||
|
class TaskListSyntax extends md.InlineSyntax {
|
||||||
|
/// Creates a new instance.
|
||||||
|
@Deprecated('Use [OrderedListWithCheckBoxSyntax] or [UnorderedListWithCheckBoxSyntax]')
|
||||||
|
TaskListSyntax() : super(_pattern);
|
||||||
|
|
||||||
|
static const String _pattern = r'^ *\[([ xX])\] +';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool onMatch(md.InlineParser parser, Match match) {
|
||||||
|
final md.Element el = md.Element.withTag('input');
|
||||||
|
el.attributes['type'] = 'checkbox';
|
||||||
|
el.attributes['disabled'] = 'true';
|
||||||
|
el.attributes['checked'] = '${match[1]!.trim().isNotEmpty}';
|
||||||
|
parser.addNode(el);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An interface for an padding builder for element.
|
||||||
|
abstract class MarkdownPaddingBuilder {
|
||||||
|
/// Called when an Element has been reached, before its children have been
|
||||||
|
/// visited.
|
||||||
|
void visitElementBefore(md.Element element) {}
|
||||||
|
|
||||||
|
/// Called when a widget node has been rendering and need tag padding.
|
||||||
|
EdgeInsets getPadding() => EdgeInsets.zero;
|
||||||
|
}
|
||||||
32
flutter_markdown_plus/pubspec.yaml
Normal file
32
flutter_markdown_plus/pubspec.yaml
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
name: flutter_markdown_plus
|
||||||
|
description: A Markdown renderer for Flutter. Create rich text output,
|
||||||
|
including text styles, tables, links, and more, from plain text data
|
||||||
|
formatted with simple Markdown tags.
|
||||||
|
repository: https://github.com/foresightmobile/flutter_markdown_plus
|
||||||
|
issue_tracker: https://github.com/foresightmobile/flutter_markdown_plus/issues
|
||||||
|
version: 1.0.7
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ^3.4.0
|
||||||
|
flutter: ">=3.27.1"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
markdown: ^7.3.0
|
||||||
|
meta: ^1.16.0
|
||||||
|
path: ^1.9.1
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
leak_tracker_flutter_testing: any
|
||||||
|
mockito: ^5.5.0
|
||||||
|
standard_message_codec: ^0.0.1+4
|
||||||
|
|
||||||
|
topics:
|
||||||
|
- markdown
|
||||||
|
- widgets
|
||||||
|
|
||||||
|
formatter:
|
||||||
|
page_width: 120
|
||||||
45
flutter_markdown_plus/test/all.dart
Normal file
45
flutter_markdown_plus/test/all.dart
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'blockquote_test.dart' as blockquote_test;
|
||||||
|
import 'custom_syntax_test.dart' as custome_syntax_test;
|
||||||
|
import 'emphasis_test.dart' as emphasis_test;
|
||||||
|
import 'footnote_test.dart' as footnote_test;
|
||||||
|
import 'header_test.dart' as header_test;
|
||||||
|
import 'horizontal_rule_test.dart' as horizontal_rule_test;
|
||||||
|
import 'html_test.dart' as html_test;
|
||||||
|
import 'image_test.dart' as image_test;
|
||||||
|
import 'line_break_test.dart' as line_break_test;
|
||||||
|
import 'link_test.dart' as link_test;
|
||||||
|
import 'list_test.dart' as list_test;
|
||||||
|
import 'scrollable_test.dart' as scrollable_test;
|
||||||
|
import 'selection_area_compatibility_test.dart' as selection_area_test;
|
||||||
|
import 'style_sheet_test.dart' as style_sheet_test;
|
||||||
|
import 'table_test.dart' as table_test;
|
||||||
|
import 'text_alignment_test.dart' as text_alignment_test;
|
||||||
|
import 'text_scaler_test.dart' as text_scaler;
|
||||||
|
import 'text_test.dart' as text_test;
|
||||||
|
import 'uri_test.dart' as uri_test;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
blockquote_test.defineTests();
|
||||||
|
custome_syntax_test.defineTests();
|
||||||
|
emphasis_test.defineTests();
|
||||||
|
footnote_test.defineTests();
|
||||||
|
header_test.defineTests();
|
||||||
|
horizontal_rule_test.defineTests();
|
||||||
|
html_test.defineTests();
|
||||||
|
image_test.defineTests();
|
||||||
|
line_break_test.defineTests();
|
||||||
|
link_test.defineTests();
|
||||||
|
list_test.defineTests();
|
||||||
|
scrollable_test.defineTests();
|
||||||
|
selection_area_test.defineTests();
|
||||||
|
style_sheet_test.defineTests();
|
||||||
|
table_test.defineTests();
|
||||||
|
text_test.defineTests();
|
||||||
|
text_alignment_test.defineTests();
|
||||||
|
text_scaler.defineTests();
|
||||||
|
uri_test.defineTests();
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
flutter_markdown_plus/test/assets/images/logo.png
Normal file
BIN
flutter_markdown_plus/test/assets/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
106
flutter_markdown_plus/test/blockquote_test.dart
Normal file
106
flutter_markdown_plus/test/blockquote_test.dart
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
|
void main() => defineTests();
|
||||||
|
|
||||||
|
void defineTests() {
|
||||||
|
group('Blockquote', () {
|
||||||
|
testWidgets(
|
||||||
|
'simple one word blockquote',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: '> quote'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
expectTextStrings(widgets, <String>['quote']);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'soft wrapping in blockquote',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: '> soft\n> wrap'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
expectTextStrings(widgets, <String>['soft wrap']);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should work with styling',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(
|
||||||
|
textTheme: textTheme,
|
||||||
|
);
|
||||||
|
final MarkdownStyleSheet styleSheet = MarkdownStyleSheet.fromTheme(
|
||||||
|
theme,
|
||||||
|
);
|
||||||
|
|
||||||
|
const String data =
|
||||||
|
'> this is a link: [Markdown guide](https://www.markdownguide.org) and this is **bold** and *italic*';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
MarkdownBody(
|
||||||
|
data: data,
|
||||||
|
styleSheet: styleSheet,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
final DecoratedBox blockQuoteContainer = tester.widget(
|
||||||
|
find.byType(DecoratedBox),
|
||||||
|
);
|
||||||
|
final Text quoteText = tester.widget(find.byType(Text));
|
||||||
|
final List<TextSpan> styledTextParts = (quoteText.textSpan! as TextSpan).children!.cast<TextSpan>();
|
||||||
|
|
||||||
|
expectTextStrings(
|
||||||
|
widgets,
|
||||||
|
<String>['this is a link: Markdown guide and this is bold and italic'],
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
(blockQuoteContainer.decoration as BoxDecoration).color,
|
||||||
|
(styleSheet.blockquoteDecoration as BoxDecoration?)!.color,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
(blockQuoteContainer.decoration as BoxDecoration).borderRadius,
|
||||||
|
(styleSheet.blockquoteDecoration as BoxDecoration?)!.borderRadius,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// this is a link
|
||||||
|
expect(styledTextParts[0].text, 'this is a link: ');
|
||||||
|
expect(
|
||||||
|
styledTextParts[0].style!.color,
|
||||||
|
theme.textTheme.bodyMedium!.color,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Markdown guide
|
||||||
|
expect(styledTextParts[1].text, 'Markdown guide');
|
||||||
|
expect(styledTextParts[1].style!.color, styleSheet.blockquote!.color);
|
||||||
|
|
||||||
|
/// and this is
|
||||||
|
expect(
|
||||||
|
styledTextParts[2].style!.color,
|
||||||
|
theme.textTheme.bodyMedium!.color,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// bold
|
||||||
|
expect(styledTextParts[2].text, ' and this is bold and italic');
|
||||||
|
expect(styledTextParts[2].style!.fontWeight, FontWeight.w400);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
445
flutter_markdown_plus/test/custom_syntax_test.dart
Normal file
445
flutter_markdown_plus/test/custom_syntax_test.dart
Normal file
|
|
@ -0,0 +1,445 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
|
void main() => defineTests();
|
||||||
|
|
||||||
|
void defineTests() {
|
||||||
|
group('Custom Syntax', () {
|
||||||
|
testWidgets(
|
||||||
|
'Subscript',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
Markdown(
|
||||||
|
data: 'H_2O',
|
||||||
|
extensionSet: md.ExtensionSet.none,
|
||||||
|
inlineSyntaxes: <md.InlineSyntax>[SubscriptSyntax()],
|
||||||
|
builders: <String, MarkdownElementBuilder>{
|
||||||
|
'sub': SubscriptBuilder(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
expectTextStrings(widgets, <String>['H₂O']);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Custom block element',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String blockContent = 'note block';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
Markdown(
|
||||||
|
data: '[!NOTE] $blockContent',
|
||||||
|
extensionSet: md.ExtensionSet.none,
|
||||||
|
blockSyntaxes: <md.BlockSyntax>[NoteSyntax()],
|
||||||
|
builders: <String, MarkdownElementBuilder>{
|
||||||
|
'note': NoteBuilder(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final ColoredBox container = tester.widgetList(find.byType(ColoredBox)).first as ColoredBox;
|
||||||
|
expect(container.color, Colors.red);
|
||||||
|
expect(container.child, isInstanceOf<Text>());
|
||||||
|
expect((container.child! as Text).data, blockContent);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Block with custom tag',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String textBefore = 'Before ';
|
||||||
|
const String textAfter = ' After';
|
||||||
|
const String blockContent = 'Custom content rendered in a ColoredBox';
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
Markdown(
|
||||||
|
data: '$textBefore\n{{custom}}\n$blockContent\n{{/custom}}\n$textAfter',
|
||||||
|
extensionSet: md.ExtensionSet.none,
|
||||||
|
blockSyntaxes: <md.BlockSyntax>[CustomTagBlockSyntax()],
|
||||||
|
builders: <String, MarkdownElementBuilder>{
|
||||||
|
'custom': CustomTagBlockBuilder(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final ColoredBox container = tester.widgetList(find.byType(ColoredBox)).first as ColoredBox;
|
||||||
|
expect(container.color, Colors.red);
|
||||||
|
expect(container.child, isInstanceOf<Text>());
|
||||||
|
expect((container.child! as Text).data, blockContent);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'link for wikistyle',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
Markdown(
|
||||||
|
data: 'This is a [[wiki link]]',
|
||||||
|
extensionSet: md.ExtensionSet.none,
|
||||||
|
inlineSyntaxes: <md.InlineSyntax>[WikilinkSyntax()],
|
||||||
|
builders: <String, MarkdownElementBuilder>{
|
||||||
|
'wikilink': WikilinkBuilder(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Text textWidget = tester.widget(find.byType(Text));
|
||||||
|
final TextSpan span = (textWidget.textSpan! as TextSpan).children![1] as TextSpan;
|
||||||
|
|
||||||
|
expect(span.children, null);
|
||||||
|
expect(span.recognizer.runtimeType, equals(TapGestureRecognizer));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'WidgetSpan in Text.rich is handled correctly',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
Markdown(
|
||||||
|
data: 'container is a widget that allows to customize its child',
|
||||||
|
extensionSet: md.ExtensionSet.none,
|
||||||
|
inlineSyntaxes: <md.InlineSyntax>[ContainerSyntax()],
|
||||||
|
builders: <String, MarkdownElementBuilder>{
|
||||||
|
'container': ContainerBuilder(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Text textWidget = tester.widget(find.byType(Text));
|
||||||
|
final TextSpan textSpan = textWidget.textSpan! as TextSpan;
|
||||||
|
final WidgetSpan widgetSpan = textSpan.children![0] as WidgetSpan;
|
||||||
|
expect(widgetSpan.child, isInstanceOf<Container>());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'visitElementAfterWithContext is handled correctly',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
Markdown(
|
||||||
|
data: r'# This is a header with some \color1{color} in it',
|
||||||
|
extensionSet: md.ExtensionSet.none,
|
||||||
|
inlineSyntaxes: <md.InlineSyntax>[InlineTextColorSyntax()],
|
||||||
|
builders: <String, MarkdownElementBuilder>{
|
||||||
|
'inlineTextColor': InlineTextColorElementBuilder(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Text textWidget = tester.widget(find.byType(Text));
|
||||||
|
final TextSpan rootSpan = textWidget.textSpan! as TextSpan;
|
||||||
|
final TextSpan firstSpan = rootSpan.children![0] as TextSpan;
|
||||||
|
final TextSpan secondSpan = rootSpan.children![1] as TextSpan;
|
||||||
|
final TextSpan thirdSpan = rootSpan.children![2] as TextSpan;
|
||||||
|
|
||||||
|
expect(secondSpan.style!.color, Colors.red);
|
||||||
|
expect(secondSpan.style!.fontSize, firstSpan.style!.fontSize);
|
||||||
|
expect(secondSpan.style!.fontSize, thirdSpan.style!.fontSize);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'TextSpan and WidgetSpan as children in Text.rich are handled correctly',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
Markdown(
|
||||||
|
data: 'this test replaces a string with a container',
|
||||||
|
extensionSet: md.ExtensionSet.none,
|
||||||
|
inlineSyntaxes: <md.InlineSyntax>[ContainerSyntax()],
|
||||||
|
builders: <String, MarkdownElementBuilder>{
|
||||||
|
'container': ContainerBuilder2(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Text textWidget = tester.widget(find.byType(Text));
|
||||||
|
final TextSpan textSpan = textWidget.textSpan! as TextSpan;
|
||||||
|
final TextSpan start = textSpan.children![0] as TextSpan;
|
||||||
|
expect(start.text, 'this test replaces a string with a ');
|
||||||
|
final TextSpan foo = textSpan.children![1] as TextSpan;
|
||||||
|
expect(foo.text, 'foo');
|
||||||
|
final WidgetSpan widgetSpan = textSpan.children![2] as WidgetSpan;
|
||||||
|
expect(widgetSpan.child, isInstanceOf<Container>());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Custom rendering of tags without children',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
Markdown(
|
||||||
|
data: data,
|
||||||
|
builders: <String, MarkdownElementBuilder>{
|
||||||
|
'img': ImgBuilder(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Finder imageFinder = find.byType(Image);
|
||||||
|
expect(imageFinder, findsNothing);
|
||||||
|
final Finder textFinder = find.byType(Text);
|
||||||
|
expect(textFinder, findsOneWidget);
|
||||||
|
final Text textWidget = tester.widget(find.byType(Text));
|
||||||
|
expect(textWidget.data, 'foo');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubscriptSyntax extends md.InlineSyntax {
|
||||||
|
SubscriptSyntax() : super(_pattern);
|
||||||
|
|
||||||
|
static const String _pattern = r'_([0-9]+)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool onMatch(md.InlineParser parser, Match match) {
|
||||||
|
parser.addNode(md.Element.text('sub', match[1]!));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubscriptBuilder extends MarkdownElementBuilder {
|
||||||
|
static const List<String> _subscripts = <String>['₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget visitElementAfter(md.Element element, _) {
|
||||||
|
// We don't currently have a way to control the vertical alignment of text spans.
|
||||||
|
// See https://github.com/flutter/flutter/issues/10906#issuecomment-385723664
|
||||||
|
final String textContent = element.textContent;
|
||||||
|
String text = '';
|
||||||
|
for (int i = 0; i < textContent.length; i++) {
|
||||||
|
text += _subscripts[int.parse(textContent[i])];
|
||||||
|
}
|
||||||
|
return Text.rich(TextSpan(text: text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WikilinkSyntax extends md.InlineSyntax {
|
||||||
|
WikilinkSyntax() : super(_pattern);
|
||||||
|
|
||||||
|
static const String _pattern = r'\[\[(.*?)\]\]';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool onMatch(md.InlineParser parser, Match match) {
|
||||||
|
final String link = match[1]!;
|
||||||
|
final md.Element el = md.Element('wikilink', <md.Element>[md.Element.text('span', link)])
|
||||||
|
..attributes['href'] = link.replaceAll(' ', '_');
|
||||||
|
|
||||||
|
parser.addNode(el);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WikilinkBuilder extends MarkdownElementBuilder {
|
||||||
|
@override
|
||||||
|
Widget visitElementAfter(md.Element element, _) {
|
||||||
|
final TapGestureRecognizer recognizer = TapGestureRecognizer()..onTap = () {};
|
||||||
|
addTearDown(recognizer.dispose);
|
||||||
|
return Text.rich(
|
||||||
|
TextSpan(text: element.textContent, recognizer: recognizer),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContainerSyntax extends md.InlineSyntax {
|
||||||
|
ContainerSyntax() : super(_pattern);
|
||||||
|
|
||||||
|
static const String _pattern = 'container';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool onMatch(md.InlineParser parser, Match match) {
|
||||||
|
parser.addNode(
|
||||||
|
md.Element.text('container', ''),
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContainerBuilder extends MarkdownElementBuilder {
|
||||||
|
@override
|
||||||
|
Widget? visitElementAfter(md.Element element, _) {
|
||||||
|
return Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: <InlineSpan>[
|
||||||
|
WidgetSpan(
|
||||||
|
child: Container(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContainerBuilder2 extends MarkdownElementBuilder {
|
||||||
|
@override
|
||||||
|
Widget? visitElementAfter(md.Element element, _) {
|
||||||
|
return Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: <InlineSpan>[
|
||||||
|
const TextSpan(text: 'foo'),
|
||||||
|
WidgetSpan(
|
||||||
|
child: Container(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: The implementation of inline span is incomplete, it does not handle
|
||||||
|
// bold, italic, ... text with a colored block.
|
||||||
|
// This would not work: `\color1{Text with *bold* text}`
|
||||||
|
class InlineTextColorSyntax extends md.InlineSyntax {
|
||||||
|
InlineTextColorSyntax() : super(r'\\color([1-9]){(.*?)}');
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool onMatch(md.InlineParser parser, Match match) {
|
||||||
|
final String colorId = match.group(1)!;
|
||||||
|
final String textContent = match.group(2)!;
|
||||||
|
final md.Element node = md.Element.text(
|
||||||
|
'inlineTextColor',
|
||||||
|
textContent,
|
||||||
|
)..attributes['color'] = colorId;
|
||||||
|
|
||||||
|
parser.addNode(node);
|
||||||
|
|
||||||
|
parser.addNode(
|
||||||
|
md.Text(''),
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InlineTextColorElementBuilder extends MarkdownElementBuilder {
|
||||||
|
@override
|
||||||
|
Widget visitElementAfterWithContext(
|
||||||
|
BuildContext context,
|
||||||
|
md.Element element,
|
||||||
|
TextStyle? preferredStyle,
|
||||||
|
TextStyle? parentStyle,
|
||||||
|
) {
|
||||||
|
final String innerText = element.textContent;
|
||||||
|
final String color = element.attributes['color'] ?? '';
|
||||||
|
|
||||||
|
final Map<String, Color> contentColors = <String, Color>{
|
||||||
|
'1': Colors.red,
|
||||||
|
'2': Colors.green,
|
||||||
|
'3': Colors.blue,
|
||||||
|
};
|
||||||
|
final Color? contentColor = contentColors[color];
|
||||||
|
|
||||||
|
return Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
text: innerText,
|
||||||
|
style: parentStyle?.copyWith(color: contentColor),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImgBuilder extends MarkdownElementBuilder {
|
||||||
|
@override
|
||||||
|
Widget? visitElementAfter(md.Element element, TextStyle? preferredStyle) {
|
||||||
|
return Text('foo', style: preferredStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoteBuilder extends MarkdownElementBuilder {
|
||||||
|
@override
|
||||||
|
Widget? visitText(md.Text text, TextStyle? preferredStyle) {
|
||||||
|
return ColoredBox(color: Colors.red, child: Text(text.text, style: preferredStyle));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isBlockElement() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoteSyntax extends md.BlockSyntax {
|
||||||
|
@override
|
||||||
|
md.Node? parse(md.BlockParser parser) {
|
||||||
|
final md.Line line = parser.current;
|
||||||
|
parser.advance();
|
||||||
|
return md.Element('note', <md.Node>[md.Text(line.content.substring(8))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
RegExp get pattern => RegExp(r'^\[!NOTE] ');
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomTagBlockBuilder extends MarkdownElementBuilder {
|
||||||
|
@override
|
||||||
|
bool isBlockElement() => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget visitElementAfterWithContext(
|
||||||
|
BuildContext context,
|
||||||
|
md.Element element,
|
||||||
|
TextStyle? preferredStyle,
|
||||||
|
TextStyle? parentStyle,
|
||||||
|
) {
|
||||||
|
if (element.tag == 'custom') {
|
||||||
|
final String content = element.attributes['content']!;
|
||||||
|
return ColoredBox(color: Colors.red, child: Text(content, style: preferredStyle));
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomTagBlockSyntax extends md.BlockSyntax {
|
||||||
|
@override
|
||||||
|
bool canParse(md.BlockParser parser) {
|
||||||
|
return parser.current.content.startsWith('{{custom}}');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
RegExp get pattern => RegExp(r'\{\{custom\}\}([\s\S]*?)\{\{/custom\}\}');
|
||||||
|
|
||||||
|
@override
|
||||||
|
md.Node parse(md.BlockParser parser) {
|
||||||
|
parser.advance();
|
||||||
|
|
||||||
|
final StringBuffer buffer = StringBuffer();
|
||||||
|
while (!parser.current.content.startsWith('{{/custom}}') && !parser.isDone) {
|
||||||
|
buffer.writeln(parser.current.content);
|
||||||
|
parser.advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parser.isDone) {
|
||||||
|
parser.advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
final String content = buffer.toString().trim();
|
||||||
|
final md.Element element = md.Element.empty('custom');
|
||||||
|
element.attributes['content'] = content;
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
}
|
||||||
4287
flutter_markdown_plus/test/emphasis_test.dart
Normal file
4287
flutter_markdown_plus/test/emphasis_test.dart
Normal file
File diff suppressed because it is too large
Load diff
13
flutter_markdown_plus/test/flutter_test_config.dart
Normal file
13
flutter_markdown_plus/test/flutter_test_config.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
|
||||||
|
|
||||||
|
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
|
||||||
|
LeakTesting.enable();
|
||||||
|
LeakTracking.warnForUnsupportedPlatforms = false;
|
||||||
|
await testMain();
|
||||||
|
}
|
||||||
281
flutter_markdown_plus/test/footnote_test.dart
Normal file
281
flutter_markdown_plus/test/footnote_test.dart
Normal file
|
|
@ -0,0 +1,281 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
|
void main() => defineTests();
|
||||||
|
|
||||||
|
void defineTests() {
|
||||||
|
group(
|
||||||
|
'structure',
|
||||||
|
() {
|
||||||
|
testWidgets(
|
||||||
|
'footnote is detected and handle correctly',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = 'Foo[^a]\n[^a]: Bar';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(
|
||||||
|
data: data,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
expectTextStrings(widgets, <String>[
|
||||||
|
'Foo1',
|
||||||
|
'1.',
|
||||||
|
'Bar ↩',
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'footnote is detected and handle correctly for selectable markdown',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = 'Foo[^a]\n[^a]: Bar';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(
|
||||||
|
data: data,
|
||||||
|
selectable: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
expectTextStrings(widgets, <String>[
|
||||||
|
'Foo1',
|
||||||
|
'1.',
|
||||||
|
'Bar ↩',
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'ignore footnotes without description',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = 'Foo[^1] Bar[^2]\n[^1]: Bar';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(
|
||||||
|
data: data,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
expectTextStrings(widgets, <String>[
|
||||||
|
'Foo1 Bar[^2]',
|
||||||
|
'1.',
|
||||||
|
'Bar ↩',
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
testWidgets(
|
||||||
|
'ignore superscripts and footnotes order',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '[^2]: Bar \n [^1]: Foo \n Foo[^f] Bar[^b]';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(
|
||||||
|
data: data,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
expectTextStrings(widgets, <String>[
|
||||||
|
'Foo1 Bar2',
|
||||||
|
'1.',
|
||||||
|
'Foo ↩',
|
||||||
|
'2.',
|
||||||
|
'Bar ↩',
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'handle two digits superscript',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '''
|
||||||
|
1[^1] 2[^2] 3[^3] 4[^4] 5[^5] 6[^6] 7[^7] 8[^8] 9[^9] 10[^10]
|
||||||
|
[^1]:1
|
||||||
|
[^2]:2
|
||||||
|
[^3]:3
|
||||||
|
[^4]:4
|
||||||
|
[^5]:5
|
||||||
|
[^6]:6
|
||||||
|
[^7]:7
|
||||||
|
[^8]:8
|
||||||
|
[^9]:9
|
||||||
|
[^10]:10
|
||||||
|
''';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(
|
||||||
|
data: data,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
expectTextStrings(widgets, <String>[
|
||||||
|
'11 22 33 44 55 66 77 88 99 1010',
|
||||||
|
'1.',
|
||||||
|
'1 ↩',
|
||||||
|
'2.',
|
||||||
|
'2 ↩',
|
||||||
|
'3.',
|
||||||
|
'3 ↩',
|
||||||
|
'4.',
|
||||||
|
'4 ↩',
|
||||||
|
'5.',
|
||||||
|
'5 ↩',
|
||||||
|
'6.',
|
||||||
|
'6 ↩',
|
||||||
|
'7.',
|
||||||
|
'7 ↩',
|
||||||
|
'8.',
|
||||||
|
'8 ↩',
|
||||||
|
'9.',
|
||||||
|
'9 ↩',
|
||||||
|
'10.',
|
||||||
|
'10 ↩',
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group(
|
||||||
|
'superscript textstyle replacing',
|
||||||
|
() {
|
||||||
|
testWidgets(
|
||||||
|
'superscript has correct default fontfeature',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = 'Foo[^a]\n[^a]: Bar';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(
|
||||||
|
data: data,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
final Text text = widgets.firstWhere((Widget widget) => widget is Text) as Text;
|
||||||
|
|
||||||
|
final TextSpan span = text.textSpan! as TextSpan;
|
||||||
|
final List<InlineSpan>? children = span.children;
|
||||||
|
|
||||||
|
expect(children, isNotNull);
|
||||||
|
expect(children!.length, 2);
|
||||||
|
expect(children[1].style, isNotNull);
|
||||||
|
expect(children[1].style!.fontFeatures?.length, 1);
|
||||||
|
expect(children[1].style!.fontFeatures?.first.feature, 'sups');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'superscript has correct custom fontfeature',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = 'Foo[^a]\n[^a]: Bar';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
MarkdownBody(
|
||||||
|
data: data,
|
||||||
|
styleSheet: MarkdownStyleSheet(superscriptFontFeatureTag: 'numr'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
final Text text = widgets.firstWhere((Widget widget) => widget is Text) as Text;
|
||||||
|
|
||||||
|
final TextSpan span = text.textSpan! as TextSpan;
|
||||||
|
final List<InlineSpan>? children = span.children;
|
||||||
|
|
||||||
|
expect(children, isNotNull);
|
||||||
|
expect(children!.length, 2);
|
||||||
|
expect(children[1].style, isNotNull);
|
||||||
|
expect(children[1].style!.fontFeatures?.length, 2);
|
||||||
|
expect(children[1].style!.fontFeatures?[1].feature, 'numr');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'superscript index has the same font style like text',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '# Foo[^a]\n[^a]: Bar';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(
|
||||||
|
data: data,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
final Text text = widgets.firstWhere((Widget widget) => widget is Text) as Text;
|
||||||
|
|
||||||
|
final TextSpan span = text.textSpan! as TextSpan;
|
||||||
|
final List<InlineSpan>? children = span.children;
|
||||||
|
|
||||||
|
expect(children![0].style, isNotNull);
|
||||||
|
expect(children[1].style!.fontSize, children[0].style!.fontSize);
|
||||||
|
expect(children[1].style!.fontFamily, children[0].style!.fontFamily);
|
||||||
|
expect(children[1].style!.fontStyle, children[0].style!.fontStyle);
|
||||||
|
expect(children[1].style!.fontSize, children[0].style!.fontSize);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'link is correctly copied to new superscript index',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final List<MarkdownLink> linkTapResults = <MarkdownLink>[];
|
||||||
|
const String data = 'Foo[^a]\n[^a]: Bar';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
MarkdownBody(
|
||||||
|
data: data,
|
||||||
|
onTapLink: (String text, String? href, String title) =>
|
||||||
|
linkTapResults.add(MarkdownLink(text, href, title)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
final Text text = widgets.firstWhere((Widget widget) => widget is Text) as Text;
|
||||||
|
|
||||||
|
final TextSpan span = text.textSpan! as TextSpan;
|
||||||
|
|
||||||
|
final List<Type> gestureRecognizerTypes = <Type>[];
|
||||||
|
span.visitChildren((InlineSpan inlineSpan) {
|
||||||
|
if (inlineSpan is TextSpan) {
|
||||||
|
final TapGestureRecognizer? recognizer = inlineSpan.recognizer as TapGestureRecognizer?;
|
||||||
|
gestureRecognizerTypes.add(recognizer?.runtimeType ?? Null);
|
||||||
|
if (recognizer != null) {
|
||||||
|
recognizer.onTap!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(span.children!.length, 2);
|
||||||
|
expect(
|
||||||
|
gestureRecognizerTypes,
|
||||||
|
orderedEquals(<Type>[Null, TapGestureRecognizer]),
|
||||||
|
);
|
||||||
|
expectLinkTap(linkTapResults[0], const MarkdownLink('1', '#fn-a'));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
35
flutter_markdown_plus/test/header_test.dart
Normal file
35
flutter_markdown_plus/test/header_test.dart
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
|
void main() => defineTests();
|
||||||
|
|
||||||
|
void defineTests() {
|
||||||
|
group('Header', () {
|
||||||
|
testWidgets(
|
||||||
|
'level one',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '# Header';
|
||||||
|
await tester.pumpWidget(boilerplate(const MarkdownBody(data: data)));
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = selfAndDescendantWidgetsOf(
|
||||||
|
find.byType(MarkdownBody),
|
||||||
|
tester,
|
||||||
|
);
|
||||||
|
expectWidgetTypes(widgets, <Type>[
|
||||||
|
MarkdownBody,
|
||||||
|
Column,
|
||||||
|
Wrap,
|
||||||
|
Text,
|
||||||
|
RichText,
|
||||||
|
]);
|
||||||
|
expectTextStrings(widgets, <String>['Header']);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
88
flutter_markdown_plus/test/horizontal_rule_test.dart
Normal file
88
flutter_markdown_plus/test/horizontal_rule_test.dart
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
|
void main() => defineTests();
|
||||||
|
|
||||||
|
void defineTests() {
|
||||||
|
group('Horizontal Rule', () {
|
||||||
|
testWidgets(
|
||||||
|
'3 consecutive hyphens',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '---';
|
||||||
|
await tester.pumpWidget(boilerplate(const MarkdownBody(data: data)));
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = selfAndDescendantWidgetsOf(
|
||||||
|
find.byType(MarkdownBody),
|
||||||
|
tester,
|
||||||
|
);
|
||||||
|
expectWidgetTypes(widgets, <Type>[MarkdownBody, Container, DecoratedBox, Padding, LimitedBox, ConstrainedBox]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'5 consecutive hyphens',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '-----';
|
||||||
|
await tester.pumpWidget(boilerplate(const MarkdownBody(data: data)));
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = selfAndDescendantWidgetsOf(
|
||||||
|
find.byType(MarkdownBody),
|
||||||
|
tester,
|
||||||
|
);
|
||||||
|
expectWidgetTypes(widgets, <Type>[MarkdownBody, Container, DecoratedBox, Padding, LimitedBox, ConstrainedBox]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'3 asterisks separated with spaces',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '* * *';
|
||||||
|
await tester.pumpWidget(boilerplate(const MarkdownBody(data: data)));
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = selfAndDescendantWidgetsOf(
|
||||||
|
find.byType(MarkdownBody),
|
||||||
|
tester,
|
||||||
|
);
|
||||||
|
expectWidgetTypes(widgets, <Type>[MarkdownBody, Container, DecoratedBox, Padding, LimitedBox, ConstrainedBox]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'3 asterisks separated with spaces alongside text Markdown',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '# h1\n ## h2\n* * *';
|
||||||
|
await tester.pumpWidget(boilerplate(const MarkdownBody(data: data)));
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = selfAndDescendantWidgetsOf(
|
||||||
|
find.byType(MarkdownBody),
|
||||||
|
tester,
|
||||||
|
);
|
||||||
|
expectWidgetTypes(widgets, <Type>[
|
||||||
|
MarkdownBody,
|
||||||
|
Column,
|
||||||
|
Column,
|
||||||
|
Wrap,
|
||||||
|
Text,
|
||||||
|
RichText,
|
||||||
|
SizedBox,
|
||||||
|
Column,
|
||||||
|
Wrap,
|
||||||
|
Text,
|
||||||
|
RichText,
|
||||||
|
SizedBox,
|
||||||
|
Container,
|
||||||
|
DecoratedBox,
|
||||||
|
Padding,
|
||||||
|
LimitedBox,
|
||||||
|
ConstrainedBox
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
55
flutter_markdown_plus/test/html_test.dart
Normal file
55
flutter_markdown_plus/test/html_test.dart
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
|
void main() => defineTests();
|
||||||
|
|
||||||
|
void defineTests() {
|
||||||
|
group('HTML', () {
|
||||||
|
testWidgets(
|
||||||
|
'ignore tags',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final List<String> data = <String>[
|
||||||
|
'Line 1\n<p>HTML content</p>\nLine 2',
|
||||||
|
'Line 1\n<!-- HTML\n comment\n ignored --><\nLine 2'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final String line in data) {
|
||||||
|
await tester.pumpWidget(boilerplate(MarkdownBody(data: line)));
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
expectTextStrings(widgets, <String>['Line 1', 'Line 2']);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
"doesn't convert & to & when parsing",
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const Markdown(data: '&'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expectTextStrings(tester.allWidgets, <String>['&']);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
"doesn't convert < to < when parsing",
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const Markdown(data: '<'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expectTextStrings(tester.allWidgets, <String>['<']);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
448
flutter_markdown_plus/test/image_test.dart
Normal file
448
flutter_markdown_plus/test/image_test.dart
Normal file
|
|
@ -0,0 +1,448 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:io' as io;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'image_test_mocks.dart';
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
|
void main() => defineTests();
|
||||||
|
|
||||||
|
void defineTests() {
|
||||||
|
group('Image', () {
|
||||||
|
setUp(() {
|
||||||
|
// Only needs to be done once since the HttpClient generated
|
||||||
|
// by this override is cached as a static singleton.
|
||||||
|
io.HttpOverrides.global = TestHttpOverrides();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should not interrupt styling',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '_textbefore  textafter_';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const Markdown(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Text> texts = tester.widgetList(find.byType(Text));
|
||||||
|
final Text firstTextWidget = texts.first;
|
||||||
|
final TextSpan firstTextSpan = firstTextWidget.textSpan! as TextSpan;
|
||||||
|
final Image image = tester.widget(find.byType(Image));
|
||||||
|
final NetworkImage networkImage = image.image as NetworkImage;
|
||||||
|
final Text secondTextWidget = texts.last;
|
||||||
|
final TextSpan secondTextSpan = secondTextWidget.textSpan! as TextSpan;
|
||||||
|
|
||||||
|
expect(firstTextSpan.text, 'textbefore ');
|
||||||
|
expect(firstTextSpan.style!.fontStyle, FontStyle.italic);
|
||||||
|
expect(networkImage.url, 'https://img');
|
||||||
|
expect(secondTextSpan.text, ' textafter');
|
||||||
|
expect(secondTextSpan.style!.fontStyle, FontStyle.italic);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should work with a link',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const Markdown(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Image image = tester.widget(find.byType(Image));
|
||||||
|
final NetworkImage networkImage = image.image as NetworkImage;
|
||||||
|
expect(networkImage.url, 'https://img');
|
||||||
|
expect(image.width, 50);
|
||||||
|
expect(image.height, 50);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should work with relative remote image',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const Markdown(
|
||||||
|
data: data,
|
||||||
|
imageDirectory: 'https://localhost',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
final Image image = widgets.firstWhere((Widget widget) => widget is Image) as Image;
|
||||||
|
|
||||||
|
expect(image.image is NetworkImage, isTrue);
|
||||||
|
expect((image.image as NetworkImage).url, 'https://localhost/img.png');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'local files should be files on non-web',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const Markdown(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
final Image image = widgets.firstWhere((Widget widget) => widget is Image) as Image;
|
||||||
|
|
||||||
|
expect(image.image is FileImage, isTrue);
|
||||||
|
},
|
||||||
|
skip: kIsWeb || isLinux,
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'local files should be network on web',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const Markdown(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
final Image image = widgets.firstWhere((Widget widget) => widget is Image) as Image;
|
||||||
|
|
||||||
|
expect(image.image is NetworkImage, isTrue);
|
||||||
|
},
|
||||||
|
skip: !kIsWeb || !isLinux,
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should work with resources',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
const String data = '';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
MaterialApp(
|
||||||
|
home: DefaultAssetBundle(
|
||||||
|
bundle: TestAssetBundle(),
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
color: Colors.white,
|
||||||
|
width: 500,
|
||||||
|
child: const Markdown(
|
||||||
|
data: data,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Image image = tester.allWidgets.firstWhere((Widget widget) => widget is Image) as Image;
|
||||||
|
|
||||||
|
expect(image.image is AssetImage, isTrue);
|
||||||
|
expect((image.image as AssetImage).assetName, 'assets/logo.png');
|
||||||
|
|
||||||
|
// Force the asset image to be rasterized so it can be compared.
|
||||||
|
await tester.runAsync(() async {
|
||||||
|
final Element element = tester.element(find.byType(Markdown));
|
||||||
|
await precacheImage(image.image, element);
|
||||||
|
});
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
find.byType(Container), matchesGoldenFile('assets/images/golden/image_test/resource_asset_logo.png'));
|
||||||
|
},
|
||||||
|
skip: kIsWeb || isLinux, // Goldens are platform-specific.
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should work with local image files',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const Markdown(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Image image = tester.widget(find.byType(Image));
|
||||||
|
final FileImage fileImage = image.image as FileImage;
|
||||||
|
expect(fileImage.file.path, 'img.png');
|
||||||
|
expect(image.width, 50);
|
||||||
|
expect(image.height, 50);
|
||||||
|
},
|
||||||
|
skip: kIsWeb || isLinux,
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should show properly next to text',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = 'Hello ';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const Markdown(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Text text = tester.widget(find.byType(Text));
|
||||||
|
final TextSpan textSpan = text.textSpan! as TextSpan;
|
||||||
|
expect(textSpan.text, 'Hello ');
|
||||||
|
expect(textSpan.style, isNotNull);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should work when nested in a link',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final List<String> tapTexts = <String>[];
|
||||||
|
final List<String?> tapResults = <String?>[];
|
||||||
|
const String data = '[](href)';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
Markdown(
|
||||||
|
data: data,
|
||||||
|
onTapLink: (String text, String? value, String title) {
|
||||||
|
tapTexts.add(text);
|
||||||
|
tapResults.add(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final GestureDetector detector = tester.widget(find.byType(GestureDetector));
|
||||||
|
detector.onTap!();
|
||||||
|
|
||||||
|
expect(tapTexts.length, 1);
|
||||||
|
expect(tapTexts, everyElement('alt'));
|
||||||
|
expect(tapResults.length, 1);
|
||||||
|
expect(tapResults, everyElement('href'));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should work when nested in a link with text',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final List<String> tapTexts = <String>[];
|
||||||
|
final List<String?> tapResults = <String?>[];
|
||||||
|
const String data = '[Text before  text after](href)';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
Markdown(
|
||||||
|
data: data,
|
||||||
|
onTapLink: (String text, String? value, String title) {
|
||||||
|
tapTexts.add(text);
|
||||||
|
tapResults.add(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final GestureDetector detector = tester.widget(find.byType(GestureDetector));
|
||||||
|
detector.onTap!();
|
||||||
|
|
||||||
|
final Iterable<Text> texts = tester.widgetList(find.byType(Text));
|
||||||
|
final Text firstTextWidget = texts.first;
|
||||||
|
final TextSpan firstSpan = firstTextWidget.textSpan! as TextSpan;
|
||||||
|
(firstSpan.recognizer as TapGestureRecognizer?)!.onTap!();
|
||||||
|
|
||||||
|
final Text lastTextWidget = texts.last;
|
||||||
|
final TextSpan lastSpan = lastTextWidget.textSpan! as TextSpan;
|
||||||
|
(lastSpan.recognizer as TapGestureRecognizer?)!.onTap!();
|
||||||
|
|
||||||
|
expect(firstSpan.children, null);
|
||||||
|
expect(firstSpan.text, 'Text before ');
|
||||||
|
expect(firstSpan.recognizer.runtimeType, equals(TapGestureRecognizer));
|
||||||
|
|
||||||
|
expect(lastSpan.children, null);
|
||||||
|
expect(lastSpan.text, ' text after');
|
||||||
|
expect(lastSpan.recognizer.runtimeType, equals(TapGestureRecognizer));
|
||||||
|
|
||||||
|
expect(tapTexts.length, 3);
|
||||||
|
expect(tapTexts, everyElement('Text before alt text after'));
|
||||||
|
expect(tapResults.length, 3);
|
||||||
|
expect(tapResults, everyElement('href'));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should work alongside different links',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final List<String> tapTexts = <String>[];
|
||||||
|
final List<String?> tapResults = <String?>[];
|
||||||
|
const String data = '[Link before](firstHref)[](imageHref)[link after](secondHref)';
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
Markdown(
|
||||||
|
data: data,
|
||||||
|
onTapLink: (String text, String? value, String title) {
|
||||||
|
tapTexts.add(text);
|
||||||
|
tapResults.add(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Text> texts = tester.widgetList(find.byType(Text));
|
||||||
|
final Text firstTextWidget = texts.first;
|
||||||
|
final TextSpan firstSpan = firstTextWidget.textSpan! as TextSpan;
|
||||||
|
(firstSpan.recognizer as TapGestureRecognizer?)!.onTap!();
|
||||||
|
|
||||||
|
final GestureDetector detector = tester.widget(find.byType(GestureDetector));
|
||||||
|
detector.onTap!();
|
||||||
|
|
||||||
|
final Text lastTextWidget = texts.last;
|
||||||
|
final TextSpan lastSpan = lastTextWidget.textSpan! as TextSpan;
|
||||||
|
(lastSpan.recognizer as TapGestureRecognizer?)!.onTap!();
|
||||||
|
|
||||||
|
expect(firstSpan.children, null);
|
||||||
|
expect(firstSpan.text, 'Link before');
|
||||||
|
expect(firstSpan.recognizer.runtimeType, equals(TapGestureRecognizer));
|
||||||
|
|
||||||
|
expect(lastSpan.children, null);
|
||||||
|
expect(lastSpan.text, 'link after');
|
||||||
|
expect(lastSpan.recognizer.runtimeType, equals(TapGestureRecognizer));
|
||||||
|
|
||||||
|
expect(tapTexts.length, 3);
|
||||||
|
expect(tapTexts, <String>['Link before', 'alt', 'link after']);
|
||||||
|
expect(tapResults.length, 3);
|
||||||
|
expect(tapResults, <String>['firstHref', 'imageHref', 'secondHref']);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should gracefully handle image URLs with empty scheme',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const Markdown(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(Image), findsNothing);
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should gracefully handle image URLs with invalid scheme',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const Markdown(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// On the web, any URI with an unrecognized scheme is treated as a network image.
|
||||||
|
// Thus the error builder of the Image widget is called.
|
||||||
|
// On non-web, any URI with an unrecognized scheme is treated as a file image.
|
||||||
|
// However, constructing a file from an invalid URI will throw an exception.
|
||||||
|
// Thus the Image widget is never created, nor is its error builder called.
|
||||||
|
if (kIsWeb || isLinux) {
|
||||||
|
expect(find.byType(Image), findsOneWidget);
|
||||||
|
} else {
|
||||||
|
expect(find.byType(Image), findsNothing);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should gracefully handle width parsing failures',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const Markdown(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Image image = tester.widget(find.byType(Image));
|
||||||
|
final NetworkImage networkImage = image.image as NetworkImage;
|
||||||
|
expect(networkImage.url, 'https://img');
|
||||||
|
expect(image.width, null);
|
||||||
|
expect(image.height, 50);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should gracefully handle height parsing failures',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = ' ';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const Markdown(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Image image = tester.widget(find.byType(Image));
|
||||||
|
final NetworkImage networkImage = image.image as NetworkImage;
|
||||||
|
expect(networkImage.url, 'https://img');
|
||||||
|
expect(image.width, 50);
|
||||||
|
expect(image.height, null);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'custom image builder',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '';
|
||||||
|
Widget builder(Uri uri, String? title, String? alt) => Image.asset('assets/logo.png');
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
MaterialApp(
|
||||||
|
home: DefaultAssetBundle(
|
||||||
|
bundle: TestAssetBundle(),
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
color: Colors.white,
|
||||||
|
width: 500,
|
||||||
|
child: Markdown(
|
||||||
|
data: data,
|
||||||
|
imageBuilder: builder,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
final Image image = widgets.firstWhere((Widget widget) => widget is Image) as Image;
|
||||||
|
|
||||||
|
expect(image.image.runtimeType, AssetImage);
|
||||||
|
expect((image.image as AssetImage).assetName, 'assets/logo.png');
|
||||||
|
|
||||||
|
// Force the asset image to be rasterized so it can be compared.
|
||||||
|
await tester.runAsync(() async {
|
||||||
|
final Element element = tester.element(find.byType(Markdown));
|
||||||
|
await precacheImage(image.image, element);
|
||||||
|
});
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
find.byType(Container), matchesGoldenFile('assets/images/golden/image_test/custom_builder_asset_logo.png'));
|
||||||
|
imageCache.clear();
|
||||||
|
},
|
||||||
|
skip: kIsWeb || isLinux, // Goldens are platform-specific.
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
375
flutter_markdown_plus/test/image_test_mocks.dart
Normal file
375
flutter_markdown_plus/test/image_test_mocks.dart
Normal file
|
|
@ -0,0 +1,375 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:mockito/mockito.dart';
|
||||||
|
|
||||||
|
class TestHttpOverrides extends HttpOverrides {
|
||||||
|
@override
|
||||||
|
HttpClient createHttpClient(SecurityContext? context) {
|
||||||
|
return createMockImageHttpClient(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MockHttpClient createMockImageHttpClient(SecurityContext? _) {
|
||||||
|
final MockHttpClient client = MockHttpClient();
|
||||||
|
final MockHttpClientRequest request = MockHttpClientRequest();
|
||||||
|
final MockHttpClientResponse response = MockHttpClientResponse();
|
||||||
|
final MockHttpHeaders headers = MockHttpHeaders();
|
||||||
|
|
||||||
|
final List<int> transparentImage = getTestImageData();
|
||||||
|
|
||||||
|
when(client.getUrl(any)).thenAnswer((_) => Future<MockHttpClientRequest>.value(request));
|
||||||
|
|
||||||
|
when(request.headers).thenReturn(headers);
|
||||||
|
|
||||||
|
when(request.close()).thenAnswer((_) => Future<MockHttpClientResponse>.value(response));
|
||||||
|
|
||||||
|
when(client.autoUncompress = any).thenAnswer((_) => null);
|
||||||
|
|
||||||
|
when(response.contentLength).thenReturn(transparentImage.length);
|
||||||
|
|
||||||
|
when(response.statusCode).thenReturn(HttpStatus.ok);
|
||||||
|
|
||||||
|
when(response.compressionState).thenReturn(HttpClientResponseCompressionState.notCompressed);
|
||||||
|
|
||||||
|
// Define an image stream that streams the mock test image for all
|
||||||
|
// image tests that request an image.
|
||||||
|
StreamSubscription<List<int>> imageStream(Invocation invocation) {
|
||||||
|
final void Function(List<int>)? onData = invocation.positionalArguments[0] as void Function(List<int>)?;
|
||||||
|
final void Function()? onDone = invocation.namedArguments[#onDone] as void Function()?;
|
||||||
|
final void Function(Object, [StackTrace?])? onError =
|
||||||
|
invocation.namedArguments[#onError] as void Function(Object, [StackTrace?])?;
|
||||||
|
final bool? cancelOnError = invocation.namedArguments[#cancelOnError] as bool?;
|
||||||
|
|
||||||
|
return Stream<List<int>>.fromIterable(<List<int>>[transparentImage]).listen(
|
||||||
|
onData,
|
||||||
|
onError: onError,
|
||||||
|
onDone: onDone,
|
||||||
|
cancelOnError: cancelOnError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
when(response.listen(any,
|
||||||
|
onError: anyNamed('onError'), onDone: anyNamed('onDone'), cancelOnError: anyNamed('cancelOnError')))
|
||||||
|
.thenAnswer(imageStream);
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A list of integers that can be consumed as image data in a stream.
|
||||||
|
final List<int> _transparentImage = <int>[
|
||||||
|
// Image bytes.
|
||||||
|
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49,
|
||||||
|
0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06,
|
||||||
|
0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44,
|
||||||
|
0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D,
|
||||||
|
0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE,
|
||||||
|
];
|
||||||
|
|
||||||
|
List<int> getTestImageData() {
|
||||||
|
return _transparentImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Define the "fake" data types to be used in mock data type definitions. These
|
||||||
|
/// fake data types are important in the definition of the return values of the
|
||||||
|
/// properties and methods of the mock data types for null safety.
|
||||||
|
// ignore: avoid_implementing_value_types
|
||||||
|
class _FakeDuration extends Fake implements Duration {}
|
||||||
|
|
||||||
|
class _FakeHttpClientRequest extends Fake implements HttpClientRequest {}
|
||||||
|
|
||||||
|
class _FakeUri extends Fake implements Uri {}
|
||||||
|
|
||||||
|
class _FakeHttpHeaders extends Fake implements HttpHeaders {}
|
||||||
|
|
||||||
|
class _FakeHttpClientResponse extends Fake implements HttpClientResponse {}
|
||||||
|
|
||||||
|
class _FakeSocket extends Fake implements Socket {}
|
||||||
|
|
||||||
|
class _FakeStreamSubscription<T> extends Fake implements StreamSubscription<T> {}
|
||||||
|
|
||||||
|
/// A class which mocks [HttpClient].
|
||||||
|
///
|
||||||
|
/// See the documentation for Mockito's code generation for more information.
|
||||||
|
class MockHttpClient extends Mock implements HttpClient {
|
||||||
|
MockHttpClient() {
|
||||||
|
throwOnMissingStub(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Duration get idleTimeout =>
|
||||||
|
super.noSuchMethod(Invocation.getter(#idleTimeout), returnValue: _FakeDuration()) as Duration;
|
||||||
|
|
||||||
|
@override
|
||||||
|
set idleTimeout(Duration? idleTimeout) => super.noSuchMethod(Invocation.setter(#idleTimeout, idleTimeout));
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get autoUncompress => super.noSuchMethod(Invocation.getter(#autoUncompress), returnValue: false) as bool;
|
||||||
|
|
||||||
|
@override
|
||||||
|
set autoUncompress(bool? autoUncompress) => super.noSuchMethod(Invocation.setter(#autoUncompress, autoUncompress));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<HttpClientRequest> open(String? method, String? host, int? port, String? path) =>
|
||||||
|
super.noSuchMethod(Invocation.method(#open, <Object?>[method, host, port, path]),
|
||||||
|
returnValue: Future<_FakeHttpClientRequest>.value(_FakeHttpClientRequest())) as Future<HttpClientRequest>;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<HttpClientRequest> openUrl(String? method, Uri? url) =>
|
||||||
|
super.noSuchMethod(Invocation.method(#openUrl, <Object?>[method, url]),
|
||||||
|
returnValue: Future<_FakeHttpClientRequest>.value(_FakeHttpClientRequest())) as Future<HttpClientRequest>;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<HttpClientRequest> get(String? host, int? port, String? path) =>
|
||||||
|
super.noSuchMethod(Invocation.method(#get, <Object?>[host, port, path]),
|
||||||
|
returnValue: Future<_FakeHttpClientRequest>.value(_FakeHttpClientRequest())) as Future<HttpClientRequest>;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<HttpClientRequest> getUrl(Uri? url) => super.noSuchMethod(Invocation.method(#getUrl, <Object?>[url]),
|
||||||
|
returnValue: Future<_FakeHttpClientRequest>.value(_FakeHttpClientRequest())) as Future<HttpClientRequest>;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<HttpClientRequest> post(String? host, int? port, String? path) =>
|
||||||
|
super.noSuchMethod(Invocation.method(#post, <Object?>[host, port, path]),
|
||||||
|
returnValue: Future<_FakeHttpClientRequest>.value(_FakeHttpClientRequest())) as Future<HttpClientRequest>;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<HttpClientRequest> postUrl(Uri? url) => super.noSuchMethod(Invocation.method(#postUrl, <Object?>[url]),
|
||||||
|
returnValue: Future<_FakeHttpClientRequest>.value(_FakeHttpClientRequest())) as Future<HttpClientRequest>;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<HttpClientRequest> put(String? host, int? port, String? path) =>
|
||||||
|
super.noSuchMethod(Invocation.method(#put, <Object?>[host, port, path]),
|
||||||
|
returnValue: Future<_FakeHttpClientRequest>.value(_FakeHttpClientRequest())) as Future<HttpClientRequest>;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<HttpClientRequest> putUrl(Uri? url) => super.noSuchMethod(Invocation.method(#putUrl, <Object?>[url]),
|
||||||
|
returnValue: Future<_FakeHttpClientRequest>.value(_FakeHttpClientRequest())) as Future<HttpClientRequest>;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<HttpClientRequest> delete(String? host, int? port, String? path) =>
|
||||||
|
super.noSuchMethod(Invocation.method(#delete, <Object?>[host, port, path]),
|
||||||
|
returnValue: Future<_FakeHttpClientRequest>.value(_FakeHttpClientRequest())) as Future<HttpClientRequest>;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<HttpClientRequest> deleteUrl(Uri? url) => super.noSuchMethod(Invocation.method(#deleteUrl, <Object?>[url]),
|
||||||
|
returnValue: Future<_FakeHttpClientRequest>.value(_FakeHttpClientRequest())) as Future<HttpClientRequest>;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<HttpClientRequest> patch(String? host, int? port, String? path) =>
|
||||||
|
super.noSuchMethod(Invocation.method(#patch, <Object?>[host, port, path]),
|
||||||
|
returnValue: Future<_FakeHttpClientRequest>.value(_FakeHttpClientRequest())) as Future<HttpClientRequest>;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<HttpClientRequest> patchUrl(Uri? url) => super.noSuchMethod(Invocation.method(#patchUrl, <Object?>[url]),
|
||||||
|
returnValue: Future<_FakeHttpClientRequest>.value(_FakeHttpClientRequest())) as Future<HttpClientRequest>;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<HttpClientRequest> head(String? host, int? port, String? path) =>
|
||||||
|
super.noSuchMethod(Invocation.method(#head, <Object?>[host, port, path]),
|
||||||
|
returnValue: Future<_FakeHttpClientRequest>.value(_FakeHttpClientRequest())) as Future<HttpClientRequest>;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<HttpClientRequest> headUrl(Uri? url) => super.noSuchMethod(Invocation.method(#headUrl, <Object?>[url]),
|
||||||
|
returnValue: Future<_FakeHttpClientRequest>.value(_FakeHttpClientRequest())) as Future<HttpClientRequest>;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void addCredentials(Uri? url, String? realm, HttpClientCredentials? credentials) =>
|
||||||
|
super.noSuchMethod(Invocation.method(#addCredentials, <Object?>[url, realm, credentials]));
|
||||||
|
|
||||||
|
@override
|
||||||
|
void addProxyCredentials(String? host, int? port, String? realm, HttpClientCredentials? credentials) =>
|
||||||
|
super.noSuchMethod(Invocation.method(#addProxyCredentials, <Object?>[host, port, realm, credentials]));
|
||||||
|
|
||||||
|
@override
|
||||||
|
void close({bool? force = false}) =>
|
||||||
|
super.noSuchMethod(Invocation.method(#close, <Object?>[], <Symbol, Object?>{#force: force}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A class which mocks [HttpClientRequest].
|
||||||
|
///
|
||||||
|
/// See the documentation for Mockito's code generation for more information.
|
||||||
|
class MockHttpClientRequest extends Mock implements HttpClientRequest {
|
||||||
|
MockHttpClientRequest() {
|
||||||
|
throwOnMissingStub(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get persistentConnection =>
|
||||||
|
super.noSuchMethod(Invocation.getter(#persistentConnection), returnValue: false) as bool;
|
||||||
|
|
||||||
|
@override
|
||||||
|
set persistentConnection(bool? persistentConnection) =>
|
||||||
|
super.noSuchMethod(Invocation.setter(#persistentConnection, persistentConnection));
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get followRedirects => super.noSuchMethod(Invocation.getter(#followRedirects), returnValue: false) as bool;
|
||||||
|
|
||||||
|
@override
|
||||||
|
set followRedirects(bool? followRedirects) =>
|
||||||
|
super.noSuchMethod(Invocation.setter(#followRedirects, followRedirects));
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get maxRedirects => super.noSuchMethod(Invocation.getter(#maxRedirects), returnValue: 0) as int;
|
||||||
|
|
||||||
|
@override
|
||||||
|
set maxRedirects(int? maxRedirects) => super.noSuchMethod(Invocation.setter(#maxRedirects, maxRedirects));
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get contentLength => super.noSuchMethod(Invocation.getter(#contentLength), returnValue: 0) as int;
|
||||||
|
|
||||||
|
@override
|
||||||
|
set contentLength(int? contentLength) => super.noSuchMethod(Invocation.setter(#contentLength, contentLength));
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get bufferOutput => super.noSuchMethod(Invocation.getter(#bufferOutput), returnValue: false) as bool;
|
||||||
|
|
||||||
|
@override
|
||||||
|
set bufferOutput(bool? bufferOutput) => super.noSuchMethod(Invocation.setter(#bufferOutput, bufferOutput));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get method => super.noSuchMethod(Invocation.getter(#method), returnValue: '') as String;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Uri get uri => super.noSuchMethod(Invocation.getter(#uri), returnValue: _FakeUri()) as Uri;
|
||||||
|
|
||||||
|
@override
|
||||||
|
HttpHeaders get headers =>
|
||||||
|
super.noSuchMethod(Invocation.getter(#headers), returnValue: _FakeHttpHeaders()) as HttpHeaders;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Cookie> get cookies => super.noSuchMethod(Invocation.getter(#cookies), returnValue: <Cookie>[]) as List<Cookie>;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<HttpClientResponse> get done => super.noSuchMethod(Invocation.getter(#done),
|
||||||
|
returnValue: Future<_FakeHttpClientResponse>.value(_FakeHttpClientResponse())) as Future<HttpClientResponse>;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<HttpClientResponse> close() => super.noSuchMethod(Invocation.method(#close, <Object?>[]),
|
||||||
|
returnValue: Future<_FakeHttpClientResponse>.value(_FakeHttpClientResponse())) as Future<HttpClientResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A class which mocks [HttpClientResponse].
|
||||||
|
///
|
||||||
|
/// See the documentation for Mockito's code generation for more information.
|
||||||
|
class MockHttpClientResponse extends Mock implements HttpClientResponse {
|
||||||
|
MockHttpClientResponse() {
|
||||||
|
throwOnMissingStub(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include an override method for the inherited listen method. This method
|
||||||
|
// intercepts HttpClientResponse listen calls to return a mock image.
|
||||||
|
@override
|
||||||
|
StreamSubscription<List<int>> listen(void Function(List<int> event)? onData,
|
||||||
|
{Function? onError, void Function()? onDone, bool? cancelOnError}) =>
|
||||||
|
super.noSuchMethod(
|
||||||
|
Invocation.method(
|
||||||
|
#listen,
|
||||||
|
<Object?>[onData],
|
||||||
|
<Symbol, Object?>{#onError: onError, #onDone: onDone, #cancelOnError: cancelOnError},
|
||||||
|
),
|
||||||
|
returnValue: _FakeStreamSubscription<List<int>>()) as StreamSubscription<List<int>>;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get statusCode => super.noSuchMethod(Invocation.getter(#statusCode), returnValue: 0) as int;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get reasonPhrase => super.noSuchMethod(Invocation.getter(#reasonPhrase), returnValue: '') as String;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get contentLength => super.noSuchMethod(Invocation.getter(#contentLength), returnValue: 0) as int;
|
||||||
|
|
||||||
|
@override
|
||||||
|
HttpClientResponseCompressionState get compressionState => super.noSuchMethod(Invocation.getter(#compressionState),
|
||||||
|
returnValue: HttpClientResponseCompressionState.notCompressed) as HttpClientResponseCompressionState;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get persistentConnection =>
|
||||||
|
super.noSuchMethod(Invocation.getter(#persistentConnection), returnValue: false) as bool;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isRedirect => super.noSuchMethod(Invocation.getter(#isRedirect), returnValue: false) as bool;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<RedirectInfo> get redirects =>
|
||||||
|
super.noSuchMethod(Invocation.getter(#redirects), returnValue: <RedirectInfo>[]) as List<RedirectInfo>;
|
||||||
|
|
||||||
|
@override
|
||||||
|
HttpHeaders get headers =>
|
||||||
|
super.noSuchMethod(Invocation.getter(#headers), returnValue: _FakeHttpHeaders()) as HttpHeaders;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Cookie> get cookies => super.noSuchMethod(Invocation.getter(#cookies), returnValue: <Cookie>[]) as List<Cookie>;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<HttpClientResponse> redirect([String? method, Uri? url, bool? followLoops]) =>
|
||||||
|
super.noSuchMethod(Invocation.method(#redirect, <Object?>[method, url, followLoops]),
|
||||||
|
returnValue: Future<_FakeHttpClientResponse>.value(_FakeHttpClientResponse())) as Future<HttpClientResponse>;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Socket> detachSocket() => super.noSuchMethod(Invocation.method(#detachSocket, <Object?>[]),
|
||||||
|
returnValue: Future<_FakeSocket>.value(_FakeSocket())) as Future<Socket>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A class which mocks [HttpHeaders].
|
||||||
|
///
|
||||||
|
/// See the documentation for Mockito's code generation for more information.
|
||||||
|
class MockHttpHeaders extends Mock implements HttpHeaders {
|
||||||
|
MockHttpHeaders() {
|
||||||
|
throwOnMissingStub(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get contentLength => super.noSuchMethod(Invocation.getter(#contentLength), returnValue: 0) as int;
|
||||||
|
|
||||||
|
@override
|
||||||
|
set contentLength(int? contentLength) => super.noSuchMethod(Invocation.setter(#contentLength, contentLength));
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get persistentConnection =>
|
||||||
|
super.noSuchMethod(Invocation.getter(#persistentConnection), returnValue: false) as bool;
|
||||||
|
|
||||||
|
@override
|
||||||
|
set persistentConnection(bool? persistentConnection) =>
|
||||||
|
super.noSuchMethod(Invocation.setter(#persistentConnection, persistentConnection));
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get chunkedTransferEncoding =>
|
||||||
|
super.noSuchMethod(Invocation.getter(#chunkedTransferEncoding), returnValue: false) as bool;
|
||||||
|
|
||||||
|
@override
|
||||||
|
set chunkedTransferEncoding(bool? chunkedTransferEncoding) =>
|
||||||
|
super.noSuchMethod(Invocation.setter(#chunkedTransferEncoding, chunkedTransferEncoding));
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String>? operator [](String? name) =>
|
||||||
|
super.noSuchMethod(Invocation.method(#[], <Object?>[name])) as List<String>?;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? value(String? name) => super.noSuchMethod(Invocation.method(#value, <Object?>[name])) as String?;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void add(String? name, Object? value, {bool? preserveHeaderCase = false}) => super.noSuchMethod(
|
||||||
|
Invocation.method(#add, <Object?>[name, value], <Symbol, Object?>{#preserveHeaderCase: preserveHeaderCase}));
|
||||||
|
|
||||||
|
@override
|
||||||
|
void set(String? name, Object? value, {bool? preserveHeaderCase = false}) => super.noSuchMethod(
|
||||||
|
Invocation.method(#set, <Object?>[name, value], <Symbol, Object?>{#preserveHeaderCase: preserveHeaderCase}));
|
||||||
|
|
||||||
|
@override
|
||||||
|
void remove(String? name, Object? value) => super.noSuchMethod(Invocation.method(#remove, <Object?>[name, value]));
|
||||||
|
|
||||||
|
@override
|
||||||
|
void removeAll(String? name) => super.noSuchMethod(Invocation.method(#removeAll, <Object?>[name]));
|
||||||
|
|
||||||
|
@override
|
||||||
|
void forEach(void Function(String, List<String>)? action) =>
|
||||||
|
super.noSuchMethod(Invocation.method(#forEach, <Object?>[action]));
|
||||||
|
|
||||||
|
@override
|
||||||
|
void noFolding(String? name) => super.noSuchMethod(Invocation.method(#noFolding, <Object?>[name]));
|
||||||
|
}
|
||||||
78
flutter_markdown_plus/test/inline_widget_test.dart
Normal file
78
flutter_markdown_plus/test/inline_widget_test.dart
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
|
void main() => defineTests();
|
||||||
|
|
||||||
|
void defineTests() {
|
||||||
|
group('InlineWidget', () {
|
||||||
|
testWidgets(
|
||||||
|
'Test inline widget',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
MarkdownBody(
|
||||||
|
data: 'Hello, foo bar',
|
||||||
|
builders: <String, MarkdownElementBuilder>{
|
||||||
|
'sub': SubscriptBuilder(),
|
||||||
|
},
|
||||||
|
extensionSet: md.ExtensionSet(
|
||||||
|
<md.BlockSyntax>[],
|
||||||
|
<md.InlineSyntax>[SubscriptSyntax()],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Text textWidget = tester.firstWidget(find.byType(Text));
|
||||||
|
final TextSpan span = textWidget.textSpan! as TextSpan;
|
||||||
|
|
||||||
|
final TextSpan part1 = span.children![0] as TextSpan;
|
||||||
|
expect(part1.toPlainText(), 'Hello, ');
|
||||||
|
|
||||||
|
final WidgetSpan part2 = span.children![1] as WidgetSpan;
|
||||||
|
expect(part2.alignment, PlaceholderAlignment.middle);
|
||||||
|
expect(part2.child, isA<Text>());
|
||||||
|
expect((part2.child as Text).data, 'foo');
|
||||||
|
|
||||||
|
final TextSpan part3 = span.children![2] as TextSpan;
|
||||||
|
expect(part3.toPlainText(), ' bar');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubscriptBuilder extends MarkdownElementBuilder {
|
||||||
|
@override
|
||||||
|
Widget visitElementAfterWithContext(
|
||||||
|
BuildContext context,
|
||||||
|
md.Element element,
|
||||||
|
TextStyle? preferredStyle,
|
||||||
|
TextStyle? parentStyle,
|
||||||
|
) {
|
||||||
|
return Text.rich(WidgetSpan(
|
||||||
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
child: Text(element.textContent),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubscriptSyntax extends md.InlineSyntax {
|
||||||
|
SubscriptSyntax() : super(_pattern);
|
||||||
|
|
||||||
|
static const String _pattern = r'(foo)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool onMatch(md.InlineParser parser, Match match) {
|
||||||
|
parser.addNode(md.Element.text('sub', match[1]!));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
357
flutter_markdown_plus/test/line_break_test.dart
Normal file
357
flutter_markdown_plus/test/line_break_test.dart
Normal file
|
|
@ -0,0 +1,357 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
|
void main() => defineTests();
|
||||||
|
|
||||||
|
void defineTests() {
|
||||||
|
group('Hard Line Breaks', () {
|
||||||
|
testWidgets(
|
||||||
|
// Example 654 from GFM.
|
||||||
|
'two spaces at end of line',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = 'foo \nbar';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Finder richTextFinder = find.byType(RichText);
|
||||||
|
expect(richTextFinder, findsOneWidget);
|
||||||
|
|
||||||
|
final RichText richText = richTextFinder.evaluate().first.widget as RichText;
|
||||||
|
final String text = richText.text.toPlainText();
|
||||||
|
expect(text, 'foo\nbar');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
// Example 655 from GFM.
|
||||||
|
'backslash at end of line',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = 'foo\\\nbar';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Finder richTextFinder = find.byType(RichText);
|
||||||
|
expect(richTextFinder, findsOneWidget);
|
||||||
|
|
||||||
|
final RichText richText = richTextFinder.evaluate().first.widget as RichText;
|
||||||
|
final String text = richText.text.toPlainText();
|
||||||
|
expect(text, 'foo\nbar');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
// Example 656 from GFM.
|
||||||
|
'more than two spaces at end of line',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = 'foo \nbar';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Finder richTextFinder = find.byType(RichText);
|
||||||
|
expect(richTextFinder, findsOneWidget);
|
||||||
|
|
||||||
|
final RichText richText = richTextFinder.evaluate().first.widget as RichText;
|
||||||
|
final String text = richText.text.toPlainText();
|
||||||
|
expect(text, 'foo\nbar');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
// Example 657 from GFM.
|
||||||
|
'leading spaces at beginning of next line are ignored',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = 'foo \n bar';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Finder richTextFinder = find.byType(RichText);
|
||||||
|
expect(richTextFinder, findsOneWidget);
|
||||||
|
|
||||||
|
final RichText richText = richTextFinder.evaluate().first.widget as RichText;
|
||||||
|
final String text = richText.text.toPlainText();
|
||||||
|
expect(text, 'foo\nbar');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
// Example 658 from GFM.
|
||||||
|
'leading spaces at beginning of next line are ignored',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = 'foo\\\n bar';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Finder richTextFinder = find.byType(RichText);
|
||||||
|
expect(richTextFinder, findsOneWidget);
|
||||||
|
|
||||||
|
final RichText richText = richTextFinder.evaluate().first.widget as RichText;
|
||||||
|
final String text = richText.text.toPlainText();
|
||||||
|
expect(text, 'foo\nbar');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
// Example 659 from GFM.
|
||||||
|
'two spaces line break inside emphasis',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '*foo \nbar*';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Finder textFinder = find.byType(Text);
|
||||||
|
expect(textFinder, findsOneWidget);
|
||||||
|
|
||||||
|
final Text textWidget = textFinder.evaluate().first.widget as Text;
|
||||||
|
final String text = textWidget.textSpan!.toPlainText();
|
||||||
|
expect(text, 'foo\nbar');
|
||||||
|
|
||||||
|
// There should be three spans of text.
|
||||||
|
final TextSpan textSpan = textWidget.textSpan! as TextSpan;
|
||||||
|
expect(textSpan, isNotNull);
|
||||||
|
expect(textSpan.children!.length == 3, isTrue);
|
||||||
|
|
||||||
|
// First text span has italic style with normal weight.
|
||||||
|
final InlineSpan firstSpan = textSpan.children![0];
|
||||||
|
expectTextSpanStyle(firstSpan as TextSpan, FontStyle.italic, FontWeight.normal);
|
||||||
|
|
||||||
|
// Second span is just the newline character with no font style or weight.
|
||||||
|
|
||||||
|
// Third text span has italic style with normal weight.
|
||||||
|
final InlineSpan thirdSpan = textSpan.children![2];
|
||||||
|
expectTextSpanStyle(thirdSpan as TextSpan, FontStyle.italic, FontWeight.normal);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
// Example 660 from GFM.
|
||||||
|
'backslash line break inside emphasis',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '*foo\\\nbar*';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Finder textFinder = find.byType(Text);
|
||||||
|
expect(textFinder, findsOneWidget);
|
||||||
|
|
||||||
|
final Text textWidget = textFinder.evaluate().first.widget as Text;
|
||||||
|
final String text = textWidget.textSpan!.toPlainText();
|
||||||
|
expect(text, 'foo\nbar');
|
||||||
|
|
||||||
|
// There should be three spans of text.
|
||||||
|
final TextSpan textSpan = textWidget.textSpan! as TextSpan;
|
||||||
|
expect(textSpan, isNotNull);
|
||||||
|
expect(textSpan.children!.length == 3, isTrue);
|
||||||
|
|
||||||
|
// First text span has italic style with normal weight.
|
||||||
|
final InlineSpan firstSpan = textSpan.children![0];
|
||||||
|
expectTextSpanStyle(firstSpan as TextSpan, FontStyle.italic, FontWeight.normal);
|
||||||
|
|
||||||
|
// Second span is just the newline character with no font style or weight.
|
||||||
|
|
||||||
|
// Third text span has italic style with normal weight.
|
||||||
|
final InlineSpan thirdSpan = textSpan.children![2];
|
||||||
|
expectTextSpanStyle(thirdSpan as TextSpan, FontStyle.italic, FontWeight.normal);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
// Example 661 from GFM.
|
||||||
|
'two space line break does not occur in code span',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '`code \nspan`';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Finder textFinder = find.byType(Text);
|
||||||
|
expect(textFinder, findsOneWidget);
|
||||||
|
|
||||||
|
final Text textWidget = textFinder.evaluate().first.widget as Text;
|
||||||
|
final String text = textWidget.textSpan!.toPlainText();
|
||||||
|
expect(text, 'code span');
|
||||||
|
|
||||||
|
final TextSpan textSpan = textWidget.textSpan! as TextSpan;
|
||||||
|
expect(textSpan, isNotNull);
|
||||||
|
expect(textSpan.style, isNotNull);
|
||||||
|
expect(textSpan.style!.fontFamily == 'monospace', isTrue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
// Example 662 from GFM.
|
||||||
|
'backslash line break does not occur in code span',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '`code\\\nspan`';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Finder textFinder = find.byType(Text);
|
||||||
|
expect(textFinder, findsOneWidget);
|
||||||
|
|
||||||
|
final Text textWidget = textFinder.evaluate().first.widget as Text;
|
||||||
|
final String text = textWidget.textSpan!.toPlainText();
|
||||||
|
expect(text, r'code\ span');
|
||||||
|
|
||||||
|
final TextSpan textSpan = textWidget.textSpan! as TextSpan;
|
||||||
|
expect(textSpan, isNotNull);
|
||||||
|
expect(textSpan.style, isNotNull);
|
||||||
|
expect(textSpan.style!.fontFamily == 'monospace', isTrue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
// Example 665 from GFM.
|
||||||
|
'backslash at end of paragraph is ignored',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = r'foo\';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Finder richTextFinder = find.byType(RichText);
|
||||||
|
expect(richTextFinder, findsOneWidget);
|
||||||
|
|
||||||
|
final RichText richText = richTextFinder.evaluate().first.widget as RichText;
|
||||||
|
final String text = richText.text.toPlainText();
|
||||||
|
expect(text, r'foo\');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
// Example 666 from GFM.
|
||||||
|
'two spaces at end of paragraph is ignored',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = 'foo ';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Finder richTextFinder = find.byType(RichText);
|
||||||
|
expect(richTextFinder, findsOneWidget);
|
||||||
|
|
||||||
|
final RichText richText = richTextFinder.evaluate().first.widget as RichText;
|
||||||
|
final String text = richText.text.toPlainText();
|
||||||
|
expect(text, 'foo');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
// Example 667 from GFM.
|
||||||
|
'backslash at end of header is ignored',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = r'### foo\';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Finder richTextFinder = find.byType(RichText);
|
||||||
|
expect(richTextFinder, findsOneWidget);
|
||||||
|
|
||||||
|
final RichText richText = richTextFinder.evaluate().first.widget as RichText;
|
||||||
|
final String text = richText.text.toPlainText();
|
||||||
|
expect(text, r'foo\');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
// Example 668 from GFM.
|
||||||
|
'two spaces at end of header is ignored',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '### foo ';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Finder richTextFinder = find.byType(RichText);
|
||||||
|
expect(richTextFinder, findsOneWidget);
|
||||||
|
|
||||||
|
final RichText richText = richTextFinder.evaluate().first.widget as RichText;
|
||||||
|
final String text = richText.text.toPlainText();
|
||||||
|
expect(text, 'foo');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Soft Line Breaks', () {
|
||||||
|
testWidgets(
|
||||||
|
// Example 669 from GFM.
|
||||||
|
'lines of text in paragraph',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = 'foo\nbaz';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Finder richTextFinder = find.byType(RichText);
|
||||||
|
expect(richTextFinder, findsOneWidget);
|
||||||
|
|
||||||
|
final RichText richText = richTextFinder.evaluate().first.widget as RichText;
|
||||||
|
final String text = richText.text.toPlainText();
|
||||||
|
expect(text, 'foo baz');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
// Example 670 from GFM.
|
||||||
|
'spaces at beginning and end of lines of text in paragraph are removed',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = 'foo \n baz';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Finder richTextFinder = find.byType(RichText);
|
||||||
|
expect(richTextFinder, findsOneWidget);
|
||||||
|
|
||||||
|
final RichText richText = richTextFinder.evaluate().first.widget as RichText;
|
||||||
|
final String text = richText.text.toPlainText();
|
||||||
|
expect(text, 'foo baz');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
2498
flutter_markdown_plus/test/link_test.dart
Normal file
2498
flutter_markdown_plus/test/link_test.dart
Normal file
File diff suppressed because it is too large
Load diff
302
flutter_markdown_plus/test/list_test.dart
Normal file
302
flutter_markdown_plus/test/list_test.dart
Normal file
|
|
@ -0,0 +1,302 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
|
void main() => defineTests();
|
||||||
|
|
||||||
|
void defineTests() {
|
||||||
|
group('Unordered List', () {
|
||||||
|
testWidgets(
|
||||||
|
'simple 3 item list',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '- Item 1\n- Item 2\n- Item 3';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
expectTextStrings(widgets, <String>[
|
||||||
|
'•',
|
||||||
|
'Item 1',
|
||||||
|
'•',
|
||||||
|
'Item 2',
|
||||||
|
'•',
|
||||||
|
'Item 3',
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'empty list item',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '- \n- Item 2\n- Item 3';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
expectTextStrings(widgets, <String>[
|
||||||
|
'•',
|
||||||
|
'•',
|
||||||
|
'Item 2',
|
||||||
|
'•',
|
||||||
|
'Item 3',
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
// Example 236 from the GitHub Flavored Markdown specification.
|
||||||
|
'leading space are ignored', (WidgetTester tester) async {
|
||||||
|
const String data = ' - one\n\n two';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
expectTextStrings(widgets, <String>[
|
||||||
|
'•',
|
||||||
|
'one',
|
||||||
|
'two',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'leading spaces are ignored (non-paragraph test case)',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '- one\n- two\n- three';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
expectTextStrings(widgets, <String>[
|
||||||
|
'•',
|
||||||
|
'one',
|
||||||
|
'•',
|
||||||
|
'two',
|
||||||
|
'•',
|
||||||
|
'three',
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('custom bullet builder', (WidgetTester tester) async {
|
||||||
|
const String data = '* Item 1\n * Item 2\n * Item 3\n * Item 4\n* Item 5';
|
||||||
|
Widget builder(MarkdownBulletParameters parameters) => Text(
|
||||||
|
'${parameters.index} ${parameters.style == BulletStyle.orderedList ? 'ordered' : 'unordered'} ${parameters.nestLevel}',
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
Markdown(data: data, bulletBuilder: builder),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
|
||||||
|
expectTextStrings(widgets, <String>[
|
||||||
|
'0 unordered 0',
|
||||||
|
'Item 1',
|
||||||
|
'0 unordered 1',
|
||||||
|
'Item 2',
|
||||||
|
'0 unordered 2',
|
||||||
|
'Item 3',
|
||||||
|
'1 unordered 1',
|
||||||
|
'Item 4',
|
||||||
|
'1 unordered 0',
|
||||||
|
'Item 5',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Ordered List', () {
|
||||||
|
testWidgets(
|
||||||
|
'2 distinct ordered lists with separate index values',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '1. Item 1\n1. Item 2\n2. Item 3\n\n\n'
|
||||||
|
'10. Item 10\n13. Item 11';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
expectTextStrings(
|
||||||
|
widgets, <String>['1.', 'Item 1', '2.', 'Item 2', '3.', 'Item 3', '4.', 'Item 10', '5.', 'Item 11']);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('leading space are ignored', (WidgetTester tester) async {
|
||||||
|
const String data = ' 1. one\n\n two';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
expectTextStrings(widgets, <String>['1.', 'one', 'two']);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('custom bullet builder', (WidgetTester tester) async {
|
||||||
|
const String data = '1. Item 1\n 1. Item 2\n 1. Item 3\n 1. Item 4\n1. Item 5';
|
||||||
|
Widget builder(MarkdownBulletParameters parameters) => Text(
|
||||||
|
'${parameters.index} ${parameters.style == BulletStyle.orderedList ? 'ordered' : 'unordered'} ${parameters.nestLevel}',
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
Markdown(data: data, bulletBuilder: builder),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
|
||||||
|
expectTextStrings(widgets, <String>[
|
||||||
|
'0 ordered 0',
|
||||||
|
'Item 1',
|
||||||
|
'0 ordered 1',
|
||||||
|
'Item 2',
|
||||||
|
'0 ordered 2',
|
||||||
|
'Item 3',
|
||||||
|
'1 ordered 1',
|
||||||
|
'Item 4',
|
||||||
|
'1 ordered 0',
|
||||||
|
'Item 5',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Task List', () {
|
||||||
|
testWidgets(
|
||||||
|
'simple 2 item task list',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '- [x] Item 1\n- [ ] Item 2';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
|
||||||
|
expectTextStrings(widgets, <String>[
|
||||||
|
String.fromCharCode(Icons.check_box.codePoint),
|
||||||
|
'Item 1',
|
||||||
|
String.fromCharCode(Icons.check_box_outline_blank.codePoint),
|
||||||
|
'Item 2',
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('custom bullet builder', (WidgetTester tester) async {
|
||||||
|
const String data = '* Item 1\n* Item 2\n1) Item 3\n2) Item 4';
|
||||||
|
Widget builder(MarkdownBulletParameters parameters) =>
|
||||||
|
Text('${parameters.index} ${parameters.style == BulletStyle.orderedList ? 'ordered' : 'unordered'}');
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
Markdown(data: data, bulletBuilder: builder),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
|
||||||
|
expectTextStrings(widgets, <String>[
|
||||||
|
'0 unordered',
|
||||||
|
'Item 1',
|
||||||
|
'1 unordered',
|
||||||
|
'Item 2',
|
||||||
|
'0 ordered',
|
||||||
|
'Item 3',
|
||||||
|
'1 ordered',
|
||||||
|
'Item 4',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'custom checkbox builder',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '- [x] Item 1\n- [ ] Item 2';
|
||||||
|
Widget builder(bool checked) => Text('$checked');
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
Markdown(data: data, checkboxBuilder: builder),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
|
||||||
|
expectTextStrings(widgets, <String>[
|
||||||
|
'true',
|
||||||
|
'Item 1',
|
||||||
|
'false',
|
||||||
|
'Item 2',
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('fitContent', () {
|
||||||
|
testWidgets(
|
||||||
|
'uses maximum width when false',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '- Foo\n- Bar';
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const Column(
|
||||||
|
children: <Widget>[
|
||||||
|
MarkdownBody(fitContent: false, data: data),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final double screenWidth = find.byType(Column).evaluate().first.size!.width;
|
||||||
|
final double markdownBodyWidth = find.byType(MarkdownBody).evaluate().single.size!.width;
|
||||||
|
|
||||||
|
expect(markdownBodyWidth, equals(screenWidth));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'uses minimum width when true',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '- Foo\n- Bar';
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const Column(
|
||||||
|
children: <Widget>[
|
||||||
|
MarkdownBody(data: data),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final double screenWidth = find.byType(Column).evaluate().first.size!.width;
|
||||||
|
final double markdownBodyWidth = find.byType(MarkdownBody).evaluate().single.size!.width;
|
||||||
|
|
||||||
|
expect(markdownBodyWidth, lessThan(screenWidth));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
|
void main() => defineTests();
|
||||||
|
|
||||||
|
void defineTests() {
|
||||||
|
group('MarkdownBody shrinkWrap test', () {
|
||||||
|
testWidgets(
|
||||||
|
'Given a MarkdownBody with shrinkWrap=true '
|
||||||
|
'Then it wraps its content',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(boilerplate(
|
||||||
|
const Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
Text('shrinkWrap=true'),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: MarkdownBody(
|
||||||
|
data: 'This is a [link](https://flutter.dev/)',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
final Rect stackRect = tester.getRect(find.byType(Stack));
|
||||||
|
final Rect textRect = tester.getRect(find.text('shrinkWrap=true'));
|
||||||
|
final Rect markdownBodyRect = tester.getRect(find.byType(MarkdownBody));
|
||||||
|
|
||||||
|
// The Text should be on the top of the Stack
|
||||||
|
expect(textRect.top, equals(stackRect.top));
|
||||||
|
expect(textRect.bottom, lessThan(stackRect.bottom));
|
||||||
|
// The MarkdownBody should be on the bottom of the Stack
|
||||||
|
expect(markdownBodyRect.top, greaterThan(stackRect.top));
|
||||||
|
expect(markdownBodyRect.bottom, equals(stackRect.bottom));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
testWidgets(
|
||||||
|
'Given a MarkdownBody with shrinkWrap=false '
|
||||||
|
'Then it expands to the maximum allowed height',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(boilerplate(
|
||||||
|
const Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
Text('shrinkWrap=false test'),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: MarkdownBody(
|
||||||
|
data: 'This is a [link](https://flutter.dev/)',
|
||||||
|
shrinkWrap: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
final Rect stackRect = tester.getRect(find.byType(Stack));
|
||||||
|
final Rect textRect = tester.getRect(find.text('shrinkWrap=false test'));
|
||||||
|
final Rect markdownBodyRect = tester.getRect(find.byType(MarkdownBody));
|
||||||
|
|
||||||
|
// The Text should be on the top of the Stack
|
||||||
|
expect(textRect.top, equals(stackRect.top));
|
||||||
|
expect(textRect.bottom, lessThan(stackRect.bottom));
|
||||||
|
// The MarkdownBody should take all Stack's height
|
||||||
|
expect(markdownBodyRect.top, equals(stackRect.top));
|
||||||
|
expect(markdownBodyRect.bottom, equals(stackRect.bottom));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
67
flutter_markdown_plus/test/padding_test.dart
Normal file
67
flutter_markdown_plus/test/padding_test.dart
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
|
void main() => defineTests();
|
||||||
|
|
||||||
|
void defineTests() {
|
||||||
|
group('Padding builders', () {
|
||||||
|
testWidgets(
|
||||||
|
'use paddingBuilders for p',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const double paddingX = 10.0;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
Markdown(
|
||||||
|
data: '**line 1**\n\n# H1\n',
|
||||||
|
paddingBuilders: <String, MarkdownPaddingBuilder>{
|
||||||
|
'p': CustomPaddingBuilder(paddingX * 1),
|
||||||
|
'strong': CustomPaddingBuilder(paddingX * 2),
|
||||||
|
'h1': CustomPaddingBuilder(paddingX * 3),
|
||||||
|
'img': CustomPaddingBuilder(paddingX * 4),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<Padding> paddings = tester.widgetList<Padding>(find.byType(Padding)).toList();
|
||||||
|
|
||||||
|
expect(paddings.length, 4);
|
||||||
|
expect(
|
||||||
|
paddings[0].padding.along(Axis.horizontal) == paddingX * 1 * 2,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
paddings[1].padding.along(Axis.horizontal) == paddingX * 3 * 2,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
paddings[2].padding.along(Axis.horizontal) == paddingX * 1 * 2,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
paddings[3].padding.along(Axis.horizontal) == paddingX * 4 * 2,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
imageCache.clear();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomPaddingBuilder extends MarkdownPaddingBuilder {
|
||||||
|
CustomPaddingBuilder(this.paddingX);
|
||||||
|
|
||||||
|
double paddingX;
|
||||||
|
|
||||||
|
@override
|
||||||
|
EdgeInsets getPadding() {
|
||||||
|
return EdgeInsets.symmetric(horizontal: paddingX);
|
||||||
|
}
|
||||||
|
}
|
||||||
195
flutter_markdown_plus/test/scrollable_test.dart
Normal file
195
flutter_markdown_plus/test/scrollable_test.dart
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
|
void main() => defineTests();
|
||||||
|
|
||||||
|
void defineTests() {
|
||||||
|
group('Scrollable', () {
|
||||||
|
testWidgets(
|
||||||
|
'code block',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = "```\nvoid main() {\n print('Hello World!');\n}\n```";
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MediaQuery(
|
||||||
|
data: MediaQueryData(),
|
||||||
|
child: MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
final Iterable<SingleChildScrollView> scrollViews = widgets.whereType<SingleChildScrollView>();
|
||||||
|
expect(scrollViews, isNotEmpty);
|
||||||
|
expect(scrollViews.first.controller, isNotNull);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'two code blocks use different scroll controllers',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = "```\nvoid main() {\n print('Hello World!');\n}\n```"
|
||||||
|
'\n'
|
||||||
|
"```\nvoid main() {\n print('Hello World!');\n}\n```";
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MediaQuery(
|
||||||
|
data: MediaQueryData(),
|
||||||
|
child: MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
final Iterable<SingleChildScrollView> scrollViews = widgets.whereType<SingleChildScrollView>();
|
||||||
|
expect(scrollViews, hasLength(2));
|
||||||
|
expect(scrollViews.first.controller, isNotNull);
|
||||||
|
expect(scrollViews.last.controller, isNotNull);
|
||||||
|
expect(scrollViews.first.controller, isNot(equals(scrollViews.last.controller)));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'controller',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ScrollController controller = ScrollController(
|
||||||
|
initialScrollOffset: 209.0,
|
||||||
|
);
|
||||||
|
addTearDown(controller.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
Markdown(controller: controller, data: ''),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
double realOffset() {
|
||||||
|
return tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(controller.offset, equals(209.0));
|
||||||
|
expect(realOffset(), equals(controller.offset));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Scrollable wrapping',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const Markdown(data: ''),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<Widget> widgets = selfAndDescendantWidgetsOf(
|
||||||
|
find.byType(Markdown),
|
||||||
|
tester,
|
||||||
|
).toList();
|
||||||
|
expectWidgetTypes(widgets.take(2), <Type>[
|
||||||
|
Markdown,
|
||||||
|
ListView,
|
||||||
|
]);
|
||||||
|
expectWidgetTypes(widgets.reversed.take(2).toList().reversed, <Type>[
|
||||||
|
SliverPadding,
|
||||||
|
SliverList,
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'table with fixed column width',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '|Header 1|Header 2|Header 3|'
|
||||||
|
'\n|-----|-----|-----|'
|
||||||
|
'\n|Col 1|Col 2|Col 3|';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
MediaQuery(
|
||||||
|
data: const MediaQueryData(),
|
||||||
|
child: MarkdownBody(
|
||||||
|
data: data,
|
||||||
|
styleSheet: MarkdownStyleSheet(
|
||||||
|
tableColumnWidth: const FixedColumnWidth(150),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
final Iterable<SingleChildScrollView> scrollViews = widgets.whereType<SingleChildScrollView>();
|
||||||
|
expect(scrollViews, isNotEmpty);
|
||||||
|
expect(scrollViews.first.controller, isNotNull);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'table with intrinsic column width',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '|Header 1|Header 2|Header 3|'
|
||||||
|
'\n|-----|-----|-----|'
|
||||||
|
'\n|Col 1|Col 2|Col 3|';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
MediaQuery(
|
||||||
|
data: const MediaQueryData(),
|
||||||
|
child: MarkdownBody(
|
||||||
|
data: data,
|
||||||
|
styleSheet: MarkdownStyleSheet(
|
||||||
|
tableColumnWidth: const IntrinsicColumnWidth(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
final Iterable<SingleChildScrollView> scrollViews = widgets.whereType<SingleChildScrollView>();
|
||||||
|
expect(scrollViews, isNotEmpty);
|
||||||
|
expect(scrollViews.first.controller, isNotNull);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'two tables use different scroll controllers',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '|Header 1|Header 2|Header 3|'
|
||||||
|
'\n|-----|-----|-----|'
|
||||||
|
'\n|Col 1|Col 2|Col 3|'
|
||||||
|
'\n'
|
||||||
|
'\n|Header 1|Header 2|Header 3|'
|
||||||
|
'\n|-----|-----|-----|'
|
||||||
|
'\n|Col 1|Col 2|Col 3|';
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
MediaQuery(
|
||||||
|
data: const MediaQueryData(),
|
||||||
|
child: MarkdownBody(
|
||||||
|
data: data,
|
||||||
|
styleSheet: MarkdownStyleSheet(
|
||||||
|
tableColumnWidth: const FixedColumnWidth(150),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
final Iterable<SingleChildScrollView> scrollViews = widgets.whereType<SingleChildScrollView>();
|
||||||
|
expect(scrollViews, hasLength(2));
|
||||||
|
expect(scrollViews.first.controller, isNotNull);
|
||||||
|
expect(scrollViews.last.controller, isNotNull);
|
||||||
|
expect(scrollViews.first.controller, isNot(equals(scrollViews.last.controller)));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() => defineTests();
|
||||||
|
|
||||||
|
void defineTests() {
|
||||||
|
group('Compatible with SelectionArea when selectable is default to false', () {
|
||||||
|
testWidgets(
|
||||||
|
'Text can be selected',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
SelectedContent? content;
|
||||||
|
|
||||||
|
const String data = 'How are you?';
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
home: SelectionArea(
|
||||||
|
child: const Markdown(
|
||||||
|
data: data,
|
||||||
|
),
|
||||||
|
onSelectionChanged: (SelectedContent? selectedContent) => content = selectedContent,
|
||||||
|
)));
|
||||||
|
|
||||||
|
final TestGesture gesture =
|
||||||
|
await tester.startGesture(tester.getTopLeft(find.text('How are you?')), kind: PointerDeviceKind.mouse);
|
||||||
|
addTearDown(gesture.removePointer);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await gesture.moveTo(tester.getBottomRight(find.text('How are you?')));
|
||||||
|
await gesture.up();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(content, isNotNull);
|
||||||
|
expect(content!.plainText, 'How are you?');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'List can be selected',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
SelectedContent? content;
|
||||||
|
|
||||||
|
const String data = '- Item 1\n- Item 2\n- Item 3';
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
home: SelectionArea(
|
||||||
|
child: const Markdown(
|
||||||
|
data: data,
|
||||||
|
),
|
||||||
|
onSelectionChanged: (SelectedContent? selectedContent) => content = selectedContent,
|
||||||
|
)));
|
||||||
|
|
||||||
|
final TestGesture gesture =
|
||||||
|
await tester.startGesture(tester.getTopLeft(find.byType(Markdown)), kind: PointerDeviceKind.mouse);
|
||||||
|
addTearDown(gesture.removePointer);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await gesture.moveTo(tester.getBottomRight(find.byType(Markdown)));
|
||||||
|
await gesture.up();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(content, isNotNull);
|
||||||
|
expect(content!.plainText, '•Item 1•Item 2•Item 3');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
445
flutter_markdown_plus/test/style_sheet_test.dart
Normal file
445
flutter_markdown_plus/test/style_sheet_test.dart
Normal file
|
|
@ -0,0 +1,445 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
|
void main() => defineTests();
|
||||||
|
|
||||||
|
void defineTests() {
|
||||||
|
group('Style Sheet', () {
|
||||||
|
testWidgets(
|
||||||
|
'equality - Cupertino',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const CupertinoThemeData theme = CupertinoThemeData(brightness: Brightness.light);
|
||||||
|
|
||||||
|
final MarkdownStyleSheet style1 = MarkdownStyleSheet.fromCupertinoTheme(theme);
|
||||||
|
final MarkdownStyleSheet style2 = MarkdownStyleSheet.fromCupertinoTheme(theme);
|
||||||
|
expect(style1, equals(style2));
|
||||||
|
expect(style1.hashCode, equals(style2.hashCode));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'equality - Material',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
|
||||||
|
final MarkdownStyleSheet style1 = MarkdownStyleSheet.fromTheme(theme);
|
||||||
|
final MarkdownStyleSheet style2 = MarkdownStyleSheet.fromTheme(theme);
|
||||||
|
expect(style1, equals(style2));
|
||||||
|
expect(style1.hashCode, equals(style2.hashCode));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'MarkdownStyleSheet.fromCupertinoTheme',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const CupertinoThemeData cTheme = CupertinoThemeData(
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
);
|
||||||
|
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet.fromCupertinoTheme(cTheme);
|
||||||
|
|
||||||
|
// a
|
||||||
|
expect(style.a!.color, CupertinoColors.link.darkColor);
|
||||||
|
expect(style.a!.fontSize, cTheme.textTheme.textStyle.fontSize);
|
||||||
|
|
||||||
|
// p
|
||||||
|
expect(style.p, cTheme.textTheme.textStyle);
|
||||||
|
|
||||||
|
// code
|
||||||
|
expect(style.code!.color, cTheme.textTheme.textStyle.color);
|
||||||
|
expect(style.code!.fontSize, cTheme.textTheme.textStyle.fontSize! * 0.85);
|
||||||
|
expect(style.code!.fontFamily, 'monospace');
|
||||||
|
|
||||||
|
// H1
|
||||||
|
expect(style.h1!.color, cTheme.textTheme.textStyle.color);
|
||||||
|
expect(style.h1!.fontSize, cTheme.textTheme.textStyle.fontSize! + 10);
|
||||||
|
expect(style.h1!.fontWeight, FontWeight.w500);
|
||||||
|
|
||||||
|
// H2
|
||||||
|
expect(style.h2!.color, cTheme.textTheme.textStyle.color);
|
||||||
|
expect(style.h2!.fontSize, cTheme.textTheme.textStyle.fontSize! + 8);
|
||||||
|
expect(style.h2!.fontWeight, FontWeight.w500);
|
||||||
|
|
||||||
|
// H3
|
||||||
|
expect(style.h3!.color, cTheme.textTheme.textStyle.color);
|
||||||
|
expect(style.h3!.fontSize, cTheme.textTheme.textStyle.fontSize! + 6);
|
||||||
|
expect(style.h3!.fontWeight, FontWeight.w500);
|
||||||
|
|
||||||
|
// H4
|
||||||
|
expect(style.h4!.color, cTheme.textTheme.textStyle.color);
|
||||||
|
expect(style.h4!.fontSize, cTheme.textTheme.textStyle.fontSize! + 4);
|
||||||
|
expect(style.h4!.fontWeight, FontWeight.w500);
|
||||||
|
|
||||||
|
// H5
|
||||||
|
expect(style.h5!.color, cTheme.textTheme.textStyle.color);
|
||||||
|
expect(style.h5!.fontSize, cTheme.textTheme.textStyle.fontSize! + 2);
|
||||||
|
expect(style.h5!.fontWeight, FontWeight.w500);
|
||||||
|
|
||||||
|
// H6
|
||||||
|
expect(style.h6!.color, cTheme.textTheme.textStyle.color);
|
||||||
|
expect(style.h6!.fontSize, cTheme.textTheme.textStyle.fontSize);
|
||||||
|
expect(style.h6!.fontWeight, FontWeight.w500);
|
||||||
|
|
||||||
|
// em
|
||||||
|
expect(style.em!.color, cTheme.textTheme.textStyle.color);
|
||||||
|
expect(style.em!.fontSize, cTheme.textTheme.textStyle.fontSize);
|
||||||
|
expect(style.em!.fontStyle, FontStyle.italic);
|
||||||
|
|
||||||
|
// strong
|
||||||
|
expect(style.strong!.color, cTheme.textTheme.textStyle.color);
|
||||||
|
expect(style.strong!.fontSize, cTheme.textTheme.textStyle.fontSize);
|
||||||
|
expect(style.strong!.fontWeight, FontWeight.bold);
|
||||||
|
|
||||||
|
// del
|
||||||
|
expect(style.del!.color, cTheme.textTheme.textStyle.color);
|
||||||
|
expect(style.del!.fontSize, cTheme.textTheme.textStyle.fontSize);
|
||||||
|
expect(style.del!.decoration, TextDecoration.lineThrough);
|
||||||
|
|
||||||
|
// blockqoute
|
||||||
|
expect(style.blockquote, cTheme.textTheme.textStyle);
|
||||||
|
|
||||||
|
// img
|
||||||
|
expect(style.img, cTheme.textTheme.textStyle);
|
||||||
|
|
||||||
|
// checkbox
|
||||||
|
expect(style.checkbox!.color, cTheme.primaryColor);
|
||||||
|
expect(style.checkbox!.fontSize, cTheme.textTheme.textStyle.fontSize);
|
||||||
|
|
||||||
|
// tableHead
|
||||||
|
expect(style.tableHead!.color, cTheme.textTheme.textStyle.color);
|
||||||
|
expect(style.tableHead!.fontSize, cTheme.textTheme.textStyle.fontSize);
|
||||||
|
expect(style.tableHead!.fontWeight, FontWeight.w600);
|
||||||
|
|
||||||
|
// tableBody
|
||||||
|
expect(style.tableBody, cTheme.textTheme.textStyle);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'MarkdownStyleSheet.fromTheme',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.dark().copyWith(
|
||||||
|
textTheme: const TextTheme(
|
||||||
|
bodyMedium: TextStyle(fontSize: 12.0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme);
|
||||||
|
|
||||||
|
// a
|
||||||
|
expect(style.a!.color, Colors.blue);
|
||||||
|
|
||||||
|
// p
|
||||||
|
expect(style.p, theme.textTheme.bodyMedium);
|
||||||
|
|
||||||
|
// code
|
||||||
|
expect(style.code!.color, theme.textTheme.bodyMedium!.color);
|
||||||
|
expect(style.code!.fontSize, theme.textTheme.bodyMedium!.fontSize! * 0.85);
|
||||||
|
expect(style.code!.fontFamily, 'monospace');
|
||||||
|
expect(style.code!.backgroundColor, theme.cardTheme.color);
|
||||||
|
|
||||||
|
// H1
|
||||||
|
expect(style.h1, theme.textTheme.headlineSmall);
|
||||||
|
|
||||||
|
// H2
|
||||||
|
expect(style.h2, theme.textTheme.titleLarge);
|
||||||
|
|
||||||
|
// H3
|
||||||
|
expect(style.h3, theme.textTheme.titleMedium);
|
||||||
|
|
||||||
|
// H4
|
||||||
|
expect(style.h4, theme.textTheme.bodyLarge);
|
||||||
|
|
||||||
|
// H5
|
||||||
|
expect(style.h5, theme.textTheme.bodyLarge);
|
||||||
|
|
||||||
|
// H6
|
||||||
|
expect(style.h6, theme.textTheme.bodyLarge);
|
||||||
|
|
||||||
|
// em
|
||||||
|
expect(style.em!.fontStyle, FontStyle.italic);
|
||||||
|
expect(style.em!.color, theme.textTheme.bodyMedium!.color);
|
||||||
|
|
||||||
|
// strong
|
||||||
|
expect(style.strong!.fontWeight, FontWeight.bold);
|
||||||
|
expect(style.strong!.color, theme.textTheme.bodyMedium!.color);
|
||||||
|
|
||||||
|
// del
|
||||||
|
expect(style.del!.decoration, TextDecoration.lineThrough);
|
||||||
|
expect(style.del!.color, theme.textTheme.bodyMedium!.color);
|
||||||
|
|
||||||
|
// blockqoute
|
||||||
|
expect(style.blockquote, theme.textTheme.bodyMedium);
|
||||||
|
|
||||||
|
// img
|
||||||
|
expect(style.img, theme.textTheme.bodyMedium);
|
||||||
|
|
||||||
|
// checkbox
|
||||||
|
expect(style.checkbox!.color, theme.primaryColor);
|
||||||
|
expect(style.checkbox!.fontSize, theme.textTheme.bodyMedium!.fontSize);
|
||||||
|
|
||||||
|
// tableHead
|
||||||
|
expect(style.tableHead!.fontWeight, FontWeight.w600);
|
||||||
|
|
||||||
|
// tableBody
|
||||||
|
expect(style.tableBody, theme.textTheme.bodyMedium);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'merge 2 style sheets',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
final MarkdownStyleSheet style1 = MarkdownStyleSheet.fromTheme(theme);
|
||||||
|
final MarkdownStyleSheet style2 = MarkdownStyleSheet(
|
||||||
|
p: const TextStyle(color: Colors.red),
|
||||||
|
blockquote: const TextStyle(fontSize: 16),
|
||||||
|
);
|
||||||
|
|
||||||
|
final MarkdownStyleSheet merged = style1.merge(style2);
|
||||||
|
expect(merged.p!.color, Colors.red);
|
||||||
|
expect(merged.blockquote!.fontSize, 16);
|
||||||
|
expect(merged.blockquote!.color, theme.textTheme.bodyMedium!.color);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'create based on which theme',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '[title](url)';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const Markdown(
|
||||||
|
data: data,
|
||||||
|
styleSheetTheme: MarkdownStyleSheetBaseTheme.cupertino,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Text widget = tester.widget(find.byType(Text));
|
||||||
|
expect(widget.textSpan!.style!.color, CupertinoColors.link.color);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'apply 2 distinct style sheets',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
|
||||||
|
final MarkdownStyleSheet style1 = MarkdownStyleSheet.fromTheme(theme);
|
||||||
|
final MarkdownStyleSheet style2 = MarkdownStyleSheet.largeFromTheme(theme);
|
||||||
|
expect(style1, isNot(style2));
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
Markdown(
|
||||||
|
data: '# Test',
|
||||||
|
styleSheet: style1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final RichText text1 = tester.widget(find.byType(RichText));
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
Markdown(
|
||||||
|
data: '# Test',
|
||||||
|
styleSheet: style2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final RichText text2 = tester.widget(find.byType(RichText));
|
||||||
|
|
||||||
|
expect(text1.text, isNot(text2.text));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'use stylesheet option listBulletPadding',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const double paddingX = 20.0;
|
||||||
|
final MarkdownStyleSheet style =
|
||||||
|
MarkdownStyleSheet(listBulletPadding: const EdgeInsets.symmetric(horizontal: paddingX));
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
Markdown(
|
||||||
|
data: '1. Bullet\n 2. Bullet\n * Bullet',
|
||||||
|
styleSheet: style,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<Padding> paddings = tester.widgetList<Padding>(find.byType(Padding)).toList();
|
||||||
|
|
||||||
|
expect(paddings.length, 3);
|
||||||
|
expect(
|
||||||
|
paddings.every(
|
||||||
|
(Padding p) => p.padding.along(Axis.horizontal) == paddingX * 2,
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'check widgets for use stylesheet option h1Padding',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '# Header';
|
||||||
|
const double paddingX = 20.0;
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet(
|
||||||
|
h1Padding: const EdgeInsets.symmetric(horizontal: paddingX),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(boilerplate(MarkdownBody(
|
||||||
|
data: data,
|
||||||
|
styleSheet: style,
|
||||||
|
)));
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = selfAndDescendantWidgetsOf(
|
||||||
|
find.byType(MarkdownBody),
|
||||||
|
tester,
|
||||||
|
);
|
||||||
|
expectWidgetTypes(widgets, <Type>[
|
||||||
|
MarkdownBody,
|
||||||
|
Column,
|
||||||
|
Padding,
|
||||||
|
Wrap,
|
||||||
|
Text,
|
||||||
|
RichText,
|
||||||
|
]);
|
||||||
|
expectTextStrings(widgets, <String>['Header']);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'use stylesheet option pPadding',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const double paddingX = 20.0;
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet(
|
||||||
|
pPadding: const EdgeInsets.symmetric(horizontal: paddingX),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
Markdown(
|
||||||
|
data: 'Test line 1\n\nTest line 2\n\nTest line 3\n# H1',
|
||||||
|
styleSheet: style,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<Padding> paddings = tester.widgetList<Padding>(find.byType(Padding)).toList();
|
||||||
|
|
||||||
|
expect(paddings.length, 3);
|
||||||
|
expect(
|
||||||
|
paddings.every(
|
||||||
|
(Padding p) => p.padding.along(Axis.horizontal) == paddingX * 2,
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'use stylesheet option h1Padding-h6Padding',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const double paddingX = 20.0;
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet(
|
||||||
|
h1Padding: const EdgeInsets.symmetric(horizontal: paddingX),
|
||||||
|
h2Padding: const EdgeInsets.symmetric(horizontal: paddingX),
|
||||||
|
h3Padding: const EdgeInsets.symmetric(horizontal: paddingX),
|
||||||
|
h4Padding: const EdgeInsets.symmetric(horizontal: paddingX),
|
||||||
|
h5Padding: const EdgeInsets.symmetric(horizontal: paddingX),
|
||||||
|
h6Padding: const EdgeInsets.symmetric(horizontal: paddingX),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
Markdown(
|
||||||
|
data: 'Test\n\n# H1\n## H2\n### H3\n#### H4\n##### H5\n###### H6\n',
|
||||||
|
styleSheet: style,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<Padding> paddings = tester.widgetList<Padding>(find.byType(Padding)).toList();
|
||||||
|
|
||||||
|
expect(paddings.length, 6);
|
||||||
|
expect(
|
||||||
|
paddings.every(
|
||||||
|
(Padding p) => p.padding.along(Axis.horizontal) == paddingX * 2,
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'deprecated textScaleFactor is converted to linear scaler',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const double scaleFactor = 2.0;
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet(
|
||||||
|
textScaleFactor: scaleFactor,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(style.textScaler, const TextScaler.linear(scaleFactor));
|
||||||
|
expect(style.textScaleFactor, scaleFactor);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'deprecated textScaleFactor is null when a scaler is provided',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const TextScaler scaler = TextScaler.linear(2.0);
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet(
|
||||||
|
textScaler: scaler,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(style.textScaler, scaler);
|
||||||
|
expect(style.textScaleFactor, null);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'copyWith textScaler overwrites both textScaler and textScaleFactor',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final MarkdownStyleSheet original = MarkdownStyleSheet(
|
||||||
|
textScaleFactor: 2.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const TextScaler newScaler = TextScaler.linear(3.0);
|
||||||
|
final MarkdownStyleSheet copy = original.copyWith(
|
||||||
|
textScaler: newScaler,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(copy.textScaler, newScaler);
|
||||||
|
expect(copy.textScaleFactor, null);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'copyWith textScaleFactor overwrites both textScaler and textScaleFactor',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final MarkdownStyleSheet original = MarkdownStyleSheet(
|
||||||
|
textScaleFactor: 2.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const double newScaleFactor = 3.0;
|
||||||
|
final MarkdownStyleSheet copy = original.copyWith(
|
||||||
|
textScaleFactor: newScaleFactor,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(copy.textScaler, const TextScaler.linear(newScaleFactor));
|
||||||
|
expect(copy.textScaleFactor, newScaleFactor);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
702
flutter_markdown_plus/test/table_test.dart
Normal file
702
flutter_markdown_plus/test/table_test.dart
Normal file
|
|
@ -0,0 +1,702 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
|
void main() => defineTests();
|
||||||
|
|
||||||
|
void defineTests() {
|
||||||
|
group('Table', () {
|
||||||
|
testWidgets(
|
||||||
|
'should show properly',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '|Header 1|Header 2|\n|-----|-----|\n|Col 1|Col 2|';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
expectTextStrings(widgets, <String>['Header 1', 'Header 2', 'Col 1', 'Col 2']);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'work without the outer pipes',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = 'Header 1|Header 2\n-----|-----\nCol 1|Col 2';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
expectTextStrings(widgets, <String>['Header 1', 'Header 2', 'Col 1', 'Col 2']);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should work with alignments',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '|Header 1|Header 2|Header 3|\n|:----|:----:|----:|\n|Col 1|Col 2|Col 3|';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<DefaultTextStyle> styles = tester.widgetList(find.byType(DefaultTextStyle));
|
||||||
|
|
||||||
|
expect(styles.first.textAlign, TextAlign.left);
|
||||||
|
expect(styles.elementAt(1).textAlign, TextAlign.center);
|
||||||
|
expect(styles.last.textAlign, TextAlign.right);
|
||||||
|
|
||||||
|
final Iterable<Wrap> wraps = tester.widgetList(find.byType(Wrap));
|
||||||
|
|
||||||
|
expect(wraps.first.alignment, WrapAlignment.start);
|
||||||
|
expect(wraps.elementAt(1).alignment, WrapAlignment.center);
|
||||||
|
expect(wraps.last.alignment, WrapAlignment.end);
|
||||||
|
|
||||||
|
final Iterable<Text> texts = tester.widgetList(find.byType(Text));
|
||||||
|
|
||||||
|
expect(texts.first.textAlign, TextAlign.left);
|
||||||
|
expect(texts.elementAt(1).textAlign, TextAlign.center);
|
||||||
|
expect(texts.last.textAlign, TextAlign.right);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should work with styling',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '|Header|\n|----|\n|*italic*|';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
final Text text = widgets.lastWhere((Widget widget) => widget is Text) as Text;
|
||||||
|
|
||||||
|
expectTextStrings(widgets, <String>['Header', 'italic']);
|
||||||
|
expect(text.textSpan!.style!.fontStyle, FontStyle.italic);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should work next to other tables',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '|first header|\n|----|\n|first col|\n\n'
|
||||||
|
'|second header|\n|----|\n|second col|';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> tables = tester.widgetList(find.byType(Table));
|
||||||
|
|
||||||
|
expect(tables.length, 2);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'column width should follow stylesheet',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
|
||||||
|
const String data = '|Header|\n|----|\n|Column|';
|
||||||
|
const FixedColumnWidth columnWidth = FixedColumnWidth(100);
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme).copyWith(
|
||||||
|
tableColumnWidth: columnWidth,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(boilerplate(MarkdownBody(data: data, styleSheet: style)));
|
||||||
|
|
||||||
|
final Table table = tester.widget(find.byType(Table));
|
||||||
|
|
||||||
|
expect(table.defaultColumnWidth, columnWidth);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'table cell vertical alignment should default to middle',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
|
||||||
|
const String data = '|Header|\n|----|\n|Column|';
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme);
|
||||||
|
await tester.pumpWidget(boilerplate(MarkdownBody(data: data, styleSheet: style)));
|
||||||
|
|
||||||
|
final Table table = tester.widget(find.byType(Table));
|
||||||
|
|
||||||
|
expect(table.defaultVerticalAlignment, TableCellVerticalAlignment.middle);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'table cell vertical alignment should follow stylesheet',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
|
||||||
|
const String data = '|Header|\n|----|\n|Column|';
|
||||||
|
const TableCellVerticalAlignment tableCellVerticalAlignment = TableCellVerticalAlignment.top;
|
||||||
|
final MarkdownStyleSheet style =
|
||||||
|
MarkdownStyleSheet.fromTheme(theme).copyWith(tableVerticalAlignment: tableCellVerticalAlignment);
|
||||||
|
|
||||||
|
await tester.pumpWidget(boilerplate(MarkdownBody(data: data, styleSheet: style)));
|
||||||
|
|
||||||
|
final Table table = tester.widget(find.byType(Table));
|
||||||
|
|
||||||
|
expect(table.defaultVerticalAlignment, tableCellVerticalAlignment);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'table cell vertical alignment should follow stylesheet for different values',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
|
||||||
|
const String data = '|Header|\n|----|\n|Column|';
|
||||||
|
const TableCellVerticalAlignment tableCellVerticalAlignment = TableCellVerticalAlignment.bottom;
|
||||||
|
final MarkdownStyleSheet style =
|
||||||
|
MarkdownStyleSheet.fromTheme(theme).copyWith(tableVerticalAlignment: tableCellVerticalAlignment);
|
||||||
|
|
||||||
|
await tester.pumpWidget(boilerplate(MarkdownBody(data: data, styleSheet: style)));
|
||||||
|
|
||||||
|
final Table table = tester.widget(find.byType(Table));
|
||||||
|
|
||||||
|
expect(table.defaultVerticalAlignment, tableCellVerticalAlignment);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'table scrollbar thumbVisibility should follow stylesheet',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
|
||||||
|
const String data = '|Header|\n|----|\n|Column|';
|
||||||
|
const bool tableScrollbarThumbVisibility = true;
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme).copyWith(
|
||||||
|
tableColumnWidth: const FixedColumnWidth(100),
|
||||||
|
tableScrollbarThumbVisibility: tableScrollbarThumbVisibility);
|
||||||
|
|
||||||
|
await tester.pumpWidget(boilerplate(MarkdownBody(data: data, styleSheet: style)));
|
||||||
|
|
||||||
|
final Scrollbar scrollbar = tester.widget(find.byType(Scrollbar));
|
||||||
|
|
||||||
|
expect(scrollbar.thumbVisibility, tableScrollbarThumbVisibility);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'table scrollbar thumbVisibility should follow stylesheet',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
|
||||||
|
const String data = '|Header|\n|----|\n|Column|';
|
||||||
|
const bool tableScrollbarThumbVisibility = false;
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme).copyWith(
|
||||||
|
tableColumnWidth: const FixedColumnWidth(100),
|
||||||
|
tableScrollbarThumbVisibility: tableScrollbarThumbVisibility);
|
||||||
|
|
||||||
|
await tester.pumpWidget(boilerplate(MarkdownBody(data: data, styleSheet: style)));
|
||||||
|
|
||||||
|
final Scrollbar scrollbar = tester.widget(find.byType(Scrollbar));
|
||||||
|
|
||||||
|
expect(scrollbar.thumbVisibility, tableScrollbarThumbVisibility);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'table with last row of empty table cells',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
|
||||||
|
const String data = '|Header 1|Header 2|\n|----|----|\n| | |';
|
||||||
|
const FixedColumnWidth columnWidth = FixedColumnWidth(100);
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme).copyWith(
|
||||||
|
tableColumnWidth: columnWidth,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(boilerplate(MarkdownBody(data: data, styleSheet: style)));
|
||||||
|
|
||||||
|
final Table table = tester.widget(find.byType(Table));
|
||||||
|
|
||||||
|
expectTableSize(2, 2);
|
||||||
|
|
||||||
|
expect(find.byType(Text), findsNWidgets(4));
|
||||||
|
final List<String?> cellText = find
|
||||||
|
.byType(Text)
|
||||||
|
.evaluate()
|
||||||
|
.map((Element e) => e.widget)
|
||||||
|
.cast<Text>()
|
||||||
|
.map((Text text) => text.textSpan!)
|
||||||
|
.cast<TextSpan>()
|
||||||
|
.map((TextSpan e) => e.text)
|
||||||
|
.toList();
|
||||||
|
expect(cellText[0], 'Header 1');
|
||||||
|
expect(cellText[1], 'Header 2');
|
||||||
|
expect(cellText[2], '');
|
||||||
|
expect(cellText[3], '');
|
||||||
|
|
||||||
|
expect(table.defaultColumnWidth, columnWidth);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'table with an empty row an last row has an empty table cell',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
|
||||||
|
const String data = '|Header 1|Header 2|\n|----|----|\n| | |\n| bar | |';
|
||||||
|
const FixedColumnWidth columnWidth = FixedColumnWidth(100);
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme).copyWith(
|
||||||
|
tableColumnWidth: columnWidth,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(boilerplate(MarkdownBody(data: data, styleSheet: style)));
|
||||||
|
|
||||||
|
final Table table = tester.widget(find.byType(Table));
|
||||||
|
|
||||||
|
expectTableSize(3, 2);
|
||||||
|
|
||||||
|
expect(find.byType(RichText), findsNWidgets(6));
|
||||||
|
final List<String?> cellText = find
|
||||||
|
.byType(Text)
|
||||||
|
.evaluate()
|
||||||
|
.map((Element e) => e.widget)
|
||||||
|
.cast<Text>()
|
||||||
|
.map((Text richText) => richText.textSpan!)
|
||||||
|
.cast<TextSpan>()
|
||||||
|
.map((TextSpan e) => e.text)
|
||||||
|
.toList();
|
||||||
|
expect(cellText[0], 'Header 1');
|
||||||
|
expect(cellText[1], 'Header 2');
|
||||||
|
expect(cellText[2], '');
|
||||||
|
expect(cellText[3], '');
|
||||||
|
expect(cellText[4], 'bar');
|
||||||
|
expect(cellText[5], '');
|
||||||
|
|
||||||
|
expect(table.defaultColumnWidth, columnWidth);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group('GFM Examples', () {
|
||||||
|
testWidgets(
|
||||||
|
// Example 198 from GFM.
|
||||||
|
'simple table',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
|
||||||
|
const String data = '| foo | bar |\n| --- | --- |\n| baz | bim |';
|
||||||
|
const FixedColumnWidth columnWidth = FixedColumnWidth(100);
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme).copyWith(
|
||||||
|
tableColumnWidth: columnWidth,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(boilerplate(MarkdownBody(data: data, styleSheet: style)));
|
||||||
|
|
||||||
|
final Table table = tester.widget(find.byType(Table));
|
||||||
|
|
||||||
|
expectTableSize(2, 2);
|
||||||
|
|
||||||
|
expect(find.byType(Text), findsNWidgets(4));
|
||||||
|
final List<String?> cellText = find
|
||||||
|
.byType(Text)
|
||||||
|
.evaluate()
|
||||||
|
.map((Element e) => e.widget)
|
||||||
|
.cast<Text>()
|
||||||
|
.map((Text text) => text.textSpan!)
|
||||||
|
.cast<TextSpan>()
|
||||||
|
.map((TextSpan e) => e.text)
|
||||||
|
.toList();
|
||||||
|
expect(cellText[0], 'foo');
|
||||||
|
expect(cellText[1], 'bar');
|
||||||
|
expect(cellText[2], 'baz');
|
||||||
|
expect(cellText[3], 'bim');
|
||||||
|
expect(table.defaultColumnWidth, columnWidth);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
// Example 199 from GFM.
|
||||||
|
'input table cell data does not need to match column length',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
|
||||||
|
const String data = '| abc | defghi |\n:-: | -----------:\nbar | baz';
|
||||||
|
const FixedColumnWidth columnWidth = FixedColumnWidth(100);
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme).copyWith(
|
||||||
|
tableColumnWidth: columnWidth,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(boilerplate(MarkdownBody(data: data, styleSheet: style)));
|
||||||
|
|
||||||
|
final Table table = tester.widget(find.byType(Table));
|
||||||
|
|
||||||
|
expectTableSize(2, 2);
|
||||||
|
|
||||||
|
expect(find.byType(Text), findsNWidgets(4));
|
||||||
|
final List<String?> cellText = find
|
||||||
|
.byType(Text)
|
||||||
|
.evaluate()
|
||||||
|
.map((Element e) => e.widget)
|
||||||
|
.cast<Text>()
|
||||||
|
.map((Text text) => text.textSpan!)
|
||||||
|
.cast<TextSpan>()
|
||||||
|
.map((TextSpan e) => e.text)
|
||||||
|
.toList();
|
||||||
|
expect(cellText[0], 'abc');
|
||||||
|
expect(cellText[1], 'defghi');
|
||||||
|
expect(cellText[2], 'bar');
|
||||||
|
expect(cellText[3], 'baz');
|
||||||
|
expect(table.defaultColumnWidth, columnWidth);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
// Example 200 from GFM.
|
||||||
|
'include a pipe in table cell data by escaping the pipe',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
|
||||||
|
const String data = '| f\\|oo |\n| ------ |\n| b \\| az |\n| b **\\|** im |';
|
||||||
|
const FixedColumnWidth columnWidth = FixedColumnWidth(100);
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme).copyWith(
|
||||||
|
tableColumnWidth: columnWidth,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(boilerplate(MarkdownBody(data: data, styleSheet: style)));
|
||||||
|
|
||||||
|
final Table table = tester.widget(find.byType(Table));
|
||||||
|
|
||||||
|
expectTableSize(1, 3);
|
||||||
|
|
||||||
|
expect(find.byType(Text), findsNWidgets(4));
|
||||||
|
final List<String?> cellText = find
|
||||||
|
.byType(Text)
|
||||||
|
.evaluate()
|
||||||
|
.map((Element e) => e.widget)
|
||||||
|
.cast<Text>()
|
||||||
|
.map((Text text) => text.textSpan!)
|
||||||
|
.cast<TextSpan>()
|
||||||
|
.map((TextSpan e) => e.text)
|
||||||
|
.toList();
|
||||||
|
expect(cellText[0], 'f|oo');
|
||||||
|
expect(cellText[1], 'defghi');
|
||||||
|
expect(cellText[2], 'b | az');
|
||||||
|
expect(cellText[3], 'b | im');
|
||||||
|
expect(table.defaultColumnWidth, columnWidth);
|
||||||
|
},
|
||||||
|
// TODO(mjordan56): Remove skip once the issue #340 in the markdown package
|
||||||
|
// is fixed and released. https://github.com/dart-lang/markdown/issues/340
|
||||||
|
// This test will need adjusting once issue #340 is fixed.
|
||||||
|
skip: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
// Example 201 from GFM.
|
||||||
|
'table definition is complete at beginning of new block',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
|
||||||
|
const String data = '| abc | def |\n| --- | --- |\n| bar | baz |\n> bar';
|
||||||
|
const FixedColumnWidth columnWidth = FixedColumnWidth(100);
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme).copyWith(
|
||||||
|
tableColumnWidth: columnWidth,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(boilerplate(MarkdownBody(data: data, styleSheet: style)));
|
||||||
|
|
||||||
|
final Table table = tester.widget(find.byType(Table));
|
||||||
|
|
||||||
|
expectTableSize(2, 2);
|
||||||
|
|
||||||
|
expect(find.byType(Text), findsNWidgets(5));
|
||||||
|
final List<String?> text = find
|
||||||
|
.byType(Text)
|
||||||
|
.evaluate()
|
||||||
|
.map((Element e) => e.widget)
|
||||||
|
.cast<Text>()
|
||||||
|
.map((Text text) => text.textSpan!)
|
||||||
|
.cast<TextSpan>()
|
||||||
|
.map((TextSpan e) => e.text)
|
||||||
|
.toList();
|
||||||
|
expect(text[0], 'abc');
|
||||||
|
expect(text[1], 'def');
|
||||||
|
expect(text[2], 'bar');
|
||||||
|
expect(text[3], 'baz');
|
||||||
|
expect(table.defaultColumnWidth, columnWidth);
|
||||||
|
|
||||||
|
// Blockquote
|
||||||
|
expect(find.byType(DecoratedBox), findsOneWidget);
|
||||||
|
expect(text[4], 'bar');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
// Example 202 from GFM.
|
||||||
|
'table definition is complete at first empty line',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
|
||||||
|
const String data = '| abc | def |\n| --- | --- |\n| bar | baz |\nbar\n\nbar';
|
||||||
|
const FixedColumnWidth columnWidth = FixedColumnWidth(100);
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme).copyWith(
|
||||||
|
tableColumnWidth: columnWidth,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(boilerplate(MarkdownBody(data: data, styleSheet: style)));
|
||||||
|
|
||||||
|
final Table table = tester.widget(find.byType(Table));
|
||||||
|
|
||||||
|
expectTableSize(3, 2);
|
||||||
|
|
||||||
|
expect(find.byType(Text), findsNWidgets(7));
|
||||||
|
final List<String?> text = find
|
||||||
|
.byType(Text)
|
||||||
|
.evaluate()
|
||||||
|
.map((Element e) => e.widget)
|
||||||
|
.cast<Text>()
|
||||||
|
.map((Text text) => text.textSpan!)
|
||||||
|
.cast<TextSpan>()
|
||||||
|
.map((TextSpan e) => e.text)
|
||||||
|
.toList();
|
||||||
|
expect(text, <String>['abc', 'def', 'bar', 'baz', 'bar', '', 'bar']);
|
||||||
|
expect(table.defaultColumnWidth, columnWidth);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
// Example 203 from GFM.
|
||||||
|
'table header row must match the delimiter row in number of cells',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
|
||||||
|
const String data = '| abc | def |\n| --- |\n| bar |';
|
||||||
|
const FixedColumnWidth columnWidth = FixedColumnWidth(100);
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme).copyWith(
|
||||||
|
tableColumnWidth: columnWidth,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(boilerplate(MarkdownBody(data: data, styleSheet: style)));
|
||||||
|
|
||||||
|
expect(find.byType(Table), findsNothing);
|
||||||
|
final List<String?> text = find
|
||||||
|
.byType(Text)
|
||||||
|
.evaluate()
|
||||||
|
.map((Element e) => e.widget)
|
||||||
|
.cast<Text>()
|
||||||
|
.map((Text text) => text.textSpan!)
|
||||||
|
.cast<TextSpan>()
|
||||||
|
.map((TextSpan e) => e.text)
|
||||||
|
.toList();
|
||||||
|
expect(text[0], '| abc | def | | --- | | bar |');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
// Example 204 from GFM.
|
||||||
|
'remainder of table cells may vary, excess cells are ignored',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
|
||||||
|
const String data = '| abc | def |\n| --- | --- |\n| bar |\n| bar | baz | boo |';
|
||||||
|
const FixedColumnWidth columnWidth = FixedColumnWidth(100);
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme).copyWith(
|
||||||
|
tableColumnWidth: columnWidth,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(boilerplate(MarkdownBody(data: data, styleSheet: style)));
|
||||||
|
|
||||||
|
final Table table = tester.widget(find.byType(Table));
|
||||||
|
|
||||||
|
expectTableSize(3, 2);
|
||||||
|
|
||||||
|
expect(find.byType(Text), findsNWidgets(6));
|
||||||
|
final List<String?> cellText = find
|
||||||
|
.byType(Text)
|
||||||
|
.evaluate()
|
||||||
|
.map((Element e) => e.widget)
|
||||||
|
.cast<Text>()
|
||||||
|
.map((Text text) => text.textSpan!)
|
||||||
|
.cast<TextSpan>()
|
||||||
|
.map((TextSpan e) => e.text)
|
||||||
|
.toList();
|
||||||
|
expect(cellText, <String>['abc', 'def', 'bar', '', 'bar', 'baz']);
|
||||||
|
expect(table.defaultColumnWidth, columnWidth);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
// Example 205 from GFM.
|
||||||
|
'no table body is created when no rows are defined',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
|
||||||
|
const String data = '| abc | def |\n| --- | --- |';
|
||||||
|
const FixedColumnWidth columnWidth = FixedColumnWidth(100);
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme).copyWith(
|
||||||
|
tableColumnWidth: columnWidth,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(boilerplate(MarkdownBody(data: data, styleSheet: style)));
|
||||||
|
|
||||||
|
final Table table = tester.widget(find.byType(Table));
|
||||||
|
|
||||||
|
expectTableSize(1, 2);
|
||||||
|
|
||||||
|
expect(find.byType(Text), findsNWidgets(2));
|
||||||
|
final List<String?> cellText = find
|
||||||
|
.byType(Text)
|
||||||
|
.evaluate()
|
||||||
|
.map((Element e) => e.widget)
|
||||||
|
.cast<Text>()
|
||||||
|
.map((Text text) => text.textSpan!)
|
||||||
|
.cast<TextSpan>()
|
||||||
|
.map((TextSpan e) => e.text)
|
||||||
|
.toList();
|
||||||
|
expect(cellText[0], 'abc');
|
||||||
|
expect(cellText[1], 'def');
|
||||||
|
expect(table.defaultColumnWidth, columnWidth);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'table header cells should use tableHeadCellsPadding when specified',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
|
||||||
|
const String data = '|Header|\n|----|\n|Body|';
|
||||||
|
const EdgeInsets headerPadding = EdgeInsets.all(20);
|
||||||
|
const EdgeInsets bodyPadding = EdgeInsets.all(10);
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme).copyWith(
|
||||||
|
tableHeadCellsPadding: headerPadding,
|
||||||
|
tableCellsPadding: bodyPadding,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(boilerplate(MarkdownBody(data: data, styleSheet: style)));
|
||||||
|
|
||||||
|
final Iterable<Padding> paddings = tester.widgetList(find.byType(Padding));
|
||||||
|
// Filter to get only TableCell paddings (not other paddings like table padding)
|
||||||
|
final List<Padding> cellPaddings = paddings.where((Padding p) {
|
||||||
|
return p.padding == headerPadding || p.padding == bodyPadding;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
expect(cellPaddings.length, 2);
|
||||||
|
expect(cellPaddings[0].padding, headerPadding); // Header cell
|
||||||
|
expect(cellPaddings[1].padding, bodyPadding); // Body cell
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'table header cells should use tableCellsPadding when tableHeadCellsPadding is null',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
|
||||||
|
const String data = '|Header|\n|----|\n|Body|';
|
||||||
|
const EdgeInsets cellPadding = EdgeInsets.all(12);
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme).copyWith(
|
||||||
|
tableCellsPadding: cellPadding,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(boilerplate(MarkdownBody(data: data, styleSheet: style)));
|
||||||
|
|
||||||
|
final Iterable<Padding> paddings = tester.widgetList(find.byType(Padding));
|
||||||
|
final List<Padding> cellPaddings = paddings.where((Padding p) {
|
||||||
|
return p.padding == cellPadding;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
expect(cellPaddings.length, 2); // Both header and body use same padding
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'table header row should use tableHeadCellsDecoration when specified',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
|
||||||
|
const String data = '|Header|\n|----|\n|Body|';
|
||||||
|
final BoxDecoration headerDecoration = BoxDecoration(
|
||||||
|
color: Colors.blue.shade100,
|
||||||
|
);
|
||||||
|
final BoxDecoration bodyDecoration = BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
);
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme).copyWith(
|
||||||
|
tableHeadCellsDecoration: headerDecoration,
|
||||||
|
tableCellsDecoration: bodyDecoration,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(boilerplate(MarkdownBody(data: data, styleSheet: style)));
|
||||||
|
|
||||||
|
final Table table = tester.widget(find.byType(Table));
|
||||||
|
|
||||||
|
expect(table.children.length, 2); // Header and body rows
|
||||||
|
expect(table.children[0].decoration, headerDecoration); // Header row
|
||||||
|
expect(table.children[1].decoration, isNull); // Body row (odd row, no decoration by default)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'table header row should use tableCellsDecoration when tableHeadCellsDecoration is null',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
|
||||||
|
const String data = '|Header|\n|----|\n|Body|';
|
||||||
|
final BoxDecoration bodyDecoration = BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
);
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme).copyWith(
|
||||||
|
tableCellsDecoration: bodyDecoration,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(boilerplate(MarkdownBody(data: data, styleSheet: style)));
|
||||||
|
|
||||||
|
final Table table = tester.widget(find.byType(Table));
|
||||||
|
|
||||||
|
expect(table.children.length, 2); // Header and body rows
|
||||||
|
expect(table.children[0].decoration, bodyDecoration); // Header falls back to body decoration
|
||||||
|
expect(table.children[1].decoration, isNull); // First body row (odd) remains undecorated
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'table with multiple rows should apply decorations correctly',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
|
||||||
|
const String data = '|H1|H2|\n|--|--|\n|R1C1|R1C2|\n|R2C1|R2C2|\n|R3C1|R3C2|';
|
||||||
|
final BoxDecoration headerDecoration = BoxDecoration(
|
||||||
|
color: Colors.blue.shade100,
|
||||||
|
);
|
||||||
|
final BoxDecoration bodyDecoration = BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
);
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme).copyWith(
|
||||||
|
tableHeadCellsDecoration: headerDecoration,
|
||||||
|
tableCellsDecoration: bodyDecoration,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(boilerplate(MarkdownBody(data: data, styleSheet: style)));
|
||||||
|
|
||||||
|
final Table table = tester.widget(find.byType(Table));
|
||||||
|
|
||||||
|
expect(table.children.length, 4); // 1 header + 3 body rows
|
||||||
|
expect(table.children[0].decoration, headerDecoration); // Header row (length=0)
|
||||||
|
expect(table.children[1].decoration, isNull); // Body row 1 (length=1, odd)
|
||||||
|
expect(table.children[2].decoration, bodyDecoration); // Body row 2 (length=2, even)
|
||||||
|
expect(table.children[3].decoration, isNull); // Body row 3 (length=3, odd)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
107
flutter_markdown_plus/test/text_alignment_test.dart
Normal file
107
flutter_markdown_plus/test/text_alignment_test.dart
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
|
void main() => defineTests();
|
||||||
|
|
||||||
|
void defineTests() {
|
||||||
|
group('Text Alignment', () {
|
||||||
|
testWidgets(
|
||||||
|
'apply text alignments from stylesheet',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
final MarkdownStyleSheet style1 = MarkdownStyleSheet.fromTheme(theme).copyWith(
|
||||||
|
h1Align: WrapAlignment.center,
|
||||||
|
h3Align: WrapAlignment.end,
|
||||||
|
);
|
||||||
|
|
||||||
|
const String data = '# h1\n ## h2';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
MarkdownBody(
|
||||||
|
data: data,
|
||||||
|
styleSheet: style1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = selfAndDescendantWidgetsOf(
|
||||||
|
find.byType(MarkdownBody),
|
||||||
|
tester,
|
||||||
|
);
|
||||||
|
expectWidgetTypes(widgets, <Type>[
|
||||||
|
MarkdownBody,
|
||||||
|
Column,
|
||||||
|
Column,
|
||||||
|
Wrap,
|
||||||
|
Text,
|
||||||
|
RichText,
|
||||||
|
SizedBox,
|
||||||
|
Column,
|
||||||
|
Wrap,
|
||||||
|
Text,
|
||||||
|
RichText,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect((widgets.firstWhere((Widget w) => w is RichText) as RichText).textAlign, TextAlign.center);
|
||||||
|
expect((widgets.last as RichText).textAlign, TextAlign.start,
|
||||||
|
reason: 'default alignment if none is set in stylesheet');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should align formatted text',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme).copyWith(
|
||||||
|
textAlign: WrapAlignment.spaceBetween,
|
||||||
|
);
|
||||||
|
|
||||||
|
const String data = 'hello __my formatted text__';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
MarkdownBody(
|
||||||
|
data: data,
|
||||||
|
styleSheet: style,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final RichText text = tester.widgetList(find.byType(RichText)).single as RichText;
|
||||||
|
expect(text.textAlign, TextAlign.justify);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should align selectable text',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme);
|
||||||
|
final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme).copyWith(
|
||||||
|
textAlign: WrapAlignment.spaceBetween,
|
||||||
|
);
|
||||||
|
|
||||||
|
const String data = 'hello __my formatted text__';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
MediaQuery(
|
||||||
|
data: const MediaQueryData(),
|
||||||
|
child: MarkdownBody(
|
||||||
|
data: data,
|
||||||
|
styleSheet: style,
|
||||||
|
selectable: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final SelectableText text = tester.widgetList(find.byType(SelectableText)).single as SelectableText;
|
||||||
|
expect(text.textAlign, TextAlign.justify);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
76
flutter_markdown_plus/test/text_scaler_test.dart
Normal file
76
flutter_markdown_plus/test/text_scaler_test.dart
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
|
void main() => defineTests();
|
||||||
|
|
||||||
|
void defineTests() {
|
||||||
|
group('Text Scaler', () {
|
||||||
|
testWidgets(
|
||||||
|
'should use style textScaler in RichText',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const TextScaler scaler = TextScaler.linear(2.0);
|
||||||
|
const String data = 'Hello';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
MarkdownBody(
|
||||||
|
styleSheet: MarkdownStyleSheet(textScaler: scaler),
|
||||||
|
data: data,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final RichText richText = tester.widget(find.byType(RichText));
|
||||||
|
expect(richText.textScaler, scaler);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should use MediaQuery textScaler in RichText',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const TextScaler scaler = TextScaler.linear(2.0);
|
||||||
|
const String data = 'Hello';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MediaQuery(
|
||||||
|
data: MediaQueryData(textScaler: scaler),
|
||||||
|
child: MarkdownBody(
|
||||||
|
data: data,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final RichText richText = tester.widget(find.byType(RichText));
|
||||||
|
expect(richText.textScaler, scaler);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should use MediaQuery textScaler in SelectableText.rich',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const TextScaler scaler = TextScaler.linear(2.0);
|
||||||
|
const String data = 'Hello';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MediaQuery(
|
||||||
|
data: MediaQueryData(textScaler: scaler),
|
||||||
|
child: MarkdownBody(
|
||||||
|
data: data,
|
||||||
|
selectable: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final SelectableText selectableText = tester.widget(find.byType(SelectableText));
|
||||||
|
expect(selectableText.textScaler, scaler);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
400
flutter_markdown_plus/test/text_test.dart
Normal file
400
flutter_markdown_plus/test/text_test.dart
Normal file
|
|
@ -0,0 +1,400 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
|
void main() => defineTests();
|
||||||
|
|
||||||
|
void defineTests() {
|
||||||
|
group('Data', () {
|
||||||
|
testWidgets(
|
||||||
|
'simple data',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
// extract to variable; if run with --track-widget-creation using const
|
||||||
|
// widgets aren't necessarily identical if created on different lines.
|
||||||
|
const Markdown markdown = Markdown(data: 'Data1');
|
||||||
|
|
||||||
|
await tester.pumpWidget(boilerplate(markdown));
|
||||||
|
expectTextStrings(tester.allWidgets, <String>['Data1']);
|
||||||
|
|
||||||
|
final String stateBefore = dumpRenderView();
|
||||||
|
await tester.pumpWidget(boilerplate(markdown));
|
||||||
|
final String stateAfter = dumpRenderView();
|
||||||
|
expect(stateBefore, equals(stateAfter));
|
||||||
|
|
||||||
|
await tester.pumpWidget(boilerplate(const Markdown(data: 'Data2')));
|
||||||
|
expectTextStrings(tester.allWidgets, <String>['Data2']);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Text', () {
|
||||||
|
testWidgets(
|
||||||
|
'Empty string',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: ''),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = selfAndDescendantWidgetsOf(
|
||||||
|
find.byType(MarkdownBody),
|
||||||
|
tester,
|
||||||
|
);
|
||||||
|
expectWidgetTypes(widgets, <Type>[
|
||||||
|
MarkdownBody,
|
||||||
|
Column,
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Simple string',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: 'Hello'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = selfAndDescendantWidgetsOf(
|
||||||
|
find.byType(MarkdownBody),
|
||||||
|
tester,
|
||||||
|
);
|
||||||
|
expectWidgetTypes(widgets, <Type>[
|
||||||
|
MarkdownBody,
|
||||||
|
Column,
|
||||||
|
Wrap,
|
||||||
|
Text,
|
||||||
|
RichText,
|
||||||
|
]);
|
||||||
|
expectTextStrings(widgets, <String>['Hello']);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Leading spaces', () {
|
||||||
|
testWidgets(
|
||||||
|
// Example 192 from the GitHub Flavored Markdown specification.
|
||||||
|
'leading space are ignored', (WidgetTester tester) async {
|
||||||
|
const String data = ' aaa\n bbb';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = selfAndDescendantWidgetsOf(
|
||||||
|
find.byType(MarkdownBody),
|
||||||
|
tester,
|
||||||
|
);
|
||||||
|
expectWidgetTypes(widgets, <Type>[
|
||||||
|
MarkdownBody,
|
||||||
|
Column,
|
||||||
|
Wrap,
|
||||||
|
Text,
|
||||||
|
RichText,
|
||||||
|
]);
|
||||||
|
expectTextStrings(widgets, <String>['aaa bbb']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Line Break', () {
|
||||||
|
testWidgets(
|
||||||
|
// Example 654 from the GitHub Flavored Markdown specification.
|
||||||
|
'two spaces at end of line inside a block element',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = 'line 1 \nline 2';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = selfAndDescendantWidgetsOf(
|
||||||
|
find.byType(MarkdownBody),
|
||||||
|
tester,
|
||||||
|
);
|
||||||
|
expectWidgetTypes(widgets, <Type>[MarkdownBody, Column, Wrap, Text, RichText]);
|
||||||
|
expectTextStrings(widgets, <String>['line 1\nline 2']);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
// Example 655 from the GitHub Flavored Markdown specification.
|
||||||
|
'backslash at end of line inside a block element',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = 'line 1\\\nline 2';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = selfAndDescendantWidgetsOf(
|
||||||
|
find.byType(MarkdownBody),
|
||||||
|
tester,
|
||||||
|
);
|
||||||
|
expectWidgetTypes(widgets, <Type>[MarkdownBody, Column, Wrap, Text, RichText]);
|
||||||
|
expectTextStrings(widgets, <String>['line 1\nline 2']);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'non-applicable line break',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = 'line 1.\nline 2.';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = selfAndDescendantWidgetsOf(
|
||||||
|
find.byType(MarkdownBody),
|
||||||
|
tester,
|
||||||
|
);
|
||||||
|
expectWidgetTypes(widgets, <Type>[
|
||||||
|
MarkdownBody,
|
||||||
|
Column,
|
||||||
|
Wrap,
|
||||||
|
Text,
|
||||||
|
RichText,
|
||||||
|
]);
|
||||||
|
expectTextStrings(widgets, <String>['line 1. line 2.']);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'non-applicable line break',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = 'line 1.\nline 2.';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = selfAndDescendantWidgetsOf(
|
||||||
|
find.byType(MarkdownBody),
|
||||||
|
tester,
|
||||||
|
);
|
||||||
|
expectWidgetTypes(widgets, <Type>[
|
||||||
|
MarkdownBody,
|
||||||
|
Column,
|
||||||
|
Wrap,
|
||||||
|
Text,
|
||||||
|
RichText,
|
||||||
|
]);
|
||||||
|
expectTextStrings(widgets, <String>['line 1. line 2.']);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'soft line break',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = 'line 1.\nline 2.';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(
|
||||||
|
data: data,
|
||||||
|
softLineBreak: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = selfAndDescendantWidgetsOf(
|
||||||
|
find.byType(MarkdownBody),
|
||||||
|
tester,
|
||||||
|
);
|
||||||
|
expectWidgetTypes(widgets, <Type>[MarkdownBody, Column, Wrap, Text, RichText]);
|
||||||
|
expectTextStrings(widgets, <String>['line 1.\nline 2.']);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Selectable', () {
|
||||||
|
testWidgets(
|
||||||
|
'header with line of text',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '# Title\nHello _World_!';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MediaQuery(
|
||||||
|
data: MediaQueryData(),
|
||||||
|
child: Markdown(
|
||||||
|
data: data,
|
||||||
|
selectable: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(SelectableText), findsNWidgets(2));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'header with line of text and onTap callback',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '# Title\nHello _World_!';
|
||||||
|
String? textTapResults;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
MediaQuery(
|
||||||
|
data: const MediaQueryData(),
|
||||||
|
child: Markdown(
|
||||||
|
data: data,
|
||||||
|
selectable: true,
|
||||||
|
onTapText: () => textTapResults = 'Text has been tapped.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> selectableWidgets = tester.widgetList(find.byType(SelectableText));
|
||||||
|
expect(selectableWidgets.length, 2);
|
||||||
|
|
||||||
|
final SelectableText selectableTitle = selectableWidgets.first as SelectableText;
|
||||||
|
expect(selectableTitle, isNotNull);
|
||||||
|
expect(selectableTitle.onTap, isNotNull);
|
||||||
|
selectableTitle.onTap!();
|
||||||
|
expect(textTapResults == 'Text has been tapped.', true);
|
||||||
|
|
||||||
|
textTapResults = null;
|
||||||
|
final SelectableText selectableText = selectableWidgets.last as SelectableText;
|
||||||
|
expect(selectableText, isNotNull);
|
||||||
|
expect(selectableText.onTap, isNotNull);
|
||||||
|
selectableText.onTap!();
|
||||||
|
expect(textTapResults == 'Text has been tapped.', true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Selectable without onSelectionChanged',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '# abc def ghi\njkl opq';
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: MarkdownBody(
|
||||||
|
data: data,
|
||||||
|
selectable: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the positions before character 'd' and 'f'.
|
||||||
|
final Offset dPos = positionInRenderedText(tester, 'abc def ghi', 4);
|
||||||
|
final Offset fPos = positionInRenderedText(tester, 'abc def ghi', 6);
|
||||||
|
// Select from 'd' until 'f'.
|
||||||
|
final TestGesture firstGesture = await tester.startGesture(dPos, kind: PointerDeviceKind.mouse);
|
||||||
|
addTearDown(firstGesture.removePointer);
|
||||||
|
await tester.pump();
|
||||||
|
await firstGesture.moveTo(fPos);
|
||||||
|
await firstGesture.up();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Find the positions before character 'j' and 'o'.
|
||||||
|
final Offset jPos = positionInRenderedText(tester, 'jkl opq', 0);
|
||||||
|
final Offset oPos = positionInRenderedText(tester, 'jkl opq', 4);
|
||||||
|
// Select from 'j' until 'o'.
|
||||||
|
final TestGesture secondGesture = await tester.startGesture(jPos, kind: PointerDeviceKind.mouse);
|
||||||
|
addTearDown(secondGesture.removePointer);
|
||||||
|
await tester.pump();
|
||||||
|
await secondGesture.moveTo(oPos);
|
||||||
|
await secondGesture.up();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'header with line of text and onSelectionChanged callback',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '# abc def ghi\njkl opq';
|
||||||
|
String? selectableText;
|
||||||
|
String? selectedText;
|
||||||
|
void onSelectionChanged(String? text, TextSelection selection, SelectionChangedCause? cause) {
|
||||||
|
selectableText = text;
|
||||||
|
selectedText = text != null ? selection.textInside(text) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: MarkdownBody(
|
||||||
|
data: data,
|
||||||
|
selectable: true,
|
||||||
|
onSelectionChanged: onSelectionChanged,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the positions before character 'd' and 'f'.
|
||||||
|
final Offset dPos = positionInRenderedText(tester, 'abc def ghi', 4);
|
||||||
|
final Offset fPos = positionInRenderedText(tester, 'abc def ghi', 6);
|
||||||
|
// Select from 'd' until 'f'.
|
||||||
|
final TestGesture firstGesture = await tester.startGesture(dPos, kind: PointerDeviceKind.mouse);
|
||||||
|
addTearDown(firstGesture.removePointer);
|
||||||
|
await tester.pump();
|
||||||
|
await firstGesture.moveTo(fPos);
|
||||||
|
await firstGesture.up();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(selectableText, 'abc def ghi');
|
||||||
|
expect(selectedText, 'de');
|
||||||
|
|
||||||
|
// Find the positions before character 'j' and 'o'.
|
||||||
|
final Offset jPos = positionInRenderedText(tester, 'jkl opq', 0);
|
||||||
|
final Offset oPos = positionInRenderedText(tester, 'jkl opq', 4);
|
||||||
|
// Select from 'j' until 'o'.
|
||||||
|
final TestGesture secondGesture = await tester.startGesture(jPos, kind: PointerDeviceKind.mouse);
|
||||||
|
addTearDown(secondGesture.removePointer);
|
||||||
|
await tester.pump();
|
||||||
|
await secondGesture.moveTo(oPos);
|
||||||
|
await secondGesture.up();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(selectableText, 'jkl opq');
|
||||||
|
expect(selectedText, 'jkl ');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Strikethrough', () {
|
||||||
|
testWidgets('single word', (WidgetTester tester) async {
|
||||||
|
const String data = '~~strikethrough~~';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const MarkdownBody(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = selfAndDescendantWidgetsOf(
|
||||||
|
find.byType(MarkdownBody),
|
||||||
|
tester,
|
||||||
|
);
|
||||||
|
expectWidgetTypes(widgets, <Type>[
|
||||||
|
MarkdownBody,
|
||||||
|
Column,
|
||||||
|
Wrap,
|
||||||
|
Text,
|
||||||
|
RichText,
|
||||||
|
]);
|
||||||
|
expectTextStrings(widgets, <String>['strikethrough']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
94
flutter_markdown_plus/test/uri_test.dart
Normal file
94
flutter_markdown_plus/test/uri_test.dart
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
|
void main() => defineTests();
|
||||||
|
|
||||||
|
void defineTests() {
|
||||||
|
group('Uri Data Scheme', () {
|
||||||
|
testWidgets(
|
||||||
|
'should work with image in uri data scheme',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const Markdown(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
final Image image = widgets.firstWhere((Widget widget) => widget is Image) as Image;
|
||||||
|
expect(image.image.runtimeType, MemoryImage);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should work with base64 text in uri data scheme',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String imageData = '';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const Markdown(data: imageData),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Text widget = tester.widget(find.byType(Text));
|
||||||
|
expect(widget.runtimeType, Text);
|
||||||
|
expect(widget.data, 'Flutter');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should work with text in uri data scheme',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String imageData = '';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const Markdown(data: imageData),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Text widget = tester.widget(find.byType(Text));
|
||||||
|
expect(widget.runtimeType, Text);
|
||||||
|
expect(widget.data, 'Hello, Flutter');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should work with empty uri data scheme',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String imageData = '';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const Markdown(data: imageData),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Text widget = tester.widget(find.byType(Text));
|
||||||
|
expect(widget.runtimeType, Text);
|
||||||
|
expect(widget.data, '');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should work with unsupported mime types of uri data scheme',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const String data = '';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
boilerplate(
|
||||||
|
const Markdown(data: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
|
final SizedBox widget = widgets.firstWhere((Widget widget) => widget is SizedBox) as SizedBox;
|
||||||
|
expect(widget.runtimeType, SizedBox);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
248
flutter_markdown_plus/test/utils.dart
Normal file
248
flutter_markdown_plus/test/utils.dart
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io' as io;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
final TextTheme textTheme =
|
||||||
|
Typography.material2018().black.merge(const TextTheme(bodyMedium: TextStyle(fontSize: 12.0)));
|
||||||
|
|
||||||
|
Iterable<Widget> selfAndDescendantWidgetsOf(Finder start, WidgetTester tester) {
|
||||||
|
final Element startElement = tester.element(start);
|
||||||
|
final Iterable<Widget> descendants =
|
||||||
|
collectAllElementsFrom(startElement, skipOffstage: false).map((Element e) => e.widget);
|
||||||
|
return <Widget>[
|
||||||
|
startElement.widget,
|
||||||
|
...descendants,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the RenderEditable displaying the given text.
|
||||||
|
RenderEditable findRenderEditableWithText(WidgetTester tester, String text) {
|
||||||
|
final Iterable<RenderObject> roots = tester.renderObjectList(find.byType(EditableText));
|
||||||
|
expect(roots, isNotEmpty);
|
||||||
|
|
||||||
|
late RenderEditable renderEditable;
|
||||||
|
void recursiveFinder(RenderObject child) {
|
||||||
|
if (child is RenderEditable && child.plainText == text) {
|
||||||
|
renderEditable = child;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
child.visitChildren(recursiveFinder);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final RenderObject root in roots) {
|
||||||
|
root.visitChildren(recursiveFinder);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(renderEditable, isNotNull);
|
||||||
|
return renderEditable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the [textOffset] position in rendered [text].
|
||||||
|
Offset positionInRenderedText(WidgetTester tester, String text, int textOffset) {
|
||||||
|
final RenderEditable renderEditable = findRenderEditableWithText(tester, text);
|
||||||
|
final Iterable<TextSelectionPoint> textOffsetPoints = renderEditable.getEndpointsForSelection(
|
||||||
|
TextSelection.collapsed(offset: textOffset),
|
||||||
|
);
|
||||||
|
// Map the points to global positions.
|
||||||
|
final List<TextSelectionPoint> endpoints = textOffsetPoints.map<TextSelectionPoint>((TextSelectionPoint point) {
|
||||||
|
return TextSelectionPoint(
|
||||||
|
renderEditable.localToGlobal(point.point),
|
||||||
|
point.direction,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
expect(endpoints.length, 1);
|
||||||
|
return endpoints[0].point + const Offset(kIsWeb ? 1.0 : 0.0, -2.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void expectWidgetTypes(Iterable<Widget> widgets, List<Type> expected) {
|
||||||
|
final List<Type> actual = widgets.map((Widget w) => w.runtimeType).toList();
|
||||||
|
expect(actual, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
void expectTextStrings(Iterable<Widget> widgets, List<String> strings) {
|
||||||
|
int currentString = 0;
|
||||||
|
for (final Widget widget in widgets) {
|
||||||
|
TextSpan? span;
|
||||||
|
if (widget is RichText) {
|
||||||
|
span = widget.text as TextSpan;
|
||||||
|
} else if (widget is SelectableText) {
|
||||||
|
span = widget.textSpan;
|
||||||
|
}
|
||||||
|
if (span != null) {
|
||||||
|
final String text = _extractTextFromTextSpan(span);
|
||||||
|
expect(text, equals(strings[currentString]));
|
||||||
|
currentString += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _extractTextFromTextSpan(TextSpan span) {
|
||||||
|
String text = span.text ?? '';
|
||||||
|
if (span.children != null) {
|
||||||
|
for (final TextSpan child in span.children!.toList().cast<TextSpan>()) {
|
||||||
|
text += _extractTextFromTextSpan(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the font style and weight of the text span.
|
||||||
|
void expectTextSpanStyle(TextSpan textSpan, FontStyle? style, FontWeight weight) {
|
||||||
|
// Verify a text style is set
|
||||||
|
expect(textSpan.style, isNotNull, reason: 'text span text style is null');
|
||||||
|
|
||||||
|
// Font style check
|
||||||
|
if (style == null) {
|
||||||
|
expect(textSpan.style!.fontStyle, isNull, reason: 'font style is not null');
|
||||||
|
} else {
|
||||||
|
expect(textSpan.style!.fontStyle, isNotNull, reason: 'font style is null');
|
||||||
|
expect(
|
||||||
|
textSpan.style!.fontStyle == style,
|
||||||
|
isTrue,
|
||||||
|
reason: 'font style is not $style',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Font weight check
|
||||||
|
expect(textSpan.style, isNotNull, reason: 'font style is null');
|
||||||
|
expect(
|
||||||
|
textSpan.style!.fontWeight == weight,
|
||||||
|
isTrue,
|
||||||
|
reason: 'font weight is not $weight',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class MarkdownLink {
|
||||||
|
const MarkdownLink(this.text, this.destination, [this.title = '']);
|
||||||
|
|
||||||
|
final String text;
|
||||||
|
final String? destination;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
other is MarkdownLink && other.text == text && other.destination == destination && other.title == title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => '$text$destination$title'.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return '[$text]($destination "$title")';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify a valid link structure has been created. This routine checks for the
|
||||||
|
/// link text and the associated [TapGestureRecognizer] on the text span.
|
||||||
|
void expectValidLink(String linkText) {
|
||||||
|
final Finder textFinder = find.byType(Text);
|
||||||
|
expect(textFinder, findsOneWidget);
|
||||||
|
final Text text = textFinder.evaluate().first.widget as Text;
|
||||||
|
|
||||||
|
// Verify the link text.
|
||||||
|
expect(text.textSpan, isNotNull);
|
||||||
|
expect(text.textSpan, isA<TextSpan>());
|
||||||
|
|
||||||
|
// Verify the link text is a onTap gesture recognizer.
|
||||||
|
final TextSpan textSpan = text.textSpan! as TextSpan;
|
||||||
|
expectLinkTextSpan(textSpan, linkText);
|
||||||
|
}
|
||||||
|
|
||||||
|
void expectLinkTextSpan(TextSpan textSpan, String linkText) {
|
||||||
|
expect(textSpan.children, isNull);
|
||||||
|
expect(textSpan.toPlainText(), linkText);
|
||||||
|
expect(textSpan.recognizer, isNotNull);
|
||||||
|
expect(textSpan.recognizer, isA<TapGestureRecognizer>());
|
||||||
|
final TapGestureRecognizer? tapRecognizer = textSpan.recognizer as TapGestureRecognizer?;
|
||||||
|
expect(tapRecognizer?.onTap, isNotNull);
|
||||||
|
|
||||||
|
// Execute the onTap callback handler.
|
||||||
|
tapRecognizer!.onTap!();
|
||||||
|
}
|
||||||
|
|
||||||
|
void expectInvalidLink(String linkText) {
|
||||||
|
final Finder textFinder = find.byType(Text);
|
||||||
|
expect(textFinder, findsOneWidget);
|
||||||
|
final Text text = textFinder.evaluate().first.widget as Text;
|
||||||
|
|
||||||
|
expect(text.textSpan, isNotNull);
|
||||||
|
expect(text.textSpan, isA<TextSpan>());
|
||||||
|
final String plainText = text.textSpan!.toPlainText();
|
||||||
|
expect(plainText, linkText);
|
||||||
|
|
||||||
|
final TextSpan textSpan = text.textSpan! as TextSpan;
|
||||||
|
expect(textSpan.recognizer, isNull);
|
||||||
|
}
|
||||||
|
|
||||||
|
void expectTableSize(int rows, int columns) {
|
||||||
|
final Finder tableFinder = find.byType(Table);
|
||||||
|
expect(tableFinder, findsOneWidget);
|
||||||
|
final Table table = tableFinder.evaluate().first.widget as Table;
|
||||||
|
|
||||||
|
expect(table.children.length, rows);
|
||||||
|
for (int index = 0; index < rows; index++) {
|
||||||
|
expect(table.children[index].children.length, columns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void expectLinkTap(MarkdownLink? actual, MarkdownLink expected) {
|
||||||
|
expect(actual, equals(expected), reason: 'incorrect link tap results, actual: $actual expected: $expected');
|
||||||
|
}
|
||||||
|
|
||||||
|
String dumpRenderView() {
|
||||||
|
return WidgetsBinding.instance.rootElement!.toStringDeep().replaceAll(
|
||||||
|
RegExp(r'SliverChildListDelegate#\d+', multiLine: true),
|
||||||
|
'SliverChildListDelegate',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wraps a widget with a left-to-right [Directionality] for tests.
|
||||||
|
Widget boilerplate(Widget child) {
|
||||||
|
return Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestAssetBundle extends CachingAssetBundle {
|
||||||
|
@override
|
||||||
|
Future<ByteData> load(String key) async {
|
||||||
|
if (key == 'AssetManifest.json') {
|
||||||
|
const String manifest = r'{"assets/logo.png":["assets/logo.png"]}';
|
||||||
|
final ByteData asset = ByteData.view(utf8.encoder.convert(manifest).buffer);
|
||||||
|
return Future<ByteData>.value(asset);
|
||||||
|
} else if (key == 'AssetManifest.bin') {
|
||||||
|
final ByteData manifest =
|
||||||
|
const StandardMessageCodec().encodeMessage(<String, List<Object>>{'assets/logo.png': <Object>[]})!;
|
||||||
|
return Future<ByteData>.value(manifest);
|
||||||
|
} else if (key == 'AssetManifest.smcbin') {
|
||||||
|
final ByteData manifest =
|
||||||
|
const StandardMessageCodec().encodeMessage(<String, List<Object>>{'assets/logo.png': <Object>[]})!;
|
||||||
|
return Future<ByteData>.value(manifest);
|
||||||
|
} else if (key == 'assets/logo.png') {
|
||||||
|
// The root directory tests are run from is different for 'flutter test'
|
||||||
|
// verses 'flutter test test/*_test.dart'. Adjust the root directory
|
||||||
|
// to access the assets directory.
|
||||||
|
final io.Directory rootDirectory = io.Directory.current.path.endsWith('${io.Platform.pathSeparator}test')
|
||||||
|
? io.Directory.current.parent
|
||||||
|
: io.Directory.current;
|
||||||
|
final io.File file = io.File('${rootDirectory.path}/test/assets/images/logo.png');
|
||||||
|
|
||||||
|
final ByteData asset = ByteData.view(file.readAsBytesSync().buffer);
|
||||||
|
return asset;
|
||||||
|
} else {
|
||||||
|
throw ArgumentError('Unknown asset key: $key');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,8 @@ dependency_overrides:
|
||||||
path: ./dependencies/dots_indicator
|
path: ./dependencies/dots_indicator
|
||||||
ed25519_edwards:
|
ed25519_edwards:
|
||||||
path: ./dependencies/ed25519_edwards
|
path: ./dependencies/ed25519_edwards
|
||||||
|
flutter_markdown_plus:
|
||||||
|
path: ./dependencies/flutter_markdown_plus
|
||||||
flutter_sharing_intent:
|
flutter_sharing_intent:
|
||||||
path: ./dependencies/flutter_sharing_intent
|
path: ./dependencies/flutter_sharing_intent
|
||||||
hand_signature:
|
hand_signature:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue