diff --git a/lib/src/models/documents/delta_x.dart b/lib/src/models/documents/delta_x.dart index 754c1e06..eb6777f0 100644 --- a/lib/src/models/documents/delta_x.dart +++ b/lib/src/models/documents/delta_x.dart @@ -1,8 +1,10 @@ +import 'package:flutter/foundation.dart'; import 'package:html2md/html2md.dart' as html2md; import 'package:markdown/markdown.dart' as md; import 'package:meta/meta.dart'; import '../../../markdown_quill.dart'; import '../../../quill_delta.dart'; +import '../../utils/html2md_utils.dart'; @immutable @experimental @@ -18,7 +20,8 @@ class DeltaX { String markdownText, { Md2DeltaConfigs md2DeltaConfigs = const Md2DeltaConfigs(), }) { - final mdDocument = md.Document(encodeHtml: false); + final mdDocument = md.Document( + encodeHtml: false, inlineSyntaxes: [UnderlineSyntax(), VideoSyntax()]); final mdToDelta = MarkdownToDelta( markdownDocument: mdDocument, customElementToBlockAttribute: @@ -42,15 +45,29 @@ class DeltaX { /// used for **production applications**. /// @experimental - static Delta fromHtml(String htmlText, {Html2MdConfigs? configs}) { + static Delta fromHtml( + String htmlText, { + Html2MdConfigs? configs, + }) { + configs = Html2MdConfigs( + customRules: [ + Html2MdRules.underlineRule, + Html2MdRules.videoRule, + ...configs?.customRules ?? [] + ], + ignoreIf: configs?.ignoreIf, + rootTag: configs?.rootTag, + imageBaseUrl: configs?.imageBaseUrl, + styleOptions: configs?.styleOptions ?? {'emDelimiter': '*'}, + ); final markdownText = html2md .convert( htmlText, - rules: configs?.customRules, - ignore: configs?.ignoreIf, - rootTag: configs?.rootTag, - imageBaseUrl: configs?.imageBaseUrl, - styleOptions: configs?.styleOptions, + rules: configs.customRules, + ignore: configs.ignoreIf, + rootTag: configs.rootTag, + imageBaseUrl: configs.imageBaseUrl, + styleOptions: configs.styleOptions, ) .replaceAll( 'unsafe:', @@ -60,7 +77,6 @@ class DeltaX { } } -@immutable @experimental class Md2DeltaConfigs { const Md2DeltaConfigs({ @@ -74,40 +90,3 @@ class Md2DeltaConfigs { 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 53194f77..2fd0dc39 100644 --- a/lib/src/packages/quill_markdown/markdown_to_delta.dart +++ b/lib/src/packages/quill_markdown/markdown_to_delta.dart @@ -1,5 +1,6 @@ import 'dart:collection'; import 'dart:convert'; +import 'dart:math'; import 'package:collection/collection.dart'; import 'package:markdown/markdown.dart' as md; @@ -82,6 +83,8 @@ class MarkdownToDelta extends Converter final _elementToInlineAttr = { 'em': (_) => [Attribute.italic], + 'u': (_) => [Attribute.underline], + 'ins': (_) => [Attribute.underline], 'strong': (_) => [Attribute.bold], 'del': (_) => [Attribute.strikeThrough], 'a': (element) => [LinkAttribute(element.attributes['href'])], @@ -91,6 +94,7 @@ class MarkdownToDelta extends Converter final _elementToEmbed = { 'hr': (_) => horizontalRule, 'img': (elAttrs) => BlockEmbed.image(elAttrs['src'] ?? ''), + 'video': (elAttrs) => BlockEmbed.video(elAttrs['src'] ?? '') }; var _delta = Delta(); @@ -121,6 +125,7 @@ class MarkdownToDelta extends Converter _topLevelNodes.addAll(mdNodes); for (final node in mdNodes) { + print(node.toString()); node.accept(this); } @@ -173,6 +178,8 @@ class MarkdownToDelta extends Converter @override bool visitElementBefore(md.Element element) { + print( + 'Visit before: [tag: ${element.tag}, attributes: ${element.attributes}]'); _insertNewLineBeforeElementIfNeeded(element); final tag = element.tag; @@ -205,6 +212,9 @@ class MarkdownToDelta extends Converter void visitElementAfter(md.Element element) { final tag = element.tag; + print( + 'Visit after: [tag: ${element.tag}, attributes: ${element.attributes}]'); + if (_isEmbedElement(element)) { _delta.insert(_toEmbeddable(element).toJson()); } diff --git a/lib/src/utils/html2md_utils.dart b/lib/src/utils/html2md_utils.dart new file mode 100644 index 00000000..8f72de2f --- /dev/null +++ b/lib/src/utils/html2md_utils.dart @@ -0,0 +1,105 @@ +// ignore_for_file: implementation_imports + +import 'package:html2md/html2md.dart' as hmd; +import 'package:markdown/markdown.dart' as md; +import 'package:markdown/src/ast.dart' as ast; +import 'package:markdown/src/util.dart' as util; +import 'package:meta/meta.dart'; + +///Local syntax implementation for underline +class UnderlineSyntax extends md.DelimiterSyntax { + UnderlineSyntax() + : super( + '', + requiresDelimiterRun: true, + allowIntraWord: true, + tags: [md.DelimiterTag('u', 5)], + ); +} + +// [ character +const int $lbracket = 0x5B; +final RegExp youtubeVideoUrlValidator = RegExp( + r'^(?:https?:)?(?:\/\/)?(?:youtu\.be\/|(?:www\.|m\.)?youtube\.com\/(?:watch|v|embed)(?:\.php)?(?:\?.*v=|\/))([a-zA-Z0-9\_-]{7,15})(?:[\?&][a-zA-Z0-9\_-]+=[a-zA-Z0-9\_-]+)*(?:[&\/\#].*)?$'); + +class VideoSyntax extends md.LinkSyntax { + VideoSyntax({super.linkResolver}) + : super( + pattern: r'\[', + startCharacter: $lbracket, + ); + + @override + ast.Element createNode( + String destination, + String? title, { + required List Function() getChildren, + }) { + final element = md.Element.empty('video'); + element.attributes['src'] = util.normalizeLinkDestination( + util.escapePunctuation(destination), + ); + if (title != null && title.isNotEmpty) { + element.attributes['title'] = util.normalizeLinkTitle(title); + } + return element; + } +} + +class Html2MdRules { + const Html2MdRules._(); + + ///This rule avoid the default converter from html2md ignore underline tag for or + static 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'; + }); + static final videoRule = + hmd.Rule('video', filters: ['iframe'], replacement: (content, node) { + //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)'; + }); +} + +@experimental +class Html2MdConfigs { + const Html2MdConfigs({ + this.customRules, + this.ignoreIf, + this.rootTag, + this.imageBaseUrl, + this.styleOptions, + }); + + /// 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/test/utils/delta_x_test.dart b/test/utils/delta_x_test.dart index a6b8fcee..4ce0dd4b 100644 --- a/test/utils/delta_x_test.dart +++ b/test/utils/delta_x_test.dart @@ -5,6 +5,12 @@ 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 htmlWithVideo = + ''; final expectedDeltaEmp = Delta.fromOperations([ Operation.insert( 'This is a normal sentence, and this section has greater emp'), @@ -12,11 +18,25 @@ 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'), + ]); + 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 video and parse correctly', () { + final delta = DeltaX.fromHtml(htmlWithVideo); + print(delta); + }); }