diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart index ffe65ea7cc..b4179777c9 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart @@ -82,11 +82,11 @@ void main() { HeadingBlockKeys.type, ); expect( - importedPageEditorState.getNodeAtPath([2])!.type, + importedPageEditorState.getNodeAtPath([1])!.type, HeadingBlockKeys.type, ); expect( - importedPageEditorState.getNodeAtPath([4])!.type, + importedPageEditorState.getNodeAtPath([2])!.type, TableBlockKeys.type, ); }); diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/ai_message_bubble.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/ai_message_bubble.dart index 8246378bda..61788a650f 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/ai_message_bubble.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/ai_message_bubble.dart @@ -1,10 +1,16 @@ +import 'dart:convert'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_input.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_popmenu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; +import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -176,9 +182,20 @@ class CopyButton extends StatelessWidget { size: const Size.square(14), color: Theme.of(context).colorScheme.primary, ), - onPressed: () { - Clipboard.setData(ClipboardData(text: textMessage.text)); - showMessageToast(LocaleKeys.grid_row_copyProperty.tr()); + onPressed: () async { + final document = markdownToDocument(textMessage.text); + await getIt().setData( + ClipboardServiceData( + plainText: textMessage.text, + inAppJson: jsonEncode(document.toJson()), + ), + ); + if (context.mounted) { + showToastNotification( + context, + message: LocaleKeys.grid_url_copiedNotification.tr(), + ); + } }, ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart index 48c69eef97..6a819e8d53 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart @@ -1,10 +1,9 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; import 'chat_input.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_markdown_text.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_markdown_text.dart new file mode 100644 index 0000000000..59f89ad3a3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_markdown_text.dart @@ -0,0 +1,377 @@ +import 'package:appflowy/plugins/document/presentation/editor_configuration.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/plugins/document/presentation/editor_style.dart'; +import 'package:appflowy/util/theme_extension.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:markdown_widget/markdown_widget.dart'; + +import 'selectable_highlight.dart'; + +enum AIMarkdownType { + appflowyEditor, + markdownWidget, +} + +// Wrap the appflowy_editor or markdown_widget as a chat text message widget +class AIMarkdownText extends StatelessWidget { + const AIMarkdownText({ + super.key, + required this.markdown, + this.type = AIMarkdownType.appflowyEditor, + }); + + final String markdown; + final AIMarkdownType type; + + @override + Widget build(BuildContext context) { + switch (type) { + case AIMarkdownType.appflowyEditor: + return _AppFlowyEditorMarkdown(markdown: markdown); + case AIMarkdownType.markdownWidget: + return _ThirdPartyMarkdown(markdown: markdown); + } + } +} + +class _AppFlowyEditorMarkdown extends StatefulWidget { + const _AppFlowyEditorMarkdown({ + required this.markdown, + }); + + // the text should be the markdown format + final String markdown; + + @override + State<_AppFlowyEditorMarkdown> createState() => + _AppFlowyEditorMarkdownState(); +} + +class _AppFlowyEditorMarkdownState extends State<_AppFlowyEditorMarkdown> { + late EditorState editorState; + late final styleCustomizer = EditorStyleCustomizer( + context: context, + padding: EdgeInsets.zero, + ); + late final editorStyle = styleCustomizer.style().copyWith( + // hide the cursor + cursorColor: Colors.transparent, + cursorWidth: 0, + ); + late EditorScrollController scrollController; + + @override + void initState() { + super.initState(); + + editorState = _parseMarkdown(widget.markdown); + scrollController = EditorScrollController( + editorState: editorState, + shrinkWrap: true, + ); + } + + @override + void didUpdateWidget(covariant _AppFlowyEditorMarkdown oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.markdown != widget.markdown) { + editorState.dispose(); + editorState = _parseMarkdown(widget.markdown); + scrollController.dispose(); + scrollController = EditorScrollController( + editorState: editorState, + shrinkWrap: true, + ); + } + } + + @override + void dispose() { + scrollController.dispose(); + editorState.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final blockBuilders = getEditorBuilderMap( + context: context, + editorState: editorState, + styleCustomizer: styleCustomizer, + // the editor is not editable in the chat + editable: false, + ); + return IntrinsicHeight( + child: AppFlowyEditor( + shrinkWrap: true, + // the editor is not editable in the chat + editable: false, + editorStyle: editorStyle, + editorScrollController: scrollController, + blockComponentBuilders: blockBuilders, + commandShortcutEvents: [customCopyCommand], + editorState: editorState, + ), + ); + } + + EditorState _parseMarkdown(String markdown) { + final document = markdownToDocument( + markdown, + markdownParsers: [ + const MarkdownCodeBlockParser(), + ], + ); + final editorState = EditorState(document: document); + return editorState; + } +} + +class _ThirdPartyMarkdown extends StatelessWidget { + const _ThirdPartyMarkdown({ + required this.markdown, + }); + + final String markdown; + + @override + Widget build(BuildContext context) { + return MarkdownWidget( + data: markdown, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + config: configFromContext(context), + ); + } + + MarkdownConfig configFromContext(BuildContext context) { + return MarkdownConfig( + configs: [ + HrConfig(color: AFThemeExtension.of(context).textColor), + _ChatH1Config( + style: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 24, + fontWeight: FontWeight.bold, + height: 1.5, + ), + dividerColor: AFThemeExtension.of(context).lightGreyHover, + ), + _ChatH2Config( + style: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 20, + fontWeight: FontWeight.bold, + height: 1.5, + ), + dividerColor: AFThemeExtension.of(context).lightGreyHover, + ), + _ChatH3Config( + style: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 18, + fontWeight: FontWeight.bold, + height: 1.5, + ), + dividerColor: AFThemeExtension.of(context).lightGreyHover, + ), + H4Config( + style: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 16, + fontWeight: FontWeight.bold, + height: 1.5, + ), + ), + H5Config( + style: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 14, + fontWeight: FontWeight.bold, + height: 1.5, + ), + ), + H6Config( + style: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 12, + fontWeight: FontWeight.bold, + height: 1.5, + ), + ), + PreConfig( + builder: (code, language) { + return ConstrainedBox( + constraints: const BoxConstraints( + minWidth: 800, + ), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(6.0)), + child: SelectableHighlightView( + code, + language: language, + theme: getHighlightTheme(context), + padding: const EdgeInsets.all(14), + textStyle: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 14, + fontWeight: FontWeight.bold, + height: 1.5, + ), + ), + ), + ); + }, + ), + PConfig( + textStyle: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.5, + ), + ), + CodeConfig( + style: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.5, + ), + ), + BlockquoteConfig( + sideColor: AFThemeExtension.of(context).lightGreyHover, + textColor: AFThemeExtension.of(context).textColor, + ), + ], + ); + } + + Map getHighlightTheme(BuildContext context) { + return { + 'root': TextStyle( + color: const Color(0xffabb2bf), + backgroundColor: + Theme.of(context).isLightMode ? Colors.white : Colors.black38, + ), + 'comment': const TextStyle( + color: Color(0xff5c6370), + fontStyle: FontStyle.italic, + ), + 'quote': const TextStyle( + color: Color(0xff5c6370), + fontStyle: FontStyle.italic, + ), + 'doctag': const TextStyle(color: Color(0xffc678dd)), + 'keyword': const TextStyle(color: Color(0xffc678dd)), + 'formula': const TextStyle(color: Color(0xffc678dd)), + 'section': const TextStyle(color: Color(0xffe06c75)), + 'name': const TextStyle(color: Color(0xffe06c75)), + 'selector-tag': const TextStyle(color: Color(0xffe06c75)), + 'deletion': const TextStyle(color: Color(0xffe06c75)), + 'subst': const TextStyle(color: Color(0xffe06c75)), + 'literal': const TextStyle(color: Color(0xff56b6c2)), + 'string': const TextStyle(color: Color(0xff98c379)), + 'regexp': const TextStyle(color: Color(0xff98c379)), + 'addition': const TextStyle(color: Color(0xff98c379)), + 'attribute': const TextStyle(color: Color(0xff98c379)), + 'meta-string': const TextStyle(color: Color(0xff98c379)), + 'built_in': const TextStyle(color: Color(0xffe6c07b)), + 'attr': const TextStyle(color: Color(0xffd19a66)), + 'variable': const TextStyle(color: Color(0xffd19a66)), + 'template-variable': const TextStyle(color: Color(0xffd19a66)), + 'type': const TextStyle(color: Color(0xffd19a66)), + 'selector-class': const TextStyle(color: Color(0xffd19a66)), + 'selector-attr': const TextStyle(color: Color(0xffd19a66)), + 'selector-pseudo': const TextStyle(color: Color(0xffd19a66)), + 'number': const TextStyle(color: Color(0xffd19a66)), + 'symbol': const TextStyle(color: Color(0xff61aeee)), + 'bullet': const TextStyle(color: Color(0xff61aeee)), + 'link': const TextStyle(color: Color(0xff61aeee)), + 'meta': const TextStyle(color: Color(0xff61aeee)), + 'selector-id': const TextStyle(color: Color(0xff61aeee)), + 'title': const TextStyle(color: Color(0xff61aeee)), + 'emphasis': const TextStyle(fontStyle: FontStyle.italic), + 'strong': const TextStyle(fontWeight: FontWeight.bold), + }; + } +} + +class _ChatH1Config extends HeadingConfig { + const _ChatH1Config({ + this.style = const TextStyle( + fontSize: 32, + height: 40 / 32, + fontWeight: FontWeight.bold, + ), + required this.dividerColor, + }); + + @override + final TextStyle style; + final Color dividerColor; + + @override + String get tag => MarkdownTag.h1.name; + + @override + HeadingDivider? get divider => HeadingDivider( + space: 10, + color: dividerColor, + height: 10, + ); +} + +///config class for h2 +class _ChatH2Config extends HeadingConfig { + const _ChatH2Config({ + this.style = const TextStyle( + fontSize: 24, + height: 30 / 24, + fontWeight: FontWeight.bold, + ), + required this.dividerColor, + }); + @override + final TextStyle style; + final Color dividerColor; + + @override + String get tag => MarkdownTag.h2.name; + + @override + HeadingDivider? get divider => HeadingDivider( + space: 10, + color: dividerColor, + height: 10, + ); +} + +class _ChatH3Config extends HeadingConfig { + const _ChatH3Config({ + this.style = const TextStyle( + fontSize: 24, + height: 30 / 24, + fontWeight: FontWeight.bold, + ), + required this.dividerColor, + }); + + @override + final TextStyle style; + final Color dividerColor; + + @override + String get tag => MarkdownTag.h3.name; + + @override + HeadingDivider? get divider => HeadingDivider( + space: 10, + color: dividerColor, + height: 10, + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart index 305eba9cd2..ba0bc3db43 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart @@ -2,19 +2,15 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_loading.dart'; -import 'package:appflowy/util/theme_extension.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:fixnum/fixnum.dart'; -import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart'; -import 'package:markdown_widget/markdown_widget.dart'; - -import 'selectable_highlight.dart'; class ChatAITextMessageWidget extends StatelessWidget { const ChatAITextMessageWidget({ @@ -59,248 +55,12 @@ class ChatAITextMessageWidget extends StatelessWidget { if (state.text.isEmpty) { return const ChatAILoading(); } else { - return _textWidgetBuilder(user, context, state.text); + return AIMarkdownText(markdown: state.text); } }, ), ); } - - Widget _textWidgetBuilder( - User user, - BuildContext context, - String text, - ) { - return MarkdownWidget( - data: text, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - config: configFromContext(context), - ); - } - - MarkdownConfig configFromContext(BuildContext context) { - return MarkdownConfig( - configs: [ - HrConfig(color: AFThemeExtension.of(context).textColor), - ChatH1Config( - style: TextStyle( - color: AFThemeExtension.of(context).textColor, - fontSize: 24, - fontWeight: FontWeight.bold, - height: 1.5, - ), - dividerColor: AFThemeExtension.of(context).lightGreyHover, - ), - ChatH2Config( - style: TextStyle( - color: AFThemeExtension.of(context).textColor, - fontSize: 20, - fontWeight: FontWeight.bold, - height: 1.5, - ), - dividerColor: AFThemeExtension.of(context).lightGreyHover, - ), - ChatH3Config( - style: TextStyle( - color: AFThemeExtension.of(context).textColor, - fontSize: 18, - fontWeight: FontWeight.bold, - height: 1.5, - ), - dividerColor: AFThemeExtension.of(context).lightGreyHover, - ), - H4Config( - style: TextStyle( - color: AFThemeExtension.of(context).textColor, - fontSize: 16, - fontWeight: FontWeight.bold, - height: 1.5, - ), - ), - H5Config( - style: TextStyle( - color: AFThemeExtension.of(context).textColor, - fontSize: 14, - fontWeight: FontWeight.bold, - height: 1.5, - ), - ), - H6Config( - style: TextStyle( - color: AFThemeExtension.of(context).textColor, - fontSize: 12, - fontWeight: FontWeight.bold, - height: 1.5, - ), - ), - PreConfig( - builder: (code, language) { - return ConstrainedBox( - constraints: const BoxConstraints( - minWidth: 800, - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(6.0)), - child: SelectableHighlightView( - code, - language: language, - theme: getHightlineTheme(context), - padding: const EdgeInsets.all(14), - textStyle: TextStyle( - color: AFThemeExtension.of(context).textColor, - fontSize: 14, - fontWeight: FontWeight.bold, - height: 1.5, - ), - ), - ), - ); - }, - ), - PConfig( - textStyle: TextStyle( - color: AFThemeExtension.of(context).textColor, - fontSize: 16, - fontWeight: FontWeight.w500, - height: 1.5, - ), - ), - CodeConfig( - style: TextStyle( - color: AFThemeExtension.of(context).textColor, - fontSize: 16, - fontWeight: FontWeight.w500, - height: 1.5, - ), - ), - BlockquoteConfig( - sideColor: AFThemeExtension.of(context).lightGreyHover, - textColor: AFThemeExtension.of(context).textColor, - ), - ], - ); - } -} - -Map getHightlineTheme(BuildContext context) { - return { - 'root': TextStyle( - color: const Color(0xffabb2bf), - backgroundColor: - Theme.of(context).isLightMode ? Colors.white : Colors.black38, - ), - 'comment': - const TextStyle(color: Color(0xff5c6370), fontStyle: FontStyle.italic), - 'quote': - const TextStyle(color: Color(0xff5c6370), fontStyle: FontStyle.italic), - 'doctag': const TextStyle(color: Color(0xffc678dd)), - 'keyword': const TextStyle(color: Color(0xffc678dd)), - 'formula': const TextStyle(color: Color(0xffc678dd)), - 'section': const TextStyle(color: Color(0xffe06c75)), - 'name': const TextStyle(color: Color(0xffe06c75)), - 'selector-tag': const TextStyle(color: Color(0xffe06c75)), - 'deletion': const TextStyle(color: Color(0xffe06c75)), - 'subst': const TextStyle(color: Color(0xffe06c75)), - 'literal': const TextStyle(color: Color(0xff56b6c2)), - 'string': const TextStyle(color: Color(0xff98c379)), - 'regexp': const TextStyle(color: Color(0xff98c379)), - 'addition': const TextStyle(color: Color(0xff98c379)), - 'attribute': const TextStyle(color: Color(0xff98c379)), - 'meta-string': const TextStyle(color: Color(0xff98c379)), - 'built_in': const TextStyle(color: Color(0xffe6c07b)), - 'attr': const TextStyle(color: Color(0xffd19a66)), - 'variable': const TextStyle(color: Color(0xffd19a66)), - 'template-variable': const TextStyle(color: Color(0xffd19a66)), - 'type': const TextStyle(color: Color(0xffd19a66)), - 'selector-class': const TextStyle(color: Color(0xffd19a66)), - 'selector-attr': const TextStyle(color: Color(0xffd19a66)), - 'selector-pseudo': const TextStyle(color: Color(0xffd19a66)), - 'number': const TextStyle(color: Color(0xffd19a66)), - 'symbol': const TextStyle(color: Color(0xff61aeee)), - 'bullet': const TextStyle(color: Color(0xff61aeee)), - 'link': const TextStyle(color: Color(0xff61aeee)), - 'meta': const TextStyle(color: Color(0xff61aeee)), - 'selector-id': const TextStyle(color: Color(0xff61aeee)), - 'title': const TextStyle(color: Color(0xff61aeee)), - 'emphasis': const TextStyle(fontStyle: FontStyle.italic), - 'strong': const TextStyle(fontWeight: FontWeight.bold), - }; -} - -class ChatH1Config extends HeadingConfig { - const ChatH1Config({ - this.style = const TextStyle( - fontSize: 32, - height: 40 / 32, - fontWeight: FontWeight.bold, - ), - required this.dividerColor, - }); - - @override - final TextStyle style; - final Color dividerColor; - - @override - String get tag => MarkdownTag.h1.name; - - @override - HeadingDivider? get divider => HeadingDivider( - space: 10, - color: dividerColor, - height: 10, - ); -} - -///config class for h2 -class ChatH2Config extends HeadingConfig { - const ChatH2Config({ - this.style = const TextStyle( - fontSize: 24, - height: 30 / 24, - fontWeight: FontWeight.bold, - ), - required this.dividerColor, - }); - @override - final TextStyle style; - final Color dividerColor; - - @override - String get tag => MarkdownTag.h2.name; - - @override - HeadingDivider? get divider => HeadingDivider( - space: 10, - color: dividerColor, - height: 10, - ); -} - -class ChatH3Config extends HeadingConfig { - const ChatH3Config({ - this.style = const TextStyle( - fontSize: 24, - height: 30 / 24, - fontWeight: FontWeight.bold, - ), - required this.dividerColor, - }); - - @override - final TextStyle style; - final Color dividerColor; - - @override - String get tag => MarkdownTag.h3.name; - - @override - HeadingDivider? get divider => HeadingDivider( - space: 10, - color: dividerColor, - height: 10, - ); } class StreamingError extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart index 3709cdd413..0c507ace16 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart @@ -52,7 +52,6 @@ class TextMessageText extends StatelessWidget { text, fontSize: 16, fontWeight: FontWeight.w500, - lineHeight: 1.5, maxLines: null, selectable: true, color: AFThemeExtension.of(context).textColor, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_code_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_code_parser.dart new file mode 100644 index 0000000000..4033db3142 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_code_parser.dart @@ -0,0 +1,52 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; +import 'package:markdown/markdown.dart' as md; + +class MarkdownCodeBlockParser extends CustomMarkdownParser { + const MarkdownCodeBlockParser(); + + @override + List transform( + md.Node element, + List parsers, { + MarkdownListType listType = MarkdownListType.unknown, + int? startNumber, + }) { + if (element is! md.Element) { + return []; + } + + if (element.tag != 'pre') { + return []; + } + + final ec = element.children; + if (ec == null || ec.isEmpty) { + return []; + } + + final code = ec.first; + if (code is! md.Element || code.tag != 'code') { + return []; + } + + String? language; + if (code.attributes.containsKey('class')) { + final classes = code.attributes['class']!.split(' '); + final languageClass = classes.firstWhere( + (c) => c.startsWith('language-'), + orElse: () => '', + ); + language = languageClass.substring('language-'.length); + } + + final deltaDecoder = DeltaMarkdownDecoder(); + + return [ + codeBlockNode( + language: language, + delta: deltaDecoder.convertNodes(code.children), + ), + ]; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_parsers.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_parsers.dart index 3e4fc3a764..e57ce61df6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_parsers.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_parsers.dart @@ -1,3 +1,4 @@ export 'callout_node_parser.dart'; +export 'markdown_code_parser.dart'; export 'math_equation_node_parser.dart'; export 'toggle_list_node_parser.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/import/import_panel.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/import/import_panel.dart index a888a071e3..5ab9d56cdc 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/import/import_panel.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/import/import_panel.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/markdown_code_parser.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/settings/share/import_service.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/import/import_type.dart'; @@ -220,7 +221,12 @@ class _ImportPanelState extends State { Uint8List? _documentDataFrom(ImportType importType, String data) { switch (importType) { case ImportType.markdownOrText: - final document = markdownToDocument(data); + final document = markdownToDocument( + data, + markdownParsers: [ + const MarkdownCodeBlockParser(), + ], + ); return DocumentDataPBFromTo.fromDocument(document)?.writeToBuffer(); case ImportType.historyDocument: final document = EditorMigration.migrateDocument(data); diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index d223227470..5489eaf67b 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -53,11 +53,11 @@ packages: dependency: "direct main" description: path: "." - ref: e8ee051 - resolved-ref: e8ee051719eded6621ccdc2722f696411c020209 + ref: d2d9873 + resolved-ref: d2d987312d3a667336c7e12c36da7dbbb62d66db url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git - version: "3.0.0" + version: "3.1.0" appflowy_editor_plugins: dependency: "direct main" description: @@ -121,6 +121,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + barcode: + dependency: transitive + description: + name: barcode + sha256: ab180ce22c6555d77d45f0178a523669db67f95856e3378259ef2ffeb43e6003 + url: "https://pub.dev" + source: hosted + version: "2.2.8" + bidi: + dependency: transitive + description: + name: bidi + sha256: "1a7d0c696324b2089f72e7671fd1f1f64fef44c980f3cebc84e803967c597b63" + url: "https://pub.dev" + source: hosted + version: "2.0.10" bitsdojo_window: dependency: "direct main" description: @@ -965,6 +981,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + image: + dependency: transitive + description: + name: image + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + url: "https://pub.dev" + source: hosted + version: "4.2.0" image_gallery_saver: dependency: "direct main" description: @@ -1219,7 +1243,7 @@ packages: source: hosted version: "1.2.0" markdown: - dependency: transitive + dependency: "direct main" description: name: markdown sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 @@ -1434,6 +1458,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0+3" + pdf: + dependency: transitive + description: + name: pdf + sha256: "81d5522bddc1ef5c28e8f0ee40b71708761753c163e0c93a40df56fd515ea0f0" + url: "https://pub.dev" + source: hosted + version: "3.11.0" + pdf_widget_wrapper: + dependency: transitive + description: + name: pdf_widget_wrapper + sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5 + url: "https://pub.dev" + source: hosted + version: "1.0.4" percent_indicator: dependency: "direct main" description: @@ -1554,6 +1594,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + printing: + dependency: transitive + description: + name: printing + sha256: cc4b256a5a89d5345488e3318897b595867f5181b8c5ed6fc63bfa5f2044aec3 + url: "https://pub.dev" + source: hosted + version: "5.13.1" process: dependency: transitive description: @@ -1594,6 +1642,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.3" + qr: + dependency: transitive + description: + name: qr + sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" realtime_client: dependency: transitive description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 8d2b63996a..01921f368b 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -141,6 +141,7 @@ dependencies: shimmer: ^3.0.0 isolates: ^3.0.3+8 markdown_widget: ^2.3.2+6 + markdown: # Window Manager for MacOS and Linux window_manager: ^0.3.9 @@ -187,7 +188,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "e8ee051" + ref: "d2d9873" appflowy_editor_plugins: git: