Added support for html underline and videos

pull/1955/head
CatHood0 10 months ago
parent fbf6389ee4
commit bbdc68cd26
  1. 69
      lib/src/models/documents/delta_x.dart
  2. 10
      lib/src/packages/quill_markdown/markdown_to_delta.dart
  3. 105
      lib/src/utils/html2md_utils.dart
  4. 28
      test/utils/delta_x_test.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<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;
}

@ -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<String, Delta>
final _elementToInlineAttr = <String, ElementToAttributeConvertor>{
'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<String, Delta>
final _elementToEmbed = <String, ElementToEmbeddableConvertor>{
'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<String, Delta>
_topLevelNodes.addAll(mdNodes);
for (final node in mdNodes) {
print(node.toString());
node.accept(this);
}
@ -173,6 +178,8 @@ class MarkdownToDelta extends Converter<String, Delta>
@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<String, Delta>
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());
}

@ -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(
'<und>',
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<ast.Node> 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 <u> or <ins>
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 '<und>$content<und>';
});
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<hmd.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;
}

@ -5,6 +5,12 @@ 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 htmlWithVideo =
'<iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ" title="YouTube video player"></iframe>';
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);
});
}

Loading…
Cancel
Save