Преглед на файлове

[flutter]: solve conflicts

annie преди 3 години
родител
ревизия
57ba7d4d4d
променени са 60 файла, в които са добавени 8138 реда и са изтрити 387 реда
  1. 110 12
      README.md
  2. 1 1
      app_flowy/lib/startup/tasks/application_task.dart
  3. 79 0
      app_flowy/lib/user/application/sign_in_bloc.freezed.dart
  4. 110 0
      app_flowy/lib/user/application/sign_up_bloc.freezed.dart
  5. 27 0
      app_flowy/lib/user/application/splash_bloc.freezed.dart
  6. 75 0
      app_flowy/lib/user/domain/auth_state.freezed.dart
  7. 177 0
      app_flowy/lib/workspace/application/app/app_bloc.freezed.dart
  8. 187 0
      app_flowy/lib/workspace/application/doc/doc_bloc.freezed.dart
  9. 52 0
      app_flowy/lib/workspace/application/doc/share_bloc.dart
  10. 867 0
      app_flowy/lib/workspace/application/doc/share_bloc.freezed.dart
  11. 49 0
      app_flowy/lib/workspace/application/edit_pannel/edit_pannel_bloc.freezed.dart
  12. 105 0
      app_flowy/lib/workspace/application/home/home_bloc.freezed.dart
  13. 123 0
      app_flowy/lib/workspace/application/home/home_listen_bloc.freezed.dart
  14. 145 0
      app_flowy/lib/workspace/application/menu/menu_bloc.freezed.dart
  15. 49 0
      app_flowy/lib/workspace/application/menu/menu_user_bloc.freezed.dart
  16. 177 0
      app_flowy/lib/workspace/application/trash/trash_bloc.freezed.dart
  17. 177 0
      app_flowy/lib/workspace/application/view/view_bloc.freezed.dart
  18. 110 0
      app_flowy/lib/workspace/application/workspace/welcome_bloc.freezed.dart
  19. 12 0
      app_flowy/lib/workspace/domain/i_share.dart
  20. 7 5
      app_flowy/lib/workspace/domain/page_stack/page_stack.dart
  21. 10 0
      app_flowy/lib/workspace/infrastructure/deps_resolver.dart
  22. 27 0
      app_flowy/lib/workspace/infrastructure/i_share_impl.dart
  23. 30 0
      app_flowy/lib/workspace/infrastructure/markdown/delta_markdown.dart
  24. 113 0
      app_flowy/lib/workspace/infrastructure/markdown/src/ast.dart
  25. 1096 0
      app_flowy/lib/workspace/infrastructure/markdown/src/block_parser.dart
  26. 255 0
      app_flowy/lib/workspace/infrastructure/markdown/src/delta_markdown_decoder.dart
  27. 272 0
      app_flowy/lib/workspace/infrastructure/markdown/src/delta_markdown_encoder.dart
  28. 88 0
      app_flowy/lib/workspace/infrastructure/markdown/src/document.dart
  29. 1510 0
      app_flowy/lib/workspace/infrastructure/markdown/src/emojis.dart
  30. 64 0
      app_flowy/lib/workspace/infrastructure/markdown/src/extension_set.dart
  31. 121 0
      app_flowy/lib/workspace/infrastructure/markdown/src/html_renderer.dart
  32. 1271 0
      app_flowy/lib/workspace/infrastructure/markdown/src/inline_parser.dart
  33. 71 0
      app_flowy/lib/workspace/infrastructure/markdown/src/util.dart
  34. 2 0
      app_flowy/lib/workspace/infrastructure/markdown/src/version.dart
  35. 15 0
      app_flowy/lib/workspace/infrastructure/repos/share_repo.dart
  36. 25 37
      app_flowy/lib/workspace/presentation/home/navigation.dart
  37. 2 2
      app_flowy/lib/workspace/presentation/stack_page/blank/blank_page.dart
  38. 145 62
      app_flowy/lib/workspace/presentation/stack_page/doc/doc_stack_page.dart
  39. 2 2
      app_flowy/lib/workspace/presentation/stack_page/trash/trash_page.dart
  40. 48 1
      app_flowy/lib/workspace/presentation/widgets/dialogs.dart
  41. 2 2
      app_flowy/lib/workspace/presentation/widgets/home_top_bar.dart
  42. 8 5
      app_flowy/lib/workspace/presentation/widgets/menu/widget/app/header/header.dart
  43. 0 3
      app_flowy/lib/workspace/presentation/widgets/menu/widget/app/header/right_click_action.dart
  44. 0 3
      app_flowy/lib/workspace/presentation/widgets/menu/widget/app/section/disclosure_action.dart
  45. 1 2
      app_flowy/lib/workspace/presentation/widgets/menu/widget/app/section/item.dart
  46. 1 1
      app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart
  47. 10 20
      app_flowy/packages/flowy_infra_ui/lib/style_widget/text_input.dart
  48. 4 1
      app_flowy/packages/flowy_infra_ui/lib/widget/rounded_input_field.dart
  49. 14 0
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace-infra/export.pb.dart
  50. 4 2
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace-infra/export.pbenum.dart
  51. 5 3
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace-infra/export.pbjson.dart
  52. 201 194
      app_flowy/pubspec.lock
  53. 2 0
      app_flowy/pubspec.yaml
  54. 3 3
      app_flowy/windows/flutter/generated_plugin_registrant.cc
  55. 1 0
      delta_markdown
  56. BIN
      doc/imgs/run.png
  57. 7 2
      rust-lib/flowy-workspace-infra/src/entities/share/export.rs
  58. 62 22
      rust-lib/flowy-workspace-infra/src/protobuf/model/export.rs
  59. 3 1
      rust-lib/flowy-workspace-infra/src/protobuf/proto/export.proto
  60. 4 1
      rust-lib/flowy-workspace/src/services/view_controller.rs

+ 110 - 12
README.md

