diff --git a/lib/src/models/documents/delta_x.dart b/lib/src/models/documents/delta_x.dart index 754c1e06a..30d035946 100644 --- a/lib/src/models/documents/delta_x.dart +++ b/lib/src/models/documents/delta_x.dart @@ -3,6 +3,7 @@ import 'package:markdown/markdown.dart' as md; import 'package:meta/meta.dart'; import '../../../markdown_quill.dart'; import '../../../quill_delta.dart'; +import '../../utils/delta_x_utils.dart'; @immutable @experimental @@ -14,20 +15,12 @@ class DeltaX { /// This api is **experimental** and designed to be used **internally** and shouldn't /// used for **production applications**. @experimental - static Delta fromMarkdown( - String markdownText, { - Md2DeltaConfigs md2DeltaConfigs = const Md2DeltaConfigs(), - }) { - final mdDocument = md.Document(encodeHtml: false); - final mdToDelta = MarkdownToDelta( - markdownDocument: mdDocument, - customElementToBlockAttribute: - md2DeltaConfigs.customElementToBlockAttribute, - customElementToEmbeddable: md2DeltaConfigs.customElementToEmbeddable, - customElementToInlineAttribute: - md2DeltaConfigs.customElementToInlineAttribute, - softLineBreak: md2DeltaConfigs.softLineBreak, + static Delta fromMarkdown(String markdownText) { + final mdDocument = md.Document( + encodeHtml: false, + inlineSyntaxes: [UnderlineSyntax(), VideoSyntax()], ); + final mdToDelta = MarkdownToDelta(markdownDocument: mdDocument); return mdToDelta.convert(markdownText); } @@ -42,72 +35,15 @@ class DeltaX { /// used for **production applications**. /// @experimental - static Delta fromHtml(String htmlText, {Html2MdConfigs? configs}) { - final markdownText = html2md - .convert( - htmlText, - rules: configs?.customRules, - ignore: configs?.ignoreIf, - rootTag: configs?.rootTag, - imageBaseUrl: configs?.imageBaseUrl, - styleOptions: configs?.styleOptions, - ) - .replaceAll( - 'unsafe:', - '', - ); + static Delta fromHtml(String htmlText) { + final markdownText = html2md.convert( + htmlText, + rules: [underlineRule, videoRule], + styleOptions: {'emDelimiter': '*'}, + ).replaceAll( + 'unsafe:', + '', + ); return fromMarkdown(markdownText); } } - -@immutable -@experimental -class Md2DeltaConfigs { - const Md2DeltaConfigs({ - this.customElementToInlineAttribute = const {}, - this.customElementToBlockAttribute = const {}, - this.customElementToEmbeddable = const {}, - this.softLineBreak = false, - }); - final Map customElementToInlineAttribute; - final Map customElementToBlockAttribute; - final Map customElementToEmbeddable; - final bool softLineBreak; -} - -@immutable -@experimental -class Html2MdConfigs { - const Html2MdConfigs({ - this.customRules, - this.ignoreIf, - this.rootTag, - this.imageBaseUrl, - this.styleOptions = const {'emDelimiter': '*'}, - //emDelimiter set em to be "*" instead a "_" - }); - - /// The [rules] parameter can be used to customize element processing. - final List? customRules; - - /// Elements list in [ignore] would be ingored. - final List? ignoreIf; - - final String? rootTag; - final String? imageBaseUrl; - - /// The default and available style options: - /// - /// | Name | Default | Options | - /// | ------------- |:-------------:| -----:| - /// | headingStyle | "setext" | "setext", "atx" | - /// | hr | "* * *" | "* * *", "- - -", "_ _ _" | - /// | bulletListMarker | "*" | "*", "-", "_" | - /// | codeBlockStyle | "indented" | "indented", "fenced" | - /// | fence | "\`\`\`" | "\`\`\`", "~~~" | - /// | emDelimiter | "_" | "_", "*" | - /// | strongDelimiter | "**" | "**", "__" | - /// | linkStyle | "inlined" | "inlined", "referenced" | - /// | linkReferenceStyle | "full" | "full", "collapsed", "shortcut" | - final Map? styleOptions; -} diff --git a/lib/src/packages/quill_markdown/markdown_to_delta.dart b/lib/src/packages/quill_markdown/markdown_to_delta.dart index 53194f774..f9103447d 100644 --- a/lib/src/packages/quill_markdown/markdown_to_delta.dart +++ b/lib/src/packages/quill_markdown/markdown_to_delta.dart @@ -82,6 +82,7 @@ class MarkdownToDelta extends Converter final _elementToInlineAttr = { 'em': (_) => [Attribute.italic], + 'u': (_) => [Attribute.underline], 'strong': (_) => [Attribute.bold], 'del': (_) => [Attribute.strikeThrough], 'a': (element) => [LinkAttribute(element.attributes['href'])], @@ -91,6 +92,7 @@ class MarkdownToDelta extends Converter final _elementToEmbed = { 'hr': (_) => horizontalRule, 'img': (elAttrs) => BlockEmbed.image(elAttrs['src'] ?? ''), + 'video': (elAttrs) => BlockEmbed.video(elAttrs['src'] ?? '') }; var _delta = Delta(); diff --git a/lib/src/utils/delta_x_utils.dart b/lib/src/utils/delta_x_utils.dart new file mode 100644 index 000000000..b7767b1e6 --- /dev/null +++ b/lib/src/utils/delta_x_utils.dart @@ -0,0 +1,80 @@ +import 'package:html2md/html2md.dart' as hmd; +import 'package:markdown/markdown.dart' as md; + +// [ character +const int _$lbracket = 0x5B; +final RegExp _youtubeVideoUrlValidator = RegExp( + r'^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$'); + +///Local syntax implementation for underline +class UnderlineSyntax extends md.DelimiterSyntax { + UnderlineSyntax() + : super( + '', + requiresDelimiterRun: true, + allowIntraWord: true, + tags: [md.DelimiterTag('u', 5)], + ); +} + +class VideoSyntax extends md.LinkSyntax { + VideoSyntax({super.linkResolver}) + : super( + pattern: r'\[', + startCharacter: _$lbracket, + ); + + @override + md.Element createNode( + String destination, + String? title, { + required List Function() getChildren, + }) { + final element = md.Element.empty('video'); + element.attributes['src'] = destination; + if (title != null && title.isNotEmpty) { + element.attributes['title'] = title; + } + return element; + } +} + +///This rule avoid the default converter from html2md ignore underline tag for or +final underlineRule = + hmd.Rule('underline', filters: ['u', 'ins'], replacement: (content, node) { + //Is used a local underline implemenation since markdown just use underline with html tags + return '$content'; +}); +final videoRule = hmd.Rule('video', filters: ['iframe', 'video'], + replacement: (content, node) { + //This need to be verified by a different way of iframes, since video tag can have children + if (node.nodeName == 'video') { + //if has children then just will be taked as different part of code + if (node.childNum > 0) { + var child = node.firstChild!; + var src = child.getAttribute('src'); + if (src == null) { + child = node.childNodes().last; + src = child.getAttribute('src'); + } + if (!_youtubeVideoUrlValidator.hasMatch(src ?? '')) { + return ''; + } + return '[$content]($src)'; + } + final src = node.getAttribute('src'); + if (src == null || !_youtubeVideoUrlValidator.hasMatch(src)) { + return node.outerHTML; + } + return '[$content]($src)'; + } + //by now, we can only access to src + final src = node.getAttribute('src'); + //if the source is null or is not valid youtube url, then just return the html instead remove it + //by now is only available validation for youtube videos + if (src == null || !_youtubeVideoUrlValidator.hasMatch(src)) { + return node.outerHTML; + } + final title = node.getAttribute('title'); + return '[$title]($src)'; +}); diff --git a/pubspec.yaml b/pubspec.yaml index 399ab2120..339ec3f33 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -75,6 +75,7 @@ dev_dependencies: yaml: ^3.1.2 http: ^1.2.1 + path: any flutter: uses-material-design: true generate: true diff --git a/test/utils/delta_x_test.dart b/test/utils/delta_x_test.dart index a6b8fcee4..489a74930 100644 --- a/test/utils/delta_x_test.dart +++ b/test/utils/delta_x_test.dart @@ -5,6 +5,16 @@ import 'package:test/test.dart'; void main() { const htmlWithEmp = '

This is a normal sentence, and this section has greater emphasis.

'; + + const htmlWithUnderline = + '

This is a normal sentence, and this section has greater underline'; + + const htmlWithIframeVideo = + ''; + + const htmlWithVideoTag = + ''' +'''; final expectedDeltaEmp = Delta.fromOperations([ Operation.insert( 'This is a normal sentence, and this section has greater emp'), @@ -12,11 +22,35 @@ void main() { Operation.insert('\n'), ]); + final expectedDeltaUnderline = Delta.fromOperations([ + Operation.insert( + 'This is a normal sentence, and this section has greater '), + Operation.insert('underline', {'underline': true}), + Operation.insert('\n'), + ]); + + final expectedDeltaVideo = Delta.fromOperations([ + Operation.insert({'video': 'https://www.youtube.com/embed/dQw4w9WgXcQ'}), + Operation.insert('\n'), + ]); + test('should detect emphasis and parse correctly', () { - final delta = DeltaX.fromHtml( - htmlWithEmp, - configs: const Html2MdConfigs(), - ); + final delta = DeltaX.fromHtml(htmlWithEmp); expect(delta, expectedDeltaEmp); }); + + test('should detect underline and parse correctly', () { + final delta = DeltaX.fromHtml(htmlWithUnderline); + expect(delta, expectedDeltaUnderline); + }); + + test('should detect iframe and parse correctly', () { + final delta = DeltaX.fromHtml(htmlWithIframeVideo); + expect(delta, expectedDeltaVideo); + }); + + test('should detect video and parse correctly', () { + final delta = DeltaX.fromHtml(htmlWithVideoTag); + expect(delta, expectedDeltaVideo); + }); }