Skip to content

Commit

Permalink
Add support for html underline and videos (#1955)
Browse files Browse the repository at this point in the history
* fixed #1953 italic detection error

fix: issue where when html2md parse <em> to "_" instead of "*" that won't be detected MarkdownToDelta converter
feat(test): added test for DeltaX
feat: added config classes to MarkdownToDelta and html2md that allow users configure their own styles

* Added support for html underline and videos

* removed print calls and fix no expect in test

* removed useless element attr for underline

* improved video url validator pattern

* Added support for <video> tag

* chore: dart fixes

* fix: removed useless params

* fix: imports issue

---------

Co-authored-by: CatHood0 <[email protected]>
  • Loading branch information
CatHood0 and CatHood0 authored Jun 28, 2024
1 parent 83e2754 commit 1f32e84
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 83 deletions.
94 changes: 15 additions & 79 deletions lib/src/models/documents/delta_x.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}

Expand All @@ -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<String, ElementToAttributeConvertor> customElementToInlineAttribute;
final Map<String, ElementToAttributeConvertor> customElementToBlockAttribute;
final Map<String, ElementToEmbeddableConvertor> 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<html2md.Rule>? customRules;

/// Elements list in [ignore] would be ingored.
final List<String>? 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<String, String>? styleOptions;
}
2 changes: 2 additions & 0 deletions lib/src/packages/quill_markdown/markdown_to_delta.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class MarkdownToDelta extends Converter<String, Delta>

final _elementToInlineAttr = <String, ElementToAttributeConvertor>{
'em': (_) => [Attribute.italic],
'u': (_) => [Attribute.underline],
'strong': (_) => [Attribute.bold],
'del': (_) => [Attribute.strikeThrough],
'a': (element) => [LinkAttribute(element.attributes['href'])],
Expand All @@ -91,6 +92,7 @@ class MarkdownToDelta extends Converter<String, Delta>
final _elementToEmbed = <String, ElementToEmbeddableConvertor>{
'hr': (_) => horizontalRule,
'img': (elAttrs) => BlockEmbed.image(elAttrs['src'] ?? ''),
'video': (elAttrs) => BlockEmbed.video(elAttrs['src'] ?? '')
};

var _delta = Delta();
Expand Down
80 changes: 80 additions & 0 deletions lib/src/utils/delta_x_utils.dart
Original file line number Diff line number Diff line change
@@ -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(
'<und>',
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<md.Node> 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 <u> or <ins>
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 '<und>$content<und>';
});
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 <source> 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 '<video>${child.outerHTML}</video>';
}
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)';
});
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ dev_dependencies:
yaml: ^3.1.2
http: ^1.2.1

path: any
flutter:
uses-material-design: true
generate: true
42 changes: 38 additions & 4 deletions test/utils/delta_x_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,52 @@ import 'package:test/test.dart';
void main() {
const htmlWithEmp =
'<p>This is a normal sentence, and this section has greater emp<em>hasis.</em></p>';

const htmlWithUnderline =
'<p>This is a normal sentence, and this section has greater <u>underline</u>';

const htmlWithIframeVideo =
'<iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ" title="YouTube video player"></iframe>';

const htmlWithVideoTag =
'''<video src="https://www.youtube.com/embed/dQw4w9WgXcQ">Your browser does not support the video tag.</video>
''';
final expectedDeltaEmp = Delta.fromOperations([
Operation.insert(
'This is a normal sentence, and this section has greater emp'),
Operation.insert('hasis.', {'italic': true}),
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);
});
}

0 comments on commit 1f32e84

Please sign in to comment.