diff --git a/README.md b/README.md index bbe62b20b4..49f73985d7 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ These are the available packages in this repository. | [css\_colors](./packages/css_colors/) | [![pub package](https://img.shields.io/pub/v/css_colors.svg)](https://pub.dev/packages/css_colors) | | [extension\_google\_sign\_in\_as\_googleapis\_auth](./packages/extension_google_sign_in_as_googleapis_auth/) | [![pub package](https://img.shields.io/pub/v/extension_google_sign_in_as_googleapis_auth.svg)](https://pub.dev/packages/extension_google_sign_in_as_googleapis_auth) | | [fuchsia\_ctl](./packages/fuchsia_ctl/) | [![pub package](https://img.shields.io/pub/v/fuchsia_ctl.svg)](https://pub.dev/packages/fuchsia_ctl) | +| [flutter\_markdown](./packages/flutter_markdown/) | [![pub package](https://img.shields.io/pub/v/flutter_markdown.svg)](https://pub.dev/packages/flutter_markdown) | | [multicast\_dns](./packages/multicast_dns/) | [![pub package](https://img.shields.io/pub/v/multicast_dns.svg)](https://pub.dev/packages/multicast_dns) | | [palette\_generator](./packages/palette_generator/) | [![pub package](https://img.shields.io/pub/v/palette_generator.svg)](https://pub.dartlang.org/packages/palette_generator) | | [pigeon](./packages/pigeon/) | [![pub package](https://img.shields.io/pub/v/pigeon.svg)](https://pub.dev/packages/pigeon) | diff --git a/packages/flutter_markdown/.github/ISSUE_TEMPLATE/bug_report.md b/packages/flutter_markdown/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..4e9254e777 --- /dev/null +++ b/packages/flutter_markdown/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,24 @@ +--- +name: Bug report +about: Create an issue to report a bug +title: 'Bug: ' +labels: '' +assignees: '' + +--- + +**Describe the bug** +Please provide a clear and concise description of the bug. + +**How to reproduce** +Steps to reproduce the behavior: +1. + +**Expected behavior** +A description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain the problem. + +**Additional context** +Add any other context about the problem here. diff --git a/packages/flutter_markdown/.github/ISSUE_TEMPLATE/config.yml b/packages/flutter_markdown/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..09d8e31088 --- /dev/null +++ b/packages/flutter_markdown/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,3 @@ +# Use one of the three issue templates provided. The "Other issue" +# template should be used instead of a blank issue. +blank_issues_enabled: false \ No newline at end of file diff --git a/packages/flutter_markdown/.github/ISSUE_TEMPLATE/feature_request.md b/packages/flutter_markdown/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..93e105c34f --- /dev/null +++ b/packages/flutter_markdown/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an feature enhancement or new functionality +title: 'Feature: ' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of the problem. + +**Describe a possible solution.** +Do you have a possible solution or approach you think should be considered? + +**Describe possible alternatives.** +Are there possible alternative solutions or features. + +**Additional context.** +List any additional context or screenshots about the feature request here. diff --git a/packages/flutter_markdown/.github/ISSUE_TEMPLATE/other_issue.md b/packages/flutter_markdown/.github/ISSUE_TEMPLATE/other_issue.md new file mode 100644 index 0000000000..92d3fda17a --- /dev/null +++ b/packages/flutter_markdown/.github/ISSUE_TEMPLATE/other_issue.md @@ -0,0 +1,11 @@ +--- +name: Other issue +about: Issues that are not bugs or feature requests. +title: '' +labels: '' +assignees: '' + +--- + +**Describe the issue** +Please provide a clear and concise description of the issue. diff --git a/packages/flutter_markdown/.github/pull_request_template.md b/packages/flutter_markdown/.github/pull_request_template.md new file mode 100644 index 0000000000..e819a48d1c --- /dev/null +++ b/packages/flutter_markdown/.github/pull_request_template.md @@ -0,0 +1,23 @@ +**IMPORTANT: Please do not create a Pull Request without creating an issue first.** + +*Any change needs to be discussed before proceeding. Failure to do so may result in the rejection of the pull request.* + +Please provide enough information so that others can review your pull request: + + + +**Test plan (required)** + + + +**Closing issues** + + + +**Additional considerations** + + \ No newline at end of file diff --git a/packages/flutter_markdown/.github/workflows/flutter_ci.yml b/packages/flutter_markdown/.github/workflows/flutter_ci.yml new file mode 100644 index 0000000000..12e1facb5c --- /dev/null +++ b/packages/flutter_markdown/.github/workflows/flutter_ci.yml @@ -0,0 +1,35 @@ +name: flutter_markdown + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + workflow_dispatch: + schedule: + - cron: '0 0 * * *' # Every day at midnight + +defaults: + run: + shell: bash + +jobs: + flutter-tests: + name: Test Flutter ${{ matrix.flutter_version }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + flutter_version: [dev, beta] # Disable stable until Null Safety goes stable + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: '12.x' + - uses: subosito/flutter-action@v1 + with: + channel: ${{ matrix.flutter_version }} + - run: flutter analyze + - run: flutter format --dry-run --set-exit-if-changed . + - run: flutter test diff --git a/packages/flutter_markdown/.gitignore b/packages/flutter_markdown/.gitignore new file mode 100644 index 0000000000..437cb45872 --- /dev/null +++ b/packages/flutter_markdown/.gitignore @@ -0,0 +1,36 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Exceptions to above rules. +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/packages/flutter_markdown/.vscode/launch.json b/packages/flutter_markdown/.vscode/launch.json new file mode 100644 index 0000000000..1c3102dbd3 --- /dev/null +++ b/packages/flutter_markdown/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "flutter_markdown", + "request": "launch", + "type": "dart" + }, + { + "name": "example", + "cwd": "example", + "request": "launch", + "type": "dart" + } + ] +} \ No newline at end of file diff --git a/packages/flutter_markdown/CHANGELOG.md b/packages/flutter_markdown/CHANGELOG.md new file mode 100644 index 0000000000..cf6752919a --- /dev/null +++ b/packages/flutter_markdown/CHANGELOG.md @@ -0,0 +1,159 @@ +## 0.6.2 + + * Updated metadata for new source location + * Style changes to conform to flutter/packages analyzer settings + + ## 0.6.1 + + * Added builder option bulletBuilder + +## 0.6.0 + + * Null safety release + * Added stylesheet option listBulletPadding + * Fixed blockquote inline styling + * Added onTapText handler for selectable text + +## 0.6.0-nullsafety.2 + + * Dependencies updated for null safety + +## 0.6.0-nullsafety.1 + + * Fix null safety on web + * Image test mocks fixed for null safety + +## 0.6.0-nullsafety.0 + + * Initial null safety migration. + +## 0.5.2 + + * Added `MarkdownListItemCrossAxisAlignment` to allow for intrinsic height + measurements of lists. + +## 0.5.1 + + * Fix user defined builders + +## 0.5.0 + + * BREAKING CHANGE: `MarkdownTapLinkCallback` now has three parameters, not one, exposing more + information about a tapped link. + * Note for upgraders, the old single parameter `href` is now the second parameter to match the specification. + * Android example upgraded + * Test coverage updated to match GitHub Flavoured Markdown and CommonMark + * Handle links with empty descriptions + * Handle empty rows in tables + +## 0.4.4 + + * Fix handling of newline character in blockquote + * Add new example demo + * Use the start attribute in ordered list to set the first number + * Revert changes made in PR #235 (which broke newline handling) + +## 0.4.3 + + * Fix merging of `MarkdownStyleSheets` + * Fix `MarkdownStyleSheet` textScaleFactor to use default value of 1.0, if not provided, instead using the textScaleFactor of the nearest MediaQuery + +## 0.4.2 + + * Fix parsing of image caption & alt attributes + * Fix baseline alignment in lists + * Support `LineBreakSyntax` + +## 0.4.1 + + * Downgrade Flutter minimum from 1.17.1 to 1.17.0 for Pub + +## 0.4.0 + + * Updated for Flutter 1.17 + * Ignore newlines in paragraphs + * Improve handling of horizontal rules + +## 0.3.5 + + * Fix hardcoded colors and improve Darktheme + * Fix text alignment when formatting is involved + +## 0.3.4 + + * Add support for text paragraphs and blockquotes. + +## 0.3.3 + + * Add the ability to control the scroll position of the `MarkdownWidget`. + +## 0.3.2 + + * Uplift `package:markdown` dependency version to enable deleting HTML unescape URI workaround + * Explictly state that Flutter 1.10.7 is the minimum supported Flutter version in the library `pubspec.yaml`. + +## 0.3.1 + + * Expose `tableColumnWidth` + * Add `MarkdownStyleSheet.fromCupertinoTheme` + * Fix `MarkdownStyleSheet.blockquote` + * Flutter for web support + * Add physic and shrinkWrap to Markdown widget + * Add MarkdownBody.fitContent + * Support select text to copy + * Fix list bullet alignment + * HTML unescape URIs (temporary workaround for [dart-lang/markdown #272](https://github.com/dart-lang/markdown/issues/272)) + * Rebuilt `example/android` and `example/ios` directories + +**Note:** this version has an implicit minimum supported version of Flutter 1.10.7. +See [flutter/flutter_markdown issue #156](https://github.com/flutter/flutter_markdown/issues/156) for more detail. + +## 0.3.0 + + * Support GitHub flavoured Markdown + * Support strikethrough + * Convert TextSpan to use new InlineSpan API + +## 0.2.0 + + * Updated environment sdk constraints to make the package + Dart 2 compatible. As a result, usage of this version and higher + requires a Dart 2 SDK. + +## 0.1.6 + + * Updated `markdown` dependency. + +## 0.1.5 + + * Add `mockito` as a dev dependency. Eliminate use of `package:http`, which + is no longer part of Flutter. + +## 0.1.4 + + * Add `li` style to bullets + +## 0.1.3 + + * Add `path` and `http` as declared dependencies in `pubspec.yaml` + +## 0.1.2 + + * Add support for horizontal rules. + * Fix the `onTap` callback on images nested in hyperlinks + +## 0.1.1 + + * Add support for local file paths in image links. Make sure to set the + `imageDirectory` property to specify the base directory containing the image + files. + +## 0.1.0 + + * Roll the dependency on `markdown` to 1.0.0 + * Add a test and example for image links + * Fix the `onTap` callback on hyperlinks + +## 0.0.9 + + * First published version diff --git a/packages/flutter_markdown/LICENSE b/packages/flutter_markdown/LICENSE new file mode 100644 index 0000000000..e7892520aa --- /dev/null +++ b/packages/flutter_markdown/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2017 Google, Inc. 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. diff --git a/packages/flutter_markdown/README.md b/packages/flutter_markdown/README.md new file mode 100644 index 0000000000..2852bf330a --- /dev/null +++ b/packages/flutter_markdown/README.md @@ -0,0 +1,148 @@ +# Flutter Markdown +[![pub package](https://img.shields.io/pub/v/flutter_markdown.svg)](https://pub.dartlang.org/packages/flutter_markdown) +[![Build Status](https://github.com/flutter/flutter_markdown/workflows/flutter_markdown/badge.svg)](https://github.com/flutter/flutter_markdown/actions?workflow=flutter_markdown) + + +A markdown renderer for Flutter. It supports the +[original format](https://daringfireball.net/projects/markdown/), but no inline +HTML. + +## Overview + +The [`flutter_markdown`](https://pub.dev/packages/flutter_markdown) package +renders Markdown, a lightweight markup language, into a Flutter widget +containing a rich text representation. + +`flutter_markdown` is built on top of the Dart +[`markdown`](https://pub.dev/packages/markdown) package, which parses +the Markdown into an abstract syntax tree (AST). The nodes of the AST are an +HTML representation of the Markdown data. + +## Flutter Isn't a HTML Renderer + +While this approach to creating a rich text representation of Markdown source +text in Flutter works well, Flutter isn't a HTML renderer like a web browser. +Markdown was developed by John Gruber in 2004 to allow users to turn readable, +plain text content into rich text HTML. This close association with HTML allows +for the injection of HTML into the Markdown source data. Markdown parsers +generally ignore hand-tuned HTML and pass it through to be included in the +generated HTML. This *trick* has allowed users to perform some customization +or tweaking of the HTML output. A common HTML tweak is to insert HTML line-break +elements **\
** in Markdown source to force additional line breaks not +supported by the Markdown syntax. This HTML *trick* doesn't apply to Flutter. The +`flutter_markdown` package doesn't support inline HTML. + +## Markdown Specifications and `flutter_markdown` Compliance + +There are three seminal documents regarding Markdown syntax; the +[original Markdown syntax documentation](https://daringfireball.net/projects/markdown/syntax) +specified by John Gruber, the +[CommonMark specification](https://spec.commonmark.org/0.29/), and the +[GitHub Flavored Markdown specification](https://github.github.com/gfm/). + +The CommonMark specification brings order to and clarifies the Markdown syntax +cases left ambiguous or unclear in the Gruber document. GitHub Flavored +Markdown (GFM) is a strict superset of CommonMark used by GitHub. + +The `markdown` package, and in extension, the `flutter_markdown` package, supports +four levels of Markdown syntax; basic, CommonMark, GitHub Flavored, and GitHub +Web. Basic, CommonMark, and GitHub Flavored adhere to the three Markdown +documents, respectively. GitHub Web adds header ID and emoji support. The +`flutter_markdown` package defaults to GitHub Flavored Markdown. + +## Getting Started + +Using the Markdown widget is simple, just pass in the source markdown as a +string: + + Markdown(data: markdownSource); + +If you do not want the padding or scrolling behavior, use the MarkdownBody +instead: + + MarkdownBody(data: markdownSource); + +By default, Markdown uses the formatting from the current material design theme, +but it's possible to create your own custom styling. Use the MarkdownStyle class +to pass in your own style. If you don't want to use Markdown outside of material +design, use the MarkdownRaw class. + +## Emoji Support + +Emoji glyphs can be included in the formatted text displayed by the Markdown +widget by either inserting the emoji glyph directly or using the inline emoji +tag syntax in the source Markdown document. + +Markdown documents using UTF-8 encoding can insert emojis, symbols, and other +Unicode characters directly in the source document. Emoji glyphs inserted +directly in the Markdown source data are treated as text and preserved in the +formatted output of the Markdown widget. For example, in the following Markdown +widget constructor, a text string with a smiley face emoji is passed in as the +source Markdown data. + +``` +Markdown( + controller: controller, + selectable: true, + data: 'Insert emoji here😀 ', +) +``` + +The resulting Markdown widget will contain a single line of text with the +emoji preserved in the formatted text output. + +The second method for including emoji glyphs is to provide the Markdown +widget with a syntax extension for inline emoji tags. The Markdown +package includes a syntax extension for emojis, EmojiSyntax. The default +extension set used by the Markdown widget is the GitHub flavored extension +set. This pre-defined extension set approximates the GitHub supported +Markdown tags, providing syntax handlers for fenced code blocks, tables, +auto-links, and strike-through. To include the inline emoji tag syntax +while maintaining the default GitHub flavored Markdown behavior, define +an extension set that combines EmojiSyntax with ExtensionSet.gitHubFlavored. + +``` +import 'package:markdown/markdown.dart' as md; + +Markdown( + controller: controller, + selectable: true, + data: 'Insert emoji :smiley: here', + extensionSet: md.ExtensionSet( + md.ExtensionSet.gitHubFlavored.blockSyntaxes, + [md.EmojiSyntax(), ...md.ExtensionSet.gitHubFlavored.inlineSyntaxes], + ), +) +``` + +## Image Support + +The `Img` tag only supports the following image locations: + +* From the network: Use a URL prefixed by either `http://` or `https://`. + +* From local files on the device: Use an absolute path to the file, for example by + concatenating the file name with the path returned by a known storage location, + such as those provided by the [`path_provider`](https://pub.dartlang.org/packages/path_provider) + plugin. + +* From image locations referring to bundled assets: Use an asset name prefixed by `resource:`. + like `resource:assets/image.png`. + +## Verifying Markdown Behavior + +Verifying Markdown behavior in other applications can often be useful to track +down or identify unexpected output from the `flutter_markdown` package. Two +valuable resources are the +[Dart Markdown Live Editor](https://dart-lang.github.io/markdown/) and the +[Markdown Live Preview](https://markdownlivepreview.com/). These two resources +are dynamic, online Markdown viewers. + +## Markdown Resources + +Here are some additional Markdown syntax resources: + +- [Markdown Guide](https://www.markdownguide.org/) +- [CommonMark Markdown Reference](https://commonmark.org/help/) +- [GitHub Guides - Mastering Markdown](https://guides.github.com/features/mastering-markdown/#GitHub-flavored-markdown) + - [Download PDF cheatsheet version](https://guides.github.com/pdfs/markdown-cheatsheet-online.pdf) diff --git a/packages/flutter_markdown/example/.gitignore b/packages/flutter_markdown/example/.gitignore new file mode 100644 index 0000000000..437cb45872 --- /dev/null +++ b/packages/flutter_markdown/example/.gitignore @@ -0,0 +1,36 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Exceptions to above rules. +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/packages/flutter_markdown/example/.metadata b/packages/flutter_markdown/example/.metadata new file mode 100644 index 0000000000..962d678e2d --- /dev/null +++ b/packages/flutter_markdown/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 532a8fed41a4f6595965f02f3edf9666ba5ebf44 + channel: master + +project_type: app diff --git a/packages/flutter_markdown/example/README.md b/packages/flutter_markdown/example/README.md new file mode 100644 index 0000000000..1529200b10 --- /dev/null +++ b/packages/flutter_markdown/example/README.md @@ -0,0 +1,16 @@ +# flutter_markdown_example + +Demonstrates how to use the flutter_markdown package. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/flutter_markdown/example/android/.gitignore b/packages/flutter_markdown/example/android/.gitignore new file mode 100644 index 0000000000..0a741cb43d --- /dev/null +++ b/packages/flutter_markdown/example/android/.gitignore @@ -0,0 +1,11 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties diff --git a/packages/flutter_markdown/example/android/app/build.gradle b/packages/flutter_markdown/example/android/app/build.gradle new file mode 100644 index 0000000000..e1a9001e29 --- /dev/null +++ b/packages/flutter_markdown/example/android/app/build.gradle @@ -0,0 +1,59 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 30 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "io.flutter.packages.flutter_markdown_example" + minSdkVersion 16 + targetSdkVersion 30 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/packages/flutter_markdown/example/android/app/src/debug/AndroidManifest.xml b/packages/flutter_markdown/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000000..a29a9b499a --- /dev/null +++ b/packages/flutter_markdown/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/flutter_markdown/example/android/app/src/main/AndroidManifest.xml b/packages/flutter_markdown/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..6d02f614dc --- /dev/null +++ b/packages/flutter_markdown/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + diff --git a/packages/flutter_markdown/example/android/app/src/main/kotlin/io/flutter/packages/flutter_markdown_example/MainActivity.kt b/packages/flutter_markdown/example/android/app/src/main/kotlin/io/flutter/packages/flutter_markdown_example/MainActivity.kt new file mode 100644 index 0000000000..d55943d22c --- /dev/null +++ b/packages/flutter_markdown/example/android/app/src/main/kotlin/io/flutter/packages/flutter_markdown_example/MainActivity.kt @@ -0,0 +1,6 @@ +package io.flutter.packages.flutter_markdown_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/packages/flutter_markdown/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/flutter_markdown/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000000..f74085f3f6 --- /dev/null +++ b/packages/flutter_markdown/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/flutter_markdown/example/android/app/src/main/res/drawable/launch_background.xml b/packages/flutter_markdown/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000000..304732f884 --- /dev/null +++ b/packages/flutter_markdown/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/flutter_markdown/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/flutter_markdown/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..db77bb4b7b Binary files /dev/null and b/packages/flutter_markdown/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/flutter_markdown/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/flutter_markdown/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..17987b79bb Binary files /dev/null and b/packages/flutter_markdown/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/flutter_markdown/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/flutter_markdown/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..09d4391482 Binary files /dev/null and b/packages/flutter_markdown/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/flutter_markdown/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/flutter_markdown/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..d5f1c8d34e Binary files /dev/null and b/packages/flutter_markdown/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/flutter_markdown/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/flutter_markdown/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..4d6372eebd Binary files /dev/null and b/packages/flutter_markdown/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/flutter_markdown/example/android/app/src/main/res/values-night/styles.xml b/packages/flutter_markdown/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000000..449a9f9308 --- /dev/null +++ b/packages/flutter_markdown/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/flutter_markdown/example/android/app/src/main/res/values/styles.xml b/packages/flutter_markdown/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000000..d74aa35c28 --- /dev/null +++ b/packages/flutter_markdown/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/flutter_markdown/example/android/app/src/profile/AndroidManifest.xml b/packages/flutter_markdown/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000000..a29a9b499a --- /dev/null +++ b/packages/flutter_markdown/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/flutter_markdown/example/android/build.gradle b/packages/flutter_markdown/example/android/build.gradle new file mode 100644 index 0000000000..c505a86352 --- /dev/null +++ b/packages/flutter_markdown/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.3.50' + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/packages/flutter_markdown/example/android/gradle.properties b/packages/flutter_markdown/example/android/gradle.properties new file mode 100644 index 0000000000..94adc3a3f9 --- /dev/null +++ b/packages/flutter_markdown/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/packages/flutter_markdown/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/flutter_markdown/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..bc6a58afdd --- /dev/null +++ b/packages/flutter_markdown/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/packages/flutter_markdown/example/android/settings.gradle b/packages/flutter_markdown/example/android/settings.gradle new file mode 100644 index 0000000000..44e62bcf06 --- /dev/null +++ b/packages/flutter_markdown/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/packages/flutter_markdown/example/assets/logo.png b/packages/flutter_markdown/example/assets/logo.png new file mode 100644 index 0000000000..00357cb9c9 Binary files /dev/null and b/packages/flutter_markdown/example/assets/logo.png differ diff --git a/packages/flutter_markdown/example/assets/markdown_test_page.md b/packages/flutter_markdown/example/assets/markdown_test_page.md new file mode 100644 index 0000000000..f550015a0f --- /dev/null +++ b/packages/flutter_markdown/example/assets/markdown_test_page.md @@ -0,0 +1,199 @@ +# Markdown Test Page + +# Basic Syntax + +## Headings +--- +# Heading One + +Sint sit cillum pariatur eiusmod nulla pariatur ipsum. Sit laborum anim qui mollit tempor pariatur nisi minim dolor. Aliquip et adipisicing sit sit fugiat commodo id sunt. Nostrud enim ad commodo incididunt cupidatat in ullamco ullamco Lorem cupidatat velit enim et Lorem. + +## Heading Two + +Aute officia nulla deserunt do deserunt cillum velit magna. Officia veniam culpa anim minim dolore labore pariatur voluptate id ad est duis quis velit dolor pariatur enim. Incididunt enim excepteur do veniam consequat culpa do voluptate dolor fugiat ad adipisicing sit. + +### Heading Three + +Voluptate cupidatat cillum elit quis ipsum eu voluptate fugiat consectetur enim. Quis ut voluptate culpa ex anim aute consectetur dolore proident voluptate exercitation eiusmod. Esse in do anim magna minim culpa sint. + +#### Heading Four + +Commodo fugiat aliqua minim quis pariatur mollit id tempor. Non occaecat minim esse enim aliqua adipisicing nostrud duis consequat eu adipisicing qui. Minim aliquip sit excepteur ipsum consequat laborum pariatur excepteur. + +##### Heading Five + +Veniam enim esse amet veniam deserunt laboris amet enim consequat. Minim nostrud deserunt cillum consectetur commodo eu enim nostrud ullamco occaecat excepteur. Aliquip et ut est commodo enim dolor amet sint excepteur. + +###### Heading Six + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + +## Paragraphs +--- +Incididunt ex adipisicing ea ullamco consectetur in voluptate proident fugiat tempor deserunt reprehenderit ullamco id dolore laborum. Do laboris laboris minim incididunt qui consectetur exercitation adipisicing dolore et magna consequat magna anim sunt. Officia fugiat Lorem sunt pariatur incididunt Lorem reprehenderit proident irure. + +Officia dolore laborum aute incididunt commodo nisi velit est est elit et dolore elit exercitation. Enim aliquip magna id ipsum aliquip consectetur ad nulla quis. Incididunt pariatur dolor consectetur cillum enim velit cupidatat laborum quis ex. + +Officia irure in non voluptate adipisicing sit amet tempor duis dolore deserunt enim ut. Reprehenderit incididunt in ad anim et deserunt deserunt Lorem laborum quis. Enim aute anim labore proident laboris voluptate elit excepteur in. + +## Inline Emphasis - Bold and Italic +--- +Sint ea do **exercitation** incididunt et minim ad labore sunt. Minim deserunt labore laboris velit nulla incididunt ipsum nulla. **Consequat commodo** reprehenderit duis esse esse ex**ercita**tion minim enim Lorem dolore duis irure. Et ad ipsum labore enim ipsum **cupidatat consequat**. Commodo non ea __cupidatat magna__ deserunt dolore ipsum velit nulla elit veniam nulla eiusmod proident officia. + +*"Proident sit veniam in est proident officia adipisicing"* ea tempor cillum non cillum velit deserunt. Sit tempor ad*ipisic*ing ea laboris anim aute reprehenderit id eu ea. Aute Lorem minim _adipisicing nostrud mollit_ ad ut voluptate do nulla esse occaecat aliqua sint anim. + +## Blockquotes +--- +Ad nisi laborum aute cupidatat magna deserunt eu id laboris id. Aliquip nulla cupidatat sint ex Lorem mollit laborum dolor amet est ut esse aute. Nostrud ex consequat id incididunt proident ipsum minim duis aliqua ut ex et ad quis. + +> Ipsum et cupidatat mollit exercitation enim duis sunt irure aliqua reprehenderit mollit. Pariatur Lorem pariatur laboris do culpa do elit irure. + +Labore ea magna Lorem consequat aliquip consectetur cillum duis dolore. Et veniam dolor qui incididunt minim amet laboris sit. + +> Qui est sit et reprehenderit aute est esse enim aliqua id aliquip ea anim. Pariatur sint reprehenderit mollit velit voluptate enim consectetur sint enim. Quis exercitation proident elit non id qui culpa dolore esse aliquip consequat. + +## Lists +--- +### Ordered List + +1. Longan +2. Lychee +3. Excepteur ad cupidatat do elit laborum amet cillum reprehenderit consequat quis. + Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip aliquip velit pariatur dolore. +4. Marionberry +4. Melon + - Cantaloupe + - Honeydew + - Watermelon +6. Miracle fruit +7. Mulberry + + +9. Strawberry +12. Plum + +### Unordered List + +- Olive +- Orange + - Blood orange + - Clementine +- Papaya +- Ut aute ipsum occaecat nisi culpa Lorem id occaecat cupidatat id id magna laboris ad duis. Fugiat cillum dolore veniam nostrud proident sint consectetur eiusmod irure adipisicing. +- Passionfruit + +## Inline Code +--- +Duis duis est `code in text` velit velit aute culpa ex quis pariatur pariatur laborum aute pariatur duis tempor sunt ad. Irure magna voluptate dolore consectetur consectetur irure esse. Anim magna `md.ExtensionSet.GitHub` dolor. + +## Horizontal Rule +--- +In dolore velit aliquip labore mollit minim tempor veniam eu veniam ad in sint aliquip mollit mollit. Ex occaecat non deserunt elit laborum sunt tempor sint consequat culpa culpa qui sit. + +*** +or + +--- +or +____________ + +In laboris eiusmod reprehenderit aliquip sit proident occaecat. Non sit labore anim elit veniam Lorem minim commodo eiusmod irure do minim nisi. + +## Link +--- +[Google]: https://www.google.com + +Excepteur ad cupidatat do elit laborum amet cillum reprehenderit consequat quis. +Deserunt officia esse [Flutter](https://www.flutter.dev) aliquip consectetur duis ut labore laborum commodo aliquip aliquip velit pariatur dolore. + +[Flutter](https://www.flutter.dev) + +Excepteur ad cupidatat do *[Flutter](https://www.flutter.dev)* elit laborum amet cillum reprehenderit **[Dart](https://www.dart.dev)** aliquip sit proident occaecat. Non sit labore anim elit veniam Lorem minim commodo eiusmod irure do minim nisi. + +In laboris eiusmod reprehenderit aliquip sit proident occaecat. Non sit labore anim elit veniam Lorem minim commodo eiusmod irure do minim nisi [Google's Home Page][Google]. + + +## Image +--- + +Minim id consequat adipisicing cupidatat laborum culpa veniam non consectetur et duis pariatur reprehenderit eu ex consectetur. Sunt nisi qui eiusmod ut cillum laborum Lorem officia aliquip laboris ullamco nostrud laboris non irure laboris. + +![Super wide](https://picsum.photos/id/15/1280/800) + +In laboris eiusmod reprehenderit aliquip sit proident occaecat. Non sit labore anim elit veniam Lorem minim commodo eiusmod irure do minim nisi. + +![Flutter logo](/dart-lang/site-shared/master/src/_assets/image/flutter/icon/64.png) + +Officia irure in non voluptate adipisicing sit amet tempor duis dolore deserunt enim ut. Reprehenderit incididunt in ad anim et deserunt deserunt Lorem laborum quis. Enim aute anim labore proident laboris voluptate elit excepteur in. + +![Not so big](https://picsum.photos/id/180/480/400) + +In laboris eiusmod reprehenderit aliquip sit proident occaecat. Non sit labore anim elit veniam Lorem minim commodo eiusmod irure do minim nisi. + +![alt](resource:assets/logo.png) + +# Extended Syntax + +# Table +--- +Duis sunt ut pariatur reprehenderit mollit mollit magna dolore in pariatur nulla commodo sit dolor ad fugiat. + +| Table Heading 1 | Table Heading 2 | Table Heading 3 | +| --------------- | --------------- | --------------- | +| Item 1 | Item 2 | Item 3 | +| Item 1 | Item 2 | Item 3 | +| Item 1 | Item 2 | Item 3 | + +Ex amet id ex aliquip id do laborum excepteur exercitation elit sint commodo occaecat nostrud est. Nostrud pariatur esse veniam laborum non sint magna sit laboris minim in id. + +## Fenced Code Block +--- + +Et fugiat ad nisi amet magna labore do cillum fugiat occaecat cillum Lorem proident. In sint dolor ullamco ad do adipisicing amet. + +``` +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:markdown/markdown.dart' as md; + +const String _markdownData = """ +# Markdown Example +Markdown allows you to easily include formatted text, images, +and even formatted Dart code in your app. +"""; + +void main() { + runApp( + MaterialApp( + title: "Markdown Demo", + home: Scaffold( + appBar: AppBar( + title: const Text('Markdown Demo'), + ), + body: SafeArea( + child: Markdown( + data: _markdownData, + ), + ), + ), + ), + ); +} +``` + +## Strikethrough +--- + +~~Voluptate cupidatat cillum elit quis ipsum eu voluptate fugiat consectetur enim. Quis ut voluptate culpa ex anim aute consectetur dolore proident voluptate exercitation eiusmod. Esse in do anim magna minim culpa sint.~~ + +Commodo ***~~fugiat aliqua minim quis pariatur mollit~~*** id tempor. Non occaecat **~~minim~~** anim aute ~~**consectetur esse**~~ enim aliqua *~~adipisicing~~* nostrud duis consequat eu ~~*adipisicing*~~ qui. Minim aliquip ~~***sit excepteur ipsum consequat***~~ laborum pariatur excepteur. + +## Task List +--- +Officia irure in non voluptate adipisicing sit amet tempor duis dolore deserunt enim ut. Reprehenderit incididunt in ad anim et deserunt deserunt Lorem laborum quis. Enim aute anim labore proident laboris voluptate elit excepteur in. + +- [ ] laboris voluptate +- [x] consectetur +- [ ] eiusmod irure do + +Labore ea magna Lorem consequat aliquip consectetur cillum duis dolore. Et veniam dolor qui incididunt minim amet laboris sit. diff --git a/packages/flutter_markdown/example/assets/original_markdown_example_data.md b/packages/flutter_markdown/example/assets/original_markdown_example_data.md new file mode 100644 index 0000000000..56e3f66263 --- /dev/null +++ b/packages/flutter_markdown/example/assets/original_markdown_example_data.md @@ -0,0 +1,112 @@ +# Markdown Example +Markdown allows you to easily include formatted text, images, and even formatted Dart code in your app. + +## Titles + +Setext-style + +``` +This is an H1 +============= + +This is an H2 +------------- +``` + +Atx-style + +``` +# This is an H1 + +## This is an H2 + +###### This is an H6 +``` + +Select the valid headers: + +- [x] `# hello` +- [ ] `#hello` + +## Links + +[Google's Homepage][Google] + +``` +[inline-style](https://www.google.com) + +[reference-style][Google] +``` + +## Images + +![Flutter logo](/dart-lang/site-shared/master/src/_assets/image/flutter/icon/64.png) + +## Tables + +|Syntax |Result | +|---------------------------------------|-------------------------------------| +|`*italic 1*` |*italic 1* | +|`_italic 2_` | _italic 2_ | +|`**bold 1**` |**bold 1** | +|`__bold 2__` |__bold 2__ | +|`This is a ~~strikethrough~~` |This is a ~~strikethrough~~ | +|`***italic bold 1***` |***italic bold 1*** | +|`___italic bold 2___` |___italic bold 2___ | +|`***~~italic bold strikethrough 1~~***`|***~~italic bold strikethrough 1~~***| +|`~~***italic bold strikethrough 2***~~`|~~***italic bold strikethrough 2***~~| + +## Styling +Style text as _italic_, __bold__, ~~strikethrough~~, or `inline code`. + +- Use bulleted lists +- To better clarify +- Your points + +## Code blocks +Formatted Dart code looks really pretty too: + +``` +void main() { + runApp(MaterialApp( + home: Scaffold( + body: Markdown(data: markdownData), + ), + )); +} +``` + +## Center Title + +###### ※ ※ ※ + +_* How to implement it see main.dart#L129 in example._ + +## Custom Syntax + +NaOH + Al_2O_3 = NaAlO_2 + H_2O + +C_4H_10 = C_2H_6 + C_2H_4 + +## Markdown widget + +This is an example of how to create your own Markdown widget: + + Markdown(data: 'Hello _world_!'); + +Enjoy! + +[Google]: https://www.google.com/ + +## Line Breaks + +This is an example of how to create line breaks (tab or two whitespaces): + +line 1 + + +line 2 + + + +line 3 diff --git a/packages/flutter_markdown/example/fonts/RobotoMono-Regular.ttf b/packages/flutter_markdown/example/fonts/RobotoMono-Regular.ttf new file mode 100644 index 0000000000..7c4ce36a44 Binary files /dev/null and b/packages/flutter_markdown/example/fonts/RobotoMono-Regular.ttf differ diff --git a/packages/flutter_markdown/example/ios/.gitignore b/packages/flutter_markdown/example/ios/.gitignore new file mode 100644 index 0000000000..e96ef602b8 --- /dev/null +++ b/packages/flutter_markdown/example/ios/.gitignore @@ -0,0 +1,32 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/flutter_markdown/example/ios/Flutter/AppFrameworkInfo.plist b/packages/flutter_markdown/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000000..9367d483e4 --- /dev/null +++ b/packages/flutter_markdown/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 8.0 + + diff --git a/packages/flutter_markdown/example/ios/Flutter/Debug.xcconfig b/packages/flutter_markdown/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000000..592ceee85b --- /dev/null +++ b/packages/flutter_markdown/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/packages/flutter_markdown/example/ios/Flutter/Release.xcconfig b/packages/flutter_markdown/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000000..592ceee85b --- /dev/null +++ b/packages/flutter_markdown/example/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/packages/flutter_markdown/example/ios/Runner.xcodeproj/project.pbxproj b/packages/flutter_markdown/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..e757aaeab9 --- /dev/null +++ b/packages/flutter_markdown/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,471 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.packages.flutterMarkdownExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.packages.flutterMarkdownExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.packages.flutterMarkdownExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/flutter_markdown/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/flutter_markdown/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/packages/flutter_markdown/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/flutter_markdown/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/flutter_markdown/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/packages/flutter_markdown/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/flutter_markdown/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/flutter_markdown/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000000..f9b0d7c5ea --- /dev/null +++ b/packages/flutter_markdown/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/flutter_markdown/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/flutter_markdown/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000000..a28140cfdb --- /dev/null +++ b/packages/flutter_markdown/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/flutter_markdown/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/flutter_markdown/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..1d526a16ed --- /dev/null +++ b/packages/flutter_markdown/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/flutter_markdown/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/flutter_markdown/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/packages/flutter_markdown/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/flutter_markdown/example/ios/Runner/AppDelegate.swift b/packages/flutter_markdown/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000000..70693e4a8c --- /dev/null +++ b/packages/flutter_markdown/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..d36b1fab2d --- /dev/null +++ b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000..dc9ada4725 Binary files /dev/null and b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000000..28c6bf0301 Binary files /dev/null and b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000..2ccbfd967d Binary files /dev/null and b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000..f091b6b0bc Binary files /dev/null and b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000..4cde12118d Binary files /dev/null and b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000..d0ef06e7ed Binary files /dev/null and b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000000..dcdc2306c2 Binary files /dev/null and b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000000..2ccbfd967d Binary files /dev/null and b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000..c8f9ed8f5c Binary files /dev/null and b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000000..a6d6b8609d Binary files /dev/null and b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000..a6d6b8609d Binary files /dev/null and b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000..75b2d164a5 Binary files /dev/null and b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000000..c4df70d39d Binary files /dev/null and b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000000..6a84f41e14 Binary files /dev/null and b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000000..d0e1f58536 Binary files /dev/null and b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000000..0bedcf2fd4 --- /dev/null +++ b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000000..9da19eacad Binary files /dev/null and b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000..9da19eacad Binary files /dev/null and b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000..9da19eacad Binary files /dev/null and b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000000..89c2725b70 --- /dev/null +++ b/packages/flutter_markdown/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/flutter_markdown/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/flutter_markdown/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..f2e259c7c9 --- /dev/null +++ b/packages/flutter_markdown/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/flutter_markdown/example/ios/Runner/Base.lproj/Main.storyboard b/packages/flutter_markdown/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000000..f3c28516fb --- /dev/null +++ b/packages/flutter_markdown/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/flutter_markdown/example/ios/Runner/Info.plist b/packages/flutter_markdown/example/ios/Runner/Info.plist new file mode 100644 index 0000000000..2fb29c6052 --- /dev/null +++ b/packages/flutter_markdown/example/ios/Runner/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + flutter_markdown_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/packages/flutter_markdown/example/ios/Runner/Runner-Bridging-Header.h b/packages/flutter_markdown/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000000..308a2a560b --- /dev/null +++ b/packages/flutter_markdown/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/packages/flutter_markdown/example/lib/demos/basic_markdown_demo.dart b/packages/flutter_markdown/example/lib/demos/basic_markdown_demo.dart new file mode 100644 index 0000000000..41363e7230 --- /dev/null +++ b/packages/flutter_markdown/example/lib/demos/basic_markdown_demo.dart @@ -0,0 +1,170 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; + +import '../shared/dropdown_menu.dart'; +import '../shared/markdown_demo_widget.dart'; +import '../shared/markdown_extensions.dart'; + +// ignore_for_file: public_member_api_docs + +const String _notes = """ +# Basic Markdown Demo +--- +The Basic Markdown Demo shows the effect of the four Markdown extension sets +on formatting basic and extended Markdown tags. + +## Overview + +The Dart [markdown](https://pub.dev/packages/markdown) package parses Markdown +into HTML. The flutter_markdown package builds on this package using the +abstract syntax tree generated by the parser to make a tree of widgets instead +of HTML elements. + +The markdown package supports the basic block and inline Markdown syntax +specified in the original Markdown implementation as well as a few Markdown +extensions. The markdown package uses extension sets to make extension +management easy. There are four pre-defined extension sets; none, Common Mark, +GitHub Flavored, and GitHub Web. The default extension set used by the +flutter_markdown package is GitHub Flavored. + +The Basic Markdown Demo shows the effect each of the pre-defined extension sets +has on a test Markdown document with basic and extended Markdown tags. Use the +Extension Set dropdown menu to select an extension set and view the Markdown +widget's output. + +## Comments + +Since GitHub Flavored is the default extension set, it is the initial setting +for the formatted Markdown view in the demo. +"""; + +class BasicMarkdownDemo extends StatefulWidget implements MarkdownDemoWidget { + const BasicMarkdownDemo({Key? key}) : super(key: key); + + static const String _title = 'Basic Markdown Demo'; + + @override + String get title => BasicMarkdownDemo._title; + + @override + String get description => 'Shows the effect the four Markdown extension sets ' + 'have on basic and extended Markdown tagged elements.'; + + @override + Future get data => + rootBundle.loadString('assets/markdown_test_page.md'); + + @override + Future get notes => Future.value(_notes); + + @override + _BasicMarkdownDemoState createState() => _BasicMarkdownDemoState(); +} + +class _BasicMarkdownDemoState extends State { + MarkdownExtensionSet _extensionSet = MarkdownExtensionSet.githubFlavored; + + final Map _menuItems = + Map.fromIterables( + MarkdownExtensionSet.values.map((MarkdownExtensionSet e) => e.displayTitle), + MarkdownExtensionSet.values, + ); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: widget.data, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return Column( + children: [ + DropdownMenu( + items: _menuItems, + label: 'Extension Set:', + initialValue: _extensionSet, + onChanged: (MarkdownExtensionSet? value) { + if (value != _extensionSet) { + setState(() { + _extensionSet = value!; + }); + } + }, + ), + Expanded( + child: Markdown( + key: Key(_extensionSet.name), + data: snapshot.data!, + imageDirectory: 'https://raw.githubusercontent.com', + extensionSet: _extensionSet.value, + onTapLink: (String text, String? href, String title) => + linkOnTapHandler(context, text, href, title), + ), + ), + ], + ); + } else { + return const CircularProgressIndicator(); + } + }, + ); + } + + // Handle the link. The [href] in the callback contains information + // from the link. The url_launcher package or other similar package + // can be used to execute the link. + Future linkOnTapHandler( + BuildContext context, + String text, + String? href, + String title, + ) async { + showDialog( + context: context, + builder: (BuildContext context) => + _createDialog(context, text, href, title), + ); + } + + Widget _createDialog( + BuildContext context, String text, String? href, String title) => + AlertDialog( + title: const Text('Reference Link'), + content: SingleChildScrollView( + child: ListBody( + children: [ + Text( + 'See the following link for more information:', + style: Theme.of(context).textTheme.bodyText1, + ), + const SizedBox(height: 8), + Text( + 'Link text: $text', + style: Theme.of(context).textTheme.bodyText2, + ), + const SizedBox(height: 8), + Text( + 'Link destination: $href', + style: Theme.of(context).textTheme.bodyText2, + ), + const SizedBox(height: 8), + Text( + 'Link title: $title', + style: Theme.of(context).textTheme.bodyText2, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ) + ], + ); +} diff --git a/packages/flutter_markdown/example/lib/demos/centered_header_demo.dart b/packages/flutter_markdown/example/lib/demos/centered_header_demo.dart new file mode 100644 index 0000000000..ad3632c6eb --- /dev/null +++ b/packages/flutter_markdown/example/lib/demos/centered_header_demo.dart @@ -0,0 +1,77 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/flutter_markdown.dart'; +import 'package:markdown/markdown.dart' as md; +import '../shared/markdown_demo_widget.dart'; + +// ignore_for_file: public_member_api_docs + +const String _data = ''' +## Centered Title + +###### ※ ※ ※ + +'''; + +const String _notes = ''' +# Centered Title Demo +--- + +## Overview +This example demonstrates how to implement a centered headline using a custom builder. + +'''; + +class CenteredHeaderDemo extends StatelessWidget implements MarkdownDemoWidget { + const CenteredHeaderDemo({Key? key}) : super(key: key); + + static const String _title = 'Centered Header Demo'; + + @override + String get title => CenteredHeaderDemo._title; + + @override + String get description => + 'An example of using a user defined builder to implement a centered headline'; + + @override + Future get data => Future.value(_data); + + @override + Future get notes => Future.value(_notes); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: data, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return Markdown( + data: snapshot.data!, + builders: { + 'h2': CenteredHeaderBuilder(), + 'h6': CenteredHeaderBuilder(), + }, + ); + } else { + return const CircularProgressIndicator(); + } + }, + ); + } +} + +class CenteredHeaderBuilder extends MarkdownElementBuilder { + @override + Widget visitText(md.Text text, TextStyle? preferredStyle) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(text.text, style: preferredStyle), + ], + ); + } +} diff --git a/packages/flutter_markdown/example/lib/demos/extended_emoji_demo.dart b/packages/flutter_markdown/example/lib/demos/extended_emoji_demo.dart new file mode 100644 index 0000000000..edf9b9397a --- /dev/null +++ b/packages/flutter_markdown/example/lib/demos/extended_emoji_demo.dart @@ -0,0 +1,122 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/flutter_markdown.dart'; +import 'package:markdown/markdown.dart' as md; +import '../shared/markdown_demo_widget.dart'; +import '../shared/markdown_extensions.dart'; + +// ignore_for_file: public_member_api_docs + +const String _notes = """ +# Extended Emoji Demo +--- + +## Overview + +This simple example demonstrates how to subclass an existing inline syntax +parser to extend, enhance, or modify its behavior. This example shows how to +subclass the EmojiSyntax inline syntax parser to support alternative names for +the thumbs up and thumbs down emoji characters. The emoji character map used by +EmojiSyntax has the keys "+1" and "-1" associated with the thumbs up and thumbs +down emoji characters, respectively. The ExtendedEmojiSyntax subclass extends +the EmojiSyntax class by overriding the onMatch method to intercept the call +from the parser. ExtendedEmojiSyntax either handles the matched tag or passes +the match along to its parent for processing. + +``` +class ExtendedEmojiSyntax extends md.EmojiSyntax { + static const alternateTags = { + 'thumbsup': '👍', + 'thumbsdown': '👎', + }; + + @override + bool onMatch(md.InlineParser parser, Match match) { + var emoji = alternateTags[match[1]]; + if (emoji != null) { + parser.addNode(md.Text(emoji)); + return true; + } + return super.onMatch(parser, match); + } +} +``` +"""; + +class ExtendedEmojiDemo extends StatelessWidget implements MarkdownDemoWidget { + const ExtendedEmojiDemo({Key? key}) : super(key: key); + + static const String _title = 'Extended Emoji Demo'; + + @override + String get title => ExtendedEmojiDemo._title; + + @override + String get description => 'Demonstrates how to extend an existing inline' + ' syntax parser by intercepting the parser onMatch routine.'; + + @override + Future get data => + Future.value('Simple test :smiley: :thumbsup:!'); + + @override + Future get notes => Future.value(_notes); + + static const String _notExtended = '# Using Emoji Syntax\n'; + + static const String _extended = '# Using Extened Emoji Syntax\n'; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: data, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return Container( + margin: const EdgeInsets.all(12), + constraints: const BoxConstraints.expand(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MarkdownBody( + data: _notExtended + snapshot.data!, + extensionSet: MarkdownExtensionSet.githubWeb.value, + ), + const SizedBox( + height: 24, + ), + MarkdownBody( + data: _extended + snapshot.data!, + extensionSet: md.ExtensionSet([], + [ExtendedEmojiSyntax()]), + ), + ], + ), + ); + } else { + return const CircularProgressIndicator(); + } + }, + ); + } +} + +class ExtendedEmojiSyntax extends md.EmojiSyntax { + static const Map alternateTags = { + 'thumbsup': '👍', + 'thumbsdown': '👎', + }; + + @override + bool onMatch(md.InlineParser parser, Match match) { + final String? emoji = alternateTags[match[1]!]; + if (emoji != null) { + parser.addNode(md.Text(emoji)); + return true; + } + return super.onMatch(parser, match); + } +} diff --git a/packages/flutter_markdown/example/lib/demos/minimal_markdown_demo.dart b/packages/flutter_markdown/example/lib/demos/minimal_markdown_demo.dart new file mode 100644 index 0000000000..8bf9bea67a --- /dev/null +++ b/packages/flutter_markdown/example/lib/demos/minimal_markdown_demo.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import '../shared/markdown_demo_widget.dart'; + +// ignore_for_file: public_member_api_docs + +const String _data = ''' +# Minimal Markdown Test +--- +This is a simple Markdown test. Provide a text string with Markdown tags +to the Markdown widget and it will display the formatted output in a scrollable +widget. + +## Section 1 +Maecenas eget **arcu egestas**, mollis ex vitae, posuere magna. Nunc eget + aliquam tortor. Vestibulum porta sodales efficitur. Mauris interdum turpis + eget est condimentum, vitae porttitor diam ornare. + +### Subsection A +Sed et massa finibus, blandit massa vel, vulputate velit. Vestibulum vitae +venenatis libero. ***Curabitur sem lectus, feugiat eu justo in, eleifend +accumsan ante.*** Sed a fermentum elit. Curabitur sodales metus id mi ornare, +in ullamcorper magna congue. +'''; + +const String _notes = """ +# Minimal Markdown Demo +--- + +## Overview + +The simplest use case that illustrates how to make use of the +flutter_markdown package is to include a Markdown widget in a widget tree +and supply it with a character string of text containing Markdown formatting +syntax. Here is a simple Flutter app that creates a Markdown widget that +formats and displays the text in the string _markdownData. The resulting +Flutter app demonstrates the use of headers, rules, and emphasis text from +plain text Markdown syntax. + +## Usage + +The code sample below demonstrates a simple Flutter app with a Markdown widget. + +``` +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; + +const String _markdownData = \""" +# Minimal Markdown Test +--- +This is a simple Markdown test. Provide a text string with Markdown tags +to the Markdown widget and it will display the formatted output in a +scrollable widget. + +## Section 1 +Maecenas eget **arcu egestas**, mollis ex vitae, posuere magna. Nunc eget +aliquam tortor. Vestibulum porta sodales efficitur. Mauris interdum turpis +eget est condimentum, vitae porttitor diam ornare. + +### Subsection A +Sed et massa finibus, blandit massa vel, vulputate velit. Vestibulum vitae +venenatis libero. **__Curabitur sem lectus, feugiat eu justo in, eleifend +accumsan ante.__** Sed a fermentum elit. Curabitur sodales metus id mi +ornare, in ullamcorper magna congue. +\"""; + +void main() { + runApp( + MaterialApp( + title: "Markdown Demo", + home: Scaffold( + appBar: AppBar( + title: const Text('Simple Markdown Demo'), + ), + body: SafeArea( + child: Markdown( + data: _markdownData, + ), + ), + ), + ), + ); +} +``` +"""; + +class MinimalMarkdownDemo extends StatelessWidget + implements MarkdownDemoWidget { + const MinimalMarkdownDemo({Key? key}) : super(key: key); + + static const String _title = 'Minimal Markdown Demo'; + + @override + String get title => MinimalMarkdownDemo._title; + + @override + String get description => 'A minimal example of how to use the Markdown ' + 'widget in a Flutter app.'; + + @override + Future get data => Future.value(_data); + + @override + Future get notes => Future.value(_notes); + + @override + Widget build(BuildContext context) { + return const Markdown( + data: _data, + ); + } +} diff --git a/packages/flutter_markdown/example/lib/demos/original_demo.dart b/packages/flutter_markdown/example/lib/demos/original_demo.dart new file mode 100644 index 0000000000..1af3c08a51 --- /dev/null +++ b/packages/flutter_markdown/example/lib/demos/original_demo.dart @@ -0,0 +1,183 @@ +// Copyright 2016 The Chromium 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/flutter_markdown.dart'; +import '../shared/markdown_demo_widget.dart'; + +// ignore_for_file: public_member_api_docs + +const String _markdownData = """ +# Markdown Example +Markdown allows you to easily include formatted text, images, and even formatted +Dart code in your app. + +## Titles + +Setext-style + +``` +This is an H1 +============= + +This is an H2 +------------- +``` + +Atx-style + +``` +# This is an H1 + +## This is an H2 + +###### This is an H6 +``` + +Select the valid headers: + +- [x] `# hello` +- [ ] `#hello` + +## Links + +[Google's Homepage][Google] + +``` +[inline-style](https://www.google.com) + +[reference-style][Google] +``` + +## Images + +![Flutter logo](/dart-lang/site-shared/master/src/_assets/image/flutter/icon/64.png) + +## Tables + +|Syntax |Result | +|---------------------------------------|-------------------------------------| +|`*italic 1*` |*italic 1* | +|`_italic 2_` | _italic 2_ | +|`**bold 1**` |**bold 1** | +|`__bold 2__` |__bold 2__ | +|`This is a ~~strikethrough~~` |This is a ~~strikethrough~~ | +|`***italic bold 1***` |***italic bold 1*** | +|`___italic bold 2___` |___italic bold 2___ | +|`***~~italic bold strikethrough 1~~***`|***~~italic bold strikethrough 1~~***| +|`~~***italic bold strikethrough 2***~~`|~~***italic bold strikethrough 2***~~| + +## Styling +Style text as _italic_, __bold__, ~~strikethrough~~, or `inline code`. + +- Use bulleted lists +- To better clarify +- Your points + +## Code blocks +Formatted Dart code looks really pretty too: + +``` +void main() { + runApp(MaterialApp( + home: Scaffold( + body: Markdown(data: markdownData), + ), + )); +} +``` + +## Center Title + +###### ※ ※ ※ + +_* How to implement it see main.dart#L129 in example._ + +## Custom Syntax + +NaOH + Al_2O_3 = NaAlO_2 + H_2O + +C_4H_10 = C_2H_6 + C_2H_4 + +## Markdown widget + +This is an example of how to create your own Markdown widget: + + Markdown(data: 'Hello _world_!'); + +Enjoy! + +[Google]: https://www.google.com/ + +## Line Breaks + +This is an example of how to create line breaks (tab or two whitespaces): + +line 1 + + +line 2 + + + +line 3 +"""; + +const String _notes = """ +# Original Markdown Demo +--- + +## Overview + +This is the original Flutter Markdown demo example that was created to +show how to use the flutter_markdown package. There were limitations in +the implementation of this demo example that didn't show the full potential +or extensibility of using the flutter_markdown package. This demo example +is being preserved for reference purposes. + +## Comments + +This demo example is being preserved for reference purposes. +"""; + +class OriginalMarkdownDemo extends StatelessWidget + implements MarkdownDemoWidget { + OriginalMarkdownDemo({Key? key}) : super(key: key); + + static const String _title = 'Original Markdown Demo'; + + @override + String get title => OriginalMarkdownDemo._title; + + @override + String get description => 'The original demo example. This demo was ' + 'include with versions of the package prior to version 0.4.4.'; + + @override + Future get data => Future.value(_markdownData); + + @override + Future get notes => Future.value(_notes); + + final ScrollController controller = ScrollController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Markdown( + controller: controller, + selectable: true, + data: _markdownData, + imageDirectory: 'https://raw.githubusercontent.com', + ), + ), + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.arrow_upward), + onPressed: () => controller.animateTo(0, + duration: const Duration(seconds: 1), curve: Curves.easeOut), + ), + ); + } +} diff --git a/packages/flutter_markdown/example/lib/demos/subscript_syntax_demo.dart b/packages/flutter_markdown/example/lib/demos/subscript_syntax_demo.dart new file mode 100644 index 0000000000..3995237961 --- /dev/null +++ b/packages/flutter_markdown/example/lib/demos/subscript_syntax_demo.dart @@ -0,0 +1,169 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/flutter_markdown.dart'; +import 'package:markdown/markdown.dart' as md; +import '../shared/markdown_demo_widget.dart'; + +// ignore_for_file: public_member_api_docs + +// Markdown source data showing the use of subscript tags. +const String _data = ''' +## Subscript Syntax + +NaOH + Al_2O_3 = NaAlO_2 + H_2O + +C_4H_10 = C_2H_6 + C_2H_4 +'''; + +const String _notes = """ +# Subscript Syntax Demo +--- + +## Overview + +This is an example of how to create an inline syntax parser with an +associated element builder. This example defines an inline syntax parser that +matches instances of an underscore character "**_**" followed by a integer +numerical value. When the parser finds a match for this syntax sequence, +a '**sub**' element is inserted into the abstract syntac tree. The supplied +builder for the '**sub**' element, SubscriptBuilder, is then called to create +an appropriate RichText widget for the formatted output. + +## Usage + +To support a new custom inline Markdown tag, an inline syntax object needs to be +defined for the Markdown parser and an element builder which is deligated the +task of building the appropriate Flutter widgets for the resulting Markdown +output. Instances of these objects need to be provided to the Markdown widget. + +``` + Markdown( + data: _data, + builders: { + 'sub': SubscriptBuilder(), + }, + extensionSet: md.ExtensionSet([], [SubscriptSyntax()]), + ); +``` + +### Inline Syntax Class + +``` +class SubscriptSyntax extends md.InlineSyntax { + static final _pattern = r'_([0-9]+)'; + + SubscriptSyntax() : super(_pattern); + + @override + bool onMatch(md.InlineParser parser, Match match) { + parser.addNode(md.Element.text('sub', match[1])); + return true; + } +``` + +### Markdown Element Builder + +``` +class SubscriptBuilder extends MarkdownElementBuilder { + static const List _subscripts = [ + '₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉' + ]; + + @override + Widget visitElementAfter(md.Element element, TextStyle preferredStyle) { + String textContent = element.textContent; + String text = ''; + for (int i = 0; i < textContent.length; i++) { + text += _subscripts[int.parse(textContent[i])]; + } + return SelectableText.rich(TextSpan(text: text)); + } +} +``` +"""; + +/// The subscript syntax demo provides an example of creating an inline syntax +/// object which defines the syntax for the Markdown inline parser and an +/// accompanying Markdown element builder object to handle subscript tags. +class SubscriptSyntaxDemo extends StatelessWidget + implements MarkdownDemoWidget { + const SubscriptSyntaxDemo({Key? key}) : super(key: key); + + static const String _title = 'Subscript Syntax Demo'; + + @override + String get title => SubscriptSyntaxDemo._title; + + @override + String get description => 'An example of how to create a custom inline ' + 'syntax parser and element builder for numerical subscripts.'; + + @override + Future get data => Future.value(_data); + + @override + Future get notes => Future.value(_notes); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: data, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return Markdown( + data: snapshot.data!, + builders: { + 'sub': SubscriptBuilder(), + }, + extensionSet: md.ExtensionSet( + [], [SubscriptSyntax()]), + ); + } else { + return const CircularProgressIndicator(); + } + }, + ); + } +} + +class SubscriptBuilder extends MarkdownElementBuilder { + static const List _subscripts = [ + '₀', + '₁', + '₂', + '₃', + '₄', + '₅', + '₆', + '₇', + '₈', + '₉' + ]; + + @override + Widget visitElementAfter(md.Element element, TextStyle? preferredStyle) { + // 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 SelectableText.rich(TextSpan(text: text)); + } +} + +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; + } +} diff --git a/packages/flutter_markdown/example/lib/demos/wrap_alignment_demo.dart b/packages/flutter_markdown/example/lib/demos/wrap_alignment_demo.dart new file mode 100644 index 0000000000..52db4c7095 --- /dev/null +++ b/packages/flutter_markdown/example/lib/demos/wrap_alignment_demo.dart @@ -0,0 +1,133 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/services.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import '../shared/dropdown_menu.dart'; +import '../shared/markdown_demo_widget.dart'; +import '../shared/markdown_extensions.dart'; + +// ignore_for_file: public_member_api_docs + +const String _notes = ''' +# Wrap Alignment Demo +--- +The Wrap Alignment Demo shows the effect of defining a wrap alignment for +various Markdown elements. Wrap alignments for the block elements text +paragraphs, headers, ordered and unordered lists, blockquotes, and code blocks +are set in the **MarkdownStyleSheet**. This demo shows the effect of setting +this parameter universally on these block elements for illustration purposes, +but they are independent settings. + +This demo also shows the effect of setting the **MarkdownStyleSheet** block +spacing parameter. The Markdown widget lays out block elements in a column using +**SizedBox** widgets to separate widgets with formatted output. The block +spacing parameter sets the height of the **SizedBox**. +'''; + +class WrapAlignmentDemo extends StatefulWidget implements MarkdownDemoWidget { + const WrapAlignmentDemo({Key? key}) : super(key: key); + + static const String _title = 'Wrap Alignment Demo'; + + @override + String get title => WrapAlignmentDemo._title; + + @override + String get description => 'Shows the effect the wrap alignment and block ' + 'spacing parameters have on various Markdown tagged elements.'; + + @override + Future get data => + rootBundle.loadString('assets/markdown_test_page.md'); + + @override + Future get notes => Future.value(_notes); + + @override + _WrapAlignmentDemoState createState() => _WrapAlignmentDemoState(); +} + +class _WrapAlignmentDemoState extends State { + double _blockSpacing = 8.0; + + WrapAlignment _wrapAlignment = WrapAlignment.start; + + final Map _wrapAlignmentMenuItems = + Map.fromIterables( + WrapAlignment.values.map((WrapAlignment e) => e.displayTitle), + WrapAlignment.values, + ); + + static const List _spacing = [4.0, 8.0, 16.0, 24.0, 32.0]; + final Map _blockSpacingMenuItems = + Map.fromIterables( + _spacing.map((double e) => e.toString()), + _spacing, + ); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: widget.data, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return Column( + children: [ + DropdownMenu( + items: _wrapAlignmentMenuItems, + label: 'Wrap Alignment:', + initialValue: _wrapAlignment, + onChanged: (WrapAlignment? value) { + if (value != _wrapAlignment) { + setState(() { + _wrapAlignment = value!; + }); + } + }, + ), + DropdownMenu( + items: _blockSpacingMenuItems, + label: 'Block Spacing:', + initialValue: _blockSpacing, + onChanged: (double? value) { + if (value != _blockSpacing) { + setState(() { + _blockSpacing = value!; + }); + } + }, + ), + Expanded( + child: Markdown( + key: Key(_wrapAlignment.toString()), + data: snapshot.data!, + imageDirectory: 'https://raw.githubusercontent.com', + styleSheet: + MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith( + blockSpacing: _blockSpacing, + textAlign: _wrapAlignment, + h1Align: _wrapAlignment, + h2Align: _wrapAlignment, + h3Align: _wrapAlignment, + h4Align: _wrapAlignment, + h5Align: _wrapAlignment, + h6Align: _wrapAlignment, + unorderedListAlign: _wrapAlignment, + orderedListAlign: _wrapAlignment, + blockquoteAlign: _wrapAlignment, + codeblockAlign: _wrapAlignment, + ), + ), + ), + ], + ); + } else { + return const CircularProgressIndicator(); + } + }, + ); + } +} diff --git a/packages/flutter_markdown/example/lib/main.dart b/packages/flutter_markdown/example/lib/main.dart new file mode 100644 index 0000000000..bddea46eda --- /dev/null +++ b/packages/flutter_markdown/example/lib/main.dart @@ -0,0 +1,78 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +/// +/// The simplest use case that illustrates how to make use of the +/// flutter_markdown package is to include a Markdown widget in a widget tree +/// and supply it with a character string of text containing Markdown formatting +/// syntax. Here is a simple Flutter app that creates a Markdown widget that +/// formats and displays the text in the string _markdownData. The resulting +/// Flutter app demonstrates the use of headers, rules, and emphasis text from +/// plain text Markdown syntax. +/// +/// import 'package:flutter/material.dart'; +/// import 'package:flutter_markdown/flutter_markdown.dart'; +/// +/// const String _markdownData = """ +/// # Minimal Markdown Test +/// --- +/// This is a simple Markdown test. Provide a text string with Markdown tags +/// to the Markdown widget and it will display the formatted output in a +/// scrollable widget. +/// +/// ## Section 1 +/// Maecenas eget **arcu egestas**, mollis ex vitae, posuere magna. Nunc eget +/// aliquam tortor. Vestibulum porta sodales efficitur. Mauris interdum turpis +/// eget est condimentum, vitae porttitor diam ornare. +/// +/// ### Subsection A +/// Sed et massa finibus, blandit massa vel, vulputate velit. Vestibulum vitae +/// venenatis libero. **__Curabitur sem lectus, feugiat eu justo in, eleifend +/// accumsan ante.__** Sed a fermentum elit. Curabitur sodales metus id mi +/// ornare, in ullamcorper magna congue. +/// """; +/// +/// void main() { +/// runApp( +/// MaterialApp( +/// title: "Markdown Demo", +/// home: Scaffold( +/// appBar: AppBar( +/// title: const Text('Simple Markdown Demo'), +/// ), +/// body: SafeArea( +/// child: Markdown( +/// data: _markdownData, +/// ), +/// ), +/// ), +/// ), +/// ); +/// } +/// +/// The flutter_markdown package has options for customizing and extending the +/// parsing of Markdown syntax and building of the formatted output. The demos +/// in this example app illustrate some of the potentials of the +/// flutter_markdown package. + +import 'package:flutter/material.dart'; +import 'screens/demo_screen.dart'; +import 'screens/home_screen.dart'; +import 'shared/markdown_demo_widget.dart'; + +void main() { + runApp( + MaterialApp( + title: 'Markdown Demos', + initialRoute: '/', + home: HomeScreen(), + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + builder: (_) => DemoScreen( + child: settings.arguments as MarkdownDemoWidget?, + ), + ); + }, + ), + ); +} diff --git a/packages/flutter_markdown/example/lib/screens/demo_card.dart b/packages/flutter_markdown/example/lib/screens/demo_card.dart new file mode 100644 index 0000000000..e54bdb5f23 --- /dev/null +++ b/packages/flutter_markdown/example/lib/screens/demo_card.dart @@ -0,0 +1,56 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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 '../screens/demo_screen.dart'; +import '../shared/markdown_demo_widget.dart'; + +// ignore_for_file: public_member_api_docs + +class DemoCard extends StatelessWidget { + const DemoCard({Key? key, required this.widget}) : super(key: key); + + final MarkdownDemoWidget widget; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => Navigator.pushNamed( + context, + DemoScreen.routeName, + arguments: widget, + ), + child: Container( + alignment: Alignment.center, + child: ConstrainedBox( + constraints: + const BoxConstraints(minHeight: 50, minWidth: 425, maxWidth: 425), + child: Card( + color: Colors.blue, + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.title, + style: Theme.of(context).primaryTextTheme.headline5, + ), + const SizedBox( + height: 6, + ), + Text( + widget.description, + style: Theme.of(context).primaryTextTheme.bodyText1, + ), + ], + ), + )), + ), + ), + ); + } +} diff --git a/packages/flutter_markdown/example/lib/screens/demo_screen.dart b/packages/flutter_markdown/example/lib/screens/demo_screen.dart new file mode 100644 index 0000000000..169f873a0c --- /dev/null +++ b/packages/flutter_markdown/example/lib/screens/demo_screen.dart @@ -0,0 +1,178 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/flutter_markdown.dart'; + +import '../shared/markdown_demo_widget.dart'; +import '../shared/markdown_extensions.dart'; + +// ignore_for_file: public_member_api_docs + +class DemoScreen extends StatelessWidget { + const DemoScreen({Key? key, required this.child}) : super(key: key); + + static const String routeName = '/demoScreen'; + + final MarkdownDemoWidget? child; + + static const List _tabLabels = ['Formatted', 'Raw', 'Notes']; + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 3, + child: Scaffold( + appBar: AppBar( + title: Text(child!.title), + bottom: TabBar( + indicatorPadding: const EdgeInsets.only(bottom: 8), + indicatorSize: TabBarIndicatorSize.label, + tabs: [ + for (String label in _tabLabels) Tab(text: label), + ], + ), + ), + body: TabBarView( + children: [ + DemoFormattedView(child: child), + DemoRawDataView(data: child!.data), + DemoNotesView(notes: child!.notes), //child.notes as String), + ], + ), + ), + ); + } +} + +class DemoFormattedView extends StatelessWidget { + const DemoFormattedView({Key? key, required this.child}) : super(key: key); + + final Widget? child; + + @override + Widget build(BuildContext context) { + return Container( + alignment: Alignment.center, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 1250), + child: child, + ), + ); + } +} + +class DemoRawDataView extends StatelessWidget { + const DemoRawDataView({Key? key, required this.data}) : super(key: key); + + final Future data; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: data, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return SingleChildScrollView( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Text( + snapshot.data!, + softWrap: true, + style: Theme.of(context) + .primaryTextTheme + .bodyText1! + .copyWith(fontFamily: 'Roboto Mono', color: Colors.black), + ), + ), + ); + } else { + return const CircularProgressIndicator(); + } + }, + ); + } +} + +class DemoNotesView extends StatelessWidget { + const DemoNotesView({Key? key, required this.notes}) : super(key: key); + + final Future notes; + + // Handle the link. The [href] in the callback contains information + // from the link. The url_launcher package or other similar package + // can be used to execute the link. + Future linkOnTapHandler( + BuildContext context, + String text, + String? href, + String title, + ) async { + showDialog( + context: context, + builder: (BuildContext context) => + _createDialog(context, text, href, title), + ); + } + + Widget _createDialog( + BuildContext context, + String text, + String? href, + String title, + ) => + AlertDialog( + title: const Text('Reference Link'), + content: SingleChildScrollView( + child: ListBody( + children: [ + Text( + 'See the following link for more information:', + style: Theme.of(context).textTheme.bodyText1, + ), + const SizedBox(height: 8), + Text( + 'Link text: $text', + style: Theme.of(context).textTheme.bodyText2, + ), + const SizedBox(height: 8), + Text( + 'Link destination: $href', + style: Theme.of(context).textTheme.bodyText2, + ), + const SizedBox(height: 8), + Text( + 'Link title: $title', + style: Theme.of(context).textTheme.bodyText2, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ) + ], + ); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: notes, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return Markdown( + data: snapshot.data!, + extensionSet: MarkdownExtensionSet.githubFlavored.value, + onTapLink: (String text, String? href, String title) => + linkOnTapHandler(context, text, href, title), + ); + } else { + return const CircularProgressIndicator(); + } + }, + ); + } +} diff --git a/packages/flutter_markdown/example/lib/screens/home_screen.dart b/packages/flutter_markdown/example/lib/screens/home_screen.dart new file mode 100644 index 0000000000..ed96c94eeb --- /dev/null +++ b/packages/flutter_markdown/example/lib/screens/home_screen.dart @@ -0,0 +1,52 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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 '../demos/basic_markdown_demo.dart'; +import '../demos/centered_header_demo.dart'; +import '../demos/extended_emoji_demo.dart'; +import '../demos/minimal_markdown_demo.dart'; +import '../demos/original_demo.dart'; +import '../demos/subscript_syntax_demo.dart'; +import '../demos/wrap_alignment_demo.dart'; +import '../screens/demo_card.dart'; +import '../shared/markdown_demo_widget.dart'; + +// ignore_for_file: public_member_api_docs + +class HomeScreen extends StatelessWidget { + HomeScreen({Key? key}) : super(key: key); + + static const String routeName = '/homeScreen'; + + final List _demos = [ + const MinimalMarkdownDemo(), + const BasicMarkdownDemo(), + const WrapAlignmentDemo(), + const SubscriptSyntaxDemo(), + const ExtendedEmojiDemo(), + OriginalMarkdownDemo(), + const CenteredHeaderDemo(), + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + title: const Text('Markdown Demos'), + ), + body: SafeArea( + child: Container( + color: Colors.black12, + child: ListView( + children: [ + for (MarkdownDemoWidget demo in _demos) DemoCard(widget: demo), + ], + ), + ), + ), + ); + } +} diff --git a/packages/flutter_markdown/example/lib/shared/dropdown_menu.dart b/packages/flutter_markdown/example/lib/shared/dropdown_menu.dart new file mode 100644 index 0000000000..db1371bb98 --- /dev/null +++ b/packages/flutter_markdown/example/lib/shared/dropdown_menu.dart @@ -0,0 +1,82 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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'; + +// ignore_for_file: public_member_api_docs + +class DropdownMenu extends StatelessWidget { + DropdownMenu({ + Key? key, + required this.items, + required this.initialValue, + required this.label, + this.labelStyle, + Color? background, + EdgeInsetsGeometry? padding, + Color? menuItemBackground, + EdgeInsetsGeometry? menuItemMargin, + this.onChanged, + }) : assert( + items.isNotEmpty, 'The items map must contain at least one entry'), + background = background ?? Colors.black12, + padding = + padding ?? const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + menuItemBackground = menuItemBackground ?? Colors.white, + menuItemMargin = menuItemMargin ?? const EdgeInsets.only(left: 4), + super(key: key); + + final Map items; + + final T initialValue; + + final String label; + + final TextStyle? labelStyle; + + final ValueChanged? onChanged; + + final Color background; + + final EdgeInsetsGeometry padding; + + final Color menuItemBackground; + + final EdgeInsetsGeometry menuItemMargin; + + @override + Widget build(BuildContext context) { + return Container( + color: background, + padding: padding, + child: Row( + children: [ + Text( + label, + style: labelStyle ?? Theme.of(context).textTheme.subtitle1, + ), + Container( + color: menuItemBackground, + margin: menuItemMargin, + child: DropdownButton( + isDense: true, + value: initialValue, + items: >[ + for (String item in items.keys) + DropdownMenuItem( + child: Container( + padding: const EdgeInsets.only(left: 4), + child: Text(item), + ), + value: items[item], + ), + ], + onChanged: (T? value) => onChanged!(value), + ), + ), + ], + ), + ); + } +} diff --git a/packages/flutter_markdown/example/lib/shared/markdown_demo_widget.dart b/packages/flutter_markdown/example/lib/shared/markdown_demo_widget.dart new file mode 100644 index 0000000000..5fa6d0ad42 --- /dev/null +++ b/packages/flutter_markdown/example/lib/shared/markdown_demo_widget.dart @@ -0,0 +1,35 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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'; + +// ignore_for_file: public_member_api_docs + +abstract class MarkdownDemoWidget extends Widget { + const MarkdownDemoWidget({Key? key}) : super(key: key); + + // The title property should be a short name to uniquely identify the example + // demo. The title will be displayed at the top of the card in the home screen + // to identify the demo and as the banner title on the demo screen. + String get title; + + // The description property should be a short explanation to provide + // additional information to clarify the actions performed by the demo. This + // should be a terse explanation of no more than three sentences. + String get description; + + // The data property is the sample Markdown source text data to be displayed + // in the Formatted and Raw tabs of the demo screen. This data will be used by + // the demo widget that implements MarkdownDemoWidget to format the Markdown + // data to be displayed in the Formatted tab. The raw source text of data is + // used by the Raw tab of the demo screen. The data can be as short or as long + // as needed for demonstration purposes. + Future get data; + + // The notes property is a detailed explanation of the syntax, concepts, + // comments, notes, or other additional information useful in explaining the + // demo. The notes are displayed in the Notes tab of the demo screen. Notes + // supports Markdown data to allow for rich text formatting. + Future get notes; +} diff --git a/packages/flutter_markdown/example/lib/shared/markdown_extensions.dart b/packages/flutter_markdown/example/lib/shared/markdown_extensions.dart new file mode 100644 index 0000000000..feb0e47c02 --- /dev/null +++ b/packages/flutter_markdown/example/lib/shared/markdown_extensions.dart @@ -0,0 +1,62 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:markdown/markdown.dart' as md; + +// ignore_for_file: public_member_api_docs + +enum MarkdownExtensionSet { none, commonMark, githubFlavored, githubWeb } + +extension MarkdownExtensionSetExtension on MarkdownExtensionSet { + String get name => describeEnum(this); + + String get displayTitle => () { + switch (this) { + case MarkdownExtensionSet.none: + return 'None'; + case MarkdownExtensionSet.commonMark: + return 'Common Mark'; + case MarkdownExtensionSet.githubFlavored: + return 'GitHub Flavored'; + case MarkdownExtensionSet.githubWeb: + return 'GitHub Web'; + } + }(); + + md.ExtensionSet get value => () { + switch (this) { + case MarkdownExtensionSet.none: + return md.ExtensionSet.none; + case MarkdownExtensionSet.commonMark: + return md.ExtensionSet.commonMark; + case MarkdownExtensionSet.githubFlavored: + return md.ExtensionSet.gitHubFlavored; + case MarkdownExtensionSet.githubWeb: + return md.ExtensionSet.gitHubWeb; + } + }(); +} + +extension WrapAlignmentExtension on WrapAlignment { + String get name => describeEnum(this); + + String get displayTitle => () { + switch (this) { + case WrapAlignment.center: + return 'Center'; + case WrapAlignment.end: + return 'End'; + case WrapAlignment.spaceAround: + return 'Space Around'; + case WrapAlignment.spaceBetween: + return 'Space Between'; + case WrapAlignment.spaceEvenly: + return 'Space Evenly'; + case WrapAlignment.start: + return 'Start'; + } + }(); +} diff --git a/packages/flutter_markdown/example/linux/.gitignore b/packages/flutter_markdown/example/linux/.gitignore new file mode 100644 index 0000000000..d3896c9844 --- /dev/null +++ b/packages/flutter_markdown/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/packages/flutter_markdown/example/linux/CMakeLists.txt b/packages/flutter_markdown/example/linux/CMakeLists.txt new file mode 100644 index 0000000000..76cc6c58c8 --- /dev/null +++ b/packages/flutter_markdown/example/linux/CMakeLists.txt @@ -0,0 +1,106 @@ +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +set(BINARY_NAME "flutter_markdown_example") +set(APPLICATION_ID "io.flutter.packages.flutter_markdown_example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Application build +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) +apply_standard_settings(${BINARY_NAME}) +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) +add_dependencies(${BINARY_NAME} flutter_assemble) +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/packages/flutter_markdown/example/linux/flutter/CMakeLists.txt b/packages/flutter_markdown/example/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000000..a1da1b9e53 --- /dev/null +++ b/packages/flutter_markdown/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,91 @@ +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) +pkg_check_modules(BLKID REQUIRED IMPORTED_TARGET blkid) +pkg_check_modules(LZMA REQUIRED IMPORTED_TARGET liblzma) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO + PkgConfig::BLKID + PkgConfig::LZMA +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + linux-x64 ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/packages/flutter_markdown/example/linux/flutter/generated_plugin_registrant.cc b/packages/flutter_markdown/example/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000000..890de29bba --- /dev/null +++ b/packages/flutter_markdown/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,7 @@ +// +// Generated file. Do not edit. +// + +#include "generated_plugin_registrant.h" + +void fl_register_plugins(FlPluginRegistry* registry) {} diff --git a/packages/flutter_markdown/example/linux/flutter/generated_plugin_registrant.h b/packages/flutter_markdown/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000000..9bf7478940 --- /dev/null +++ b/packages/flutter_markdown/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,13 @@ +// +// Generated file. Do not edit. +// + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/flutter_markdown/example/linux/flutter/generated_plugins.cmake b/packages/flutter_markdown/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000000..51436ae8c9 --- /dev/null +++ b/packages/flutter_markdown/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,15 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) diff --git a/packages/flutter_markdown/example/linux/main.cc b/packages/flutter_markdown/example/linux/main.cc new file mode 100644 index 0000000000..e7c5c54370 --- /dev/null +++ b/packages/flutter_markdown/example/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/packages/flutter_markdown/example/linux/my_application.cc b/packages/flutter_markdown/example/linux/my_application.cc new file mode 100644 index 0000000000..a18de4cde7 --- /dev/null +++ b/packages/flutter_markdown/example/linux/my_application.cc @@ -0,0 +1,106 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "flutter_markdown_example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "flutter_markdown_example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new( + my_application_get_type(), "application-id", APPLICATION_ID, nullptr)); +} diff --git a/packages/flutter_markdown/example/linux/my_application.h b/packages/flutter_markdown/example/linux/my_application.h new file mode 100644 index 0000000000..72271d5e41 --- /dev/null +++ b/packages/flutter_markdown/example/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/packages/flutter_markdown/example/macos/.gitignore b/packages/flutter_markdown/example/macos/.gitignore new file mode 100644 index 0000000000..d2fd377230 --- /dev/null +++ b/packages/flutter_markdown/example/macos/.gitignore @@ -0,0 +1,6 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/xcuserdata/ diff --git a/packages/flutter_markdown/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/flutter_markdown/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000000..c2efd0b608 --- /dev/null +++ b/packages/flutter_markdown/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/flutter_markdown/example/macos/Flutter/Flutter-Release.xcconfig b/packages/flutter_markdown/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000000..c2efd0b608 --- /dev/null +++ b/packages/flutter_markdown/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/flutter_markdown/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/flutter_markdown/example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000000..cccf817a52 --- /dev/null +++ b/packages/flutter_markdown/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,10 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { +} diff --git a/packages/flutter_markdown/example/macos/Runner.xcodeproj/project.pbxproj b/packages/flutter_markdown/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..39be746092 --- /dev/null +++ b/packages/flutter_markdown/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,572 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* flutter_markdown_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "flutter_markdown_example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* flutter_markdown_example.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* flutter_markdown_example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 0930; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/packages/flutter_markdown/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/flutter_markdown/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/packages/flutter_markdown/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/flutter_markdown/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/flutter_markdown/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000000..74c450c90f --- /dev/null +++ b/packages/flutter_markdown/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/flutter_markdown/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/flutter_markdown/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..1d526a16ed --- /dev/null +++ b/packages/flutter_markdown/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/flutter_markdown/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/flutter_markdown/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/packages/flutter_markdown/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/flutter_markdown/example/macos/Runner/AppDelegate.swift b/packages/flutter_markdown/example/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000000..d53ef64377 --- /dev/null +++ b/packages/flutter_markdown/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..a2ec33f19f --- /dev/null +++ b/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000..3c4935a7ca Binary files /dev/null and b/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000..ed4cc16421 Binary files /dev/null and b/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000000..483be61389 Binary files /dev/null and b/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000000..bcbf36df2f Binary files /dev/null and b/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000000..9c0a652864 Binary files /dev/null and b/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000000..e71a726136 Binary files /dev/null and b/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000000..8a31fe2dd3 Binary files /dev/null and b/packages/flutter_markdown/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/packages/flutter_markdown/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/flutter_markdown/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000000..537341abf9 --- /dev/null +++ b/packages/flutter_markdown/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,339 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/flutter_markdown/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/flutter_markdown/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000000..206ecf51aa --- /dev/null +++ b/packages/flutter_markdown/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = flutter_markdown_example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = io.flutter.packages.flutterMarkdownExample + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2021 io.flutter.packages. All rights reserved. diff --git a/packages/flutter_markdown/example/macos/Runner/Configs/Debug.xcconfig b/packages/flutter_markdown/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000000..36b0fd9464 --- /dev/null +++ b/packages/flutter_markdown/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/flutter_markdown/example/macos/Runner/Configs/Release.xcconfig b/packages/flutter_markdown/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000000..dff4f49561 --- /dev/null +++ b/packages/flutter_markdown/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/flutter_markdown/example/macos/Runner/Configs/Warnings.xcconfig b/packages/flutter_markdown/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000000..42bcbf4780 --- /dev/null +++ b/packages/flutter_markdown/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/flutter_markdown/example/macos/Runner/DebugProfile.entitlements b/packages/flutter_markdown/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000000..08c3ab17cc --- /dev/null +++ b/packages/flutter_markdown/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + + diff --git a/packages/flutter_markdown/example/macos/Runner/Info.plist b/packages/flutter_markdown/example/macos/Runner/Info.plist new file mode 100644 index 0000000000..4789daa6a4 --- /dev/null +++ b/packages/flutter_markdown/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/packages/flutter_markdown/example/macos/Runner/MainFlutterWindow.swift b/packages/flutter_markdown/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000000..2722837ec9 --- /dev/null +++ b/packages/flutter_markdown/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/packages/flutter_markdown/example/macos/Runner/Release.entitlements b/packages/flutter_markdown/example/macos/Runner/Release.entitlements new file mode 100644 index 0000000000..ee95ab7e58 --- /dev/null +++ b/packages/flutter_markdown/example/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/packages/flutter_markdown/example/pubspec.yaml b/packages/flutter_markdown/example/pubspec.yaml new file mode 100644 index 0000000000..5221a12bea --- /dev/null +++ b/packages/flutter_markdown/example/pubspec.yaml @@ -0,0 +1,28 @@ +name: flutter_markdown_example +description: Demonstrates how to use the flutter_markdown package. +publish_to: "none" + +environment: + sdk: '>=2.12.0-0 <3.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_markdown: + path: ../ + flutter_test: + sdk: flutter + +flutter: + assets: + - assets/ + + fonts: + - family: 'Roboto Mono' + fonts: + - asset: fonts/RobotoMono-Regular.ttf + + uses-material-design: true diff --git a/packages/flutter_markdown/example/web/favicon.png b/packages/flutter_markdown/example/web/favicon.png new file mode 100644 index 0000000000..8aaa46ac1a Binary files /dev/null and b/packages/flutter_markdown/example/web/favicon.png differ diff --git a/packages/flutter_markdown/example/web/icons/Icon-192.png b/packages/flutter_markdown/example/web/icons/Icon-192.png new file mode 100644 index 0000000000..b749bfef07 Binary files /dev/null and b/packages/flutter_markdown/example/web/icons/Icon-192.png differ diff --git a/packages/flutter_markdown/example/web/icons/Icon-512.png b/packages/flutter_markdown/example/web/icons/Icon-512.png new file mode 100644 index 0000000000..88cfd48dff Binary files /dev/null and b/packages/flutter_markdown/example/web/icons/Icon-512.png differ diff --git a/packages/flutter_markdown/example/web/index.html b/packages/flutter_markdown/example/web/index.html new file mode 100644 index 0000000000..e4e333b77c --- /dev/null +++ b/packages/flutter_markdown/example/web/index.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + flutter_markdown_example + + + + + + + + diff --git a/packages/flutter_markdown/example/web/manifest.json b/packages/flutter_markdown/example/web/manifest.json new file mode 100644 index 0000000000..0b30ee999b --- /dev/null +++ b/packages/flutter_markdown/example/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "flutter_markdown_example", + "short_name": "flutter_markdown_example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/packages/flutter_markdown/example/windows/.gitignore b/packages/flutter_markdown/example/windows/.gitignore new file mode 100644 index 0000000000..d492d0d98c --- /dev/null +++ b/packages/flutter_markdown/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/packages/flutter_markdown/example/windows/CMakeLists.txt b/packages/flutter_markdown/example/windows/CMakeLists.txt new file mode 100644 index 0000000000..52880babfc --- /dev/null +++ b/packages/flutter_markdown/example/windows/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.15) +project(flutter_markdown_example LANGUAGES CXX) + +set(BINARY_NAME "flutter_markdown_example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() + +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/packages/flutter_markdown/example/windows/flutter/CMakeLists.txt b/packages/flutter_markdown/example/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000000..744f08a938 --- /dev/null +++ b/packages/flutter_markdown/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,102 @@ +cmake_minimum_required(VERSION 3.15) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/packages/flutter_markdown/example/windows/flutter/generated_plugin_registrant.cc b/packages/flutter_markdown/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000000..a6177ab0b7 --- /dev/null +++ b/packages/flutter_markdown/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,7 @@ +// +// Generated file. Do not edit. +// + +#include "generated_plugin_registrant.h" + +void RegisterPlugins(flutter::PluginRegistry* registry) {} diff --git a/packages/flutter_markdown/example/windows/flutter/generated_plugin_registrant.h b/packages/flutter_markdown/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000000..9846246b4d --- /dev/null +++ b/packages/flutter_markdown/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,13 @@ +// +// Generated file. Do not edit. +// + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/flutter_markdown/example/windows/flutter/generated_plugins.cmake b/packages/flutter_markdown/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000000..4d10c25186 --- /dev/null +++ b/packages/flutter_markdown/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,15 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) diff --git a/packages/flutter_markdown/example/windows/runner/CMakeLists.txt b/packages/flutter_markdown/example/windows/runner/CMakeLists.txt new file mode 100644 index 0000000000..977e38b5d1 --- /dev/null +++ b/packages/flutter_markdown/example/windows/runner/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.15) +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "run_loop.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) +apply_standard_settings(${BINARY_NAME}) +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/packages/flutter_markdown/example/windows/runner/Runner.rc b/packages/flutter_markdown/example/windows/runner/Runner.rc new file mode 100644 index 0000000000..7b61799edc --- /dev/null +++ b/packages/flutter_markdown/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "io.flutter.packages" "\0" + VALUE "FileDescription", "A new Flutter project." "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "flutter_markdown_example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2021 io.flutter.packages. All rights reserved." "\0" + VALUE "OriginalFilename", "flutter_markdown_example.exe" "\0" + VALUE "ProductName", "flutter_markdown_example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/packages/flutter_markdown/example/windows/runner/flutter_window.cpp b/packages/flutter_markdown/example/windows/runner/flutter_window.cpp new file mode 100644 index 0000000000..c422723045 --- /dev/null +++ b/packages/flutter_markdown/example/windows/runner/flutter_window.cpp @@ -0,0 +1,64 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(RunLoop* run_loop, + const flutter::DartProject& project) + : run_loop_(run_loop), project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + run_loop_->RegisterFlutterInstance(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + run_loop_->UnregisterFlutterInstance(flutter_controller_->engine()); + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opporutunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/packages/flutter_markdown/example/windows/runner/flutter_window.h b/packages/flutter_markdown/example/windows/runner/flutter_window.h new file mode 100644 index 0000000000..b663ddd501 --- /dev/null +++ b/packages/flutter_markdown/example/windows/runner/flutter_window.h @@ -0,0 +1,39 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "run_loop.h" +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow driven by the |run_loop|, hosting a + // Flutter view running |project|. + explicit FlutterWindow(RunLoop* run_loop, + const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The run loop driving events for this window. + RunLoop* run_loop_; + + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/packages/flutter_markdown/example/windows/runner/main.cpp b/packages/flutter_markdown/example/windows/runner/main.cpp new file mode 100644 index 0000000000..19b45edf35 --- /dev/null +++ b/packages/flutter_markdown/example/windows/runner/main.cpp @@ -0,0 +1,41 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "run_loop.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + RunLoop run_loop; + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(&run_loop, project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"flutter_markdown_example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + run_loop.Run(); + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/flutter_markdown/example/windows/runner/resource.h b/packages/flutter_markdown/example/windows/runner/resource.h new file mode 100644 index 0000000000..d5d958dc42 --- /dev/null +++ b/packages/flutter_markdown/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/packages/flutter_markdown/example/windows/runner/resources/app_icon.ico b/packages/flutter_markdown/example/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000..c04e20caf6 Binary files /dev/null and b/packages/flutter_markdown/example/windows/runner/resources/app_icon.ico differ diff --git a/packages/flutter_markdown/example/windows/runner/run_loop.cpp b/packages/flutter_markdown/example/windows/runner/run_loop.cpp new file mode 100644 index 0000000000..2d6636ab6b --- /dev/null +++ b/packages/flutter_markdown/example/windows/runner/run_loop.cpp @@ -0,0 +1,66 @@ +#include "run_loop.h" + +#include + +#include + +RunLoop::RunLoop() {} + +RunLoop::~RunLoop() {} + +void RunLoop::Run() { + bool keep_running = true; + TimePoint next_flutter_event_time = TimePoint::clock::now(); + while (keep_running) { + std::chrono::nanoseconds wait_duration = + std::max(std::chrono::nanoseconds(0), + next_flutter_event_time - TimePoint::clock::now()); + ::MsgWaitForMultipleObjects( + 0, nullptr, FALSE, static_cast(wait_duration.count() / 1000), + QS_ALLINPUT); + bool processed_events = false; + MSG message; + // All pending Windows messages must be processed; MsgWaitForMultipleObjects + // won't return again for items left in the queue after PeekMessage. + while (::PeekMessage(&message, nullptr, 0, 0, PM_REMOVE)) { + processed_events = true; + if (message.message == WM_QUIT) { + keep_running = false; + break; + } + ::TranslateMessage(&message); + ::DispatchMessage(&message); + // Allow Flutter to process messages each time a Windows message is + // processed, to prevent starvation. + next_flutter_event_time = + std::min(next_flutter_event_time, ProcessFlutterMessages()); + } + // If the PeekMessage loop didn't run, process Flutter messages. + if (!processed_events) { + next_flutter_event_time = + std::min(next_flutter_event_time, ProcessFlutterMessages()); + } + } +} + +void RunLoop::RegisterFlutterInstance( + flutter::FlutterEngine* flutter_instance) { + flutter_instances_.insert(flutter_instance); +} + +void RunLoop::UnregisterFlutterInstance( + flutter::FlutterEngine* flutter_instance) { + flutter_instances_.erase(flutter_instance); +} + +RunLoop::TimePoint RunLoop::ProcessFlutterMessages() { + TimePoint next_event_time = TimePoint::max(); + for (auto instance : flutter_instances_) { + std::chrono::nanoseconds wait_duration = instance->ProcessMessages(); + if (wait_duration != std::chrono::nanoseconds::max()) { + next_event_time = + std::min(next_event_time, TimePoint::clock::now() + wait_duration); + } + } + return next_event_time; +} diff --git a/packages/flutter_markdown/example/windows/runner/run_loop.h b/packages/flutter_markdown/example/windows/runner/run_loop.h new file mode 100644 index 0000000000..5f2c4a9ad7 --- /dev/null +++ b/packages/flutter_markdown/example/windows/runner/run_loop.h @@ -0,0 +1,38 @@ +#ifndef RUNNER_RUN_LOOP_H_ +#define RUNNER_RUN_LOOP_H_ + +#include + +#include +#include + +// A runloop that will service events for Flutter instances as well +// as native messages. +class RunLoop { + public: + RunLoop(); + ~RunLoop(); + + // Prevent copying + RunLoop(RunLoop const&) = delete; + RunLoop& operator=(RunLoop const&) = delete; + + // Runs the run loop until the application quits. + void Run(); + + // Registers the given Flutter instance for event servicing. + void RegisterFlutterInstance(flutter::FlutterEngine* flutter_instance); + + // Unregisters the given Flutter instance from event servicing. + void UnregisterFlutterInstance(flutter::FlutterEngine* flutter_instance); + + private: + using TimePoint = std::chrono::steady_clock::time_point; + + // Processes all currently pending messages for registered Flutter instances. + TimePoint ProcessFlutterMessages(); + + std::set flutter_instances_; +}; + +#endif // RUNNER_RUN_LOOP_H_ diff --git a/packages/flutter_markdown/example/windows/runner/runner.exe.manifest b/packages/flutter_markdown/example/windows/runner/runner.exe.manifest new file mode 100644 index 0000000000..c977c4a425 --- /dev/null +++ b/packages/flutter_markdown/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/packages/flutter_markdown/example/windows/runner/utils.cpp b/packages/flutter_markdown/example/windows/runner/utils.cpp new file mode 100644 index 0000000000..afa363b236 --- /dev/null +++ b/packages/flutter_markdown/example/windows/runner/utils.cpp @@ -0,0 +1,63 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE* unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = + ::WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, + nullptr, 0, nullptr, nullptr); + if (target_length == 0) { + return std::string(); + } + std::string utf8_string; + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/packages/flutter_markdown/example/windows/runner/utils.h b/packages/flutter_markdown/example/windows/runner/utils.h new file mode 100644 index 0000000000..3879d54755 --- /dev/null +++ b/packages/flutter_markdown/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/flutter_markdown/example/windows/runner/win32_window.cpp b/packages/flutter_markdown/example/windows/runner/win32_window.cpp new file mode 100644 index 0000000000..44091b3f3c --- /dev/null +++ b/packages/flutter_markdown/example/windows/runner/win32_window.cpp @@ -0,0 +1,237 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { ++g_active_window_count; } + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { return window_handle_; } + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/packages/flutter_markdown/example/windows/runner/win32_window.h b/packages/flutter_markdown/example/windows/runner/win32_window.h new file mode 100644 index 0000000000..4ae64a12b4 --- /dev/null +++ b/packages/flutter_markdown/example/windows/runner/win32_window.h @@ -0,0 +1,95 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/packages/flutter_markdown/flutter_markdown.iml b/packages/flutter_markdown/flutter_markdown.iml new file mode 100644 index 0000000000..4f0181093d --- /dev/null +++ b/packages/flutter_markdown/flutter_markdown.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/flutter_markdown/lib/flutter_markdown.dart b/packages/flutter_markdown/lib/flutter_markdown.dart new file mode 100644 index 0000000000..8d7ed6ea08 --- /dev/null +++ b/packages/flutter_markdown/lib/flutter_markdown.dart @@ -0,0 +1,10 @@ +// Copyright 2016 The Chromium 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; + +export 'src/builder.dart'; +export 'src/style_sheet.dart'; +export 'src/widget.dart'; diff --git a/packages/flutter_markdown/lib/src/_functions_io.dart b/packages/flutter_markdown/lib/src/_functions_io.dart new file mode 100644 index 0000000000..2fbf2145b6 --- /dev/null +++ b/packages/flutter_markdown/lib/src/_functions_io.dart @@ -0,0 +1,85 @@ +// Copyright 2016 The Chromium 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); + } else if (uri.scheme == 'data') { + return _handleDataSchemeUri(uri, width, height); + } else if (uri.scheme == 'resource') { + return Image.asset(uri.path, width: width, height: height); + } 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); + } else { + return Image.file(File.fromUri(fileUri), width: width, height: height); + } + } +}; + +/// 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)); + break; + case MarkdownStyleSheetBaseTheme.cupertino: + result = + MarkdownStyleSheet.fromCupertinoTheme(CupertinoTheme.of(context)); + break; + case MarkdownStyleSheetBaseTheme.material: + default: + result = MarkdownStyleSheet.fromTheme(Theme.of(context)); + } + + return result.copyWith( + textScaleFactor: MediaQuery.textScaleFactorOf(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, + ); + } else if (mimeType.startsWith('text/')) { + return Text(uri.data!.contentAsString()); + } + return const SizedBox(); +} diff --git a/packages/flutter_markdown/lib/src/_functions_web.dart b/packages/flutter_markdown/lib/src/_functions_web.dart new file mode 100644 index 0000000000..018d71e7c4 --- /dev/null +++ b/packages/flutter_markdown/lib/src/_functions_web.dart @@ -0,0 +1,88 @@ +// Copyright 2016 The Chromium 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:html'; // ignore: avoid_web_libraries_in_flutter + +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); + } else if (uri.scheme == 'data') { + return _handleDataSchemeUri(uri, width, height); + } else if (uri.scheme == 'resource') { + return Image.asset(uri.path, width: width, height: height); + } else { + final Uri fileUri = imageDirectory != null + ? Uri.parse(p.join(imageDirectory, uri.toString())) + : uri; + if (fileUri.scheme == 'http' || fileUri.scheme == 'https') { + return Image.network(fileUri.toString(), width: width, height: height); + } else { + final String src = p.join(p.current, fileUri.toString()); + return Image.network(src, width: width, height: height); + } + } +}; + +/// 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: + final String userAgent = window.navigator.userAgent; + result = userAgent.contains('Mac OS X') + ? MarkdownStyleSheet.fromCupertinoTheme(CupertinoTheme.of(context)) + : MarkdownStyleSheet.fromTheme(Theme.of(context)); + break; + case MarkdownStyleSheetBaseTheme.cupertino: + result = + MarkdownStyleSheet.fromCupertinoTheme(CupertinoTheme.of(context)); + break; + case MarkdownStyleSheetBaseTheme.material: + default: + result = MarkdownStyleSheet.fromTheme(Theme.of(context)); + } + + return result.copyWith( + textScaleFactor: MediaQuery.textScaleFactorOf(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, + ); + } else if (mimeType.startsWith('text/')) { + return Text(uri.data!.contentAsString()); + } + return const SizedBox(); +} diff --git a/packages/flutter_markdown/lib/src/builder.dart b/packages/flutter_markdown/lib/src/builder.dart new file mode 100644 index 0000000000..c19d577bc8 --- /dev/null +++ b/packages/flutter_markdown/lib/src/builder.dart @@ -0,0 +1,761 @@ +// Copyright 2016 The Chromium 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:markdown/markdown.dart' as md; + +import '_functions_io.dart' if (dart.library.html) '_functions_web.dart'; +import 'style_sheet.dart'; +import 'widget.dart'; + +const List _kBlockTags = [ + 'p', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'li', + 'blockquote', + 'pre', + 'ol', + 'ul', + 'hr', + 'table', + 'thead', + 'tbody', + 'tr' +]; + +const List _kListTags = ['ul', 'ol']; + +bool _isBlockTag(String? tag) => _kBlockTags.contains(tag); + +bool _isListTag(String tag) => _kListTags.contains(tag); + +class _BlockElement { + _BlockElement(this.tag); + + final String? tag; + final List children = []; + + int nextListIndex = 0; +} + +class _TableElement { + final List rows = []; +} + +/// A collection of widgets that should be placed adjacent to (inline with) +/// other inline elements in the same parent block. +/// +/// Inline elements can be textual (a/em/strong) represented by [RichText] +/// widgets or images (img) represented by [Image.network] widgets. +/// +/// Inline elements can be nested within other inline elements, inheriting their +/// parent's style along with the style of the block they are in. +/// +/// When laying out inline widgets, first, any adjacent RichText widgets are +/// merged, then, all inline widgets are enclosed in a parent [Wrap] widget. +class _InlineElement { + _InlineElement(this.tag, {this.style}); + + final String? tag; + + /// Created by merging the style defined for this element's [tag] in the + /// delegate's [MarkdownStyleSheet] with the style of its parent. + final TextStyle? style; + + final List children = []; +} + +/// A delegate used by [MarkdownBuilder] to control the widgets it creates. +abstract class MarkdownBuilderDelegate { + /// Returns a gesture recognizer to use for an `a` element with the given + /// text, `href` attribute, and title. + GestureRecognizer createLink(String text, String? href, String title); + + /// Returns formatted text to use to display the given contents of a `pre` + /// element. + /// + /// The `styleSheet` is the value of [MarkdownBuilder.styleSheet]. + TextSpan formatText(MarkdownStyleSheet styleSheet, String code); +} + +/// Builds a [Widget] tree from parsed Markdown. +/// +/// See also: +/// +/// * [Markdown], which is a widget that parses and displays Markdown. +class MarkdownBuilder implements md.NodeVisitor { + /// Creates an object that builds a [Widget] tree from parsed Markdown. + MarkdownBuilder({ + required this.delegate, + required this.selectable, + required this.styleSheet, + required this.imageDirectory, + required this.imageBuilder, + required this.checkboxBuilder, + required this.bulletBuilder, + required this.builders, + required this.listItemCrossAxisAlignment, + this.fitContent = false, + this.onTapText, + }); + + /// A delegate that controls how link and `pre` elements behave. + final MarkdownBuilderDelegate delegate; + + /// If true, the text is selectable. + /// + /// Defaults to false. + final bool selectable; + + /// Defines which [TextStyle] objects to use for each type of element. + final MarkdownStyleSheet styleSheet; + + /// The base directory holding images referenced by Img tags with local or network file paths. + final String? imageDirectory; + + /// Call when build an image widget. + final MarkdownImageBuilder? imageBuilder; + + /// Call when build a checkbox widget. + final MarkdownCheckboxBuilder? checkboxBuilder; + + /// Called when building a custom bullet. + final MarkdownBulletBuilder? bulletBuilder; + + /// Call when build a custom widget. + final Map builders; + + /// 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; + + /// Default tap handler used when [selectable] is set to true + final VoidCallback? onTapText; + + final List _listIndents = []; + final List<_BlockElement> _blocks = <_BlockElement>[]; + final List<_TableElement> _tables = <_TableElement>[]; + final List<_InlineElement> _inlines = <_InlineElement>[]; + final List _linkHandlers = []; + String? _currentBlockTag; + String? _lastTag; + bool _isInBlockquote = false; + + /// Returns widgets that display the given Markdown nodes. + /// + /// The returned widgets are typically used as children in a [ListView]. + List build(List nodes) { + _listIndents.clear(); + _blocks.clear(); + _tables.clear(); + _inlines.clear(); + _linkHandlers.clear(); + _isInBlockquote = false; + + _blocks.add(_BlockElement(null)); + + for (final md.Node node in nodes) { + assert(_blocks.length == 1); + node.accept(this); + } + + assert(_tables.isEmpty); + assert(_inlines.isEmpty); + assert(!_isInBlockquote); + return _blocks.single.children; + } + + @override + bool visitElementBefore(md.Element element) { + final String tag = element.tag; + _currentBlockTag ??= tag; + + if (builders.containsKey(tag)) { + builders[tag]!.visitElementBefore(element); + } + + int? start; + if (_isBlockTag(tag)) { + _addAnonymousBlockIfNeeded(); + if (_isListTag(tag)) { + _listIndents.add(tag); + if (element.attributes['start'] != null) + start = int.parse(element.attributes['start']!) - 1; + } else if (tag == 'blockquote') { + _isInBlockquote = true; + } else if (tag == 'table') { + _tables.add(_TableElement()); + } else if (tag == 'tr') { + final int length = _tables.single.rows.length; + BoxDecoration? decoration = + styleSheet.tableCellsDecoration as BoxDecoration?; + if (length == 0 || length.isOdd) { + decoration = null; + } + _tables.single.rows.add(TableRow( + decoration: decoration, + // TODO(stuartmorgan): This should be fixed, not suppressed; enabling + // this lint warning exposed that the builder is modifying the + // children of TableRows, even though they are @immutable. + // ignore: prefer_const_literals_to_create_immutables + children: [], + )); + } + final _BlockElement bElement = _BlockElement(tag); + if (start != null) { + bElement.nextListIndex = start; + } + _blocks.add(bElement); + } else { + if (tag == 'a') { + final String? text = extractTextFromElement(element); + // Don't add empty links + if (text == null) { + return false; + } + final String? destination = element.attributes['href']; + final String title = element.attributes['title'] ?? ''; + + _linkHandlers.add( + delegate.createLink(text, destination, title), + ); + } + + _addParentInlineIfNeeded(_blocks.last.tag); + + // The Markdown parser passes empty table data tags for blank + // table cells. Insert a text node with an empty string in this + // case for the table cell to get properly created. + if (element.tag == 'td' && + element.children != null && + element.children!.isEmpty) { + element.children!.add(md.Text('')); + } + + final TextStyle parentStyle = _inlines.last.style!; + _inlines.add(_InlineElement( + tag, + style: parentStyle.merge(styleSheet.styles[tag]), + )); + } + + return true; + } + + /// Returns the text, if any, from [element] and its descendants. + String? extractTextFromElement(md.Node element) { + return element is md.Element && (element.children?.isNotEmpty ?? false) + ? element.children! + .map((md.Node e) => + e is md.Text ? e.text : extractTextFromElement(e)) + .join('') + : (element is md.Element && (element.attributes.isNotEmpty) + ? element.attributes['alt'] + : ''); + } + + @override + void visitText(md.Text text) { + // Don't allow text directly under the root. + if (_blocks.last.tag == null) { + return; + } + + _addParentInlineIfNeeded(_blocks.last.tag); + + // Define trim text function to remove spaces from text elements in + // accordance with Markdown specifications. + String trimText(String text) { + // The leading spaces pattern is used to identify spaces + // at the beginning of a line of text. + final RegExp _leadingSpacesPattern = RegExp(r'^ *'); + + // The soft line break pattern is used to identify the spaces at the end of a + // line of text and the leading spaces in the immediately following the line + // of text. These spaces are removed in accordance with the Markdown + // specification on soft line breaks when lines of text are joined. + final RegExp _softLineBreakPattern = RegExp(r' ?\n *'); + + // Leading spaces following a hard line break are ignored. + // https://github.github.com/gfm/#example-657 + if (_lastTag == 'br') { + text = text.replaceAll(_leadingSpacesPattern, ''); + } + + // Spaces at end of the line and beginning of the next line are removed. + // https://github.github.com/gfm/#example-670 + return text.replaceAll(_softLineBreakPattern, ' '); + } + + Widget? child; + if (_blocks.isNotEmpty && builders.containsKey(_blocks.last.tag)) { + child = builders[_blocks.last.tag!]! + .visitText(text, styleSheet.styles[_blocks.last.tag!]); + } else if (_blocks.last.tag == 'pre') { + child = Scrollbar( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: styleSheet.codeblockPadding, + child: _buildRichText(delegate.formatText(styleSheet, text.text)), + ), + ); + } else { + child = _buildRichText( + TextSpan( + style: _isInBlockquote + ? styleSheet.blockquote!.merge(_inlines.last.style) + : _inlines.last.style, + text: _isInBlockquote ? text.text : trimText(text.text), + recognizer: _linkHandlers.isNotEmpty ? _linkHandlers.last : null, + ), + textAlign: _textAlignForBlockTag(_currentBlockTag), + ); + } + if (child != null) { + _inlines.last.children.add(child); + } + } + + @override + void visitElementAfter(md.Element element) { + final String tag = element.tag; + + if (_isBlockTag(tag)) { + _addAnonymousBlockIfNeeded(); + + final _BlockElement current = _blocks.removeLast(); + Widget child; + + if (current.children.isNotEmpty) { + child = Column( + crossAxisAlignment: fitContent + ? CrossAxisAlignment.start + : CrossAxisAlignment.stretch, + children: current.children, + ); + } else { + child = const SizedBox(); + } + + if (_isListTag(tag)) { + assert(_listIndents.isNotEmpty); + _listIndents.removeLast(); + } else if (tag == 'li') { + if (_listIndents.isNotEmpty) { + if (element.children!.isEmpty) { + element.children!.add(md.Text('')); + } + Widget bullet; + final dynamic el = element.children![0]; + if (el is md.Element && el.attributes['type'] == 'checkbox') { + final bool val = el.attributes['checked'] != 'false'; + bullet = _buildCheckbox(val); + } else { + bullet = _buildBullet(_listIndents.last); + } + child = Row( + textBaseline: listItemCrossAxisAlignment == + MarkdownListItemCrossAxisAlignment.start + ? null + : TextBaseline.alphabetic, + crossAxisAlignment: listItemCrossAxisAlignment == + MarkdownListItemCrossAxisAlignment.start + ? CrossAxisAlignment.start + : CrossAxisAlignment.baseline, + children: [ + SizedBox( + width: styleSheet.listIndent! + + styleSheet.listBulletPadding!.left + + styleSheet.listBulletPadding!.right, + child: bullet, + ), + Expanded(child: child) + ], + ); + } + } else if (tag == 'table') { + child = Table( + defaultColumnWidth: styleSheet.tableColumnWidth!, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + border: styleSheet.tableBorder, + children: _tables.removeLast().rows, + ); + } else if (tag == 'blockquote') { + _isInBlockquote = false; + child = DecoratedBox( + decoration: styleSheet.blockquoteDecoration!, + child: Padding( + padding: styleSheet.blockquotePadding!, + child: child, + ), + ); + } else if (tag == 'pre') { + child = DecoratedBox( + decoration: styleSheet.codeblockDecoration!, + child: child, + ); + } else if (tag == 'hr') { + child = Container(decoration: styleSheet.horizontalRuleDecoration); + } + + _addBlockChild(child); + } else { + final _InlineElement current = _inlines.removeLast(); + final _InlineElement parent = _inlines.last; + + if (builders.containsKey(tag)) { + final Widget? child = + builders[tag]!.visitElementAfter(element, styleSheet.styles[tag]); + if (child != null) { + current.children[0] = child; + } + } else if (tag == 'img') { + // create an image widget for this image + current.children.add(_buildImage( + element.attributes['src']!, + element.attributes['title'], + element.attributes['alt'], + )); + } else if (tag == 'br') { + current.children.add(_buildRichText(const TextSpan(text: '\n'))); + } else if (tag == 'th' || tag == 'td') { + TextAlign? align; + final String? style = element.attributes['style']; + if (style == null) { + align = tag == 'th' ? styleSheet.tableHeadAlign : TextAlign.left; + } else { + final RegExp regExp = RegExp(r'text-align: (left|center|right)'); + final Match match = regExp.matchAsPrefix(style)!; + switch (match[1]) { + case 'left': + align = TextAlign.left; + break; + case 'center': + align = TextAlign.center; + break; + case 'right': + align = TextAlign.right; + break; + } + } + final Widget child = _buildTableCell( + _mergeInlineChildren(current.children, align), + textAlign: align, + ); + _tables.single.rows.last.children!.add(child); + } else if (tag == 'a') { + _linkHandlers.removeLast(); + } + + if (current.children.isNotEmpty) { + parent.children.addAll(current.children); + } + } + if (_currentBlockTag == tag) { + _currentBlockTag = null; + } + _lastTag = tag; + } + + Widget _buildImage(String src, String? title, String? alt) { + final List parts = src.split('#'); + if (parts.isEmpty) { + return const SizedBox(); + } + + final String path = parts.first; + double? width; + double? height; + if (parts.length == 2) { + final List dimensions = parts.last.split('x'); + if (dimensions.length == 2) { + width = double.parse(dimensions[0]); + height = double.parse(dimensions[1]); + } + } + + final Uri uri = Uri.parse(path); + Widget child; + if (imageBuilder != null) { + child = imageBuilder!(uri, title, alt); + } else { + child = kDefaultImageBuilder(uri, imageDirectory, width, height); + } + + if (_linkHandlers.isNotEmpty) { + final TapGestureRecognizer recognizer = + _linkHandlers.last as TapGestureRecognizer; + return GestureDetector(child: child, onTap: recognizer.onTap); + } else { + return child; + } + } + + Widget _buildCheckbox(bool checked) { + if (checkboxBuilder != null) { + return checkboxBuilder!(checked); + } + return Padding( + padding: styleSheet.listBulletPadding!, + child: Icon( + checked ? Icons.check_box : Icons.check_box_outline_blank, + size: styleSheet.checkbox!.fontSize, + color: styleSheet.checkbox!.color, + ), + ); + } + + Widget _buildBullet(String listTag) { + final int index = _blocks.last.nextListIndex; + final bool isUnordered = listTag == 'ul'; + + if (bulletBuilder != null) { + return Padding( + padding: styleSheet.listBulletPadding!, + child: bulletBuilder!(index, + isUnordered ? BulletStyle.unorderedList : BulletStyle.orderedList), + ); + } + + if (isUnordered) { + return Padding( + padding: styleSheet.listBulletPadding!, + child: Text( + '•', + textAlign: TextAlign.center, + style: styleSheet.listBullet, + ), + ); + } + + return Padding( + padding: styleSheet.listBulletPadding!, + child: Text( + '${index + 1}.', + textAlign: TextAlign.right, + style: styleSheet.listBullet, + ), + ); + } + + Widget _buildTableCell(List children, {TextAlign? textAlign}) { + return TableCell( + child: Padding( + padding: styleSheet.tableCellsPadding!, + child: DefaultTextStyle( + style: styleSheet.tableBody!, + textAlign: textAlign, + child: Wrap(children: children as List), + ), + ), + ); + } + + void _addParentInlineIfNeeded(String? tag) { + if (_inlines.isEmpty) { + _inlines.add(_InlineElement( + tag, + style: styleSheet.styles[tag!], + )); + } + } + + void _addBlockChild(Widget child) { + final _BlockElement parent = _blocks.last; + if (parent.children.isNotEmpty) { + parent.children.add(SizedBox(height: styleSheet.blockSpacing)); + } + parent.children.add(child); + parent.nextListIndex += 1; + } + + void _addAnonymousBlockIfNeeded() { + if (_inlines.isEmpty) { + return; + } + + WrapAlignment blockAlignment = WrapAlignment.start; + TextAlign textAlign = TextAlign.start; + if (_isBlockTag(_currentBlockTag)) { + blockAlignment = _wrapAlignmentForBlockTag(_currentBlockTag); + textAlign = _textAlignForBlockTag(_currentBlockTag); + } + + final _InlineElement inline = _inlines.single; + if (inline.children.isNotEmpty) { + final List mergedInlines = _mergeInlineChildren( + inline.children, + textAlign, + ); + final Wrap wrap = Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: mergedInlines, + alignment: blockAlignment, + ); + _addBlockChild(wrap); + _inlines.clear(); + } + } + + /// Merges adjacent [TextSpan] children + List _mergeInlineChildren( + List children, + TextAlign? textAlign, + ) { + final List mergedTexts = []; + for (final Widget child in children) { + if (mergedTexts.isNotEmpty && + mergedTexts.last is RichText && + child is RichText) { + final RichText previous = mergedTexts.removeLast() as RichText; + final TextSpan previousTextSpan = previous.text as TextSpan; + final List children = previousTextSpan.children != null + ? List.from(previousTextSpan.children!) + : [previousTextSpan]; + children.add(child.text as TextSpan); + final TextSpan? mergedSpan = _mergeSimilarTextSpans(children); + mergedTexts.add(_buildRichText( + mergedSpan, + textAlign: textAlign, + )); + } else if (mergedTexts.isNotEmpty && + mergedTexts.last is SelectableText && + child is SelectableText) { + final SelectableText previous = + mergedTexts.removeLast() as SelectableText; + final TextSpan previousTextSpan = previous.textSpan!; + final List children = previousTextSpan.children != null + ? List.from(previousTextSpan.children!) + : [previousTextSpan]; + if (child.textSpan != null) { + children.add(child.textSpan!); + } + final TextSpan? mergedSpan = _mergeSimilarTextSpans(children); + mergedTexts.add( + _buildRichText( + mergedSpan, + textAlign: textAlign, + ), + ); + } else { + mergedTexts.add(child); + } + } + return mergedTexts; + } + + TextAlign _textAlignForBlockTag(String? blockTag) { + final WrapAlignment wrapAlignment = _wrapAlignmentForBlockTag(blockTag); + switch (wrapAlignment) { + case WrapAlignment.start: + return TextAlign.start; + case WrapAlignment.center: + return TextAlign.center; + case WrapAlignment.end: + return TextAlign.end; + case WrapAlignment.spaceAround: + return TextAlign.justify; + case WrapAlignment.spaceBetween: + return TextAlign.justify; + case WrapAlignment.spaceEvenly: + return TextAlign.justify; + } + } + + WrapAlignment _wrapAlignmentForBlockTag(String? blockTag) { + switch (blockTag) { + case 'p': + return styleSheet.textAlign; + case 'h1': + return styleSheet.h1Align; + case 'h2': + return styleSheet.h2Align; + case 'h3': + return styleSheet.h3Align; + case 'h4': + return styleSheet.h4Align; + case 'h5': + return styleSheet.h5Align; + case 'h6': + return styleSheet.h6Align; + case 'ul': + return styleSheet.unorderedListAlign; + case 'ol': + return styleSheet.orderedListAlign; + case 'blockquote': + return styleSheet.blockquoteAlign; + case 'pre': + return styleSheet.codeblockAlign; + case 'hr': + print('Markdown did not handle hr for alignment'); + break; + case 'li': + print('Markdown did not handle li for alignment'); + break; + } + return WrapAlignment.start; + } + + /// Combine text spans with equivalent properties into a single span. + TextSpan? _mergeSimilarTextSpans(List? textSpans) { + if (textSpans == null || textSpans.length < 2) { + return TextSpan(children: textSpans); + } + + final List mergedSpans = [textSpans.first]; + + for (int index = 1; index < textSpans.length; index++) { + final TextSpan nextChild = textSpans[index]; + if (nextChild is TextSpan && + nextChild.recognizer == mergedSpans.last.recognizer && + nextChild.semanticsLabel == mergedSpans.last.semanticsLabel && + nextChild.style == mergedSpans.last.style) { + final TextSpan previous = mergedSpans.removeLast(); + mergedSpans.add(TextSpan( + text: previous.toPlainText() + nextChild.toPlainText(), + recognizer: previous.recognizer, + semanticsLabel: previous.semanticsLabel, + style: previous.style, + )); + } else { + mergedSpans.add(nextChild); + } + } + + // When the mergered spans compress into a single TextSpan return just that + // TextSpan, otherwise bundle the set of TextSpans under a single parent. + return mergedSpans.length == 1 + ? mergedSpans.first + : TextSpan(children: mergedSpans); + } + + Widget _buildRichText(TextSpan? text, {TextAlign? textAlign}) { + if (selectable) { + return SelectableText.rich( + text!, + textScaleFactor: styleSheet.textScaleFactor, + textAlign: textAlign ?? TextAlign.start, + onTap: onTapText, + ); + } else { + return RichText( + text: text!, + textScaleFactor: styleSheet.textScaleFactor!, + textAlign: textAlign ?? TextAlign.start, + ); + } + } +} diff --git a/packages/flutter_markdown/lib/src/style_sheet.dart b/packages/flutter_markdown/lib/src/style_sheet.dart new file mode 100644 index 0000000000..f815047143 --- /dev/null +++ b/packages/flutter_markdown/lib/src/style_sheet.dart @@ -0,0 +1,688 @@ +// Copyright 2016 The Chromium 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.code, + this.h1, + this.h2, + this.h3, + this.h4, + this.h5, + this.h6, + 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.tableBorder, + this.tableColumnWidth, + this.tableCellsPadding, + this.tableCellsDecoration, + 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.textScaleFactor, + }) : _styles = { + '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.bodyText2?.fontSize != null); + return MarkdownStyleSheet( + a: const TextStyle(color: Colors.blue), + p: theme.textTheme.bodyText2, + code: theme.textTheme.bodyText2!.copyWith( + backgroundColor: theme.cardTheme.color ?? theme.cardColor, + fontFamily: 'monospace', + fontSize: theme.textTheme.bodyText2!.fontSize! * 0.85, + ), + h1: theme.textTheme.headline5, + h2: theme.textTheme.headline6, + h3: theme.textTheme.subtitle1, + h4: theme.textTheme.bodyText1, + h5: theme.textTheme.bodyText1, + h6: theme.textTheme.bodyText1, + em: const TextStyle(fontStyle: FontStyle.italic), + strong: const TextStyle(fontWeight: FontWeight.bold), + del: const TextStyle(decoration: TextDecoration.lineThrough), + blockquote: theme.textTheme.bodyText2, + img: theme.textTheme.bodyText2, + checkbox: theme.textTheme.bodyText2!.copyWith( + color: theme.primaryColor, + ), + blockSpacing: 8.0, + listIndent: 24.0, + listBullet: theme.textTheme.bodyText2, + listBulletPadding: const EdgeInsets.only(right: 4), + tableHead: const TextStyle(fontWeight: FontWeight.w600), + tableBody: theme.textTheme.bodyText2, + tableHeadAlign: TextAlign.center, + tableBorder: TableBorder.all( + color: theme.dividerColor, + width: 1, + ), + 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, + code: theme.textTheme.textStyle.copyWith( + backgroundColor: theme.brightness == Brightness.dark + ? CupertinoColors.systemGrey6.darkColor + : CupertinoColors.systemGrey6.color, + fontFamily: 'monospace', + fontSize: theme.textTheme.textStyle.fontSize! * 0.85, + ), + h1: theme.textTheme.textStyle.copyWith( + fontWeight: FontWeight.w500, + fontSize: theme.textTheme.textStyle.fontSize! + 10, + ), + h2: theme.textTheme.textStyle.copyWith( + fontWeight: FontWeight.w500, + fontSize: theme.textTheme.textStyle.fontSize! + 8, + ), + h3: theme.textTheme.textStyle.copyWith( + fontWeight: FontWeight.w500, + fontSize: theme.textTheme.textStyle.fontSize! + 6, + ), + h4: theme.textTheme.textStyle.copyWith( + fontWeight: FontWeight.w500, + fontSize: theme.textTheme.textStyle.fontSize! + 4, + ), + h5: theme.textTheme.textStyle.copyWith( + fontWeight: FontWeight.w500, + fontSize: theme.textTheme.textStyle.fontSize! + 2, + ), + h6: theme.textTheme.textStyle.copyWith( + fontWeight: FontWeight.w500, + ), + 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, + 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, + width: 1, + ), + ), + ), + ); + } + + /// 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.bodyText2, + code: theme.textTheme.bodyText2!.copyWith( + backgroundColor: theme.cardTheme.color ?? theme.cardColor, + fontFamily: 'monospace', + fontSize: theme.textTheme.bodyText2!.fontSize! * 0.85, + ), + h1: theme.textTheme.headline2, + h2: theme.textTheme.headline3, + h3: theme.textTheme.headline4, + h4: theme.textTheme.headline5, + h5: theme.textTheme.headline6, + h6: theme.textTheme.subtitle1, + em: const TextStyle(fontStyle: FontStyle.italic), + strong: const TextStyle(fontWeight: FontWeight.bold), + del: const TextStyle(decoration: TextDecoration.lineThrough), + blockquote: theme.textTheme.bodyText2, + img: theme.textTheme.bodyText2, + checkbox: theme.textTheme.bodyText2!.copyWith( + color: theme.primaryColor, + ), + blockSpacing: 8.0, + listIndent: 24.0, + listBullet: theme.textTheme.bodyText2, + listBulletPadding: const EdgeInsets.only(right: 4), + tableHead: const TextStyle(fontWeight: FontWeight.w600), + tableBody: theme.textTheme.bodyText2, + tableHeadAlign: TextAlign.center, + 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, + TextStyle? code, + TextStyle? h1, + TextStyle? h2, + TextStyle? h3, + TextStyle? h4, + TextStyle? h5, + TextStyle? h6, + 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, + TableBorder? tableBorder, + TableColumnWidth? tableColumnWidth, + EdgeInsets? tableCellsPadding, + Decoration? tableCellsDecoration, + 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, + double? textScaleFactor, + }) { + return MarkdownStyleSheet( + a: a ?? this.a, + p: p ?? this.p, + code: code ?? this.code, + h1: h1 ?? this.h1, + h2: h2 ?? this.h2, + h3: h3 ?? this.h3, + h4: h4 ?? this.h4, + h5: h5 ?? this.h5, + h6: h6 ?? this.h6, + 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, + tableBorder: tableBorder ?? this.tableBorder, + tableColumnWidth: tableColumnWidth ?? this.tableColumnWidth, + tableCellsPadding: tableCellsPadding ?? this.tableCellsPadding, + tableCellsDecoration: tableCellsDecoration ?? this.tableCellsDecoration, + 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, + textScaleFactor: textScaleFactor ?? this.textScaleFactor, + ); + } + + /// 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), + code: code!.merge(other.code), + h1: h1!.merge(other.h1), + h2: h2!.merge(other.h2), + h3: h3!.merge(other.h3), + h4: h4!.merge(other.h4), + h5: h5!.merge(other.h5), + h6: h6!.merge(other.h6), + 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, + tableBorder: other.tableBorder, + tableColumnWidth: other.tableColumnWidth, + tableCellsPadding: other.tableCellsPadding, + tableCellsDecoration: other.tableCellsDecoration, + 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, + ); + } + + /// The [TextStyle] to use for `a` elements. + final TextStyle? a; + + /// The [TextStyle] to use for `p` elements. + final TextStyle? p; + + /// The [TextStyle] to use for `code` elements. + final TextStyle? code; + + /// The [TextStyle] to use for `h1` elements. + final TextStyle? h1; + + /// The [TextStyle] to use for `h2` elements. + final TextStyle? h2; + + /// The [TextStyle] to use for `h3` elements. + final TextStyle? h3; + + /// The [TextStyle] to use for `h4` elements. + final TextStyle? h4; + + /// The [TextStyle] to use for `h5` elements. + final TextStyle? h5; + + /// The [TextStyle] to use for `h6` elements. + final TextStyle? h6; + + /// 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 [TableBorder] to use for `table` elements. + final TableBorder? tableBorder; + + /// The [TableColumnWidth] to use for `th` and `td` elements. + final TableColumnWidth? tableColumnWidth; + + /// 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 `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 scale factor to use in textual elements + final double? textScaleFactor; + + /// A [Map] from element name to the corresponding [TextStyle] object. + Map get styles => _styles; + Map _styles; + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(dynamic other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != MarkdownStyleSheet) { + return false; + } + final MarkdownStyleSheet typedOther = other; + return typedOther.a == a && + typedOther.p == p && + typedOther.code == code && + typedOther.h1 == h1 && + typedOther.h2 == h2 && + typedOther.h3 == h3 && + typedOther.h4 == h4 && + typedOther.h5 == h5 && + typedOther.h6 == h6 && + typedOther.em == em && + typedOther.strong == strong && + typedOther.del == del && + typedOther.blockquote == blockquote && + typedOther.img == img && + typedOther.checkbox == checkbox && + typedOther.blockSpacing == blockSpacing && + typedOther.listIndent == listIndent && + typedOther.listBullet == listBullet && + typedOther.listBulletPadding == listBulletPadding && + typedOther.tableHead == tableHead && + typedOther.tableBody == tableBody && + typedOther.tableHeadAlign == tableHeadAlign && + typedOther.tableBorder == tableBorder && + typedOther.tableColumnWidth == tableColumnWidth && + typedOther.tableCellsPadding == tableCellsPadding && + typedOther.tableCellsDecoration == tableCellsDecoration && + typedOther.blockquotePadding == blockquotePadding && + typedOther.blockquoteDecoration == blockquoteDecoration && + typedOther.codeblockPadding == codeblockPadding && + typedOther.codeblockDecoration == codeblockDecoration && + typedOther.horizontalRuleDecoration == horizontalRuleDecoration && + typedOther.textAlign == textAlign && + typedOther.h1Align == h1Align && + typedOther.h2Align == h2Align && + typedOther.h3Align == h3Align && + typedOther.h4Align == h4Align && + typedOther.h5Align == h5Align && + typedOther.h6Align == h6Align && + typedOther.unorderedListAlign == unorderedListAlign && + typedOther.orderedListAlign == orderedListAlign && + typedOther.blockquoteAlign == blockquoteAlign && + typedOther.codeblockAlign == codeblockAlign && + typedOther.textScaleFactor == textScaleFactor; + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode { + return hashList([ + a, + p, + code, + h1, + h2, + h3, + h4, + h5, + h6, + em, + strong, + del, + blockquote, + img, + checkbox, + blockSpacing, + listIndent, + listBullet, + listBulletPadding, + tableHead, + tableBody, + tableHeadAlign, + tableBorder, + tableColumnWidth, + tableCellsPadding, + tableCellsDecoration, + blockquotePadding, + blockquoteDecoration, + codeblockPadding, + codeblockDecoration, + horizontalRuleDecoration, + textAlign, + h1Align, + h2Align, + h3Align, + h4Align, + h5Align, + h6Align, + unorderedListAlign, + orderedListAlign, + blockquoteAlign, + codeblockAlign, + textScaleFactor, + ]); + } +} diff --git a/packages/flutter_markdown/lib/src/widget.dart b/packages/flutter_markdown/lib/src/widget.dart new file mode 100644 index 0000000000..5be6ae6dc9 --- /dev/null +++ b/packages/flutter_markdown/lib/src/widget.dart @@ -0,0 +1,534 @@ +// Copyright 2016 The Chromium 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/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:markdown/markdown.dart' as md; +import 'package:meta/meta.dart'; + +import '_functions_io.dart' if (dart.library.html) '_functions_web.dart'; +import 'builder.dart'; +import 'style_sheet.dart'; + +/// 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(int index, BulletStyle style); + +/// 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 { + /// 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 of this tag, will passing + /// to [preferredStyle]. + /// + /// If you needn't build a widget, return null. + 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/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/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. +/// * +abstract class MarkdownWidget extends StatefulWidget { + /// Creates a widget that parses and displays Markdown. + /// + /// The [data] argument must not be null. + const MarkdownWidget({ + Key? key, + required this.data, + this.selectable = false, + this.styleSheet, + this.styleSheetTheme = MarkdownStyleSheetBaseTheme.material, + this.syntaxHighlighter, + this.onTapLink, + this.onTapText, + this.imageDirectory, + this.blockSyntaxes, + this.inlineSyntaxes, + this.extensionSet, + this.imageBuilder, + this.checkboxBuilder, + this.bulletBuilder, + this.builders = const {}, + this.fitContent = false, + this.listItemCrossAxisAlignment = + MarkdownListItemCrossAxisAlignment.baseline, + }) : super(key: key); + + /// 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; + + /// 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? blockSyntaxes; + + /// Collection of custom inline syntax types to be used parsing the Markdown data. + final List? 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 builders; + + /// 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; + + /// Subclasses should override this function to display the given children, + /// which are the parsed representation of [data]. + @protected + Widget build(BuildContext context, List? children); + + @override + _MarkdownWidgetState createState() => _MarkdownWidgetState(); +} + +class _MarkdownWidgetState extends State + implements MarkdownBuilderDelegate { + List? _children; + final List _recognizers = []; + + @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 ?? []) + ..add(TaskListSyntax()), + extensionSet: widget.extensionSet ?? md.ExtensionSet.gitHubFlavored, + encodeHtml: false, + ); + + // Parse the source Markdown data into nodes of an Abstract Syntax Tree. + final List lines = const LineSplitter().convert(widget.data); + final List 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, + fitContent: widget.fitContent, + listItemCrossAxisAlignment: widget.listItemCrossAxisAlignment, + onTapText: widget.onTapText, + ); + + _children = builder.build(astNodes); + } + + void _disposeRecognizers() { + if (_recognizers.isEmpty) { + return; + } + final List localRecognizers = + List.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. +/// * +class MarkdownBody extends MarkdownWidget { + /// Creates a non-scrolling widget that parses and displays Markdown. + const MarkdownBody({ + Key? key, + required String data, + bool selectable = false, + MarkdownStyleSheet? styleSheet, + MarkdownStyleSheetBaseTheme? styleSheetTheme, + SyntaxHighlighter? syntaxHighlighter, + MarkdownTapLinkCallback? onTapLink, + VoidCallback? onTapText, + String? imageDirectory, + List? blockSyntaxes, + List? inlineSyntaxes, + md.ExtensionSet? extensionSet, + MarkdownImageBuilder? imageBuilder, + MarkdownCheckboxBuilder? checkboxBuilder, + MarkdownBulletBuilder? bulletBuilder, + Map builders = + const {}, + MarkdownListItemCrossAxisAlignment listItemCrossAxisAlignment = + MarkdownListItemCrossAxisAlignment.baseline, + this.shrinkWrap = true, + bool fitContent = true, + }) : super( + key: key, + data: data, + selectable: selectable, + styleSheet: styleSheet, + styleSheetTheme: styleSheetTheme, + syntaxHighlighter: syntaxHighlighter, + onTapLink: onTapLink, + onTapText: onTapText, + imageDirectory: imageDirectory, + blockSyntaxes: blockSyntaxes, + inlineSyntaxes: inlineSyntaxes, + extensionSet: extensionSet, + imageBuilder: imageBuilder, + checkboxBuilder: checkboxBuilder, + builders: builders, + listItemCrossAxisAlignment: listItemCrossAxisAlignment, + bulletBuilder: bulletBuilder, + fitContent: fitContent, + ); + + /// See [ScrollView.shrinkWrap] + final bool shrinkWrap; + + @override + Widget build(BuildContext context, List? children) { + if (children!.length == 1) { + 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. +/// * +class Markdown extends MarkdownWidget { + /// Creates a scrolling widget that parses and displays Markdown. + const Markdown({ + Key? key, + required String data, + bool selectable = false, + MarkdownStyleSheet? styleSheet, + MarkdownStyleSheetBaseTheme? styleSheetTheme, + SyntaxHighlighter? syntaxHighlighter, + MarkdownTapLinkCallback? onTapLink, + VoidCallback? onTapText, + String? imageDirectory, + List? blockSyntaxes, + List? inlineSyntaxes, + md.ExtensionSet? extensionSet, + MarkdownImageBuilder? imageBuilder, + MarkdownCheckboxBuilder? checkboxBuilder, + MarkdownBulletBuilder? bulletBuilder, + Map builders = + const {}, + MarkdownListItemCrossAxisAlignment listItemCrossAxisAlignment = + MarkdownListItemCrossAxisAlignment.baseline, + this.padding = const EdgeInsets.all(16.0), + this.controller, + this.physics, + this.shrinkWrap = false, + }) : super( + key: key, + data: data, + selectable: selectable, + styleSheet: styleSheet, + styleSheetTheme: styleSheetTheme, + syntaxHighlighter: syntaxHighlighter, + onTapLink: onTapLink, + onTapText: onTapText, + imageDirectory: imageDirectory, + blockSyntaxes: blockSyntaxes, + inlineSyntaxes: inlineSyntaxes, + extensionSet: extensionSet, + imageBuilder: imageBuilder, + checkboxBuilder: checkboxBuilder, + builders: builders, + listItemCrossAxisAlignment: listItemCrossAxisAlignment, + bulletBuilder: bulletBuilder, + ); + + /// 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? 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-). +class TaskListSyntax extends md.InlineSyntax { + /// Cretaes a new instance. + TaskListSyntax() : super(_pattern); + + // FIXME: Waiting for dart-lang/markdown#269 to land + 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; + } +} diff --git a/packages/flutter_markdown/pubspec.yaml b/packages/flutter_markdown/pubspec.yaml new file mode 100644 index 0000000000..03863ae23f --- /dev/null +++ b/packages/flutter_markdown/pubspec.yaml @@ -0,0 +1,22 @@ +name: flutter_markdown +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/flutter/packages/tree/master/packages/flutter_markdown +version: 0.6.2 + +dependencies: + flutter: + sdk: flutter + markdown: ^4.0.0 + meta: ^1.3.0 + path: ^1.8.0 + +dev_dependencies: + flutter_test: + sdk: flutter + mockito: ^5.0.0 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.17.0" diff --git a/packages/flutter_markdown/test/all.dart b/packages/flutter_markdown/test/all.dart new file mode 100644 index 0000000000..3e14e329e2 --- /dev/null +++ b/packages/flutter_markdown/test/all.dart @@ -0,0 +1,43 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +library flutter_markdown.all_test; + +import 'blockquote_test.dart' as blockquote_test; +import 'custom_syntax_test.dart' as custome_syntax_test; +import 'emphasis_test.dart' as emphasis_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 '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_scale_factor_test.dart' as text_scale_factor; +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(); + 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(); + style_sheet_test.defineTests(); + table_test.defineTests(); + text_test.defineTests(); + text_alignment_test.defineTests(); + text_scale_factor.defineTests(); + uri_test.defineTests(); +} diff --git a/packages/flutter_markdown/test/assets/images/golden/image_test/custom_builder_asset_logo.png b/packages/flutter_markdown/test/assets/images/golden/image_test/custom_builder_asset_logo.png new file mode 100644 index 0000000000..cbf90d4575 Binary files /dev/null and b/packages/flutter_markdown/test/assets/images/golden/image_test/custom_builder_asset_logo.png differ diff --git a/packages/flutter_markdown/test/assets/images/golden/image_test/resource_asset_logo.png b/packages/flutter_markdown/test/assets/images/golden/image_test/resource_asset_logo.png new file mode 100644 index 0000000000..cbf90d4575 Binary files /dev/null and b/packages/flutter_markdown/test/assets/images/golden/image_test/resource_asset_logo.png differ diff --git a/packages/flutter_markdown/test/assets/images/logo.png b/packages/flutter_markdown/test/assets/images/logo.png new file mode 100644 index 0000000000..00357cb9c9 Binary files /dev/null and b/packages/flutter_markdown/test/assets/images/logo.png differ diff --git a/packages/flutter_markdown/test/blockquote_test.dart b/packages/flutter_markdown/test/blockquote_test.dart new file mode 100644 index 0000000000..168cd49e2c --- /dev/null +++ b/packages/flutter_markdown/test/blockquote_test.dart @@ -0,0 +1,108 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/widgets.dart'; +import 'package:flutter_markdown/flutter_markdown.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 widgets = tester.allWidgets; + expectTextStrings(widgets, ['quote']); + }, + ); + + 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 widgets = tester.allWidgets; + final DecoratedBox blockQuoteContainer = tester.widget( + find.byType(DecoratedBox), + ); + final RichText qouteText = tester.widget(find.byType(RichText)); + final List styledTextParts = + (qouteText.text as TextSpan).children!.cast(); + + expectTextStrings( + widgets, + [ + '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.bodyText2!.color, + ); + + /// Markdown guide + expect(styledTextParts[1].text, 'Markdown guide'); + expect(styledTextParts[1].style!.color, Colors.blue); + + /// and this is + expect(styledTextParts[2].text, ' and this is '); + expect( + styledTextParts[2].style!.color, + theme.textTheme.bodyText2!.color, + ); + + /// bold + expect(styledTextParts[3].text, 'bold'); + expect(styledTextParts[3].style!.fontWeight, FontWeight.bold); + + /// and + expect(styledTextParts[4].text, ' and '); + expect( + styledTextParts[4].style!.color, + theme.textTheme.bodyText2!.color, + ); + + /// italic + expect(styledTextParts[5].text, 'italic'); + expect(styledTextParts[5].style!.fontStyle, FontStyle.italic); + }, + ); + }); +} diff --git a/packages/flutter_markdown/test/custom_syntax_test.dart b/packages/flutter_markdown/test/custom_syntax_test.dart new file mode 100644 index 0000000000..550f223e1d --- /dev/null +++ b/packages/flutter_markdown/test/custom_syntax_test.dart @@ -0,0 +1,128 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/flutter_markdown.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: [SubscriptSyntax()], + builders: { + 'sub': SubscriptBuilder(), + }, + ), + ), + ); + + final Iterable widgets = tester.allWidgets; + expectTextStrings(widgets, ['H₂O']); + }, + ); + + testWidgets( + 'link for wikistyle', + (WidgetTester tester) async { + await tester.pumpWidget( + boilerplate( + Markdown( + data: 'This is a [[wiki link]]', + extensionSet: md.ExtensionSet.none, + inlineSyntaxes: [WikilinkSyntax()], + builders: { + 'wikilink': WikilinkBuilder(), + }, + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = + (textWidget.text as TextSpan).children![1] as TextSpan; + + expect(span.children, null); + expect(span.recognizer.runtimeType, equals(TapGestureRecognizer)); + }, + ); + }); +} + +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 _subscripts = [ + '₀', + '₁', + '₂', + '₃', + '₄', + '₅', + '₆', + '₇', + '₈', + '₉' + ]; + + @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 RichText(text: 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 md.Element el = md.Element.withTag('wikilink'); + el.attributes['href'] = match[1]!.replaceAll(' ', '_'); + el.children!.add(md.Element.text('span', match[1]!)); + + parser.addNode(el); + return true; + } +} + +class WikilinkBuilder extends MarkdownElementBuilder { + @override + Widget visitElementAfter(md.Element element, _) { + return RichText( + text: TextSpan( + text: element.textContent, + recognizer: TapGestureRecognizer()..onTap = () {}), + ); + } +} diff --git a/packages/flutter_markdown/test/emphasis_test.dart b/packages/flutter_markdown/test/emphasis_test.dart new file mode 100644 index 0000000000..0224f048d1 --- /dev/null +++ b/packages/flutter_markdown/test/emphasis_test.dart @@ -0,0 +1,4410 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/flutter_markdown.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'utils.dart'; + +// The emphasis and strong emphasis section of the GitHub Flavored Markdown +// specification (https://github.github.com/gfm/#emphasis-and-strong-emphasis) +// is extensive covering over 130 example cases. The tests in this file cover +// all of the GFM tests; example 360 through 490. + +void main() => defineTests(); + +void defineTests() { + group( + 'Emphasis', + () { + group( + 'Rule 1', + () { + // Rule 1 tests check the single '*' can open emphasis. + testWidgets( + // Example 360 from GFM. + 'italic text using asterisk tags', + (WidgetTester tester) async { + const String data = '*foo 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 bar'); + + expectTextSpanStyle( + richText.text as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 361 from GFM. + 'invalid left-flanking delimiter run because * is followed by whitespace', + (WidgetTester tester) async { + const String data = 'a * foo bar*'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 362 from GFM. + 'invalid left-flanking delimiter run because * preceded by alphanumeric followed by punctuation', + (WidgetTester tester) async { + const String data = 'a*"foo bar"*'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + // NOTE: Example 363 is not included. The test is "Unicode nonbreaking + // spaces count as whitespace, too: '* a *' The Markdown parse sees + // this as a unordered list item." https://github.github.com/gfm/#example-363 + + testWidgets( + // Example 364 from GFM. + 'intraword emphasis with * is permitted alpha characters', + (WidgetTester tester) async { + const String data = 'foo*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; + expect(richText, isNotNull); + final String text = richText.text.toPlainText(); + expect(text, 'foobar'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is normal text with no emphasis. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.normal, + ); + + // Second span has italic style with normal weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 365 from GFM. + 'intraword emphasis with * is permitted numeric characters', + (WidgetTester tester) async { + const String data = '5*6*78'; + 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; + expect(richText, isNotNull); + final String text = richText.text.toPlainText(); + expect(text, '5678'); + + // There should be three spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 3, isTrue); + + // First text span is normal text with no emphasis. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.normal, + ); + + // Second span has italic style with normal weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + + // Third text span is normal text with no emphasis. + final InlineSpan thirdSpan = textSpan.children![2]; + expectTextSpanStyle( + thirdSpan as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + }, + ); + + group('Rule 2', () { + testWidgets( + // Example 366 from GFM. + 'italic text using underscore tags', + (WidgetTester tester) async { + const String data = '_foo 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 bar'); + + expectTextSpanStyle( + richText.text as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 367 from GFM. + 'invalid left-flanking delimiter run because _ is followed by whitespace', + (WidgetTester tester) async { + const String data = '_ foo bar_'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 368 from GFM. + 'invalid left-flanking delimiter run because _ preceded by alphanumeric followed by punctuation', + (WidgetTester tester) async { + const String data = 'a_"foo bar"_'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 369 from GFM. + 'emphasis with _ is not allowed inside words alpha characters', + (WidgetTester tester) async { + const String data = 'foo_bar_'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 370 from GFM. + 'emphasis with _ is not allowed inside words numeric characters', + (WidgetTester tester) async { + const String data = '5_6_78'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 371 from GFM. + 'emphasis with _ is not allowed inside words unicode characters', + (WidgetTester tester) async { + const String data = 'пристаням_стремятся_'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 372 from GFM. + 'invalid first delimiter right-flanking followed by second delimiter left-flanking', + (WidgetTester tester) async { + const String data = 'aa_"bb"_cc'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 373 from GFM. + 'valid open delimiter left- and right-flanking preceded by punctuation', + (WidgetTester tester) async { + const String data = 'foo-_(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-(bar)'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is normal text with no emphasis. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.normal, + ); + + // Second span has italic style with normal weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + }); + + group('Rule 3', () { + testWidgets( + // Example 374 from GFM. + 'invalid emphasis - closing delimiter does not match opening delimiter', + (WidgetTester tester) async { + const String data = '_foo*'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 375 from GFM. + 'invalid emphasis - closing * is preceded by whitespace', + (WidgetTester tester) async { + const String data = '*foo bar *'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 376 from GFM. + 'invalid emphasis - closing * is preceded by newline', + (WidgetTester tester) async { + const String data = '*foo bar\n*'; + 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 bar *'); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 377 from GFM. + 'invalid emphasis - second * is preceded by punctuation followed by alphanumeric', + (WidgetTester tester) async { + const String data = '*(*foo)'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 378 from GFM. + 'nested * emphasis', + (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)'); + + expectTextSpanStyle( + richText.text as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 379 from GFM. + 'intraword emphasis with * is allowed', + (WidgetTester tester) async { + const String data = '*foo*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, 'foobar'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, 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 normal text with no emphasis. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + }); + + group('Rule 4', () { + testWidgets( + // Example 380 from GFM. + 'invalid emphasis because closing _ is preceded by whitespace', + (WidgetTester tester) async { + const String data = '_foo bar _'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 381 from GFM. + 'invalid emphasis because second _ is preceded by punctuation and followed by an alphanumeric', + (WidgetTester tester) async { + const String data = '_(_foo)'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 382 from GFM. + 'nested _ emphasis', + (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)'); + + expectTextSpanStyle( + richText.text as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 383 from GFM. + 'intraword emphasis with _ is disallowed - alpha characters', + (WidgetTester tester) async { + const String data = '_foo_bar'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 384 from GFM. + 'intraword emphasis with _ is disallowed - unicode characters', + (WidgetTester tester) async { + const String data = '_пристаням_стремятся'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 385 from GFM. + 'intraword emphasis with _ is disallowed - nested emphasis tags', + (WidgetTester tester) async { + const String data = '_foo_bar_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_bar_baz'); + + expectTextSpanStyle( + richText.text as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 386 from GFM. + 'valid emphasis closing delimiter is both left- and right-flanking followed by punctuation', + (WidgetTester tester) async { + const String data = '_(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, '(bar).'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, 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 normal text with no emphasis. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + }); + + group('Rule 5', () { + testWidgets( + // Example 387 from GFM. + 'strong emphasis using ** emphasis tags', + (WidgetTester tester) async { + const String data = '**foo 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 bar'); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 388 from GFM. + 'invalid strong emphasis - opening delimiter followed by whitespace', + (WidgetTester tester) async { + const String data = '** foo bar**'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 389 from GFM. + 'invalid strong emphasis - opening ** is preceded by an alphanumeric and followed by punctuation', + (WidgetTester tester) async { + const String data = 'a**"foo"**'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 390 from GFM. + 'intraword strong emphasis with ** is permitted', + (WidgetTester tester) async { + const String data = 'foo**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, 'foobar'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is normal text with no emphasis. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.normal, + ); + + // Second span has bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + }); + + group('Rule 6', () { + testWidgets( + // Example 391 from GFM. + 'strong emphasis using __ emphasis tags', + (WidgetTester tester) async { + const String data = '__foo 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 bar'); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 392 from GFM. + 'invalid strong emphasis - opening delimiter followed by whitespace', + (WidgetTester tester) async { + const String data = '__ foo bar__'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 393 from GFM. + 'invalid strong emphasis - opening delimiter followed by newline', + (WidgetTester tester) async { + const String data = '__\nfoo 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 bar__'); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 394 from GFM. + 'invalid strong emphasis - opening __ is preceded by an alphanumeric and followed by punctuation', + (WidgetTester tester) async { + const String data = 'a__"foo"__'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 395 from GFM. + 'intraword strong emphasis is forbidden with __ - alpha characters', + (WidgetTester tester) async { + const String data = 'foo__bar__'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 396 from GFM. + 'intraword strong emphasis is forbidden with __ - numeric characters', + (WidgetTester tester) async { + const String data = '5__6__78'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 397 from GFM. + 'intraword strong emphasis is forbidden with __ - unicode characters', + (WidgetTester tester) async { + const String data = 'пристаням__стремятся__'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 398 from GFM. + 'intraword strong emphasis is forbidden with __ - nested strong emphasis', + (WidgetTester tester) async { + const String data = '__foo, __bar__, baz__'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, 'foo, bar, baz'); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 399 from GFM. + 'valid strong emphasis because opening delimiter is both left- and right-flanking preceded by punctuation', + (WidgetTester tester) async { + const String data = 'foo-__(bar)__'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, 'foo-(bar)'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is normal text with no emphasis. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.normal, + ); + + // Second span has bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + }); + + group('Rule 7', () { + testWidgets( + // Example 400 from GFM. + 'invalid strong emphasis - closing delimiter is preceded by whitespace', + (WidgetTester tester) async { + const String data = '**foo bar **'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 401 from GFM. + 'invalid strong emphasis - second ** is preceded by punctuation and followed by an alphanumeric', + (WidgetTester tester) async { + const String data = '**(**foo)'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 402 from GFM. + 'emphasis with nested strong emphasis', + (WidgetTester tester) async { + const String data = '*(**foo**)*'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, '(foo)'); + + // There should be three spans of text. + final TextSpan textSpan = richText.text 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 has italic style with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.bold, + ); + + // 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 403 from GFM. + 'strong emphasis with multiple nested emphasis', + (WidgetTester tester) async { + const String data = + '**Gomphocarpus (*Gomphocarpus physocarpus*, syn. *Asclepias physocarpa*)**'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, + 'Gomphocarpus (Gomphocarpus physocarpus, syn. Asclepias physocarpa)'); + + // There should be five spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 5, isTrue); + + // First text span has bold weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.bold, + ); + + // Second span has both italic style with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.bold, + ); + + // Third text span has bold weight. + final InlineSpan thirdSpan = textSpan.children![2]; + expectTextSpanStyle( + thirdSpan as TextSpan, + null, + FontWeight.bold, + ); + + // Fourth text span has both italic style with bold weight. + final InlineSpan fourthSpan = textSpan.children![3]; + expectTextSpanStyle( + fourthSpan as TextSpan, + FontStyle.italic, + FontWeight.bold, + ); + + // Fifth text span has bold weight. + final InlineSpan fifthSpan = textSpan.children![4]; + expectTextSpanStyle( + fifthSpan as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 404 from GFM. + 'strong emphasis with nested emphasis', + (WidgetTester tester) async { + const String data = '**foo "*bar*" foo**'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, 'foo "bar" foo'); + + // There should be three spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 3, isTrue); + + // First text span has bold weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.bold, + ); + + // Second span has both italic style with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.bold, + ); + + // Third text span has bold weight. + final InlineSpan thirdSpan = textSpan.children![2]; + expectTextSpanStyle( + thirdSpan as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 405 from GFM. + 'intraword strong emphasis', + (WidgetTester tester) async { + const String data = '**foo**bar'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, 'foobar'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is normal text with strong emphasis. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.bold, + ); + + // Second span is normal text with no emphasis. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + }); + + group('Rule 8', () { + testWidgets( + // Example 406 from GFM. + 'invalid strong emphasis - closing delimiter is preceded by whitespace', + (WidgetTester tester) async { + const String data = '__foo bar __'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 407 from GFM. + 'invalid strong emphasis - second __ is preceded by punctuation followed by alphanumeric', + (WidgetTester tester) async { + const String data = '__(__foo)'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 408 from GFM. + 'strong emphasis nested in emphasis', + (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)'); + + // There should be three spans of text. + final TextSpan textSpan = richText.text 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 has italic style with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.bold, + ); + + // 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 409 from GFM. + 'intraword strong emphasis is forbidden with __ - alpha characters', + (WidgetTester tester) async { + const String data = '__foo__bar'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 410 from GFM. + 'intraword strong emphasis is forbidden with __ - unicode characters', + (WidgetTester tester) async { + const String data = '__пристаням__стремятся'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 411 from GFM. + 'intraword nested strong emphasis is forbidden with __', + (WidgetTester tester) async { + const String data = '__foo__bar__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__bar__baz'); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 412 from GFM. + 'strong emphasis because closing delimiter is both left- and right-flanking is followed by punctuation', + (WidgetTester tester) async { + const String data = '__(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, '(bar).'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is normal text with strong emphasis. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.bold, + ); + + // Second span has italic style with normal weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + }); + + group('Rule 9', () { + testWidgets( + // Example 413 from GFM. + 'nonempty sequence emphasis span - text followed by link', + (WidgetTester tester) async { + const String data = '*foo [bar](/url)*'; + 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 bar'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is plain text and has italic style with normal weight. + final TextSpan firstSpan = textSpan.children![0] as TextSpan; + expect(firstSpan.recognizer, isNull); + expectTextSpanStyle( + firstSpan, + FontStyle.italic, + FontWeight.normal, + ); + + // Second span has italic style with normal weight. + final TextSpan secondSpan = textSpan.children![1] as TextSpan; + expect(secondSpan.recognizer, isNotNull); + expect(secondSpan.recognizer is GestureRecognizer, isTrue); + expectTextSpanStyle( + secondSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 414 from GFM. + 'nonempty sequence emphasis span - two lines of text', + (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 bar'); + + expectTextSpanStyle( + richText.text as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 415 from GFM. + 'strong emphasis nested inside emphasis - _ delimiter', + (WidgetTester tester) async { + const String data = '_foo __bar__ 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 bar baz'); + + // There should be three spans of text. + final TextSpan textSpan = richText.text 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 has italic style with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.bold, + ); + + // 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 416 from GFM. + 'emphasis nested inside emphasis', + (WidgetTester tester) async { + const String data = '_foo _bar_ 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 bar baz'); + + expectTextSpanStyle( + richText.text as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 417 from GFM. + 'intraword emphasis nested inside emphasis - _ delimiter', + (WidgetTester tester) async { + const String data = '__foo_ 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 bar'); + + expectTextSpanStyle( + richText.text as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 418 from GFM. + 'intraword emphasis nested inside emphasis - * delimiter', + (WidgetTester tester) async { + const String data = '*foo *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 bar'); + + expectTextSpanStyle( + richText.text as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 419 from GFM. + 'strong emphasis nested inside emphasis - * delimiter', + (WidgetTester tester) async { + const String data = '*foo **bar** 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 bar baz'); + + // There should be three spans of text. + final TextSpan textSpan = richText.text 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 has italic style with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.bold, + ); + + // 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 418 from GFM. + 'intraword strong emphasis nested inside emphasis - * delimiter', + (WidgetTester tester) async { + const String data = '*foo**bar**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, 'foobarbaz'); + + // There should be three spans of text. + final TextSpan textSpan = richText.text 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 has italic style with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.bold, + ); + + // 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 421 from GFM. + 'consecutive emphasis sections are not allowed', + (WidgetTester tester) async { + const String data = '*foo**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**bar'); + + expectTextSpanStyle( + richText.text as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 422 from GFM. + 'strong emphasis nested inside emphasis - space after first word', + (WidgetTester tester) async { + const String data = '***foo** 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 bar'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span has italic style with bold weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + FontStyle.italic, + FontWeight.bold, + ); + + // Second span has italic style with normal weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 423 from GFM. + 'strong emphasis nested inside emphasis - space before second word', + (WidgetTester tester) async { + const String data = '*foo **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 bar'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, 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 has italic style with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 424 from GFM. + 'intraword strong emphasis nested inside emphasis', + (WidgetTester tester) async { + const String data = '*foo**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, 'foobar'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, 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 has italic style with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 425 from GFM. + 'intraword emphasis and strong emphasis', + (WidgetTester tester) async { + const String data = 'foo***bar***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, 'foobarbaz'); + + // There should be three spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 3, isTrue); + + // First text span is plain text with normal weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.normal, + ); + + // Second span has italic style with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.bold, + ); + + // Third text span is plain text with normal weight. + final InlineSpan thirdSpan = textSpan.children![2]; + expectTextSpanStyle( + thirdSpan as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 426 from GFM. + 'intraword emphasis and strong emphasis - multiples of 3', + (WidgetTester tester) async { + const String data = 'foo******bar*********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, 'foobar***baz'); + + // There should be three spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 3, isTrue); + + // First text span is plain text with normal weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.normal, + ); + + // Second span is plain text with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + null, + FontWeight.bold, + ); + + // Third text span is plain text with normal weight. + final InlineSpan thirdSpan = textSpan.children![2]; + expectTextSpanStyle( + thirdSpan as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 427 from GFM. + 'infinite levels of nesting are possible within emphasis', + (WidgetTester tester) async { + const String data = '*foo **bar *baz*\nbim** bop*'; + 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 bar baz bim bop'); + + // There should be five spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length, 3); + + // First text span has italic style and normal weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + + // Second span has both italic style with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.bold, + ); + + // Third text span has bold weight. + final InlineSpan thirdSpan = textSpan.children![2]; + expectTextSpanStyle( + thirdSpan as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 428 from GFM. + 'infinite levels of nesting are possible within emphasis - text and a link', + (WidgetTester tester) async { + const String data = '*foo [*bar*](/url)*'; + 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 bar'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span has italic style and normal weight. + final TextSpan firstSpan = textSpan.children![0] as TextSpan; + expect(firstSpan.recognizer, isNull); + expectTextSpanStyle( + firstSpan, + FontStyle.italic, + FontWeight.normal, + ); + + // Second span has italic style with normal weight. + final TextSpan secondSpan = textSpan.children![1] as TextSpan; + expect(secondSpan.recognizer, isNotNull); + expect(secondSpan.recognizer is GestureRecognizer, isTrue); + expectTextSpanStyle( + secondSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 429 from GFM. + 'there can be no empty emphasis * delimiter', + (WidgetTester tester) async { + const String data = '** is not an empty emphasis'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 430 from GFM. + 'there can be no empty strong emphasis * delimiter', + (WidgetTester tester) async { + const String data = '**** is not an empty strong emphasis'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + }); + + group('Rule 10', () { + testWidgets( + // Example 431 from GFM. + 'nonempty sequence of inline elements with strong emphasis - text and a link', + (WidgetTester tester) async { + const String data = '**foo [bar](/url)**'; + 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 bar'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is normal text with bold weight. + final TextSpan firstSpan = textSpan.children![0] as TextSpan; + expect(firstSpan.recognizer, isNull); + expectTextSpanStyle( + firstSpan, + null, + FontWeight.bold, + ); + + // Second span is a link with bold weight. + final TextSpan secondSpan = textSpan.children![1] as TextSpan; + expect(secondSpan.recognizer, isNotNull); + expect(secondSpan.recognizer is GestureRecognizer, isTrue); + expectTextSpanStyle( + secondSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 432 from GFM. + 'nonempty sequence of inline elements with strong emphasis - two lines of texts', + (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 bar'); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 433 from GFM. + 'emphasis and strong emphasis nested inside strong emphasis - nested emphasis', + (WidgetTester tester) async { + const String data = '__foo _bar_ 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 bar baz'); + + // There should be three spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 3, isTrue); + + // First text span is plain text with bold weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.bold, + ); + + // Second span has italic style with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.bold, + ); + + // Third text span is plain text with bold weight. + final InlineSpan thirdSpan = textSpan.children![2]; + expectTextSpanStyle( + thirdSpan as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 434 from GFM. + 'emphasis and strong emphasis nested inside strong emphasis - nested strong emphasis', + (WidgetTester tester) async { + const String data = '__foo __bar__ 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 bar baz'); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 435 from GFM. + 'emphasis and strong emphasis nested inside strong emphasis - nested strong emphasis', + (WidgetTester tester) async { + const String data = '____foo__ 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 bar'); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 436 from GFM. + 'emphasis and strong emphasis nested inside strong emphasis - nested strong emphasis', + (WidgetTester tester) async { + const String data = '**foo **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 bar'); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 437 from GFM. + 'emphasis and strong emphasis nested inside strong emphasis - nested emphasis', + (WidgetTester tester) async { + const String data = '**foo *bar* 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 bar baz'); + + // There should be three spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 3, isTrue); + + // First text span is plain text with bold weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.bold, + ); + + // Second span has italic style with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.bold, + ); + + // Third text span is plain text with bold weight. + final InlineSpan thirdSpan = textSpan.children![2]; + expectTextSpanStyle( + thirdSpan as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 438 from GFM. + 'emphasis and strong emphasis nested inside strong emphasis - intraword nested emphasis', + (WidgetTester tester) async { + const String data = '**foo*bar*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, 'foobarbaz'); + + // There should be three spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 3, isTrue); + + // First text span is plain text with bold weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.bold, + ); + + // Second span has italic style with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.bold, + ); + + // Third text span is plain text with bold weight. + final InlineSpan thirdSpan = textSpan.children![2]; + expectTextSpanStyle( + thirdSpan as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 439 from GFM. + 'emphasis and strong emphasis nested inside strong emphasis - nested emphasis on first word', + (WidgetTester tester) async { + const String data = '***foo* 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 bar'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span has italic style with bold weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + FontStyle.italic, + FontWeight.bold, + ); + + // Second span is plain text with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 440 from GFM. + 'emphasis and strong emphasis nested inside strong emphasis - nested emphasis on second word', + (WidgetTester tester) async { + const String data = '**foo *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 bar'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is plain text with bold weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.bold, + ); + + // Second span has italic style with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 441 from GFM. + 'infinite levels of nesting are possible within strong emphasis', + (WidgetTester tester) async { + const String data = '**foo *bar **baz**\nbim* bop**'; + 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 bar baz bim bop'); + + // There should be five spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length, 3); + + // First text span is plain text with bold weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.bold, + ); + + // Second span has both italic style with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.bold, + ); + + // Third text span has both italic style with bold weight. + final InlineSpan thirdSpan = textSpan.children![2]; + expectTextSpanStyle( + thirdSpan as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 442 from GFM. + 'infinite levels of nesting are possible within strong emphasis - text and a link', + (WidgetTester tester) async { + const String data = '**foo [*bar*](/url)**'; + 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 bar'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is plain text and bold weight. + final TextSpan firstSpan = textSpan.children![0] as TextSpan; + expect(firstSpan.recognizer, isNull); + expectTextSpanStyle( + firstSpan, + null, + FontWeight.bold, + ); + + // Second span has both italic style with normal weight. + final TextSpan secondSpan = textSpan.children![1] as TextSpan; + expect(secondSpan.recognizer, isNotNull); + expect(secondSpan.recognizer is GestureRecognizer, isTrue); + expectTextSpanStyle( + secondSpan, + FontStyle.italic, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 443 from GFM. + 'there can be no empty emphasis _ delimiter', + (WidgetTester tester) async { + const String data = '__ is not an empty emphasis'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 444 from GFM. + 'there can be no empty strong emphasis _ delimiter', + (WidgetTester tester) async { + const String data = '____ is not an empty strong emphasis'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + }); + + group('Rule 11', () { + testWidgets( + // Example 445 from GFM. + 'an * cannot occur at the beginning or end of * delimited emphasis', + (WidgetTester tester) async { + const String data = 'foo ***'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 446 from GFM. + 'an escaped * can occur inside * delimited emphasis', + (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); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, 'foo *'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is normal text with normal weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.normal, + ); + + // Second span has italic style with normal weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 446 from GFM. + 'an _ can occur inside * delimited emphasis', + (WidgetTester tester) async { + const String data = 'foo *_*'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, 'foo _'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is normal text with normal weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.normal, + ); + + // Second span has italic style with normal weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 448 from GFM. + 'an * cannot occur at the beginning or end of ** delimited strong emphasis', + (WidgetTester tester) async { + const String data = 'foo *****'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 449 from GFM. + 'an escaped * can occur inside ** delimited strong emphasis', + (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); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, 'foo *'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is normal text with normal weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.normal, + ); + + // Second span is normal text with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 450 from GFM. + 'an _ can occur inside ** delimited strong emphasis', + (WidgetTester tester) async { + const String data = 'foo **_**'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, 'foo _'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is normal text with normal weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.normal, + ); + + // Second span is normal text with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 451 from GFM. + 'unmatched emphasis delimiters excess * at beginning', + (WidgetTester tester) async { + const String data = '**foo*'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, '*foo'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is normal text with normal weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.normal, + ); + + // Second span has italic style with normal weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 452 from GFM. + 'unmatched emphasis delimiters excess * at end', + (WidgetTester tester) async { + const String data = '*foo**'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, 'foo*'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, 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 normal text with normal weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 453 from GFM. + 'unmatched strong emphasis delimiters excess * at beginning', + (WidgetTester tester) async { + const String data = '***foo**'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, '*foo'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is normal text with normal weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.normal, + ); + + // Second span is normal text with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 454 from GFM. + 'unmatched strong emphasis delimiters excess * at beginning', + (WidgetTester tester) async { + const String data = '****foo*'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, '***foo'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is normal text with normal weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.normal, + ); + + // Second span has italic style with normal weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 455 from GFM. + 'unmatched strong emphasis delimiters excess * at end', + (WidgetTester tester) async { + const String data = '**foo***'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, 'foo*'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is normal text with bold weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.bold, + ); + + // Second span is plain text with normal weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 456 from GFM. + 'unmatched strong emphasis delimiters excess * at end', + (WidgetTester tester) async { + const String data = '*foo****'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, 'foo***'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, 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 plain text with normal weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + }); + + group('Rule 12', () { + testWidgets( + // Example 457 from GFM. + 'an _ cannot occur at the beginning or end of _ delimited emphasis', + (WidgetTester tester) async { + const String data = 'foo ___'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 458 from GFM. + 'an escaped _ can occur inside _ delimited emphasis', + (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); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, 'foo _'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is normal text with normal weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.normal, + ); + + // Second span has italic style with normal weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 459 from GFM. + 'an * can occur inside _ delimited emphasis', + (WidgetTester tester) async { + const String data = 'foo _*_'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, 'foo *'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is normal text with normal weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.normal, + ); + + // Second span has italic style with normal weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 460 from GFM. + 'an _ cannot occur at the beginning or end of __ delimited strong emphasis', + (WidgetTester tester) async { + const String data = 'foo _____'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, data); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 461 from GFM. + 'an escaped _ can occur inside __ delimited strong emphasis', + (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); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, 'foo _'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is normal text with normal weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.normal, + ); + + // Second span is normal text with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 462 from GFM. + 'an * can occur inside __ delimited strong emphasis', + (WidgetTester tester) async { + const String data = 'foo __*__'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, 'foo *'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is normal text with normal weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.normal, + ); + + // Second span is normal text with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 463 from GFM. + 'unmatched emphasis delimiters excess _ at beginning', + (WidgetTester tester) async { + const String data = '__foo_'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, '_foo'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is normal text with normal weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.normal, + ); + + // Second span has italic style with normal weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 464 from GFM. + 'unmatched emphasis delimiters excess _ at end', + (WidgetTester tester) async { + const String data = '_foo__'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, 'foo_'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, 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 normal text with normal weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 465 from GFM. + 'unmatched strong emphasis delimiters excess _ at beginning', + (WidgetTester tester) async { + const String data = '___foo__'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, '_foo'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is normal text with normal weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.normal, + ); + + // Second span is normal text with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 466 from GFM. + 'unmatched strong emphasis delimiters excess _ at beginning', + (WidgetTester tester) async { + const String data = '____foo_'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, '___foo'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is normal text with normal weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.normal, + ); + + // Second span has italic style with normal weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 467 from GFM. + 'unmatched strong emphasis delimiters excess _ at end', + (WidgetTester tester) async { + const String data = '__foo___'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, 'foo_'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is normal text with bold weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.bold, + ); + + // Second span is plain text with normal weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 468 from GFM. + 'unmatched strong emphasis delimiters excess _ at end', + (WidgetTester tester) async { + const String data = '_foo____'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + // Expect text to be unchanged from original data string. + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, 'foo___'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, 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 plain text with normal weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + }); + + group('Rule 13', () { + testWidgets( + // Example 469 from GFM. + 'nested delimiters must be different - nested * is strong emphasis', + (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'); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 470 from GFM. + 'nested delimiters must be different - nest _ in * emphasis', + (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'); + + expectTextSpanStyle( + richText.text as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 471 from GFM. + 'nested delimiters must be different - nested _ is strong emphasis', + (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'); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 472 from GFM. + 'nested delimiters must be different - nest * in _ emphasis', + (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'); + + expectTextSpanStyle( + richText.text as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 473 from GFM. + 'nested delimiters must be different - nested * strong emphasis', + (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'); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 474 from GFM. + 'nested delimiters must be different - nested _ strong emphasis', + (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'); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 475 from GFM. + 'nested delimiters must be different - long sequence of * delimiters', + (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'); + + expectTextSpanStyle( + richText.text as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + }); + + // Rule 14 doesn't make any difference to flutter_markdown but tests for + // rule 14 are included here for completeness. + group('Rule 14', () { + testWidgets( + // Example 476 from GFM. + 'font style and weight order * delimiter', + (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'); + + expectTextSpanStyle( + richText.text as TextSpan, + FontStyle.italic, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 476 from GFM. + 'font style and weight order _ delimiter', + (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'); + + expectTextSpanStyle( + richText.text as TextSpan, + FontStyle.italic, + FontWeight.bold, + ); + }, + ); + }); + + group('Rule 15', () { + testWidgets( + // Example 478 from GFM. + 'overlapping * and _ emphasis delimiters', + (WidgetTester tester) async { + const String data = '*foo _bar* 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 _bar baz_'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, 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 plain text with normal weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + null, + FontWeight.normal, + ); + }, + ); + + testWidgets( + // Example 479 from GFM. + 'overlapping * and __ emphasis delimiters', + (WidgetTester tester) async { + const String data = '*foo __bar *baz bim__ bam*'; + 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 bar *baz bim bam'); + + // There should be three spans of text. + final TextSpan textSpan = richText.text 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 has italic style with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.bold, + ); + + // Third text span has italic style with normal weight. + final InlineSpan thirdSpan = textSpan.children![2]; + expectTextSpanStyle( + thirdSpan as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + }); + + group('Rule 16', () { + testWidgets( + // Example 480 from GFM. + 'overlapping ** strong emphasis delimiters', + (WidgetTester tester) async { + const String data = '**foo **bar 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 bar baz'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is plain text with normal weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.normal, + ); + + // Second span is plain text with bold weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + null, + FontWeight.bold, + ); + }, + ); + + testWidgets( + // Example 479 from GFM. + 'overlapping * emphasis delimiters', + (WidgetTester tester) async { + const String data = '*foo *bar 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 bar baz'); + + // There should be two spans of text. + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan, isNotNull); + expect(textSpan.children!.length == 2, isTrue); + + // First text span is plain text with normal weight. + final InlineSpan firstSpan = textSpan.children![0]; + expectTextSpanStyle( + firstSpan as TextSpan, + null, + FontWeight.normal, + ); + + // Second span has italic style with normal weight. + final InlineSpan secondSpan = textSpan.children![1]; + expectTextSpanStyle( + secondSpan as TextSpan, + FontStyle.italic, + FontWeight.normal, + ); + }, + ); + }); + + group('Rule 17', () { + // The markdown package does not follow rule 17. Sam Rawlins made the + // following comment on issue #280 on March 7, 2020: + // + // In terms of the spec, we are not following Rule 17 of "Emphasis and + // strong emphasis." Inline code spans, links, images, and HTML tags + // group more tightly than emphasis. Currently the Dart package respects + // the broader rule that any time we can close a tag, we do, attempting + // in the order of most recent openings first. I don't think this is + // terribly hard to correct. + // https://github.com/dart-lang/markdown/issues/280 + // + // Test for rule 17 are not included since markdown package is not + // following the rule. + }); + }, + ); +} diff --git a/packages/flutter_markdown/test/header_test.dart b/packages/flutter_markdown/test/header_test.dart new file mode 100644 index 0000000000..029c13d250 --- /dev/null +++ b/packages/flutter_markdown/test/header_test.dart @@ -0,0 +1,32 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/flutter_markdown.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 widgets = tester.allWidgets; + expectWidgetTypes(widgets, [ + Directionality, + MarkdownBody, + Column, + Wrap, + RichText, + ]); + expectTextStrings(widgets, ['Header']); + }, + ); + }); +} diff --git a/packages/flutter_markdown/test/horizontal_rule_test.dart b/packages/flutter_markdown/test/horizontal_rule_test.dart new file mode 100644 index 0000000000..72c5b75768 --- /dev/null +++ b/packages/flutter_markdown/test/horizontal_rule_test.dart @@ -0,0 +1,99 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/flutter_markdown.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 widgets = tester.allWidgets; + expectWidgetTypes(widgets, [ + Directionality, + 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 widgets = tester.allWidgets; + expectWidgetTypes(widgets, [ + Directionality, + 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 widgets = tester.allWidgets; + expectWidgetTypes(widgets, [ + Directionality, + 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 widgets = tester.allWidgets; + expectWidgetTypes(widgets, [ + Directionality, + MarkdownBody, + Column, + Column, + Wrap, + RichText, + SizedBox, + Column, + Wrap, + RichText, + SizedBox, + Container, + DecoratedBox, + Padding, + LimitedBox, + ConstrainedBox + ]); + }, + ); + }); +} diff --git a/packages/flutter_markdown/test/html_test.dart b/packages/flutter_markdown/test/html_test.dart new file mode 100644 index 0000000000..8dc7b55ecd --- /dev/null +++ b/packages/flutter_markdown/test/html_test.dart @@ -0,0 +1,68 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/flutter_markdown.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 data = [ + 'Line 1\n

HTML content

\nLine 2', + 'Line 1\n<\nLine 2' + ]; + + for (final String line in data) { + await tester.pumpWidget(boilerplate(MarkdownBody(data: line))); + + final Iterable widgets = tester.allWidgets; + expectTextStrings(widgets, ['Line 1', 'Line 2']); + } + }, + ); + + testWidgets( + 'doesn\'t convert & to & when parsing', + (WidgetTester tester) async { + await tester.pumpWidget( + boilerplate( + const Markdown(data: '&'), + ), + ); + expectTextStrings(tester.allWidgets, ['&']); + }, + ); + + testWidgets( + 'doesn\'t convert < to < when parsing', + (WidgetTester tester) async { + await tester.pumpWidget( + boilerplate( + const Markdown(data: '<'), + ), + ); + expectTextStrings(tester.allWidgets, ['<']); + }, + ); + + testWidgets( + 'doesn\'t convert existing HTML entities when parsing', + (WidgetTester tester) async { + await tester.pumpWidget( + boilerplate( + const Markdown(data: '& © < {'), + ), + ); + expectTextStrings( + tester.allWidgets, ['& © < {']); + }, + ); + }); +} diff --git a/packages/flutter_markdown/test/image_test.dart b/packages/flutter_markdown/test/image_test.dart new file mode 100644 index 0000000000..8ac659472b --- /dev/null +++ b/packages/flutter_markdown/test/image_test.dart @@ -0,0 +1,366 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_markdown/flutter_markdown.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 ![alt](https://img) textafter_'; + await tester.pumpWidget( + boilerplate( + const Markdown(data: data), + ), + ); + + final Iterable texts = + tester.widgetList(find.byType(RichText)); + final RichText firstTextWidget = texts.first; + final TextSpan firstTextSpan = firstTextWidget.text as TextSpan; + final Image image = tester.widget(find.byType(Image)); + final NetworkImage networkImage = image.image as NetworkImage; + final RichText secondTextWidget = texts.last; + final TextSpan secondTextSpan = secondTextWidget.text 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 = '![alt](https://img#50x50)'; + 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 = '![alt](/img.png)'; + await tester.pumpWidget( + boilerplate( + const Markdown( + data: data, + imageDirectory: 'https://localhost', + ), + ), + ); + + final Iterable 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', + (WidgetTester tester) async { + const String data = '![alt](http.png)'; + await tester.pumpWidget( + boilerplate( + const Markdown(data: data), + ), + ); + + final Iterable widgets = tester.allWidgets; + final Image image = + widgets.firstWhere((Widget widget) => widget is Image) as Image; + + expect(image.image is FileImage, isTrue); + }, + ); + + testWidgets( + 'should work with resources', + (WidgetTester tester) async { + TestWidgetsFlutterBinding.ensureInitialized(); + const String data = '![alt](resource:assets/logo.png)'; + 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')); + }, + ); + + testWidgets( + 'should work with local image files', + (WidgetTester tester) async { + const String data = '![alt](img.png#50x50)'; + 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); + }, + ); + + testWidgets( + 'should show properly next to text', + (WidgetTester tester) async { + const String data = 'Hello ![alt](img#50x50)'; + await tester.pumpWidget( + boilerplate( + const Markdown(data: data), + ), + ); + + final RichText richText = tester.widget(find.byType(RichText)); + final TextSpan textSpan = richText.text as TextSpan; + expect(textSpan.text, 'Hello '); + expect(textSpan.style, isNotNull); + }, + ); + + testWidgets( + 'should work when nested in a link', + (WidgetTester tester) async { + final List tapTexts = []; + final List tapResults = []; + const String data = '[![alt](https://img#50x50)](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 tapTexts = []; + final List tapResults = []; + const String data = + '[Text before ![alt](https://img#50x50) 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 texts = + tester.widgetList(find.byType(RichText)); + final RichText firstTextWidget = texts.first; + final TextSpan firstSpan = firstTextWidget.text as TextSpan; + (firstSpan.recognizer as TapGestureRecognizer?)!.onTap!(); + + final RichText lastTextWidget = texts.last; + final TextSpan lastSpan = lastTextWidget.text 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 tapTexts = []; + final List tapResults = []; + const String data = + '[Link before](firstHref)[![alt](https://img#50x50)](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 texts = + tester.widgetList(find.byType(RichText)); + final RichText firstTextWidget = texts.first; + final TextSpan firstSpan = firstTextWidget.text as TextSpan; + (firstSpan.recognizer as TapGestureRecognizer?)!.onTap!(); + + final GestureDetector detector = + tester.widget(find.byType(GestureDetector)); + detector.onTap!(); + + final RichText lastTextWidget = texts.last; + final TextSpan lastSpan = lastTextWidget.text 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, ['Link before', 'alt', 'link after']); + expect(tapResults.length, 3); + expect(tapResults, ['firstHref', 'imageHref', 'secondHref']); + }, + ); + + testWidgets( + 'custom image builder', + (WidgetTester tester) async { + const String data = '![alt](https://img.png)'; + 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 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')); + }, + ); + }); +} diff --git a/packages/flutter_markdown/test/image_test_mocks.dart b/packages/flutter_markdown/test/image_test_mocks.dart new file mode 100644 index 0000000000..b7c397db07 --- /dev/null +++ b/packages/flutter_markdown/test/image_test_mocks.dart @@ -0,0 +1,487 @@ +// Copyright 2021 Quiverware LLC. Open source contribution. 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:convert'; +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 _transparentImage = getTestImageData(); + + when(client.getUrl(any)) + .thenAnswer((_) => Future.value(request)); + + when(request.headers).thenReturn(headers); + + when(request.close()) + .thenAnswer((_) => Future.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> imageStream(Invocation invocation) { + final void Function(List)? onData = invocation.positionalArguments[0]; + final void Function()? onDone = invocation.namedArguments[#onDone]; + final void Function(Object, [StackTrace?])? onError = + invocation.namedArguments[#onError]; + final bool? cancelOnError = invocation.namedArguments[#cancelOnError]; + + return Stream>.fromIterable(>[_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; +} + +// This string represents the hexidecial bytes of a transparent image. A +// string is used to make the visual representation of the data compact. A +// List of the same data requires over 60 lines in a source file with +// each element in the array on a single line. +const String _imageBytesAsString = ''' + 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, + '''; + +// Convert the string representing the hexidecimal bytes in the image into +// a list of integers that can be consumed as image data in a stream. +final List _transparentImage = const LineSplitter() + .convert(_imageBytesAsString.replaceAllMapped( + RegExp(r' *0x([A-F0-9]{2}),? *\n? *'), (Match m) => '${m[1]}\n')) + .map((String b) => int.parse(b, radix: 16)) + .toList(); + +List 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. +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 extends Fake implements StreamSubscription { +} + +/// 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 open( + String? method, String? host, int? port, String? path) => + super.noSuchMethod( + Invocation.method(#open, [method, host, port, path]), + returnValue: Future<_FakeHttpClientRequest>.value( + _FakeHttpClientRequest())) as Future; + + @override + Future openUrl(String? method, Uri? url) => + super.noSuchMethod(Invocation.method(#openUrl, [method, url]), + returnValue: Future<_FakeHttpClientRequest>.value( + _FakeHttpClientRequest())) as Future; + + @override + Future get(String? host, int? port, String? path) => + super.noSuchMethod(Invocation.method(#get, [host, port, path]), + returnValue: Future<_FakeHttpClientRequest>.value( + _FakeHttpClientRequest())) as Future; + + @override + Future getUrl(Uri? url) => super.noSuchMethod( + Invocation.method(#getUrl, [url]), + returnValue: + Future<_FakeHttpClientRequest>.value(_FakeHttpClientRequest())) + as Future; + + @override + Future post(String? host, int? port, String? path) => + super.noSuchMethod(Invocation.method(#post, [host, port, path]), + returnValue: Future<_FakeHttpClientRequest>.value( + _FakeHttpClientRequest())) as Future; + + @override + Future postUrl(Uri? url) => super.noSuchMethod( + Invocation.method(#postUrl, [url]), + returnValue: + Future<_FakeHttpClientRequest>.value(_FakeHttpClientRequest())) + as Future; + + @override + Future put(String? host, int? port, String? path) => + super.noSuchMethod(Invocation.method(#put, [host, port, path]), + returnValue: Future<_FakeHttpClientRequest>.value( + _FakeHttpClientRequest())) as Future; + + @override + Future putUrl(Uri? url) => super.noSuchMethod( + Invocation.method(#putUrl, [url]), + returnValue: + Future<_FakeHttpClientRequest>.value(_FakeHttpClientRequest())) + as Future; + + @override + Future delete(String? host, int? port, String? path) => + super.noSuchMethod( + Invocation.method(#delete, [host, port, path]), + returnValue: Future<_FakeHttpClientRequest>.value( + _FakeHttpClientRequest())) as Future; + + @override + Future deleteUrl(Uri? url) => super.noSuchMethod( + Invocation.method(#deleteUrl, [url]), + returnValue: + Future<_FakeHttpClientRequest>.value(_FakeHttpClientRequest())) + as Future; + + @override + Future patch(String? host, int? port, String? path) => + super.noSuchMethod(Invocation.method(#patch, [host, port, path]), + returnValue: Future<_FakeHttpClientRequest>.value( + _FakeHttpClientRequest())) as Future; + + @override + Future patchUrl(Uri? url) => super.noSuchMethod( + Invocation.method(#patchUrl, [url]), + returnValue: + Future<_FakeHttpClientRequest>.value(_FakeHttpClientRequest())) + as Future; + + @override + Future head(String? host, int? port, String? path) => + super.noSuchMethod(Invocation.method(#head, [host, port, path]), + returnValue: Future<_FakeHttpClientRequest>.value( + _FakeHttpClientRequest())) as Future; + + @override + Future headUrl(Uri? url) => super.noSuchMethod( + Invocation.method(#headUrl, [url]), + returnValue: + Future<_FakeHttpClientRequest>.value(_FakeHttpClientRequest())) + as Future; + + @override + void addCredentials( + Uri? url, String? realm, HttpClientCredentials? credentials) => + super.noSuchMethod(Invocation.method( + #addCredentials, [url, realm, credentials])); + + @override + void addProxyCredentials(String? host, int? port, String? realm, + HttpClientCredentials? credentials) => + super.noSuchMethod(Invocation.method( + #addProxyCredentials, [host, port, realm, credentials])); + + @override + void close({bool? force = false}) => super.noSuchMethod( + Invocation.method(#close, [], {#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 get cookies => + super.noSuchMethod(Invocation.getter(#cookies), returnValue: []) + as List; + + @override + Future get done => super.noSuchMethod( + Invocation.getter(#done), + returnValue: + Future<_FakeHttpClientResponse>.value(_FakeHttpClientResponse())) + as Future; + + @override + Future close() => super.noSuchMethod( + Invocation.method(#close, []), + returnValue: + Future<_FakeHttpClientResponse>.value(_FakeHttpClientResponse())) + as Future; +} + +/// 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> listen(void Function(List event)? onData, + {Function? onError, void Function()? onDone, bool? cancelOnError}) => + super.noSuchMethod( + Invocation.method( + #listen, + [onData], + { + #onError: onError, + #onDone: onDone, + #cancelOnError: cancelOnError + }, + ), + returnValue: _FakeStreamSubscription>()); + + @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 get redirects => + super.noSuchMethod(Invocation.getter(#redirects), + returnValue: []) as List; + + @override + HttpHeaders get headers => super.noSuchMethod(Invocation.getter(#headers), + returnValue: _FakeHttpHeaders()) as HttpHeaders; + + @override + List get cookies => + super.noSuchMethod(Invocation.getter(#cookies), returnValue: []) + as List; + + @override + Future redirect( + [String? method, Uri? url, bool? followLoops]) => + super.noSuchMethod( + Invocation.method(#redirect, [method, url, followLoops]), + returnValue: Future<_FakeHttpClientResponse>.value( + _FakeHttpClientResponse())) as Future; + + @override + Future detachSocket() => super.noSuchMethod( + Invocation.method(#detachSocket, []), + returnValue: Future<_FakeSocket>.value(_FakeSocket())) as Future; +} + +/// 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? operator [](String? name) => + super.noSuchMethod(Invocation.method(#[], [name])) + as List?; + + @override + String? value(String? name) => + super.noSuchMethod(Invocation.method(#value, [name])) as String?; + + @override + void add(String? name, Object? value, {bool? preserveHeaderCase = false}) => + super.noSuchMethod(Invocation.method(#add, [name, value], + {#preserveHeaderCase: preserveHeaderCase})); + + @override + void set(String? name, Object? value, {bool? preserveHeaderCase = false}) => + super.noSuchMethod(Invocation.method(#set, [name, value], + {#preserveHeaderCase: preserveHeaderCase})); + + @override + void remove(String? name, Object? value) => + super.noSuchMethod(Invocation.method(#remove, [name, value])); + + @override + void removeAll(String? name) => + super.noSuchMethod(Invocation.method(#removeAll, [name])); + + @override + void forEach(void Function(String, List)? action) => + super.noSuchMethod(Invocation.method(#forEach, [action])); + + @override + void noFolding(String? name) => + super.noSuchMethod(Invocation.method(#noFolding, [name])); +} diff --git a/packages/flutter_markdown/test/line_break_test.dart b/packages/flutter_markdown/test/line_break_test.dart new file mode 100644 index 0000000000..56126059ba --- /dev/null +++ b/packages/flutter_markdown/test/line_break_test.dart @@ -0,0 +1,376 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/flutter_markdown.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 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'); + + // There should be three spans of text. + final TextSpan textSpan = richText.text 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 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'); + + // There should be three spans of text. + final TextSpan textSpan = richText.text 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 richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + + final RichText richText = + richTextFinder.evaluate().first.widget as RichText; + final String text = richText.text.toPlainText(); + expect(text, 'code span'); + + final TextSpan textSpan = richText.text 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 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'code\ span'); + + final TextSpan textSpan = richText.text 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'); + }, + ); + }); +} diff --git a/packages/flutter_markdown/test/link_test.dart b/packages/flutter_markdown/test/link_test.dart new file mode 100644 index 0000000000..318b754dab --- /dev/null +++ b/packages/flutter_markdown/test/link_test.dart @@ -0,0 +1,2624 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/flutter_markdown.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'utils.dart'; + +void main() => defineTests(); + +void defineTests() { + group('Link', () { + testWidgets( + 'should work with nested elements', + (WidgetTester tester) async { + final List linkTapResults = []; + const String data = '[Link `with nested code` Text](href)'; + await tester.pumpWidget( + boilerplate( + Markdown( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults.add(MarkdownLink(text, href, title)), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + + final List gestureRecognizerTypes = []; + 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, 3); + expect(gestureRecognizerTypes.length, 3); + expect(gestureRecognizerTypes, everyElement(TapGestureRecognizer)); + expect(linkTapResults.length, 3); + + // Each of the child text span runs should return the same link info. + for (final MarkdownLink tapResult in linkTapResults) { + expectLinkTap(tapResult, + const MarkdownLink('Link with nested code Text', 'href')); + } + }, + ); + + testWidgets( + 'should work next to other links', + (WidgetTester tester) async { + final List linkTapResults = []; + const String data = + '[First Link](firstHref) and [Second Link](secondHref)'; + await tester.pumpWidget( + boilerplate( + Markdown( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults.add(MarkdownLink(text, href, title)), + ), + ), + ); + + final RichText textWidget = + tester.widgetList(find.byType(RichText)).first as RichText; + final TextSpan span = textWidget.text as TextSpan; + + final List gestureRecognizerTypes = []; + 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, 3); + expect( + gestureRecognizerTypes, + orderedEquals( + [TapGestureRecognizer, Null, TapGestureRecognizer]), + ); + expectLinkTap( + linkTapResults[0], const MarkdownLink('First Link', 'firstHref')); + expectLinkTap( + linkTapResults[1], const MarkdownLink('Second Link', 'secondHref')); + }, + ); + + testWidgets( + // Example 493 from GFM. + 'simple inline link', + (WidgetTester tester) async { + const String data = '[link](/uri "title")'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap( + linkTapResults, const MarkdownLink('link', '/uri', 'title')); + }, + ); + + testWidgets( + 'empty inline link', + (WidgetTester tester) async { + const String data = '[](/uri "title")'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expect(find.byType(RichText), findsNothing); + expect(linkTapResults, isNull); + }, + ); + + testWidgets( + // Example 494 from GFM. + 'simple inline link - title omitted', + (WidgetTester tester) async { + const String data = '[link](/uri)'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap(linkTapResults, const MarkdownLink('link', '/uri')); + }, + ); + + testWidgets( + // Example 495 from GFM. + 'simple inline link - both destination and title omitted', + (WidgetTester tester) async { + const String data = '[link]()'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap(linkTapResults, const MarkdownLink('link', '')); + }, + ); + + testWidgets( + // Example 496 from GFM. + 'simple inline link - both < > enclosed destination and title omitted', + (WidgetTester tester) async { + const String data = '[link](<>)'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap(linkTapResults, const MarkdownLink('link', '')); + }, + ); + + testWidgets( + // Example 497 from GFM. + 'link destination with space and not < > enclosed', + (WidgetTester tester) async { + const String data = '[link](/my url)'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + // Link is treated as ordinary text. + expectInvalidLink('[link](/my url)'); + expect(linkTapResults, isNull); + }, + ); + + testWidgets( + // Example 498 from GFM. + 'link destination with space and < > enclosed', + (WidgetTester tester) async { + const String data = '[link]()'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap(linkTapResults, const MarkdownLink('link', '/my%20url')); + }, + ); + + testWidgets( + // Example 499 from GFM. + 'link destination cannot contain line breaks - not < > enclosed', + (WidgetTester tester) async { + const String data = '[link](foo\nbar)'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + // Link is treated as ordinary text. + expectInvalidLink('[link](foo bar)'); + expect(linkTapResults, isNull); + }, + ); + + testWidgets( + // Example 500 from GFM. + 'link destination cannot contain line breaks - < > enclosed', + (WidgetTester tester) async { + const String data = '[link]()'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + // Link is treated as ordinary text. + expectInvalidLink('[link]()'); + expect(linkTapResults, isNull); + }, + ); + + testWidgets( + // Example 501 from GFM. + 'link destination containing ")" and < > enclosed', + (WidgetTester tester) async { + const String data = '[link]()'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap(linkTapResults, const MarkdownLink('link', '/my)url')); + }, + ); + + testWidgets( + // Example 502 from GFM. + 'pointy brackets that enclose links must be unescaped', + (WidgetTester tester) async { + const String data = r'[link]()'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + // Link is treated as ordinary text. + expectInvalidLink('[link](\n[link](bar)'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + // Link is treated as ordinary text. + expectInvalidLink('[link]( [link](bar)'); + expect(linkTapResults, isNull); + }, + ); + + testWidgets( + // Example 504 from GFM. + 'parentheses inside link destination may be escaped', + (WidgetTester tester) async { + const String data = r'[link](\(foo\))'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap(linkTapResults, const MarkdownLink('link', '(foo)')); + }, + ); + + testWidgets( + // Example 505 from GFM. + 'multiple balanced parentheses are allowed without escaping', + (WidgetTester tester) async { + const String data = '[link](foo(and(bar)))'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap( + linkTapResults, const MarkdownLink('link', 'foo(and(bar))')); + }, + ); + + testWidgets( + // Example 506 from GFM. + 'escaped unbalanced parentheses', + (WidgetTester tester) async { + // Use raw string so backslash isn't treated as an escape character. + const String data = r'[link](foo\(and\(bar\))'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap( + linkTapResults, const MarkdownLink('link', 'foo(and(bar)')); + }, + ); + + testWidgets( + // Example 507 from GFM. + 'pointy brackets enclosed unbalanced parentheses', + (WidgetTester tester) async { + const String data = '[link]()'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap( + linkTapResults, const MarkdownLink('link', 'foo(and(bar)')); + }, + ); + + testWidgets( + // Example 508 from GFM. + 'parentheses and other symbols can be escaped', + (WidgetTester tester) async { + // Use raw string so backslash isn't treated as an escape character. + const String data = r'[link](foo\)\:)'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap(linkTapResults, const MarkdownLink('link', 'foo):')); + }, + ); + + testWidgets( + // Example 509 case 1 from GFM. + 'link destinations with just fragment identifier', + (WidgetTester tester) async { + const String data = '[link](#fragment)'; + + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap(linkTapResults, const MarkdownLink('link', '#fragment')); + }, + ); + + testWidgets( + // Example 509 case 2 from GFM. + 'link destinations with URL and fragment identifier', + (WidgetTester tester) async { + const String data = '[link](http://example.com#fragment)'; + + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap(linkTapResults, + const MarkdownLink('link', 'http://example.com#fragment')); + }, + ); + + testWidgets( + // Example 509 case 3 from GFM. + 'link destinations with URL, fragment identifier, and query', + (WidgetTester tester) async { + const String data = '[link](http://example.com?foo=3#fragment)'; + + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap(linkTapResults, + const MarkdownLink('link', 'http://example.com?foo=3#fragment')); + }, + ); + + testWidgets( + // Example 510 from GFM. + 'link destinations with backslash before non-escapable character', + (WidgetTester tester) async { + const String data = '[link](foo\bar)'; + + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap(linkTapResults, const MarkdownLink('link', 'foo\bar')); + }, + ); + + testWidgets( + // Example 511 from GFM. + 'URL escaping should be left alone inside link destination', + (WidgetTester tester) async { + const String data = '[link](foo%20bä)'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap( + linkTapResults, const MarkdownLink('link', 'foo%20bä')); + }, + ); + + testWidgets( + // Example 512 from GFM. + 'omitting link destination uses title for destination', + (WidgetTester tester) async { + const String data = '[link]("title")'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap( + linkTapResults, const MarkdownLink('link', '%22title%22')); + }, + ); + + testWidgets( + // Example 513a from GFM. + 'link title in double quotes', + (WidgetTester tester) async { + const String data = '[link](/url "title")'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap( + linkTapResults, const MarkdownLink('link', '/url', 'title')); + }, + ); + + testWidgets( + // Example 513b from GFM. + 'link title in single quotes', + (WidgetTester tester) async { + const String data = '[link](/url \'title\')'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap( + linkTapResults, const MarkdownLink('link', '/url', 'title')); + }, + ); + + testWidgets( + // Example 513c from GFM. + 'link title in parentheses', + (WidgetTester tester) async { + const String data = '[link](/url (title))'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap( + linkTapResults, const MarkdownLink('link', '/url', 'title')); + }, + ); + + testWidgets( + // Example 514 from GFM. + 'backslash escapes, entity, and numeric character references are allowed in title', + (WidgetTester tester) async { + const String data = r'[link](/url "title \""")'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap(linkTapResults, + const MarkdownLink('link', '/url', 'title %22"')); + }, + ); + + testWidgets( + // Example 515 from GFM. + 'link title must be separated with whitespace and not Unicode whitespace', + (WidgetTester tester) async { + const String data = '[link](/url\u{C2A0}"title")'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap(linkTapResults, + const MarkdownLink('link', '/url\u{C2A0}%22title%22')); + }, + ); + + testWidgets( + // Example 516 from GFM. + 'nested balanced quotes are not allowed without escaping', + (WidgetTester tester) async { + const String data = '[link](/url "title "and" title")'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + // Link is treated as ordinary text. + expectInvalidLink('[link](/url "title "and" title")'); + expect(linkTapResults, isNull); + }, + ); + + testWidgets( + // Example 517 from GFM. + 'nested balanced quotes using different quote type', + (WidgetTester tester) async { + const String data = '[link](/url \'title "and" title\')'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap(linkTapResults, + const MarkdownLink('link', '/url', 'title %22and%22 title')); + }, + ); + + testWidgets( + // Example 518 from GFM. + 'whitespace is allowed around the destination and title', + (WidgetTester tester) async { + const String data = '[link]( /url "title")'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link'); + expectLinkTap( + linkTapResults, const MarkdownLink('link', '/url', 'title')); + }, + ); + + testWidgets( + // Example 519 from GFM. + 'whitespace is not allowed between link text and following parentheses', + (WidgetTester tester) async { + const String data = '[link] (/url)'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + // Link is treated as ordinary text. + expectInvalidLink('[link] (/url)'); + expect(linkTapResults, isNull); + }, + ); + + testWidgets( + // Example 520 from GFM. + 'link text may contain balanced brackets', + (WidgetTester tester) async { + const String data = '[link [foo [bar]]](/uri)'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link [foo [bar]]'); + expectLinkTap( + linkTapResults, const MarkdownLink('link [foo [bar]]', '/uri')); + }, + ); + + testWidgets( + // Example 521 from GFM. + 'link text may not contain unbalanced brackets', + (WidgetTester tester) async { + const String data = '[link] bar](/uri)'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + // Link is treated as ordinary text. + expectInvalidLink('[link] bar](/uri)'); + expect(linkTapResults, isNull); + }, + ); + + testWidgets( + // Example 522 from GFM. + 'link text may not contain unbalanced brackets - unintended link text', + (WidgetTester tester) async { + const String data = '[link [bar](/uri)'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 2); + expect(span.children![0], isA()); + expect(span.children![0].toPlainText(), '[link '); + + expectLinkTextSpan(span.children![1] as TextSpan, 'bar'); + expectLinkTap(linkTapResults, const MarkdownLink('bar', '/uri')); + }, + ); + + testWidgets( + // Example 523 from GFM. + 'link text with escaped open square bracket', + (WidgetTester tester) async { + const String data = r'[link \[bar](/uri)'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link [bar'); + expectLinkTap(linkTapResults, const MarkdownLink('link [bar', '/uri')); + }, + ); + + testWidgets( + // Example 524 from GFM. + 'link text with inline emphasis and code', + (WidgetTester tester) async { + const String data = '[link *foo **bar** `#`*](/uri)'; + final List linkTapResults = []; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults.add(MarkdownLink(text, href, title)), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 5); + expectTextSpanStyle( + span.children![0] as TextSpan, null, FontWeight.normal); + expectTextSpanStyle( + span.children![1] as TextSpan, FontStyle.italic, FontWeight.normal); + expectTextSpanStyle( + span.children![2] as TextSpan, FontStyle.italic, FontWeight.bold); + expectTextSpanStyle( + span.children![3] as TextSpan, FontStyle.italic, FontWeight.normal); + expect((span.children![4] as TextSpan).style!.fontFamily, 'monospace'); + + final List gestureRecognizerTypes = []; + span.visitChildren((InlineSpan inlineSpan) { + if (inlineSpan is TextSpan) { + final TapGestureRecognizer? recognizer = + inlineSpan.recognizer as TapGestureRecognizer?; + gestureRecognizerTypes.add(recognizer.runtimeType); + recognizer!.onTap!(); + } + return true; + }); + + expect(gestureRecognizerTypes.length, 5); + expect(gestureRecognizerTypes, everyElement(TapGestureRecognizer)); + expect(linkTapResults.length, 5); + + // Each of the child text span runs should return the same link info. + for (final MarkdownLink tapResult in linkTapResults) { + expectLinkTap( + tapResult, const MarkdownLink('link foo bar #', '/uri')); + } + }, + ); + + testWidgets( + // Example 525 from GFM. + 'inline image link text', + (WidgetTester tester) async { + const String data = '[![moon](moon.jpg)](/uri)'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final Finder gestureFinder = find.byType(GestureDetector); + expect(gestureFinder, findsOneWidget); + final GestureDetector gestureWidget = + gestureFinder.evaluate().first.widget as GestureDetector; + expect(gestureWidget.child, isA()); + expect(gestureWidget.onTap, isNotNull); + + gestureWidget.onTap!(); + expectLinkTap(linkTapResults, const MarkdownLink('moon', '/uri')); + }, + ); + + testWidgets( + // Example 526 from GFM. + 'links cannot be nested - outter link ignored', + (WidgetTester tester) async { + const String data = '[foo [bar](/uri)](/uri)'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 3); + expect(span.children![0], isA()); + expect(span.children![0].toPlainText(), '[foo '); + + expectLinkTextSpan(span.children![1] as TextSpan, 'bar'); + expectLinkTap(linkTapResults, const MarkdownLink('bar', '/uri')); + + expect(span.children![2], isA()); + expect(span.children![2].toPlainText(), '](/uri)'); + }, + ); + + testWidgets( + // Example 527 from GFM. + 'links cannot be nested - outter link ignored with emphasis', + (WidgetTester tester) async { + const String data = '[foo *[bar [baz](/uri)](/uri)*](/uri)'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 5); + expect(span.children, everyElement(isA())); + + expect(span.children![0].toPlainText(), '[foo '); + expectTextSpanStyle( + span.children![0] as TextSpan, null, FontWeight.normal); + + expect(span.children![1].toPlainText(), '[bar '); + expectTextSpanStyle( + span.children![1] as TextSpan, FontStyle.italic, FontWeight.normal); + + expect(span.children![2].toPlainText(), 'baz'); + expectTextSpanStyle( + span.children![2] as TextSpan, FontStyle.italic, FontWeight.normal); + + expect(span.children![3].toPlainText(), '](/uri)'); + expectTextSpanStyle( + span.children![3] as TextSpan, FontStyle.italic, FontWeight.normal); + + expect(span.children![4].toPlainText(), '](/uri)'); + expectTextSpanStyle( + span.children![4] as TextSpan, null, FontWeight.normal); + + expectLinkTextSpan(span.children![2] as TextSpan, 'baz'); + expectLinkTap(linkTapResults, const MarkdownLink('baz', '/uri')); + }, + ); + + testWidgets( + // Example 528 from GFM. + 'links cannot be nested in image linksinline image link text', + (WidgetTester tester) async { + const String data = '![[[foo](uri1)](uri2)](uri3)'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final Finder gestureFinder = find.byType(GestureDetector); + expect(gestureFinder, findsNothing); + + final Finder imageFinder = find.byType(Image); + expect(imageFinder, findsOneWidget); + final Image image = imageFinder.evaluate().first.widget as Image; + final FileImage fi = image.image as FileImage; + expect(fi.file.path, equals('uri3')); + expect(linkTapResults, isNull); + }, + ); + + testWidgets( + // Example 529 from GFM. + 'link text grouping has precedence over emphasis grouping example 1', + (WidgetTester tester) async { + const String data = r'*[foo*](/uri)'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 2); + expect(span.children![0], isA()); + expect(span.children![0].toPlainText(), '*'); + expectTextSpanStyle( + span.children![0] as TextSpan, null, FontWeight.normal); + + expectLinkTextSpan(span.children![1] as TextSpan, 'foo*'); + expectLinkTap(linkTapResults, const MarkdownLink('foo*', '/uri')); + }, + ); + + testWidgets( + // Example 530 from GFM. + 'link text grouping has precedence over emphasis grouping example 2', + (WidgetTester tester) async { + const String data = '[foo *bar](baz*)'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('foo *bar'); + expectLinkTap(linkTapResults, const MarkdownLink('foo *bar', 'baz*')); + }, + ); + + testWidgets( + // Example 531 from GFM. + 'brackets that aren\'t part of links do not take precedence', + (WidgetTester tester) async { + const String data = '*foo [bar* baz]'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 2); + expect(span.children, everyElement(isA())); + + expect(span.children![0].toPlainText(), 'foo [bar'); + expectTextSpanStyle( + span.children![0] as TextSpan, FontStyle.italic, FontWeight.normal); + + expect(span.children![1].toPlainText(), ' baz]'); + expectTextSpanStyle( + span.children![1] as TextSpan, null, FontWeight.normal); + }, + ); + + testWidgets( + // Example 532 from GFM. + 'HTML tag takes precedence over link grouping', + (WidgetTester tester) async { + const String data = '[foo '; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + // Link is treated as ordinary text. + expectInvalidLink('[foo '); + expect(linkTapResults, isNull); + }, + ); + + testWidgets( + // Example 533 from GFM. + 'code span takes precedence over link grouping', + (WidgetTester tester) async { + const String data = '[foo`](/uri)`'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final Finder gestureFinder = find.byType(GestureDetector); + expect(gestureFinder, findsNothing); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 2); + expect(span.children, everyElement(isA())); + + expect(span.children![0], isA()); + expect(span.children![0].toPlainText(), '[foo'); + expectTextSpanStyle( + span.children![0] as TextSpan, null, FontWeight.normal); + + expectTextSpanStyle( + span.children![0] as TextSpan, null, FontWeight.normal); + expect((span.children![1] as TextSpan).style!.fontFamily, 'monospace'); + expect(linkTapResults, isNull); + }, + ); + + testWidgets( + // Example 534 from GFM. + 'autolinks take precedence over link grouping', + (WidgetTester tester) async { + const String data = '[foo'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 2); + expect(span.children, everyElement(isA())); + + expect(span.children![0].toPlainText(), '[foo'); + expectTextSpanStyle( + span.children![0] as TextSpan, null, FontWeight.normal); + + expectLinkTextSpan( + span.children![1] as TextSpan, 'http://example.com/?search=](uri)'); + expectLinkTap( + linkTapResults, + const MarkdownLink('http://example.com/?search=](uri)', + 'http://example.com/?search=%5D(uri)')); + }, + ); + }); + group('Reference Link', () { + testWidgets( + // Example 535 from GFM. + 'simple reference link', + (WidgetTester tester) async { + const String data = '[foo][bar]\n\n[bar]: /url "title"'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('foo'); + expectLinkTap( + linkTapResults, const MarkdownLink('foo', '/url', 'title')); + }, + ); + + testWidgets( + // Example 536 from GFM. + 'reference link with balanced brackets in link text', + (WidgetTester tester) async { + const String data = '[link [foo [bar]]][ref]\n\n[ref]: /uri'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link [foo [bar]]'); + expectLinkTap( + linkTapResults, const MarkdownLink('link [foo [bar]]', '/uri')); + }, + ); + + testWidgets( + // Example 537 from GFM. + 'reference link with unbalanced but escaped bracket in link text', + (WidgetTester tester) async { + const String data = '[link \\[bar][ref]\n\n[ref]: /uri'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('link [bar'); + expectLinkTap(linkTapResults, const MarkdownLink('link [bar', '/uri')); + }, + ); + + testWidgets( + // Example 538 from GFM. + 'reference link with inline emphasis and code span in link text', + (WidgetTester tester) async { + const String data = '[link *foo **bar** `#`*][ref]\n\n[ref]: /uri'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 5); + expect(span.children, everyElement(isA())); + + expect(span.children![0].toPlainText(), 'link '); + expectTextSpanStyle( + span.children![0] as TextSpan, null, FontWeight.normal); + + expect(span.children![1].toPlainText(), 'foo '); + expectTextSpanStyle( + span.children![1] as TextSpan, FontStyle.italic, FontWeight.normal); + + expect(span.children![2].toPlainText(), 'bar'); + expectTextSpanStyle( + span.children![2] as TextSpan, FontStyle.italic, FontWeight.bold); + + expect(span.children![3].toPlainText(), ' '); + expectTextSpanStyle( + span.children![3] as TextSpan, FontStyle.italic, FontWeight.normal); + + expect(span.children![4].toPlainText(), '#'); + expectTextSpanStyle( + span.children![4] as TextSpan, null, FontWeight.normal); + expect((span.children![4] as TextSpan).style!.fontFamily, 'monospace'); + + for (final InlineSpan element in span.children!) { + final TextSpan textSpan = element as TextSpan; + expect(textSpan.recognizer, isNotNull); + expect(textSpan.recognizer, isA()); + final TapGestureRecognizer? tapRecognizer = + textSpan.recognizer as TapGestureRecognizer?; + expect(tapRecognizer?.onTap, isNotNull); + + tapRecognizer!.onTap!(); + expectLinkTap( + linkTapResults, const MarkdownLink('link foo bar #', '/uri')); + + // Clear link tap results. + linkTapResults = null; + } + }, + ); + + testWidgets( + // Example 539 from GFM. + 'referenence link with inline image link text', + (WidgetTester tester) async { + const String data = '[![moon](moon.jpg)][ref]\n\n[ref]: /uri'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final Finder gestureFinder = find.byType(GestureDetector); + expect(gestureFinder, findsOneWidget); + final GestureDetector gestureWidget = + gestureFinder.evaluate().first.widget as GestureDetector; + expect(gestureWidget.child, isA()); + expect(gestureWidget.onTap, isNotNull); + + gestureWidget.onTap!(); + expectLinkTap(linkTapResults, const MarkdownLink('moon', '/uri')); + }, + ); + + testWidgets( + // Example 540 from GFM. + 'reference links cannot have nested links', + (WidgetTester tester) async { + const String data = '[foo [bar](/uri)][ref]\n\n[ref]: /uri'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 4); + + expect(span.children![0], isA()); + expect(span.children![0].toPlainText(), '[foo '); + + expectLinkTextSpan(span.children![1] as TextSpan, 'bar'); + expectLinkTap(linkTapResults, const MarkdownLink('bar', '/uri')); + + expect(span.children![2], isA()); + expect(span.children![2].toPlainText(), ']'); + + expectLinkTextSpan(span.children![3] as TextSpan, 'ref'); + expectLinkTap(linkTapResults, const MarkdownLink('ref', '/uri')); + }, + ); + + testWidgets( + // Example 541 from GFM. + 'reference links cannot have nested reference links', + (WidgetTester tester) async { + const String data = '[foo *bar [baz][ref]*][ref]\n\n[ref]: /uri'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 5); + + expect(span.children![0], isA()); + expect(span.children![0].toPlainText(), '[foo '); + expectTextSpanStyle( + span.children![0] as TextSpan, null, FontWeight.normal); + + expect(span.children![1], isA()); + expect(span.children![1].toPlainText(), 'bar '); + expectTextSpanStyle( + span.children![1] as TextSpan, FontStyle.italic, FontWeight.normal); + + expectLinkTextSpan(span.children![2] as TextSpan, 'baz'); + expectTextSpanStyle( + span.children![2] as TextSpan, FontStyle.italic, FontWeight.normal); + expectLinkTap(linkTapResults, const MarkdownLink('baz', '/uri')); + + expect(span.children![3], isA()); + expect(span.children![3].toPlainText(), ']'); + expectTextSpanStyle( + span.children![3] as TextSpan, null, FontWeight.normal); + + expectLinkTextSpan(span.children![4] as TextSpan, 'ref'); + expectTextSpanStyle( + span.children![4] as TextSpan, null, FontWeight.normal); + expectLinkTap(linkTapResults, const MarkdownLink('ref', '/uri')); + }, + ); + + testWidgets( + // Example 542 from GFM. + 'reference link text grouping has precedence over emphasis grouping example 1', + (WidgetTester tester) async { + const String data = '*[foo*][ref]\n\n[ref]: /uri'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 2); + expect(span.children![0], isA()); + expect(span.children![0].toPlainText(), '*'); + expectTextSpanStyle( + span.children![0] as TextSpan, null, FontWeight.normal); + + expectLinkTextSpan(span.children![1] as TextSpan, 'foo*'); + expectLinkTap(linkTapResults, const MarkdownLink('foo*', '/uri')); + }, + ); + + testWidgets( + // Example 543 from GFM. + 'reference link text grouping has precedence over emphasis grouping example 2', + (WidgetTester tester) async { + const String data = '[foo *bar][ref]*\n\n[ref]: /uri'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 2); + + expectLinkTextSpan(span.children![0] as TextSpan, 'foo *bar'); + expectTextSpanStyle( + span.children![0] as TextSpan, null, FontWeight.normal); + expectLinkTap(linkTapResults, const MarkdownLink('foo *bar', '/uri')); + + expect(span.children![1], isA()); + expect(span.children![1].toPlainText(), '*'); + expectTextSpanStyle( + span.children![0] as TextSpan, null, FontWeight.normal); + }, + ); + + testWidgets( + // Example 544 from GFM. + 'HTML tag takes precedence over reference link grouping', + (WidgetTester tester) async { + const String data = '[foo \n\n[ref]: /uri'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + // Link is treated as ordinary text. + expectInvalidLink('[foo '); + expect(linkTapResults, isNull); + }, + ); + + testWidgets( + // Example 545 from GFM. + 'code span takes precedence over reference link grouping', + (WidgetTester tester) async { + const String data = '[foo`][ref]`\n\n[ref]: /uri'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final Finder gestureFinder = find.byType(GestureDetector); + expect(gestureFinder, findsNothing); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 2); + expect(span.children, everyElement(isA())); + + expect(span.children![0], isA()); + expect(span.children![0].toPlainText(), '[foo'); + expectTextSpanStyle( + span.children![0] as TextSpan, null, FontWeight.normal); + + expect(span.children![1].toPlainText(), '][ref]'); + expect((span.children![1] as TextSpan).style!.fontFamily, 'monospace'); + expect(linkTapResults, isNull); + }, + ); + + testWidgets( + // Example 534 from GFM. + 'autolinks take precedence over reference link grouping', + (WidgetTester tester) async { + const String data = + '[foo\n\n[ref]: /uri'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 2); + expect(span.children, everyElement(isA())); + + expect(span.children![0].toPlainText(), '[foo'); + expectTextSpanStyle( + span.children![0] as TextSpan, null, FontWeight.normal); + + expectLinkTextSpan( + span.children![1] as TextSpan, 'http://example.com/?search=][ref]'); + expectLinkTap( + linkTapResults, + const MarkdownLink('http://example.com/?search=][ref]', + 'http://example.com/?search=%5D%5Bref%5D')); + }, + ); + + testWidgets( + // Example 547 from GFM. + 'reference link matching is case-insensitive', + (WidgetTester tester) async { + const String data = '[foo][BaR]\n\n[bar]: /url "title"'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('foo'); + expectLinkTap( + linkTapResults, const MarkdownLink('foo', '/url', 'title')); + }, + ); + + testWidgets( + // Example 548 from GFM. + 'reference link support Unicode case fold - GFM', + (WidgetTester tester) async { + const String data = '[ẞ]\n\n[SS]: /url'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('ẞ'); + expectLinkTap(linkTapResults, const MarkdownLink('ẞ', '/url', 'title')); + }, + // TODO(mjordan56): Remove skip once the issue #333 in the markdown package + // is fixed and released. https://github.com/dart-lang/markdown/issues/333 + skip: true, + ); + + testWidgets( + // Example 536 from CommonMark. NOTE: The CommonMark and GFM specifications + // use different examples for Unicode case folding. Both are being added + // to the test suite since each example produces different cases to test. + 'reference link support Unicode case fold - CommonMark', + (WidgetTester tester) async { + const String data = + '[Толпой][Толпой] is a Russian word.\n\n[ТОЛПОЙ]: /url'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 2); + + expectLinkTextSpan(span.children![0] as TextSpan, 'Толпой'); + expectLinkTap(linkTapResults, const MarkdownLink('Толпой', '/url')); + + expect(span.children![1], isA()); + expect(span.children![1].toPlainText(), ' is a Russian word.'); + expectTextSpanStyle( + span.children![0] as TextSpan, null, FontWeight.normal); + }, + ); + + testWidgets( + // Example 549 from GFM. + 'reference link with internal whitespace', + (WidgetTester tester) async { + const String data = '[Foo\n bar]: /url\n\n[Baz][Foo bar]'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('Baz'); + expectLinkTap(linkTapResults, const MarkdownLink('Baz', '/url')); + }, + ); + + testWidgets( + // Example 550 from GFM. + 'reference link no whitespace between link text and link label', + (WidgetTester tester) async { + const String data = '[foo] [bar]\n\n[bar]: /url "title"'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 2); + + expect(span.children![0], isA()); + expect(span.children![0].toPlainText(), '[foo] '); + expectTextSpanStyle( + span.children![0] as TextSpan, null, FontWeight.normal); + + expectLinkTextSpan(span.children![1] as TextSpan, 'bar'); + expectLinkTap( + linkTapResults, const MarkdownLink('bar', '/url', 'title')); + }, + ); + + testWidgets( + // Example 551 from GFM. + 'reference link no line break between link text and link label', + (WidgetTester tester) async { + const String data = '[foo]\n[bar]\n\n[bar]: /url "title"'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 2); + + expect(span.children![0], isA()); + expect(span.children![0].toPlainText(), '[foo] '); + expectTextSpanStyle( + span.children![0] as TextSpan, null, FontWeight.normal); + + expectLinkTextSpan(span.children![1] as TextSpan, 'bar'); + expectLinkTap( + linkTapResults, const MarkdownLink('bar', '/url', 'title')); + }, + ); + + testWidgets( + // Example 552 from GFM. + 'multiple matching reference link definitions use first definition', + (WidgetTester tester) async { + const String data = '[foo]: /url1\n\n[foo]: /url2\n\n[bar][foo]'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('bar'); + expectLinkTap(linkTapResults, const MarkdownLink('bar', '/url1')); + }, + ); + + testWidgets( + // Example 553 from GFM. + 'reference link matching is performed on normalized strings', + (WidgetTester tester) async { + const String data = '[bar][foo\\!]\n\n[foo!]: /url'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + // Link is treated as ordinary text. + expectInvalidLink('[bar][foo!]'); + expect(linkTapResults, isNull); + }, + ); + + testWidgets( + // Example 554 from GFM. + 'reference link labels cannot contain brackets - case 1', + (WidgetTester tester) async { + const String data = '[foo][ref[]\n\n[ref[]: /uri'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final List textWidgets = + tester.widgetList(find.byType(RichText)).toList().cast(); + expect(textWidgets.length, 2); + + expect(textWidgets[0].text, isA()); + expect(textWidgets[0].text.toPlainText(), '[foo][ref[]'); + expectTextSpanStyle( + textWidgets[0].text as TextSpan, null, FontWeight.normal); + + expect(textWidgets[1].text, isA()); + expect(textWidgets[1].text.toPlainText(), '[ref[]: /uri'); + expectTextSpanStyle( + textWidgets[1].text as TextSpan, null, FontWeight.normal); + + expect(linkTapResults, isNull); + }, + // TODO(mjordan56): Remove skip once the issue #335 in the markdown package + // is fixed and released. https://github.com/dart-lang/markdown/issues/335 + skip: true, + ); + + testWidgets( + // Example 555 from GFM. + 'reference link labels cannot contain brackets - case 2', + (WidgetTester tester) async { + const String data = '[foo][ref[bar]]\n\n[ref[bar]]: /uri'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final List textWidgets = + tester.widgetList(find.byType(RichText)).toList().cast(); + expect(textWidgets.length, 2); + + expect(textWidgets[0].text, isNotNull); + expect(textWidgets[0].text, isA()); + expect(textWidgets[0].text.toPlainText(), '[foo][ref[bar]]'); + expectTextSpanStyle( + textWidgets[0].text as TextSpan, null, FontWeight.normal); + + expect(textWidgets[1].text, isNotNull); + expect(textWidgets[1].text, isA()); + expect(textWidgets[1].text.toPlainText(), '[ref[bar]]: /uri'); + expectTextSpanStyle( + textWidgets[1].text as TextSpan, null, FontWeight.normal); + + expect(linkTapResults, isNull); + }, + ); + + testWidgets( + // Example 556 from GFM. + 'reference link labels cannot contain brackets - case 3', + (WidgetTester tester) async { + const String data = '[[[foo]]]\n\n[[[foo]]]: /url'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final List textWidgets = + tester.widgetList(find.byType(RichText)).toList().cast(); + expect(textWidgets.length, 2); + + expect(textWidgets[0].text, isNotNull); + expect(textWidgets[0].text, isA()); + expect(textWidgets[0].text.toPlainText(), '[[[foo]]]'); + expectTextSpanStyle( + textWidgets[0].text as TextSpan, null, FontWeight.normal); + + expect(textWidgets[1].text, isNotNull); + expect(textWidgets[1].text, isA()); + expect(textWidgets[1].text.toPlainText(), '[[[foo]]]: /url'); + expectTextSpanStyle( + textWidgets[1].text as TextSpan, null, FontWeight.normal); + + expect(linkTapResults, isNull); + }, + ); + + testWidgets( + // Example 557 from GFM. + 'reference link labels can have escaped brackets', + (WidgetTester tester) async { + const String data = '[foo][ref\\[]\n\n[ref\\[]: /uri'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('foo'); + expectLinkTap(linkTapResults, const MarkdownLink('foo', '/uri')); + }, + ); + + testWidgets( + // Example 558 from GFM. + 'reference link labels can have escaped characters', + (WidgetTester tester) async { + const String data = '[bar\\]: /uri\n\n[bar\\]'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink(r'bar\'); + expectLinkTap(linkTapResults, const MarkdownLink(r'bar\', '/uri')); + }, + // TODO(mjordan56): Remove skip once the issue #336 in the markdown package + // is fixed and released. https://github.com/dart-lang/markdown/issues/336 + skip: true, + ); + + testWidgets( + // Example 559 from GFM. + 'reference link labels must contain at least on non-whitespace character - case 1', + (WidgetTester tester) async { + const String data = '[]\n\n[]: /uri'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final List textWidgets = + tester.widgetList(find.byType(RichText)).toList().cast(); + expect(textWidgets.length, 2); + + expect(textWidgets[0].text, isNotNull); + expect(textWidgets[0].text, isA()); + expect(textWidgets[0].text.toPlainText(), '[]'); + expectTextSpanStyle( + textWidgets[0].text as TextSpan, null, FontWeight.normal); + + expect(textWidgets[1].text, isNotNull); + expect(textWidgets[1].text, isA()); + expect(textWidgets[1].text.toPlainText(), '[]: /uri'); + expectTextSpanStyle( + textWidgets[1].text as TextSpan, null, FontWeight.normal); + + expect(linkTapResults, isNull); + }, + ); + + testWidgets( + // Example 560 from GFM. + 'reference link labels must contain at least on non-whitespace character - case 2', + (WidgetTester tester) async { + const String data = '[\n ]\n\n[\n ]: /uri'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final List textWidgets = + tester.widgetList(find.byType(RichText)).toList().cast(); + expect(textWidgets.length, 2); + + expect(textWidgets[0].text, isNotNull); + expect(textWidgets[0].text, isA()); + expect(textWidgets[0].text.toPlainText(), '[ ]'); + expectTextSpanStyle( + textWidgets[0].text as TextSpan, null, FontWeight.normal); + + expect(textWidgets[1].text, isNotNull); + expect(textWidgets[1].text, isA()); + expect(textWidgets[1].text.toPlainText(), '[ ]: /uri'); + expectTextSpanStyle( + textWidgets[1].text as TextSpan, null, FontWeight.normal); + + expect(linkTapResults, isNull); + }, + ); + + testWidgets( + // Example 561 from GFM. + 'collapsed reference link', + (WidgetTester tester) async { + const String data = '[foo][]\n\n[foo]: /url "title"'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('foo'); + expectLinkTap( + linkTapResults, const MarkdownLink('foo', '/url', 'title')); + }, + ); + + testWidgets( + // Example 562 from GFM. + 'collapsed reference link with inline emphasis in link text', + (WidgetTester tester) async { + const String data = '[*foo* bar][]\n\n[*foo* bar]: /url "title"'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 2); + expect(span.children, everyElement(isA())); + + expect(span.children![0].toPlainText(), 'foo'); + expectTextSpanStyle( + span.children![0] as TextSpan, FontStyle.italic, FontWeight.normal); + + expect(span.children![1].toPlainText(), ' bar'); + expectTextSpanStyle( + span.children![1] as TextSpan, null, FontWeight.normal); + + for (final InlineSpan element in span.children!) { + final TextSpan textSpan = element as TextSpan; + expect(textSpan.recognizer, isNotNull); + expect(textSpan.recognizer, isA()); + final TapGestureRecognizer? tapRecognizer = + textSpan.recognizer as TapGestureRecognizer?; + expect(tapRecognizer?.onTap, isNotNull); + + tapRecognizer!.onTap!(); + expectLinkTap( + linkTapResults, const MarkdownLink('foo bar', '/url', 'title')); + + // Clear link tap results. + linkTapResults = null; + } + }, + ); + + testWidgets( + // Example 563 from GFM. + 'collapsed reference links are case-insensitive', + (WidgetTester tester) async { + const String data = '[Foo][]\n\n[foo]: /url "title"'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('Foo'); + expectLinkTap( + linkTapResults, const MarkdownLink('Foo', '/url', 'title')); + }, + ); + + testWidgets( + // Example 564 from GFM. + 'collapsed reference link no whitespace between link text and link label', + (WidgetTester tester) async { + const String data = '[foo] \n\n[]\n\n[foo]: /url "title"'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final List textWidgets = + tester.widgetList(find.byType(RichText)).toList().cast(); + expect(textWidgets.length, 2); + + expect(textWidgets[0].text, isNotNull); + expect(textWidgets[0].text, isA()); + expect(textWidgets[0].text.toPlainText(), 'foo'); + + expect(textWidgets[0].text, isNotNull); + expect(textWidgets[0].text, isA()); + expectLinkTextSpan(textWidgets[0].text as TextSpan, 'foo'); + expectLinkTap( + linkTapResults, const MarkdownLink('foo', '/url', 'title')); + + expect(textWidgets[1].text, isNotNull); + expect(textWidgets[1].text, isA()); + expect(textWidgets[1].text.toPlainText(), '[]'); + expectTextSpanStyle( + textWidgets[1].text as TextSpan, null, FontWeight.normal); + }, + ); + + testWidgets( + // Example 565 from GFM. + 'shortcut reference link', + (WidgetTester tester) async { + const String data = '[foo]\n\n[foo]: /url "title"'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('foo'); + expectLinkTap( + linkTapResults, const MarkdownLink('foo', '/url', 'title')); + }, + ); + + testWidgets( + // Example 566 from GFM. + 'shortcut reference link with inline emphasis in link text', + (WidgetTester tester) async { + const String data = '[*foo* bar]\n\n[*foo* bar]: /url "title"'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 2); + expect(span.children, everyElement(isA())); + + expect(span.children![0].toPlainText(), 'foo'); + expectTextSpanStyle( + span.children![0] as TextSpan, FontStyle.italic, FontWeight.normal); + + expect(span.children![1].toPlainText(), ' bar'); + expectTextSpanStyle( + span.children![1] as TextSpan, null, FontWeight.normal); + + for (final InlineSpan element in span.children!) { + final TextSpan textSpan = element as TextSpan; + expect(textSpan.recognizer, isNotNull); + expect(textSpan.recognizer, isA()); + final TapGestureRecognizer? tapRecognizer = + textSpan.recognizer as TapGestureRecognizer?; + expect(tapRecognizer?.onTap, isNotNull); + + tapRecognizer!.onTap!(); + expectLinkTap( + linkTapResults, const MarkdownLink('foo bar', '/url', 'title')); + + // Clear link tap results. + linkTapResults = null; + } + }, + ); + + testWidgets( + // Example 567 from GFM. + 'shortcut reference link with inline emphasis nested in link text', + (WidgetTester tester) async { + const String data = '[*foo* bar]\n\n[*foo* bar]: /url "title"'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 2); + expect(span.children, everyElement(isA())); + + expect(span.children![0].toPlainText(), 'foo'); + expectTextSpanStyle( + span.children![0] as TextSpan, FontStyle.italic, FontWeight.normal); + + expect(span.children![1].toPlainText(), ' bar'); + expectTextSpanStyle( + span.children![1] as TextSpan, null, FontWeight.normal); + + for (final InlineSpan element in span.children!) { + final TextSpan textSpan = element as TextSpan; + expect(textSpan.recognizer, isNotNull); + expect(textSpan.recognizer, isA()); + final TapGestureRecognizer? tapRecognizer = + textSpan.recognizer as TapGestureRecognizer?; + expect(tapRecognizer?.onTap, isNotNull); + + tapRecognizer!.onTap!(); + expectLinkTap( + linkTapResults, const MarkdownLink('foo bar', '/url', 'title')); + + // Clear link tap results. + linkTapResults = null; + } + }, + ); + + testWidgets( + // Example 568 from GFM. + 'shortcut reference link with unbalanced open square brackets', + (WidgetTester tester) async { + const String data = '[[bar [foo]\n\n[foo]: /url'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 2); + + expect(span.children![0], isA()); + expect(span.children![0].toPlainText(), '[[bar '); + expectTextSpanStyle( + span.children![0] as TextSpan, null, FontWeight.normal); + + expectLinkTextSpan(span.children![1] as TextSpan, 'foo'); + expectLinkTap(linkTapResults, const MarkdownLink('foo', '/url')); + }, + ); + + testWidgets( + // Example 569 from GFM. + 'shortcut reference links are case-insensitive', + (WidgetTester tester) async { + const String data = '[Foo]\n\n[foo]: /url "title"'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('Foo'); + expectLinkTap( + linkTapResults, const MarkdownLink('Foo', '/url', 'title')); + }, + ); + + testWidgets( + // Example 570 from GFM. + 'shortcut reference link should preserve space after link text', + (WidgetTester tester) async { + const String data = '[foo] bar\n\n[foo]: /url'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 2); + + expectLinkTextSpan(span.children![0] as TextSpan, 'foo'); + expectLinkTap(linkTapResults, const MarkdownLink('foo', '/url')); + + expect(span.children![1], isA()); + expect(span.children![1].toPlainText(), ' bar'); + expectTextSpanStyle( + span.children![1] as TextSpan, null, FontWeight.normal); + }, + ); + + testWidgets( + // Example 571 from GFM. + 'shortcut reference link backslash escape opening bracket to avoid link', + (WidgetTester tester) async { + const String data = '\\[foo]\n\n[foo]: /url "title"'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + // Link is treated as ordinary text. + expectInvalidLink('[foo]'); + expect(linkTapResults, isNull); + }, + ); + + testWidgets( + // Example 572 from GFM. + 'shortcut reference link text grouping has precedence over emphasis grouping', + (WidgetTester tester) async { + const String data = '[foo*]: /url\n\n*[foo*]'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 2); + expect(span.children![0], isA()); + expect(span.children![0].toPlainText(), '*'); + expectTextSpanStyle( + span.children![0] as TextSpan, null, FontWeight.normal); + + expectLinkTextSpan(span.children![1] as TextSpan, 'foo*'); + expectLinkTap(linkTapResults, const MarkdownLink('foo*', '/url')); + }, + ); + + testWidgets( + // Example 573 from GFM. + 'full link reference takes precedence over shortcut link reference', + (WidgetTester tester) async { + const String data = '[foo][bar]\n\n[foo]: /url1\n[bar]: /url2'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('foo'); + expectLinkTap(linkTapResults, const MarkdownLink('foo', '/url2')); + }, + ); + + testWidgets( + // Example 574 from GFM. + 'compact link reference takes precedence over shortcut link reference', + (WidgetTester tester) async { + const String data = '[foo][]\n\n[foo]: /url1'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('foo'); + expectLinkTap(linkTapResults, const MarkdownLink('foo', '/url1')); + }, + ); + + testWidgets( + // Example 575 from GFM. + 'inline link reference, no link destination takes precedence over shortcut link reference', + (WidgetTester tester) async { + const String data = '[foo]()\n\n[foo]: /url1'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + expectValidLink('foo'); + expectLinkTap(linkTapResults, const MarkdownLink('foo', '')); + }, + ); + + testWidgets( + // Example 576 from GFM. + 'inline link reference, invalid link destination is a link followed by text', + (WidgetTester tester) async { + const String data = '[foo](not a link)\n\n[foo]: /url1'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 2); + + expectLinkTextSpan(span.children![0] as TextSpan, 'foo'); + expectLinkTap(linkTapResults, const MarkdownLink('foo', '/url1')); + + expect(span.children![1], isA()); + expect(span.children![1].toPlainText(), '(not a link)'); + expectTextSpanStyle( + span.children![1] as TextSpan, null, FontWeight.normal); + }, + ); + + testWidgets( + // Example 577 from GFM. + 'three sequential runs of square-bracketed text, normal text and a link reference', + (WidgetTester tester) async { + const String data = '[foo][bar][baz]\n\n[baz]: /url'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 2); + + expect(span.children![0], isA()); + expect(span.children![0].toPlainText(), '[foo]'); + expectTextSpanStyle( + span.children![0] as TextSpan, null, FontWeight.normal); + + expectLinkTextSpan(span.children![1] as TextSpan, 'bar'); + expectLinkTap(linkTapResults, const MarkdownLink('bar', '/url')); + }, + ); + + testWidgets( + // Example 578 from GFM. + 'three sequential runs of square-bracketed text, two link references', + (WidgetTester tester) async { + const String data = '[foo][bar][baz]\n\n[baz]: /url1\n[bar]: /url2'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 2); + + expectLinkTextSpan(span.children![0] as TextSpan, 'foo'); + expectLinkTap(linkTapResults, const MarkdownLink('foo', '/url2')); + + expectLinkTextSpan(span.children![1] as TextSpan, 'baz'); + expectLinkTap(linkTapResults, const MarkdownLink('baz', '/url1')); + }, + ); + + testWidgets( + // Example 579 from GFM. + 'full reference link followed by a shortcut reference link', + (WidgetTester tester) async { + const String data = '[foo][bar][baz]\n\n[baz]: /url1\n[foo]: /url2'; + MarkdownLink? linkTapResults; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + data: data, + onTapLink: (String text, String? href, String title) => + linkTapResults = MarkdownLink(text, href, title), + ), + ), + ); + + final RichText textWidget = tester.widget(find.byType(RichText)); + final TextSpan span = textWidget.text as TextSpan; + expect(span.children!.length, 2); + expect(span.children![0], isA()); + expect(span.children![0].toPlainText(), '[foo]'); + + expectLinkTextSpan(span.children![1] as TextSpan, 'bar'); + expectLinkTap(linkTapResults, const MarkdownLink('bar', '/url1')); + }, + ); + }); +} diff --git a/packages/flutter_markdown/test/list_test.dart b/packages/flutter_markdown/test/list_test.dart new file mode 100644 index 0000000000..d177041a2d --- /dev/null +++ b/packages/flutter_markdown/test/list_test.dart @@ -0,0 +1,158 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/widgets.dart'; +import 'package:flutter_markdown/flutter_markdown.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 widgets = tester.allWidgets; + expectTextStrings(widgets, [ + '•', + '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 widgets = tester.allWidgets; + expectTextStrings(widgets, [ + '•', + '•', + 'Item 2', + '•', + 'Item 3', + ]); + }, + ); + }); + + 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 widgets = tester.allWidgets; + expectTextStrings(widgets, [ + '1.', + 'Item 1', + '2.', + 'Item 2', + '3.', + 'Item 3', + '10.', + 'Item 10', + '11.', + 'Item 11' + ]); + }, + ); + }); + + 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 widgets = tester.allWidgets; + + expectTextStrings(widgets, [ + 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(int index, BulletStyle style) => Text( + '$index ${style == BulletStyle.orderedList ? 'ordered' : 'unordered'}'); + + await tester.pumpWidget( + boilerplate( + Markdown(data: data, bulletBuilder: builder), + ), + ); + + final Iterable widgets = tester.allWidgets; + + expectTextStrings(widgets, [ + '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 widgets = tester.allWidgets; + + expectTextStrings(widgets, [ + 'true', + 'Item 1', + 'false', + 'Item 2', + ]); + }, + ); + }); +} diff --git a/packages/flutter_markdown/test/scrollable_test.dart b/packages/flutter_markdown/test/scrollable_test.dart new file mode 100644 index 0000000000..b9f793370b --- /dev/null +++ b/packages/flutter_markdown/test/scrollable_test.dart @@ -0,0 +1,81 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/flutter_markdown.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 widgets = tester.allWidgets; + expect(widgets.whereType(), isNotEmpty); + }, + ); + + testWidgets( + 'controller', + (WidgetTester tester) async { + final ScrollController controller = ScrollController( + initialScrollOffset: 209.0, + ); + + await tester.pumpWidget( + boilerplate( + Markdown(controller: controller, data: ''), + ), + ); + + double realOffset() { + return tester + .state(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 widgets = tester.allWidgets.toList(); + expectWidgetTypes(widgets.take(3), [ + Directionality, + Markdown, + ListView, + ]); + expectWidgetTypes(widgets.reversed.take(2).toList().reversed, [ + SliverPadding, + SliverList, + ]); + }, + ); + }); +} diff --git a/packages/flutter_markdown/test/style_sheet_test.dart b/packages/flutter_markdown/test/style_sheet_test.dart new file mode 100644 index 0000000000..df12b15a7f --- /dev/null +++ b/packages/flutter_markdown/test/style_sheet_test.dart @@ -0,0 +1,307 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/widgets.dart'; +import 'package:flutter_markdown/flutter_markdown.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'); + expect( + style.code!.backgroundColor, CupertinoColors.systemGrey6.darkColor); + + // 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( + bodyText2: TextStyle(fontSize: 12.0), + ), + ); + + final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme); + + // a + expect(style.a!.color, Colors.blue); + + // p + expect(style.p, theme.textTheme.bodyText2); + + // code + expect(style.code!.color, theme.textTheme.bodyText2!.color); + expect( + style.code!.fontSize, theme.textTheme.bodyText2!.fontSize! * 0.85); + expect(style.code!.fontFamily, 'monospace'); + expect(style.code!.backgroundColor, theme.cardColor); + + // H1 + expect(style.h1, theme.textTheme.headline5); + + // H2 + expect(style.h2, theme.textTheme.headline6); + + // H3 + expect(style.h3, theme.textTheme.subtitle1); + + // H4 + expect(style.h4, theme.textTheme.bodyText1); + + // H5 + expect(style.h5, theme.textTheme.bodyText1); + + // H6 + expect(style.h6, theme.textTheme.bodyText1); + + // em + expect(style.em!.fontStyle, FontStyle.italic); + expect(style.em!.color, theme.textTheme.bodyText2!.color); + + // strong + expect(style.strong!.fontWeight, FontWeight.bold); + expect(style.strong!.color, theme.textTheme.bodyText2!.color); + + // del + expect(style.del!.decoration, TextDecoration.lineThrough); + expect(style.del!.color, theme.textTheme.bodyText2!.color); + + // blockqoute + expect(style.blockquote, theme.textTheme.bodyText2); + + // img + expect(style.img, theme.textTheme.bodyText2); + + // checkbox + expect(style.checkbox!.color, theme.primaryColor); + expect(style.checkbox!.fontSize, theme.textTheme.bodyText2!.fontSize); + + // tableHead + expect(style.tableHead!.fontWeight, FontWeight.w600); + + // tableBody + expect(style.tableBody, theme.textTheme.bodyText2); + }, + ); + + 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.bodyText2!.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 RichText widget = tester.widget(find.byType(RichText)); + expect(widget.text.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 paddings = + tester.widgetList(find.byType(Padding)).toList(); + + expect(paddings.length, 3); + expect( + paddings.every( + (Padding p) => p.padding.along(Axis.horizontal) == paddingX * 2, + ), + true, + ); + }, + ); + }); +} diff --git a/packages/flutter_markdown/test/table_test.dart b/packages/flutter_markdown/test/table_test.dart new file mode 100644 index 0000000000..85bc6b8a2e --- /dev/null +++ b/packages/flutter_markdown/test/table_test.dart @@ -0,0 +1,529 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/widgets.dart'; +import 'package:flutter_markdown/flutter_markdown.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 widgets = tester.allWidgets; + expectTextStrings( + widgets, ['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 widgets = tester.allWidgets; + expectTextStrings( + widgets, ['Header 1', 'Header 2', 'Col 1', 'Col 2']); + }, + ); + + testWidgets( + 'should work with alignments', + (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 styles = + tester.widgetList(find.byType(DefaultTextStyle)); + + expect(styles.first.textAlign, TextAlign.center); + expect(styles.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 widgets = tester.allWidgets; + final RichText richText = widgets + .lastWhere((Widget widget) => widget is RichText) as RichText; + + expectTextStrings(widgets, ['Header', 'italic']); + expect(richText.text.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 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 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(RichText), findsNWidgets(4)); + final List cellText = find + .byType(RichText) + .evaluate() + .map((Element e) => e.widget) + .cast() + .map((RichText richText) => richText.text) + .cast() + .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 cellText = find + .byType(RichText) + .evaluate() + .map((Element e) => e.widget) + .cast() + .map((RichText richText) => richText.text) + .cast() + .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(RichText), findsNWidgets(4)); + final List cellText = find + .byType(RichText) + .evaluate() + .map((Element e) => e.widget) + .cast() + .map((RichText richText) => richText.text) + .cast() + .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(RichText), findsNWidgets(4)); + final List cellText = find + .byType(RichText) + .evaluate() + .map((Element e) => e.widget) + .cast() + .map((RichText richText) => richText.text) + .cast() + .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(RichText), findsNWidgets(4)); + final List cellText = find + .byType(RichText) + .evaluate() + .map((Element e) => e.widget) + .cast() + .map((RichText richText) => richText.text) + .cast() + .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(RichText), findsNWidgets(5)); + final List text = find + .byType(RichText) + .evaluate() + .map((Element e) => e.widget) + .cast() + .map((RichText richText) => richText.text) + .cast() + .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(RichText), findsNWidgets(6)); + final List text = find + .byType(RichText) + .evaluate() + .map((Element e) => e.widget) + .cast() + .map((RichText richText) => richText.text) + .cast() + .map((TextSpan e) => e.text) + .toList(); + expect(text[0], 'abc'); + expect(text[1], 'def'); + expect(text[2], 'bar'); + expect(text[3], 'baz'); + expect(text[4], 'bar'); + expect(table.defaultColumnWidth, columnWidth); + + // Paragraph text + expect(text[5], 'bar'); + }, + ); + + 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 text = find + .byType(RichText) + .evaluate() + .map((Element e) => e.widget) + .cast() + .map((RichText richText) => richText.text) + .cast() + .map((TextSpan e) => e.text) + .toList(); + expect(text[0], '| abc | def | | --- | | bar |'); + }, + // TODO(mjordan56): Remove skip once the issue #341 in the markdown package + // is fixed and released. https://github.com/dart-lang/markdown/issues/341 + skip: true, + ); + + 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(RichText), findsNWidgets(5)); + final List cellText = find + .byType(RichText) + .evaluate() + .map((Element e) => e.widget) + .cast() + .map((RichText richText) => richText.text) + .cast() + .map((TextSpan e) => e.text) + .toList(); + expect(cellText[0], 'abc'); + expect(cellText[1], 'def'); + expect(cellText[2], 'bar'); + expect(cellText[3], 'bar'); + expect(cellText[4], '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(RichText), findsNWidgets(2)); + final List cellText = find + .byType(RichText) + .evaluate() + .map((Element e) => e.widget) + .cast() + .map((RichText richText) => richText.text) + .cast() + .map((TextSpan e) => e.text) + .toList(); + expect(cellText[0], 'abc'); + expect(cellText[1], 'def'); + expect(table.defaultColumnWidth, columnWidth); + }, + ); + }); + }); +} diff --git a/packages/flutter_markdown/test/text_alignment_test.dart b/packages/flutter_markdown/test/text_alignment_test.dart new file mode 100644 index 0000000000..243c5da63e --- /dev/null +++ b/packages/flutter_markdown/test/text_alignment_test.dart @@ -0,0 +1,116 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/widgets.dart'; +import 'package:flutter_markdown/flutter_markdown.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 widgets = tester.allWidgets; + expectWidgetTypes(widgets, [ + Directionality, + MarkdownBody, + Column, + Column, + Wrap, + RichText, + SizedBox, + Column, + Wrap, + 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); + }, + ); + }); +} diff --git a/packages/flutter_markdown/test/text_scale_factor_test.dart b/packages/flutter_markdown/test/text_scale_factor_test.dart new file mode 100644 index 0000000000..fa8334b6cb --- /dev/null +++ b/packages/flutter_markdown/test/text_scale_factor_test.dart @@ -0,0 +1,75 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/widgets.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'utils.dart'; + +void main() => defineTests(); + +void defineTests() { + group('Text Scale Factor', () { + testWidgets( + 'should use style textScaleFactor in RichText', + (WidgetTester tester) async { + const String data = 'Hello'; + await tester.pumpWidget( + boilerplate( + MarkdownBody( + styleSheet: MarkdownStyleSheet(textScaleFactor: 2.0), + data: data, + ), + ), + ); + + final RichText richText = tester.widget(find.byType(RichText)); + expect(richText.textScaleFactor, 2.0); + }, + ); + + testWidgets( + 'should use MediaQuery textScaleFactor in RichText', + (WidgetTester tester) async { + const String data = 'Hello'; + await tester.pumpWidget( + boilerplate( + const MediaQuery( + data: MediaQueryData(textScaleFactor: 2.0), + child: MarkdownBody( + data: data, + ), + ), + ), + ); + + final RichText richText = tester.widget(find.byType(RichText)); + expect(richText.textScaleFactor, 2.0); + }, + ); + + testWidgets( + 'should use MediaQuery textScaleFactor in SelectableText.rich', + (WidgetTester tester) async { + const String data = 'Hello'; + await tester.pumpWidget( + boilerplate( + const MediaQuery( + data: MediaQueryData(textScaleFactor: 2.0), + child: MarkdownBody( + data: data, + selectable: true, + ), + ), + ), + ); + + final SelectableText selectableText = + tester.widget(find.byType(SelectableText)); + expect(selectableText.textScaleFactor, 2.0); + }, + ); + }); +} diff --git a/packages/flutter_markdown/test/text_test.dart b/packages/flutter_markdown/test/text_test.dart new file mode 100644 index 0000000000..01381b11bd --- /dev/null +++ b/packages/flutter_markdown/test/text_test.dart @@ -0,0 +1,218 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/widgets.dart'; +import 'package:flutter_markdown/flutter_markdown.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, ['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, ['Data2']); + }, + ); + }); + + group('Text', () { + testWidgets( + 'Empty string', + (WidgetTester tester) async { + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: ''), + ), + ); + + final Iterable widgets = tester.allWidgets; + expectWidgetTypes( + widgets, [Directionality, MarkdownBody, Column]); + }, + ); + + testWidgets( + 'Simple string', + (WidgetTester tester) async { + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: 'Hello'), + ), + ); + + final Iterable widgets = tester.allWidgets; + expectWidgetTypes(widgets, + [Directionality, MarkdownBody, Column, Wrap, RichText]); + expectTextStrings(widgets, ['Hello']); + }, + ); + }); + + 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 widgets = tester.allWidgets; + expectWidgetTypes(widgets, + [Directionality, MarkdownBody, Column, Wrap, RichText]); + expectTextStrings(widgets, ['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 widgets = tester.allWidgets; + expectWidgetTypes(widgets, + [Directionality, MarkdownBody, Column, Wrap, RichText]); + expectTextStrings(widgets, ['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 widgets = tester.allWidgets; + expectWidgetTypes(widgets, + [Directionality, MarkdownBody, Column, Wrap, RichText]); + expectTextStrings(widgets, ['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 widgets = tester.allWidgets; + expectWidgetTypes(widgets, + [Directionality, MarkdownBody, Column, Wrap, RichText]); + expectTextStrings(widgets, ['line 1. line 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 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); + }, + ); + }); + + group('Strikethrough', () { + testWidgets('single word', (WidgetTester tester) async { + const String data = '~~strikethrough~~'; + await tester.pumpWidget( + boilerplate( + const MarkdownBody(data: data), + ), + ); + + final Iterable widgets = tester.allWidgets; + expectWidgetTypes(widgets, + [Directionality, MarkdownBody, Column, Wrap, RichText]); + expectTextStrings(widgets, ['strikethrough']); + }); + }); +} diff --git a/packages/flutter_markdown/test/uri_test.dart b/packages/flutter_markdown/test/uri_test.dart new file mode 100644 index 0000000000..f68279464d --- /dev/null +++ b/packages/flutter_markdown/test/uri_test.dart @@ -0,0 +1,97 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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/flutter_markdown.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 = + '![alt](data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=)'; + await tester.pumpWidget( + boilerplate( + const Markdown(data: data), + ), + ); + + final Iterable 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 = '![alt](data:text/plan;base64,Rmx1dHRlcg==)'; + 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 = '![alt](data:text/plan,Hello%2C%20Flutter)'; + 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 = '![alt](data:,)'; + 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 = '![alt](data:application/javascript,var%20test=1)'; + await tester.pumpWidget( + boilerplate( + const Markdown(data: data), + ), + ); + + final Iterable widgets = tester.allWidgets; + final SizedBox widget = widgets + .firstWhere((Widget widget) => widget is SizedBox) as SizedBox; + expect(widget.runtimeType, SizedBox); + }, + ); + }); +} diff --git a/packages/flutter_markdown/test/utils.dart b/packages/flutter_markdown/test/utils.dart new file mode 100644 index 0000000000..a19789fa2c --- /dev/null +++ b/packages/flutter_markdown/test/utils.dart @@ -0,0 +1,202 @@ +// Copyright 2020 Quiverware LLC. Open source contribution. 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 'dart:typed_data'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +final TextTheme textTheme = + Typography.material2018(platform: TargetPlatform.android) + .black + .merge(const TextTheme(bodyText2: TextStyle(fontSize: 12.0))); + +void expectWidgetTypes(Iterable widgets, List expected) { + final List actual = widgets.map((Widget w) => w.runtimeType).toList(); + expect(actual, expected); +} + +void expectTextStrings(Iterable widgets, List strings) { + int currentString = 0; + for (final Widget widget in widgets) { + if (widget is RichText) { + final TextSpan span = widget.text as TextSpan; + 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! as Iterable) { + 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 richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + final RichText richText = richTextFinder.evaluate().first.widget as RichText; + + // Verify the link text. + expect(richText.text, isNotNull); + expect(richText.text, isA()); + + // Verify the link text is a onTap gesture recognizer. + final TextSpan textSpan = richText.text 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()); + final TapGestureRecognizer? tapRecognizer = + textSpan.recognizer as TapGestureRecognizer?; + expect(tapRecognizer?.onTap, isNotNull); + + // Execute the onTap callback handler. + tapRecognizer!.onTap!(); +} + +void expectInvalidLink(String linkText) { + final Finder richTextFinder = find.byType(RichText); + expect(richTextFinder, findsOneWidget); + final RichText richText = richTextFinder.evaluate().first.widget as RichText; + + expect(richText.text, isNotNull); + expect(richText.text, isA()); + final String text = richText.text.toPlainText(); + expect(text, linkText); + + final TextSpan textSpan = richText.text 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!.renderViewElement!.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 { + static const String manifest = r'{"assets/logo.png":["assets/logo.png"]}'; + + @override + Future load(String key) async { + if (key == 'AssetManifest.json') { + final ByteData asset = + ByteData.view(utf8.encoder.convert(manifest).buffer); + return Future.value(asset); + } 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); + if (asset == null) { + throw FlutterError('Unable to load asset: $key'); + } + return asset; + } else { + throw 'Unknown asset key: $key'; + } + } +}