From 5fcda94b59502d808d40ce353c631aa0b2d6873d Mon Sep 17 00:00:00 2001 From: Ellet Date: Thu, 7 Dec 2023 20:35:52 +0300 Subject: [PATCH] 5 --- CHANGELOG.md | 2 + README.md | 8 +- doc/todo.md | 11 +- .../lib/presentation/quill/quill_screen.dart | 31 +- example/pubspec.yaml | 1 + flutter_quill_extensions/CHANGELOG.md | 2 + .../image/editor/image_embed_types.dart | 2 +- .../lib/embeds/video/video.dart | 2 +- .../{controller.dart => controller_ext.dart} | 0 .../lib/flutter_quill_extensions.dart | 2 +- flutter_quill_test/CHANGELOG.md | 2 + lib/flutter_quill.dart | 4 +- lib/markdown_quill.dart | 5 + .../config/editor/editor_configurations.dart | 4 +- .../raw_editor/raw_editor_configurations.dart | 2 +- .../toolbar/buttons/color_configurations.dart | 2 +- .../buttons/font_size_configurations.dart | 2 +- .../simple_toolbar_configurations.dart | 4 +- lib/src/models/documents/document.dart | 3 +- lib/src/models/documents/nodes/container.dart | 2 +- lib/src/models/documents/nodes/leaf.dart | 2 +- lib/src/models/documents/nodes/line.dart | 2 +- lib/src/models/documents/nodes/node.dart | 2 +- .../custom_quill_attributes.dart | 11 + .../quill_markdown/delta_to_markdown.dart | 359 +++++++++++++++ .../embeddable_table_syntax.dart | 116 +++++ .../quill_markdown/markdown_to_delta.dart | 424 ++++++++++++++++++ lib/src/packages/quill_markdown/utils.dart | 59 +++ lib/src/utils/embeds.dart | 2 +- lib/src/widgets/editor/editor.dart | 2 +- lib/src/widgets/others/delegate.dart | 2 +- lib/src/widgets/{others => quill}/embeds.dart | 2 +- .../quill_controller.dart} | 46 +- .../widgets/{others => quill}/text_block.dart | 14 +- .../widgets/{others => quill}/text_line.dart | 22 +- .../widgets/raw_editor/raw_editor_state.dart | 66 ++- .../widgets/style_widgets/number_point.dart | 2 +- .../toolbar/buttons/clear_format_button.dart | 2 +- .../toolbar/buttons/color/color_button.dart | 2 +- .../toolbar/buttons/custom_button_button.dart | 2 +- .../toolbar/buttons/font_family_button.dart | 2 +- .../toolbar/buttons/font_size_button.dart | 2 +- .../toolbar/buttons/history_button.dart | 2 +- .../toolbar/buttons/indent_button.dart | 2 +- .../toolbar/buttons/link_style2_button.dart | 2 +- .../toolbar/buttons/link_style_button.dart | 2 +- .../toolbar/buttons/search/search_button.dart | 2 +- .../toolbar/buttons/search/search_dialog.dart | 2 +- .../buttons/select_alignment_buttons.dart | 2 +- .../buttons/select_header_style_button.dart | 2 +- .../buttons/select_header_style_buttons.dart | 2 +- .../buttons/toggle_check_list_button.dart | 2 +- .../toolbar/buttons/toggle_style_button.dart | 2 +- packages/quill_html_converter/CHANGELOG.md | 2 + .../lib/quill_html_converter.dart | 32 -- packages/quill_html_converter/pubspec.yaml | 7 +- pubspec.yaml | 5 + 57 files changed, 1185 insertions(+), 117 deletions(-) rename flutter_quill_extensions/lib/extensions/{controller.dart => controller_ext.dart} (100%) create mode 100644 lib/markdown_quill.dart create mode 100644 lib/src/packages/quill_markdown/custom_quill_attributes.dart create mode 100644 lib/src/packages/quill_markdown/delta_to_markdown.dart create mode 100644 lib/src/packages/quill_markdown/embeddable_table_syntax.dart create mode 100644 lib/src/packages/quill_markdown/markdown_to_delta.dart create mode 100644 lib/src/packages/quill_markdown/utils.dart rename lib/src/widgets/{others => quill}/embeds.dart (96%) rename lib/src/widgets/{others/controller.dart => quill/quill_controller.dart} (90%) rename lib/src/widgets/{others => quill}/text_block.dart (98%) rename lib/src/widgets/{others => quill}/text_line.dart (99%) diff --git a/CHANGELOG.md b/CHANGELOG.md index b52dd226b..52b9aadd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. ## 9.0.0-dev-7 * Fix a bug in chaning the background/font color of ol/ul list +* Better support for pasting HTML contents from external websites to the editor +* The experimental support of converting the HTML from `quill_html_converter` is now built-in in the `flutter_quill` and removed from there (Breaking change for `quill_html_converter`) * Flutter Quill Extensions: * Fix link bug in the video url * Fix patterns diff --git a/README.md b/README.md index 058f5c8b2..750f7a74f 100644 --- a/README.md +++ b/README.md @@ -238,11 +238,13 @@ To see how to use the extension package, please take a look at the [README](./fl Having your document stored in Quill Delta format is sometimes not enough. Often you'll need to convert it to other formats such as HTML to publish it, or send an email. +**Note**: This package support converting from HTML back to Quill delta but it's experimental and used internally when pasting Html content from the cliboard to the Quill Editor + You have two options: -1. Using [quill_html_converter](./packages/quill_html_converter/) to convert to/from HTML, the package can convert the Quill delta to HTML well -(it uses [vsc_quill_delta_to_html](https://pub.dev/packages/vsc_quill_delta_to_html)) but the converting from HTML back to Quill delta is experimental -2. Another option is to use +1. Using [quill_html_converter](./packages/quill_html_converter/) to convert to HTML, the package can convert the Quill delta to HTML well +(it uses [vsc_quill_delta_to_html](https://pub.dev/packages/vsc_quill_delta_to_html)), it just a handy extension to do it more quickly +1. Another option is to use [vsc_quill_delta_to_html](https://pub.dev/packages/vsc_quill_delta_to_html) to convert your document to HTML. This package has full support for all Quill operations—including images, videos, formulas, diff --git a/doc/todo.md b/doc/todo.md index 4f38ed016..813a90669 100644 --- a/doc/todo.md +++ b/doc/todo.md @@ -29,6 +29,10 @@ This is a todo list page that added recently and will be updated soon. - Extract the shared properties between `QuillRawEditorConfigurations` and `QuillEditorConfigurations` - The todo in the this [commit](https://github.com/singerdmx/flutter-quill/commit/79597ea6425357795c0663588ac079665241f23a) needs to be checked - use `maybeOf` and of instead `ofNotNull` in the providers to follow flutter offical convenstion, completly rework the providers and update the build context extensions + - Add line through to the text when the check point checked is true + - Change the color of the numbers and dots in ol/ul to match the ones in the item list + - Fix the bugs of the font family and font size + - Try to update Quill Html Converter ### Bugs @@ -39,12 +43,7 @@ Please go to the [issues](https://github.com/singerdmx/flutter-quill/issues) ## Flutter Quill Extensions ### Features -- Add support for copying images to the Clipboard ### Improvemenets -Please check the todos, this list will be updated soon. - -### Bugs - -Please check the todos, this list will be updated soon. \ No newline at end of file +### Bugs \ No newline at end of file diff --git a/example/lib/presentation/quill/quill_screen.dart b/example/lib/presentation/quill/quill_screen.dart index 712a334ab..51908827e 100644 --- a/example/lib/presentation/quill/quill_screen.dart +++ b/example/lib/presentation/quill/quill_screen.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill_extensions/flutter_quill_extensions.dart' show FlutterQuillEmbeds, QuillSharedExtensionsConfigurations; - import 'package:quill_html_converter/quill_html_converter.dart'; import 'package:share_plus/share_plus.dart' show Share; @@ -46,6 +45,18 @@ class _QuillScreenState extends State { _controller.document = widget.args.document; } + // Future _init() async { + // final reader = await ClipboardReader.readClipboard(); + // if (reader.canProvide(Formats.htmlText)) { + // final html = await reader.readValue(Formats.htmlText); + // if (html == null) { + // return; + // } + // final delta = DeltaHtmlExt.fromHtml(html); + // _controller.document = Document.fromDelta(delta); + // } + // } + @override void dispose() { _controller.dispose(); @@ -65,7 +76,7 @@ class _QuillScreenState extends State { onPressed: () { final html = _controller.document.toDelta().toHtml(); _controller.document = - Document.fromDelta(DeltaHtmlExt.fromHtml(html)); + Document.fromDelta(QuillController.fromHtml(html)); }, icon: const Icon(Icons.html), ), @@ -119,14 +130,14 @@ class _QuillScreenState extends State { codeBlock: QuillEditorCodeBlockElementOptions( enableLineNumbers: true, ), - orderedList: QuillEditorOrderedListElementOptions( - backgroundColor: Colors.amber, - fontColor: Colors.black, - ), - unorderedList: QuillEditorUnOrderedListElementOptions( - backgroundColor: Colors.green, - fontColor: Colors.red, - ), + // orderedList: QuillEditorOrderedListElementOptions( + // backgroundColor: Colors.amber, + // fontColor: Colors.black, + // ), + // unorderedList: QuillEditorUnOrderedListElementOptions( + // backgroundColor: Colors.green, + // fontColor: Colors.red, + // ), ), ), scrollController: _editorScrollController, diff --git a/example/pubspec.yaml b/example/pubspec.yaml index bf3192add..b3d2e15c1 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -44,6 +44,7 @@ dependencies: file_picker: ^6.1.1 # For sharing text share_plus: ^7.2.1 + super_clipboard: ^0.7.3 dependency_overrides: flutter_quill: diff --git a/flutter_quill_extensions/CHANGELOG.md b/flutter_quill_extensions/CHANGELOG.md index b52dd226b..52b9aadd2 100644 --- a/flutter_quill_extensions/CHANGELOG.md +++ b/flutter_quill_extensions/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. ## 9.0.0-dev-7 * Fix a bug in chaning the background/font color of ol/ul list +* Better support for pasting HTML contents from external websites to the editor +* The experimental support of converting the HTML from `quill_html_converter` is now built-in in the `flutter_quill` and removed from there (Breaking change for `quill_html_converter`) * Flutter Quill Extensions: * Fix link bug in the video url * Fix patterns diff --git a/flutter_quill_extensions/lib/embeds/image/editor/image_embed_types.dart b/flutter_quill_extensions/lib/embeds/image/editor/image_embed_types.dart index c25036755..e2036da14 100644 --- a/flutter_quill_extensions/lib/embeds/image/editor/image_embed_types.dart +++ b/flutter_quill_extensions/lib/embeds/image/editor/image_embed_types.dart @@ -4,7 +4,7 @@ import 'package:flutter/widgets.dart' show BuildContext; import 'package:flutter_quill/flutter_quill.dart'; import 'package:meta/meta.dart' show immutable; -import '../../../extensions/controller.dart'; +import '../../../extensions/controller_ext.dart'; import '../../../services/image_picker/s_image_picker.dart'; /// When request picking an image, for example when the image button toolbar diff --git a/flutter_quill_extensions/lib/embeds/video/video.dart b/flutter_quill_extensions/lib/embeds/video/video.dart index b2e1ca24e..2acd4246c 100644 --- a/flutter_quill_extensions/lib/embeds/video/video.dart +++ b/flutter_quill_extensions/lib/embeds/video/video.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart' show BuildContext; import 'package:flutter_quill/flutter_quill.dart'; import 'package:meta/meta.dart' show immutable; -import '../../extensions/controller.dart'; +import '../../extensions/controller_ext.dart'; import '../../services/image_picker/s_image_picker.dart'; /// When request picking an video, for example when the video button toolbar diff --git a/flutter_quill_extensions/lib/extensions/controller.dart b/flutter_quill_extensions/lib/extensions/controller_ext.dart similarity index 100% rename from flutter_quill_extensions/lib/extensions/controller.dart rename to flutter_quill_extensions/lib/extensions/controller_ext.dart diff --git a/flutter_quill_extensions/lib/flutter_quill_extensions.dart b/flutter_quill_extensions/lib/flutter_quill_extensions.dart index f63285547..d7492043c 100644 --- a/flutter_quill_extensions/lib/flutter_quill_extensions.dart +++ b/flutter_quill_extensions/lib/flutter_quill_extensions.dart @@ -36,7 +36,7 @@ export 'embeds/video/editor/video_embed.dart'; export 'embeds/video/editor/video_web_embed.dart'; export 'embeds/video/toolbar/video_button.dart'; export 'embeds/video/video.dart'; -export 'extensions/controller.dart'; +export 'extensions/controller_ext.dart'; export 'models/config/editor/image/image.dart'; export 'models/config/editor/image/image_web.dart'; export 'models/config/editor/video/video.dart'; diff --git a/flutter_quill_test/CHANGELOG.md b/flutter_quill_test/CHANGELOG.md index b52dd226b..52b9aadd2 100644 --- a/flutter_quill_test/CHANGELOG.md +++ b/flutter_quill_test/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. ## 9.0.0-dev-7 * Fix a bug in chaning the background/font color of ol/ul list +* Better support for pasting HTML contents from external websites to the editor +* The experimental support of converting the HTML from `quill_html_converter` is now built-in in the `flutter_quill` and removed from there (Breaking change for `quill_html_converter`) * Flutter Quill Extensions: * Fix link bug in the video url * Fix patterns diff --git a/lib/flutter_quill.dart b/lib/flutter_quill.dart index 54f4eaec2..15e58183e 100644 --- a/lib/flutter_quill.dart +++ b/lib/flutter_quill.dart @@ -23,10 +23,10 @@ export 'src/models/themes/quill_dialog_theme.dart'; export 'src/models/themes/quill_icon_theme.dart'; export 'src/utils/embeds.dart'; export 'src/widgets/editor/editor.dart'; -export 'src/widgets/others/controller.dart'; +export 'src/widgets/quill/quill_controller.dart'; export 'src/widgets/others/cursor.dart'; export 'src/widgets/others/default_styles.dart'; -export 'src/widgets/others/embeds.dart'; +export 'src/widgets/quill/embeds.dart'; export 'src/widgets/others/link.dart' show LinkActionPickerDelegate, LinkMenuAction; export 'src/widgets/raw_editor/raw_editor.dart'; diff --git a/lib/markdown_quill.dart b/lib/markdown_quill.dart new file mode 100644 index 000000000..cca0aa1c9 --- /dev/null +++ b/lib/markdown_quill.dart @@ -0,0 +1,5 @@ +library quill_markdown; + +export 'src/packages/quill_markdown/delta_to_markdown.dart'; +export 'src/packages/quill_markdown/embeddable_table_syntax.dart'; +export 'src/packages/quill_markdown/markdown_to_delta.dart'; diff --git a/lib/src/models/config/editor/editor_configurations.dart b/lib/src/models/config/editor/editor_configurations.dart index b96886c31..029f5a48d 100644 --- a/lib/src/models/config/editor/editor_configurations.dart +++ b/lib/src/models/config/editor/editor_configurations.dart @@ -7,10 +7,10 @@ import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart' show experimental; import '../../../widgets/editor/editor_builder.dart'; -import '../../../widgets/others/controller.dart'; +import '../../../widgets/quill/quill_controller.dart'; import '../../../widgets/others/default_styles.dart'; import '../../../widgets/others/delegate.dart'; -import '../../../widgets/others/embeds.dart'; +import '../../../widgets/quill/embeds.dart'; import '../../../widgets/others/link.dart'; import '../../../widgets/raw_editor/raw_editor.dart'; import '../../themes/quill_dialog_theme.dart'; diff --git a/lib/src/models/config/raw_editor/raw_editor_configurations.dart b/lib/src/models/config/raw_editor/raw_editor_configurations.dart index 1ddb9973b..03744e9e4 100644 --- a/lib/src/models/config/raw_editor/raw_editor_configurations.dart +++ b/lib/src/models/config/raw_editor/raw_editor_configurations.dart @@ -25,7 +25,7 @@ import 'package:flutter/widgets.dart' Widget; import 'package:meta/meta.dart' show immutable; -import '../../../widgets/others/controller.dart'; +import '../../../widgets/quill/quill_controller.dart'; import '../../../widgets/others/cursor.dart'; import '../../../widgets/others/default_styles.dart'; import '../../../widgets/others/delegate.dart'; diff --git a/lib/src/models/config/toolbar/buttons/color_configurations.dart b/lib/src/models/config/toolbar/buttons/color_configurations.dart index 04e45451c..b3c08c251 100644 --- a/lib/src/models/config/toolbar/buttons/color_configurations.dart +++ b/lib/src/models/config/toolbar/buttons/color_configurations.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart' show Color; -import '../../../../widgets/others/controller.dart'; +import '../../../../widgets/quill/quill_controller.dart'; import '../../quill_shared_configurations.dart' show QuillSharedConfigurations; import 'base_configurations.dart'; diff --git a/lib/src/models/config/toolbar/buttons/font_size_configurations.dart b/lib/src/models/config/toolbar/buttons/font_size_configurations.dart index 5cebf9f6d..ec3c66825 100644 --- a/lib/src/models/config/toolbar/buttons/font_size_configurations.dart +++ b/lib/src/models/config/toolbar/buttons/font_size_configurations.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart' import 'package:flutter/widgets.dart' show Color, EdgeInsets, EdgeInsetsGeometry, TextOverflow, TextStyle; -import '../../../../widgets/others/controller.dart'; +import '../../../../widgets/quill/quill_controller.dart'; import '../../../documents/attribute.dart'; import '../../../themes/quill_icon_theme.dart'; import '../../quill_configurations.dart'; diff --git a/lib/src/models/config/toolbar/simple_toolbar_configurations.dart b/lib/src/models/config/toolbar/simple_toolbar_configurations.dart index 1ed627e95..300547360 100644 --- a/lib/src/models/config/toolbar/simple_toolbar_configurations.dart +++ b/lib/src/models/config/toolbar/simple_toolbar_configurations.dart @@ -4,8 +4,8 @@ import 'package:flutter/foundation.dart' show immutable; import 'package:flutter/widgets.dart' show Axis, Widget, WrapAlignment, WrapCrossAlignment; -import '../../../widgets/others/controller.dart'; -import '../../../widgets/others/embeds.dart'; +import '../../../widgets/quill/quill_controller.dart'; +import '../../../widgets/quill/embeds.dart'; import '../../themes/quill_dialog_theme.dart'; import '../../themes/quill_icon_theme.dart'; import 'buttons/base_configurations.dart'; diff --git a/lib/src/models/documents/document.dart b/lib/src/models/documents/document.dart index 1b4f96969..e0efbdf8e 100644 --- a/lib/src/models/documents/document.dart +++ b/lib/src/models/documents/document.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import '../../widgets/others/embeds.dart'; +import '../../widgets/quill/embeds.dart'; import '../quill_delta.dart'; import '../rules/rule.dart'; import '../structs/doc_change.dart'; @@ -402,6 +402,7 @@ class Document { throw ArgumentError.value(doc, 'Document Delta cannot be empty.'); } + // print(doc.last.data.runtimeType); assert((doc.last.data as String).endsWith('\n')); var offset = 0; diff --git a/lib/src/models/documents/nodes/container.dart b/lib/src/models/documents/nodes/container.dart index 5b5e0941b..f5f1900d3 100644 --- a/lib/src/models/documents/nodes/container.dart +++ b/lib/src/models/documents/nodes/container.dart @@ -1,6 +1,6 @@ import 'dart:collection'; -import '../../../widgets/others/embeds.dart'; +import '../../../widgets/quill/embeds.dart'; import '../style.dart'; import 'leaf.dart'; import 'line.dart'; diff --git a/lib/src/models/documents/nodes/leaf.dart b/lib/src/models/documents/nodes/leaf.dart index 314bb4eed..115f7058b 100644 --- a/lib/src/models/documents/nodes/leaf.dart +++ b/lib/src/models/documents/nodes/leaf.dart @@ -1,6 +1,6 @@ import 'dart:math' as math; -import '../../../widgets/others/embeds.dart'; +import '../../../widgets/quill/embeds.dart'; import '../../quill_delta.dart'; import '../style.dart'; import 'embeddable.dart'; diff --git a/lib/src/models/documents/nodes/line.dart b/lib/src/models/documents/nodes/line.dart index bc2920cb1..d202ba08a 100644 --- a/lib/src/models/documents/nodes/line.dart +++ b/lib/src/models/documents/nodes/line.dart @@ -2,7 +2,7 @@ import 'dart:math' as math; import 'package:collection/collection.dart'; -import '../../../widgets/others/embeds.dart'; +import '../../../widgets/quill/embeds.dart'; import '../../quill_delta.dart'; import '../../structs/offset_value.dart'; import '../attribute.dart'; diff --git a/lib/src/models/documents/nodes/node.dart b/lib/src/models/documents/nodes/node.dart index 4b9bcd7d9..3b78603c6 100644 --- a/lib/src/models/documents/nodes/node.dart +++ b/lib/src/models/documents/nodes/node.dart @@ -1,6 +1,6 @@ import 'dart:collection'; -import '../../../widgets/others/embeds.dart'; +import '../../../widgets/quill/embeds.dart'; import '../../quill_delta.dart'; import '../attribute.dart'; import '../style.dart'; diff --git a/lib/src/packages/quill_markdown/custom_quill_attributes.dart b/lib/src/packages/quill_markdown/custom_quill_attributes.dart new file mode 100644 index 000000000..bb62ddbd4 --- /dev/null +++ b/lib/src/packages/quill_markdown/custom_quill_attributes.dart @@ -0,0 +1,11 @@ +import 'package:flutter_quill/flutter_quill.dart'; + +/// Custom attribute to save the language of codeblock +class CodeBlockLanguageAttribute extends Attribute { + /// @nodoc + const CodeBlockLanguageAttribute(String? value) + : super(attrKey, AttributeScope.ignore, value); + + /// attribute key + static const attrKey = 'x-md-codeblock-lang'; +} diff --git a/lib/src/packages/quill_markdown/delta_to_markdown.dart b/lib/src/packages/quill_markdown/delta_to_markdown.dart new file mode 100644 index 000000000..cfeb97f62 --- /dev/null +++ b/lib/src/packages/quill_markdown/delta_to_markdown.dart @@ -0,0 +1,359 @@ +import 'dart:convert'; +import 'dart:ui'; + +import 'package:collection/collection.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import './custom_quill_attributes.dart'; +import './utils.dart'; + +class _AttributeHandler { + _AttributeHandler({ + this.beforeContent, + this.afterContent, + }); + + final void Function( + Attribute attribute, + Node node, + StringSink output, + )? beforeContent; + + final void Function( + Attribute attribute, + Node node, + StringSink output, + )? afterContent; +} + +/// Outputs [Embed] element as markdown. +typedef EmbedToMarkdown = void Function(Embed embed, StringSink out); + +extension on Object? { + T? asNullable() { + final self = this; + return self == null ? null : self as T; + } +} + +/// Convertor from [Delta] to quill Markdown string. +class DeltaToMarkdown extends Converter + implements _NodeVisitor { + /// + DeltaToMarkdown({ + Map? customEmbedHandlers, + }) { + if (customEmbedHandlers != null) { + _embedHandlers.addAll(customEmbedHandlers); + } + } + + @override + String convert(Delta input) { + final newDelta = transform(input); + + final quillDocument = Document.fromDelta(newDelta); + + final outBuffer = quillDocument.root.accept(this); + + return outBuffer.toString(); + } + + final Map _blockAttrsHandlers = { + Attribute.codeBlock.key: _AttributeHandler( + beforeContent: (attribute, node, output) { + var infoString = ''; + if (node.containsAttr(CodeBlockLanguageAttribute.attrKey)) { + infoString = node.getAttrValueOr( + CodeBlockLanguageAttribute.attrKey, + '', + ); + } + if (infoString.isEmpty) { + final linesWithLang = (node as Block).children.where((child) => + child.containsAttr(CodeBlockLanguageAttribute.attrKey)); + if (linesWithLang.isNotEmpty) { + infoString = linesWithLang.first.getAttrValueOr( + CodeBlockLanguageAttribute.attrKey, + 'or', + ); + } + } + + output.writeln('```$infoString'); + }, + afterContent: (attribute, node, output) => output.writeln('```'), + ), + }; + + final Map _lineAttrsHandlers = { + Attribute.header.key: _AttributeHandler( + beforeContent: (attribute, node, output) { + output + ..write('#' * (attribute.value.asNullable() ?? 1)) + ..write(' '); + }, + ), + Attribute.blockQuote.key: _AttributeHandler( + beforeContent: (attribute, node, output) => output.write('> '), + ), + Attribute.list.key: _AttributeHandler( + beforeContent: (attribute, node, output) { + final indentLevel = node.getAttrValueOr(Attribute.indent.key, 0); + final isNumbered = attribute.value == 'ordered'; + output + ..write((isNumbered ? ' ' : ' ') * indentLevel) + ..write('${isNumbered ? '1.' : '-'} '); + }, + ), + }; + + final Map _textAttrsHandlers = { + Attribute.italic.key: _AttributeHandler( + beforeContent: (attribute, node, output) { + if (node.previous?.containsAttr(attribute.key) != true) { + output.write('_'); + } + }, + afterContent: (attribute, node, output) { + if (node.next?.containsAttr(attribute.key) != true) { + output.write('_'); + } + }, + ), + Attribute.bold.key: _AttributeHandler( + beforeContent: (attribute, node, output) { + if (node.previous?.containsAttr(attribute.key) != true) { + output.write('**'); + } + }, + afterContent: (attribute, node, output) { + if (node.next?.containsAttr(attribute.key) != true) { + output.write('**'); + } + }, + ), + Attribute.strikeThrough.key: _AttributeHandler( + beforeContent: (attribute, node, output) { + if (node.previous?.containsAttr(attribute.key) != true) { + output.write('~~'); + } + }, + afterContent: (attribute, node, output) { + if (node.next?.containsAttr(attribute.key) != true) { + output.write('~~'); + } + }, + ), + Attribute.inlineCode.key: _AttributeHandler( + beforeContent: (attribute, node, output) { + if (node.previous?.containsAttr(attribute.key) != true) { + output.write('`'); + } + }, + afterContent: (attribute, node, output) { + if (node.next?.containsAttr(attribute.key) != true) { + output.write('`'); + } + }, + ), + Attribute.link.key: _AttributeHandler( + beforeContent: (attribute, node, output) { + if (node.previous?.containsAttr(attribute.key, attribute.value) != + true) { + output.write('['); + } + }, + afterContent: (attribute, node, output) { + if (node.next?.containsAttr(attribute.key, attribute.value) != true) { + output.write('](${attribute.value.asNullable() ?? ''})'); + } + }, + ), + }; + + final Map _embedHandlers = { + BlockEmbed.imageType: (embed, out) => out.write('![](${embed.value.data})'), + horizontalRuleType: (embed, out) { + // adds new line after it + // make --- separated so it doesn't get rendered as header + out.writeln('- - -'); + }, + }; + + @override + StringSink visitRoot(Root root, [StringSink? output]) { + final out = output ??= StringBuffer(); + for (final container in root.children) { + container.accept(this, out); + } + return out; + } + + @override + StringSink visitBlock(Block block, [StringSink? output]) { + final out = output ??= StringBuffer(); + _handleAttribute(_blockAttrsHandlers, block, output, () { + for (final line in block.children) { + line.accept(this, out); + } + }); + return out; + } + + @override + StringSink visitLine(Line line, [StringSink? output]) { + final out = output ??= StringBuffer(); + final style = line.style; + _handleAttribute(_lineAttrsHandlers, line, output, () { + for (final leaf in line.children) { + leaf.accept(this, out); + } + }); + if (style.isEmpty || + style.values.every((item) => item.scope != AttributeScope.block)) { + out.writeln(); + } + if (style.containsKey(Attribute.list.key) && + line.nextLine?.style.containsKey(Attribute.list.key) != true) { + out.writeln(); + } + out.writeln(); + return out; + } + + @override + StringSink visitText(QuillText text, [StringSink? output]) { + final out = output ??= StringBuffer(); + final style = text.style; + _handleAttribute( + _textAttrsHandlers, + text, + output, + () { + var content = text.value; + if (!(style.containsKey(Attribute.codeBlock.key) || + style.containsKey(Attribute.inlineCode.key) || + (text.parent?.style.containsKey(Attribute.codeBlock.key) ?? + false))) { + content = content.replaceAllMapped( + RegExp(r'[\\\`\*\_\{\}\[\]\(\)\#\+\-\.\!\>\<]'), (match) { + return '\\${match[0]}'; + }); + } + out.write(content); + }, + sortedAttrsBySpan: true, + ); + return out; + } + + @override + StringSink visitEmbed(Embed embed, [StringSink? output]) { + final out = output ??= StringBuffer(); + + final type = embed.value.type; + + _embedHandlers[type]!.call(embed, out); + + return out; + } + + void _handleAttribute( + Map handlers, + Node node, + StringSink output, + VoidCallback contentHandler, { + bool sortedAttrsBySpan = false, + }) { + final attrs = sortedAttrsBySpan + ? node.attrsSortedByLongestSpan() + : node.style.attributes.values.toList(); + final handlersToUse = attrs + .where((attr) => handlers.containsKey(attr.key)) + .map((attr) => MapEntry(attr.key, handlers[attr.key]!)) + .toList(); + for (final handlerEntry in handlersToUse) { + handlerEntry.value.beforeContent?.call( + node.style.attributes[handlerEntry.key]!, + node, + output, + ); + } + contentHandler(); + for (final handlerEntry in handlersToUse.reversed) { + handlerEntry.value.afterContent?.call( + node.style.attributes[handlerEntry.key]!, + node, + output, + ); + } + } +} + +//// AST with visitor + +abstract class _NodeVisitor { + const _NodeVisitor._(); + + T visitRoot(Root root, [T? context]); + + T visitBlock(Block block, [T? context]); + + T visitLine(Line line, [T? context]); + + T visitText(QuillText text, [T? context]); + + T visitEmbed(Embed embed, [T? context]); +} + +extension _NodeX on Node { + T accept(_NodeVisitor visitor, [T? context]) { + switch (runtimeType) { + case Root _: + return visitor.visitRoot(this as Root, context); + case Block _: + return visitor.visitBlock(this as Block, context); + case Line _: + return visitor.visitLine(this as Line, context); + case QuillText _: + return visitor.visitText(this as QuillText, context); + case Embed _: + return visitor.visitEmbed(this as Embed, context); + } + throw Exception('Container of type $runtimeType cannot be visited'); + } + + bool containsAttr(String attributeKey, [Object? value]) { + if (!style.containsKey(attributeKey)) { + return false; + } + if (value == null) { + return true; + } + return style.attributes[attributeKey]!.value == value; + } + + T getAttrValueOr(String attributeKey, T or) { + final attrs = style.attributes; + final attrValue = attrs[attributeKey]?.value as T?; + return attrValue ?? or; + } + + List> attrsSortedByLongestSpan() { + final attrCount = , int>{}; + var node = this; + // get the first node + while (node.previous != null) { + node = node.previous!; + node.style.attributes.forEach((key, value) { + attrCount[value] = (attrCount[value] ?? 0) + 1; + }); + node = node.next!; + } + + final attrs = style.attributes.values.sorted( + (attr1, attr2) => attrCount[attr2]!.compareTo(attrCount[attr1]!)); + + return attrs; + } +} diff --git a/lib/src/packages/quill_markdown/embeddable_table_syntax.dart b/lib/src/packages/quill_markdown/embeddable_table_syntax.dart new file mode 100644 index 000000000..d33e703c5 --- /dev/null +++ b/lib/src/packages/quill_markdown/embeddable_table_syntax.dart @@ -0,0 +1,116 @@ +import 'package:charcode/charcode.dart'; +import 'package:flutter_quill/flutter_quill.dart' hide Node; +import 'package:markdown/markdown.dart'; + +/// Parses markdown table and saves the table markdown content into the element attributes. +class EmbeddableTableSyntax extends BlockSyntax { + /// @nodoc + const EmbeddableTableSyntax(); + static const _base = TableSyntax(); + + @override + bool canEndBlock(BlockParser parser) => false; + + @override + RegExp get pattern => _base.pattern; + + @override + bool canParse(BlockParser parser) => _base.canParse(parser); + + /// Parses a table into its three parts: + /// + /// * a head row of head cells (`` cells) + /// * a divider of hyphens and pipes (not rendered) + /// * many body rows of body cells (`` cells) + @override + Node? parse(BlockParser parser) { + final columnCount = _columnCount(parser.next!.content); + final headCells = _columnCount(parser.current.content); + final valBuf = + StringBuffer('${parser.current.content}\n${parser.next!.content}'); + parser.advance(); + if (columnCount != headCells) { + return null; + } + + // advance header and divider of hyphens. + parser.advance(); + + while (!parser.isDone && !BlockSyntax.isAtBlockEnd(parser)) { + valBuf.write('\n${parser.current.content}'); + parser.advance(); + } + + return Element.empty(EmbeddableTable.tableType) + ..attributes['data'] = valBuf.toString(); + } + + int _columnCount(String line) { + final startIndex = _walkPastOpeningPipe(line); + + var endIndex = line.length - 1; + while (endIndex > 0) { + final ch = line.codeUnitAt(endIndex); + if (ch == $pipe) { + endIndex--; + break; + } + if (ch != $space && ch != $tab) { + break; + } + endIndex--; + } + + return line.substring(startIndex, endIndex + 1).split('|').length; + } + + int _walkPastWhitespace(String line, int index) { + while (index < line.length) { + final ch = line.codeUnitAt(index); + if (ch != $space && ch != $tab) { + break; + } + //ignore: parameter_assignments + index++; + } + return index; + } + + int _walkPastOpeningPipe(String line) { + var index = 0; + while (index < line.length) { + final ch = line.codeUnitAt(index); + if (ch == $pipe) { + index++; + index = _walkPastWhitespace(line, index); + } + if (ch != $space && ch != $tab) { + // No leading pipe. + break; + } + index++; + } + return index; + } +} + +/// An [Embeddable] table that can used to render a table in quill_editor +class EmbeddableTable extends BlockEmbed { + /// @nodoc + EmbeddableTable(String data) : super(tableType, data); + + /// [Embeddable] type + static const tableType = 'x-embed-table'; + + /// Create from markdown. + //ignore: prefer_constructors_over_static_methods + static EmbeddableTable fromMdSyntax(Map attributes) => + EmbeddableTable(attributes['data']!); + + /// Outputs table markdown to output. + static void toMdSyntax(Embed embed, StringSink out) { + out + ..writeln(embed.value.data) + ..writeln(); + } +} diff --git a/lib/src/packages/quill_markdown/markdown_to_delta.dart b/lib/src/packages/quill_markdown/markdown_to_delta.dart new file mode 100644 index 000000000..d4bca2e7d --- /dev/null +++ b/lib/src/packages/quill_markdown/markdown_to_delta.dart @@ -0,0 +1,424 @@ +import 'dart:collection'; +import 'dart:convert'; + +import 'package:collection/collection.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:markdown/markdown.dart' as md; + +import './custom_quill_attributes.dart'; +import './embeddable_table_syntax.dart'; +import './utils.dart'; + +/// Converts markdown [md.Element] to list of [Attribute]. +typedef ElementToAttributeConvertor = List> Function( + md.Element element, +); + +/// Converts markdown [md.Element] to [Embeddable]. +typedef ElementToEmbeddableConvertor = Embeddable Function( + Map elAttrs, +); + +/// Convertor from Markdown string to quill [Delta]. +class MarkdownToDelta extends Converter + implements md.NodeVisitor { + /// + MarkdownToDelta({ + required this.markdownDocument, + this.customElementToInlineAttribute = const {}, + this.customElementToBlockAttribute = const {}, + this.customElementToEmbeddable = const {}, + this.softLineBreak = false, + }); + + final md.Document markdownDocument; + final Map customElementToInlineAttribute; + final Map customElementToBlockAttribute; + final Map customElementToEmbeddable; + final bool softLineBreak; + + // final _blockTags = [ + // 'p', + // 'h1', + // 'h2', + // 'h3', + // 'h4', + // 'h5', + // 'h6', + // 'li', + // 'blockquote', + // 'pre', + // 'ol', + // 'ul', + // 'hr', + // 'table', + // 'thead', + // 'tbody', + // 'tr' + // ]; + + final _elementToBlockAttr = { + 'ul': (_) => [Attribute.ul], + 'ol': (_) => [Attribute.ol], + 'pre': (element) { + final codeChild = element.children!.first as md.Element; + final language = (codeChild.attributes['class'] ?? '') + .split(' ') + .where((class_) => class_.startsWith('language-')) + .firstOrNull + ?.split('-') + .lastOrNull; + return [ + Attribute.codeBlock, + if (language != null) CodeBlockLanguageAttribute(language), + ]; + }, + 'blockquote': (_) => [Attribute.blockQuote], + 'h1': (_) => [Attribute.h1], + 'h2': (_) => [Attribute.h2], + 'h3': (_) => [Attribute.h3], + }; + + final _elementToInlineAttr = { + 'em': (_) => [Attribute.italic], + 'strong': (_) => [Attribute.bold], + 'del': (_) => [Attribute.strikeThrough], + 'a': (element) => [LinkAttribute(element.attributes['href'])], + 'code': (_) => [Attribute.inlineCode], + }; + + final _elementToEmbed = { + 'hr': (_) => horizontalRule, + 'img': (elAttrs) => BlockEmbed.image(elAttrs['src'] ?? ''), + }; + + var _delta = Delta(); + final _activeInlineAttributes = Queue>>(); + final _activeBlockAttributes = Queue>>(); + final _topLevelNodes = []; + bool _isInBlockQuote = false; + bool _isInCodeblock = false; + bool _justPreviousBlockExit = false; + String? _lastTag; + String? _currentBlockTag; + int _listItemIndent = -1; + + @override + Delta convert(String input) { + _delta = Delta(); + _activeInlineAttributes.clear(); + _activeBlockAttributes.clear(); + _topLevelNodes.clear(); + _lastTag = null; + _currentBlockTag = null; + _isInBlockQuote = false; + _isInCodeblock = false; + _justPreviousBlockExit = false; + _listItemIndent = -1; + + final lines = const LineSplitter().convert(input); + final mdNodes = markdownDocument.parseLines(lines); + + _topLevelNodes.addAll(mdNodes); + + for (final node in mdNodes) { + node.accept(this); + } + + // Ensure the delta ends with a newline. + _appendLastNewLineIfNeeded(); + + return _delta; + } + + void _appendLastNewLineIfNeeded() { + if (_delta.isEmpty) return; + final dynamic lastValue = _delta.last.value; + if (!(lastValue is String && lastValue.endsWith('\n'))) { + _delta.insert('\n', _effectiveBlockAttrs()); + } + } + + @override + void visitText(md.Text text) { + String renderedText; + if (_isInBlockQuote) { + renderedText = text.text; + } else if (_isInCodeblock) { + renderedText = text.text.endsWith('\n') + ? text.text.substring(0, text.text.length - 1) + : text.text; + } else { + renderedText = _trimTextToMdSpec(text.text); + } + + if (renderedText.contains('\n')) { + var lines = renderedText.split('\n'); + if (renderedText.endsWith('\n')) { + lines = lines.sublist(0, lines.length - 1); + } + for (var i = 0; i < lines.length; i++) { + final isLastItem = i == lines.length - 1; + final line = lines[i]; + _delta.insert(line, _effectiveInlineAttrs()); + if (!isLastItem) { + _delta.insert('\n', _effectiveBlockAttrs()); + } + } + } else { + _delta.insert(renderedText, _effectiveInlineAttrs()); + } + _lastTag = null; + _justPreviousBlockExit = false; + } + + @override + bool visitElementBefore(md.Element element) { + _insertNewLineBeforeElementIfNeeded(element); + + final tag = element.tag; + _currentBlockTag ??= tag; + _lastTag = tag; + + if (_haveBlockAttrs(element)) { + _activeBlockAttributes.addLast(_toBlockAttributes(element)); + } + if (_haveInlineAttrs(element)) { + _activeInlineAttributes.addLast(_toInlineAttributes(element)); + } + + if (tag == 'blockquote') { + _isInBlockQuote = true; + } + + if (tag == 'pre') { + _isInCodeblock = true; + } + + if (tag == 'li') { + _listItemIndent++; + } + + return true; + } + + @override + void visitElementAfter(md.Element element) { + final tag = element.tag; + + if (_isEmbedElement(element)) { + _delta.insert(_toEmbeddable(element).toJson()); + } + + if (tag == 'br') { + _delta.insert('\n'); + } + + // exit block with new line + // hr need to be followed by new line + _insertNewLineAfterElementIfNeeded(element); + + if (tag == 'blockquote') { + _isInBlockQuote = false; + } + + if (tag == 'pre') { + _isInCodeblock = false; + } + + if (tag == 'li') { + _listItemIndent--; + } + + if (_haveBlockAttrs(element)) { + _activeBlockAttributes.removeLast(); + } + + if (_haveInlineAttrs(element)) { + _activeInlineAttributes.removeLast(); + } + + if (_currentBlockTag == tag) { + _currentBlockTag = null; + } + _lastTag = tag; + } + + void _insertNewLine() { + _delta.insert('\n', _effectiveBlockAttrs()); + } + + void _insertNewLineBeforeElementIfNeeded(md.Element element) { + if (!_isInBlockQuote && + _lastTag == 'blockquote' && + element.tag == 'blockquote') { + _insertNewLine(); + return; + } + + if (!_isInCodeblock && _lastTag == 'pre' && element.tag == 'pre') { + _insertNewLine(); + return; + } + + if (_listItemIndent >= 0 && (element.tag == 'ul' || element.tag == 'ol')) { + _insertNewLine(); + return; + } + } + + void _insertNewLineAfterElementIfNeeded(md.Element element) { + // TODO: refactor this to allow embeds to specify if they require + // new line after them + if (element.tag == 'hr' || element.tag == EmbeddableTable.tableType) { + // Always add new line after divider + _justPreviousBlockExit = true; + _insertNewLine(); + return; + } + + // if all the p children are embeddable add a new line + // example: images in a single line + if (element.tag == 'p' && + (element.children?.every( + (child) => child is md.Element && _isEmbedElement(child), + ) ?? + false)) { + _justPreviousBlockExit = true; + _insertNewLine(); + return; + } + + if (!_justPreviousBlockExit && + (_isTopLevelNode(element) || + _haveBlockAttrs(element) || + element.tag == 'li')) { + _justPreviousBlockExit = true; + _insertNewLine(); + return; + } + } + + bool _isTopLevelNode(md.Node node) => _topLevelNodes.contains(node); + + Map? _effectiveBlockAttrs() { + if (_activeBlockAttributes.isEmpty) return null; + final attrsRespectingExclusivity = >[ + if (_listItemIndent > 0) IndentAttribute(level: _listItemIndent), + ]; + + for (final attr in _activeBlockAttributes.expand((e) => e)) { + final isExclusiveAttr = Attribute.exclusiveBlockKeys.contains( + attr.key, + ); + final isThereAlreadyExclusiveAttr = attrsRespectingExclusivity.any( + (element) => Attribute.exclusiveBlockKeys.contains(element.key), + ); + + if (!(isExclusiveAttr && isThereAlreadyExclusiveAttr)) { + attrsRespectingExclusivity.add(attr); + } + } + + return { + for (final a in attrsRespectingExclusivity) ...a.toJson(), + }; + } + + Map? _effectiveInlineAttrs() { + if (_activeInlineAttributes.isEmpty) return null; + return { + for (final attrs in _activeInlineAttributes) + for (final a in attrs) ...a.toJson(), + }; + } + + // Define trim text function to remove spaces from text elements in + // accordance with Markdown specifications. + String _trimTextToMdSpec(String text) { + var result = text; + // The leading spaces pattern is used to identify spaces + // at the beginning of a line of text. + final leadingSpacesPattern = RegExp('^ *'); + + // The soft line break is used to identify the spaces at the end of a line + // of text and the leading spaces in the immediately following the line + // of text. These spaces are removed in accordance with the Markdown + // specification on soft line breaks when lines of text are joined. + final softLineBreak = RegExp(r' ?\n *'); + + // Leading spaces following a hard line break are ignored. + // https://github.github.com/gfm/#example-657 + if (const ['p', 'ol', 'li', 'br'].contains(_lastTag)) { + result = result.replaceAll(leadingSpacesPattern, ''); + } + + if (softLineBreak.hasMatch(result)) { + return result; + } + return result.replaceAll(softLineBreak, ' '); + } + + Map _effectiveElementToInlineAttr() { + return { + ...customElementToInlineAttribute, + ..._elementToInlineAttr, + }; + } + + bool _haveInlineAttrs(md.Element element) { + if (_isInCodeblock && element.tag == 'code') return false; + return _effectiveElementToInlineAttr().containsKey(element.tag); + } + + List> _toInlineAttributes(md.Element element) { + List>? result; + if (!(_isInCodeblock && element.tag == 'code')) { + result = _effectiveElementToInlineAttr()[element.tag]?.call(element); + } + if (result == null) { + throw Exception( + 'Element $element cannot be converted to inline attribute'); + } + return result; + } + + Map _effectiveElementToBlockAttr() { + return { + ...customElementToBlockAttribute, + ..._elementToBlockAttr, + }; + } + + bool _haveBlockAttrs(md.Element element) { + return _effectiveElementToBlockAttr().containsKey(element.tag); + } + + List> _toBlockAttributes(md.Element element) { + final result = _effectiveElementToBlockAttr()[element.tag]?.call(element); + if (result == null) { + throw Exception( + 'Element $element cannot be converted to block attribute'); + } + return result; + } + + Map _effectiveElementToEmbed() { + return { + ...customElementToEmbeddable, + ..._elementToEmbed, + }; + } + + bool _isEmbedElement(md.Element element) => + _effectiveElementToEmbed().containsKey(element.tag); + + Embeddable _toEmbeddable(md.Element element) { + final result = + _effectiveElementToEmbed()[element.tag]?.call(element.attributes); + if (result == null) { + throw Exception('Element $element cannot be converted to Embeddable'); + } + return result; + } +} diff --git a/lib/src/packages/quill_markdown/utils.dart b/lib/src/packages/quill_markdown/utils.dart new file mode 100644 index 000000000..c5843ec5c --- /dev/null +++ b/lib/src/packages/quill_markdown/utils.dart @@ -0,0 +1,59 @@ +//ignore_for_file: cast_nullable_to_non_nullable +import 'package:flutter_quill/flutter_quill.dart'; + +import './embeddable_table_syntax.dart'; + +/// To allow embedding images/videos in horizontal mode. +const BlockEmbed horizontalRule = BlockEmbed(horizontalRuleType, 'hr'); + +/// Necessary for [horizontalRule] BlockEmbed. +const String horizontalRuleType = 'divider'; + +/// Format the passed delta to ensure that there is new line +/// after embeds +Delta transform(Delta delta) { + final res = Delta(); + final ops = delta.toList(); + for (var i = 0; i < ops.length; i++) { + final op = ops[i]; + res.push(op); + autoAppendNewlineAfterEmbeddable(i, ops, op, res, [ + 'hr', + EmbeddableTable.tableType, + ]); + } + return res; +} + +/// Appends new line after embeds if needed +void autoAppendNewlineAfterEmbeddable( + int i, + List ops, + Operation op, + Delta res, + List types, +) { + final nextOpIsEmbed = i + 1 < ops.length && + ops[i + 1].isInsert && + ops[i + 1].data is Map && + types.any((type) => (ops[i + 1].data as Map).containsKey(type)); + + if (nextOpIsEmbed && + op.data is String && + (op.data as String).isNotEmpty && + !(op.data as String).endsWith('\n')) { + res.push(Operation.insert('\n')); + } + // embed could be image or video + final opInsertEmbed = op.isInsert && + op.data is Map && + types.any((type) => (op.data as Map).containsKey(type)); + final nextOpIsLineBreak = i + 1 < ops.length && + ops[i + 1].isInsert && + ops[i + 1].data is String && + (ops[i + 1].data as String).startsWith('\n'); + if (opInsertEmbed && (i + 1 == ops.length - 1 || !nextOpIsLineBreak)) { + // automatically append '\n' for embeddable + res.push(Operation.insert('\n')); + } +} diff --git a/lib/src/utils/embeds.dart b/lib/src/utils/embeds.dart index 973942bb1..4b3e2de38 100644 --- a/lib/src/utils/embeds.dart +++ b/lib/src/utils/embeds.dart @@ -2,7 +2,7 @@ import 'dart:math'; import '../models/documents/nodes/leaf.dart'; import '../models/structs/offset_value.dart'; -import '../widgets/others/controller.dart'; +import '../widgets/quill/quill_controller.dart'; OffsetValue getEmbedNode(QuillController controller, int offset) { var offset = controller.selection.start; diff --git a/lib/src/widgets/editor/editor.dart b/lib/src/widgets/editor/editor.dart index 68665809c..0017b68c0 100644 --- a/lib/src/widgets/editor/editor.dart +++ b/lib/src/widgets/editor/editor.dart @@ -18,7 +18,7 @@ import '../../utils/platform.dart'; import '../others/box.dart'; import '../others/cursor.dart'; import '../others/delegate.dart'; -import '../others/embeds.dart'; +import '../quill/embeds.dart'; import '../others/float_cursor.dart'; import '../others/text_selection.dart'; import '../raw_editor/raw_editor.dart'; diff --git a/lib/src/widgets/others/delegate.dart b/lib/src/widgets/others/delegate.dart index 3bf19cdc5..f22074abe 100644 --- a/lib/src/widgets/others/delegate.dart +++ b/lib/src/widgets/others/delegate.dart @@ -7,8 +7,8 @@ import '../../models/documents/attribute.dart'; import '../../models/documents/nodes/leaf.dart'; import '../../utils/platform.dart'; import '../editor/editor.dart'; +import '../quill/embeds.dart'; import '../raw_editor/raw_editor.dart'; -import 'embeds.dart'; import 'text_selection.dart'; typedef EmbedsBuilder = EmbedBuilder Function(Embed node); diff --git a/lib/src/widgets/others/embeds.dart b/lib/src/widgets/quill/embeds.dart similarity index 96% rename from lib/src/widgets/others/embeds.dart rename to lib/src/widgets/quill/embeds.dart index 9b828d1bc..d575f2fe1 100644 --- a/lib/src/widgets/others/embeds.dart +++ b/lib/src/widgets/quill/embeds.dart @@ -4,7 +4,7 @@ import '../../../extensions.dart'; import '../../models/documents/nodes/leaf.dart' as leaf; import '../../models/themes/quill_dialog_theme.dart'; import '../../models/themes/quill_icon_theme.dart'; -import 'controller.dart'; +import 'quill_controller.dart'; abstract class EmbedBuilder { const EmbedBuilder(); diff --git a/lib/src/widgets/others/controller.dart b/lib/src/widgets/quill/quill_controller.dart similarity index 90% rename from lib/src/widgets/others/controller.dart rename to lib/src/widgets/quill/quill_controller.dart index 0928cf88e..fd3403003 100644 --- a/lib/src/widgets/others/controller.dart +++ b/lib/src/widgets/quill/quill_controller.dart @@ -1,8 +1,11 @@ import 'dart:math' as math; -import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:html2md/html2md.dart' as html2md; +import 'package:markdown/markdown.dart' as md; +import '../../../markdown_quill.dart'; import '../../models/documents/attribute.dart'; import '../../models/documents/document.dart'; import '../../models/documents/nodes/embeddable.dart'; @@ -42,7 +45,7 @@ class QuillController extends ChangeNotifier { Document get document => _document; - set document(doc) { + set document(Document doc) { _document = doc; // Prevent the selection from @@ -51,6 +54,11 @@ class QuillController extends ChangeNotifier { notifyListeners(); } + void updateDocument(Document newDocument) { + _document = newDocument; + notifyListeners(); + } + /// Tells whether to keep or reset the [toggledStyle] /// when user adds a new line. final bool _keepStyleOnNewLine; @@ -439,4 +447,38 @@ class QuillController extends ChangeNotifier { // Notify toolbar buttons directly with attributes Map toolbarButtonToggler = const {}; + + /// Convert the HTML Raw string to [Delta] + /// + /// It will run using the following steps: + /// + /// 1. Convert the html to markdown string using `html2md` package + /// 2. Convert the markdown string to quill delta json string + /// 3. Decode the delta json string to [Delta] + /// + /// for more [info](https://github.com/singerdmx/flutter-quill/issues/1100) + static Delta fromHtml(String html) { + final markdown = html2md + .convert( + html, + ) + .replaceAll('unsafe:', ''); + + final mdDocument = md.Document(encodeHtml: false); + + final mdToDelta = MarkdownToDelta(markdownDocument: mdDocument); + + return mdToDelta.convert(markdown); + + // final deltaJsonString = markdownToDelta(markdown); + // final deltaJson = jsonDecode(deltaJsonString); + // if (deltaJson is! List) { + // throw ArgumentError( + // 'The delta json string should be of type list when jsonDecode() it', + // ); + // } + // return Delta.fromJson( + // deltaJson, + // ); + } } diff --git a/lib/src/widgets/others/text_block.dart b/lib/src/widgets/quill/text_block.dart similarity index 98% rename from lib/src/widgets/others/text_block.dart rename to lib/src/widgets/quill/text_block.dart index 878064804..c13c5eeb1 100644 --- a/lib/src/widgets/others/text_block.dart +++ b/lib/src/widgets/quill/text_block.dart @@ -11,14 +11,14 @@ import '../editor/editor.dart'; import '../style_widgets/bullet_point.dart'; import '../style_widgets/checkbox_point.dart'; import '../style_widgets/number_point.dart'; -import 'box.dart'; -import 'controller.dart'; -import 'cursor.dart'; -import 'default_styles.dart'; -import 'delegate.dart'; -import 'link.dart'; +import '../others/box.dart'; +import 'quill_controller.dart'; +import '../others/cursor.dart'; +import '../others/default_styles.dart'; +import '../others/delegate.dart'; +import '../others/link.dart'; import 'text_line.dart'; -import 'text_selection.dart'; +import '../others/text_selection.dart'; const List arabianRomanNumbers = [ 1000, diff --git a/lib/src/widgets/others/text_line.dart b/lib/src/widgets/quill/text_line.dart similarity index 99% rename from lib/src/widgets/others/text_line.dart rename to lib/src/widgets/quill/text_line.dart index 3a155333f..7ec68bd4a 100644 --- a/lib/src/widgets/others/text_line.dart +++ b/lib/src/widgets/quill/text_line.dart @@ -20,15 +20,15 @@ import '../../models/structs/vertical_spacing.dart'; import '../../utils/color.dart'; import '../../utils/font.dart'; import '../../utils/platform.dart'; -import 'box.dart'; -import 'controller.dart'; -import 'cursor.dart'; -import 'default_styles.dart'; -import 'delegate.dart'; -import 'keyboard_listener.dart'; -import 'link.dart'; -import 'proxy.dart'; -import 'text_selection.dart'; +import '../others/box.dart'; +import 'quill_controller.dart'; +import '../others/cursor.dart'; +import '../others/default_styles.dart'; +import '../others/delegate.dart'; +import '../others/keyboard_listener.dart'; +import '../others/link.dart'; +import '../others/proxy.dart'; +import '../others/text_selection.dart'; class TextLine extends StatefulWidget { const TextLine({ @@ -163,7 +163,7 @@ class _TextLineState extends State { ); } } - final textSpan = _getTextSpanForWholeLine(context); + final textSpan = _getTextSpanForWholeLine(); final strutStyle = StrutStyle.fromTextStyle(textSpan.style!); final textAlign = _getTextAlign(); final child = RichText( @@ -185,7 +185,7 @@ class _TextLineState extends State { ); } - InlineSpan _getTextSpanForWholeLine(BuildContext context) { + InlineSpan _getTextSpanForWholeLine() { final lineStyle = _getLineStyle(widget.styles); if (!widget.line.hasEmbed) { return _buildTextSpan(widget.styles, widget.line.children, lineStyle); diff --git a/lib/src/widgets/raw_editor/raw_editor_state.dart b/lib/src/widgets/raw_editor/raw_editor_state.dart index 897658cca..64d1e28c0 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state.dart @@ -28,6 +28,7 @@ import '../../models/documents/nodes/embeddable.dart'; import '../../models/documents/nodes/leaf.dart' as leaf; import '../../models/documents/nodes/line.dart'; import '../../models/documents/nodes/node.dart'; +import '../../models/quill_delta.dart'; import '../../models/structs/offset_value.dart'; import '../../models/structs/vertical_spacing.dart'; import '../../utils/cast.dart'; @@ -35,14 +36,14 @@ import '../../utils/delta.dart'; import '../../utils/embeds.dart'; import '../../utils/platform.dart'; import '../editor/editor.dart'; -import '../others/controller.dart'; +import '../quill/quill_controller.dart'; import '../others/cursor.dart'; import '../others/default_styles.dart'; import '../others/keyboard_listener.dart'; import '../others/link.dart'; import '../others/proxy.dart'; -import '../others/text_block.dart'; -import '../others/text_line.dart'; +import '../quill/text_block.dart'; +import '../quill/text_line.dart'; import '../others/text_selection.dart'; import 'quill_single_child_scroll_view.dart'; import 'raw_editor.dart'; @@ -224,14 +225,65 @@ class QuillRawEditorState extends EditorState if (!selection.isValid) { return; } + + // TODO: Could be improved + Delta? deltaFromCliboard; + final reader = await ClipboardReader.readClipboard(); + if (reader.canProvide(Formats.htmlText)) { + final html = await reader.readValue(Formats.htmlText); + if (html == null) { + return; + } + deltaFromCliboard = QuillController.fromHtml(html); + } + if (deltaFromCliboard != null) { + // final index = selection.baseOffset; + // final length = selection.extentOffset - index; + + final list = controller.document.toDelta().toList() + ..insertAll(controller.document.toDelta().toList().length - 1, + deltaFromCliboard.toList()); + + final delta = controller.document.toDelta(); + for (final operation in list) { + delta.push(operation); + } + + controller + ..updateDocument( + Document.fromDelta(delta), + ) + ..updateSelection( + TextSelection.collapsed( + offset: controller.document.length, + ), + ChangeSource.local, + ); + + bringIntoView(textEditingValue.selection.extent); + + // Collapse the selection and hide the toolbar and handles. + userUpdateTextEditingValue( + TextEditingValue( + text: textEditingValue.text, + selection: TextSelection.collapsed( + offset: textEditingValue.selection.end, + ), + ), + cause, + ); + + return; + } + // Snapshot the input before using `await`. // See https://github.com/flutter/flutter/issues/11427 - final text = await Clipboard.getData(Clipboard.kTextPlain); - if (text != null) { + final plainText = await Clipboard.getData(Clipboard.kTextPlain); + if (plainText != null) { _replaceText( ReplaceTextIntent( textEditingValue, - text.text!, + plainText.text!, selection, cause, ), @@ -1693,6 +1745,8 @@ class QuillRawEditorState extends EditorState } } + // TODO: Review those + @override bool get liveTextInputEnabled => false; diff --git a/lib/src/widgets/style_widgets/number_point.dart b/lib/src/widgets/style_widgets/number_point.dart index 398dd3945..ed5d96c8c 100644 --- a/lib/src/widgets/style_widgets/number_point.dart +++ b/lib/src/widgets/style_widgets/number_point.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import '../../extensions/quill_configurations_ext.dart'; import '../../models/documents/attribute.dart'; -import '../others/text_block.dart'; +import '../quill/text_block.dart'; class QuillEditorNumberPoint extends StatelessWidget { const QuillEditorNumberPoint({ diff --git a/lib/src/widgets/toolbar/buttons/clear_format_button.dart b/lib/src/widgets/toolbar/buttons/clear_format_button.dart index bf1ba0962..751474116 100644 --- a/lib/src/widgets/toolbar/buttons/clear_format_button.dart +++ b/lib/src/widgets/toolbar/buttons/clear_format_button.dart @@ -4,7 +4,7 @@ import '../../../extensions/quill_configurations_ext.dart'; import '../../../l10n/extensions/localizations.dart'; import '../../../models/documents/attribute.dart'; import '../../../models/themes/quill_icon_theme.dart'; -import '../../others/controller.dart'; +import '../../quill/quill_controller.dart'; import '../base_toolbar.dart'; class QuillToolbarClearFormatButton extends StatelessWidget { diff --git a/lib/src/widgets/toolbar/buttons/color/color_button.dart b/lib/src/widgets/toolbar/buttons/color/color_button.dart index ff2dce6b6..2140afc40 100644 --- a/lib/src/widgets/toolbar/buttons/color/color_button.dart +++ b/lib/src/widgets/toolbar/buttons/color/color_button.dart @@ -7,7 +7,7 @@ import '../../../../models/documents/attribute.dart'; import '../../../../models/documents/style.dart'; import '../../../../models/themes/quill_icon_theme.dart'; import '../../../../utils/color.dart'; -import '../../../others/controller.dart'; +import '../../../quill/quill_controller.dart'; import '../../base_toolbar.dart'; import 'color_dialog.dart'; diff --git a/lib/src/widgets/toolbar/buttons/custom_button_button.dart b/lib/src/widgets/toolbar/buttons/custom_button_button.dart index 27145d344..825b3d2f9 100644 --- a/lib/src/widgets/toolbar/buttons/custom_button_button.dart +++ b/lib/src/widgets/toolbar/buttons/custom_button_button.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import '../../../extensions/quill_configurations_ext.dart'; import '../../../models/themes/quill_icon_theme.dart'; -import '../../others/controller.dart'; +import '../../quill/quill_controller.dart'; import '../base_toolbar.dart'; class QuillToolbarCustomButton extends StatelessWidget { diff --git a/lib/src/widgets/toolbar/buttons/font_family_button.dart b/lib/src/widgets/toolbar/buttons/font_family_button.dart index dc23f92fa..8714d6fa3 100644 --- a/lib/src/widgets/toolbar/buttons/font_family_button.dart +++ b/lib/src/widgets/toolbar/buttons/font_family_button.dart @@ -7,7 +7,7 @@ import '../../../models/config/toolbar/buttons/font_family_configurations.dart'; import '../../../models/documents/attribute.dart'; import '../../../models/documents/style.dart'; import '../../../models/themes/quill_icon_theme.dart'; -import '../../others/controller.dart'; +import '../../quill/quill_controller.dart'; class QuillToolbarFontFamilyButton extends StatefulWidget { QuillToolbarFontFamilyButton({ diff --git a/lib/src/widgets/toolbar/buttons/font_size_button.dart b/lib/src/widgets/toolbar/buttons/font_size_button.dart index 0debeba72..d1ee7752e 100644 --- a/lib/src/widgets/toolbar/buttons/font_size_button.dart +++ b/lib/src/widgets/toolbar/buttons/font_size_button.dart @@ -8,7 +8,7 @@ import '../../../models/documents/attribute.dart'; import '../../../models/documents/style.dart'; import '../../../models/themes/quill_icon_theme.dart'; import '../../../utils/font.dart'; -import '../../others/controller.dart'; +import '../../quill/quill_controller.dart'; class QuillToolbarFontSizeButton extends StatefulWidget { QuillToolbarFontSizeButton({ diff --git a/lib/src/widgets/toolbar/buttons/history_button.dart b/lib/src/widgets/toolbar/buttons/history_button.dart index 4346a6e37..5436e74cf 100644 --- a/lib/src/widgets/toolbar/buttons/history_button.dart +++ b/lib/src/widgets/toolbar/buttons/history_button.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import '../../../extensions/quill_configurations_ext.dart'; import '../../../l10n/extensions/localizations.dart'; -import '../../others/controller.dart'; +import '../../quill/quill_controller.dart'; import '../base_toolbar.dart'; class QuillToolbarHistoryButton extends StatefulWidget { diff --git a/lib/src/widgets/toolbar/buttons/indent_button.dart b/lib/src/widgets/toolbar/buttons/indent_button.dart index f82097b16..654b3d472 100644 --- a/lib/src/widgets/toolbar/buttons/indent_button.dart +++ b/lib/src/widgets/toolbar/buttons/indent_button.dart @@ -4,7 +4,7 @@ import '../../../extensions/quill_configurations_ext.dart'; import '../../../l10n/extensions/localizations.dart'; import '../../../models/config/toolbar/buttons/indent_configurations.dart'; import '../../../models/themes/quill_icon_theme.dart'; -import '../../others/controller.dart'; +import '../../quill/quill_controller.dart'; import '../base_toolbar.dart' show QuillToolbarBaseButtonOptions, QuillToolbarIconButton; diff --git a/lib/src/widgets/toolbar/buttons/link_style2_button.dart b/lib/src/widgets/toolbar/buttons/link_style2_button.dart index a663d8647..63372fbae 100644 --- a/lib/src/widgets/toolbar/buttons/link_style2_button.dart +++ b/lib/src/widgets/toolbar/buttons/link_style2_button.dart @@ -10,7 +10,7 @@ import '../../../l10n/widgets/localizations.dart'; import '../../../models/documents/attribute.dart'; import '../../../models/themes/quill_dialog_theme.dart'; import '../../../models/themes/quill_icon_theme.dart'; -import '../../others/controller.dart'; +import '../../quill/quill_controller.dart'; import '../../others/link.dart'; import '../base_toolbar.dart'; diff --git a/lib/src/widgets/toolbar/buttons/link_style_button.dart b/lib/src/widgets/toolbar/buttons/link_style_button.dart index e52533d11..21d87dff6 100644 --- a/lib/src/widgets/toolbar/buttons/link_style_button.dart +++ b/lib/src/widgets/toolbar/buttons/link_style_button.dart @@ -8,7 +8,7 @@ import '../../../models/rules/insert.dart'; import '../../../models/structs/link_dialog_action.dart'; import '../../../models/themes/quill_dialog_theme.dart'; import '../../../models/themes/quill_icon_theme.dart'; -import '../../others/controller.dart'; +import '../../quill/quill_controller.dart'; import '../../others/link.dart'; import '../base_toolbar.dart'; diff --git a/lib/src/widgets/toolbar/buttons/search/search_button.dart b/lib/src/widgets/toolbar/buttons/search/search_button.dart index ed8f634c3..78936d35c 100644 --- a/lib/src/widgets/toolbar/buttons/search/search_button.dart +++ b/lib/src/widgets/toolbar/buttons/search/search_button.dart @@ -5,7 +5,7 @@ import '../../../../l10n/extensions/localizations.dart'; import '../../../../l10n/widgets/localizations.dart'; import '../../../../models/themes/quill_dialog_theme.dart'; import '../../../../models/themes/quill_icon_theme.dart'; -import '../../../others/controller.dart'; +import '../../../quill/quill_controller.dart'; import '../../base_toolbar.dart'; class QuillToolbarSearchButton extends StatelessWidget { diff --git a/lib/src/widgets/toolbar/buttons/search/search_dialog.dart b/lib/src/widgets/toolbar/buttons/search/search_dialog.dart index 65b2c4d4c..337059f4f 100644 --- a/lib/src/widgets/toolbar/buttons/search/search_dialog.dart +++ b/lib/src/widgets/toolbar/buttons/search/search_dialog.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import '../../../../l10n/extensions/localizations.dart'; import '../../../../models/documents/document.dart'; import '../../../../models/themes/quill_dialog_theme.dart'; -import '../../../others/controller.dart'; +import '../../../quill/quill_controller.dart'; @immutable class QuillToolbarSearchDialogChildBuilderExtraOptions { diff --git a/lib/src/widgets/toolbar/buttons/select_alignment_buttons.dart b/lib/src/widgets/toolbar/buttons/select_alignment_buttons.dart index af5c383a5..7259a2c59 100644 --- a/lib/src/widgets/toolbar/buttons/select_alignment_buttons.dart +++ b/lib/src/widgets/toolbar/buttons/select_alignment_buttons.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import '../../../models/config/toolbar/buttons/select_alignment_configurations.dart'; import '../../../models/documents/attribute.dart'; -import '../../others/controller.dart'; +import '../../quill/quill_controller.dart'; import 'toggle_style_button.dart'; enum _AlignmentOptions { diff --git a/lib/src/widgets/toolbar/buttons/select_header_style_button.dart b/lib/src/widgets/toolbar/buttons/select_header_style_button.dart index 189146a8e..1e92c84fd 100644 --- a/lib/src/widgets/toolbar/buttons/select_header_style_button.dart +++ b/lib/src/widgets/toolbar/buttons/select_header_style_button.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import '../../../../translations.dart'; import '../../../models/config/toolbar/buttons/select_header_style_configurations.dart'; import '../../../models/documents/attribute.dart'; -import '../../others/controller.dart'; +import '../../quill/quill_controller.dart'; enum _HeaderStyleOptions { normal, diff --git a/lib/src/widgets/toolbar/buttons/select_header_style_buttons.dart b/lib/src/widgets/toolbar/buttons/select_header_style_buttons.dart index b1d3dd2c3..a49151860 100644 --- a/lib/src/widgets/toolbar/buttons/select_header_style_buttons.dart +++ b/lib/src/widgets/toolbar/buttons/select_header_style_buttons.dart @@ -7,7 +7,7 @@ import '../../../l10n/extensions/localizations.dart'; import '../../../models/documents/attribute.dart'; import '../../../models/documents/style.dart'; import '../../../models/themes/quill_icon_theme.dart'; -import '../../others/controller.dart'; +import '../../quill/quill_controller.dart'; import '../base_toolbar.dart'; class QuillToolbarSelectHeaderStyleButtons extends StatefulWidget { diff --git a/lib/src/widgets/toolbar/buttons/toggle_check_list_button.dart b/lib/src/widgets/toolbar/buttons/toggle_check_list_button.dart index 866882481..7c2ba94f9 100644 --- a/lib/src/widgets/toolbar/buttons/toggle_check_list_button.dart +++ b/lib/src/widgets/toolbar/buttons/toggle_check_list_button.dart @@ -8,7 +8,7 @@ import '../../../models/documents/attribute.dart'; import '../../../models/documents/style.dart'; import '../../../models/themes/quill_icon_theme.dart'; import '../../../utils/widgets.dart'; -import '../../others/controller.dart'; +import '../../quill/quill_controller.dart'; import 'toggle_style_button.dart'; class QuillToolbarToggleCheckListButton extends StatefulWidget { diff --git a/lib/src/widgets/toolbar/buttons/toggle_style_button.dart b/lib/src/widgets/toolbar/buttons/toggle_style_button.dart index 345f339d9..fd558ebb5 100644 --- a/lib/src/widgets/toolbar/buttons/toggle_style_button.dart +++ b/lib/src/widgets/toolbar/buttons/toggle_style_button.dart @@ -6,7 +6,7 @@ import '../../../models/documents/attribute.dart'; import '../../../models/documents/style.dart'; import '../../../models/themes/quill_icon_theme.dart'; import '../../../utils/widgets.dart'; -import '../../others/controller.dart'; +import '../../quill/quill_controller.dart'; import '../base_toolbar.dart'; typedef ToggleStyleButtonBuilder = Widget Function( diff --git a/packages/quill_html_converter/CHANGELOG.md b/packages/quill_html_converter/CHANGELOG.md index b52dd226b..52b9aadd2 100644 --- a/packages/quill_html_converter/CHANGELOG.md +++ b/packages/quill_html_converter/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. ## 9.0.0-dev-7 * Fix a bug in chaning the background/font color of ol/ul list +* Better support for pasting HTML contents from external websites to the editor +* The experimental support of converting the HTML from `quill_html_converter` is now built-in in the `flutter_quill` and removed from there (Breaking change for `quill_html_converter`) * Flutter Quill Extensions: * Fix link bug in the video url * Fix patterns diff --git a/packages/quill_html_converter/lib/quill_html_converter.dart b/packages/quill_html_converter/lib/quill_html_converter.dart index 72645e997..2e8d080fa 100644 --- a/packages/quill_html_converter/lib/quill_html_converter.dart +++ b/packages/quill_html_converter/lib/quill_html_converter.dart @@ -1,11 +1,6 @@ library quill_html_converter; -import 'dart:convert' show jsonDecode; - -import 'package:delta_markdown_converter/delta_markdown_converter.dart' - as delta_markdown show markdownToDelta; import 'package:flutter_quill/flutter_quill.dart' show Delta; -import 'package:html2md/html2md.dart' as html2md; import 'package:vsc_quill_delta_to_html/vsc_quill_delta_to_html.dart' as conventer show ConverterOptions, QuillDeltaToHtmlConverter; @@ -30,31 +25,4 @@ extension DeltaHtmlExt on Delta { ).convert(); return html; } - - /// Convert the HTML Raw string to [Delta] - /// - /// It will run using the following steps: - /// - /// 1. Convert the html to markdown string using `html2md` package - /// 2. Convert the markdown string to quill delta json string - /// 3. Decode the delta json string to [Delta] - /// - /// for more [info](https://github.com/singerdmx/flutter-quill/issues/1100) - static Delta fromHtml(String html) { - final markdown = html2md - .convert( - html, - ) - .replaceAll('unsafe:', ''); - final deltaJsonString = delta_markdown.markdownToDelta(markdown); - final deltaJson = jsonDecode(deltaJsonString); - if (deltaJson is! List) { - throw ArgumentError( - 'The delta json string should be of type list when jsonDecode() it', - ); - } - return Delta.fromJson( - deltaJson, - ); - } } diff --git a/packages/quill_html_converter/pubspec.yaml b/packages/quill_html_converter/pubspec.yaml index 1264fccb1..32c279ef5 100644 --- a/packages/quill_html_converter/pubspec.yaml +++ b/packages/quill_html_converter/pubspec.yaml @@ -20,11 +20,14 @@ environment: dependencies: flutter: sdk: flutter - flutter_quill: ^7.10.2 + flutter_quill: ^9.0.0-dev-6 vsc_quill_delta_to_html: ^1.0.3 html2md: ^1.3.1 # markdown: ^7.1.1 - delta_markdown_converter: ^0.0.2 + # delta_markdown_converter: ^0.0.3-dev + markdown: ^7.1.1 + charcode: ^1.3.1 + collection: ^1.18.0 dev_dependencies: flutter_test: diff --git a/pubspec.yaml b/pubspec.yaml index e421ba8b8..5efb4f162 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,11 +52,16 @@ dependencies: equatable: ^2.0.5 meta: ^1.9.1 + # For Quill HTML + markdown: ^7.1.1 + html2md: ^1.3.1 + # Plugins url_launcher: ^6.1.14 flutter_keyboard_visibility: ^5.4.1 device_info_plus: ^9.1.0 super_clipboard: ^0.7.3 + charcode: ^1.3.1 dev_dependencies: flutter_lints: ^3.0.1