@@ -1,23 +1,121 @@
-[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
-![Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg)
+<h1 align="center" style="border-bottom: none">
+    <b>
+        <a href="https://www.appflowy.io">AppFlowy.IO</a><br>
+    </b>
+    ⭐️ The Open Source Notion Alternative ⭐️ <br>
+</h1>
 
+<p align="center">
+You are in charge of your data and customizations.
+</p>
 
-# what is AppFlowy?
 
-## Features
+<div align="center">
+ 
+![GitHub stars](https://img.shields.io/github/stars/{AppFlowy-IO}/{appflowy}.svg)
 
-## Documentation
+</div>
 
-* [**Getting Started**](doc/GETTING_STARTED.md)
-* [**Roadmap**](doc/ROADMAP.md)
-* [**Deep Dive AppFlowy**](doc/APPFLOWY_SYSTEM_DESIGN.md)
+<p align="center">
+    <a href="http://www.appflowy.io"><b>Website</b></a> •
+    <a href="https://discord.gg/9Q2xaN37tV"><b>Discord</b></a> •
+    <a href="https://twitter.com/appflowy"><b>Twitter</b></a> •
+</p>  
 
+<p align="center"><img src="" alt="The Open Source Notion Alternative." width="1000px" /></p>
+
+
+
+## Built With
+
+* [Flutter](https://flutter.dev/)
+
+* [Rust](https://www.rust-lang.org/)
+
+## Stay Up-to-Date
+
+GIF (how to star)
+
+## Getting Started
+
+To get a local copy up and running, please follow these simple steps.
+
+Development environment setup:
+
+**Step 1:**
+
+* git clone  [https://github.com/AppFlowy-IO/appflowy.git](https://github.com/AppFlowy-IO/appflowy.git)
+
+**Step 2:**
+
+- `cd appflowy`
+- `make install_rust`
+
+> skip if you already installed it
+> 
+- `make nstall_cargo_make`
+
+> AppFlowy uses [https://github.com/sagiegurari/cargo-make](https://github.com/sagiegurari/cargo-make) to construct the build scripts
+> 
+- `cargo make flowy_dev`
+> It's located at xxx/appflowy/scripts/makefile/env.toml. `flowy_dev` consists of three tasks:
+> * `install_targets`
+> * `install_diesel`
+> * `install_protobuf`
+
+**Step 3:**
+
+* Follow the instructions [here](https://flutter.dev/docs/get-started/install) to install Flutter. As AppFlowy uses the `dev` channel, you need to switch the channel. Just type:
+
+`flutter channel dev`
+
+**Step 4:**
+
+* Open the `app_flowy` folder located at xx/appflowy/app_flowy with Visual Studio or other IDEs at your disposal
+* Go to the Run and Debug tab and then click the run button.
+![Run the project](https://github.com/AppFlowy-IO/appflowy/blob/main/doc/imgs/run.png)
+
+## Roadmap
+
+[AppFlowy Roadmap](https://trello.com/b/NCyXCXXh/appflowy-roadmap)
+
+If you'd like to propose a feature, submit an issue [here](https://github.com/AppFlowy-IO/appflowy/issues)
+
+## **Releases**
+
+Please see the [changelog](https://www.appflowy.io/whatsnew) for more details about a given release.
 
 ## Contributing
-Read the [Contributing Doc](doc/CONTRIBUTING.md) before you want to contribute.
 
-## Social Media
-* Slack (to be determined)
+Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [CONTRIBUTING.md](https://github.com/AppFlowy-IO/appflowy/blob/main/doc/CONTRIBUTING.md) for details.
+
+## Why Are We Building This?
+
+Notion has been our favorite project and knowledge management tool in recent years because of its aesthetic appeal and functionality. Our team uses it daily, and we are on its paid plan. However, as we all know Notion has its limitations. These include weak data security and poor compatibility with mobile devices. Likewise, alternative collaborative workplace management tools also have their constraints.
+
+The limitations we encountered using these tools rooted in our past work experience with collaborative productivity tools lead to our firm belief that there is, and will be a glass ceiling on what's possible in the future for tools like Notion. This emanates from these tools probable struggles to scale horizontally at some point. It implies that they will likely be forced to prioritize for a proportion of customers whose needs can be quite different from the rest. While decision-makers want a workplace OS, the truth is that it is not very possible to come up with a one-size fits all solution in such a fragmented market.
+
+When a customer's evolving core needs are not satisfied, they either switch to another or build one from the ground up, in-house. Consequently, they either go under another ceiling or buy an expensive ticket to learn a hard lesson. This is a requirement for many resources and expertise, building a reliable and easy-to-use collaborative tool, not to mention the speed and native experience. The same may apply to individual users as well.
+
+All these restrictions necessitate our mission - to make it possible for anyone to create apps that suit their needs well.
+
+- To individuals, we would like to offer Notion's functionality along with data security and cross-platform native experience.
+- To enterprises and hackers, AppFlowy is dedicated to offering building blocks, that is, collaboration infra services to enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term maintainability.
+
+We decided to achieve this mission by upholding the three most fundamental values:
+
+- Data privacy first
+- Reliable native experience
+- Community-driven extensibility
+
+To be honest, we do not claim to outperform Notion in terms of functionality and design, at least for now. Besides, our priority doesn't lie in more functionality at the moment. Instead, we would like to cultivate a community to democratize the knowledge and wheels of making complex workplace management tools, while enabling people and businesses to create beautiful things on their own by equipping them with a versatile toolbox of building blocks.
 
 ## License
-AppFlowy is under the Apache 2.0 license. See the [LICENSE](/LICENSE) file for details.
+
+Distributed under the AGPLv3 License. See `LICENSE.md` for more information.
+
+## Acknowledgements
+
+Special thanks to these amazing projects which help power AppFlowy.IO:
+
+- [flutter-quill](https://github.com/singerdmx/flutter-quill)

+ 1 - 1
app_flowy/lib/startup/tasks/application_task.dart

@@ -33,7 +33,7 @@ class ApplicationWidget extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     const ratio = 1.73;
-    const minWidth = 1200.0;
+    const minWidth = 800.0;
     setWindowMinSize(const Size(minWidth, minWidth / ratio));
     // const launchWidth = 1310.0;
     // setWindowFrame(const Rect.fromLTWH(0, 0, launchWidth, launchWidth / ratio));

+ 79 - 0
app_flowy/lib/user/application/sign_in_bloc.freezed.dart

@@ -1,3 +1,4 @@
+// coverage:ignore-file
 // GENERATED CODE - DO NOT MODIFY BY HAND
 // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
 
@@ -46,6 +47,13 @@ mixin _$SignInEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? signedInWithUserEmailAndPassword,
+    TResult Function(String email)? emailChanged,
+    TResult Function(String password)? passwordChanged,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? signedInWithUserEmailAndPassword,
     TResult Function(String email)? emailChanged,
@@ -62,6 +70,14 @@ mixin _$SignInEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(SignedInWithUserEmailAndPassword value)?
+        signedInWithUserEmailAndPassword,
+    TResult Function(EmailChanged value)? emailChanged,
+    TResult Function(PasswordChanged value)? passwordChanged,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
     TResult Function(SignedInWithUserEmailAndPassword value)?
         signedInWithUserEmailAndPassword,
@@ -140,6 +156,16 @@ class _$SignedInWithUserEmailAndPassword
     return signedInWithUserEmailAndPassword();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? signedInWithUserEmailAndPassword,
+    TResult Function(String email)? emailChanged,
+    TResult Function(String password)? passwordChanged,
+  }) {
+    return signedInWithUserEmailAndPassword?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -165,6 +191,17 @@ class _$SignedInWithUserEmailAndPassword
     return signedInWithUserEmailAndPassword(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(SignedInWithUserEmailAndPassword value)?
+        signedInWithUserEmailAndPassword,
+    TResult Function(EmailChanged value)? emailChanged,
+    TResult Function(PasswordChanged value)? passwordChanged,
+  }) {
+    return signedInWithUserEmailAndPassword?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -257,6 +294,16 @@ class _$EmailChanged implements EmailChanged {
     return emailChanged(email);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? signedInWithUserEmailAndPassword,
+    TResult Function(String email)? emailChanged,
+    TResult Function(String password)? passwordChanged,
+  }) {
+    return emailChanged?.call(email);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -282,6 +329,17 @@ class _$EmailChanged implements EmailChanged {
     return emailChanged(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(SignedInWithUserEmailAndPassword value)?
+        signedInWithUserEmailAndPassword,
+    TResult Function(EmailChanged value)? emailChanged,
+    TResult Function(PasswordChanged value)? passwordChanged,
+  }) {
+    return emailChanged?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -380,6 +438,16 @@ class _$PasswordChanged implements PasswordChanged {
     return passwordChanged(password);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? signedInWithUserEmailAndPassword,
+    TResult Function(String email)? emailChanged,
+    TResult Function(String password)? passwordChanged,
+  }) {
+    return passwordChanged?.call(password);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -405,6 +473,17 @@ class _$PasswordChanged implements PasswordChanged {
     return passwordChanged(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(SignedInWithUserEmailAndPassword value)?
+        signedInWithUserEmailAndPassword,
+    TResult Function(EmailChanged value)? emailChanged,
+    TResult Function(PasswordChanged value)? passwordChanged,
+  }) {
+    return passwordChanged?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({

+ 110 - 0
app_flowy/lib/user/application/sign_up_bloc.freezed.dart

@@ -1,3 +1,4 @@
+// coverage:ignore-file
 // GENERATED CODE - DO NOT MODIFY BY HAND
 // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
 
@@ -53,6 +54,14 @@ mixin _$SignUpEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? signUpWithUserEmailAndPassword,
+    TResult Function(String email)? emailChanged,
+    TResult Function(String password)? passwordChanged,
+    TResult Function(String password)? repeatPasswordChanged,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? signUpWithUserEmailAndPassword,
     TResult Function(String email)? emailChanged,
@@ -72,6 +81,15 @@ mixin _$SignUpEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(SignUpWithUserEmailAndPassword value)?
+        signUpWithUserEmailAndPassword,
+    TResult Function(EmailChanged value)? emailChanged,
+    TResult Function(PasswordChanged value)? passwordChanged,
+    TResult Function(RepeatPasswordChanged value)? repeatPasswordChanged,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
     TResult Function(SignUpWithUserEmailAndPassword value)?
         signUpWithUserEmailAndPassword,
@@ -151,6 +169,17 @@ class _$SignUpWithUserEmailAndPassword
     return signUpWithUserEmailAndPassword();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? signUpWithUserEmailAndPassword,
+    TResult Function(String email)? emailChanged,
+    TResult Function(String password)? passwordChanged,
+    TResult Function(String password)? repeatPasswordChanged,
+  }) {
+    return signUpWithUserEmailAndPassword?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -179,6 +208,18 @@ class _$SignUpWithUserEmailAndPassword
     return signUpWithUserEmailAndPassword(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(SignUpWithUserEmailAndPassword value)?
+        signUpWithUserEmailAndPassword,
+    TResult Function(EmailChanged value)? emailChanged,
+    TResult Function(PasswordChanged value)? passwordChanged,
+    TResult Function(RepeatPasswordChanged value)? repeatPasswordChanged,
+  }) {
+    return signUpWithUserEmailAndPassword?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -273,6 +314,17 @@ class _$EmailChanged implements EmailChanged {
     return emailChanged(email);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? signUpWithUserEmailAndPassword,
+    TResult Function(String email)? emailChanged,
+    TResult Function(String password)? passwordChanged,
+    TResult Function(String password)? repeatPasswordChanged,
+  }) {
+    return emailChanged?.call(email);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -301,6 +353,18 @@ class _$EmailChanged implements EmailChanged {
     return emailChanged(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(SignUpWithUserEmailAndPassword value)?
+        signUpWithUserEmailAndPassword,
+    TResult Function(EmailChanged value)? emailChanged,
+    TResult Function(PasswordChanged value)? passwordChanged,
+    TResult Function(RepeatPasswordChanged value)? repeatPasswordChanged,
+  }) {
+    return emailChanged?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -401,6 +465,17 @@ class _$PasswordChanged implements PasswordChanged {
     return passwordChanged(password);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? signUpWithUserEmailAndPassword,
+    TResult Function(String email)? emailChanged,
+    TResult Function(String password)? passwordChanged,
+    TResult Function(String password)? repeatPasswordChanged,
+  }) {
+    return passwordChanged?.call(password);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -429,6 +504,18 @@ class _$PasswordChanged implements PasswordChanged {
     return passwordChanged(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(SignUpWithUserEmailAndPassword value)?
+        signUpWithUserEmailAndPassword,
+    TResult Function(EmailChanged value)? emailChanged,
+    TResult Function(PasswordChanged value)? passwordChanged,
+    TResult Function(RepeatPasswordChanged value)? repeatPasswordChanged,
+  }) {
+    return passwordChanged?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -530,6 +617,17 @@ class _$RepeatPasswordChanged implements RepeatPasswordChanged {
     return repeatPasswordChanged(password);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? signUpWithUserEmailAndPassword,
+    TResult Function(String email)? emailChanged,
+    TResult Function(String password)? passwordChanged,
+    TResult Function(String password)? repeatPasswordChanged,
+  }) {
+    return repeatPasswordChanged?.call(password);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -558,6 +656,18 @@ class _$RepeatPasswordChanged implements RepeatPasswordChanged {
     return repeatPasswordChanged(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(SignUpWithUserEmailAndPassword value)?
+        signUpWithUserEmailAndPassword,
+    TResult Function(EmailChanged value)? emailChanged,
+    TResult Function(PasswordChanged value)? passwordChanged,
+    TResult Function(RepeatPasswordChanged value)? repeatPasswordChanged,
+  }) {
+    return repeatPasswordChanged?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({

+ 27 - 0
app_flowy/lib/user/application/splash_bloc.freezed.dart

@@ -1,3 +1,4 @@
+// coverage:ignore-file
 // GENERATED CODE - DO NOT MODIFY BY HAND
 // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
 
@@ -32,6 +33,11 @@ mixin _$SplashEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? getUser,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? getUser,
     required TResult orElse(),
@@ -43,6 +49,11 @@ mixin _$SplashEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_GetUser value)? getUser,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
     TResult Function(_GetUser value)? getUser,
     required TResult orElse(),
@@ -108,6 +119,14 @@ class _$_GetUser implements _GetUser {
     return getUser();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? getUser,
+  }) {
+    return getUser?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -128,6 +147,14 @@ class _$_GetUser implements _GetUser {
     return getUser(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_GetUser value)? getUser,
+  }) {
+    return getUser?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({

+ 75 - 0
app_flowy/lib/user/domain/auth_state.freezed.dart

@@ -1,3 +1,4 @@
+// coverage:ignore-file
 // GENERATED CODE - DO NOT MODIFY BY HAND
 // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
 
@@ -46,6 +47,13 @@ mixin _$AuthState {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function(UserProfile userProfile)? authenticated,
+    TResult Function(UserError error)? unauthenticated,
+    TResult Function()? initial,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
     TResult Function(UserProfile userProfile)? authenticated,
     TResult Function(UserError error)? unauthenticated,
@@ -61,6 +69,13 @@ mixin _$AuthState {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Authenticated value)? authenticated,
+    TResult Function(Unauthenticated value)? unauthenticated,
+    TResult Function(_Initial value)? initial,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
     TResult Function(Authenticated value)? authenticated,
     TResult Function(Unauthenticated value)? unauthenticated,
@@ -157,6 +172,16 @@ class _$Authenticated implements Authenticated {
     return authenticated(userProfile);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function(UserProfile userProfile)? authenticated,
+    TResult Function(UserError error)? unauthenticated,
+    TResult Function()? initial,
+  }) {
+    return authenticated?.call(userProfile);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -181,6 +206,16 @@ class _$Authenticated implements Authenticated {
     return authenticated(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Authenticated value)? authenticated,
+    TResult Function(Unauthenticated value)? unauthenticated,
+    TResult Function(_Initial value)? initial,
+  }) {
+    return authenticated?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -276,6 +311,16 @@ class _$Unauthenticated implements Unauthenticated {
     return unauthenticated(error);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function(UserProfile userProfile)? authenticated,
+    TResult Function(UserError error)? unauthenticated,
+    TResult Function()? initial,
+  }) {
+    return unauthenticated?.call(error);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -300,6 +345,16 @@ class _$Unauthenticated implements Unauthenticated {
     return unauthenticated(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Authenticated value)? authenticated,
+    TResult Function(Unauthenticated value)? unauthenticated,
+    TResult Function(_Initial value)? initial,
+  }) {
+    return unauthenticated?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -368,6 +423,16 @@ class _$_Initial implements _Initial {
     return initial();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function(UserProfile userProfile)? authenticated,
+    TResult Function(UserError error)? unauthenticated,
+    TResult Function()? initial,
+  }) {
+    return initial?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -392,6 +457,16 @@ class _$_Initial implements _Initial {
     return initial(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Authenticated value)? authenticated,
+    TResult Function(Unauthenticated value)? unauthenticated,
+    TResult Function(_Initial value)? initial,
+  }) {
+    return initial?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({

+ 177 - 0
app_flowy/lib/workspace/application/app/app_bloc.freezed.dart

@@ -1,3 +1,4 @@
+// coverage:ignore-file
 // GENERATED CODE - DO NOT MODIFY BY HAND
 // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
 
@@ -68,6 +69,16 @@ mixin _$AppEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(String name, String desc, ViewType viewType)? createView,
+    TResult Function()? delete,
+    TResult Function(String newName)? rename,
+    TResult Function(List<View> views)? didReceiveViews,
+    TResult Function(App app)? appDidUpdate,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? initial,
     TResult Function(String name, String desc, ViewType viewType)? createView,
@@ -89,6 +100,16 @@ mixin _$AppEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(CreateView value)? createView,
+    TResult Function(Delete value)? delete,
+    TResult Function(Rename value)? rename,
+    TResult Function(ReceiveViews value)? didReceiveViews,
+    TResult Function(AppDidUpdate value)? appDidUpdate,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
     TResult Function(Initial value)? initial,
     TResult Function(CreateView value)? createView,
@@ -164,6 +185,19 @@ class _$Initial implements Initial {
     return initial();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(String name, String desc, ViewType viewType)? createView,
+    TResult Function()? delete,
+    TResult Function(String newName)? rename,
+    TResult Function(List<View> views)? didReceiveViews,
+    TResult Function(App app)? appDidUpdate,
+  }) {
+    return initial?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -194,6 +228,19 @@ class _$Initial implements Initial {
     return initial(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(CreateView value)? createView,
+    TResult Function(Delete value)? delete,
+    TResult Function(Rename value)? rename,
+    TResult Function(ReceiveViews value)? didReceiveViews,
+    TResult Function(AppDidUpdate value)? appDidUpdate,
+  }) {
+    return initial?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -312,6 +359,19 @@ class _$CreateView implements CreateView {
     return createView(name, desc, viewType);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(String name, String desc, ViewType viewType)? createView,
+    TResult Function()? delete,
+    TResult Function(String newName)? rename,
+    TResult Function(List<View> views)? didReceiveViews,
+    TResult Function(App app)? appDidUpdate,
+  }) {
+    return createView?.call(name, desc, viewType);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -342,6 +402,19 @@ class _$CreateView implements CreateView {
     return createView(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(CreateView value)? createView,
+    TResult Function(Delete value)? delete,
+    TResult Function(Rename value)? rename,
+    TResult Function(ReceiveViews value)? didReceiveViews,
+    TResult Function(AppDidUpdate value)? appDidUpdate,
+  }) {
+    return createView?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -420,6 +493,19 @@ class _$Delete implements Delete {
     return delete();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(String name, String desc, ViewType viewType)? createView,
+    TResult Function()? delete,
+    TResult Function(String newName)? rename,
+    TResult Function(List<View> views)? didReceiveViews,
+    TResult Function(App app)? appDidUpdate,
+  }) {
+    return delete?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -450,6 +536,19 @@ class _$Delete implements Delete {
     return delete(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(CreateView value)? createView,
+    TResult Function(Delete value)? delete,
+    TResult Function(Rename value)? rename,
+    TResult Function(ReceiveViews value)? didReceiveViews,
+    TResult Function(AppDidUpdate value)? appDidUpdate,
+  }) {
+    return delete?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -545,6 +644,19 @@ class _$Rename implements Rename {
     return rename(newName);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(String name, String desc, ViewType viewType)? createView,
+    TResult Function()? delete,
+    TResult Function(String newName)? rename,
+    TResult Function(List<View> views)? didReceiveViews,
+    TResult Function(App app)? appDidUpdate,
+  }) {
+    return rename?.call(newName);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -575,6 +687,19 @@ class _$Rename implements Rename {
     return rename(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(CreateView value)? createView,
+    TResult Function(Delete value)? delete,
+    TResult Function(Rename value)? rename,
+    TResult Function(ReceiveViews value)? didReceiveViews,
+    TResult Function(AppDidUpdate value)? appDidUpdate,
+  }) {
+    return rename?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -676,6 +801,19 @@ class _$ReceiveViews implements ReceiveViews {
     return didReceiveViews(views);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(String name, String desc, ViewType viewType)? createView,
+    TResult Function()? delete,
+    TResult Function(String newName)? rename,
+    TResult Function(List<View> views)? didReceiveViews,
+    TResult Function(App app)? appDidUpdate,
+  }) {
+    return didReceiveViews?.call(views);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -706,6 +844,19 @@ class _$ReceiveViews implements ReceiveViews {
     return didReceiveViews(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(CreateView value)? createView,
+    TResult Function(Delete value)? delete,
+    TResult Function(Rename value)? rename,
+    TResult Function(ReceiveViews value)? didReceiveViews,
+    TResult Function(AppDidUpdate value)? appDidUpdate,
+  }) {
+    return didReceiveViews?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -808,6 +959,19 @@ class _$AppDidUpdate implements AppDidUpdate {
     return appDidUpdate(app);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(String name, String desc, ViewType viewType)? createView,
+    TResult Function()? delete,
+    TResult Function(String newName)? rename,
+    TResult Function(List<View> views)? didReceiveViews,
+    TResult Function(App app)? appDidUpdate,
+  }) {
+    return appDidUpdate?.call(app);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -838,6 +1002,19 @@ class _$AppDidUpdate implements AppDidUpdate {
     return appDidUpdate(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(CreateView value)? createView,
+    TResult Function(Delete value)? delete,
+    TResult Function(Rename value)? rename,
+    TResult Function(ReceiveViews value)? didReceiveViews,
+    TResult Function(AppDidUpdate value)? appDidUpdate,
+  }) {
+    return appDidUpdate?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({

+ 187 - 0
app_flowy/lib/workspace/application/doc/doc_bloc.freezed.dart

@@ -1,3 +1,4 @@
+// coverage:ignore-file
 // GENERATED CODE - DO NOT MODIFY BY HAND
 // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
 
@@ -52,6 +53,15 @@ mixin _$DocEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? deleted,
+    TResult Function()? restore,
+    TResult Function()? restorePage,
+    TResult Function()? deletePermanently,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? initial,
     TResult Function()? deleted,
@@ -71,6 +81,15 @@ mixin _$DocEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(Deleted value)? deleted,
+    TResult Function(Restore value)? restore,
+    TResult Function(RestorePage value)? restorePage,
+    TResult Function(DeletePermanently value)? deletePermanently,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
     TResult Function(Initial value)? initial,
     TResult Function(Deleted value)? deleted,
@@ -143,6 +162,18 @@ class _$Initial implements Initial {
     return initial();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? deleted,
+    TResult Function()? restore,
+    TResult Function()? restorePage,
+    TResult Function()? deletePermanently,
+  }) {
+    return initial?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -171,6 +202,18 @@ class _$Initial implements Initial {
     return initial(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(Deleted value)? deleted,
+    TResult Function(Restore value)? restore,
+    TResult Function(RestorePage value)? restorePage,
+    TResult Function(DeletePermanently value)? deletePermanently,
+  }) {
+    return initial?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -238,6 +281,18 @@ class _$Deleted implements Deleted {
     return deleted();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? deleted,
+    TResult Function()? restore,
+    TResult Function()? restorePage,
+    TResult Function()? deletePermanently,
+  }) {
+    return deleted?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -266,6 +321,18 @@ class _$Deleted implements Deleted {
     return deleted(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(Deleted value)? deleted,
+    TResult Function(Restore value)? restore,
+    TResult Function(RestorePage value)? restorePage,
+    TResult Function(DeletePermanently value)? deletePermanently,
+  }) {
+    return deleted?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -333,6 +400,18 @@ class _$Restore implements Restore {
     return restore();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? deleted,
+    TResult Function()? restore,
+    TResult Function()? restorePage,
+    TResult Function()? deletePermanently,
+  }) {
+    return restore?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -361,6 +440,18 @@ class _$Restore implements Restore {
     return restore(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(Deleted value)? deleted,
+    TResult Function(Restore value)? restore,
+    TResult Function(RestorePage value)? restorePage,
+    TResult Function(DeletePermanently value)? deletePermanently,
+  }) {
+    return restore?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -430,6 +521,18 @@ class _$RestorePage implements RestorePage {
     return restorePage();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? deleted,
+    TResult Function()? restore,
+    TResult Function()? restorePage,
+    TResult Function()? deletePermanently,
+  }) {
+    return restorePage?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -458,6 +561,18 @@ class _$RestorePage implements RestorePage {
     return restorePage(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(Deleted value)? deleted,
+    TResult Function(Restore value)? restore,
+    TResult Function(RestorePage value)? restorePage,
+    TResult Function(DeletePermanently value)? deletePermanently,
+  }) {
+    return restorePage?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -527,6 +642,18 @@ class _$DeletePermanently implements DeletePermanently {
     return deletePermanently();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? deleted,
+    TResult Function()? restore,
+    TResult Function()? restorePage,
+    TResult Function()? deletePermanently,
+  }) {
+    return deletePermanently?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -555,6 +682,18 @@ class _$DeletePermanently implements DeletePermanently {
     return deletePermanently(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(Deleted value)? deleted,
+    TResult Function(Restore value)? restore,
+    TResult Function(RestorePage value)? restorePage,
+    TResult Function(DeletePermanently value)? deletePermanently,
+  }) {
+    return deletePermanently?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -790,6 +929,12 @@ mixin _$DocLoadState {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? loading,
+    TResult Function(Either<Unit, WorkspaceError> successOrFail)? finish,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? loading,
     TResult Function(Either<Unit, WorkspaceError> successOrFail)? finish,
@@ -803,6 +948,12 @@ mixin _$DocLoadState {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Loading value)? loading,
+    TResult Function(_Finish value)? finish,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
     TResult Function(_Loading value)? loading,
     TResult Function(_Finish value)? finish,
@@ -871,6 +1022,15 @@ class _$_Loading implements _Loading {
     return loading();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? loading,
+    TResult Function(Either<Unit, WorkspaceError> successOrFail)? finish,
+  }) {
+    return loading?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -893,6 +1053,15 @@ class _$_Loading implements _Loading {
     return loading(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Loading value)? loading,
+    TResult Function(_Finish value)? finish,
+  }) {
+    return loading?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -981,6 +1150,15 @@ class _$_Finish implements _Finish {
     return finish(successOrFail);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? loading,
+    TResult Function(Either<Unit, WorkspaceError> successOrFail)? finish,
+  }) {
+    return finish?.call(successOrFail);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -1003,6 +1181,15 @@ class _$_Finish implements _Finish {
     return finish(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Loading value)? loading,
+    TResult Function(_Finish value)? finish,
+  }) {
+    return finish?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({

+ 52 - 0
app_flowy/lib/workspace/application/doc/share_bloc.dart

@@ -0,0 +1,52 @@
+import 'package:app_flowy/workspace/domain/i_share.dart';
+import 'package:app_flowy/workspace/infrastructure/markdown/delta_markdown.dart';
+import 'package:flowy_sdk/protobuf/flowy-workspace-infra/export.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-workspace-infra/view_create.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-workspace/errors.pb.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:dartz/dartz.dart';
+part 'share_bloc.freezed.dart';
+
+class DocShareBloc extends Bloc<DocShareEvent, DocShareState> {
+  IShare shareManager;
+  View view;
+  DocShareBloc({required this.view, required this.shareManager}) : super(const DocShareState.initial()) {
+    on<DocShareEvent>((event, emit) async {
+      await event.map(
+        shareMarkdown: (ShareMarkdown value) async {
+          await shareManager.exportMarkdown(view.id).then((result) {
+            result.fold(
+              (value) => emit(DocShareState.finish(left(_convertDeltaToMarkdown(value)))),
+              (error) => emit(DocShareState.finish(right(error))),
+            );
+          });
+
+          emit(const DocShareState.loading());
+        },
+        shareLink: (ShareLink value) {},
+        shareText: (ShareText value) {},
+      );
+    });
+  }
+
+  ExportData _convertDeltaToMarkdown(ExportData value) {
+    final result = deltaToMarkdown(value.data);
+    value.data = result;
+    return value;
+  }
+}
+
+@freezed
+class DocShareEvent with _$DocShareEvent {
+  const factory DocShareEvent.shareMarkdown() = ShareMarkdown;
+  const factory DocShareEvent.shareText() = ShareText;
+  const factory DocShareEvent.shareLink() = ShareLink;
+}
+
+@freezed
+class DocShareState with _$DocShareState {
+  const factory DocShareState.initial() = _Initial;
+  const factory DocShareState.loading() = _Loading;
+  const factory DocShareState.finish(Either<ExportData, WorkspaceError> successOrFail) = _Finish;
+}

+ 867 - 0
app_flowy/lib/workspace/application/doc/share_bloc.freezed.dart

@@ -0,0 +1,867 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
+
+part of 'share_bloc.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity<T>(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+    'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
+
+/// @nodoc
+class _$DocShareEventTearOff {
+  const _$DocShareEventTearOff();
+
+  ShareMarkdown shareMarkdown() {
+    return const ShareMarkdown();
+  }
+
+  ShareText shareText() {
+    return const ShareText();
+  }
+
+  ShareLink shareLink() {
+    return const ShareLink();
+  }
+}
+
+/// @nodoc
+const $DocShareEvent = _$DocShareEventTearOff();
+
+/// @nodoc
+mixin _$DocShareEvent {
+  @optionalTypeArgs
+  TResult when<TResult extends Object?>({
+    required TResult Function() shareMarkdown,
+    required TResult Function() shareText,
+    required TResult Function() shareLink,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? shareMarkdown,
+    TResult Function()? shareText,
+    TResult Function()? shareLink,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
+  TResult maybeWhen<TResult extends Object?>({
+    TResult Function()? shareMarkdown,
+    TResult Function()? shareText,
+    TResult Function()? shareLink,
+    required TResult orElse(),
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
+  TResult map<TResult extends Object?>({
+    required TResult Function(ShareMarkdown value) shareMarkdown,
+    required TResult Function(ShareText value) shareText,
+    required TResult Function(ShareLink value) shareLink,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(ShareMarkdown value)? shareMarkdown,
+    TResult Function(ShareText value)? shareText,
+    TResult Function(ShareLink value)? shareLink,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
+  TResult maybeMap<TResult extends Object?>({
+    TResult Function(ShareMarkdown value)? shareMarkdown,
+    TResult Function(ShareText value)? shareText,
+    TResult Function(ShareLink value)? shareLink,
+    required TResult orElse(),
+  }) =>
+      throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $DocShareEventCopyWith<$Res> {
+  factory $DocShareEventCopyWith(
+          DocShareEvent value, $Res Function(DocShareEvent) then) =
+      _$DocShareEventCopyWithImpl<$Res>;
+}
+
+/// @nodoc
+class _$DocShareEventCopyWithImpl<$Res>
+    implements $DocShareEventCopyWith<$Res> {
+  _$DocShareEventCopyWithImpl(this._value, this._then);
+
+  final DocShareEvent _value;
+  // ignore: unused_field
+  final $Res Function(DocShareEvent) _then;
+}
+
+/// @nodoc
+abstract class $ShareMarkdownCopyWith<$Res> {
+  factory $ShareMarkdownCopyWith(
+          ShareMarkdown value, $Res Function(ShareMarkdown) then) =
+      _$ShareMarkdownCopyWithImpl<$Res>;
+}
+
+/// @nodoc
+class _$ShareMarkdownCopyWithImpl<$Res>
+    extends _$DocShareEventCopyWithImpl<$Res>
+    implements $ShareMarkdownCopyWith<$Res> {
+  _$ShareMarkdownCopyWithImpl(
+      ShareMarkdown _value, $Res Function(ShareMarkdown) _then)
+      : super(_value, (v) => _then(v as ShareMarkdown));
+
+  @override
+  ShareMarkdown get _value => super._value as ShareMarkdown;
+}
+
+/// @nodoc
+
+class _$ShareMarkdown implements ShareMarkdown {
+  const _$ShareMarkdown();
+
+  @override
+  String toString() {
+    return 'DocShareEvent.shareMarkdown()';
+  }
+
+  @override
+  bool operator ==(dynamic other) {
+    return identical(this, other) || (other is ShareMarkdown);
+  }
+
+  @override
+  int get hashCode => runtimeType.hashCode;
+
+  @override
+  @optionalTypeArgs
+  TResult when<TResult extends Object?>({
+    required TResult Function() shareMarkdown,
+    required TResult Function() shareText,
+    required TResult Function() shareLink,
+  }) {
+    return shareMarkdown();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? shareMarkdown,
+    TResult Function()? shareText,
+    TResult Function()? shareLink,
+  }) {
+    return shareMarkdown?.call();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeWhen<TResult extends Object?>({
+    TResult Function()? shareMarkdown,
+    TResult Function()? shareText,
+    TResult Function()? shareLink,
+    required TResult orElse(),
+  }) {
+    if (shareMarkdown != null) {
+      return shareMarkdown();
+    }
+    return orElse();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult map<TResult extends Object?>({
+    required TResult Function(ShareMarkdown value) shareMarkdown,
+    required TResult Function(ShareText value) shareText,
+    required TResult Function(ShareLink value) shareLink,
+  }) {
+    return shareMarkdown(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(ShareMarkdown value)? shareMarkdown,
+    TResult Function(ShareText value)? shareText,
+    TResult Function(ShareLink value)? shareLink,
+  }) {
+    return shareMarkdown?.call(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeMap<TResult extends Object?>({
+    TResult Function(ShareMarkdown value)? shareMarkdown,
+    TResult Function(ShareText value)? shareText,
+    TResult Function(ShareLink value)? shareLink,
+    required TResult orElse(),
+  }) {
+    if (shareMarkdown != null) {
+      return shareMarkdown(this);
+    }
+    return orElse();
+  }
+}
+
+abstract class ShareMarkdown implements DocShareEvent {
+  const factory ShareMarkdown() = _$ShareMarkdown;
+}
+
+/// @nodoc
+abstract class $ShareTextCopyWith<$Res> {
+  factory $ShareTextCopyWith(ShareText value, $Res Function(ShareText) then) =
+      _$ShareTextCopyWithImpl<$Res>;
+}
+
+/// @nodoc
+class _$ShareTextCopyWithImpl<$Res> extends _$DocShareEventCopyWithImpl<$Res>
+    implements $ShareTextCopyWith<$Res> {
+  _$ShareTextCopyWithImpl(ShareText _value, $Res Function(ShareText) _then)
+      : super(_value, (v) => _then(v as ShareText));
+
+  @override
+  ShareText get _value => super._value as ShareText;
+}
+
+/// @nodoc
+
+class _$ShareText implements ShareText {
+  const _$ShareText();
+
+  @override
+  String toString() {
+    return 'DocShareEvent.shareText()';
+  }
+
+  @override
+  bool operator ==(dynamic other) {
+    return identical(this, other) || (other is ShareText);
+  }
+
+  @override
+  int get hashCode => runtimeType.hashCode;
+
+  @override
+  @optionalTypeArgs
+  TResult when<TResult extends Object?>({
+    required TResult Function() shareMarkdown,
+    required TResult Function() shareText,
+    required TResult Function() shareLink,
+  }) {
+    return shareText();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? shareMarkdown,
+    TResult Function()? shareText,
+    TResult Function()? shareLink,
+  }) {
+    return shareText?.call();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeWhen<TResult extends Object?>({
+    TResult Function()? shareMarkdown,
+    TResult Function()? shareText,
+    TResult Function()? shareLink,
+    required TResult orElse(),
+  }) {
+    if (shareText != null) {
+      return shareText();
+    }
+    return orElse();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult map<TResult extends Object?>({
+    required TResult Function(ShareMarkdown value) shareMarkdown,
+    required TResult Function(ShareText value) shareText,
+    required TResult Function(ShareLink value) shareLink,
+  }) {
+    return shareText(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(ShareMarkdown value)? shareMarkdown,
+    TResult Function(ShareText value)? shareText,
+    TResult Function(ShareLink value)? shareLink,
+  }) {
+    return shareText?.call(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeMap<TResult extends Object?>({
+    TResult Function(ShareMarkdown value)? shareMarkdown,
+    TResult Function(ShareText value)? shareText,
+    TResult Function(ShareLink value)? shareLink,
+    required TResult orElse(),
+  }) {
+    if (shareText != null) {
+      return shareText(this);
+    }
+    return orElse();
+  }
+}
+
+abstract class ShareText implements DocShareEvent {
+  const factory ShareText() = _$ShareText;
+}
+
+/// @nodoc
+abstract class $ShareLinkCopyWith<$Res> {
+  factory $ShareLinkCopyWith(ShareLink value, $Res Function(ShareLink) then) =
+      _$ShareLinkCopyWithImpl<$Res>;
+}
+
+/// @nodoc
+class _$ShareLinkCopyWithImpl<$Res> extends _$DocShareEventCopyWithImpl<$Res>
+    implements $ShareLinkCopyWith<$Res> {
+  _$ShareLinkCopyWithImpl(ShareLink _value, $Res Function(ShareLink) _then)
+      : super(_value, (v) => _then(v as ShareLink));
+
+  @override
+  ShareLink get _value => super._value as ShareLink;
+}
+
+/// @nodoc
+
+class _$ShareLink implements ShareLink {
+  const _$ShareLink();
+
+  @override
+  String toString() {
+    return 'DocShareEvent.shareLink()';
+  }
+
+  @override
+  bool operator ==(dynamic other) {
+    return identical(this, other) || (other is ShareLink);
+  }
+
+  @override
+  int get hashCode => runtimeType.hashCode;
+
+  @override
+  @optionalTypeArgs
+  TResult when<TResult extends Object?>({
+    required TResult Function() shareMarkdown,
+    required TResult Function() shareText,
+    required TResult Function() shareLink,
+  }) {
+    return shareLink();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? shareMarkdown,
+    TResult Function()? shareText,
+    TResult Function()? shareLink,
+  }) {
+    return shareLink?.call();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeWhen<TResult extends Object?>({
+    TResult Function()? shareMarkdown,
+    TResult Function()? shareText,
+    TResult Function()? shareLink,
+    required TResult orElse(),
+  }) {
+    if (shareLink != null) {
+      return shareLink();
+    }
+    return orElse();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult map<TResult extends Object?>({
+    required TResult Function(ShareMarkdown value) shareMarkdown,
+    required TResult Function(ShareText value) shareText,
+    required TResult Function(ShareLink value) shareLink,
+  }) {
+    return shareLink(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(ShareMarkdown value)? shareMarkdown,
+    TResult Function(ShareText value)? shareText,
+    TResult Function(ShareLink value)? shareLink,
+  }) {
+    return shareLink?.call(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeMap<TResult extends Object?>({
+    TResult Function(ShareMarkdown value)? shareMarkdown,
+    TResult Function(ShareText value)? shareText,
+    TResult Function(ShareLink value)? shareLink,
+    required TResult orElse(),
+  }) {
+    if (shareLink != null) {
+      return shareLink(this);
+    }
+    return orElse();
+  }
+}
+
+abstract class ShareLink implements DocShareEvent {
+  const factory ShareLink() = _$ShareLink;
+}
+
+/// @nodoc
+class _$DocShareStateTearOff {
+  const _$DocShareStateTearOff();
+
+  _Initial initial() {
+    return const _Initial();
+  }
+
+  _Loading loading() {
+    return const _Loading();
+  }
+
+  _Finish finish(Either<ExportData, WorkspaceError> successOrFail) {
+    return _Finish(
+      successOrFail,
+    );
+  }
+}
+
+/// @nodoc
+const $DocShareState = _$DocShareStateTearOff();
+
+/// @nodoc
+mixin _$DocShareState {
+  @optionalTypeArgs
+  TResult when<TResult extends Object?>({
+    required TResult Function() initial,
+    required TResult Function() loading,
+    required TResult Function(Either<ExportData, WorkspaceError> successOrFail)
+        finish,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? loading,
+    TResult Function(Either<ExportData, WorkspaceError> successOrFail)? finish,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
+  TResult maybeWhen<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? loading,
+    TResult Function(Either<ExportData, WorkspaceError> successOrFail)? finish,
+    required TResult orElse(),
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
+  TResult map<TResult extends Object?>({
+    required TResult Function(_Initial value) initial,
+    required TResult Function(_Loading value) loading,
+    required TResult Function(_Finish value) finish,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(_Loading value)? loading,
+    TResult Function(_Finish value)? finish,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
+  TResult maybeMap<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(_Loading value)? loading,
+    TResult Function(_Finish value)? finish,
+    required TResult orElse(),
+  }) =>
+      throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $DocShareStateCopyWith<$Res> {
+  factory $DocShareStateCopyWith(
+          DocShareState value, $Res Function(DocShareState) then) =
+      _$DocShareStateCopyWithImpl<$Res>;
+}
+
+/// @nodoc
+class _$DocShareStateCopyWithImpl<$Res>
+    implements $DocShareStateCopyWith<$Res> {
+  _$DocShareStateCopyWithImpl(this._value, this._then);
+
+  final DocShareState _value;
+  // ignore: unused_field
+  final $Res Function(DocShareState) _then;
+}
+
+/// @nodoc
+abstract class _$InitialCopyWith<$Res> {
+  factory _$InitialCopyWith(_Initial value, $Res Function(_Initial) then) =
+      __$InitialCopyWithImpl<$Res>;
+}
+
+/// @nodoc
+class __$InitialCopyWithImpl<$Res> extends _$DocShareStateCopyWithImpl<$Res>
+    implements _$InitialCopyWith<$Res> {
+  __$InitialCopyWithImpl(_Initial _value, $Res Function(_Initial) _then)
+      : super(_value, (v) => _then(v as _Initial));
+
+  @override
+  _Initial get _value => super._value as _Initial;
+}
+
+/// @nodoc
+
+class _$_Initial implements _Initial {
+  const _$_Initial();
+
+  @override
+  String toString() {
+    return 'DocShareState.initial()';
+  }
+
+  @override
+  bool operator ==(dynamic other) {
+    return identical(this, other) || (other is _Initial);
+  }
+
+  @override
+  int get hashCode => runtimeType.hashCode;
+
+  @override
+  @optionalTypeArgs
+  TResult when<TResult extends Object?>({
+    required TResult Function() initial,
+    required TResult Function() loading,
+    required TResult Function(Either<ExportData, WorkspaceError> successOrFail)
+        finish,
+  }) {
+    return initial();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? loading,
+    TResult Function(Either<ExportData, WorkspaceError> successOrFail)? finish,
+  }) {
+    return initial?.call();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeWhen<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? loading,
+    TResult Function(Either<ExportData, WorkspaceError> successOrFail)? finish,
+    required TResult orElse(),
+  }) {
+    if (initial != null) {
+      return initial();
+    }
+    return orElse();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult map<TResult extends Object?>({
+    required TResult Function(_Initial value) initial,
+    required TResult Function(_Loading value) loading,
+    required TResult Function(_Finish value) finish,
+  }) {
+    return initial(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(_Loading value)? loading,
+    TResult Function(_Finish value)? finish,
+  }) {
+    return initial?.call(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeMap<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(_Loading value)? loading,
+    TResult Function(_Finish value)? finish,
+    required TResult orElse(),
+  }) {
+    if (initial != null) {
+      return initial(this);
+    }
+    return orElse();
+  }
+}
+
+abstract class _Initial implements DocShareState {
+  const factory _Initial() = _$_Initial;
+}
+
+/// @nodoc
+abstract class _$LoadingCopyWith<$Res> {
+  factory _$LoadingCopyWith(_Loading value, $Res Function(_Loading) then) =
+      __$LoadingCopyWithImpl<$Res>;
+}
+
+/// @nodoc
+class __$LoadingCopyWithImpl<$Res> extends _$DocShareStateCopyWithImpl<$Res>
+    implements _$LoadingCopyWith<$Res> {
+  __$LoadingCopyWithImpl(_Loading _value, $Res Function(_Loading) _then)
+      : super(_value, (v) => _then(v as _Loading));
+
+  @override
+  _Loading get _value => super._value as _Loading;
+}
+
+/// @nodoc
+
+class _$_Loading implements _Loading {
+  const _$_Loading();
+
+  @override
+  String toString() {
+    return 'DocShareState.loading()';
+  }
+
+  @override
+  bool operator ==(dynamic other) {
+    return identical(this, other) || (other is _Loading);
+  }
+
+  @override
+  int get hashCode => runtimeType.hashCode;
+
+  @override
+  @optionalTypeArgs
+  TResult when<TResult extends Object?>({
+    required TResult Function() initial,
+    required TResult Function() loading,
+    required TResult Function(Either<ExportData, WorkspaceError> successOrFail)
+        finish,
+  }) {
+    return loading();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? loading,
+    TResult Function(Either<ExportData, WorkspaceError> successOrFail)? finish,
+  }) {
+    return loading?.call();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeWhen<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? loading,
+    TResult Function(Either<ExportData, WorkspaceError> successOrFail)? finish,
+    required TResult orElse(),
+  }) {
+    if (loading != null) {
+      return loading();
+    }
+    return orElse();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult map<TResult extends Object?>({
+    required TResult Function(_Initial value) initial,
+    required TResult Function(_Loading value) loading,
+    required TResult Function(_Finish value) finish,
+  }) {
+    return loading(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(_Loading value)? loading,
+    TResult Function(_Finish value)? finish,
+  }) {
+    return loading?.call(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeMap<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(_Loading value)? loading,
+    TResult Function(_Finish value)? finish,
+    required TResult orElse(),
+  }) {
+    if (loading != null) {
+      return loading(this);
+    }
+    return orElse();
+  }
+}
+
+abstract class _Loading implements DocShareState {
+  const factory _Loading() = _$_Loading;
+}
+
+/// @nodoc
+abstract class _$FinishCopyWith<$Res> {
+  factory _$FinishCopyWith(_Finish value, $Res Function(_Finish) then) =
+      __$FinishCopyWithImpl<$Res>;
+  $Res call({Either<ExportData, WorkspaceError> successOrFail});
+}
+
+/// @nodoc
+class __$FinishCopyWithImpl<$Res> extends _$DocShareStateCopyWithImpl<$Res>
+    implements _$FinishCopyWith<$Res> {
+  __$FinishCopyWithImpl(_Finish _value, $Res Function(_Finish) _then)
+      : super(_value, (v) => _then(v as _Finish));
+
+  @override
+  _Finish get _value => super._value as _Finish;
+
+  @override
+  $Res call({
+    Object? successOrFail = freezed,
+  }) {
+    return _then(_Finish(
+      successOrFail == freezed
+          ? _value.successOrFail
+          : successOrFail // ignore: cast_nullable_to_non_nullable
+              as Either<ExportData, WorkspaceError>,
+    ));
+  }
+}
+
+/// @nodoc
+
+class _$_Finish implements _Finish {
+  const _$_Finish(this.successOrFail);
+
+  @override
+  final Either<ExportData, WorkspaceError> successOrFail;
+
+  @override
+  String toString() {
+    return 'DocShareState.finish(successOrFail: $successOrFail)';
+  }
+
+  @override
+  bool operator ==(dynamic other) {
+    return identical(this, other) ||
+        (other is _Finish &&
+            (identical(other.successOrFail, successOrFail) ||
+                const DeepCollectionEquality()
+                    .equals(other.successOrFail, successOrFail)));
+  }
+
+  @override
+  int get hashCode =>
+      runtimeType.hashCode ^ const DeepCollectionEquality().hash(successOrFail);
+
+  @JsonKey(ignore: true)
+  @override
+  _$FinishCopyWith<_Finish> get copyWith =>
+      __$FinishCopyWithImpl<_Finish>(this, _$identity);
+
+  @override
+  @optionalTypeArgs
+  TResult when<TResult extends Object?>({
+    required TResult Function() initial,
+    required TResult Function() loading,
+    required TResult Function(Either<ExportData, WorkspaceError> successOrFail)
+        finish,
+  }) {
+    return finish(successOrFail);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? loading,
+    TResult Function(Either<ExportData, WorkspaceError> successOrFail)? finish,
+  }) {
+    return finish?.call(successOrFail);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeWhen<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? loading,
+    TResult Function(Either<ExportData, WorkspaceError> successOrFail)? finish,
+    required TResult orElse(),
+  }) {
+    if (finish != null) {
+      return finish(successOrFail);
+    }
+    return orElse();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult map<TResult extends Object?>({
+    required TResult Function(_Initial value) initial,
+    required TResult Function(_Loading value) loading,
+    required TResult Function(_Finish value) finish,
+  }) {
+    return finish(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(_Loading value)? loading,
+    TResult Function(_Finish value)? finish,
+  }) {
+    return finish?.call(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeMap<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(_Loading value)? loading,
+    TResult Function(_Finish value)? finish,
+    required TResult orElse(),
+  }) {
+    if (finish != null) {
+      return finish(this);
+    }
+    return orElse();
+  }
+}
+
+abstract class _Finish implements DocShareState {
+  const factory _Finish(Either<ExportData, WorkspaceError> successOrFail) =
+      _$_Finish;
+
+  Either<ExportData, WorkspaceError> get successOrFail =>
+      throw _privateConstructorUsedError;
+  @JsonKey(ignore: true)
+  _$FinishCopyWith<_Finish> get copyWith => throw _privateConstructorUsedError;
+}

+ 49 - 0
app_flowy/lib/workspace/application/edit_pannel/edit_pannel_bloc.freezed.dart

@@ -1,3 +1,4 @@
+// coverage:ignore-file
 // GENERATED CODE - DO NOT MODIFY BY HAND
 // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
 
@@ -43,6 +44,12 @@ mixin _$EditPannelEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function(EditPannelContext context)? startEdit,
+    TResult Function(EditPannelContext context)? endEdit,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
     TResult Function(EditPannelContext context)? startEdit,
     TResult Function(EditPannelContext context)? endEdit,
@@ -56,6 +63,12 @@ mixin _$EditPannelEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_StartEdit value)? startEdit,
+    TResult Function(_EndEdit value)? endEdit,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
     TResult Function(_StartEdit value)? startEdit,
     TResult Function(_EndEdit value)? endEdit,
@@ -169,6 +182,15 @@ class _$_StartEdit implements _StartEdit {
     return startEdit(context);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function(EditPannelContext context)? startEdit,
+    TResult Function(EditPannelContext context)? endEdit,
+  }) {
+    return startEdit?.call(context);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -191,6 +213,15 @@ class _$_StartEdit implements _StartEdit {
     return startEdit(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_StartEdit value)? startEdit,
+    TResult Function(_EndEdit value)? endEdit,
+  }) {
+    return startEdit?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -286,6 +317,15 @@ class _$_EndEdit implements _EndEdit {
     return endEdit(context);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function(EditPannelContext context)? startEdit,
+    TResult Function(EditPannelContext context)? endEdit,
+  }) {
+    return endEdit?.call(context);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -308,6 +348,15 @@ class _$_EndEdit implements _EndEdit {
     return endEdit(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_StartEdit value)? startEdit,
+    TResult Function(_EndEdit value)? endEdit,
+  }) {
+    return endEdit?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({

+ 105 - 0
app_flowy/lib/workspace/application/home/home_bloc.freezed.dart

@@ -1,3 +1,4 @@
+// coverage:ignore-file
 // GENERATED CODE - DO NOT MODIFY BY HAND
 // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
 
@@ -53,6 +54,14 @@ mixin _$HomeEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function(bool isLoading)? showLoading,
+    TResult Function(bool forceCollapse)? forceCollapse,
+    TResult Function(EditPannelContext editContext)? setEditPannel,
+    TResult Function()? dismissEditPannel,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
     TResult Function(bool isLoading)? showLoading,
     TResult Function(bool forceCollapse)? forceCollapse,
@@ -70,6 +79,14 @@ mixin _$HomeEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_ShowLoading value)? showLoading,
+    TResult Function(_ForceCollapse value)? forceCollapse,
+    TResult Function(_ShowEditPannel value)? setEditPannel,
+    TResult Function(_DismissEditPannel value)? dismissEditPannel,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
     TResult Function(_ShowLoading value)? showLoading,
     TResult Function(_ForceCollapse value)? forceCollapse,
@@ -168,6 +185,17 @@ class _$_ShowLoading implements _ShowLoading {
     return showLoading(isLoading);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function(bool isLoading)? showLoading,
+    TResult Function(bool forceCollapse)? forceCollapse,
+    TResult Function(EditPannelContext editContext)? setEditPannel,
+    TResult Function()? dismissEditPannel,
+  }) {
+    return showLoading?.call(isLoading);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -194,6 +222,17 @@ class _$_ShowLoading implements _ShowLoading {
     return showLoading(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_ShowLoading value)? showLoading,
+    TResult Function(_ForceCollapse value)? forceCollapse,
+    TResult Function(_ShowEditPannel value)? setEditPannel,
+    TResult Function(_DismissEditPannel value)? dismissEditPannel,
+  }) {
+    return showLoading?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -292,6 +331,17 @@ class _$_ForceCollapse implements _ForceCollapse {
     return forceCollapse(this.forceCollapse);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function(bool isLoading)? showLoading,
+    TResult Function(bool forceCollapse)? forceCollapse,
+    TResult Function(EditPannelContext editContext)? setEditPannel,
+    TResult Function()? dismissEditPannel,
+  }) {
+    return forceCollapse?.call(this.forceCollapse);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -318,6 +368,17 @@ class _$_ForceCollapse implements _ForceCollapse {
     return forceCollapse(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_ShowLoading value)? showLoading,
+    TResult Function(_ForceCollapse value)? forceCollapse,
+    TResult Function(_ShowEditPannel value)? setEditPannel,
+    TResult Function(_DismissEditPannel value)? dismissEditPannel,
+  }) {
+    return forceCollapse?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -416,6 +477,17 @@ class _$_ShowEditPannel implements _ShowEditPannel {
     return setEditPannel(editContext);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function(bool isLoading)? showLoading,
+    TResult Function(bool forceCollapse)? forceCollapse,
+    TResult Function(EditPannelContext editContext)? setEditPannel,
+    TResult Function()? dismissEditPannel,
+  }) {
+    return setEditPannel?.call(editContext);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -442,6 +514,17 @@ class _$_ShowEditPannel implements _ShowEditPannel {
     return setEditPannel(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_ShowLoading value)? showLoading,
+    TResult Function(_ForceCollapse value)? forceCollapse,
+    TResult Function(_ShowEditPannel value)? setEditPannel,
+    TResult Function(_DismissEditPannel value)? dismissEditPannel,
+  }) {
+    return setEditPannel?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -516,6 +599,17 @@ class _$_DismissEditPannel implements _DismissEditPannel {
     return dismissEditPannel();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function(bool isLoading)? showLoading,
+    TResult Function(bool forceCollapse)? forceCollapse,
+    TResult Function(EditPannelContext editContext)? setEditPannel,
+    TResult Function()? dismissEditPannel,
+  }) {
+    return dismissEditPannel?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -542,6 +636,17 @@ class _$_DismissEditPannel implements _DismissEditPannel {
     return dismissEditPannel(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_ShowLoading value)? showLoading,
+    TResult Function(_ForceCollapse value)? forceCollapse,
+    TResult Function(_ShowEditPannel value)? setEditPannel,
+    TResult Function(_DismissEditPannel value)? dismissEditPannel,
+  }) {
+    return dismissEditPannel?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({

+ 123 - 0
app_flowy/lib/workspace/application/home/home_listen_bloc.freezed.dart

@@ -1,3 +1,4 @@
+// coverage:ignore-file
 // GENERATED CODE - DO NOT MODIFY BY HAND
 // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
 
@@ -44,6 +45,13 @@ mixin _$HomeListenEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? started,
+    TResult Function()? stop,
+    TResult Function(String msg)? unauthorized,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? started,
     TResult Function()? stop,
@@ -59,6 +67,13 @@ mixin _$HomeListenEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Started value)? started,
+    TResult Function(_Stop value)? stop,
+    TResult Function(_Unauthorized value)? unauthorized,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
     TResult Function(_Started value)? started,
     TResult Function(_Stop value)? stop,
@@ -129,6 +144,16 @@ class _$_Started implements _Started {
     return started();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? started,
+    TResult Function()? stop,
+    TResult Function(String msg)? unauthorized,
+  }) {
+    return started?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -153,6 +178,16 @@ class _$_Started implements _Started {
     return started(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Started value)? started,
+    TResult Function(_Stop value)? stop,
+    TResult Function(_Unauthorized value)? unauthorized,
+  }) {
+    return started?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -216,6 +251,16 @@ class _$_Stop implements _Stop {
     return stop();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? started,
+    TResult Function()? stop,
+    TResult Function(String msg)? unauthorized,
+  }) {
+    return stop?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -240,6 +285,16 @@ class _$_Stop implements _Stop {
     return stop(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Started value)? started,
+    TResult Function(_Stop value)? stop,
+    TResult Function(_Unauthorized value)? unauthorized,
+  }) {
+    return stop?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -331,6 +386,16 @@ class _$_Unauthorized implements _Unauthorized {
     return unauthorized(msg);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? started,
+    TResult Function()? stop,
+    TResult Function(String msg)? unauthorized,
+  }) {
+    return unauthorized?.call(msg);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -355,6 +420,16 @@ class _$_Unauthorized implements _Unauthorized {
     return unauthorized(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Started value)? started,
+    TResult Function(_Stop value)? stop,
+    TResult Function(_Unauthorized value)? unauthorized,
+  }) {
+    return unauthorized?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -406,6 +481,12 @@ mixin _$HomeListenState {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? loading,
+    TResult Function(String msg)? unauthorized,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? loading,
     TResult Function(String msg)? unauthorized,
@@ -419,6 +500,12 @@ mixin _$HomeListenState {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Loading value)? loading,
+    TResult Function(Unauthorized value)? unauthorized,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
     TResult Function(Loading value)? loading,
     TResult Function(Unauthorized value)? unauthorized,
@@ -487,6 +574,15 @@ class _$Loading implements Loading {
     return loading();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? loading,
+    TResult Function(String msg)? unauthorized,
+  }) {
+    return loading?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -509,6 +605,15 @@ class _$Loading implements Loading {
     return loading(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Loading value)? loading,
+    TResult Function(Unauthorized value)? unauthorized,
+  }) {
+    return loading?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -598,6 +703,15 @@ class _$Unauthorized implements Unauthorized {
     return unauthorized(msg);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? loading,
+    TResult Function(String msg)? unauthorized,
+  }) {
+    return unauthorized?.call(msg);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -620,6 +734,15 @@ class _$Unauthorized implements Unauthorized {
     return unauthorized(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Loading value)? loading,
+    TResult Function(Unauthorized value)? unauthorized,
+  }) {
+    return unauthorized?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({

+ 145 - 0
app_flowy/lib/workspace/application/menu/menu_bloc.freezed.dart

@@ -1,3 +1,4 @@
+// coverage:ignore-file
 // GENERATED CODE - DO NOT MODIFY BY HAND
 // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
 
@@ -61,6 +62,16 @@ mixin _$MenuEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? collapse,
+    TResult Function(HomeStackContext<dynamic, dynamic> context)? openPage,
+    TResult Function(String name, String? desc)? createApp,
+    TResult Function(Either<List<App>, WorkspaceError> appsOrFail)?
+        didReceiveApps,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? initial,
     TResult Function()? collapse,
@@ -81,6 +92,15 @@ mixin _$MenuEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(Collapse value)? collapse,
+    TResult Function(OpenPage value)? openPage,
+    TResult Function(CreateApp value)? createApp,
+    TResult Function(ReceiveApps value)? didReceiveApps,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
     TResult Function(_Initial value)? initial,
     TResult Function(Collapse value)? collapse,
@@ -155,6 +175,19 @@ class _$_Initial implements _Initial {
     return initial();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? collapse,
+    TResult Function(HomeStackContext<dynamic, dynamic> context)? openPage,
+    TResult Function(String name, String? desc)? createApp,
+    TResult Function(Either<List<App>, WorkspaceError> appsOrFail)?
+        didReceiveApps,
+  }) {
+    return initial?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -184,6 +217,18 @@ class _$_Initial implements _Initial {
     return initial(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(Collapse value)? collapse,
+    TResult Function(OpenPage value)? openPage,
+    TResult Function(CreateApp value)? createApp,
+    TResult Function(ReceiveApps value)? didReceiveApps,
+  }) {
+    return initial?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -253,6 +298,19 @@ class _$Collapse implements Collapse {
     return collapse();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? collapse,
+    TResult Function(HomeStackContext<dynamic, dynamic> context)? openPage,
+    TResult Function(String name, String? desc)? createApp,
+    TResult Function(Either<List<App>, WorkspaceError> appsOrFail)?
+        didReceiveApps,
+  }) {
+    return collapse?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -282,6 +340,18 @@ class _$Collapse implements Collapse {
     return collapse(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(Collapse value)? collapse,
+    TResult Function(OpenPage value)? openPage,
+    TResult Function(CreateApp value)? createApp,
+    TResult Function(ReceiveApps value)? didReceiveApps,
+  }) {
+    return collapse?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -376,6 +446,19 @@ class _$OpenPage implements OpenPage {
     return openPage(context);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? collapse,
+    TResult Function(HomeStackContext<dynamic, dynamic> context)? openPage,
+    TResult Function(String name, String? desc)? createApp,
+    TResult Function(Either<List<App>, WorkspaceError> appsOrFail)?
+        didReceiveApps,
+  }) {
+    return openPage?.call(context);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -405,6 +488,18 @@ class _$OpenPage implements OpenPage {
     return openPage(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(Collapse value)? collapse,
+    TResult Function(OpenPage value)? openPage,
+    TResult Function(CreateApp value)? createApp,
+    TResult Function(ReceiveApps value)? didReceiveApps,
+  }) {
+    return openPage?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -517,6 +612,19 @@ class _$CreateApp implements CreateApp {
     return createApp(name, desc);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? collapse,
+    TResult Function(HomeStackContext<dynamic, dynamic> context)? openPage,
+    TResult Function(String name, String? desc)? createApp,
+    TResult Function(Either<List<App>, WorkspaceError> appsOrFail)?
+        didReceiveApps,
+  }) {
+    return createApp?.call(name, desc);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -546,6 +654,18 @@ class _$CreateApp implements CreateApp {
     return createApp(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(Collapse value)? collapse,
+    TResult Function(OpenPage value)? openPage,
+    TResult Function(CreateApp value)? createApp,
+    TResult Function(ReceiveApps value)? didReceiveApps,
+  }) {
+    return createApp?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -649,6 +769,19 @@ class _$ReceiveApps implements ReceiveApps {
     return didReceiveApps(appsOrFail);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? collapse,
+    TResult Function(HomeStackContext<dynamic, dynamic> context)? openPage,
+    TResult Function(String name, String? desc)? createApp,
+    TResult Function(Either<List<App>, WorkspaceError> appsOrFail)?
+        didReceiveApps,
+  }) {
+    return didReceiveApps?.call(appsOrFail);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -678,6 +811,18 @@ class _$ReceiveApps implements ReceiveApps {
     return didReceiveApps(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(Collapse value)? collapse,
+    TResult Function(OpenPage value)? openPage,
+    TResult Function(CreateApp value)? createApp,
+    TResult Function(ReceiveApps value)? didReceiveApps,
+  }) {
+    return didReceiveApps?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({

+ 49 - 0
app_flowy/lib/workspace/application/menu/menu_user_bloc.freezed.dart

@@ -1,3 +1,4 @@
+// coverage:ignore-file
 // GENERATED CODE - DO NOT MODIFY BY HAND
 // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
 
@@ -37,6 +38,12 @@ mixin _$MenuUserEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? fetchWorkspaces,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? initial,
     TResult Function()? fetchWorkspaces,
@@ -50,6 +57,12 @@ mixin _$MenuUserEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(_FetchWorkspaces value)? fetchWorkspaces,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
     TResult Function(_Initial value)? initial,
     TResult Function(_FetchWorkspaces value)? fetchWorkspaces,
@@ -118,6 +131,15 @@ class _$_Initial implements _Initial {
     return initial();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? fetchWorkspaces,
+  }) {
+    return initial?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -140,6 +162,15 @@ class _$_Initial implements _Initial {
     return initial(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(_FetchWorkspaces value)? fetchWorkspaces,
+  }) {
+    return initial?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -204,6 +235,15 @@ class _$_FetchWorkspaces implements _FetchWorkspaces {
     return fetchWorkspaces();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? fetchWorkspaces,
+  }) {
+    return fetchWorkspaces?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -226,6 +266,15 @@ class _$_FetchWorkspaces implements _FetchWorkspaces {
     return fetchWorkspaces(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(_FetchWorkspaces value)? fetchWorkspaces,
+  }) {
+    return fetchWorkspaces?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({

+ 177 - 0
app_flowy/lib/workspace/application/trash/trash_bloc.freezed.dart

@@ -1,3 +1,4 @@
+// coverage:ignore-file
 // GENERATED CODE - DO NOT MODIFY BY HAND
 // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
 
@@ -63,6 +64,16 @@ mixin _$TrashEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(List<Trash> trash)? didReceiveTrash,
+    TResult Function(String trashId)? putback,
+    TResult Function(Trash trash)? delete,
+    TResult Function()? restoreAll,
+    TResult Function()? deleteAll,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? initial,
     TResult Function(List<Trash> trash)? didReceiveTrash,
@@ -84,6 +95,16 @@ mixin _$TrashEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(ReceiveTrash value)? didReceiveTrash,
+    TResult Function(Putback value)? putback,
+    TResult Function(Delete value)? delete,
+    TResult Function(RestoreAll value)? restoreAll,
+    TResult Function(DeleteAll value)? deleteAll,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
     TResult Function(Initial value)? initial,
     TResult Function(ReceiveTrash value)? didReceiveTrash,
@@ -159,6 +180,19 @@ class _$Initial implements Initial {
     return initial();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(List<Trash> trash)? didReceiveTrash,
+    TResult Function(String trashId)? putback,
+    TResult Function(Trash trash)? delete,
+    TResult Function()? restoreAll,
+    TResult Function()? deleteAll,
+  }) {
+    return initial?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -189,6 +223,19 @@ class _$Initial implements Initial {
     return initial(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(ReceiveTrash value)? didReceiveTrash,
+    TResult Function(Putback value)? putback,
+    TResult Function(Delete value)? delete,
+    TResult Function(RestoreAll value)? restoreAll,
+    TResult Function(DeleteAll value)? deleteAll,
+  }) {
+    return initial?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -285,6 +332,19 @@ class _$ReceiveTrash implements ReceiveTrash {
     return didReceiveTrash(trash);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(List<Trash> trash)? didReceiveTrash,
+    TResult Function(String trashId)? putback,
+    TResult Function(Trash trash)? delete,
+    TResult Function()? restoreAll,
+    TResult Function()? deleteAll,
+  }) {
+    return didReceiveTrash?.call(trash);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -315,6 +375,19 @@ class _$ReceiveTrash implements ReceiveTrash {
     return didReceiveTrash(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(ReceiveTrash value)? didReceiveTrash,
+    TResult Function(Putback value)? putback,
+    TResult Function(Delete value)? delete,
+    TResult Function(RestoreAll value)? restoreAll,
+    TResult Function(DeleteAll value)? deleteAll,
+  }) {
+    return didReceiveTrash?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -414,6 +487,19 @@ class _$Putback implements Putback {
     return putback(trashId);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(List<Trash> trash)? didReceiveTrash,
+    TResult Function(String trashId)? putback,
+    TResult Function(Trash trash)? delete,
+    TResult Function()? restoreAll,
+    TResult Function()? deleteAll,
+  }) {
+    return putback?.call(trashId);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -444,6 +530,19 @@ class _$Putback implements Putback {
     return putback(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(ReceiveTrash value)? didReceiveTrash,
+    TResult Function(Putback value)? putback,
+    TResult Function(Delete value)? delete,
+    TResult Function(RestoreAll value)? restoreAll,
+    TResult Function(DeleteAll value)? deleteAll,
+  }) {
+    return putback?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -542,6 +641,19 @@ class _$Delete implements Delete {
     return delete(trash);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(List<Trash> trash)? didReceiveTrash,
+    TResult Function(String trashId)? putback,
+    TResult Function(Trash trash)? delete,
+    TResult Function()? restoreAll,
+    TResult Function()? deleteAll,
+  }) {
+    return delete?.call(trash);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -572,6 +684,19 @@ class _$Delete implements Delete {
     return delete(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(ReceiveTrash value)? didReceiveTrash,
+    TResult Function(Putback value)? putback,
+    TResult Function(Delete value)? delete,
+    TResult Function(RestoreAll value)? restoreAll,
+    TResult Function(DeleteAll value)? deleteAll,
+  }) {
+    return delete?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -646,6 +771,19 @@ class _$RestoreAll implements RestoreAll {
     return restoreAll();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(List<Trash> trash)? didReceiveTrash,
+    TResult Function(String trashId)? putback,
+    TResult Function(Trash trash)? delete,
+    TResult Function()? restoreAll,
+    TResult Function()? deleteAll,
+  }) {
+    return restoreAll?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -676,6 +814,19 @@ class _$RestoreAll implements RestoreAll {
     return restoreAll(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(ReceiveTrash value)? didReceiveTrash,
+    TResult Function(Putback value)? putback,
+    TResult Function(Delete value)? delete,
+    TResult Function(RestoreAll value)? restoreAll,
+    TResult Function(DeleteAll value)? deleteAll,
+  }) {
+    return restoreAll?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -745,6 +896,19 @@ class _$DeleteAll implements DeleteAll {
     return deleteAll();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(List<Trash> trash)? didReceiveTrash,
+    TResult Function(String trashId)? putback,
+    TResult Function(Trash trash)? delete,
+    TResult Function()? restoreAll,
+    TResult Function()? deleteAll,
+  }) {
+    return deleteAll?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -775,6 +939,19 @@ class _$DeleteAll implements DeleteAll {
     return deleteAll(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(ReceiveTrash value)? didReceiveTrash,
+    TResult Function(Putback value)? putback,
+    TResult Function(Delete value)? delete,
+    TResult Function(RestoreAll value)? restoreAll,
+    TResult Function(DeleteAll value)? deleteAll,
+  }) {
+    return deleteAll?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({

+ 177 - 0
app_flowy/lib/workspace/application/view/view_bloc.freezed.dart

@@ -1,3 +1,4 @@
+// coverage:ignore-file
 // GENERATED CODE - DO NOT MODIFY BY HAND
 // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
 
@@ -64,6 +65,16 @@ mixin _$ViewEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(bool isEditing)? setIsEditing,
+    TResult Function(String newName)? rename,
+    TResult Function()? delete,
+    TResult Function()? duplicate,
+    TResult Function(Either<View, WorkspaceError> result)? viewDidUpdate,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? initial,
     TResult Function(bool isEditing)? setIsEditing,
@@ -85,6 +96,16 @@ mixin _$ViewEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(SetEditing value)? setIsEditing,
+    TResult Function(Rename value)? rename,
+    TResult Function(Delete value)? delete,
+    TResult Function(Duplicate value)? duplicate,
+    TResult Function(ViewDidUpdate value)? viewDidUpdate,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
     TResult Function(Initial value)? initial,
     TResult Function(SetEditing value)? setIsEditing,
@@ -160,6 +181,19 @@ class _$Initial implements Initial {
     return initial();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(bool isEditing)? setIsEditing,
+    TResult Function(String newName)? rename,
+    TResult Function()? delete,
+    TResult Function()? duplicate,
+    TResult Function(Either<View, WorkspaceError> result)? viewDidUpdate,
+  }) {
+    return initial?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -190,6 +224,19 @@ class _$Initial implements Initial {
     return initial(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(SetEditing value)? setIsEditing,
+    TResult Function(Rename value)? rename,
+    TResult Function(Delete value)? delete,
+    TResult Function(Duplicate value)? duplicate,
+    TResult Function(ViewDidUpdate value)? viewDidUpdate,
+  }) {
+    return initial?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -287,6 +334,19 @@ class _$SetEditing implements SetEditing {
     return setIsEditing(isEditing);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(bool isEditing)? setIsEditing,
+    TResult Function(String newName)? rename,
+    TResult Function()? delete,
+    TResult Function()? duplicate,
+    TResult Function(Either<View, WorkspaceError> result)? viewDidUpdate,
+  }) {
+    return setIsEditing?.call(isEditing);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -317,6 +377,19 @@ class _$SetEditing implements SetEditing {
     return setIsEditing(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(SetEditing value)? setIsEditing,
+    TResult Function(Rename value)? rename,
+    TResult Function(Delete value)? delete,
+    TResult Function(Duplicate value)? duplicate,
+    TResult Function(ViewDidUpdate value)? viewDidUpdate,
+  }) {
+    return setIsEditing?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -417,6 +490,19 @@ class _$Rename implements Rename {
     return rename(newName);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(bool isEditing)? setIsEditing,
+    TResult Function(String newName)? rename,
+    TResult Function()? delete,
+    TResult Function()? duplicate,
+    TResult Function(Either<View, WorkspaceError> result)? viewDidUpdate,
+  }) {
+    return rename?.call(newName);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -447,6 +533,19 @@ class _$Rename implements Rename {
     return rename(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(SetEditing value)? setIsEditing,
+    TResult Function(Rename value)? rename,
+    TResult Function(Delete value)? delete,
+    TResult Function(Duplicate value)? duplicate,
+    TResult Function(ViewDidUpdate value)? viewDidUpdate,
+  }) {
+    return rename?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -521,6 +620,19 @@ class _$Delete implements Delete {
     return delete();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(bool isEditing)? setIsEditing,
+    TResult Function(String newName)? rename,
+    TResult Function()? delete,
+    TResult Function()? duplicate,
+    TResult Function(Either<View, WorkspaceError> result)? viewDidUpdate,
+  }) {
+    return delete?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -551,6 +663,19 @@ class _$Delete implements Delete {
     return delete(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(SetEditing value)? setIsEditing,
+    TResult Function(Rename value)? rename,
+    TResult Function(Delete value)? delete,
+    TResult Function(Duplicate value)? duplicate,
+    TResult Function(ViewDidUpdate value)? viewDidUpdate,
+  }) {
+    return delete?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -621,6 +746,19 @@ class _$Duplicate implements Duplicate {
     return duplicate();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(bool isEditing)? setIsEditing,
+    TResult Function(String newName)? rename,
+    TResult Function()? delete,
+    TResult Function()? duplicate,
+    TResult Function(Either<View, WorkspaceError> result)? viewDidUpdate,
+  }) {
+    return duplicate?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -651,6 +789,19 @@ class _$Duplicate implements Duplicate {
     return duplicate(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(SetEditing value)? setIsEditing,
+    TResult Function(Rename value)? rename,
+    TResult Function(Delete value)? delete,
+    TResult Function(Duplicate value)? duplicate,
+    TResult Function(ViewDidUpdate value)? viewDidUpdate,
+  }) {
+    return duplicate?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -748,6 +899,19 @@ class _$ViewDidUpdate implements ViewDidUpdate {
     return viewDidUpdate(result);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(bool isEditing)? setIsEditing,
+    TResult Function(String newName)? rename,
+    TResult Function()? delete,
+    TResult Function()? duplicate,
+    TResult Function(Either<View, WorkspaceError> result)? viewDidUpdate,
+  }) {
+    return viewDidUpdate?.call(result);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -778,6 +942,19 @@ class _$ViewDidUpdate implements ViewDidUpdate {
     return viewDidUpdate(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(SetEditing value)? setIsEditing,
+    TResult Function(Rename value)? rename,
+    TResult Function(Delete value)? delete,
+    TResult Function(Duplicate value)? duplicate,
+    TResult Function(ViewDidUpdate value)? viewDidUpdate,
+  }) {
+    return viewDidUpdate?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({

+ 110 - 0
app_flowy/lib/workspace/application/workspace/welcome_bloc.freezed.dart

@@ -1,3 +1,4 @@
+// coverage:ignore-file
 // GENERATED CODE - DO NOT MODIFY BY HAND
 // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
 
@@ -57,6 +58,15 @@ mixin _$WelcomeEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(String name, String desc)? createWorkspace,
+    TResult Function(Workspace workspace)? openWorkspace,
+    TResult Function(Either<List<Workspace>, WorkspaceError> workspacesOrFail)?
+        workspacesReveived,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? initial,
     TResult Function(String name, String desc)? createWorkspace,
@@ -75,6 +85,14 @@ mixin _$WelcomeEvent {
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(CreateWorkspace value)? createWorkspace,
+    TResult Function(OpenWorkspace value)? openWorkspace,
+    TResult Function(WorkspacesReceived value)? workspacesReveived,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
     TResult Function(Initial value)? initial,
     TResult Function(CreateWorkspace value)? createWorkspace,
@@ -148,6 +166,18 @@ class _$Initial implements Initial {
     return initial();
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(String name, String desc)? createWorkspace,
+    TResult Function(Workspace workspace)? openWorkspace,
+    TResult Function(Either<List<Workspace>, WorkspaceError> workspacesOrFail)?
+        workspacesReveived,
+  }) {
+    return initial?.call();
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -175,6 +205,17 @@ class _$Initial implements Initial {
     return initial(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(CreateWorkspace value)? createWorkspace,
+    TResult Function(OpenWorkspace value)? openWorkspace,
+    TResult Function(WorkspacesReceived value)? workspacesReveived,
+  }) {
+    return initial?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -281,6 +322,18 @@ class _$CreateWorkspace implements CreateWorkspace {
     return createWorkspace(name, desc);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(String name, String desc)? createWorkspace,
+    TResult Function(Workspace workspace)? openWorkspace,
+    TResult Function(Either<List<Workspace>, WorkspaceError> workspacesOrFail)?
+        workspacesReveived,
+  }) {
+    return createWorkspace?.call(name, desc);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -308,6 +361,17 @@ class _$CreateWorkspace implements CreateWorkspace {
     return createWorkspace(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(CreateWorkspace value)? createWorkspace,
+    TResult Function(OpenWorkspace value)? openWorkspace,
+    TResult Function(WorkspacesReceived value)? workspacesReveived,
+  }) {
+    return createWorkspace?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -409,6 +473,18 @@ class _$OpenWorkspace implements OpenWorkspace {
     return openWorkspace(workspace);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(String name, String desc)? createWorkspace,
+    TResult Function(Workspace workspace)? openWorkspace,
+    TResult Function(Either<List<Workspace>, WorkspaceError> workspacesOrFail)?
+        workspacesReveived,
+  }) {
+    return openWorkspace?.call(workspace);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -436,6 +512,17 @@ class _$OpenWorkspace implements OpenWorkspace {
     return openWorkspace(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(CreateWorkspace value)? createWorkspace,
+    TResult Function(OpenWorkspace value)? openWorkspace,
+    TResult Function(WorkspacesReceived value)? workspacesReveived,
+  }) {
+    return openWorkspace?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
@@ -538,6 +625,18 @@ class _$WorkspacesReceived implements WorkspacesReceived {
     return workspacesReveived(workspacesOrFail);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(String name, String desc)? createWorkspace,
+    TResult Function(Workspace workspace)? openWorkspace,
+    TResult Function(Either<List<Workspace>, WorkspaceError> workspacesOrFail)?
+        workspacesReveived,
+  }) {
+    return workspacesReveived?.call(workspacesOrFail);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
@@ -565,6 +664,17 @@ class _$WorkspacesReceived implements WorkspacesReceived {
     return workspacesReveived(this);
   }
 
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(CreateWorkspace value)? createWorkspace,
+    TResult Function(OpenWorkspace value)? openWorkspace,
+    TResult Function(WorkspacesReceived value)? workspacesReveived,
+  }) {
+    return workspacesReveived?.call(this);
+  }
+
   @override
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({

+ 12 - 0
app_flowy/lib/workspace/domain/i_share.dart

@@ -0,0 +1,12 @@
+import 'dart:async';
+import 'package:dartz/dartz.dart';
+import 'package:flowy_sdk/protobuf/flowy-workspace-infra/protobuf.dart';
+import 'package:flowy_sdk/protobuf/flowy-workspace/errors.pb.dart';
+
+abstract class IShare {
+  Future<Either<ExportData, WorkspaceError>> exportText(String docId);
+
+  Future<Either<ExportData, WorkspaceError>> exportMarkdown(String docId);
+
+  Future<Either<ExportData, WorkspaceError>> exportURL(String docId);
+}

+ 7 - 5
app_flowy/lib/workspace/domain/page_stack/page_stack.dart

@@ -9,7 +9,8 @@ import 'package:app_flowy/workspace/presentation/widgets/prelude.dart';
 typedef NavigationCallback = void Function(String id);
 
 abstract class NavigationItem {
-  Widget get naviTitle;
+  Widget get leftBarItem;
+  Widget? get rightBarItem => null;
   String get identifier;
 
   NavigationCallback get action => (id) {
@@ -29,9 +30,10 @@ abstract class HomeStackContext<T, S> with NavigationItem {
   List<NavigationItem> get navigationItems;
 
   @override
-  Widget get naviTitle;
+  Widget get leftBarItem;
 
-  Widget? Function(BuildContext context) get buildNaviAction;
+  @override
+  Widget? get rightBarItem;
 
   @override
   String get identifier;
@@ -49,7 +51,7 @@ class HomeStackNotifier extends ChangeNotifier {
   HomeStackContext stackContext;
   PublishNotifier<bool> collapsedNotifier = PublishNotifier();
 
-  Widget get titleWidget => stackContext.naviTitle;
+  Widget get titleWidget => stackContext.leftBarItem;
 
   HomeStackNotifier({HomeStackContext? context}) : stackContext = context ?? BlankStackContext();
 
@@ -71,7 +73,7 @@ class HomeStackManager {
   HomeStackManager();
 
   Widget title() {
-    return _notifier.context.naviTitle;
+    return _notifier.context.leftBarItem;
   }
 
   PublishNotifier<bool> get collapsedNotifier => _notifier.collapsedNotifier;

+ 10 - 0
app_flowy/lib/workspace/infrastructure/deps_resolver.dart

@@ -1,11 +1,13 @@
 import 'package:app_flowy/workspace/application/app/app_bloc.dart';
 import 'package:app_flowy/workspace/application/doc/doc_bloc.dart';
+import 'package:app_flowy/workspace/application/doc/share_bloc.dart';
 import 'package:app_flowy/workspace/application/menu/menu_bloc.dart';
 import 'package:app_flowy/workspace/application/menu/menu_user_bloc.dart';
 import 'package:app_flowy/workspace/application/trash/trash_bloc.dart';
 import 'package:app_flowy/workspace/application/view/view_bloc.dart';
 import 'package:app_flowy/workspace/application/workspace/welcome_bloc.dart';
 import 'package:app_flowy/workspace/domain/i_doc.dart';
+import 'package:app_flowy/workspace/domain/i_share.dart';
 import 'package:app_flowy/workspace/domain/i_trash.dart';
 import 'package:app_flowy/workspace/domain/i_view.dart';
 import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
@@ -23,8 +25,10 @@ import 'package:flowy_sdk/protobuf/flowy-workspace-infra/app_create.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-workspace-infra/view_create.pb.dart';
 import 'package:get_it/get_it.dart';
 
+import 'i_share_impl.dart';
 import 'i_user_impl.dart';
 import 'i_view_impl.dart';
+import 'repos/share_repo.dart';
 
 class HomeDepsResolver {
   static Future<void> resolve(GetIt getIt) async {
@@ -102,5 +106,11 @@ class HomeDepsResolver {
     getIt.registerFactory<ITrash>(() => ITrashImpl(repo: getIt<TrashRepo>()));
     getIt.registerFactory<ITrashListener>(() => ITrashListenerImpl(repo: getIt<TrashListenerRepo>()));
     getIt.registerFactory<TrashBloc>(() => TrashBloc(trasnManager: getIt<ITrash>(), listener: getIt<ITrashListener>()));
+
+    // share
+    getIt.registerLazySingleton<ShareRepo>(() => ShareRepo());
+    getIt.registerFactory<IShare>(() => IShareImpl(repo: getIt<ShareRepo>()));
+    getIt.registerFactoryParam<DocShareBloc, View, void>(
+        (view, _) => DocShareBloc(view: view, shareManager: getIt<IShare>()));
   }
 }

+ 27 - 0
app_flowy/lib/workspace/infrastructure/i_share_impl.dart

@@ -0,0 +1,27 @@
+import 'package:app_flowy/workspace/domain/i_share.dart';
+import 'package:flowy_sdk/protobuf/flowy-workspace-infra/protobuf.dart';
+import 'package:flowy_sdk/protobuf/flowy-workspace/errors.pb.dart';
+import 'package:dartz/dartz.dart';
+
+import 'repos/share_repo.dart';
+
+class IShareImpl extends IShare {
+  ShareRepo repo;
+
+  IShareImpl({required this.repo});
+
+  @override
+  Future<Either<ExportData, WorkspaceError>> exportText(String docId) {
+    return repo.export(docId, ExportType.Text);
+  }
+
+  @override
+  Future<Either<ExportData, WorkspaceError>> exportMarkdown(String docId) {
+    return repo.export(docId, ExportType.Markdown);
+  }
+
+  @override
+  Future<Either<ExportData, WorkspaceError>> exportURL(String docId) {
+    return repo.export(docId, ExportType.Link);
+  }
+}

+ 30 - 0
app_flowy/lib/workspace/infrastructure/markdown/delta_markdown.dart

@@ -0,0 +1,30 @@
+library delta_markdown;
+
+import 'dart:convert';
+
+import 'src/delta_markdown_decoder.dart';
+import 'src/delta_markdown_encoder.dart';
+import 'src/version.dart';
+
+const version = packageVersion;
+
+/// Codec used to convert between Markdown and Quill deltas.
+const DeltaMarkdownCodec _kCodec = DeltaMarkdownCodec();
+
+String markdownToDelta(String markdown) {
+  return _kCodec.decode(markdown);
+}
+
+String deltaToMarkdown(String delta) {
+  return _kCodec.encode(delta);
+}
+
+class DeltaMarkdownCodec extends Codec<String, String> {
+  const DeltaMarkdownCodec();
+
+  @override
+  Converter<String, String> get decoder => DeltaMarkdownDecoder();
+
+  @override
+  Converter<String, String> get encoder => DeltaMarkdownEncoder();
+}

+ 113 - 0
app_flowy/lib/workspace/infrastructure/markdown/src/ast.dart

@@ -0,0 +1,113 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+typedef Resolver = Node? Function(String name, [String? title]);
+
+/// Base class for any AST item.
+///
+/// Roughly corresponds to Node in the DOM. Will be either an Element or Text.
+class Node {
+  void accept(NodeVisitor visitor) {}
+
+  bool isToplevel = false;
+
+  String? get textContent {
+    return null;
+  }
+}
+
+/// A named tag that can contain other nodes.
+class Element extends Node {
+  /// Instantiates a [tag] Element with [children].
+  Element(this.tag, this.children) : attributes = <String, String>{};
+
+  /// Instantiates an empty, self-closing [tag] Element.
+  Element.empty(this.tag)
+      : children = null,
+        attributes = {};
+
+  /// Instantiates a [tag] Element with no [children].
+  Element.withTag(this.tag)
+      : children = [],
+        attributes = {};
+
+  /// Instantiates a [tag] Element with a single Text child.
+  Element.text(this.tag, String text)
+      : children = [Text(text)],
+        attributes = {};
+
+  final String tag;
+  final List<Node>? children;
+  final Map<String, String> attributes;
+  String? generatedId;
+
+  /// Whether this element is self-closing.
+  bool get isEmpty => children == null;
+
+  @override
+  void accept(NodeVisitor visitor) {
+    if (visitor.visitElementBefore(this)) {
+      if (children != null) {
+        for (final child in children!) {
+          child.accept(visitor);
+        }
+      }
+      visitor.visitElementAfter(this);
+    }
+  }
+
+  @override
+  String get textContent => children == null
+      ? ''
+      : children!.map((child) => child.textContent).join();
+}
+
+/// A plain text element.
+class Text extends Node {
+  Text(this.text);
+
+  final String text;
+
+  @override
+  void accept(NodeVisitor visitor) => visitor.visitText(this);
+
+  @override
+  String get textContent => text;
+}
+
+/// Inline content that has not been parsed into inline nodes (strong, links,
+/// etc).
+///
+/// These placeholder nodes should only remain in place while the block nodes
+/// of a document are still being parsed, in order to gather all reference link
+/// definitions.
+class UnparsedContent extends Node {
+  UnparsedContent(this.textContent);
+
+  @override
+  final String textContent;
+
+  @override
+  void accept(NodeVisitor visitor);
+}
+
+/// Visitor pattern for the AST.
+///
+/// Renderers or other AST transformers should implement this.
+abstract class NodeVisitor {
+  /// Called when a Text node has been reached.
+  void visitText(Text text);
+
+  /// Called when an Element has been reached, before its children have been
+  /// visited.
+  ///
+  /// Returns `false` to skip its children.
+  bool visitElementBefore(Element element);
+
+  /// Called when an Element has been reached, after its children have been
+  /// visited.
+  ///
+  /// Will not be called if [visitElementBefore] returns `false`.
+  void visitElementAfter(Element element);
+}

+ 1096 - 0
app_flowy/lib/workspace/infrastructure/markdown/src/block_parser.dart

@@ -0,0 +1,1096 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'ast.dart';
+import 'document.dart';
+import 'util.dart';
+
+/// The line contains only whitespace or is empty.
+final _emptyPattern = RegExp(r'^(?:[ \t]*)$');
+
+/// A series of `=` or `-` (on the next line) define setext-style headers.
+final _setextPattern = RegExp(r'^[ ]{0,3}(=+|-+)\s*$');
+
+/// Leading (and trailing) `#` define atx-style headers.
+///
+/// Starts with 1-6 unescaped `#` characters which must not be followed by a
+/// non-space character. Line may end with any number of `#` characters,.
+final _headerPattern = RegExp(r'^ {0,3}(#{1,6})[ \x09\x0b\x0c](.*?)#*$');
+
+/// The line starts with `>` with one optional space after.
+final _blockquotePattern = RegExp(r'^[ ]{0,3}>[ ]?(.*)$');
+
+/// A line indented four spaces. Used for code blocks and lists.
+final _indentPattern = RegExp(r'^(?:    | {0,3}\t)(.*)$');
+
+/// Fenced code block.
+final _codePattern = RegExp(r'^[ ]{0,3}(`{3,}|~{3,})(.*)$');
+
+/// Three or more hyphens, asterisks or underscores by themselves. Note that
+/// a line like `----` is valid as both HR and SETEXT. In case of a tie,
+/// SETEXT should win.
+final _hrPattern = RegExp(r'^ {0,3}([-*_])[ \t]*\1[ \t]*\1(?:\1|[ \t])*$');
+
+/// One or more whitespace, for compressing.
+final _oneOrMoreWhitespacePattern = RegExp('[ \n\r\t]+');
+
+/// A line starting with one of these markers: `-`, `*`, `+`. May have up to
+/// three leading spaces before the marker and any number of spaces or tabs
+/// after.
+///
+/// Contains a dummy group at [2], so that the groups in [_ulPattern] and
+/// [_olPattern] match up; in both, [2] is the length of the number that begins
+/// the list marker.
+final _ulPattern = RegExp(r'^([ ]{0,3})()([*+-])(([ \t])([ \t]*)(.*))?$');
+
+/// A line starting with a number like `123.`. May have up to three leading
+/// spaces before the marker and any number of spaces or tabs after.
+final _olPattern =
+    RegExp(r'^([ ]{0,3})(\d{1,9})([\.)])(([ \t])([ \t]*)(.*))?$');
+
+/// A line of hyphens separated by at least one pipe.
+final _tablePattern = RegExp(r'^[ ]{0,3}\|?( *:?\-+:? *\|)+( *:?\-+:? *)?$');
+
+/// Maintains the internal state needed to parse a series of lines into blocks
+/// of Markdown suitable for further inline parsing.
+class BlockParser {
+  BlockParser(this.lines, this.document) {
+    blockSyntaxes
+      ..addAll(document.blockSyntaxes)
+      ..addAll(standardBlockSyntaxes);
+  }
+
+  final List<String> lines;
+
+  /// The Markdown document this parser is parsing.
+  final Document document;
+
+  /// The enabled block syntaxes.
+  ///
+  /// To turn a series of lines into blocks, each of these will be tried in
+  /// turn. Order matters here.
+  final List<BlockSyntax> blockSyntaxes = [];
+
+  /// Index of the current line.
+  int _pos = 0;
+
+  /// Whether the parser has encountered a blank line between two block-level
+  /// elements.
+  bool encounteredBlankLine = false;
+
+  /// The collection of built-in block parsers.
+  final List<BlockSyntax> standardBlockSyntaxes = [
+    const EmptyBlockSyntax(),
+    const BlockTagBlockHtmlSyntax(),
+    LongBlockHtmlSyntax(r'^ {0,3}<pre(?:\s|>|$)', '</pre>'),
+    LongBlockHtmlSyntax(r'^ {0,3}<script(?:\s|>|$)', '</script>'),
+    LongBlockHtmlSyntax(r'^ {0,3}<style(?:\s|>|$)', '</style>'),
+    LongBlockHtmlSyntax('^ {0,3}<!--', '-->'),
+    LongBlockHtmlSyntax('^ {0,3}<\\?', '\\?>'),
+    LongBlockHtmlSyntax('^ {0,3}<![A-Z]', '>'),
+    LongBlockHtmlSyntax('^ {0,3}<!\\[CDATA\\[', '\\]\\]>'),
+    const OtherTagBlockHtmlSyntax(),
+    const SetextHeaderSyntax(),
+    const HeaderSyntax(),
+    const CodeBlockSyntax(),
+    const BlockquoteSyntax(),
+    const HorizontalRuleSyntax(),
+    const UnorderedListSyntax(),
+    const OrderedListSyntax(),
+    const ParagraphSyntax()
+  ];
+
+  /// Gets the current line.
+  String get current => lines[_pos];
+
+  /// Gets the line after the current one or `null` if there is none.
+  String? get next {
+    // Don't read past the end.
+    if (_pos >= lines.length - 1) {
+      return null;
+    }
+    return lines[_pos + 1];
+  }
+
+  /// Gets the line that is [linesAhead] lines ahead of the current one, or
+  /// `null` if there is none.
+  ///
+  /// `peek(0)` is equivalent to [current].
+  ///
+  /// `peek(1)` is equivalent to [next].
+  String? peek(int linesAhead) {
+    if (linesAhead < 0) {
+      throw ArgumentError('Invalid linesAhead: $linesAhead; must be >= 0.');
+    }
+    // Don't read past the end.
+    if (_pos >= lines.length - linesAhead) {
+      return null;
+    }
+    return lines[_pos + linesAhead];
+  }
+
+  void advance() {
+    _pos++;
+  }
+
+  bool get isDone => _pos >= lines.length;
+
+  /// Gets whether or not the current line matches the given pattern.
+  bool matches(RegExp regex) {
+    if (isDone) {
+      return false;
+    }
+    return regex.firstMatch(current) != null;
+  }
+
+  /// Gets whether or not the next line matches the given pattern.
+  bool matchesNext(RegExp regex) {
+    if (next == null) {
+      return false;
+    }
+    return regex.firstMatch(next!) != null;
+  }
+
+  List<Node> parseLines() {
+    final blocks = <Node>[];
+    while (!isDone) {
+      for (final syntax in blockSyntaxes) {
+        if (syntax.canParse(this)) {
+          final block = syntax.parse(this);
+          if (block != null) {
+            blocks.add(block);
+          }
+          break;
+        }
+      }
+    }
+
+    return blocks;
+  }
+}
+
+abstract class BlockSyntax {
+  const BlockSyntax();
+
+  /// Gets the regex used to identify the beginning of this block, if any.
+  RegExp? get pattern => null;
+
+  bool get canEndBlock => true;
+
+  bool canParse(BlockParser parser) {
+    return pattern!.firstMatch(parser.current) != null;
+  }
+
+  Node? parse(BlockParser parser);
+
+  List<String?> parseChildLines(BlockParser parser) {
+    // Grab all of the lines that form the block element.
+    final childLines = <String?>[];
+
+    while (!parser.isDone) {
+      final match = pattern!.firstMatch(parser.current);
+      if (match == null) {
+        break;
+      }
+      childLines.add(match[1]);
+      parser.advance();
+    }
+
+    return childLines;
+  }
+
+  /// Gets whether or not [parser]'s current line should end the previous block.
+  static bool isAtBlockEnd(BlockParser parser) {
+    if (parser.isDone) {
+      return true;
+    }
+    return parser.blockSyntaxes.any((s) => s.canParse(parser) && s.canEndBlock);
+  }
+
+  /// Generates a valid HTML anchor from the inner text of [element].
+  static String generateAnchorHash(Element element) =>
+      element.children!.first.textContent!
+          .toLowerCase()
+          .trim()
+          .replaceAll(RegExp(r'[^a-z0-9 _-]'), '')
+          .replaceAll(RegExp(r'\s'), '-');
+}
+
+class EmptyBlockSyntax extends BlockSyntax {
+  const EmptyBlockSyntax();
+
+  @override
+  RegExp get pattern => _emptyPattern;
+
+  @override
+  Node? parse(BlockParser parser) {
+    parser
+      ..encounteredBlankLine = true
+      ..advance();
+
+    // Don't actually emit anything.
+    return null;
+  }
+}
+
+/// Parses setext-style headers.
+class SetextHeaderSyntax extends BlockSyntax {
+  const SetextHeaderSyntax();
+
+  @override
+  bool canParse(BlockParser parser) {
+    if (!_interperableAsParagraph(parser.current)) {
+      return false;
+    }
+
+    var i = 1;
+    while (true) {
+      final nextLine = parser.peek(i);
+      if (nextLine == null) {
+        // We never reached an underline.
+        return false;
+      }
+      if (_setextPattern.hasMatch(nextLine)) {
+        return true;
+      }
+      // Ensure that we're still in something like paragraph text.
+      if (!_interperableAsParagraph(nextLine)) {
+        return false;
+      }
+      i++;
+    }
+  }
+
+  @override
+  Node parse(BlockParser parser) {
+    final lines = <String>[];
+    late String tag;
+    while (!parser.isDone) {
+      final match = _setextPattern.firstMatch(parser.current);
+      if (match == null) {
+        // More text.
+        lines.add(parser.current);
+        parser.advance();
+        continue;
+      } else {
+        // The underline.
+        tag = (match[1]![0] == '=') ? 'h1' : 'h2';
+        parser.advance();
+        break;
+      }
+    }
+
+    final contents = UnparsedContent(lines.join('\n'));
+
+    return Element(tag, [contents]);
+  }
+
+  bool _interperableAsParagraph(String line) =>
+      !(_indentPattern.hasMatch(line) ||
+          _codePattern.hasMatch(line) ||
+          _headerPattern.hasMatch(line) ||
+          _blockquotePattern.hasMatch(line) ||
+          _hrPattern.hasMatch(line) ||
+          _ulPattern.hasMatch(line) ||
+          _olPattern.hasMatch(line) ||
+          _emptyPattern.hasMatch(line));
+}
+
+/// Parses setext-style headers, and adds generated IDs to the generated
+/// elements.
+class SetextHeaderWithIdSyntax extends SetextHeaderSyntax {
+  const SetextHeaderWithIdSyntax();
+
+  @override
+  Node parse(BlockParser parser) {
+    final element = super.parse(parser) as Element;
+    element.generatedId = BlockSyntax.generateAnchorHash(element);
+    return element;
+  }
+}
+
+/// Parses atx-style headers: `## Header ##`.
+class HeaderSyntax extends BlockSyntax {
+  const HeaderSyntax();
+
+  @override
+  RegExp get pattern => _headerPattern;
+
+  @override
+  Node parse(BlockParser parser) {
+    final match = pattern.firstMatch(parser.current)!;
+    parser.advance();
+    final level = match[1]!.length;
+    final contents = UnparsedContent(match[2]!.trim());
+    return Element('h$level', [contents]);
+  }
+}
+
+/// Parses atx-style headers, and adds generated IDs to the generated elements.
+class HeaderWithIdSyntax extends HeaderSyntax {
+  const HeaderWithIdSyntax();
+
+  @override
+  Node parse(BlockParser parser) {
+    final element = super.parse(parser) as Element;
+    element.generatedId = BlockSyntax.generateAnchorHash(element);
+    return element;
+  }
+}
+
+/// Parses email-style blockquotes: `> quote`.
+class BlockquoteSyntax extends BlockSyntax {
+  const BlockquoteSyntax();
+
+  @override
+  RegExp get pattern => _blockquotePattern;
+
+  @override
+  List<String> parseChildLines(BlockParser parser) {
+    // Grab all of the lines that form the blockquote, stripping off the ">".
+    final childLines = <String>[];
+
+    while (!parser.isDone) {
+      final match = pattern.firstMatch(parser.current);
+      if (match != null) {
+        childLines.add(match[1]!);
+        parser.advance();
+        continue;
+      }
+
+      // A paragraph continuation is OK. This is content that cannot be parsed
+      // as any other syntax except Paragraph, and it doesn't match the bar in
+      // a Setext header.
+      if (parser.blockSyntaxes.firstWhere((s) => s.canParse(parser))
+          is ParagraphSyntax) {
+        childLines.add(parser.current);
+        parser.advance();
+      } else {
+        break;
+      }
+    }
+
+    return childLines;
+  }
+
+  @override
+  Node parse(BlockParser parser) {
+    final childLines = parseChildLines(parser);
+
+    // Recursively parse the contents of the blockquote.
+    final children = BlockParser(childLines, parser.document).parseLines();
+    return Element('blockquote', children);
+  }
+}
+
+/// Parses preformatted code blocks that are indented four spaces.
+class CodeBlockSyntax extends BlockSyntax {
+  const CodeBlockSyntax();
+
+  @override
+  RegExp get pattern => _indentPattern;
+
+  @override
+  bool get canEndBlock => false;
+
+  @override
+  List<String?> parseChildLines(BlockParser parser) {
+    final childLines = <String?>[];
+
+    while (!parser.isDone) {
+      final match = pattern.firstMatch(parser.current);
+      if (match != null) {
+        childLines.add(match[1]);
+        parser.advance();
+      } else {
+        // If there's a codeblock, then a newline, then a codeblock, keep the
+        // code blocks together.
+        final nextMatch =
+            parser.next != null ? pattern.firstMatch(parser.next!) : null;
+        if (parser.current.trim() == '' && nextMatch != null) {
+          childLines..add('')..add(nextMatch[1]);
+          parser..advance()..advance();
+        } else {
+          break;
+        }
+      }
+    }
+    return childLines;
+  }
+
+  @override
+  Node parse(BlockParser parser) {
+    final childLines = parseChildLines(parser)
+      // The Markdown tests expect a trailing newline.
+      ..add('');
+
+    // Escape the code.
+    final escaped = escapeHtml(childLines.join('\n'));
+
+    return Element('pre', [Element.text('code', escaped)]);
+  }
+}
+
+/// Parses preformatted code blocks between two ~~~ or ``` sequences.
+///
+/// See [Pandoc's documentation](http://pandoc.org/README.html#fenced-code-blocks).
+class FencedCodeBlockSyntax extends BlockSyntax {
+  const FencedCodeBlockSyntax();
+
+  @override
+  RegExp get pattern => _codePattern;
+
+  @override
+  List<String> parseChildLines(BlockParser parser, [String? endBlock]) {
+    endBlock ??= '';
+
+    final childLines = <String>[];
+    parser.advance();
+
+    while (!parser.isDone) {
+      final match = pattern.firstMatch(parser.current);
+      if (match == null || !match[1]!.startsWith(endBlock)) {
+        childLines.add(parser.current);
+        parser.advance();
+      } else {
+        parser.advance();
+        break;
+      }
+    }
+
+    return childLines;
+  }
+
+  @override
+  Node parse(BlockParser parser) {
+    // Get the syntax identifier, if there is one.
+    final match = pattern.firstMatch(parser.current)!;
+    final endBlock = match.group(1);
+    var infoString = match.group(2)!;
+
+    final childLines = parseChildLines(parser, endBlock)
+      // The Markdown tests expect a trailing newline.
+      ..add('');
+
+    final code = Element.text('code', childLines.join('\n'));
+
+    // the info-string should be trimmed
+    // http://spec.commonmark.org/0.22/#example-100
+    infoString = infoString.trim();
+    if (infoString.isNotEmpty) {
+      // only use the first word in the syntax
+      // http://spec.commonmark.org/0.22/#example-100
+      infoString = infoString.split(' ').first;
+      code.attributes['class'] = 'language-$infoString';
+    }
+
+    final element = Element('pre', [code]);
+    return element;
+  }
+}
+
+/// Parses horizontal rules like `---`, `_ _ _`, `*  *  *`, etc.
+class HorizontalRuleSyntax extends BlockSyntax {
+  const HorizontalRuleSyntax();
+
+  @override
+  RegExp get pattern => _hrPattern;
+
+  @override
+  Node parse(BlockParser parser) {
+    parser.advance();
+    return Element.empty('hr');
+  }
+}
+
+/// Parses inline HTML at the block level. This differs from other Markdown
+/// implementations in several ways:
+///
+/// 1.  This one is way way WAY simpler.
+/// 2.  Essentially no HTML parsing or validation is done. We're a Markdown
+///     parser, not an HTML parser!
+abstract class BlockHtmlSyntax extends BlockSyntax {
+  const BlockHtmlSyntax();
+
+  @override
+  bool get canEndBlock => true;
+}
+
+class BlockTagBlockHtmlSyntax extends BlockHtmlSyntax {
+  const BlockTagBlockHtmlSyntax();
+
+  static final _pattern = RegExp(
+      r'^ {0,3}</?(?:address|article|aside|base|basefont|blockquote|body|'
+      r'caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|'
+      r'figcaption|figure|footer|form|frame|frameset|h1|head|header|hr|html|'
+      r'iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|'
+      r'option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|'
+      'title|tr|track|ul)'
+      r'(?:\s|>|/>|$)');
+
+  @override
+  RegExp get pattern => _pattern;
+
+  @override
+  Node parse(BlockParser parser) {
+    final childLines = <String>[];
+
+    // Eat until we hit a blank line.
+    while (!parser.isDone && !parser.matches(_emptyPattern)) {
+      childLines.add(parser.current);
+      parser.advance();
+    }
+
+    return Text(childLines.join('\n'));
+  }
+}
+
+class OtherTagBlockHtmlSyntax extends BlockTagBlockHtmlSyntax {
+  const OtherTagBlockHtmlSyntax();
+
+  @override
+  bool get canEndBlock => false;
+
+  // Really hacky way to detect "other" HTML. This matches:
+  //
+  // * any opening spaces
+  // * open bracket and maybe a slash ("<" or "</")
+  // * some word characters
+  // * either:
+  //   * a close bracket, or
+  //   * whitespace followed by not-brackets followed by a close bracket
+  // * possible whitespace and the end of the line.
+  @override
+  RegExp get pattern => RegExp(r'^ {0,3}</?\w+(?:>|\s+[^>]*>)\s*$');
+}
+
+/// A BlockHtmlSyntax that has a specific `endPattern`.
+///
+/// In practice this means that the syntax dominates; it is allowed to eat
+/// many lines, including blank lines, before matching its `endPattern`.
+class LongBlockHtmlSyntax extends BlockHtmlSyntax {
+  LongBlockHtmlSyntax(String pattern, String endPattern)
+      : pattern = RegExp(pattern),
+        _endPattern = RegExp(endPattern);
+
+  @override
+  final RegExp pattern;
+  final RegExp _endPattern;
+
+  @override
+  Node parse(BlockParser parser) {
+    final childLines = <String>[];
+    // Eat until we hit [endPattern].
+    while (!parser.isDone) {
+      childLines.add(parser.current);
+      if (parser.matches(_endPattern)) {
+        break;
+      }
+      parser.advance();
+    }
+
+    parser.advance();
+    return Text(childLines.join('\n'));
+  }
+}
+
+class ListItem {
+  ListItem(this.lines);
+
+  bool forceBlock = false;
+  final List<String> lines;
+}
+
+/// Base class for both ordered and unordered lists.
+abstract class ListSyntax extends BlockSyntax {
+  const ListSyntax();
+
+  @override
+  bool get canEndBlock => true;
+
+  String get listTag;
+
+  /// A list of patterns that can start a valid block within a list item.
+  static final blocksInList = [
+    _blockquotePattern,
+    _headerPattern,
+    _hrPattern,
+    _indentPattern,
+    _ulPattern,
+    _olPattern
+  ];
+
+  static final _whitespaceRe = RegExp('[ \t]*');
+
+  @override
+  Node parse(BlockParser parser) {
+    final items = <ListItem>[];
+    var childLines = <String>[];
+
+    void endItem() {
+      if (childLines.isNotEmpty) {
+        items.add(ListItem(childLines));
+        childLines = <String>[];
+      }
+    }
+
+    Match? match;
+    bool tryMatch(RegExp pattern) {
+      match = pattern.firstMatch(parser.current);
+      return match != null;
+    }
+
+    String? listMarker;
+    String? indent;
+    // In case the first number in an ordered list is not 1, use it as the
+    // "start".
+    int? startNumber;
+
+    while (!parser.isDone) {
+      final leadingSpace =
+          _whitespaceRe.matchAsPrefix(parser.current)!.group(0)!;
+      final leadingExpandedTabLength = _expandedTabLength(leadingSpace);
+      if (tryMatch(_emptyPattern)) {
+        if (_emptyPattern.firstMatch(parser.next ?? '') != null) {
+          // Two blank lines ends a list.
+          break;
+        }
+        // Add a blank line to the current list item.
+        childLines.add('');
+      } else if (indent != null && indent.length <= leadingExpandedTabLength) {
+        // Strip off indent and add to current item.
+        final line = parser.current
+            .replaceFirst(leadingSpace, ' ' * leadingExpandedTabLength)
+            .replaceFirst(indent, '');
+        childLines.add(line);
+      } else if (tryMatch(_hrPattern)) {
+        // Horizontal rule takes precedence to a list item.
+        break;
+      } else if (tryMatch(_ulPattern) || tryMatch(_olPattern)) {
+        final precedingWhitespace = match![1];
+        final digits = match![2] ?? '';
+        if (startNumber == null && digits.isNotEmpty) {
+          startNumber = int.parse(digits);
+        }
+        final marker = match![3];
+        final firstWhitespace = match![5] ?? '';
+        final restWhitespace = match![6] ?? '';
+        final content = match![7] ?? '';
+        final isBlank = content.isEmpty;
+        if (listMarker != null && listMarker != marker) {
+          // Changing the bullet or ordered list delimiter starts a list.
+          break;
+        }
+        listMarker = marker;
+        final markerAsSpaces = ' ' * (digits.length + marker!.length);
+        if (isBlank) {
+          // See http://spec.commonmark.org/0.28/#list-items under "3. Item
+          // starting with a blank line."
+          //
+          // If the list item starts with a blank line, the final piece of the
+          // indentation is just a single space.
+          indent = '$precedingWhitespace$markerAsSpaces ';
+        } else if (restWhitespace.length >= 4) {
+          // See http://spec.commonmark.org/0.28/#list-items under "2. Item
+          // starting with indented code."
+          //
+          // If the list item starts with indented code, we need to _not_ count
+          // any indentation past the required whitespace character.
+          indent = precedingWhitespace! + markerAsSpaces + firstWhitespace;
+        } else {
+          indent = precedingWhitespace! +
+              markerAsSpaces +
+              firstWhitespace +
+              restWhitespace;
+        }
+        // End the current list item and start a one.
+        endItem();
+        childLines.add(restWhitespace + content);
+      } else if (BlockSyntax.isAtBlockEnd(parser)) {
+        // Done with the list.
+        break;
+      } else {
+        // If the previous item is a blank line, this means we're done with the
+        // list and are starting a top-level paragraph.
+        if ((childLines.isNotEmpty) && (childLines.last == '')) {
+          parser.encounteredBlankLine = true;
+          break;
+        }
+
+        // Anything else is paragraph continuation text.
+        childLines.add(parser.current);
+      }
+      parser.advance();
+    }
+
+    endItem();
+    final itemNodes = <Element>[];
+
+    items.forEach(removeLeadingEmptyLine);
+    final anyEmptyLines = removeTrailingEmptyLines(items);
+    var anyEmptyLinesBetweenBlocks = false;
+
+    for (final item in items) {
+      final itemParser = BlockParser(item.lines, parser.document);
+      final children = itemParser.parseLines();
+      itemNodes.add(Element('li', children));
+      anyEmptyLinesBetweenBlocks =
+          anyEmptyLinesBetweenBlocks || itemParser.encounteredBlankLine;
+    }
+
+    // Must strip paragraph tags if the list is "tight".
+    // http://spec.commonmark.org/0.28/#lists
+    final listIsTight = !anyEmptyLines && !anyEmptyLinesBetweenBlocks;
+
+    if (listIsTight) {
+      // We must post-process the list items, converting any top-level paragraph
+      // elements to just text elements.
+      for (final item in itemNodes) {
+        for (var i = 0; i < item.children!.length; i++) {
+          final child = item.children![i];
+          if (child is Element && child.tag == 'p') {
+            item.children!.removeAt(i);
+            item.children!.insertAll(i, child.children!);
+          }
+        }
+      }
+    }
+
+    if (listTag == 'ol' && startNumber != 1) {
+      return Element(listTag, itemNodes)..attributes['start'] = '$startNumber';
+    } else {
+      return Element(listTag, itemNodes);
+    }
+  }
+
+  void removeLeadingEmptyLine(ListItem item) {
+    if (item.lines.isNotEmpty && _emptyPattern.hasMatch(item.lines.first)) {
+      item.lines.removeAt(0);
+    }
+  }
+
+  /// Removes any trailing empty lines and notes whether any items are separated
+  /// by such lines.
+  bool removeTrailingEmptyLines(List<ListItem> items) {
+    var anyEmpty = false;
+    for (var i = 0; i < items.length; i++) {
+      if (items[i].lines.length == 1) {
+        continue;
+      }
+      while (items[i].lines.isNotEmpty &&
+          _emptyPattern.hasMatch(items[i].lines.last)) {
+        if (i < items.length - 1) {
+          anyEmpty = true;
+        }
+        items[i].lines.removeLast();
+      }
+    }
+    return anyEmpty;
+  }
+
+  static int _expandedTabLength(String input) {
+    var length = 0;
+    for (final char in input.codeUnits) {
+      length += char == 0x9 ? 4 - (length % 4) : 1;
+    }
+    return length;
+  }
+}
+
+/// Parses unordered lists.
+class UnorderedListSyntax extends ListSyntax {
+  const UnorderedListSyntax();
+
+  @override
+  RegExp get pattern => _ulPattern;
+
+  @override
+  String get listTag => 'ul';
+}
+
+/// Parses ordered lists.
+class OrderedListSyntax extends ListSyntax {
+  const OrderedListSyntax();
+
+  @override
+  RegExp get pattern => _olPattern;
+
+  @override
+  String get listTag => 'ol';
+}
+
+/// Parses tables.
+class TableSyntax extends BlockSyntax {
+  const TableSyntax();
+
+  static final _pipePattern = RegExp(r'\s*\|\s*');
+  static final _openingPipe = RegExp(r'^\|\s*');
+  static final _closingPipe = RegExp(r'\s*\|$');
+
+  @override
+  bool get canEndBlock => false;
+
+  @override
+  bool canParse(BlockParser parser) {
+    // Note: matches *next* line, not the current one. We're looking for the
+    // bar separating the head row from the body rows.
+    return parser.matchesNext(_tablePattern);
+  }
+
+  /// Parses a table into its three parts:
+  ///
+  /// * a head row of head cells (`<th>` cells)
+  /// * a divider of hyphens and pipes (not rendered)
+  /// * many body rows of body cells (`<td>` cells)
+  @override
+  Node? parse(BlockParser parser) {
+    final alignments = parseAlignments(parser.next!);
+    final columnCount = alignments.length;
+    final headRow = parseRow(parser, alignments, 'th');
+    if (headRow.children!.length != columnCount) {
+      return null;
+    }
+    final head = Element('thead', [headRow]);
+
+    // Advance past the divider of hyphens.
+    parser.advance();
+
+    final rows = <Element>[];
+    while (!parser.isDone && !BlockSyntax.isAtBlockEnd(parser)) {
+      final row = parseRow(parser, alignments, 'td');
+      while (row.children!.length < columnCount) {
+        // Insert synthetic empty cells.
+        row.children!.add(Element.empty('td'));
+      }
+      while (row.children!.length > columnCount) {
+        row.children!.removeLast();
+      }
+      rows.add(row);
+    }
+    if (rows.isEmpty) {
+      return Element('table', [head]);
+    } else {
+      final body = Element('tbody', rows);
+
+      return Element('table', [head, body]);
+    }
+  }
+
+  List<String?> parseAlignments(String line) {
+    line = line.replaceFirst(_openingPipe, '').replaceFirst(_closingPipe, '');
+    return line.split('|').map((column) {
+      column = column.trim();
+      if (column.startsWith(':') && column.endsWith(':')) {
+        return 'center';
+      }
+      if (column.startsWith(':')) {
+        return 'left';
+      }
+      if (column.endsWith(':')) {
+        return 'right';
+      }
+      return null;
+    }).toList();
+  }
+
+  Element parseRow(
+      BlockParser parser, List<String?> alignments, String cellType) {
+    final line = parser.current
+        .replaceFirst(_openingPipe, '')
+        .replaceFirst(_closingPipe, '');
+    final cells = line.split(_pipePattern);
+    parser.advance();
+    final row = <Element>[];
+    String? preCell;
+
+    for (var cell in cells) {
+      if (preCell != null) {
+        cell = preCell + cell;
+        preCell = null;
+      }
+      if (cell.endsWith('\\')) {
+        preCell = '${cell.substring(0, cell.length - 1)}|';
+        continue;
+      }
+
+      final contents = UnparsedContent(cell);
+      row.add(Element(cellType, [contents]));
+    }
+
+    for (var i = 0; i < row.length && i < alignments.length; i++) {
+      if (alignments[i] == null) {
+        continue;
+      }
+      row[i].attributes['style'] = 'text-align: ${alignments[i]};';
+    }
+
+    return Element('tr', row);
+  }
+}
+
+/// Parses paragraphs of regular text.
+class ParagraphSyntax extends BlockSyntax {
+  const ParagraphSyntax();
+
+  static final _reflinkDefinitionStart = RegExp(r'[ ]{0,3}\[');
+
+  static final _whitespacePattern = RegExp(r'^\s*$');
+
+  @override
+  bool get canEndBlock => false;
+
+  @override
+  bool canParse(BlockParser parser) => true;
+
+  @override
+  Node parse(BlockParser parser) {
+    final childLines = <String>[];
+
+    // Eat until we hit something that ends a paragraph.
+    while (!BlockSyntax.isAtBlockEnd(parser)) {
+      childLines.add(parser.current);
+      parser.advance();
+    }
+
+    final paragraphLines = _extractReflinkDefinitions(parser, childLines);
+    if (paragraphLines == null) {
+      // Paragraph consisted solely of reference link definitions.
+      return Text('');
+    } else {
+      final contents = UnparsedContent(paragraphLines.join('\n'));
+      return Element('p', [contents]);
+    }
+  }
+
+  /// Extract reference link definitions from the front of the paragraph, and
+  /// return the remaining paragraph lines.
+  List<String>? _extractReflinkDefinitions(
+      BlockParser parser, List<String> lines) {
+    bool lineStartsReflinkDefinition(int i) =>
+        lines[i].startsWith(_reflinkDefinitionStart);
+
+    var i = 0;
+    loopOverDefinitions:
+    while (true) {
+      // Check for reflink definitions.
+      if (!lineStartsReflinkDefinition(i)) {
+        // It's paragraph content from here on out.
+        break;
+      }
+      var contents = lines[i];
+      var j = i + 1;
+      while (j < lines.length) {
+        // Check to see if the _next_ line might start a reflink definition.
+        // Even if it turns out not to be, but it started with a '[', then it
+        // is not a part of _this_ possible reflink definition.
+        if (lineStartsReflinkDefinition(j)) {
+          // Try to parse [contents] as a reflink definition.
+          if (_parseReflinkDefinition(parser, contents)) {
+            // Loop again, starting at the next possible reflink definition.
+            i = j;
+            continue loopOverDefinitions;
+          } else {
+            // Could not parse [contents] as a reflink definition.
+            break;
+          }
+        } else {
+          contents = '$contents\n${lines[j]}';
+          j++;
+        }
+      }
+      // End of the block.
+      if (_parseReflinkDefinition(parser, contents)) {
+        i = j;
+        break;
+      }
+
+      // It may be that there is a reflink definition starting at [i], but it
+      // does not extend all the way to [j], such as:
+      //
+      //     [link]: url     // line i
+      //     "title"
+      //     garbage
+      //     [link2]: url   // line j
+      //
+      // In this case, [i, i+1] is a reflink definition, and the rest is
+      // paragraph content.
+      while (j >= i) {
+        // This isn't the most efficient loop, what with this big ole'
+        // Iterable allocation (`getRange`) followed by a big 'ole String
+        // allocation, but we
+        // must walk backwards, checking each range.
+        contents = lines.getRange(i, j).join('\n');
+        if (_parseReflinkDefinition(parser, contents)) {
+          // That is the last reflink definition. The rest is paragraph
+          // content.
+          i = j;
+          break;
+        }
+        j--;
+      }
+      // The ending was not a reflink definition at all. Just paragraph
+      // content.
+
+      break;
+    }
+
+    if (i == lines.length) {
+      // No paragraph content.
+      return null;
+    } else {
+      // Ends with paragraph content.
+      return lines.sublist(i);
+    }
+  }
+
+  // Parse [contents] as a reference link definition.
+  //
+  // Also adds the reference link definition to the document.
+  //
+  // Returns whether [contents] could be parsed as a reference link definition.
+  bool _parseReflinkDefinition(BlockParser parser, String contents) {
+    final pattern = RegExp(
+        // Leading indentation.
+        r'''^[ ]{0,3}'''
+        // Reference id in brackets, and URL.
+        r'''\[((?:\\\]|[^\]])+)\]:\s*(?:<(\S+)>|(\S+))\s*'''
+        // Title in double or single quotes, or parens.
+        r'''("[^"]+"|'[^']+'|\([^)]+\)|)\s*$''',
+        multiLine: true);
+    final match = pattern.firstMatch(contents);
+    if (match == null) {
+      // Not a reference link definition.
+      return false;
+    }
+    if (match[0]!.length < contents.length) {
+      // Trailing text. No good.
+      return false;
+    }
+
+    var label = match[1]!;
+    final destination = match[2] ?? match[3];
+    var title = match[4];
+
+    // The label must contain at least one non-whitespace character.
+    if (_whitespacePattern.hasMatch(label)) {
+      return false;
+    }
+
+    if (title == '') {
+      // No title.
+      title = null;
+    } else {
+      // Remove "", '', or ().
+      title = title!.substring(1, title.length - 1);
+    }
+
+    // References are case-insensitive, and internal whitespace is compressed.
+    label =
+        label.toLowerCase().trim().replaceAll(_oneOrMoreWhitespacePattern, ' ');
+
+    parser.document.linkReferences
+        .putIfAbsent(label, () => LinkReference(label, destination!, title!));
+    return true;
+  }
+}

+ 255 - 0
app_flowy/lib/workspace/infrastructure/markdown/src/delta_markdown_decoder.dart

@@ -0,0 +1,255 @@
+import 'dart:collection';
+import 'dart:convert';
+
+import 'package:flutter_quill/models/documents/attribute.dart';
+import 'package:flutter_quill/models/quill_delta.dart';
+
+import 'ast.dart' as ast;
+import 'document.dart';
+
+class DeltaMarkdownDecoder extends Converter<String, String> {
+  @override
+  String convert(String input) {
+    final lines = input.replaceAll('\r\n', '\n').split('\n');
+
+    final markdownDocument = Document().parseLines(lines);
+
+    return jsonEncode(_DeltaVisitor().convert(markdownDocument).toJson());
+  }
+}
+
+class _DeltaVisitor implements ast.NodeVisitor {
+  static final _blockTags =
+      RegExp('h1|h2|h3|h4|h5|h6|hr|pre|ul|ol|blockquote|p|pre');
+
+  static final _embedTags = RegExp('hr|img');
+
+  late Delta delta;
+
+  late Queue<Attribute> activeInlineAttributes;
+  Attribute? activeBlockAttribute;
+  late Set<String> uniqueIds;
+
+  ast.Element? previousElement;
+  late ast.Element previousToplevelElement;
+
+  Delta convert(List<ast.Node> nodes) {
+    delta = Delta();
+    activeInlineAttributes = Queue<Attribute>();
+    uniqueIds = <String>{};
+
+    for (final node in nodes) {
+      node.accept(this);
+    }
+
+    // Ensure the delta ends with a newline.
+    if (delta.length > 0 && delta.last.value != '\n') {
+      delta.insert('\n', activeBlockAttribute?.toJson());
+    }
+
+    return delta;
+  }
+
+  @override
+  void visitText(ast.Text text) {
+    // Remove trailing newline
+    //final lines = text.text.trim().split('\n');
+
+    /*
+    final attributes = Map<String, dynamic>();
+    for (final attr in activeInlineAttributes) {
+      attributes.addAll(attr.toJson());
+    }
+
+    for (final l in lines) {
+      delta.insert(l, attributes);
+      delta.insert('\n', activeBlockAttribute.toJson());
+    }*/
+
+    final str = text.text;
+    //if (str.endsWith('\n')) str = str.substring(0, str.length - 1);
+
+    final attributes = <String, dynamic>{};
+    for (final attr in activeInlineAttributes) {
+      attributes.addAll(attr.toJson());
+    }
+
+    var newlineIndex = str.indexOf('\n');
+    var startIndex = 0;
+    while (newlineIndex != -1) {
+      final previousText = str.substring(startIndex, newlineIndex);
+      if (previousText.isNotEmpty) {
+        delta.insert(previousText, attributes.isNotEmpty ? attributes : null);
+      }
+      delta.insert('\n', activeBlockAttribute?.toJson());
+
+      startIndex = newlineIndex + 1;
+      newlineIndex = str.indexOf('\n', newlineIndex + 1);
+    }
+
+    if (startIndex < str.length) {
+      final lastStr = str.substring(startIndex);
+      delta.insert(lastStr, attributes.isNotEmpty ? attributes : null);
+    }
+  }
+
+  @override
+  bool visitElementBefore(ast.Element element) {
+    // Hackish. Separate block-level elements with newlines.
+    final attr = _tagToAttribute(element);
+
+    if (delta.isNotEmpty && _blockTags.firstMatch(element.tag) != null) {
+      if (element.isToplevel) {
+        // If the last active block attribute is not a list, we need to finish
+        // it off.
+        if (previousToplevelElement.tag != 'ul' &&
+            previousToplevelElement.tag != 'ol' &&
+            previousToplevelElement.tag != 'pre' &&
+            previousToplevelElement.tag != 'hr') {
+          delta.insert('\n', activeBlockAttribute?.toJson());
+        }
+
+        // Only separate the blocks if both are paragraphs.
+        //
+        // TODO(kolja): Determine which behavior we really want here.
+        // We can either insert an additional newline or just have the
+        // paragraphs as single lines. Zefyr will by default render two lines
+        // are different paragraphs so for now we will not add an additonal
+        // newline here.
+        //
+        // if (previousToplevelElement != null &&
+        //     previousToplevelElement.tag == 'p' &&
+        //     element.tag == 'p') {
+        //   delta.insert('\n');
+        // }
+      } else if (element.tag == 'p' &&
+          previousElement != null &&
+          !previousElement!.isToplevel &&
+          !previousElement!.children!.contains(element)) {
+        // Here we have two children of the same toplevel element. These need
+        // to be separated by additional newlines.
+
+        delta
+          // Finish off the last lower-level block.
+          ..insert('\n', activeBlockAttribute?.toJson())
+          // Add an empty line between the lower-level blocks.
+          ..insert('\n', activeBlockAttribute?.toJson());
+      }
+    }
+
+    // Keep track of the top-level block attribute.
+    if (element.isToplevel && element.tag != 'hr') {
+      // Hacky solution for horizontal rule so that the attribute is not added
+      // to the line feed at the end of the line.
+      activeBlockAttribute = attr;
+    }
+
+    if (_embedTags.firstMatch(element.tag) != null) {
+      // We write out the element here since the embed has no children or
+      // content.
+      delta.insert(attr!.toJson());
+    } else if (_blockTags.firstMatch(element.tag) == null && attr != null) {
+      activeInlineAttributes.addLast(attr);
+    }
+
+    previousElement = element;
+    if (element.isToplevel) {
+      previousToplevelElement = element;
+    }
+
+    if (element.isEmpty) {
+      // Empty element like <hr/>.
+      //buffer.write(' />');
+
+      if (element.tag == 'br') {
+        delta.insert('\n');
+      }
+
+      return false;
+    } else {
+      //buffer.write('>');
+      return true;
+    }
+  }
+
+  @override
+  void visitElementAfter(ast.Element element) {
+    if (element.tag == 'li' &&
+        (previousToplevelElement.tag == 'ol' ||
+            previousToplevelElement.tag == 'ul')) {
+      delta.insert('\n', activeBlockAttribute?.toJson());
+    }
+
+    final attr = _tagToAttribute(element);
+    if (attr == null || !attr.isInline || activeInlineAttributes.last != attr) {
+      return;
+    }
+    activeInlineAttributes.removeLast();
+
+    // Always keep track of the last element.
+    // This becomes relevant if we have something like
+    //
+    // <ul>
+    //   <li>...</li>
+    //   <li>...</li>
+    // </ul>
+    previousElement = element;
+  }
+
+  /// Uniquifies an id generated from text.
+  String uniquifyId(String id) {
+    if (!uniqueIds.contains(id)) {
+      uniqueIds.add(id);
+      return id;
+    }
+
+    var suffix = 2;
+    var suffixedId = '$id-$suffix';
+    while (uniqueIds.contains(suffixedId)) {
+      suffixedId = '$id-${suffix++}';
+    }
+    uniqueIds.add(suffixedId);
+    return suffixedId;
+  }
+
+  Attribute? _tagToAttribute(ast.Element el) {
+    switch (el.tag) {
+      case 'em':
+        return Attribute.italic;
+      case 'strong':
+        return Attribute.bold;
+      case 'ul':
+        return Attribute.ul;
+      case 'ol':
+        return Attribute.ol;
+      case 'pre':
+        return Attribute.codeBlock;
+      case 'blockquote':
+        return Attribute.blockQuote;
+      case 'h1':
+        return Attribute.h1;
+      case 'h2':
+        return Attribute.h2;
+      case 'h3':
+        return Attribute.h3;
+      case 'a':
+        final href = el.attributes['href'];
+        return LinkAttribute(href);
+      case 'img':
+        final href = el.attributes['src'];
+        return ImageAttribute(href);
+      case 'hr':
+        return DividerAttribute();
+    }
+
+    return null;
+  }
+}
+
+class ImageAttribute extends Attribute<String?> {
+  ImageAttribute(String? val) : super('image', AttributeScope.EMBEDS, val);
+}
+
+class DividerAttribute extends Attribute<String?> {
+  DividerAttribute() : super('divider', AttributeScope.EMBEDS, 'hr');
+}

+ 272 - 0
app_flowy/lib/workspace/infrastructure/markdown/src/delta_markdown_encoder.dart

@@ -0,0 +1,272 @@
+import 'dart:convert';
+
+import 'package:collection/collection.dart' show IterableExtension;
+import 'package:flutter_quill/models/documents/attribute.dart';
+import 'package:flutter_quill/models/documents/nodes/embed.dart';
+import 'package:flutter_quill/models/documents/style.dart';
+import 'package:flutter_quill/models/quill_delta.dart';
+
+class DeltaMarkdownEncoder extends Converter<String, String> {
+  static const _lineFeedAsciiCode = 0x0A;
+
+  late StringBuffer markdownBuffer;
+  late StringBuffer lineBuffer;
+
+  Attribute? currentBlockStyle;
+  late Style currentInlineStyle;
+
+  late List<String> currentBlockLines;
+
+  /// Converts the [input] delta to Markdown.
+  @override
+  String convert(String input) {
+    markdownBuffer = StringBuffer();
+    lineBuffer = StringBuffer();
+    currentInlineStyle = Style();
+    currentBlockLines = <String>[];
+
+    final inputJson = jsonDecode(input) as List<dynamic>?;
+    if (inputJson is! List<dynamic>) {
+      throw ArgumentError('Unexpected formatting of the input delta string.');
+    }
+    final delta = Delta.fromJson(inputJson);
+    final iterator = DeltaIterator(delta);
+
+    while (iterator.hasNext) {
+      final operation = iterator.next();
+
+      if (operation.data is String) {
+        final operationData = operation.data as String;
+
+        if (!operationData.contains('\n')) {
+          _handleInline(lineBuffer, operationData, operation.attributes);
+        } else {
+          _handleLine(operationData, operation.attributes);
+        }
+      } else if (operation.data is Map<String, dynamic>) {
+        _handleEmbed(operation.data as Map<String, dynamic>);
+      } else {
+        throw ArgumentError('Unexpected formatting of the input delta string.');
+      }
+    }
+
+    _handleBlock(currentBlockStyle); // Close the last block
+
+    return markdownBuffer.toString();
+  }
+
+  void _handleInline(
+    StringBuffer buffer,
+    String text,
+    Map<String, dynamic>? attributes,
+  ) {
+    final style = Style.fromJson(attributes);
+
+    // First close any current styles if needed
+    final markedForRemoval = <Attribute>[];
+    // Close the styles in reverse order, e.g. **_ for _**Test**_.
+    for (final value
+        in currentInlineStyle.attributes.values.toList().reversed) {
+      // TODO(tillf): Is block correct?
+      if (value.scope == AttributeScope.BLOCK) {
+        continue;
+      }
+      if (style.containsKey(value.key)) {
+        continue;
+      }
+
+      final padding = _trimRight(buffer);
+      _writeAttribute(buffer, value, close: true);
+      if (padding.isNotEmpty) {
+        buffer.write(padding);
+      }
+      markedForRemoval.add(value);
+    }
+
+    // Make sure to remove all attributes that are marked for removal.
+    for (final value in markedForRemoval) {
+      currentInlineStyle.attributes.removeWhere((_, v) => v == value);
+    }
+
+    // Now open any new styles.
+    for (final attribute in style.attributes.values) {
+      // TODO(tillf): Is block correct?
+      if (attribute.scope == AttributeScope.BLOCK) {
+        continue;
+      }
+      if (currentInlineStyle.containsKey(attribute.key)) {
+        continue;
+      }
+      final originalText = text;
+      text = text.trimLeft();
+      final padding = ' ' * (originalText.length - text.length);
+      if (padding.isNotEmpty) {
+        buffer.write(padding);
+      }
+      _writeAttribute(buffer, attribute);
+    }
+
+    // Write the text itself
+    buffer.write(text);
+    currentInlineStyle = style;
+  }
+
+  void _handleLine(String data, Map<String, dynamic>? attributes) {
+    final span = StringBuffer();
+
+    for (var i = 0; i < data.length; i++) {
+      if (data.codeUnitAt(i) == _lineFeedAsciiCode) {
+        if (span.isNotEmpty) {
+          // Write the span if it's not empty.
+          _handleInline(lineBuffer, span.toString(), attributes);
+        }
+        // Close any open inline styles.
+        _handleInline(lineBuffer, '', null);
+
+        final lineBlock = Style.fromJson(attributes)
+            .attributes
+            .values
+            .singleWhereOrNull((a) => a.scope == AttributeScope.BLOCK);
+
+        if (lineBlock == currentBlockStyle) {
+          currentBlockLines.add(lineBuffer.toString());
+        } else {
+          _handleBlock(currentBlockStyle);
+          currentBlockLines
+            ..clear()
+            ..add(lineBuffer.toString());
+
+          currentBlockStyle = lineBlock;
+        }
+        lineBuffer.clear();
+
+        span.clear();
+      } else {
+        span.writeCharCode(data.codeUnitAt(i));
+      }
+    }
+
+    // Remaining span
+    if (span.isNotEmpty) {
+      _handleInline(lineBuffer, span.toString(), attributes);
+    }
+  }
+
+  void _handleEmbed(Map<String, dynamic> data) {
+    final embed = BlockEmbed(data.keys.first, data.values.first as String);
+
+    if (embed.type == 'image') {
+      _writeEmbedTag(lineBuffer, embed);
+      _writeEmbedTag(lineBuffer, embed, close: true);
+    } else if (embed.type == 'divider') {
+      _writeEmbedTag(lineBuffer, embed);
+      _writeEmbedTag(lineBuffer, embed, close: true);
+    }
+  }
+
+  void _handleBlock(Attribute? blockStyle) {
+    if (currentBlockLines.isEmpty) {
+      return; // Empty block
+    }
+
+    // If there was a block before this one, add empty line between the blocks
+    if (markdownBuffer.isNotEmpty) {
+      markdownBuffer.writeln();
+    }
+
+    if (blockStyle == null) {
+      markdownBuffer
+        ..write(currentBlockLines.join('\n'))
+        ..writeln();
+    } else if (blockStyle == Attribute.codeBlock) {
+      _writeAttribute(markdownBuffer, blockStyle);
+      markdownBuffer.write(currentBlockLines.join('\n'));
+      _writeAttribute(markdownBuffer, blockStyle, close: true);
+      markdownBuffer.writeln();
+    } else {
+      // Dealing with lists or a quote.
+      for (final line in currentBlockLines) {
+        _writeBlockTag(markdownBuffer, blockStyle);
+        markdownBuffer
+          ..write(line)
+          ..writeln();
+      }
+    }
+  }
+
+  String _trimRight(StringBuffer buffer) {
+    final text = buffer.toString();
+    if (!text.endsWith(' ')) {
+      return '';
+    }
+
+    final result = text.trimRight();
+    buffer
+      ..clear()
+      ..write(result);
+    return ' ' * (text.length - result.length);
+  }
+
+  void _writeAttribute(
+    StringBuffer buffer,
+    Attribute attribute, {
+    bool close = false,
+  }) {
+    if (attribute.key == Attribute.bold.key) {
+      buffer.write('**');
+    } else if (attribute.key == Attribute.italic.key) {
+      buffer.write('_');
+    } else if (attribute.key == Attribute.link.key) {
+      buffer.write(!close ? '[' : '](${attribute.value})');
+    } else if (attribute == Attribute.codeBlock) {
+      buffer.write(!close ? '```\n' : '\n```');
+    } else {
+      throw ArgumentError('Cannot handle $attribute');
+    }
+  }
+
+  void _writeBlockTag(
+    StringBuffer buffer,
+    Attribute block, {
+    bool close = false,
+  }) {
+    if (close) {
+      return; // no close tag needed for simple blocks.
+    }
+
+    if (block == Attribute.blockQuote) {
+      buffer.write('> ');
+    } else if (block == Attribute.ul) {
+      buffer.write('* ');
+    } else if (block == Attribute.ol) {
+      buffer.write('1. ');
+    } else if (block.key == Attribute.h1.key && block.value == 1) {
+      buffer.write('# ');
+    } else if (block.key == Attribute.h2.key && block.value == 2) {
+      buffer.write('## ');
+    } else if (block.key == Attribute.h3.key && block.value == 3) {
+      buffer.write('### ');
+    } else {
+      throw ArgumentError('Cannot handle block $block');
+    }
+  }
+
+  void _writeEmbedTag(
+    StringBuffer buffer,
+    BlockEmbed embed, {
+    bool close = false,
+  }) {
+    const kImageType = 'image';
+    const kDividerType = 'divider';
+
+    if (embed.type == kImageType) {
+      if (close) {
+        buffer.write('](${embed.data})');
+      } else {
+        buffer.write('![');
+      }
+    } else if (embed.type == kDividerType && close) {
+      buffer.write('\n---\n\n');
+    }
+  }
+}

+ 88 - 0
app_flowy/lib/workspace/infrastructure/markdown/src/document.dart

@@ -0,0 +1,88 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'ast.dart';
+import 'block_parser.dart';
+import 'extension_set.dart';
+import 'inline_parser.dart';
+
+/// Maintains the context needed to parse a Markdown document.
+class Document {
+  Document({
+    Iterable<BlockSyntax>? blockSyntaxes,
+    Iterable<InlineSyntax>? inlineSyntaxes,
+    ExtensionSet? extensionSet,
+    this.linkResolver,
+    this.imageLinkResolver,
+  }) : extensionSet = extensionSet ?? ExtensionSet.commonMark {
+    _blockSyntaxes
+      ..addAll(blockSyntaxes ?? [])
+      ..addAll(this.extensionSet.blockSyntaxes);
+    _inlineSyntaxes
+      ..addAll(inlineSyntaxes ?? [])
+      ..addAll(this.extensionSet.inlineSyntaxes);
+  }
+
+  final Map<String, LinkReference> linkReferences = <String, LinkReference>{};
+  final ExtensionSet extensionSet;
+  final Resolver? linkResolver;
+  final Resolver? imageLinkResolver;
+  final _blockSyntaxes = <BlockSyntax>{};
+  final _inlineSyntaxes = <InlineSyntax>{};
+
+  Iterable<BlockSyntax> get blockSyntaxes => _blockSyntaxes;
+  Iterable<InlineSyntax> get inlineSyntaxes => _inlineSyntaxes;
+
+  /// Parses the given [lines] of Markdown to a series of AST nodes.
+  List<Node> parseLines(List<String> lines) {
+    final nodes = BlockParser(lines, this).parseLines();
+    // Make sure to mark the top level nodes as such.
+    for (final n in nodes) {
+      n.isToplevel = true;
+    }
+    _parseInlineContent(nodes);
+    return nodes;
+  }
+
+  /// Parses the given inline Markdown [text] to a series of AST nodes.
+  List<Node>? parseInline(String text) => InlineParser(text, this).parse();
+
+  void _parseInlineContent(List<Node> nodes) {
+    for (var i = 0; i < nodes.length; i++) {
+      final node = nodes[i];
+      if (node is UnparsedContent) {
+        final inlineNodes = parseInline(node.textContent)!;
+        nodes
+          ..removeAt(i)
+          ..insertAll(i, inlineNodes);
+        i += inlineNodes.length - 1;
+      } else if (node is Element && node.children != null) {
+        _parseInlineContent(node.children!);
+      }
+    }
+  }
+}
+
+/// A [link reference
+/// definition](http://spec.commonmark.org/0.28/#link-reference-definitions).
+class LinkReference {
+  /// Construct a [LinkReference], with all necessary fields.
+  ///
+  /// If the parsed link reference definition does not include a title, use
+  /// `null` for the [title] parameter.
+  LinkReference(this.label, this.destination, this.title);
+
+  /// The [link label](http://spec.commonmark.org/0.28/#link-label).
+  ///
+  /// Temporarily, this class is also being used to represent the link data for
+  /// an inline link (the destination and title), but this should change before
+  /// the package is released.
+  final String label;
+
+  /// The [link destination](http://spec.commonmark.org/0.28/#link-destination).
+  final String destination;
+
+  /// The [link title](http://spec.commonmark.org/0.28/#link-title).
+  final String title;
+}

+ 1510 - 0
app_flowy/lib/workspace/infrastructure/markdown/src/emojis.dart

@@ -0,0 +1,1510 @@
+// GENERATED FILE. DO NOT EDIT.
+//
+// This file was generated from emojilib's emoji data file:
+// https://github.com/muan/emojilib/raw/master/emojis.json
+// at 2018-07-02 15:07:49.422933 by the script, tool/update_emojis.dart.
+
+const emojis = <String, String>{
+  'grinning': '😀',
+  'grimacing': '😬',
+  'grin': '😁',
+  'joy': '😂',
+  'rofl': '🤣',
+  'smiley': '😃',
+  'smile': '😄',
+  'sweat_smile': '😅',
+  'laughing': '😆',
+  'innocent': '😇',
+  'wink': '😉',
+  'blush': '😊',
+  'slightly_smiling_face': '🙂',
+  'upside_down_face': '🙃',
+  'relaxed': '☺️',
+  'yum': '😋',
+  'relieved': '😌',
+  'heart_eyes': '😍',
+  'kissing_heart': '😘',
+  'kissing': '😗',
+  'kissing_smiling_eyes': '😙',
+  'kissing_closed_eyes': '😚',
+  'stuck_out_tongue_winking_eye': '😜',
+  'zany': '🤪',
+  'raised_eyebrow': '🤨',
+  'monocle': '🧐',
+  'stuck_out_tongue_closed_eyes': '😝',
+  'stuck_out_tongue': '😛',
+  'money_mouth_face': '🤑',
+  'nerd_face': '🤓',
+  'sunglasses': '😎',
+  'star_struck': '🤩',
+  'clown_face': '🤡',
+  'cowboy_hat_face': '🤠',
+  'hugs': '🤗',
+  'smirk': '😏',
+  'no_mouth': '😶',
+  'neutral_face': '😐',
+  'expressionless': '😑',
+  'unamused': '😒',
+  'roll_eyes': '🙄',
+  'thinking': '🤔',
+  'lying_face': '🤥',
+  'hand_over_mouth': '🤭',
+  'shushing': '🤫',
+  'symbols_over_mouth': '🤬',
+  'exploding_head': '🤯',
+  'flushed': '😳',
+  'disappointed': '😞',
+  'worried': '😟',
+  'angry': '😠',
+  'rage': '😡',
+  'pensive': '😔',
+  'confused': '😕',
+  'slightly_frowning_face': '🙁',
+  'frowning_face': '☹',
+  'persevere': '😣',
+  'confounded': '😖',
+  'tired_face': '😫',
+  'weary': '😩',
+  'triumph': '😤',
+  'open_mouth': '😮',
+  'scream': '😱',
+  'fearful': '😨',
+  'cold_sweat': '😰',
+  'hushed': '😯',
+  'frowning': '😦',
+  'anguished': '😧',
+  'cry': '😢',
+  'disappointed_relieved': '😥',
+  'drooling_face': '🤤',
+  'sleepy': '😪',
+  'sweat': '😓',
+  'sob': '😭',
+  'dizzy_face': '😵',
+  'astonished': '😲',
+  'zipper_mouth_face': '🤐',
+  'nauseated_face': '🤢',
+  'sneezing_face': '🤧',
+  'vomiting': '🤮',
+  'mask': '😷',
+  'face_with_thermometer': '🤒',
+  'face_with_head_bandage': '🤕',
+  'sleeping': '😴',
+  'zzz': '💤',
+  'poop': '💩',
+  'smiling_imp': '😈',
+  'imp': '👿',
+  'japanese_ogre': '👹',
+  'japanese_goblin': '👺',
+  'skull': '💀',
+  'ghost': '👻',
+  'alien': '👽',
+  'robot': '🤖',
+  'smiley_cat': '😺',
+  'smile_cat': '😸',
+  'joy_cat': '😹',
+  'heart_eyes_cat': '😻',
+  'smirk_cat': '😼',
+  'kissing_cat': '😽',
+  'scream_cat': '🙀',
+  'crying_cat_face': '😿',
+  'pouting_cat': '😾',
+  'palms_up': '🤲',
+  'raised_hands': '🙌',
+  'clap': '👏',
+  'wave': '👋',
+  'call_me_hand': '🤙',
+  '+1': '👍',
+  '-1': '👎',
+  'facepunch': '👊',
+  'fist': '✊',
+  'fist_left': '🤛',
+  'fist_right': '🤜',
+  'v': '✌',
+  'ok_hand': '👌',
+  'raised_hand': '✋',
+  'raised_back_of_hand': '🤚',
+  'open_hands': '👐',
+  'muscle': '💪',
+  'pray': '🙏',
+  'handshake': '🤝',
+  'point_up': '☝',
+  'point_up_2': '👆',
+  'point_down': '👇',
+  'point_left': '👈',
+  'point_right': '👉',
+  'fu': '🖕',
+  'raised_hand_with_fingers_splayed': '🖐',
+  'love_you': '🤟',
+  'metal': '🤘',
+  'crossed_fingers': '🤞',
+  'vulcan_salute': '🖖',
+  'writing_hand': '✍',
+  'selfie': '🤳',
+  'nail_care': '💅',
+  'lips': '👄',
+  'tongue': '👅',
+  'ear': '👂',
+  'nose': '👃',
+  'eye': '👁',
+  'eyes': '👀',
+  'brain': '🧠',
+  'bust_in_silhouette': '👤',
+  'busts_in_silhouette': '👥',
+  'speaking_head': '🗣',
+  'baby': '👶',
+  'child': '🧒',
+  'boy': '👦',
+  'girl': '👧',
+  'adult': '🧑',
+  'man': '👨',
+  'woman': '👩',
+  'blonde_woman': '👱‍♀️',
+  'blonde_man': '👱',
+  'bearded_person': '🧔',
+  'older_adult': '🧓',
+  'older_man': '👴',
+  'older_woman': '👵',
+  'man_with_gua_pi_mao': '👲',
+  'woman_with_headscarf': '🧕',
+  'woman_with_turban': '👳‍♀️',
+  'man_with_turban': '👳',
+  'policewoman': '👮‍♀️',
+  'policeman': '👮',
+  'construction_worker_woman': '👷‍♀️',
+  'construction_worker_man': '👷',
+  'guardswoman': '💂‍♀️',
+  'guardsman': '💂',
+  'female_detective': '🕵️‍♀️',
+  'male_detective': '🕵',
+  'woman_health_worker': '👩‍⚕️',
+  'man_health_worker': '👨‍⚕️',
+  'woman_farmer': '👩‍🌾',
+  'man_farmer': '👨‍🌾',
+  'woman_cook': '👩‍🍳',
+  'man_cook': '👨‍🍳',
+  'woman_student': '👩‍🎓',
+  'man_student': '👨‍🎓',
+  'woman_singer': '👩‍🎤',
+  'man_singer': '👨‍🎤',
+  'woman_teacher': '👩‍🏫',
+  'man_teacher': '👨‍🏫',
+  'woman_factory_worker': '👩‍🏭',
+  'man_factory_worker': '👨‍🏭',
+  'woman_technologist': '👩‍💻',
+  'man_technologist': '👨‍💻',
+  'woman_office_worker': '👩‍💼',
+  'man_office_worker': '👨‍💼',
+  'woman_mechanic': '👩‍🔧',
+  'man_mechanic': '👨‍🔧',
+  'woman_scientist': '👩‍🔬',
+  'man_scientist': '👨‍🔬',
+  'woman_artist': '👩‍🎨',
+  'man_artist': '👨‍🎨',
+  'woman_firefighter': '👩‍🚒',
+  'man_firefighter': '👨‍🚒',
+  'woman_pilot': '👩‍✈️',
+  'man_pilot': '👨‍✈️',
+  'woman_astronaut': '👩‍🚀',
+  'man_astronaut': '👨‍🚀',
+  'woman_judge': '👩‍⚖️',
+  'man_judge': '👨‍⚖️',
+  'mrs_claus': '🤶',
+  'santa': '🎅',
+  'sorceress': '🧙‍♀️',
+  'wizard': '🧙‍♂️',
+  'woman_elf': '🧝‍♀️',
+  'man_elf': '🧝‍♂️',
+  'woman_vampire': '🧛‍♀️',
+  'man_vampire': '🧛‍♂️',
+  'woman_zombie': '🧟‍♀️',
+  'man_zombie': '🧟‍♂️',
+  'woman_genie': '🧞‍♀️',
+  'man_genie': '🧞‍♂️',
+  'mermaid': '🧜‍♀️',
+  'merman': '🧜‍♂️',
+  'woman_fairy': '🧚‍♀️',
+  'man_fairy': '🧚‍♂️',
+  'angel': '👼',
+  'pregnant_woman': '🤰',
+  'breastfeeding': '🤱',
+  'princess': '👸',
+  'prince': '🤴',
+  'bride_with_veil': '👰',
+  'man_in_tuxedo': '🤵',
+  'running_woman': '🏃‍♀️',
+  'running_man': '🏃',
+  'walking_woman': '🚶‍♀️',
+  'walking_man': '🚶',
+  'dancer': '💃',
+  'man_dancing': '🕺',
+  'dancing_women': '👯',
+  'dancing_men': '👯‍♂️',
+  'couple': '👫',
+  'two_men_holding_hands': '👬',
+  'two_women_holding_hands': '👭',
+  'bowing_woman': '🙇‍♀️',
+  'bowing_man': '🙇',
+  'man_facepalming': '🤦',
+  'woman_facepalming': '🤦‍♀️',
+  'woman_shrugging': '🤷',
+  'man_shrugging': '🤷‍♂️',
+  'tipping_hand_woman': '💁',
+  'tipping_hand_man': '💁‍♂️',
+  'no_good_woman': '🙅',
+  'no_good_man': '🙅‍♂️',
+  'ok_woman': '🙆',
+  'ok_man': '🙆‍♂️',
+  'raising_hand_woman': '🙋',
+  'raising_hand_man': '🙋‍♂️',
+  'pouting_woman': '🙎',
+  'pouting_man': '🙎‍♂️',
+  'frowning_woman': '🙍',
+  'frowning_man': '🙍‍♂️',
+  'haircut_woman': '💇',
+  'haircut_man': '💇‍♂️',
+  'massage_woman': '💆',
+  'massage_man': '💆‍♂️',
+  'woman_in_steamy_room': '🧖‍♀️',
+  'man_in_steamy_room': '🧖‍♂️',
+  'couple_with_heart_woman_man': '💑',
+  'couple_with_heart_woman_woman': '👩‍❤️‍👩',
+  'couple_with_heart_man_man': '👨‍❤️‍👨',
+  'couplekiss_man_woman': '💏',
+  'couplekiss_woman_woman': '👩‍❤️‍💋‍👩',
+  'couplekiss_man_man': '👨‍❤️‍💋‍👨',
+  'family_man_woman_boy': '👪',
+  'family_man_woman_girl': '👨‍👩‍👧',
+  'family_man_woman_girl_boy': '👨‍👩‍👧‍👦',
+  'family_man_woman_boy_boy': '👨‍👩‍👦‍👦',
+  'family_man_woman_girl_girl': '👨‍👩‍👧‍👧',
+  'family_woman_woman_boy': '👩‍👩‍👦',
+  'family_woman_woman_girl': '👩‍👩‍👧',
+  'family_woman_woman_girl_boy': '👩‍👩‍👧‍👦',
+  'family_woman_woman_boy_boy': '👩‍👩‍👦‍👦',
+  'family_woman_woman_girl_girl': '👩‍👩‍👧‍👧',
+  'family_man_man_boy': '👨‍👨‍👦',
+  'family_man_man_girl': '👨‍👨‍👧',
+  'family_man_man_girl_boy': '👨‍👨‍👧‍👦',
+  'family_man_man_boy_boy': '👨‍👨‍👦‍👦',
+  'family_man_man_girl_girl': '👨‍👨‍👧‍👧',
+  'family_woman_boy': '👩‍👦',
+  'family_woman_girl': '👩‍👧',
+  'family_woman_girl_boy': '👩‍👧‍👦',
+  'family_woman_boy_boy': '👩‍👦‍👦',
+  'family_woman_girl_girl': '👩‍👧‍👧',
+  'family_man_boy': '👨‍👦',
+  'family_man_girl': '👨‍👧',
+  'family_man_girl_boy': '👨‍👧‍👦',
+  'family_man_boy_boy': '👨‍👦‍👦',
+  'family_man_girl_girl': '👨‍👧‍👧',
+  'coat': '🧥',
+  'womans_clothes': '👚',
+  'tshirt': '👕',
+  'jeans': '👖',
+  'necktie': '👔',
+  'dress': '👗',
+  'bikini': '👙',
+  'kimono': '👘',
+  'lipstick': '💄',
+  'kiss': '💋',
+  'footprints': '👣',
+  'high_heel': '👠',
+  'sandal': '👡',
+  'boot': '👢',
+  'mans_shoe': '👞',
+  'athletic_shoe': '👟',
+  'socks': '🧦',
+  'gloves': '🧤',
+  'scarf': '🧣',
+  'womans_hat': '👒',
+  'tophat': '🎩',
+  'billed_hat': '🧢',
+  'rescue_worker_helmet': '⛑',
+  'mortar_board': '🎓',
+  'crown': '👑',
+  'school_satchel': '🎒',
+  'pouch': '👝',
+  'purse': '👛',
+  'handbag': '👜',
+  'briefcase': '💼',
+  'eyeglasses': '👓',
+  'dark_sunglasses': '🕶',
+  'ring': '💍',
+  'closed_umbrella': '🌂',
+  'dog': '🐶',
+  'cat': '🐱',
+  'mouse': '🐭',
+  'hamster': '🐹',
+  'rabbit': '🐰',
+  'fox_face': '🦊',
+  'bear': '🐻',
+  'panda_face': '🐼',
+  'koala': '🐨',
+  'tiger': '🐯',
+  'lion': '🦁',
+  'cow': '🐮',
+  'pig': '🐷',
+  'pig_nose': '🐽',
+  'frog': '🐸',
+  'squid': '🦑',
+  'octopus': '🐙',
+  'shrimp': '🦐',
+  'monkey_face': '🐵',
+  'gorilla': '🦍',
+  'see_no_evil': '🙈',
+  'hear_no_evil': '🙉',
+  'speak_no_evil': '🙊',
+  'monkey': '🐒',
+  'chicken': '🐔',
+  'penguin': '🐧',
+  'bird': '🐦',
+  'baby_chick': '🐤',
+  'hatching_chick': '🐣',
+  'hatched_chick': '🐥',
+  'duck': '🦆',
+  'eagle': '🦅',
+  'owl': '🦉',
+  'bat': '🦇',
+  'wolf': '🐺',
+  'boar': '🐗',
+  'horse': '🐴',
+  'unicorn': '🦄',
+  'honeybee': '🐝',
+  'bug': '🐛',
+  'butterfly': '🦋',
+  'snail': '🐌',
+  'beetle': '🐞',
+  'ant': '🐜',
+  'grasshopper': '🦗',
+  'spider': '🕷',
+  'scorpion': '🦂',
+  'crab': '🦀',
+  'snake': '🐍',
+  'lizard': '🦎',
+  't-rex': '🦖',
+  'sauropod': '🦕',
+  'turtle': '🐢',
+  'tropical_fish': '🐠',
+  'fish': '🐟',
+  'blowfish': '🐡',
+  'dolphin': '🐬',
+  'shark': '🦈',
+  'whale': '🐳',
+  'whale2': '🐋',
+  'crocodile': '🐊',
+  'leopard': '🐆',
+  'zebra': '🦓',
+  'tiger2': '🐅',
+  'water_buffalo': '🐃',
+  'ox': '🐂',
+  'cow2': '🐄',
+  'deer': '🦌',
+  'dromedary_camel': '🐪',
+  'camel': '🐫',
+  'giraffe': '🦒',
+  'elephant': '🐘',
+  'rhinoceros': '🦏',
+  'goat': '🐐',
+  'ram': '🐏',
+  'sheep': '🐑',
+  'racehorse': '🐎',
+  'pig2': '🐖',
+  'rat': '🐀',
+  'mouse2': '🐁',
+  'rooster': '🐓',
+  'turkey': '🦃',
+  'dove': '🕊',
+  'dog2': '🐕',
+  'poodle': '🐩',
+  'cat2': '🐈',
+  'rabbit2': '🐇',
+  'chipmunk': '🐿',
+  'hedgehog': '🦔',
+  'paw_prints': '🐾',
+  'dragon': '🐉',
+  'dragon_face': '🐲',
+  'cactus': '🌵',
+  'christmas_tree': '🎄',
+  'evergreen_tree': '🌲',
+  'deciduous_tree': '🌳',
+  'palm_tree': '🌴',
+  'seedling': '🌱',
+  'herb': '🌿',
+  'shamrock': '☘',
+  'four_leaf_clover': '🍀',
+  'bamboo': '🎍',
+  'tanabata_tree': '🎋',
+  'leaves': '🍃',
+  'fallen_leaf': '🍂',
+  'maple_leaf': '🍁',
+  'ear_of_rice': '🌾',
+  'hibiscus': '🌺',
+  'sunflower': '🌻',
+  'rose': '🌹',
+  'wilted_flower': '🥀',
+  'tulip': '🌷',
+  'blossom': '🌼',
+  'cherry_blossom': '🌸',
+  'bouquet': '💐',
+  'mushroom': '🍄',
+  'chestnut': '🌰',
+  'jack_o_lantern': '🎃',
+  'shell': '🐚',
+  'spider_web': '🕸',
+  'earth_americas': '🌎',
+  'earth_africa': '🌍',
+  'earth_asia': '🌏',
+  'full_moon': '🌕',
+  'waning_gibbous_moon': '🌖',
+  'last_quarter_moon': '🌗',
+  'waning_crescent_moon': '🌘',
+  'new_moon': '🌑',
+  'waxing_crescent_moon': '🌒',
+  'first_quarter_moon': '🌓',
+  'waxing_gibbous_moon': '🌔',
+  'new_moon_with_face': '🌚',
+  'full_moon_with_face': '🌝',
+  'first_quarter_moon_with_face': '🌛',
+  'last_quarter_moon_with_face': '🌜',
+  'sun_with_face': '🌞',
+  'crescent_moon': '🌙',
+  'star': '⭐',
+  'star2': '🌟',
+  'dizzy': '💫',
+  'sparkles': '✨',
+  'comet': '☄',
+  'sunny': '☀️',
+  'sun_behind_small_cloud': '🌤',
+  'partly_sunny': '⛅',
+  'sun_behind_large_cloud': '🌥',
+  'sun_behind_rain_cloud': '🌦',
+  'cloud': '☁️',
+  'cloud_with_rain': '🌧',
+  'cloud_with_lightning_and_rain': '⛈',
+  'cloud_with_lightning': '🌩',
+  'zap': '⚡',
+  'fire': '🔥',
+  'boom': '💥',
+  'snowflake': '❄️',
+  'cloud_with_snow': '🌨',
+  'snowman': '⛄',
+  'snowman_with_snow': '☃',
+  'wind_face': '🌬',
+  'dash': '💨',
+  'tornado': '🌪',
+  'fog': '🌫',
+  'open_umbrella': '☂',
+  'umbrella': '☔',
+  'droplet': '💧',
+  'sweat_drops': '💦',
+  'ocean': '🌊',
+  'green_apple': '🍏',
+  'apple': '🍎',
+  'pear': '🍐',
+  'tangerine': '🍊',
+  'lemon': '🍋',
+  'banana': '🍌',
+  'watermelon': '🍉',
+  'grapes': '🍇',
+  'strawberry': '🍓',
+  'melon': '🍈',
+  'cherries': '🍒',
+  'peach': '🍑',
+  'pineapple': '🍍',
+  'coconut': '🥥',
+  'kiwi_fruit': '🥝',
+  'avocado': '🥑',
+  'broccoli': '🥦',
+  'tomato': '🍅',
+  'eggplant': '🍆',
+  'cucumber': '🥒',
+  'carrot': '🥕',
+  'hot_pepper': '🌶',
+  'potato': '🥔',
+  'corn': '🌽',
+  'sweet_potato': '🍠',
+  'peanuts': '🥜',
+  'honey_pot': '🍯',
+  'croissant': '🥐',
+  'bread': '🍞',
+  'baguette_bread': '🥖',
+  'pretzel': '🥨',
+  'cheese': '🧀',
+  'egg': '🥚',
+  'bacon': '🥓',
+  'steak': '🥩',
+  'pancakes': '🥞',
+  'poultry_leg': '🍗',
+  'meat_on_bone': '🍖',
+  'fried_shrimp': '🍤',
+  'fried_egg': '🍳',
+  'hamburger': '🍔',
+  'fries': '🍟',
+  'stuffed_flatbread': '🥙',
+  'hotdog': '🌭',
+  'pizza': '🍕',
+  'sandwich': '🥪',
+  'canned_food': '🥫',
+  'spaghetti': '🍝',
+  'taco': '🌮',
+  'burrito': '🌯',
+  'green_salad': '🥗',
+  'shallow_pan_of_food': '🥘',
+  'ramen': '🍜',
+  'stew': '🍲',
+  'fish_cake': '🍥',
+  'fortune_cookie': '🥠',
+  'sushi': '🍣',
+  'bento': '🍱',
+  'curry': '🍛',
+  'rice_ball': '🍙',
+  'rice': '🍚',
+  'rice_cracker': '🍘',
+  'oden': '🍢',
+  'dango': '🍡',
+  'shaved_ice': '🍧',
+  'ice_cream': '🍨',
+  'icecream': '🍦',
+  'pie': '🥧',
+  'cake': '🍰',
+  'birthday': '🎂',
+  'custard': '🍮',
+  'candy': '🍬',
+  'lollipop': '🍭',
+  'chocolate_bar': '🍫',
+  'popcorn': '🍿',
+  'dumpling': '🥟',
+  'doughnut': '🍩',
+  'cookie': '🍪',
+  'milk_glass': '🥛',
+  'beer': '🍺',
+  'beers': '🍻',
+  'clinking_glasses': '🥂',
+  'wine_glass': '🍷',
+  'tumbler_glass': '🥃',
+  'cocktail': '🍸',
+  'tropical_drink': '🍹',
+  'champagne': '🍾',
+  'sake': '🍶',
+  'tea': '🍵',
+  'cup_with_straw': '🥤',
+  'coffee': '☕',
+  'baby_bottle': '🍼',
+  'spoon': '🥄',
+  'fork_and_knife': '🍴',
+  'plate_with_cutlery': '🍽',
+  'bowl_with_spoon': '🥣',
+  'takeout_box': '🥡',
+  'chopsticks': '🥢',
+  'soccer': '⚽',
+  'basketball': '🏀',
+  'football': '🏈',
+  'baseball': '⚾',
+  'tennis': '🎾',
+  'volleyball': '🏐',
+  'rugby_football': '🏉',
+  '8ball': '🎱',
+  'golf': '⛳',
+  'golfing_woman': '🏌️‍♀️',
+  'golfing_man': '🏌',
+  'ping_pong': '🏓',
+  'badminton': '🏸',
+  'goal_net': '🥅',
+  'ice_hockey': '🏒',
+  'field_hockey': '🏑',
+  'cricket': '🏏',
+  'ski': '🎿',
+  'skier': '⛷',
+  'snowboarder': '🏂',
+  'person_fencing': '🤺',
+  'women_wrestling': '🤼‍♀️',
+  'men_wrestling': '🤼‍♂️',
+  'woman_cartwheeling': '🤸‍♀️',
+  'man_cartwheeling': '🤸‍♂️',
+  'woman_playing_handball': '🤾‍♀️',
+  'man_playing_handball': '🤾‍♂️',
+  'ice_skate': '⛸',
+  'curling_stone': '🥌',
+  'sled': '🛷',
+  'bow_and_arrow': '🏹',
+  'fishing_pole_and_fish': '🎣',
+  'boxing_glove': '🥊',
+  'martial_arts_uniform': '🥋',
+  'rowing_woman': '🚣‍♀️',
+  'rowing_man': '🚣',
+  'climbing_woman': '🧗‍♀️',
+  'climbing_man': '🧗‍♂️',
+  'swimming_woman': '🏊‍♀️',
+  'swimming_man': '🏊',
+  'woman_playing_water_polo': '🤽‍♀️',
+  'man_playing_water_polo': '🤽‍♂️',
+  'woman_in_lotus_position': '🧘‍♀️',
+  'man_in_lotus_position': '🧘‍♂️',
+  'surfing_woman': '🏄‍♀️',
+  'surfing_man': '🏄',
+  'bath': '🛀',
+  'basketball_woman': '⛹️‍♀️',
+  'basketball_man': '⛹',
+  'weight_lifting_woman': '🏋️‍♀️',
+  'weight_lifting_man': '🏋',
+  'biking_woman': '🚴‍♀️',
+  'biking_man': '🚴',
+  'mountain_biking_woman': '🚵‍♀️',
+  'mountain_biking_man': '🚵',
+  'horse_racing': '🏇',
+  'business_suit_levitating': '🕴',
+  'trophy': '🏆',
+  'running_shirt_with_sash': '🎽',
+  'medal_sports': '🏅',
+  'medal_military': '🎖',
+  '1st_place_medal': '🥇',
+  '2nd_place_medal': '🥈',
+  '3rd_place_medal': '🥉',
+  'reminder_ribbon': '🎗',
+  'rosette': '🏵',
+  'ticket': '🎫',
+  'tickets': '🎟',
+  'performing_arts': '🎭',
+  'art': '🎨',
+  'circus_tent': '🎪',
+  'woman_juggling': '🤹‍♀️',
+  'man_juggling': '🤹‍♂️',
+  'microphone': '🎤',
+  'headphones': '🎧',
+  'musical_score': '🎼',
+  'musical_keyboard': '🎹',
+  'drum': '🥁',
+  'saxophone': '🎷',
+  'trumpet': '🎺',
+  'guitar': '🎸',
+  'violin': '🎻',
+  'clapper': '🎬',
+  'video_game': '🎮',
+  'space_invader': '👾',
+  'dart': '🎯',
+  'game_die': '🎲',
+  'slot_machine': '🎰',
+  'bowling': '🎳',
+  'red_car': '🚗',
+  'taxi': '🚕',
+  'blue_car': '🚙',
+  'bus': '🚌',
+  'trolleybus': '🚎',
+  'racing_car': '🏎',
+  'police_car': '🚓',
+  'ambulance': '🚑',
+  'fire_engine': '🚒',
+  'minibus': '🚐',
+  'truck': '🚚',
+  'articulated_lorry': '🚛',
+  'tractor': '🚜',
+  'kick_scooter': '🛴',
+  'motorcycle': '🏍',
+  'bike': '🚲',
+  'motor_scooter': '🛵',
+  'rotating_light': '🚨',
+  'oncoming_police_car': '🚔',
+  'oncoming_bus': '🚍',
+  'oncoming_automobile': '🚘',
+  'oncoming_taxi': '🚖',
+  'aerial_tramway': '🚡',
+  'mountain_cableway': '🚠',
+  'suspension_railway': '🚟',
+  'railway_car': '🚃',
+  'train': '🚋',
+  'monorail': '🚝',
+  'bullettrain_side': '🚄',
+  'bullettrain_front': '🚅',
+  'light_rail': '🚈',
+  'mountain_railway': '🚞',
+  'steam_locomotive': '🚂',
+  'train2': '🚆',
+  'metro': '🚇',
+  'tram': '🚊',
+  'station': '🚉',
+  'flying_saucer': '🛸',
+  'helicopter': '🚁',
+  'small_airplane': '🛩',
+  'airplane': '✈️',
+  'flight_departure': '🛫',
+  'flight_arrival': '🛬',
+  'sailboat': '⛵',
+  'motor_boat': '🛥',
+  'speedboat': '🚤',
+  'ferry': '⛴',
+  'passenger_ship': '🛳',
+  'rocket': '🚀',
+  'artificial_satellite': '🛰',
+  'seat': '💺',
+  'canoe': '🛶',
+  'anchor': '⚓',
+  'construction': '🚧',
+  'fuelpump': '⛽',
+  'busstop': '🚏',
+  'vertical_traffic_light': '🚦',
+  'traffic_light': '🚥',
+  'checkered_flag': '🏁',
+  'ship': '🚢',
+  'ferris_wheel': '🎡',
+  'roller_coaster': '🎢',
+  'carousel_horse': '🎠',
+  'building_construction': '🏗',
+  'foggy': '🌁',
+  'tokyo_tower': '🗼',
+  'factory': '🏭',
+  'fountain': '⛲',
+  'rice_scene': '🎑',
+  'mountain': '⛰',
+  'mountain_snow': '🏔',
+  'mount_fuji': '🗻',
+  'volcano': '🌋',
+  'japan': '🗾',
+  'camping': '🏕',
+  'tent': '⛺',
+  'national_park': '🏞',
+  'motorway': '🛣',
+  'railway_track': '🛤',
+  'sunrise': '🌅',
+  'sunrise_over_mountains': '🌄',
+  'desert': '🏜',
+  'beach_umbrella': '🏖',
+  'desert_island': '🏝',
+  'city_sunrise': '🌇',
+  'city_sunset': '🌆',
+  'cityscape': '🏙',
+  'night_with_stars': '🌃',
+  'bridge_at_night': '🌉',
+  'milky_way': '🌌',
+  'stars': '🌠',
+  'sparkler': '🎇',
+  'fireworks': '🎆',
+  'rainbow': '🌈',
+  'houses': '🏘',
+  'european_castle': '🏰',
+  'japanese_castle': '🏯',
+  'stadium': '🏟',
+  'statue_of_liberty': '🗽',
+  'house': '🏠',
+  'house_with_garden': '🏡',
+  'derelict_house': '🏚',
+  'office': '🏢',
+  'department_store': '🏬',
+  'post_office': '🏣',
+  'european_post_office': '🏤',
+  'hospital': '🏥',
+  'bank': '🏦',
+  'hotel': '🏨',
+  'convenience_store': '🏪',
+  'school': '🏫',
+  'love_hotel': '🏩',
+  'wedding': '💒',
+  'classical_building': '🏛',
+  'church': '⛪',
+  'mosque': '🕌',
+  'synagogue': '🕍',
+  'kaaba': '🕋',
+  'shinto_shrine': '⛩',
+  'watch': '⌚',
+  'iphone': '📱',
+  'calling': '📲',
+  'computer': '💻',
+  'keyboard': '⌨',
+  'desktop_computer': '🖥',
+  'printer': '🖨',
+  'computer_mouse': '🖱',
+  'trackball': '🖲',
+  'joystick': '🕹',
+  'clamp': '🗜',
+  'minidisc': '💽',
+  'floppy_disk': '💾',
+  'cd': '💿',
+  'dvd': '📀',
+  'vhs': '📼',
+  'camera': '📷',
+  'camera_flash': '📸',
+  'video_camera': '📹',
+  'movie_camera': '🎥',
+  'film_projector': '📽',
+  'film_strip': '🎞',
+  'telephone_receiver': '📞',
+  'phone': '☎️',
+  'pager': '📟',
+  'fax': '📠',
+  'tv': '📺',
+  'radio': '📻',
+  'studio_microphone': '🎙',
+  'level_slider': '🎚',
+  'control_knobs': '🎛',
+  'stopwatch': '⏱',
+  'timer_clock': '⏲',
+  'alarm_clock': '⏰',
+  'mantelpiece_clock': '🕰',
+  'hourglass_flowing_sand': '⏳',
+  'hourglass': '⌛',
+  'satellite': '📡',
+  'battery': '🔋',
+  'electric_plug': '🔌',
+  'bulb': '💡',
+  'flashlight': '🔦',
+  'candle': '🕯',
+  'wastebasket': '🗑',
+  'oil_drum': '🛢',
+  'money_with_wings': '💸',
+  'dollar': '💵',
+  'yen': '💴',
+  'euro': '💶',
+  'pound': '💷',
+  'moneybag': '💰',
+  'credit_card': '💳',
+  'gem': '💎',
+  'balance_scale': '⚖',
+  'wrench': '🔧',
+  'hammer': '🔨',
+  'hammer_and_pick': '⚒',
+  'hammer_and_wrench': '🛠',
+  'pick': '⛏',
+  'nut_and_bolt': '🔩',
+  'gear': '⚙',
+  'chains': '⛓',
+  'gun': '🔫',
+  'bomb': '💣',
+  'hocho': '🔪',
+  'dagger': '🗡',
+  'crossed_swords': '⚔',
+  'shield': '🛡',
+  'smoking': '🚬',
+  'skull_and_crossbones': '☠',
+  'coffin': '⚰',
+  'funeral_urn': '⚱',
+  'amphora': '🏺',
+  'crystal_ball': '🔮',
+  'prayer_beads': '📿',
+  'barber': '💈',
+  'alembic': '⚗',
+  'telescope': '🔭',
+  'microscope': '🔬',
+  'hole': '🕳',
+  'pill': '💊',
+  'syringe': '💉',
+  'thermometer': '🌡',
+  'label': '🏷',
+  'bookmark': '🔖',
+  'toilet': '🚽',
+  'shower': '🚿',
+  'bathtub': '🛁',
+  'key': '🔑',
+  'old_key': '🗝',
+  'couch_and_lamp': '🛋',
+  'sleeping_bed': '🛌',
+  'bed': '🛏',
+  'door': '🚪',
+  'bellhop_bell': '🛎',
+  'framed_picture': '🖼',
+  'world_map': '🗺',
+  'parasol_on_ground': '⛱',
+  'moyai': '🗿',
+  'shopping': '🛍',
+  'shopping_cart': '🛒',
+  'balloon': '🎈',
+  'flags': '🎏',
+  'ribbon': '🎀',
+  'gift': '🎁',
+  'confetti_ball': '🎊',
+  'tada': '🎉',
+  'dolls': '🎎',
+  'wind_chime': '🎐',
+  'crossed_flags': '🎌',
+  'izakaya_lantern': '🏮',
+  'email': '✉️',
+  'envelope_with_arrow': '📩',
+  'incoming_envelope': '📨',
+  'e-mail': '📧',
+  'love_letter': '💌',
+  'postbox': '📮',
+  'mailbox_closed': '📪',
+  'mailbox': '📫',
+  'mailbox_with_mail': '📬',
+  'mailbox_with_no_mail': '📭',
+  'package': '📦',
+  'postal_horn': '📯',
+  'inbox_tray': '📥',
+  'outbox_tray': '📤',
+  'scroll': '📜',
+  'page_with_curl': '📃',
+  'bookmark_tabs': '📑',
+  'bar_chart': '📊',
+  'chart_with_upwards_trend': '📈',
+  'chart_with_downwards_trend': '📉',
+  'page_facing_up': '📄',
+  'date': '📅',
+  'calendar': '📆',
+  'spiral_calendar': '🗓',
+  'card_index': '📇',
+  'card_file_box': '🗃',
+  'ballot_box': '🗳',
+  'file_cabinet': '🗄',
+  'clipboard': '📋',
+  'spiral_notepad': '🗒',
+  'file_folder': '📁',
+  'open_file_folder': '📂',
+  'card_index_dividers': '🗂',
+  'newspaper_roll': '🗞',
+  'newspaper': '📰',
+  'notebook': '📓',
+  'closed_book': '📕',
+  'green_book': '📗',
+  'blue_book': '📘',
+  'orange_book': '📙',
+  'notebook_with_decorative_cover': '📔',
+  'ledger': '📒',
+  'books': '📚',
+  'open_book': '📖',
+  'link': '🔗',
+  'paperclip': '📎',
+  'paperclips': '🖇',
+  'scissors': '✂️',
+  'triangular_ruler': '📐',
+  'straight_ruler': '📏',
+  'pushpin': '📌',
+  'round_pushpin': '📍',
+  'triangular_flag_on_post': '🚩',
+  'white_flag': '🏳',
+  'black_flag': '🏴',
+  'rainbow_flag': '🏳️‍🌈',
+  'closed_lock_with_key': '🔐',
+  'lock': '🔒',
+  'unlock': '🔓',
+  'lock_with_ink_pen': '🔏',
+  'pen': '🖊',
+  'fountain_pen': '🖋',
+  'black_nib': '✒️',
+  'memo': '📝',
+  'pencil2': '✏️',
+  'crayon': '🖍',
+  'paintbrush': '🖌',
+  'mag': '🔍',
+  'mag_right': '🔎',
+  'heart': '❤️',
+  'orange_heart': '🧡',
+  'yellow_heart': '💛',
+  'green_heart': '💚',
+  'blue_heart': '💙',
+  'purple_heart': '💜',
+  'black_heart': '🖤',
+  'broken_heart': '💔',
+  'heavy_heart_exclamation': '❣',
+  'two_hearts': '💕',
+  'revolving_hearts': '💞',
+  'heartbeat': '💓',
+  'heartpulse': '💗',
+  'sparkling_heart': '💖',
+  'cupid': '💘',
+  'gift_heart': '💝',
+  'heart_decoration': '💟',
+  'peace_symbol': '☮',
+  'latin_cross': '✝',
+  'star_and_crescent': '☪',
+  'om': '🕉',
+  'wheel_of_dharma': '☸',
+  'star_of_david': '✡',
+  'six_pointed_star': '🔯',
+  'menorah': '🕎',
+  'yin_yang': '☯',
+  'orthodox_cross': '☦',
+  'place_of_worship': '🛐',
+  'ophiuchus': '⛎',
+  'aries': '♈',
+  'taurus': '♉',
+  'gemini': '♊',
+  'cancer': '♋',
+  'leo': '♌',
+  'virgo': '♍',
+  'libra': '♎',
+  'scorpius': '♏',
+  'sagittarius': '♐',
+  'capricorn': '♑',
+  'aquarius': '♒',
+  'pisces': '♓',
+  'id': '🆔',
+  'atom_symbol': '⚛',
+  'u7a7a': '🈳',
+  'u5272': '🈹',
+  'radioactive': '☢',
+  'biohazard': '☣',
+  'mobile_phone_off': '📴',
+  'vibration_mode': '📳',
+  'u6709': '🈶',
+  'u7121': '🈚',
+  'u7533': '🈸',
+  'u55b6': '🈺',
+  'u6708': '🈷️',
+  'eight_pointed_black_star': '✴️',
+  'vs': '🆚',
+  'accept': '🉑',
+  'white_flower': '💮',
+  'ideograph_advantage': '🉐',
+  'secret': '㊙️',
+  'congratulations': '㊗️',
+  'u5408': '🈴',
+  'u6e80': '🈵',
+  'u7981': '🈲',
+  'a': '🅰️',
+  'b': '🅱️',
+  'ab': '🆎',
+  'cl': '🆑',
+  'o2': '🅾️',
+  'sos': '🆘',
+  'no_entry': '⛔',
+  'name_badge': '📛',
+  'no_entry_sign': '🚫',
+  'x': '❌',
+  'o': '⭕',
+  'stop_sign': '🛑',
+  'anger': '💢',
+  'hotsprings': '♨️',
+  'no_pedestrians': '🚷',
+  'do_not_litter': '🚯',
+  'no_bicycles': '🚳',
+  'non-potable_water': '🚱',
+  'underage': '🔞',
+  'no_mobile_phones': '📵',
+  'exclamation': '❗',
+  'grey_exclamation': '❕',
+  'question': '❓',
+  'grey_question': '❔',
+  'bangbang': '‼️',
+  'interrobang': '⁉️',
+  '100': '💯',
+  'low_brightness': '🔅',
+  'high_brightness': '🔆',
+  'trident': '🔱',
+  'fleur_de_lis': '⚜',
+  'part_alternation_mark': '〽️',
+  'warning': '⚠️',
+  'children_crossing': '🚸',
+  'beginner': '🔰',
+  'recycle': '♻️',
+  'u6307': '🈯',
+  'chart': '💹',
+  'sparkle': '❇️',
+  'eight_spoked_asterisk': '✳️',
+  'negative_squared_cross_mark': '❎',
+  'white_check_mark': '✅',
+  'diamond_shape_with_a_dot_inside': '💠',
+  'cyclone': '🌀',
+  'loop': '➿',
+  'globe_with_meridians': '🌐',
+  'm': 'Ⓜ️',
+  'atm': '🏧',
+  'sa': '🈂️',
+  'passport_control': '🛂',
+  'customs': '🛃',
+  'baggage_claim': '🛄',
+  'left_luggage': '🛅',
+  'wheelchair': '♿',
+  'no_smoking': '🚭',
+  'wc': '🚾',
+  'parking': '🅿️',
+  'potable_water': '🚰',
+  'mens': '🚹',
+  'womens': '🚺',
+  'baby_symbol': '🚼',
+  'restroom': '🚻',
+  'put_litter_in_its_place': '🚮',
+  'cinema': '🎦',
+  'signal_strength': '📶',
+  'koko': '🈁',
+  'ng': '🆖',
+  'ok': '🆗',
+  'up': '🆙',
+  'cool': '🆒',
+  'new': '🆕',
+  'free': '🆓',
+  'zero': '0️⃣',
+  'one': '1️⃣',
+  'two': '2️⃣',
+  'three': '3️⃣',
+  'four': '4️⃣',
+  'five': '5️⃣',
+  'six': '6️⃣',
+  'seven': '7️⃣',
+  'eight': '8️⃣',
+  'nine': '9️⃣',
+  'keycap_ten': '🔟',
+  'asterisk': '*⃣',
+  '1234': '🔢',
+  'eject_button': '⏏️',
+  'arrow_forward': '▶️',
+  'pause_button': '⏸',
+  'next_track_button': '⏭',
+  'stop_button': '⏹',
+  'record_button': '⏺',
+  'play_or_pause_button': '⏯',
+  'previous_track_button': '⏮',
+  'fast_forward': '⏩',
+  'rewind': '⏪',
+  'twisted_rightwards_arrows': '🔀',
+  'repeat': '🔁',
+  'repeat_one': '🔂',
+  'arrow_backward': '◀️',
+  'arrow_up_small': '🔼',
+  'arrow_down_small': '🔽',
+  'arrow_double_up': '⏫',
+  'arrow_double_down': '⏬',
+  'arrow_right': '➡️',
+  'arrow_left': '⬅️',
+  'arrow_up': '⬆️',
+  'arrow_down': '⬇️',
+  'arrow_upper_right': '↗️',
+  'arrow_lower_right': '↘️',
+  'arrow_lower_left': '↙️',
+  'arrow_upper_left': '↖️',
+  'arrow_up_down': '↕️',
+  'left_right_arrow': '↔️',
+  'arrows_counterclockwise': '🔄',
+  'arrow_right_hook': '↪️',
+  'leftwards_arrow_with_hook': '↩️',
+  'arrow_heading_up': '⤴️',
+  'arrow_heading_down': '⤵️',
+  'hash': '#️⃣',
+  'information_source': 'ℹ️',
+  'abc': '🔤',
+  'abcd': '🔡',
+  'capital_abcd': '🔠',
+  'symbols': '🔣',
+  'musical_note': '🎵',
+  'notes': '🎶',
+  'wavy_dash': '〰️',
+  'curly_loop': '➰',
+  'heavy_check_mark': '✔️',
+  'arrows_clockwise': '🔃',
+  'heavy_plus_sign': '➕',
+  'heavy_minus_sign': '➖',
+  'heavy_division_sign': '➗',
+  'heavy_multiplication_x': '✖️',
+  'heavy_dollar_sign': '💲',
+  'currency_exchange': '💱',
+  'copyright': '©️',
+  'registered': '®️',
+  'tm': '™️',
+  'end': '🔚',
+  'back': '🔙',
+  'on': '🔛',
+  'top': '🔝',
+  'soon': '🔜',
+  'ballot_box_with_check': '☑️',
+  'radio_button': '🔘',
+  'white_circle': '⚪',
+  'black_circle': '⚫',
+  'red_circle': '🔴',
+  'large_blue_circle': '🔵',
+  'small_orange_diamond': '🔸',
+  'small_blue_diamond': '🔹',
+  'large_orange_diamond': '🔶',
+  'large_blue_diamond': '🔷',
+  'small_red_triangle': '🔺',
+  'black_small_square': '▪️',
+  'white_small_square': '▫️',
+  'black_large_square': '⬛',
+  'white_large_square': '⬜',
+  'small_red_triangle_down': '🔻',
+  'black_medium_square': '◼️',
+  'white_medium_square': '◻️',
+  'black_medium_small_square': '◾',
+  'white_medium_small_square': '◽',
+  'black_square_button': '🔲',
+  'white_square_button': '🔳',
+  'speaker': '🔈',
+  'sound': '🔉',
+  'loud_sound': '🔊',
+  'mute': '🔇',
+  'mega': '📣',
+  'loudspeaker': '📢',
+  'bell': '🔔',
+  'no_bell': '🔕',
+  'black_joker': '🃏',
+  'mahjong': '🀄',
+  'spades': '♠️',
+  'clubs': '♣️',
+  'hearts': '♥️',
+  'diamonds': '♦️',
+  'flower_playing_cards': '🎴',
+  'thought_balloon': '💭',
+  'right_anger_bubble': '🗯',
+  'speech_balloon': '💬',
+  'left_speech_bubble': '🗨',
+  'clock1': '🕐',
+  'clock2': '🕑',
+  'clock3': '🕒',
+  'clock4': '🕓',
+  'clock5': '🕔',
+  'clock6': '🕕',
+  'clock7': '🕖',
+  'clock8': '🕗',
+  'clock9': '🕘',
+  'clock10': '🕙',
+  'clock11': '🕚',
+  'clock12': '🕛',
+  'clock130': '🕜',
+  'clock230': '🕝',
+  'clock330': '🕞',
+  'clock430': '🕟',
+  'clock530': '🕠',
+  'clock630': '🕡',
+  'clock730': '🕢',
+  'clock830': '🕣',
+  'clock930': '🕤',
+  'clock1030': '🕥',
+  'clock1130': '🕦',
+  'clock1230': '🕧',
+  'afghanistan': '🇦🇫',
+  'aland_islands': '🇦🇽',
+  'albania': '🇦🇱',
+  'algeria': '🇩🇿',
+  'american_samoa': '🇦🇸',
+  'andorra': '🇦🇩',
+  'angola': '🇦🇴',
+  'anguilla': '🇦🇮',
+  'antarctica': '🇦🇶',
+  'antigua_barbuda': '🇦🇬',
+  'argentina': '🇦🇷',
+  'armenia': '🇦🇲',
+  'aruba': '🇦🇼',
+  'australia': '🇦🇺',
+  'austria': '🇦🇹',
+  'azerbaijan': '🇦🇿',
+  'bahamas': '🇧🇸',
+  'bahrain': '🇧🇭',
+  'bangladesh': '🇧🇩',
+  'barbados': '🇧🇧',
+  'belarus': '🇧🇾',
+  'belgium': '🇧🇪',
+  'belize': '🇧🇿',
+  'benin': '🇧🇯',
+  'bermuda': '🇧🇲',
+  'bhutan': '🇧🇹',
+  'bolivia': '🇧🇴',
+  'caribbean_netherlands': '🇧🇶',
+  'bosnia_herzegovina': '🇧🇦',
+  'botswana': '🇧🇼',
+  'brazil': '🇧🇷',
+  'british_indian_ocean_territory': '🇮🇴',
+  'british_virgin_islands': '🇻🇬',
+  'brunei': '🇧🇳',
+  'bulgaria': '🇧🇬',
+  'burkina_faso': '🇧🇫',
+  'burundi': '🇧🇮',
+  'cape_verde': '🇨🇻',
+  'cambodia': '🇰🇭',
+  'cameroon': '🇨🇲',
+  'canada': '🇨🇦',
+  'canary_islands': '🇮🇨',
+  'cayman_islands': '🇰🇾',
+  'central_african_republic': '🇨🇫',
+  'chad': '🇹🇩',
+  'chile': '🇨🇱',
+  'cn': '🇨🇳',
+  'christmas_island': '🇨🇽',
+  'cocos_islands': '🇨🇨',
+  'colombia': '🇨🇴',
+  'comoros': '🇰🇲',
+  'congo_brazzaville': '🇨🇬',
+  'congo_kinshasa': '🇨🇩',
+  'cook_islands': '🇨🇰',
+  'costa_rica': '🇨🇷',
+  'croatia': '🇭🇷',
+  'cuba': '🇨🇺',
+  'curacao': '🇨🇼',
+  'cyprus': '🇨🇾',
+  'czech_republic': '🇨🇿',
+  'denmark': '🇩🇰',
+  'djibouti': '🇩🇯',
+  'dominica': '🇩🇲',
+  'dominican_republic': '🇩🇴',
+  'ecuador': '🇪🇨',
+  'egypt': '🇪🇬',
+  'el_salvador': '🇸🇻',
+  'equatorial_guinea': '🇬🇶',
+  'eritrea': '🇪🇷',
+  'estonia': '🇪🇪',
+  'ethiopia': '🇪🇹',
+  'eu': '🇪🇺',
+  'falkland_islands': '🇫🇰',
+  'faroe_islands': '🇫🇴',
+  'fiji': '🇫🇯',
+  'finland': '🇫🇮',
+  'fr': '🇫🇷',
+  'french_guiana': '🇬🇫',
+  'french_polynesia': '🇵🇫',
+  'french_southern_territories': '🇹🇫',
+  'gabon': '🇬🇦',
+  'gambia': '🇬🇲',
+  'georgia': '🇬🇪',
+  'de': '🇩🇪',
+  'ghana': '🇬🇭',
+  'gibraltar': '🇬🇮',
+  'greece': '🇬🇷',
+  'greenland': '🇬🇱',
+  'grenada': '🇬🇩',
+  'guadeloupe': '🇬🇵',
+  'guam': '🇬🇺',
+  'guatemala': '🇬🇹',
+  'guernsey': '🇬🇬',
+  'guinea': '🇬🇳',
+  'guinea_bissau': '🇬🇼',
+  'guyana': '🇬🇾',
+  'haiti': '🇭🇹',
+  'honduras': '🇭🇳',
+  'hong_kong': '🇭🇰',
+  'hungary': '🇭🇺',
+  'iceland': '🇮🇸',
+  'india': '🇮🇳',
+  'indonesia': '🇮🇩',
+  'iran': '🇮🇷',
+  'iraq': '🇮🇶',
+  'ireland': '🇮🇪',
+  'isle_of_man': '🇮🇲',
+  'israel': '🇮🇱',
+  'it': '🇮🇹',
+  'cote_divoire': '🇨🇮',
+  'jamaica': '🇯🇲',
+  'jp': '🇯🇵',
+  'jersey': '🇯🇪',
+  'jordan': '🇯🇴',
+  'kazakhstan': '🇰🇿',
+  'kenya': '🇰🇪',
+  'kiribati': '🇰🇮',
+  'kosovo': '🇽🇰',
+  'kuwait': '🇰🇼',
+  'kyrgyzstan': '🇰🇬',
+  'laos': '🇱🇦',
+  'latvia': '🇱🇻',
+  'lebanon': '🇱🇧',
+  'lesotho': '🇱🇸',
+  'liberia': '🇱🇷',
+  'libya': '🇱🇾',
+  'liechtenstein': '🇱🇮',
+  'lithuania': '🇱🇹',
+  'luxembourg': '🇱🇺',
+  'macau': '🇲🇴',
+  'macedonia': '🇲🇰',
+  'madagascar': '🇲🇬',
+  'malawi': '🇲🇼',
+  'malaysia': '🇲🇾',
+  'maldives': '🇲🇻',
+  'mali': '🇲🇱',
+  'malta': '🇲🇹',
+  'marshall_islands': '🇲🇭',
+  'martinique': '🇲🇶',
+  'mauritania': '🇲🇷',
+  'mauritius': '🇲🇺',
+  'mayotte': '🇾🇹',
+  'mexico': '🇲🇽',
+  'micronesia': '🇫🇲',
+  'moldova': '🇲🇩',
+  'monaco': '🇲🇨',
+  'mongolia': '🇲🇳',
+  'montenegro': '🇲🇪',
+  'montserrat': '🇲🇸',
+  'morocco': '🇲🇦',
+  'mozambique': '🇲🇿',
+  'myanmar': '🇲🇲',
+  'namibia': '🇳🇦',
+  'nauru': '🇳🇷',
+  'nepal': '🇳🇵',
+  'netherlands': '🇳🇱',
+  'new_caledonia': '🇳🇨',
+  'new_zealand': '🇳🇿',
+  'nicaragua': '🇳🇮',
+  'niger': '🇳🇪',
+  'nigeria': '🇳🇬',
+  'niue': '🇳🇺',
+  'norfolk_island': '🇳🇫',
+  'northern_mariana_islands': '🇲🇵',
+  'north_korea': '🇰🇵',
+  'norway': '🇳🇴',
+  'oman': '🇴🇲',
+  'pakistan': '🇵🇰',
+  'palau': '🇵🇼',
+  'palestinian_territories': '🇵🇸',
+  'panama': '🇵🇦',
+  'papua_new_guinea': '🇵🇬',
+  'paraguay': '🇵🇾',
+  'peru': '🇵🇪',
+  'philippines': '🇵🇭',
+  'pitcairn_islands': '🇵🇳',
+  'poland': '🇵🇱',
+  'portugal': '🇵🇹',
+  'puerto_rico': '🇵🇷',
+  'qatar': '🇶🇦',
+  'reunion': '🇷🇪',
+  'romania': '🇷🇴',
+  'ru': '🇷🇺',
+  'rwanda': '🇷🇼',
+  'st_barthelemy': '🇧🇱',
+  'st_helena': '🇸🇭',
+  'st_kitts_nevis': '🇰🇳',
+  'st_lucia': '🇱🇨',
+  'st_pierre_miquelon': '🇵🇲',
+  'st_vincent_grenadines': '🇻🇨',
+  'samoa': '🇼🇸',
+  'san_marino': '🇸🇲',
+  'sao_tome_principe': '🇸🇹',
+  'saudi_arabia': '🇸🇦',
+  'senegal': '🇸🇳',
+  'serbia': '🇷🇸',
+  'seychelles': '🇸🇨',
+  'sierra_leone': '🇸🇱',
+  'singapore': '🇸🇬',
+  'sint_maarten': '🇸🇽',
+  'slovakia': '🇸🇰',
+  'slovenia': '🇸🇮',
+  'solomon_islands': '🇸🇧',
+  'somalia': '🇸🇴',
+  'south_africa': '🇿🇦',
+  'south_georgia_south_sandwich_islands': '🇬🇸',
+  'kr': '🇰🇷',
+  'south_sudan': '🇸🇸',
+  'es': '🇪🇸',
+  'sri_lanka': '🇱🇰',
+  'sudan': '🇸🇩',
+  'suriname': '🇸🇷',
+  'swaziland': '🇸🇿',
+  'sweden': '🇸🇪',
+  'switzerland': '🇨🇭',
+  'syria': '🇸🇾',
+  'taiwan': '🇹🇼',
+  'tajikistan': '🇹🇯',
+  'tanzania': '🇹🇿',
+  'thailand': '🇹🇭',
+  'timor_leste': '🇹🇱',
+  'togo': '🇹🇬',
+  'tokelau': '🇹🇰',
+  'tonga': '🇹🇴',
+  'trinidad_tobago': '🇹🇹',
+  'tunisia': '🇹🇳',
+  'tr': '🇹🇷',
+  'turkmenistan': '🇹🇲',
+  'turks_caicos_islands': '🇹🇨',
+  'tuvalu': '🇹🇻',
+  'uganda': '🇺🇬',
+  'ukraine': '🇺🇦',
+  'united_arab_emirates': '🇦🇪',
+  'uk': '🇬🇧',
+  'england': '🏴󠁧󠁢󠁥󠁮󠁧󠁿',
+  'scotland': '🏴󠁧󠁢󠁳󠁣󠁴󠁿',
+  'wales': '🏴󠁧󠁢󠁷󠁬󠁳󠁿',
+  'us': '🇺🇸',
+  'us_virgin_islands': '🇻🇮',
+  'uruguay': '🇺🇾',
+  'uzbekistan': '🇺🇿',
+  'vanuatu': '🇻🇺',
+  'vatican_city': '🇻🇦',
+  'venezuela': '🇻🇪',
+  'vietnam': '🇻🇳',
+  'wallis_futuna': '🇼🇫',
+  'western_sahara': '🇪🇭',
+  'yemen': '🇾🇪',
+  'zambia': '🇿🇲',
+  'zimbabwe': '🇿🇼',
+};

+ 64 - 0
app_flowy/lib/workspace/infrastructure/markdown/src/extension_set.dart

@@ -0,0 +1,64 @@
+import 'block_parser.dart';
+import 'inline_parser.dart';
+
+/// ExtensionSets provide a simple grouping mechanism for common Markdown
+/// flavors.
+///
+/// For example, the [gitHubFlavored] set of syntax extensions allows users to
+/// output HTML from their Markdown in a similar fashion to GitHub's parsing.
+class ExtensionSet {
+  ExtensionSet(this.blockSyntaxes, this.inlineSyntaxes);
+
+  /// The [ExtensionSet.none] extension set renders Markdown similar to
+  /// [Markdown.pl].
+  ///
+  /// However, this set does not render _exactly_ the same as Markdown.pl;
+  /// rather it is more-or-less the CommonMark standard of Markdown, without
+  /// fenced code blocks, or inline HTML.
+  ///
+  /// [Markdown.pl]: http://daringfireball.net/projects/markdown/syntax
+  static final ExtensionSet none = ExtensionSet([], []);
+
+  /// The [commonMark] extension set is close to compliance with [CommonMark].
+  ///
+  /// [CommonMark]: http://commonmark.org/
+  static final ExtensionSet commonMark =
+      ExtensionSet([const FencedCodeBlockSyntax()], [InlineHtmlSyntax()]);
+
+  /// The [gitHubWeb] extension set renders Markdown similarly to GitHub.
+  ///
+  /// This is different from the [gitHubFlavored] extension set in that GitHub
+  /// actually renders HTML different from straight [GitHub flavored Markdown].
+  ///
+  /// (The only difference currently is that [gitHubWeb] renders headers with
+  /// linkable IDs.)
+  ///
+  /// [GitHub flavored Markdown]: https://github.github.com/gfm/
+  static final ExtensionSet gitHubWeb = ExtensionSet([
+    const FencedCodeBlockSyntax(),
+    const HeaderWithIdSyntax(),
+    const SetextHeaderWithIdSyntax(),
+    const TableSyntax()
+  ], [
+    InlineHtmlSyntax(),
+    StrikethroughSyntax(),
+    EmojiSyntax(),
+    AutolinkExtensionSyntax(),
+  ]);
+
+  /// The [gitHubFlavored] extension set is close to compliance with the [GitHub
+  /// flavored Markdown spec].
+  ///
+  /// [GitHub flavored Markdown]: https://github.github.com/gfm/
+  static final ExtensionSet gitHubFlavored = ExtensionSet([
+    const FencedCodeBlockSyntax(),
+    const TableSyntax()
+  ], [
+    InlineHtmlSyntax(),
+    StrikethroughSyntax(),
+    AutolinkExtensionSyntax(),
+  ]);
+
+  final List<BlockSyntax> blockSyntaxes;
+  final List<InlineSyntax> inlineSyntaxes;
+}

+ 121 - 0
app_flowy/lib/workspace/infrastructure/markdown/src/html_renderer.dart

@@ -0,0 +1,121 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'ast.dart';
+import 'block_parser.dart';
+import 'document.dart';
+import 'extension_set.dart';
+import 'inline_parser.dart';
+
+/// Converts the given string of Markdown to HTML.
+String markdownToHtml(String markdown,
+    {Iterable<BlockSyntax>? blockSyntaxes,
+    Iterable<InlineSyntax>? inlineSyntaxes,
+    ExtensionSet? extensionSet,
+    Resolver? linkResolver,
+    Resolver? imageLinkResolver,
+    bool inlineOnly = false}) {
+  final document = Document(
+      blockSyntaxes: blockSyntaxes,
+      inlineSyntaxes: inlineSyntaxes,
+      extensionSet: extensionSet,
+      linkResolver: linkResolver,
+      imageLinkResolver: imageLinkResolver);
+
+  if (inlineOnly) {
+    return renderToHtml(document.parseInline(markdown)!);
+  }
+
+  // Replace windows line endings with unix line endings, and split.
+  final lines = markdown.replaceAll('\r\n', '\n').split('\n');
+
+  return '${renderToHtml(document.parseLines(lines))}\n';
+}
+
+/// Renders [nodes] to HTML.
+String renderToHtml(List<Node> nodes) => HtmlRenderer().render(nodes);
+
+/// Translates a parsed AST to HTML.
+class HtmlRenderer implements NodeVisitor {
+  HtmlRenderer();
+
+  static final _blockTags = RegExp('blockquote|h1|h2|h3|h4|h5|h6|hr|p|pre');
+
+  late StringBuffer buffer;
+  late Set<String> uniqueIds;
+
+  String render(List<Node> nodes) {
+    buffer = StringBuffer();
+    uniqueIds = <String>{};
+
+    for (final node in nodes) {
+      node.accept(this);
+    }
+
+    return buffer.toString();
+  }
+
+  @override
+  void visitText(Text text) {
+    buffer.write(text.text);
+  }
+
+  @override
+  bool visitElementBefore(Element element) {
+    // Hackish. Separate block-level elements with newlines.
+    if (buffer.isNotEmpty && _blockTags.firstMatch(element.tag) != null) {
+      buffer.write('\n');
+    }
+
+    buffer.write('<${element.tag}');
+
+    // Sort the keys so that we generate stable output.
+    final attributeNames = element.attributes.keys.toList()
+      ..sort((a, b) => a.compareTo(b));
+
+    for (final name in attributeNames) {
+      buffer.write(' $name="${element.attributes[name]}"');
+    }
+
+    // attach header anchor ids generated from text
+    if (element.generatedId != null) {
+      buffer.write(' id="${uniquifyId(element.generatedId!)}"');
+    }
+
+    if (element.isEmpty) {
+      // Empty element like <hr/>.
+      buffer.write(' />');
+
+      if (element.tag == 'br') {
+        buffer.write('\n');
+      }
+
+      return false;
+    } else {
+      buffer.write('>');
+      return true;
+    }
+  }
+
+  @override
+  void visitElementAfter(Element element) {
+    buffer.write('</${element.tag}>');
+  }
+
+  /// Uniquifies an id generated from text.
+  String uniquifyId(String id) {
+    if (!uniqueIds.contains(id)) {
+      uniqueIds.add(id);
+      return id;
+    }
+
+    var suffix = 2;
+    var suffixedId = '$id-$suffix';
+    while (uniqueIds.contains(suffixedId)) {
+      suffixedId = '$id-${suffix++}';
+    }
+    uniqueIds.add(suffixedId);
+    return suffixedId;
+  }
+}

+ 1271 - 0
app_flowy/lib/workspace/infrastructure/markdown/src/inline_parser.dart

@@ -0,0 +1,1271 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. 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:charcode/charcode.dart';
+
+import 'ast.dart';
+import 'document.dart';
+import 'emojis.dart';
+import 'util.dart';
+
+/// Maintains the internal state needed to parse inline span elements in
+/// Markdown.
+class InlineParser {
+  InlineParser(this.source, this.document) : _stack = <TagState>[] {
+    // User specified syntaxes are the first syntaxes to be evaluated.
+    syntaxes.addAll(document.inlineSyntaxes);
+
+    final documentHasCustomInlineSyntaxes = document.inlineSyntaxes
+        .any((s) => !document.extensionSet.inlineSyntaxes.contains(s));
+
+    // This first RegExp matches plain text to accelerate parsing. It's written
+    // so that it does not match any prefix of any following syntaxes. Most
+    // Markdown is plain text, so it's faster to match one RegExp per 'word'
+    // rather than fail to match all the following RegExps at each non-syntax
+    // character position.
+    if (documentHasCustomInlineSyntaxes) {
+      // We should be less aggressive in blowing past "words".
+      syntaxes.add(TextSyntax(r'[A-Za-z0-9]+(?=\s)'));
+    } else {
+      syntaxes.add(TextSyntax(r'[ \tA-Za-z0-9]*[A-Za-z0-9](?=\s)'));
+    }
+
+    syntaxes
+      ..addAll(_defaultSyntaxes)
+      // Custom link resolvers go after the generic text syntax.
+      ..insertAll(1, [
+        LinkSyntax(linkResolver: document.linkResolver),
+        ImageSyntax(linkResolver: document.imageLinkResolver)
+      ]);
+  }
+
+  static final List<InlineSyntax> _defaultSyntaxes =
+      List<InlineSyntax>.unmodifiable(<InlineSyntax>[
+    EmailAutolinkSyntax(),
+    AutolinkSyntax(),
+    LineBreakSyntax(),
+    LinkSyntax(),
+    ImageSyntax(),
+    // Allow any punctuation to be escaped.
+    EscapeSyntax(),
+    // "*" surrounded by spaces is left alone.
+    TextSyntax(r' \* '),
+    // "_" surrounded by spaces is left alone.
+    TextSyntax(r' _ '),
+    // Parse "**strong**" and "*emphasis*" tags.
+    TagSyntax(r'\*+', requiresDelimiterRun: true),
+    // Parse "__strong__" and "_emphasis_" tags.
+    TagSyntax(r'_+', requiresDelimiterRun: true),
+    CodeSyntax(),
+    // We will add the LinkSyntax once we know about the specific link resolver.
+  ]);
+
+  /// The string of Markdown being parsed.
+  final String source;
+
+  /// The Markdown document this parser is parsing.
+  final Document document;
+
+  final List<InlineSyntax> syntaxes = <InlineSyntax>[];
+
+  /// The current read position.
+  int pos = 0;
+
+  /// Starting position of the last unconsumed text.
+  int start = 0;
+
+  final List<TagState> _stack;
+
+  List<Node>? parse() {
+    // Make a fake top tag to hold the results.
+    _stack.add(TagState(0, 0, null, null));
+
+    while (!isDone) {
+      // See if any of the current tags on the stack match.  This takes
+      // priority over other possible matches.
+      if (_stack.reversed
+          .any((state) => state.syntax != null && state.tryMatch(this))) {
+        continue;
+      }
+
+      // See if the current text matches any defined markdown syntax.
+      if (syntaxes.any((syntax) => syntax.tryMatch(this))) {
+        continue;
+      }
+
+      // If we got here, it's just text.
+      advanceBy(1);
+    }
+
+    // Unwind any unmatched tags and get the results.
+    return _stack[0].close(this, null);
+  }
+
+  int charAt(int index) => source.codeUnitAt(index);
+
+  void writeText() {
+    writeTextRange(start, pos);
+    start = pos;
+  }
+
+  void writeTextRange(int start, int end) {
+    if (end <= start) {
+      return;
+    }
+
+    final text = source.substring(start, end);
+    final nodes = _stack.last.children;
+
+    // If the previous node is text too, just append.
+    if (nodes.isNotEmpty && nodes.last is Text) {
+      final textNode = nodes.last as Text;
+      nodes[nodes.length - 1] = Text('${textNode.text}$text');
+    } else {
+      nodes.add(Text(text));
+    }
+  }
+
+  /// Add [node] to the last [TagState] on the stack.
+  void addNode(Node node) {
+    _stack.last.children.add(node);
+  }
+
+  /// Push [state] onto the stack of [TagState]s.
+  void openTag(TagState state) => _stack.add(state);
+
+  bool get isDone => pos == source.length;
+
+  void advanceBy(int length) {
+    pos += length;
+  }
+
+  void consume(int length) {
+    pos += length;
+    start = pos;
+  }
+}
+
+/// Represents one kind of Markdown tag that can be parsed.
+abstract class InlineSyntax {
+  InlineSyntax(String pattern) : pattern = RegExp(pattern, multiLine: true);
+
+  final RegExp pattern;
+
+  /// Tries to match at the parser's current position.
+  ///
+  /// The parser's position can be overriden with [startMatchPos].
+  /// Returns whether or not the pattern successfully matched.
+  bool tryMatch(InlineParser parser, [int? startMatchPos]) {
+    startMatchPos ??= parser.pos;
+
+    final startMatch = pattern.matchAsPrefix(parser.source, startMatchPos);
+    if (startMatch == null) {
+      return false;
+    }
+
+    // Write any existing plain text up to this point.
+    parser.writeText();
+
+    if (onMatch(parser, startMatch)) {
+      parser.consume(startMatch[0]!.length);
+    }
+    return true;
+  }
+
+  /// Processes [match], adding nodes to [parser] and possibly advancing
+  /// [parser].
+  ///
+  /// Returns whether the caller should advance [parser] by `match[0].length`.
+  bool onMatch(InlineParser parser, Match match);
+}
+
+/// Represents a hard line break.
+class LineBreakSyntax extends InlineSyntax {
+  LineBreakSyntax() : super(r'(?:\\|  +)\n');
+
+  /// Create a void <br> element.
+  @override
+  bool onMatch(InlineParser parser, Match match) {
+    parser.addNode(Element.empty('br'));
+    return true;
+  }
+}
+
+/// Matches stuff that should just be passed through as straight text.
+class TextSyntax extends InlineSyntax {
+  TextSyntax(String pattern, {String? sub})
+      : substitute = sub,
+        super(pattern);
+
+  final String? substitute;
+
+  @override
+  bool onMatch(InlineParser parser, Match match) {
+    if (substitute == null) {
+      // Just use the original matched text.
+      parser.advanceBy(match[0]!.length);
+      return false;
+    }
+
+    // Insert the substitution.
+    parser.addNode(Text(substitute!));
+    return true;
+  }
+}
+
+/// Escape punctuation preceded by a backslash.
+class EscapeSyntax extends InlineSyntax {
+  EscapeSyntax() : super(r'''\\[!"#$%&'()*+,\-./:;<=>?@\[\\\]^_`{|}~]''');
+
+  @override
+  bool onMatch(InlineParser parser, Match match) {
+    // Insert the substitution.
+    parser.addNode(Text(match[0]![1]));
+    return true;
+  }
+}
+
+/// Leave inline HTML tags alone, from
+/// [CommonMark 0.28](http://spec.commonmark.org/0.28/#raw-html).
+///
+/// This is not actually a good definition (nor CommonMark's) of an HTML tag,
+/// but it is fast. It will leave text like `<a href='hi">` alone, which is
+/// incorrect.
+///
+/// TODO(srawlins): improve accuracy while ensuring performance, once
+/// Markdown benchmarking is more mature.
+class InlineHtmlSyntax extends TextSyntax {
+  InlineHtmlSyntax() : super(r'<[/!?]?[A-Za-z][A-Za-z0-9-]*(?:\s[^>]*)?>');
+}
+
+/// Matches autolinks like `<[email protected]>`.
+///
+/// See <http://spec.commonmark.org/0.28/#email-address>.
+class EmailAutolinkSyntax extends InlineSyntax {
+  EmailAutolinkSyntax() : super('<($_email)>');
+
+  static const _email =
+      r'''[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}'''
+      r'''[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*''';
+
+  @override
+  bool onMatch(InlineParser parser, Match match) {
+    final url = match[1]!;
+    final anchor = Element.text('a', escapeHtml(url));
+    anchor.attributes['href'] = Uri.encodeFull('mailto:$url');
+    parser.addNode(anchor);
+
+    return true;
+  }
+}
+
+/// Matches autolinks like `<http://foo.com>`.
+class AutolinkSyntax extends InlineSyntax {
+  AutolinkSyntax() : super(r'<(([a-zA-Z][a-zA-Z\-\+\.]+):(?://)?[^\s>]*)>');
+
+  @override
+  bool onMatch(InlineParser parser, Match match) {
+    final url = match[1]!;
+    final anchor = Element.text('a', escapeHtml(url));
+    anchor.attributes['href'] = Uri.encodeFull(url);
+    parser.addNode(anchor);
+
+    return true;
+  }
+}
+
+/// Matches autolinks like `http://foo.com`.
+class AutolinkExtensionSyntax extends InlineSyntax {
+  AutolinkExtensionSyntax() : super('$start(($scheme)($domain)($path))');
+
+  /// Broken up parts of the autolink regex for reusability and readability
+
+  // Autolinks can only come at the beginning of a line, after whitespace, or
+  // any of the delimiting characters *, _, ~, and (.
+  static const start = r'(?:^|[\s*_~(>])';
+  // An extended url autolink will be recognized when one of the schemes
+  // http://, https://, or ftp://, followed by a valid domain
+  static const scheme = r'(?:(?:https?|ftp):\/\/|www\.)';
+  // A valid domain consists of alphanumeric characters, underscores (_),
+  // hyphens (-) and periods (.). There must be at least one period, and no
+  // underscores may be present in the last two segments of the domain.
+  static const domainPart = r'\w\-';
+  static const domain = '[$domainPart][$domainPart.]+';
+  // A valid domain consists of alphanumeric characters, underscores (_),
+  // hyphens (-) and periods (.).
+  static const path = r'[^\s<]*';
+  // Trailing punctuation (specifically, ?, !, ., ,, :, *, _, and ~) will not
+  // be considered part of the autolink
+  static const truncatingPunctuationPositive = r'[?!.,:*_~]';
+
+  static final regExpTrailingPunc =
+      RegExp('$truncatingPunctuationPositive*' r'$');
+  static final regExpEndsWithColon = RegExp(r'\&[a-zA-Z0-9]+;$');
+  static final regExpWhiteSpace = RegExp(r'\s');
+
+  @override
+  bool tryMatch(InlineParser parser, [int? startMatchPos]) {
+    return super.tryMatch(parser, parser.pos > 0 ? parser.pos - 1 : 0);
+  }
+
+  @override
+  bool onMatch(InlineParser parser, Match match) {
+    var url = match[1]!;
+    var href = url;
+    var matchLength = url.length;
+
+    if (url[0] == '>' || url.startsWith(regExpWhiteSpace)) {
+      url = url.substring(1, url.length - 1);
+      href = href.substring(1, href.length - 1);
+      parser.pos++;
+      matchLength--;
+    }
+
+    // Prevent accidental standard autolink matches
+    if (url.endsWith('>') && parser.source[parser.pos - 1] == '<') {
+      return false;
+    }
+
+    // When an autolink ends in ), we scan the entire autolink for the total
+    // number of parentheses. If there is a greater number of closing
+    // parentheses than opening ones, we don’t consider the last character
+    // part of the autolink, in order to facilitate including an autolink
+    // inside a parenthesis:
+    // https://github.github.com/gfm/#example-600
+    if (url.endsWith(')')) {
+      final opening = _countChars(url, '(');
+      final closing = _countChars(url, ')');
+
+      if (closing > opening) {
+        url = url.substring(0, url.length - 1);
+        href = href.substring(0, href.length - 1);
+        matchLength--;
+      }
+    }
+
+    // Trailing punctuation (specifically, ?, !, ., ,, :, *, _, and ~) will
+    // not be considered part of the autolink, though they may be included
+    // in the interior of the link:
+    // https://github.github.com/gfm/#example-599
+    final trailingPunc = regExpTrailingPunc.firstMatch(url);
+    if (trailingPunc != null) {
+      url = url.substring(0, url.length - trailingPunc[0]!.length);
+      href = href.substring(0, href.length - trailingPunc[0]!.length);
+      matchLength -= trailingPunc[0]!.length;
+    }
+
+    // If an autolink ends in a semicolon (;), we check to see if it appears
+    // to resemble an
+    // [entity reference](https://github.github.com/gfm/#entity-references);
+    // if the preceding text is & followed by one or more alphanumeric
+    // characters. If so, it is excluded from the autolink:
+    // https://github.github.com/gfm/#example-602
+    if (url.endsWith(';')) {
+      final entityRef = regExpEndsWithColon.firstMatch(url);
+      if (entityRef != null) {
+        // Strip out HTML entity reference
+        url = url.substring(0, url.length - entityRef[0]!.length);
+        href = href.substring(0, href.length - entityRef[0]!.length);
+        matchLength -= entityRef[0]!.length;
+      }
+    }
+
+    // The scheme http will be inserted automatically
+    if (!href.startsWith('http://') &&
+        !href.startsWith('https://') &&
+        !href.startsWith('ftp://')) {
+      href = 'http://$href';
+    }
+
+    final anchor = Element.text('a', escapeHtml(url));
+    anchor.attributes['href'] = Uri.encodeFull(href);
+    parser
+      ..addNode(anchor)
+      ..consume(matchLength);
+    return false;
+  }
+
+  int _countChars(String input, String char) {
+    var count = 0;
+
+    for (var i = 0; i < input.length; i++) {
+      if (input[i] == char) {
+        count++;
+      }
+    }
+
+    return count;
+  }
+}
+
+class _DelimiterRun {
+  _DelimiterRun._(
+      {this.char,
+      this.length,
+      this.isLeftFlanking,
+      this.isRightFlanking,
+      this.isPrecededByPunctuation,
+      this.isFollowedByPunctuation});
+
+  static const String punctuation = r'''!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~''';
+  // TODO(srawlins): Unicode whitespace
+  static const String whitespace = ' \t\r\n';
+
+  final int? char;
+  final int? length;
+  final bool? isLeftFlanking;
+  final bool? isRightFlanking;
+  final bool? isPrecededByPunctuation;
+  final bool? isFollowedByPunctuation;
+
+  // ignore: prefer_constructors_over_static_methods
+  static _DelimiterRun? tryParse(
+      InlineParser parser, int runStart, int runEnd) {
+    bool leftFlanking,
+        rightFlanking,
+        precededByPunctuation,
+        followedByPunctuation;
+    String preceding, following;
+    if (runStart == 0) {
+      rightFlanking = false;
+      preceding = '\n';
+    } else {
+      preceding = parser.source.substring(runStart - 1, runStart);
+    }
+    precededByPunctuation = punctuation.contains(preceding);
+
+    if (runEnd == parser.source.length - 1) {
+      leftFlanking = false;
+      following = '\n';
+    } else {
+      following = parser.source.substring(runEnd + 1, runEnd + 2);
+    }
+    followedByPunctuation = punctuation.contains(following);
+
+    // http://spec.commonmark.org/0.28/#left-flanking-delimiter-run
+    if (whitespace.contains(following)) {
+      leftFlanking = false;
+    } else {
+      leftFlanking = !followedByPunctuation ||
+          whitespace.contains(preceding) ||
+          precededByPunctuation;
+    }
+
+    // http://spec.commonmark.org/0.28/#right-flanking-delimiter-run
+    if (whitespace.contains(preceding)) {
+      rightFlanking = false;
+    } else {
+      rightFlanking = !precededByPunctuation ||
+          whitespace.contains(following) ||
+          followedByPunctuation;
+    }
+
+    if (!leftFlanking && !rightFlanking) {
+      // Could not parse a delimiter run.
+      return null;
+    }
+
+    return _DelimiterRun._(
+        char: parser.charAt(runStart),
+        length: runEnd - runStart + 1,
+        isLeftFlanking: leftFlanking,
+        isRightFlanking: rightFlanking,
+        isPrecededByPunctuation: precededByPunctuation,
+        isFollowedByPunctuation: followedByPunctuation);
+  }
+
+  @override
+  String toString() =>
+      '<char: $char, length: $length, isLeftFlanking: $isLeftFlanking, '
+      'isRightFlanking: $isRightFlanking>';
+
+  // Whether a delimiter in this run can open emphasis or strong emphasis.
+  bool get canOpen =>
+      isLeftFlanking! &&
+      (char == $asterisk || !isRightFlanking! || isPrecededByPunctuation!);
+
+  // Whether a delimiter in this run can close emphasis or strong emphasis.
+  bool get canClose =>
+      isRightFlanking! &&
+      (char == $asterisk || !isLeftFlanking! || isFollowedByPunctuation!);
+}
+
+/// Matches syntax that has a pair of tags and becomes an element, like `*` for
+/// `<em>`. Allows nested tags.
+class TagSyntax extends InlineSyntax {
+  TagSyntax(String pattern, {String? end, this.requiresDelimiterRun = false})
+      : endPattern = RegExp((end != null) ? end : pattern, multiLine: true),
+        super(pattern);
+
+  final RegExp endPattern;
+
+  /// Whether this is parsed according to the same nesting rules as [emphasis
+  /// delimiters][].
+  ///
+  /// [emphasis delimiters]: http://spec.commonmark.org/0.28/#can-open-emphasis
+  final bool requiresDelimiterRun;
+
+  @override
+  bool onMatch(InlineParser parser, Match match) {
+    final runLength = match.group(0)!.length;
+    final matchStart = parser.pos;
+    final matchEnd = parser.pos + runLength - 1;
+    if (!requiresDelimiterRun) {
+      parser.openTag(TagState(parser.pos, matchEnd + 1, this, null));
+      return true;
+    }
+
+    final delimiterRun = _DelimiterRun.tryParse(parser, matchStart, matchEnd);
+    if (delimiterRun != null && delimiterRun.canOpen) {
+      parser.openTag(TagState(parser.pos, matchEnd + 1, this, delimiterRun));
+      return true;
+    } else {
+      parser.advanceBy(runLength);
+      return false;
+    }
+  }
+
+  bool onMatchEnd(InlineParser parser, Match match, TagState state) {
+    final runLength = match.group(0)!.length;
+    final matchStart = parser.pos;
+    final matchEnd = parser.pos + runLength - 1;
+    final openingRunLength = state.endPos - state.startPos;
+    final delimiterRun = _DelimiterRun.tryParse(parser, matchStart, matchEnd);
+
+    if (openingRunLength == 1 && runLength == 1) {
+      parser.addNode(Element('em', state.children));
+    } else if (openingRunLength == 1 && runLength > 1) {
+      parser
+        ..addNode(Element('em', state.children))
+        ..pos = parser.pos - (runLength - 1)
+        ..start = parser.pos;
+    } else if (openingRunLength > 1 && runLength == 1) {
+      parser
+        ..openTag(
+            TagState(state.startPos, state.endPos - 1, this, delimiterRun))
+        ..addNode(Element('em', state.children));
+    } else if (openingRunLength == 2 && runLength == 2) {
+      parser.addNode(Element('strong', state.children));
+    } else if (openingRunLength == 2 && runLength > 2) {
+      parser
+        ..addNode(Element('strong', state.children))
+        ..pos = parser.pos - (runLength - 2)
+        ..start = parser.pos;
+    } else if (openingRunLength > 2 && runLength == 2) {
+      parser
+        ..openTag(
+            TagState(state.startPos, state.endPos - 2, this, delimiterRun))
+        ..addNode(Element('strong', state.children));
+    } else if (openingRunLength > 2 && runLength > 2) {
+      parser
+        ..openTag(
+            TagState(state.startPos, state.endPos - 2, this, delimiterRun))
+        ..addNode(Element('strong', state.children))
+        ..pos = parser.pos - (runLength - 2)
+        ..start = parser.pos;
+    }
+
+    return true;
+  }
+}
+
+/// Matches strikethrough syntax according to the GFM spec.
+class StrikethroughSyntax extends TagSyntax {
+  StrikethroughSyntax() : super('~+', requiresDelimiterRun: true);
+
+  @override
+  bool onMatchEnd(InlineParser parser, Match match, TagState state) {
+    final runLength = match.group(0)!.length;
+    final matchStart = parser.pos;
+    final matchEnd = parser.pos + runLength - 1;
+    final delimiterRun = _DelimiterRun.tryParse(parser, matchStart, matchEnd)!;
+    if (!delimiterRun.isRightFlanking!) {
+      return false;
+    }
+
+    parser.addNode(Element('del', state.children));
+    return true;
+  }
+}
+
+/// Matches links like `[blah][label]` and `[blah](url)`.
+class LinkSyntax extends TagSyntax {
+  LinkSyntax({Resolver? linkResolver, String pattern = r'\['})
+      : linkResolver = (linkResolver ?? (_, [__]) => null),
+        super(pattern, end: r'\]');
+
+  static final _entirelyWhitespacePattern = RegExp(r'^\s*$');
+
+  final Resolver linkResolver;
+
+  // The pending [TagState]s, all together, are "active" or "inactive" based on
+  // whether a link element has just been parsed.
+  //
+  // Links cannot be nested, so we must "deactivate" any pending ones. For
+  // example, take the following text:
+  //
+  //     Text [link and [more](links)](links).
+  //
+  // Once we have parsed `Text [`, there is one (pending) link in the state
+  // stack.  It is, by default, active. Once we parse the next possible link,
+  // `[more](links)`, as a real link, we must deactive the pending links (just
+  // the one, in this case).
+  var _pendingStatesAreActive = true;
+
+  @override
+  bool onMatch(InlineParser parser, Match match) {
+    final matched = super.onMatch(parser, match);
+    if (!matched) {
+      return false;
+    }
+
+    _pendingStatesAreActive = true;
+
+    return true;
+  }
+
+  @override
+  bool onMatchEnd(InlineParser parser, Match match, TagState state) {
+    if (!_pendingStatesAreActive) {
+      return false;
+    }
+
+    final text = parser.source.substring(state.endPos, parser.pos);
+    // The current character is the `]` that closed the link text. Examine the
+    // next character, to determine what type of link we might have (a '('
+    // means a possible inline link; otherwise a possible reference link).
+    if (parser.pos + 1 >= parser.source.length) {
+      // In this case, the Markdown document may have ended with a shortcut
+      // reference link.
+
+      return _tryAddReferenceLink(parser, state, text);
+    }
+    // Peek at the next character; don't advance, so as to avoid later stepping
+    // backward.
+    final char = parser.charAt(parser.pos + 1);
+
+    if (char == $lparen) {
+      // Maybe an inline link, like `[text](destination)`.
+      parser.advanceBy(1);
+      final leftParenIndex = parser.pos;
+      final inlineLink = _parseInlineLink(parser);
+      if (inlineLink != null) {
+        return _tryAddInlineLink(parser, state, inlineLink);
+      }
+
+      // Reset the parser position.
+      parser
+        ..pos = leftParenIndex
+
+        // At this point, we've matched `[...](`, but that `(` did not pan out
+        // to be an inline link. We must now check if `[...]` is simply a
+        // shortcut reference link.
+        ..advanceBy(-1);
+      return _tryAddReferenceLink(parser, state, text);
+    }
+
+    if (char == $lbracket) {
+      parser.advanceBy(1);
+      // At this point, we've matched `[...][`. Maybe a *full* reference link,
+      // like `[foo][bar]` or a *collapsed* reference link, like `[foo][]`.
+      if (parser.pos + 1 < parser.source.length &&
+          parser.charAt(parser.pos + 1) == $rbracket) {
+        // That opening `[` is not actually part of the link. Maybe a
+        // *shortcut* reference link (followed by a `[`).
+        parser.advanceBy(1);
+        return _tryAddReferenceLink(parser, state, text);
+      }
+      final label = _parseReferenceLinkLabel(parser);
+      if (label != null) {
+        return _tryAddReferenceLink(parser, state, label);
+      }
+      return false;
+    }
+
+    // The link text (inside `[...]`) was not followed with a opening `(` nor
+    // an opening `[`. Perhaps just a simple shortcut reference link (`[...]`).
+
+    return _tryAddReferenceLink(parser, state, text);
+  }
+
+  /// Resolve a possible reference link.
+  ///
+  /// Uses [linkReferences], [linkResolver], and [_createNode] to try to
+  /// resolve [label] and [state] into a [Node]. If [label] is defined in
+  /// [linkReferences] or can be resolved by [linkResolver], returns a [Node]
+  /// that links to the resolved URL.
+  ///
+  /// Otherwise, returns `null`.
+  ///
+  /// [label] does not need to be normalized.
+  Node? _resolveReferenceLink(
+      String label, TagState state, Map<String, LinkReference> linkReferences) {
+    final normalizedLabel = label.toLowerCase();
+    final linkReference = linkReferences[normalizedLabel];
+    if (linkReference != null) {
+      return _createNode(state, linkReference.destination, linkReference.title);
+    } else {
+      // This link has no reference definition. But we allow users of the
+      // library to specify a custom resolver function ([linkResolver]) that
+      // may choose to handle this. Otherwise, it's just treated as plain
+      // text.
+
+      // Normally, label text does not get parsed as inline Markdown. However,
+      // for the benefit of the link resolver, we need to at least escape
+      // brackets, so that, e.g. a link resolver can receive `[\[\]]` as `[]`.
+      return linkResolver(label
+          .replaceAll(r'\\', r'\')
+          .replaceAll(r'\[', '[')
+          .replaceAll(r'\]', ']'));
+    }
+  }
+
+  /// Create the node represented by a Markdown link.
+  Node _createNode(TagState state, String destination, String? title) {
+    final element = Element('a', state.children);
+    element.attributes['href'] = escapeAttribute(destination);
+    if (title != null && title.isNotEmpty) {
+      element.attributes['title'] = escapeAttribute(title);
+    }
+    return element;
+  }
+
+  // Add a reference link node to [parser]'s AST.
+  //
+  // Returns whether the link was added successfully.
+  bool _tryAddReferenceLink(InlineParser parser, TagState state, String label) {
+    final element =
+        _resolveReferenceLink(label, state, parser.document.linkReferences);
+    if (element == null) {
+      return false;
+    }
+    parser
+      ..addNode(element)
+      ..start = parser.pos;
+    _pendingStatesAreActive = false;
+    return true;
+  }
+
+  // Add an inline link node to [parser]'s AST.
+  //
+  // Returns whether the link was added successfully.
+  bool _tryAddInlineLink(InlineParser parser, TagState state, InlineLink link) {
+    final element = _createNode(state, link.destination, link.title);
+    parser
+      ..addNode(element)
+      ..start = parser.pos;
+    _pendingStatesAreActive = false;
+    return true;
+  }
+
+  /// Parse a reference link label at the current position.
+  ///
+  /// Specifically, [parser.pos] is expected to be pointing at the `[` which
+  /// opens the link label.
+  ///
+  /// Returns the label if it could be parsed, or `null` if not.
+  String? _parseReferenceLinkLabel(InlineParser parser) {
+    // Walk past the opening `[`.
+    parser.advanceBy(1);
+    if (parser.isDone) {
+      return null;
+    }
+
+    final buffer = StringBuffer();
+    while (true) {
+      final char = parser.charAt(parser.pos);
+      if (char == $backslash) {
+        parser.advanceBy(1);
+        final next = parser.charAt(parser.pos);
+        if (next != $backslash && next != $rbracket) {
+          buffer.writeCharCode(char);
+        }
+        buffer.writeCharCode(next);
+      } else if (char == $rbracket) {
+        break;
+      } else {
+        buffer.writeCharCode(char);
+      }
+      parser.advanceBy(1);
+      if (parser.isDone) {
+        return null;
+      }
+      // TODO(srawlins): only check 999 characters, for performance reasons?
+    }
+
+    final label = buffer.toString();
+
+    // A link label must contain at least one non-whitespace character.
+    if (_entirelyWhitespacePattern.hasMatch(label)) {
+      return null;
+    }
+
+    return label;
+  }
+
+  /// Parse an inline [InlineLink] at the current position.
+  ///
+  /// At this point, we have parsed a link's (or image's) opening `[`, and then
+  /// a matching closing `]`, and [parser.pos] is pointing at an opening `(`.
+  /// This method will then attempt to parse a link destination wrapped in `<>`,
+  /// such as `(<http://url>)`, or a bare link destination, such as
+  /// `(http://url)`, or a link destination with a title, such as
+  /// `(http://url "title")`.
+  ///
+  /// Returns the [InlineLink] if one was parsed, or `null` if not.
+  InlineLink? _parseInlineLink(InlineParser parser) {
+    // Start walking to the character just after the opening `(`.
+    parser.advanceBy(1);
+
+    _moveThroughWhitespace(parser);
+    if (parser.isDone) {
+      return null; // EOF. Not a link.
+    }
+
+    if (parser.charAt(parser.pos) == $lt) {
+      // Maybe a `<...>`-enclosed link destination.
+      return _parseInlineBracketedLink(parser);
+    } else {
+      return _parseInlineBareDestinationLink(parser);
+    }
+  }
+
+  /// Parse an inline link with a bracketed destination (a destination wrapped
+  /// in `<...>`). The current position of the parser must be the first
+  /// character of the destination.
+  InlineLink? _parseInlineBracketedLink(InlineParser parser) {
+    parser.advanceBy(1);
+
+    final buffer = StringBuffer();
+    while (true) {
+      final char = parser.charAt(parser.pos);
+      if (char == $backslash) {
+        parser.advanceBy(1);
+        final next = parser.charAt(parser.pos);
+        if (char == $space || char == $lf || char == $cr || char == $ff) {
+          // Not a link (no whitespace allowed within `<...>`).
+          return null;
+        }
+        // TODO: Follow the backslash spec better here.
+        // http://spec.commonmark.org/0.28/#backslash-escapes
+        if (next != $backslash && next != $gt) {
+          buffer.writeCharCode(char);
+        }
+        buffer.writeCharCode(next);
+      } else if (char == $space || char == $lf || char == $cr || char == $ff) {
+        // Not a link (no whitespace allowed within `<...>`).
+        return null;
+      } else if (char == $gt) {
+        break;
+      } else {
+        buffer.writeCharCode(char);
+      }
+      parser.advanceBy(1);
+      if (parser.isDone) {
+        return null;
+      }
+    }
+    final destination = buffer.toString();
+
+    parser.advanceBy(1);
+    final char = parser.charAt(parser.pos);
+    if (char == $space || char == $lf || char == $cr || char == $ff) {
+      final title = _parseTitle(parser);
+      if (title == null && parser.charAt(parser.pos) != $rparen) {
+        // This looked like an inline link, until we found this $space
+        // followed by mystery characters; no longer a link.
+        return null;
+      }
+      return InlineLink(destination, title: title);
+    } else if (char == $rparen) {
+      return InlineLink(destination);
+    } else {
+      // We parsed something like `[foo](<url>X`. Not a link.
+      return null;
+    }
+  }
+
+  /// Parse an inline link with a "bare" destination (a destination _not_
+  /// wrapped in `<...>`). The current position of the parser must be the first
+  /// character of the destination.
+  InlineLink? _parseInlineBareDestinationLink(InlineParser parser) {
+    // According to
+    // [CommonMark](http://spec.commonmark.org/0.28/#link-destination):
+    //
+    // > A link destination consists of [...] a nonempty sequence of
+    // > characters [...], and includes parentheses only if (a) they are
+    // > backslash-escaped or (b) they are part of a balanced pair of
+    // > unescaped parentheses.
+    //
+    // We need to count the open parens. We start with 1 for the paren that
+    // opened the destination.
+    var parenCount = 1;
+    final buffer = StringBuffer();
+
+    while (true) {
+      final char = parser.charAt(parser.pos);
+      switch (char) {
+        case $backslash:
+          parser.advanceBy(1);
+          if (parser.isDone) {
+            return null; // EOF. Not a link.
+          }
+
+          final next = parser.charAt(parser.pos);
+          // Parentheses may be escaped.
+          //
+          // http://spec.commonmark.org/0.28/#example-467
+          if (next != $backslash && next != $lparen && next != $rparen) {
+            buffer.writeCharCode(char);
+          }
+          buffer.writeCharCode(next);
+          break;
+
+        case $space:
+        case $lf:
+        case $cr:
+        case $ff:
+          final destination = buffer.toString();
+          final title = _parseTitle(parser);
+          if (title == null && parser.charAt(parser.pos) != $rparen) {
+            // This looked like an inline link, until we found this $space
+            // followed by mystery characters; no longer a link.
+            return null;
+          }
+          // [_parseTitle] made sure the title was follwed by a closing `)`
+          // (but it's up to the code here to examine the balance of
+          // parentheses).
+          parenCount--;
+          if (parenCount == 0) {
+            return InlineLink(destination, title: title);
+          }
+          break;
+
+        case $lparen:
+          parenCount++;
+          buffer.writeCharCode(char);
+          break;
+
+        case $rparen:
+          parenCount--;
+          // ignore: invariant_booleans
+          if (parenCount == 0) {
+            final destination = buffer.toString();
+            return InlineLink(destination);
+          }
+          buffer.writeCharCode(char);
+          break;
+
+        default:
+          buffer.writeCharCode(char);
+      }
+      parser.advanceBy(1);
+      if (parser.isDone) {
+        return null; // EOF. Not a link.
+      }
+    }
+  }
+
+  // Walk the parser forward through any whitespace.
+  void _moveThroughWhitespace(InlineParser parser) {
+    while (true) {
+      final char = parser.charAt(parser.pos);
+      if (char != $space &&
+          char != $tab &&
+          char != $lf &&
+          char != $vt &&
+          char != $cr &&
+          char != $ff) {
+        return;
+      }
+      parser.advanceBy(1);
+      if (parser.isDone) {
+        return;
+      }
+    }
+  }
+
+  // Parse a link title in [parser] at it's current position. The parser's
+  // current position should be a whitespace character that followed a link
+  // destination.
+  String? _parseTitle(InlineParser parser) {
+    _moveThroughWhitespace(parser);
+    if (parser.isDone) {
+      return null;
+    }
+
+    // The whitespace should be followed by a title delimiter.
+    final delimiter = parser.charAt(parser.pos);
+    if (delimiter != $apostrophe &&
+        delimiter != $quote &&
+        delimiter != $lparen) {
+      return null;
+    }
+
+    final closeDelimiter = delimiter == $lparen ? $rparen : delimiter;
+    parser.advanceBy(1);
+
+    // Now we look for an un-escaped closing delimiter.
+    final buffer = StringBuffer();
+    while (true) {
+      final char = parser.charAt(parser.pos);
+      if (char == $backslash) {
+        parser.advanceBy(1);
+        final next = parser.charAt(parser.pos);
+        if (next != $backslash && next != closeDelimiter) {
+          buffer.writeCharCode(char);
+        }
+        buffer.writeCharCode(next);
+      } else if (char == closeDelimiter) {
+        break;
+      } else {
+        buffer.writeCharCode(char);
+      }
+      parser.advanceBy(1);
+      if (parser.isDone) {
+        return null;
+      }
+    }
+    final title = buffer.toString();
+
+    // Advance past the closing delimiter.
+    parser.advanceBy(1);
+    if (parser.isDone) {
+      return null;
+    }
+    _moveThroughWhitespace(parser);
+    if (parser.isDone) {
+      return null;
+    }
+    if (parser.charAt(parser.pos) != $rparen) {
+      return null;
+    }
+    return title;
+  }
+}
+
+/// Matches images like `![alternate text](url "optional title")` and
+/// `![alternate text][label]`.
+class ImageSyntax extends LinkSyntax {
+  ImageSyntax({Resolver? linkResolver})
+      : super(linkResolver: linkResolver, pattern: r'!\[');
+
+  @override
+  Node _createNode(TagState state, String destination, String? title) {
+    final element = Element.empty('img');
+    element.attributes['src'] = escapeHtml(destination);
+    element.attributes['alt'] = state.textContent;
+    if (title != null && title.isNotEmpty) {
+      element.attributes['title'] = escapeAttribute(title);
+    }
+    return element;
+  }
+
+  // Add an image node to [parser]'s AST.
+  //
+  // If [label] is present, the potential image is treated as a reference image.
+  // Otherwise, it is treated as an inline image.
+  //
+  // Returns whether the image was added successfully.
+  @override
+  bool _tryAddReferenceLink(InlineParser parser, TagState state, String label) {
+    final element =
+        _resolveReferenceLink(label, state, parser.document.linkReferences);
+    if (element == null) {
+      return false;
+    }
+    parser
+      ..addNode(element)
+      ..start = parser.pos;
+    return true;
+  }
+}
+
+/// Matches backtick-enclosed inline code blocks.
+class CodeSyntax extends InlineSyntax {
+  CodeSyntax() : super(_pattern);
+
+  // This pattern matches:
+  //
+  // * a string of backticks (not followed by any more), followed by
+  // * a non-greedy string of anything, including newlines, ending with anything
+  //   except a backtick, followed by
+  // * a string of backticks the same length as the first, not followed by any
+  //   more.
+  //
+  // This conforms to the delimiters of inline code, both in Markdown.pl, and
+  // CommonMark.
+  static const String _pattern = r'(`+(?!`))((?:.|\n)*?[^`])\1(?!`)';
+
+  @override
+  bool tryMatch(InlineParser parser, [int? startMatchPos]) {
+    if (parser.pos > 0 && parser.charAt(parser.pos - 1) == $backquote) {
+      // Not really a match! We can't just sneak past one backtick to try the
+      // next character. An example of this situation would be:
+      //
+      //     before ``` and `` after.
+      //             ^--parser.pos
+      return false;
+    }
+
+    final match = pattern.matchAsPrefix(parser.source, parser.pos);
+    if (match == null) {
+      return false;
+    }
+    parser.writeText();
+    if (onMatch(parser, match)) {
+      parser.consume(match[0]!.length);
+    }
+    return true;
+  }
+
+  @override
+  bool onMatch(InlineParser parser, Match match) {
+    parser.addNode(Element.text('code', escapeHtml(match[2]!.trim())));
+    return true;
+  }
+}
+
+/// Matches GitHub Markdown emoji syntax like `:smile:`.
+///
+/// There is no formal specification of GitHub's support for this colon-based
+/// emoji support, so this syntax is based on the results of Markdown-enabled
+/// text fields at github.com.
+class EmojiSyntax extends InlineSyntax {
+  // Emoji "aliases" are mostly limited to lower-case letters, numbers, and
+  // underscores, but GitHub also supports `:+1:` and `:-1:`.
+  EmojiSyntax() : super(':([a-z0-9_+-]+):');
+
+  @override
+  bool onMatch(InlineParser parser, Match match) {
+    final alias = match[1];
+    final emoji = emojis[alias!];
+    if (emoji == null) {
+      parser.advanceBy(1);
+      return false;
+    }
+    parser.addNode(Text(emoji));
+
+    return true;
+  }
+}
+
+/// Keeps track of a currently open tag while it is being parsed.
+///
+/// The parser maintains a stack of these so it can handle nested tags.
+class TagState {
+  TagState(this.startPos, this.endPos, this.syntax, this.openingDelimiterRun)
+      : children = <Node>[];
+
+  /// The point in the original source where this tag started.
+  final int startPos;
+
+  /// The point in the original source where open tag ended.
+  final int endPos;
+
+  /// The syntax that created this node.
+  final TagSyntax? syntax;
+
+  /// The children of this node. Will be `null` for text nodes.
+  final List<Node> children;
+
+  final _DelimiterRun? openingDelimiterRun;
+
+  /// Attempts to close this tag by matching the current text against its end
+  /// pattern.
+  bool tryMatch(InlineParser parser) {
+    final endMatch =
+        syntax!.endPattern.matchAsPrefix(parser.source, parser.pos);
+    if (endMatch == null) {
+      return false;
+    }
+
+    if (!syntax!.requiresDelimiterRun) {
+      // Close the tag.
+      close(parser, endMatch);
+      return true;
+    }
+
+    // TODO: Move this logic into TagSyntax.
+    final runLength = endMatch.group(0)!.length;
+    final openingRunLength = endPos - startPos;
+    final closingMatchStart = parser.pos;
+    final closingMatchEnd = parser.pos + runLength - 1;
+    final closingDelimiterRun =
+        _DelimiterRun.tryParse(parser, closingMatchStart, closingMatchEnd);
+    if (closingDelimiterRun != null && closingDelimiterRun.canClose) {
+      // Emphasis rules #9 and #10:
+      final oneRunOpensAndCloses =
+          (openingDelimiterRun!.canOpen && openingDelimiterRun!.canClose) ||
+              (closingDelimiterRun.canOpen && closingDelimiterRun.canClose);
+      if (oneRunOpensAndCloses &&
+          (openingRunLength + closingDelimiterRun.length!) % 3 == 0) {
+        return false;
+      }
+      // Close the tag.
+      close(parser, endMatch);
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  /// Pops this tag off the stack, completes it, and adds it to the output.
+  ///
+  /// Will discard any unmatched tags that happen to be above it on the stack.
+  /// If this is the last node in the stack, returns its children.
+  List<Node>? close(InlineParser parser, Match? endMatch) {
+    // If there are unclosed tags on top of this one when it's closed, that
+    // means they are mismatched. Mismatched tags are treated as plain text in
+    // markdown. So for each tag above this one, we write its start tag as text
+    // and then adds its children to this one's children.
+    final index = parser._stack.indexOf(this);
+
+    // Remove the unmatched children.
+    final unmatchedTags = parser._stack.sublist(index + 1);
+    parser._stack.removeRange(index + 1, parser._stack.length);
+
+    // Flatten them out onto this tag.
+    for (final unmatched in unmatchedTags) {
+      // Write the start tag as text.
+      parser.writeTextRange(unmatched.startPos, unmatched.endPos);
+
+      // Bequeath its children unto this tag.
+      children.addAll(unmatched.children);
+    }
+
+    // Pop this off the stack.
+    parser.writeText();
+    parser._stack.removeLast();
+
+    // If the stack is empty now, this is the special "results" node.
+    if (parser._stack.isEmpty) {
+      return children;
+    }
+    final endMatchIndex = parser.pos;
+
+    // We are still parsing, so add this to its parent's children.
+    if (syntax!.onMatchEnd(parser, endMatch!, this)) {
+      parser.consume(endMatch[0]!.length);
+    } else {
+      // Didn't close correctly so revert to text.
+      parser
+        ..writeTextRange(startPos, endPos)
+        .._stack.last.children.addAll(children)
+        ..pos = endMatchIndex
+        ..advanceBy(endMatch[0]!.length);
+    }
+
+    return null;
+  }
+
+  String get textContent => children.map((child) => child.textContent).join();
+}
+
+class InlineLink {
+  InlineLink(this.destination, {this.title});
+
+  final String destination;
+  final String? title;
+}

+ 71 - 0
app_flowy/lib/workspace/infrastructure/markdown/src/util.dart

@@ -0,0 +1,71 @@
+import 'dart:convert';
+
+import 'package:charcode/charcode.dart';
+
+String escapeHtml(String html) =>
+    const HtmlEscape(HtmlEscapeMode.element).convert(html);
+
+// Escape the contents of [value], so that it may be used as an HTML attribute.
+
+// Based on http://spec.commonmark.org/0.28/#backslash-escapes.
+String escapeAttribute(String value) {
+  final result = StringBuffer();
+  int ch;
+  for (var i = 0; i < value.codeUnits.length; i++) {
+    ch = value.codeUnitAt(i);
+    if (ch == $backslash) {
+      i++;
+      if (i == value.codeUnits.length) {
+        result.writeCharCode(ch);
+        break;
+      }
+      ch = value.codeUnitAt(i);
+      switch (ch) {
+        case $quote:
+          result.write('&quot;');
+          break;
+        case $exclamation:
+        case $hash:
+        case $dollar:
+        case $percent:
+        case $ampersand:
+        case $apostrophe:
+        case $lparen:
+        case $rparen:
+        case $asterisk:
+        case $plus:
+        case $comma:
+        case $dash:
+        case $dot:
+        case $slash:
+        case $colon:
+        case $semicolon:
+        case $lt:
+        case $equal:
+        case $gt:
+        case $question:
+        case $at:
+        case $lbracket:
+        case $backslash:
+        case $rbracket:
+        case $caret:
+        case $underscore:
+        case $backquote:
+        case $lbrace:
+        case $bar:
+        case $rbrace:
+        case $tilde:
+          result.writeCharCode(ch);
+          break;
+        default:
+          result.write('%5C');
+          result.writeCharCode(ch);
+      }
+    } else if (ch == $quote) {
+      result.write('%22');
+    } else {
+      result.writeCharCode(ch);
+    }
+  }
+  return result.toString();
+}

+ 2 - 0
app_flowy/lib/workspace/infrastructure/markdown/src/version.dart

@@ -0,0 +1,2 @@
+// Generated code. Do not modify.
+const packageVersion = '0.0.2';

+ 15 - 0
app_flowy/lib/workspace/infrastructure/repos/share_repo.dart

@@ -0,0 +1,15 @@
+import 'dart:async';
+import 'package:dartz/dartz.dart';
+import 'package:flowy_sdk/dispatch/dispatch.dart';
+import 'package:flowy_sdk/protobuf/flowy-workspace-infra/protobuf.dart';
+import 'package:flowy_sdk/protobuf/flowy-workspace/errors.pb.dart';
+
+class ShareRepo {
+  Future<Either<ExportData, WorkspaceError>> export(String docId, ExportType type) {
+    final request = ExportRequest.create()
+      ..docId = docId
+      ..exportType = type;
+
+    return WorkspaceEventExportDocument(request).send();
+  }
+}

+ 25 - 37
app_flowy/lib/workspace/presentation/home/navigation.dart

@@ -61,14 +61,22 @@ class FlowyNavigation extends StatelessWidget {
         );
       },
       update: (_, notifier, controller) => controller!..update(notifier),
-      child: Row(children: [
-        Selector<NavigationNotifier, PublishNotifier<bool>>(
-            selector: (context, notifier) => notifier.collapasedNotifier,
-            builder: (ctx, collapsedNotifier, child) => _renderCollapse(ctx, collapsedNotifier)),
-        Selector<NavigationNotifier, List<NavigationItem>>(
+      child: Expanded(
+        child: Row(children: [
+          Selector<NavigationNotifier, PublishNotifier<bool>>(
+              selector: (context, notifier) => notifier.collapasedNotifier,
+              builder: (ctx, collapsedNotifier, child) => _renderCollapse(ctx, collapsedNotifier)),
+          Selector<NavigationNotifier, List<NavigationItem>>(
             selector: (context, notifier) => notifier.navigationItems,
-            builder: (ctx, items, child) => Row(children: _renderNavigationItems(items))),
-      ]),
+            builder: (ctx, items, child) => Expanded(
+              child: Row(
+                children: _renderNavigationItems(items),
+                // crossAxisAlignment: WrapCrossAlignment.start,
+              ),
+            ),
+          ),
+        ]),
+      ),
     );
   }
 
@@ -106,7 +114,13 @@ class FlowyNavigation extends StatelessWidget {
     Widget last = NaviItemWidget(newItems.removeLast());
 
     List<Widget> widgets = List.empty(growable: true);
-    widgets.addAll(newItems.map((item) => NaviItemDivider(child: NaviItemWidget(item))).toList());
+    // widgets.addAll(newItems.map((item) => NaviItemDivider(child: NaviItemWidget(item))).toList());
+
+    for (final item in newItems) {
+      widgets.add(NaviItemWidget(item));
+      widgets.add(const Text('/'));
+    }
+
     widgets.add(last);
 
     return widgets;
@@ -129,39 +143,13 @@ class FlowyNavigation extends StatelessWidget {
   }
 }
 
-class IconNaviItemWidget extends StatelessWidget {
-  final NavigationItem item;
-  const IconNaviItemWidget(this.item, {Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return SizedBox(
-      height: 24,
-      child: InkWell(
-        child: item.naviTitle,
-        onTap: () {
-          debugPrint('show app document');
-        },
-      ).padding(horizontal: 8, vertical: 2),
-    );
-  }
-}
-
 class NaviItemWidget extends StatelessWidget {
   final NavigationItem item;
   const NaviItemWidget(this.item, {Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
-    return SizedBox(
-      height: 24,
-      child: InkWell(
-        child: item.naviTitle,
-        onTap: () {
-          debugPrint('show app document');
-        },
-      ).padding(horizontal: 8, vertical: 2),
-    );
+    return Expanded(child: item.leftBarItem.padding(horizontal: 2, vertical: 2));
   }
 }
 
@@ -172,7 +160,7 @@ class NaviItemDivider extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return Row(
-      children: [child, const Text('/').padding(horizontal: 2)],
+      children: [child, const Text('/')],
     );
   }
 }
@@ -184,7 +172,7 @@ class EllipsisNaviItem extends NavigationItem {
   });
 
   @override
-  Widget get naviTitle => const FlowyText.medium('...');
+  Widget get leftBarItem => const FlowyText.medium('...');
 
   @override
   NavigationCallback get action => (id) {};

+ 2 - 2
app_flowy/lib/workspace/presentation/stack_page/blank/blank_page.dart

@@ -9,10 +9,10 @@ class BlankStackContext extends HomeStackContext {
   String get identifier => "1";
 
   @override
-  Widget get naviTitle => const FlowyText.medium('Blank page', fontSize: 12);
+  Widget get leftBarItem => const FlowyText.medium('Blank page', fontSize: 12);
 
   @override
-  Widget? Function(BuildContext context) get buildNaviAction => (_) => null;
+  Widget? get rightBarItem => null;
 
   @override
   HomeStackType get type => HomeStackType.blank;

+ 145 - 62
app_flowy/lib/workspace/presentation/stack_page/doc/doc_stack_page.dart

@@ -1,15 +1,23 @@
 import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/workspace/application/doc/share_bloc.dart';
 import 'package:app_flowy/workspace/domain/i_view.dart';
 import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
 import 'package:app_flowy/workspace/domain/view_ext.dart';
+import 'package:app_flowy/workspace/infrastructure/repos/view_repo.dart';
+import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
 import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
 import 'package:flowy_infra/size.dart';
+import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
-import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flowy_infra_ui/widget/rounded_button.dart';
+import 'package:flowy_log/flowy_log.dart';
+import 'package:flowy_sdk/protobuf/flowy-workspace-infra/export.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-workspace-infra/view_create.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-workspace/errors.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:dartz/dartz.dart' as dartz;
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:clipboard/clipboard.dart';
 
 import 'doc_page.dart';
 
@@ -33,10 +41,10 @@ class DocStackContext extends HomeStackContext<String, ShareActionWrapper> {
   }
 
   @override
-  Widget get naviTitle => FlowyText.medium(_view.name, fontSize: 12);
+  Widget get leftBarItem => DocLeftBarItem(view: _view);
 
   @override
-  Widget? Function(BuildContext context) get buildNaviAction => _buildNaviAction;
+  Widget? get rightBarItem => DocShareButton(view: _view);
 
   @override
   String get identifier => _view.id;
@@ -45,7 +53,7 @@ class DocStackContext extends HomeStackContext<String, ShareActionWrapper> {
   HomeStackType get type => _view.stackType();
 
   @override
-  Widget buildWidget() => DocStackPage(_view, key: ValueKey(_view.id));
+  Widget buildWidget() => DocPage(view: _view, key: ValueKey(_view.id));
 
   @override
   List<NavigationItem> get navigationItems => _makeNavigationItems();
@@ -58,81 +66,163 @@ class DocStackContext extends HomeStackContext<String, ShareActionWrapper> {
   //     }).toList();
 
   List<NavigationItem> _makeNavigationItems() {
-    return [this];
+    return [
+      this,
+    ];
   }
 
   @override
   void dispose() {
     _listener.stop();
   }
-
-  Widget _buildNaviAction(BuildContext context) {
-    return const DocShareButton();
-  }
 }
 
-class DocShareButton extends StatelessWidget {
-  const DocShareButton({Key? key}) : super(key: key);
+class DocLeftBarItem extends StatefulWidget {
+  final View view;
+
+  const DocLeftBarItem({required this.view, Key? key}) : super(key: key);
 
   @override
-  Widget build(BuildContext context) {
-    double buttonWidth = 60;
-    return RoundedTextButton(
-      title: 'Share',
-      height: 30,
-      width: buttonWidth,
-      fontSize: 12,
-      borderRadius: Corners.s6Border,
-      color: Colors.lightBlue,
-      onPressed: () {
-        final actionList = ShareActions(onSelected: (result) {
-          result.fold(() {}, (action) {
-            switch (action) {
-              case ShareAction.markdown:
-                break;
-              case ShareAction.copyLink:
-                break;
-            }
-          });
-        });
-        actionList.show(
-          context,
-          context,
-          anchorDirection: AnchorDirection.bottomWithCenterAligned,
-          anchorOffset: Offset(-(buttonWidth / 2), 10),
-        );
-      },
-    );
-  }
+  State<DocLeftBarItem> createState() => _DocLeftBarItemState();
 }
 
-class DocStackPage extends StatefulWidget {
-  final View view;
-  const DocStackPage(this.view, {Key? key}) : super(key: key);
+class _DocLeftBarItemState extends State<DocLeftBarItem> {
+  final _controller = TextEditingController();
+  final _focusNode = FocusNode();
+  late ViewRepository repo;
 
   @override
-  _DocStackPageState createState() => _DocStackPageState();
-}
+  void initState() {
+    repo = ViewRepository(view: widget.view);
+    _focusNode.addListener(_handleFocusChanged);
 
-class _DocStackPageState extends State<DocStackPage> {
-  @override
-  Widget build(BuildContext context) {
-    return DocPage(view: widget.view);
+    super.initState();
   }
 
   @override
   void dispose() {
+    _controller.dispose();
+    _focusNode.dispose();
     super.dispose();
   }
 
   @override
-  void deactivate() {
-    super.deactivate();
+  Widget build(BuildContext context) {
+    _controller.text = widget.view.name;
+
+    final theme = context.watch<AppTheme>();
+    return IntrinsicWidth(
+      child: TextField(
+        controller: _controller,
+        focusNode: _focusNode,
+        scrollPadding: EdgeInsets.zero,
+        decoration: const InputDecoration(
+          contentPadding: EdgeInsets.zero,
+          border: InputBorder.none,
+          isDense: true,
+        ),
+        style: TextStyle(
+          color: theme.shader1,
+          fontSize: 14,
+          fontWeight: FontWeight.w500,
+          overflow: TextOverflow.ellipsis,
+        ),
+        // cursorColor: widget.cursorColor,
+        // obscureText: widget.enableObscure,
+      ),
+    );
   }
 
+  void _handleFocusChanged() {
+    if (_controller.text.isEmpty) {
+      _controller.text = widget.view.name;
+      return;
+    }
+
+    if (_controller.text != widget.view.name) {
+      repo.updateView(name: _controller.text);
+    }
+  }
+}
+
+class DocShareButton extends StatelessWidget {
+  final View view;
+  DocShareButton({Key? key, required this.view}) : super(key: ValueKey(view.id));
+
   @override
-  void didUpdateWidget(covariant DocStackPage oldWidget) {
-    super.didUpdateWidget(oldWidget);
+  Widget build(BuildContext context) {
+    double buttonWidth = 60;
+    return BlocProvider(
+      create: (context) => getIt<DocShareBloc>(param1: view),
+      child: BlocListener<DocShareBloc, DocShareState>(
+        listener: (context, state) {
+          state.map(
+            initial: (_) {},
+            loading: (_) {},
+            finish: (state) {
+              state.successOrFail.fold(
+                _handleExportData,
+                _handleExportError,
+              );
+            },
+          );
+        },
+        child: BlocBuilder<DocShareBloc, DocShareState>(
+          builder: (context, state) {
+            return RoundedTextButton(
+              title: 'Share',
+              height: 30,
+              width: buttonWidth,
+              fontSize: 12,
+              borderRadius: Corners.s6Border,
+              color: Colors.lightBlue,
+              onPressed: () => _showActionList(context, Offset(-(buttonWidth / 2), 10)),
+            );
+          },
+        ),
+      ),
+    );
+  }
+
+  void _handleExportData(ExportData exportData) {
+    switch (exportData.exportType) {
+      case ExportType.Link:
+        // TODO: Handle this case.
+        break;
+      case ExportType.Markdown:
+        FlutterClipboard.copy(exportData.data).then((value) => Log.info('copied to clipboard'));
+        break;
+      case ExportType.Text:
+        // TODO: Handle this case.
+        break;
+    }
+  }
+
+  void _handleExportError(WorkspaceError error) {}
+
+  void _showActionList(BuildContext context, Offset offset) {
+    final actionList = ShareActions(onSelected: (result) {
+      result.fold(() {}, (action) {
+        switch (action) {
+          case ShareAction.markdown:
+            context.read<DocShareBloc>().add(const DocShareEvent.shareMarkdown());
+            break;
+          case ShareAction.copyLink:
+            showWorkInProgressDialog(context);
+            break;
+        }
+      });
+    });
+    actionList.show(
+      context,
+      context,
+      anchorDirection: AnchorDirection.bottomWithCenterAligned,
+      anchorOffset: offset,
+    );
+  }
+
+  void showWorkInProgressDialog(BuildContext context) {
+    const FlowyAlertDialog(title: "Work in progress").show(context);
   }
 }
 
@@ -140,9 +230,7 @@ class ShareActions with ActionList<ShareActionWrapper> implements FlowyOverlayDe
   final Function(dartz.Option<ShareAction>) onSelected;
   final _items = ShareAction.values.map((action) => ShareActionWrapper(action)).toList();
 
-  ShareActions({
-    required this.onSelected,
-  });
+  ShareActions({required this.onSelected});
 
   @override
   double get maxWidth => 130;
@@ -167,12 +255,7 @@ class ShareActions with ActionList<ShareActionWrapper> implements FlowyOverlayDe
   FlowyOverlayDelegate? get delegate => this;
 
   @override
-  void didRemove() {
-    onSelected(dartz.none());
-  }
-
-  @override
-  ListOverlayFooter? get footer => null;
+  void didRemove() => onSelected(dartz.none());
 }
 
 enum ShareAction {

+ 2 - 2
app_flowy/lib/workspace/presentation/stack_page/trash/trash_page.dart

@@ -26,10 +26,10 @@ class TrashStackContext extends HomeStackContext {
   String get identifier => "TrashStackContext";
 
   @override
-  Widget get naviTitle => const FlowyText.medium('Trash', fontSize: 12);
+  Widget get leftBarItem => const FlowyText.medium('Trash', fontSize: 12);
 
   @override
-  Widget? Function(BuildContext context) get buildNaviAction => (_) => null;
+  Widget? get rightBarItem => null;
 
   @override
   HomeStackType get type => HomeStackType.trash;

+ 48 - 1
app_flowy/lib/workspace/presentation/widgets/dialogs.dart

@@ -53,7 +53,8 @@ class _CreateTextFieldDialog extends State<TextFieldDialog> {
             VSpace(Insets.sm * 1.5),
           ],
           FlowyFormTextInput(
-            hintText: widget.value,
+            hintText: "Page name",
+            initialValue: widget.value,
             textStyle: const TextStyle(fontSize: 24, fontWeight: FontWeight.w400),
             autoFocus: true,
             onChanged: (text) {
@@ -77,6 +78,52 @@ class _CreateTextFieldDialog extends State<TextFieldDialog> {
   }
 }
 
+class FlowyAlertDialog extends StatefulWidget {
+  final String title;
+  final void Function()? cancel;
+  final void Function()? confirm;
+
+  const FlowyAlertDialog({
+    required this.title,
+    this.confirm,
+    this.cancel,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<FlowyAlertDialog> createState() => _CreateFlowyAlertDialog();
+}
+
+class _CreateFlowyAlertDialog extends State<FlowyAlertDialog> {
+  @override
+  void initState() {
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    return StyledDialog(
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: <Widget>[
+          ...[
+            FlowyText.medium(widget.title, color: theme.shader4),
+          ],
+          if (widget.confirm != null) ...[
+            const VSpace(20),
+            OkCancelButton(
+              onOkPressed: widget.confirm!,
+              onCancelPressed: widget.confirm,
+            )
+          ]
+        ],
+      ),
+    );
+  }
+}
+
 class OkCancelDialog extends StatelessWidget {
   final VoidCallback? onOkPressed;
   final VoidCallback? onCancelPressed;

+ 2 - 2
app_flowy/lib/workspace/presentation/widgets/home_top_bar.dart

@@ -21,12 +21,12 @@ class HomeTopBar extends StatelessWidget {
         crossAxisAlignment: CrossAxisAlignment.center,
         children: [
           const FlowyNavigation(),
-          const Spacer(),
+          const HSpace(16),
           ChangeNotifierProvider.value(
             value: Provider.of<HomeStackNotifier>(context, listen: false),
             child: Consumer(
               builder: (BuildContext context, HomeStackNotifier notifier, Widget? child) {
-                return notifier.stackContext.buildNaviAction(context) ?? const SizedBox();
+                return notifier.stackContext.rightBarItem ?? const SizedBox();
               },
             ),
           ) // _renderMoreButton(),

+ 8 - 5
app_flowy/lib/workspace/presentation/widgets/menu/widget/app/header/header.dart

@@ -98,11 +98,14 @@ class MenuAppHeader extends StatelessWidget {
   }
 
   Widget _renderAddButton(BuildContext context) {
-    return AddButton(
-      onSelected: (viewType) {
-        context.read<AppBloc>().add(AppEvent.createView("New view", "", viewType));
-      },
-    ).padding(right: MenuAppSizes.headerPadding);
+    return Tooltip(
+      message: "Quickly add a page inside",
+      child: AddButton(
+        onSelected: (viewType) {
+          context.read<AppBloc>().add(AppEvent.createView("New view", "", viewType));
+        },
+      ).padding(right: MenuAppSizes.headerPadding),
+    );
   }
 
   void _handleAction(BuildContext context, Option<AppDisclosureAction> action) {

+ 0 - 3
app_flowy/lib/workspace/presentation/widgets/menu/widget/app/header/right_click_action.dart

@@ -32,9 +32,6 @@ class AppDisclosureActionSheet with ActionList<DisclosureActionWrapper> implemen
   void didRemove() {
     onSelected(dartz.none());
   }
-
-  @override
-  ListOverlayFooter? get footer => null;
 }
 
 class DisclosureActionWrapper extends ActionItem {

+ 0 - 3
app_flowy/lib/workspace/presentation/widgets/menu/widget/app/section/disclosure_action.dart

@@ -52,9 +52,6 @@ class ViewDisclosureButton extends StatelessWidget
   void didRemove() {
     onSelected(dartz.none());
   }
-
-  @override
-  ListOverlayFooter? get footer => null;
 }
 
 class ViewDisclosureActionWrapper extends ActionItem {

+ 1 - 2
app_flowy/lib/workspace/presentation/widgets/menu/widget/app/section/item.dart

@@ -56,11 +56,10 @@ class ViewSectionItem extends StatelessWidget {
     List<Widget> children = [
       SizedBox(width: 16, height: 16, child: state.view.thumbnail()),
       const HSpace(2),
-      FlowyText.regular(state.view.name, fontSize: 12),
+      Expanded(child: FlowyText.regular(state.view.name, fontSize: 12)),
     ];
 
     if (onHover || state.isEditing) {
-      children.add(const Spacer());
       children.add(
         ViewDisclosureButton(
           onTap: () => context.read<ViewBloc>().add(const ViewEvent.setIsEditing(true)),

+ 1 - 1
app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart

@@ -17,7 +17,7 @@ abstract class ActionList<T extends ActionItem> {
 
   double get itemHeight => ActionListSizes.itemHeight;
 
-  ListOverlayFooter? get footer;
+  ListOverlayFooter? get footer => null;
 
   void Function(dartz.Option<T>) get selectCallback;
 

+ 10 - 20
app_flowy/packages/flowy_infra_ui/lib/style_widget/text_input.dart

@@ -10,8 +10,7 @@ import 'package:provider/provider.dart';
 import 'package:textstyle_extensions/textstyle_extensions.dart';
 
 class FlowyFormTextInput extends StatelessWidget {
-  static EdgeInsets kDefaultTextInputPadding =
-      EdgeInsets.only(bottom: Insets.sm, top: 4);
+  static EdgeInsets kDefaultTextInputPadding = EdgeInsets.only(bottom: Insets.sm, top: 4);
 
   final String? label;
   final bool? autoFocus;
@@ -61,8 +60,7 @@ class FlowyFormTextInput extends StatelessWidget {
       inputDecoration: InputDecoration(
         isDense: true,
         contentPadding: contentPadding ?? kDefaultTextInputPadding,
-        border: const ThinUnderlineBorder(
-            borderSide: BorderSide(width: 5, color: Colors.red)),
+        border: const ThinUnderlineBorder(borderSide: BorderSide(width: 5, color: Colors.red)),
         //focusedBorder: UnderlineInputBorder(borderSide: BorderSide(width: .5, color: Colors.red)),
         hintText: hintText,
       ),
@@ -143,8 +141,7 @@ class StyledSearchTextInputState extends State<StyledSearchTextInput> {
 
   @override
   void initState() {
-    _controller =
-        widget.controller ?? TextEditingController(text: widget.initialValue);
+    _controller = widget.controller ?? TextEditingController(text: widget.initialValue);
     _focusNode = FocusNode(
       debugLabel: widget.label ?? '',
       onKey: (FocusNode node, RawKeyEvent evt) {
@@ -160,8 +157,7 @@ class StyledSearchTextInputState extends State<StyledSearchTextInput> {
       canRequestFocus: true,
     );
     // Listen for focus out events
-    _focusNode
-        .addListener(() => widget.onFocusChanged?.call(_focusNode.hasFocus));
+    _focusNode.addListener(() => widget.onFocusChanged?.call(_focusNode.hasFocus));
     widget.onFocusCreated?.call(_focusNode);
     if (widget.autoFocus ?? false) {
       scheduleMicrotask(() => _focusNode.requestFocus());
@@ -210,8 +206,7 @@ class StyledSearchTextInputState extends State<StyledSearchTextInput> {
             InputDecoration(
                 prefixIcon: widget.prefixIcon,
                 suffixIcon: widget.suffixIcon,
-                contentPadding:
-                    widget.contentPadding ?? EdgeInsets.all(Insets.m),
+                contentPadding: widget.contentPadding ?? EdgeInsets.all(Insets.m),
                 border: const OutlineInputBorder(borderSide: BorderSide.none),
                 isDense: true,
                 icon: widget.icon == null ? null : Icon(widget.icon),
@@ -259,8 +254,7 @@ class ThinUnderlineBorder extends InputBorder {
   bool get isOutline => false;
 
   @override
-  UnderlineInputBorder copyWith(
-      {BorderSide? borderSide, BorderRadius? borderRadius}) {
+  UnderlineInputBorder copyWith({BorderSide? borderSide, BorderRadius? borderRadius}) {
     return UnderlineInputBorder(
       borderSide: borderSide ?? this.borderSide,
       borderRadius: borderRadius ?? this.borderRadius,
@@ -280,8 +274,7 @@ class ThinUnderlineBorder extends InputBorder {
   @override
   Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
     return Path()
-      ..addRect(Rect.fromLTWH(rect.left, rect.top, rect.width,
-          math.max(0.0, rect.height - borderSide.width)));
+      ..addRect(Rect.fromLTWH(rect.left, rect.top, rect.width, math.max(0.0, rect.height - borderSide.width)));
   }
 
   @override
@@ -292,8 +285,7 @@ class ThinUnderlineBorder extends InputBorder {
   @override
   ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
     if (a is UnderlineInputBorder) {
-      final newBorderRadius =
-          BorderRadius.lerp(a.borderRadius, borderRadius, t);
+      final newBorderRadius = BorderRadius.lerp(a.borderRadius, borderRadius, t);
 
       if (newBorderRadius != null) {
         return UnderlineInputBorder(
@@ -308,8 +300,7 @@ class ThinUnderlineBorder extends InputBorder {
   @override
   ShapeBorder? lerpTo(ShapeBorder? b, double t) {
     if (b is UnderlineInputBorder) {
-      final newBorderRadius =
-          BorderRadius.lerp(b.borderRadius, borderRadius, t);
+      final newBorderRadius = BorderRadius.lerp(b.borderRadius, borderRadius, t);
       if (newBorderRadius != null) {
         return UnderlineInputBorder(
           borderSide: BorderSide.lerp(borderSide, b.borderSide, t),
@@ -335,8 +326,7 @@ class ThinUnderlineBorder extends InputBorder {
     double gapPercentage = 0.0,
     TextDirection? textDirection,
   }) {
-    if (borderRadius.bottomLeft != Radius.zero ||
-        borderRadius.bottomRight != Radius.zero) {
+    if (borderRadius.bottomLeft != Radius.zero || borderRadius.bottomRight != Radius.zero) {
       canvas.clipPath(getOuterPath(rect, textDirection: textDirection));
     }
     canvas.drawLine(rect.bottomLeft, rect.bottomRight, borderSide.toPaint());

+ 4 - 1
app_flowy/packages/flowy_infra_ui/lib/widget/rounded_input_field.dart

@@ -17,12 +17,15 @@ class RoundedInputField extends StatefulWidget {
   final String errorText;
   final TextStyle style;
   final ValueChanged<String>? onChanged;
+  final String? initialValue;
   late bool enableObscure;
   var _text = "";
 
   RoundedInputField({
     Key? key,
     this.hintText,
+    this.errorText = "",
+    this.initialValue,
     this.icon,
     this.obscureText = false,
     this.obscureIcon,
@@ -32,7 +35,6 @@ class RoundedInputField extends StatefulWidget {
     this.highlightBorderColor = Colors.transparent,
     this.cursorColor = Colors.black,
     this.style = const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
-    this.errorText = "",
   }) : super(key: key) {
     enableObscure = obscureText;
   }
@@ -62,6 +64,7 @@ class _RoundedInputFieldState extends State<RoundedInputField> {
         borderRadius: Corners.s10Border,
         borderColor: borderColor,
         child: TextFormField(
+          initialValue: widget.initialValue,
           onChanged: (value) {
             widget._text = value;
             if (widget.onChanged != null) {

+ 14 - 0
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace-infra/export.pb.dart

@@ -77,17 +77,22 @@ class ExportRequest extends $pb.GeneratedMessage {
 class ExportData extends $pb.GeneratedMessage {
   static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'ExportData', createEmptyInstance: create)
     ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'data')
+    ..e<ExportType>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'exportType', $pb.PbFieldType.OE, defaultOrMaker: ExportType.Text, valueOf: ExportType.valueOf, enumValues: ExportType.values)
     ..hasRequiredFields = false
   ;
 
   ExportData._() : super();
   factory ExportData({
     $core.String? data,
+    ExportType? exportType,
   }) {
     final _result = create();
     if (data != null) {
       _result.data = data;
     }
+    if (exportType != null) {
+      _result.exportType = exportType;
+    }
     return _result;
   }
   factory ExportData.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
@@ -119,5 +124,14 @@ class ExportData extends $pb.GeneratedMessage {
   $core.bool hasData() => $_has(0);
   @$pb.TagNumber(1)
   void clearData() => clearField(1);
+
+  @$pb.TagNumber(2)
+  ExportType get exportType => $_getN(1);
+  @$pb.TagNumber(2)
+  set exportType(ExportType v) { setField(2, v); }
+  @$pb.TagNumber(2)
+  $core.bool hasExportType() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearExportType() => clearField(2);
 }
 

+ 4 - 2
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace-infra/export.pbenum.dart

@@ -11,11 +11,13 @@ import 'package:protobuf/protobuf.dart' as $pb;
 
 class ExportType extends $pb.ProtobufEnum {
   static const ExportType Text = ExportType._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Text');
-  static const ExportType RichText = ExportType._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'RichText');
+  static const ExportType Markdown = ExportType._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Markdown');
+  static const ExportType Link = ExportType._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Link');
 
   static const $core.List<ExportType> values = <ExportType> [
     Text,
-    RichText,
+    Markdown,
+    Link,
   ];
 
   static final $core.Map<$core.int, ExportType> _byValue = $pb.ProtobufEnum.initByValue(values);

+ 5 - 3
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace-infra/export.pbjson.dart

@@ -13,12 +13,13 @@ const ExportType$json = const {
   '1': 'ExportType',
   '2': const [
     const {'1': 'Text', '2': 0},
-    const {'1': 'RichText', '2': 1},
+    const {'1': 'Markdown', '2': 1},
+    const {'1': 'Link', '2': 2},
   ],
 };
 
 /// Descriptor for `ExportType`. Decode as a `google.protobuf.EnumDescriptorProto`.
-final $typed_data.Uint8List exportTypeDescriptor = $convert.base64Decode('CgpFeHBvcnRUeXBlEggKBFRleHQQABIMCghSaWNoVGV4dBAB');
+final $typed_data.Uint8List exportTypeDescriptor = $convert.base64Decode('CgpFeHBvcnRUeXBlEggKBFRleHQQABIMCghNYXJrZG93bhABEggKBExpbmsQAg==');
 @$core.Deprecated('Use exportRequestDescriptor instead')
 const ExportRequest$json = const {
   '1': 'ExportRequest',
@@ -35,8 +36,9 @@ const ExportData$json = const {
   '1': 'ExportData',
   '2': const [
     const {'1': 'data', '3': 1, '4': 1, '5': 9, '10': 'data'},
+    const {'1': 'export_type', '3': 2, '4': 1, '5': 14, '6': '.ExportType', '10': 'exportType'},
   ],
 };
 
 /// Descriptor for `ExportData`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List exportDataDescriptor = $convert.base64Decode('CgpFeHBvcnREYXRhEhIKBGRhdGEYASABKAlSBGRhdGE=');
+final $typed_data.Uint8List exportDataDescriptor = $convert.base64Decode('CgpFeHBvcnREYXRhEhIKBGRhdGEYASABKAlSBGRhdGESLAoLZXhwb3J0X3R5cGUYAiABKA4yCy5FeHBvcnRUeXBlUgpleHBvcnRUeXBl');

Файловите разлики са ограничени, защото са твърде много
+ 201 - 194
app_flowy/pubspec.lock


+ 2 - 0
app_flowy/pubspec.yaml

@@ -65,6 +65,8 @@ dependencies:
   package_info_plus: ^1.3.0
   url_launcher: ^6.0.2
   avatars: ^2.0.0
+  # file_picker: ^4.2.1
+  clipboard: ^0.1.3
 
   # The following adds the Cupertino Icons font to your application.
   # Use with the CupertinoIcons class for iOS style icons.

+ 3 - 3
app_flowy/windows/flutter/generated_plugin_registrant.cc

@@ -8,7 +8,7 @@
 
 #include <flowy_editor/flowy_editor_plugin.h>
 #include <flowy_infra_ui/flowy_infra_u_i_plugin.h>
-#include <url_launcher_windows/url_launcher_plugin.h>
+#include <url_launcher_windows/url_launcher_windows.h>
 #include <window_size/window_size_plugin.h>
 
 void RegisterPlugins(flutter::PluginRegistry* registry) {
@@ -16,8 +16,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
       registry->GetRegistrarForPlugin("FlowyEditorPlugin"));
   FlowyInfraUIPluginRegisterWithRegistrar(
       registry->GetRegistrarForPlugin("FlowyInfraUIPlugin"));
-  UrlLauncherPluginRegisterWithRegistrar(
-      registry->GetRegistrarForPlugin("UrlLauncherPlugin"));
+  UrlLauncherWindowsRegisterWithRegistrar(
+      registry->GetRegistrarForPlugin("UrlLauncherWindows"));
   WindowSizePluginRegisterWithRegistrar(
       registry->GetRegistrarForPlugin("WindowSizePlugin"));
 }

+ 1 - 0
delta_markdown

@@ -0,0 +1 @@
+Subproject commit 3e121c546fe40f91e9817ad0e0f04910315db681

BIN
doc/imgs/run.png


+ 7 - 2
rust-lib/flowy-workspace-infra/src/entities/share/export.rs

@@ -5,7 +5,8 @@ use std::convert::TryInto;
 #[derive(PartialEq, Debug, ProtoBuf_Enum, Clone)]
 pub enum ExportType {
     Text     = 0,
-    RichText = 1,
+    Markdown = 1,
+    Link     = 2,
 }
 
 impl std::default::Default for ExportType {
@@ -16,7 +17,8 @@ impl std::convert::From<i32> for ExportType {
     fn from(val: i32) -> Self {
         match val {
             0 => ExportType::Text,
-            1 => ExportType::RichText,
+            1 => ExportType::Markdown,
+            2 => ExportType::Link,
             _ => {
                 log::error!("Invalid export type: {}", val);
                 ExportType::Text
@@ -54,4 +56,7 @@ impl TryInto<ExportParams> for ExportRequest {
 pub struct ExportData {
     #[pb(index = 1)]
     pub data: String,
+
+    #[pb(index = 2)]
+    pub export_type: ExportType,
 }

+ 62 - 22
rust-lib/flowy-workspace-infra/src/protobuf/model/export.rs

@@ -217,6 +217,7 @@ impl ::protobuf::reflect::ProtobufValue for ExportRequest {
 pub struct ExportData {
     // message fields
     pub data: ::std::string::String,
+    pub export_type: ExportType,
     // special fields
     pub unknown_fields: ::protobuf::UnknownFields,
     pub cached_size: ::protobuf::CachedSize,
@@ -258,6 +259,21 @@ impl ExportData {
     pub fn take_data(&mut self) -> ::std::string::String {
         ::std::mem::replace(&mut self.data, ::std::string::String::new())
     }
+
+    // .ExportType export_type = 2;
+
+
+    pub fn get_export_type(&self) -> ExportType {
+        self.export_type
+    }
+    pub fn clear_export_type(&mut self) {
+        self.export_type = ExportType::Text;
+    }
+
+    // Param is passed by value, moved
+    pub fn set_export_type(&mut self, v: ExportType) {
+        self.export_type = v;
+    }
 }
 
 impl ::protobuf::Message for ExportData {
@@ -272,6 +288,9 @@ impl ::protobuf::Message for ExportData {
                 1 => {
                     ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.data)?;
                 },
+                2 => {
+                    ::protobuf::rt::read_proto3_enum_with_unknown_fields_into(wire_type, is, &mut self.export_type, 2, &mut self.unknown_fields)?
+                },
                 _ => {
                     ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
                 },
@@ -287,6 +306,9 @@ impl ::protobuf::Message for ExportData {
         if !self.data.is_empty() {
             my_size += ::protobuf::rt::string_size(1, &self.data);
         }
+        if self.export_type != ExportType::Text {
+            my_size += ::protobuf::rt::enum_size(2, self.export_type);
+        }
         my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
         self.cached_size.set(my_size);
         my_size
@@ -296,6 +318,9 @@ impl ::protobuf::Message for ExportData {
         if !self.data.is_empty() {
             os.write_string(1, &self.data)?;
         }
+        if self.export_type != ExportType::Text {
+            os.write_enum(2, ::protobuf::ProtobufEnum::value(&self.export_type))?;
+        }
         os.write_unknown_fields(self.get_unknown_fields())?;
         ::std::result::Result::Ok(())
     }
@@ -339,6 +364,11 @@ impl ::protobuf::Message for ExportData {
                 |m: &ExportData| { &m.data },
                 |m: &mut ExportData| { &mut m.data },
             ));
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeEnum<ExportType>>(
+                "export_type",
+                |m: &ExportData| { &m.export_type },
+                |m: &mut ExportData| { &mut m.export_type },
+            ));
             ::protobuf::reflect::MessageDescriptor::new_pb_name::<ExportData>(
                 "ExportData",
                 fields,
@@ -356,6 +386,7 @@ impl ::protobuf::Message for ExportData {
 impl ::protobuf::Clear for ExportData {
     fn clear(&mut self) {
         self.data.clear();
+        self.export_type = ExportType::Text;
         self.unknown_fields.clear();
     }
 }
@@ -375,7 +406,8 @@ impl ::protobuf::reflect::ProtobufValue for ExportData {
 #[derive(Clone,PartialEq,Eq,Debug,Hash)]
 pub enum ExportType {
     Text = 0,
-    RichText = 1,
+    Markdown = 1,
+    Link = 2,
 }
 
 impl ::protobuf::ProtobufEnum for ExportType {
@@ -386,7 +418,8 @@ impl ::protobuf::ProtobufEnum for ExportType {
     fn from_i32(value: i32) -> ::std::option::Option<ExportType> {
         match value {
             0 => ::std::option::Option::Some(ExportType::Text),
-            1 => ::std::option::Option::Some(ExportType::RichText),
+            1 => ::std::option::Option::Some(ExportType::Markdown),
+            2 => ::std::option::Option::Some(ExportType::Link),
             _ => ::std::option::Option::None
         }
     }
@@ -394,7 +427,8 @@ impl ::protobuf::ProtobufEnum for ExportType {
     fn values() -> &'static [Self] {
         static values: &'static [ExportType] = &[
             ExportType::Text,
-            ExportType::RichText,
+            ExportType::Markdown,
+            ExportType::Link,
         ];
         values
     }
@@ -425,25 +459,31 @@ impl ::protobuf::reflect::ProtobufValue for ExportType {
 static file_descriptor_proto_data: &'static [u8] = b"\
     \n\x0cexport.proto\"T\n\rExportRequest\x12\x15\n\x06doc_id\x18\x01\x20\
     \x01(\tR\x05docId\x12,\n\x0bexport_type\x18\x02\x20\x01(\x0e2\x0b.Export\
-    TypeR\nexportType\"\x20\n\nExportData\x12\x12\n\x04data\x18\x01\x20\x01(\
-    \tR\x04data*$\n\nExportType\x12\x08\n\x04Text\x10\0\x12\x0c\n\x08RichTex\
-    t\x10\x01J\xd1\x02\n\x06\x12\x04\0\0\x0c\x01\n\x08\n\x01\x0c\x12\x03\0\0\
-    \x12\n\n\n\x02\x04\0\x12\x04\x02\0\x05\x01\n\n\n\x03\x04\0\x01\x12\x03\
-    \x02\x08\x15\n\x0b\n\x04\x04\0\x02\0\x12\x03\x03\x04\x16\n\x0c\n\x05\x04\
-    \0\x02\0\x05\x12\x03\x03\x04\n\n\x0c\n\x05\x04\0\x02\0\x01\x12\x03\x03\
-    \x0b\x11\n\x0c\n\x05\x04\0\x02\0\x03\x12\x03\x03\x14\x15\n\x0b\n\x04\x04\
-    \0\x02\x01\x12\x03\x04\x04\x1f\n\x0c\n\x05\x04\0\x02\x01\x06\x12\x03\x04\
-    \x04\x0e\n\x0c\n\x05\x04\0\x02\x01\x01\x12\x03\x04\x0f\x1a\n\x0c\n\x05\
-    \x04\0\x02\x01\x03\x12\x03\x04\x1d\x1e\n\n\n\x02\x04\x01\x12\x04\x06\0\
-    \x08\x01\n\n\n\x03\x04\x01\x01\x12\x03\x06\x08\x12\n\x0b\n\x04\x04\x01\
-    \x02\0\x12\x03\x07\x04\x14\n\x0c\n\x05\x04\x01\x02\0\x05\x12\x03\x07\x04\
-    \n\n\x0c\n\x05\x04\x01\x02\0\x01\x12\x03\x07\x0b\x0f\n\x0c\n\x05\x04\x01\
-    \x02\0\x03\x12\x03\x07\x12\x13\n\n\n\x02\x05\0\x12\x04\t\0\x0c\x01\n\n\n\
-    \x03\x05\0\x01\x12\x03\t\x05\x0f\n\x0b\n\x04\x05\0\x02\0\x12\x03\n\x04\r\
-    \n\x0c\n\x05\x05\0\x02\0\x01\x12\x03\n\x04\x08\n\x0c\n\x05\x05\0\x02\0\
-    \x02\x12\x03\n\x0b\x0c\n\x0b\n\x04\x05\0\x02\x01\x12\x03\x0b\x04\x11\n\
-    \x0c\n\x05\x05\0\x02\x01\x01\x12\x03\x0b\x04\x0c\n\x0c\n\x05\x05\0\x02\
-    \x01\x02\x12\x03\x0b\x0f\x10b\x06proto3\
+    TypeR\nexportType\"N\n\nExportData\x12\x12\n\x04data\x18\x01\x20\x01(\tR\
+    \x04data\x12,\n\x0bexport_type\x18\x02\x20\x01(\x0e2\x0b.ExportTypeR\nex\
+    portType*.\n\nExportType\x12\x08\n\x04Text\x10\0\x12\x0c\n\x08Markdown\
+    \x10\x01\x12\x08\n\x04Link\x10\x02J\xb1\x03\n\x06\x12\x04\0\0\x0e\x01\n\
+    \x08\n\x01\x0c\x12\x03\0\0\x12\n\n\n\x02\x04\0\x12\x04\x02\0\x05\x01\n\n\
+    \n\x03\x04\0\x01\x12\x03\x02\x08\x15\n\x0b\n\x04\x04\0\x02\0\x12\x03\x03\
+    \x04\x16\n\x0c\n\x05\x04\0\x02\0\x05\x12\x03\x03\x04\n\n\x0c\n\x05\x04\0\
+    \x02\0\x01\x12\x03\x03\x0b\x11\n\x0c\n\x05\x04\0\x02\0\x03\x12\x03\x03\
+    \x14\x15\n\x0b\n\x04\x04\0\x02\x01\x12\x03\x04\x04\x1f\n\x0c\n\x05\x04\0\
+    \x02\x01\x06\x12\x03\x04\x04\x0e\n\x0c\n\x05\x04\0\x02\x01\x01\x12\x03\
+    \x04\x0f\x1a\n\x0c\n\x05\x04\0\x02\x01\x03\x12\x03\x04\x1d\x1e\n\n\n\x02\
+    \x04\x01\x12\x04\x06\0\t\x01\n\n\n\x03\x04\x01\x01\x12\x03\x06\x08\x12\n\
+    \x0b\n\x04\x04\x01\x02\0\x12\x03\x07\x04\x14\n\x0c\n\x05\x04\x01\x02\0\
+    \x05\x12\x03\x07\x04\n\n\x0c\n\x05\x04\x01\x02\0\x01\x12\x03\x07\x0b\x0f\
+    \n\x0c\n\x05\x04\x01\x02\0\x03\x12\x03\x07\x12\x13\n\x0b\n\x04\x04\x01\
+    \x02\x01\x12\x03\x08\x04\x1f\n\x0c\n\x05\x04\x01\x02\x01\x06\x12\x03\x08\
+    \x04\x0e\n\x0c\n\x05\x04\x01\x02\x01\x01\x12\x03\x08\x0f\x1a\n\x0c\n\x05\
+    \x04\x01\x02\x01\x03\x12\x03\x08\x1d\x1e\n\n\n\x02\x05\0\x12\x04\n\0\x0e\
+    \x01\n\n\n\x03\x05\0\x01\x12\x03\n\x05\x0f\n\x0b\n\x04\x05\0\x02\0\x12\
+    \x03\x0b\x04\r\n\x0c\n\x05\x05\0\x02\0\x01\x12\x03\x0b\x04\x08\n\x0c\n\
+    \x05\x05\0\x02\0\x02\x12\x03\x0b\x0b\x0c\n\x0b\n\x04\x05\0\x02\x01\x12\
+    \x03\x0c\x04\x11\n\x0c\n\x05\x05\0\x02\x01\x01\x12\x03\x0c\x04\x0c\n\x0c\
+    \n\x05\x05\0\x02\x01\x02\x12\x03\x0c\x0f\x10\n\x0b\n\x04\x05\0\x02\x02\
+    \x12\x03\r\x04\r\n\x0c\n\x05\x05\0\x02\x02\x01\x12\x03\r\x04\x08\n\x0c\n\
+    \x05\x05\0\x02\x02\x02\x12\x03\r\x0b\x0cb\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 3 - 1
rust-lib/flowy-workspace-infra/src/protobuf/proto/export.proto

@@ -6,8 +6,10 @@ message ExportRequest {
 }
 message ExportData {
     string data = 1;
+    ExportType export_type = 2;
 }
 enum ExportType {
     Text = 0;
-    RichText = 1;
+    Markdown = 1;
+    Link = 2;
 }

+ 4 - 1
rust-lib/flowy-workspace/src/services/view_controller.rs

@@ -147,7 +147,10 @@ impl ViewController {
             .read_document_data(doc_identifier, self.database.db_pool()?)
             .await?;
 
-        Ok(ExportData { data: doc.data })
+        Ok(ExportData {
+            data: doc.data,
+            export_type: params.export_type,
+        })
     }
 
     // belong_to_id will be the app_id or view_id.

Някои файлове не бяха показани, защото твърде много файлове са промени